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:
Kerem Erkan
2026-02-13 11:12:36 +01:00
parent a2a8192dd1
commit 3a8c794492
4 changed files with 186 additions and 20 deletions

11
.gitignore vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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())
}
} }

View 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)")
}
}
}