Add install-shell-completions command, show version on bare run, update README
- New install-shell-completions command: auto-detects zsh/bash, installs completion script, configures shell rc files (idempotent) - Running asc-client without a subcommand now shows version + help - README: add Homebrew tap, download binary, and quarantine instructions; clarify build time is due to asc-swift's generated API files
This commit is contained in:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,10 +1,19 @@
|
|||||||
|
# General
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Development
|
||||||
/.build
|
/.build
|
||||||
/Packages
|
/Packages
|
||||||
/.claude
|
|
||||||
*.xcodeproj
|
*.xcodeproj
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
DerivedData/
|
DerivedData/
|
||||||
.swiftpm/configuration/registries.json
|
.swiftpm/configuration/registries.json
|
||||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
.netrc
|
.netrc
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
/.claude
|
||||||
|
.claude/
|
||||||
|
**/CLAUDE.md
|
||||||
|
!/CLAUDE.md
|
||||||
|
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -7,10 +7,37 @@ A command-line tool for the [App Store Connect API](https://developer.apple.com/
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- macOS 13+
|
- macOS 13+
|
||||||
- Swift 6.0+
|
- Swift 6.0+ (only for building from source)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
### Homebrew
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew tap keremerkan/tap
|
||||||
|
brew install asc-client
|
||||||
|
```
|
||||||
|
|
||||||
|
The tap provides a pre-built binary for Apple Silicon Macs, so installation is instant.
|
||||||
|
|
||||||
|
### Download the binary
|
||||||
|
|
||||||
|
Download the latest release from [GitHub Releases](https://github.com/keremerkan/asc-client/releases):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -L https://github.com/keremerkan/asc-client/releases/latest/download/asc-client-macos-arm64.tar.gz -o asc-client.tar.gz
|
||||||
|
tar xzf asc-client.tar.gz
|
||||||
|
mv asc-client /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
Since the binary is not signed or notarized, macOS will quarantine it on first download. Remove the quarantine attribute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xattr -d com.apple.quarantine /usr/local/bin/asc-client
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Pre-built binaries are provided for Apple Silicon (arm64) only. Intel Mac users should build from source.
|
||||||
|
|
||||||
### Build from source
|
### Build from source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -21,30 +48,17 @@ strip .build/release/asc-client
|
|||||||
cp .build/release/asc-client /usr/local/bin/
|
cp .build/release/asc-client /usr/local/bin/
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** The release build takes a few minutes due to ~2500 generated API files. `strip` removes debug symbols, reducing the binary from ~175 MB to ~59 MB.
|
> **Note:** The release build takes a few minutes because the [asc-swift](https://github.com/aaronsky/asc-swift) dependency includes ~2500 generated source files covering the entire App Store Connect API surface. `strip` removes debug symbols, reducing the binary from ~175 MB to ~59 MB.
|
||||||
|
|
||||||
### Shell completions
|
### Shell completions
|
||||||
|
|
||||||
Enable tab completion for subcommands, options, and flags:
|
Set up tab completion for subcommands, options, and flags (supports zsh and bash):
|
||||||
|
|
||||||
**zsh** (default on macOS):
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.zfunc
|
asc-client install-shell-completions
|
||||||
asc-client --generate-completion-script zsh > ~/.zfunc/_asc-client
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Add this to your `~/.zshrc` if not already present:
|
This detects your shell and configures everything automatically. Restart your shell or open a new tab to activate.
|
||||||
```bash
|
|
||||||
fpath=(~/.zfunc $fpath)
|
|
||||||
autoload -Uz compinit && compinit
|
|
||||||
```
|
|
||||||
|
|
||||||
**bash**:
|
|
||||||
```bash
|
|
||||||
asc-client --generate-completion-script bash > /usr/local/etc/bash_completion.d/asc-client
|
|
||||||
```
|
|
||||||
|
|
||||||
Restart your shell or open a new tab to activate.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ struct ASCClient: AsyncParsableCommand {
|
|||||||
commandName: "asc-client",
|
commandName: "asc-client",
|
||||||
abstract: "A command-line tool for the App Store Connect API.",
|
abstract: "A command-line tool for the App Store Connect API.",
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
subcommands: [ConfigureCommand.self, AppsCommand.self, BuildsCommand.self]
|
subcommands: [ConfigureCommand.self, AppsCommand.self, BuildsCommand.self, InstallCompletionsCommand.self]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
print("asc-client \(Self.configuration.version)")
|
||||||
|
print(Self.helpMessage())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
138
Sources/asc-client/Commands/InstallCompletionsCommand.swift
Normal file
138
Sources/asc-client/Commands/InstallCompletionsCommand.swift
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct InstallCompletionsCommand: ParsableCommand {
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
commandName: "install-shell-completions",
|
||||||
|
abstract: "Install shell completions for asc-client."
|
||||||
|
)
|
||||||
|
|
||||||
|
func run() throws {
|
||||||
|
guard let shell = ProcessInfo.processInfo.environment["SHELL"] else {
|
||||||
|
throw ValidationError("Cannot detect shell. Set the SHELL environment variable.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
let fm = FileManager.default
|
||||||
|
|
||||||
|
if shell.hasSuffix("/zsh") {
|
||||||
|
try installZsh(home: home, fm: fm)
|
||||||
|
} else if shell.hasSuffix("/bash") {
|
||||||
|
try installBash(home: home, fm: fm)
|
||||||
|
} else {
|
||||||
|
throw ValidationError(
|
||||||
|
"Only zsh and bash are supported. Detected: \(shell)")
|
||||||
|
}
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Done. Restart your shell or run: source ~/.\(shell.hasSuffix("/zsh") ? "zshrc" : "bashrc")")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func installZsh(home: URL, fm: FileManager) throws {
|
||||||
|
// 1. Create ~/.zfunc if needed
|
||||||
|
let zfuncDir = home.appendingPathComponent(".zfunc")
|
||||||
|
if !fm.fileExists(atPath: zfuncDir.path) {
|
||||||
|
try fm.createDirectory(at: zfuncDir, withIntermediateDirectories: true)
|
||||||
|
print("Created \(zfuncDir.path)/")
|
||||||
|
} else {
|
||||||
|
print("\(zfuncDir.path)/ already exists.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Write completion script
|
||||||
|
let completionScript = ASCClient.completionScript(for: .zsh)
|
||||||
|
let completionFile = zfuncDir.appendingPathComponent("_asc-client")
|
||||||
|
try completionScript.write(to: completionFile, atomically: true, encoding: .utf8)
|
||||||
|
print("Installed completion script to \(completionFile.path)")
|
||||||
|
|
||||||
|
// 3. Ensure ~/.zshrc_local has fpath and compinit
|
||||||
|
let zshrcLocal = home.appendingPathComponent(".zshrc_local")
|
||||||
|
var localContents = ""
|
||||||
|
if fm.fileExists(atPath: zshrcLocal.path) {
|
||||||
|
localContents = try String(contentsOf: zshrcLocal, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
let fpathLine = "fpath=(~/.zfunc $fpath)"
|
||||||
|
let compinitLine = "autoload -Uz compinit && compinit"
|
||||||
|
var localModified = false
|
||||||
|
|
||||||
|
if !localContents.contains(fpathLine) {
|
||||||
|
let block = "\n# asc-client completions\n\(fpathLine)\n\(compinitLine)\n"
|
||||||
|
localContents += block
|
||||||
|
localModified = true
|
||||||
|
} else if !localContents.contains(compinitLine) {
|
||||||
|
localContents = localContents.replacingOccurrences(
|
||||||
|
of: fpathLine, with: "\(fpathLine)\n\(compinitLine)")
|
||||||
|
localModified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if localModified {
|
||||||
|
try localContents.write(to: zshrcLocal, atomically: true, encoding: .utf8)
|
||||||
|
print("Updated \(zshrcLocal.path)")
|
||||||
|
} else {
|
||||||
|
print("\(zshrcLocal.path) already configured.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Ensure ~/.zshrc sources ~/.zshrc_local
|
||||||
|
try ensureSourceLine(
|
||||||
|
rcFile: home.appendingPathComponent(".zshrc"),
|
||||||
|
sourceLine: "source ~/.zshrc_local",
|
||||||
|
fm: fm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func installBash(home: URL, fm: FileManager) throws {
|
||||||
|
// 1. Create ~/.bash_completions if needed
|
||||||
|
let completionsDir = home.appendingPathComponent(".bash_completions")
|
||||||
|
if !fm.fileExists(atPath: completionsDir.path) {
|
||||||
|
try fm.createDirectory(at: completionsDir, withIntermediateDirectories: true)
|
||||||
|
print("Created \(completionsDir.path)/")
|
||||||
|
} else {
|
||||||
|
print("\(completionsDir.path)/ already exists.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Write completion script
|
||||||
|
let completionScript = ASCClient.completionScript(for: .bash)
|
||||||
|
let completionFile = completionsDir.appendingPathComponent("asc-client.bash")
|
||||||
|
try completionScript.write(to: completionFile, atomically: true, encoding: .utf8)
|
||||||
|
print("Installed completion script to \(completionFile.path)")
|
||||||
|
|
||||||
|
// 3. Ensure ~/.bashrc_local sources the completion script
|
||||||
|
let bashrcLocal = home.appendingPathComponent(".bashrc_local")
|
||||||
|
var localContents = ""
|
||||||
|
if fm.fileExists(atPath: bashrcLocal.path) {
|
||||||
|
localContents = try String(contentsOf: bashrcLocal, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceLine = "source ~/.bash_completions/asc-client.bash"
|
||||||
|
if !localContents.contains(sourceLine) {
|
||||||
|
localContents += "\n# asc-client completions\n\(sourceLine)\n"
|
||||||
|
try localContents.write(to: bashrcLocal, atomically: true, encoding: .utf8)
|
||||||
|
print("Updated \(bashrcLocal.path)")
|
||||||
|
} else {
|
||||||
|
print("\(bashrcLocal.path) already configured.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Ensure ~/.bashrc sources ~/.bashrc_local
|
||||||
|
try ensureSourceLine(
|
||||||
|
rcFile: home.appendingPathComponent(".bashrc"),
|
||||||
|
sourceLine: "[ -f ~/.bashrc_local ] && source ~/.bashrc_local",
|
||||||
|
fm: fm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func ensureSourceLine(rcFile: URL, sourceLine: String, fm: FileManager) throws {
|
||||||
|
if fm.fileExists(atPath: rcFile.path) {
|
||||||
|
let contents = try String(contentsOf: rcFile, encoding: .utf8)
|
||||||
|
if !contents.contains(sourceLine) {
|
||||||
|
let newContents = sourceLine + "\n" + contents
|
||||||
|
try newContents.write(to: rcFile, atomically: true, encoding: .utf8)
|
||||||
|
print("Updated \(rcFile.path)")
|
||||||
|
} else {
|
||||||
|
print("\(rcFile.path) already configured.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try (sourceLine + "\n").write(to: rcFile, atomically: true, encoding: .utf8)
|
||||||
|
print("Created \(rcFile.path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user