asc: App Store Connect CLI
一个快速、轻量级、可脚本化的非官方 App Store Connect API 命令行工具。从终端、IDE 或 CI/CD 流水线自动化 iOS、macOS、tvOS 和 visionOS 应用的发布工作流。
功能特性
- 完整的 App Store Connect API 覆盖:支持应用管理、TestFlight、构建、审核、销售报告、分析和订阅等 100+ 命令
- TTY 感知输出:交互式终端默认输出表格,管道和 CI 环境自动切换为 JSON 格式
- 智能分页:使用
--paginate标志自动获取所有分页数据 - 认证管理:支持系统钥匙串存储、配置文件和环境变量多种认证方式
- 显式标志设计:文档和示例中使用长格式标志(
--app、--output)提高可读性 - 破坏性操作保护:使用
--confirm标志确认危险操作,防止意外执行 - ASC Studio 桌面应用:macOS 优先的图形化工作空间,提供可视化的应用管理界面
安装指南
快速安装脚本
curl -fsSL https://raw.githubusercontent.com/rudrankriyam/App-Store-Connect-CLI/main/scripts/install.sh | bash
Homebrew (macOS)
brew tap rudrankriyam/app-store-connect-cli
brew install asc
手动安装
从 GitHub Releases 下载对应平台的二进制文件,并添加到 PATH 中。
系统要求
- Go 1.26+ (从源码构建)
- macOS 或 Linux
- App Store Connect API 密钥
认证配置
# 使用环境变量
export ASC_KEY_ID="YOUR_KEY_ID"
export ASC_PRIVATE_KEY_PATH="/path/to/AuthKey.p8"
# 或使用交互式登录
asc auth login
使用说明
基础命令
# 查看帮助
asc --help
asc builds --help
asc builds list --help
# 认证状态检查
asc auth status
# 列出应用
asc apps list --output json
# 查看构建列表
asc builds list --app APP_ID --paginate
# 查看 TestFlight 反馈
asc testflight feedback list --app APP_ID
# 获取应用审核状态
asc review status --app APP_ID
# 查看应用评论
asc reviews list --app APP_ID --limit 10
典型使用场景
列出所有应用并格式化输出:
asc apps list --paginate --output table
获取特定版本的元数据:
asc localizations list --version VERSION_ID --output json
管理截图:
asc screenshots list --version-localization LOCALIZATION_ID --output json
处理订阅组:
asc subscriptions groups list --app APP_ID --paginate
ASC Studio 桌面应用
ASC Studio 是项目的图形化桌面工作空间,提供可视化的应用管理界面。
运行开发环境:
cd apps/studio
wails dev
构建桌面应用:
cd apps/studio/frontend
npm install
npm run build
cd ../..
go build ./apps/studio
核心代码
CLI 主入口
package main
import (
"fmt"
"os"
"github.com/rudrankriyam/App-Store-Connect-CLI/cmd"
)
var (
version = "dev"
commit = "unknown"
date = "unknown"
)
func versionInfoString() string {
return fmt.Sprintf("%s (commit: %s, date: %s)", version, commit, date)
}
func run(args []string) int {
return cmd.Run(args, versionInfoString())
}
func main() {
os.Exit(run(os.Args[1:]))
}
根命令注册
package cmd
import (
"context"
"flag"
"fmt"
"os"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/rudrankriyam/App-Store-Connect-CLI/internal/cli/registry"
)
func RootCommand(version string) *ffcli.Command {
subcommands := registry.Subcommands(version)
root := &ffcli.Command{
Name: "asc",
ShortUsage: "asc <subcommand> [flags]",
ShortHelp: "Unofficial. asc is a fast, lightweight cli for App Store Connect.",
FlagSet: flag.NewFlagSet("asc", flag.ExitOnError),
Subcommands: subcommands,
}
root.FlagSet.BoolVar(&versionRequested, "version", false, "Print version and exit")
root.Exec = func(ctx context.Context, args []string) error {
if versionRequested {
fmt.Fprintln(os.Stdout, version)
return nil
}
if len(args) > 0 {
fmt.Fprintf(os.Stderr, "Unknown command: %s\n\n", args[0])
}
return flag.ErrHelp
}
return root
}
应用列表 API
package main
import (
"context"
"encoding/json"
"strings"
"time"
)
func (a *App) ListApps() (ListAppsResponse, error) {
ascPath, err := a.resolveASCPath()
if err != nil {
return ListAppsResponse{Error: "Could not find asc binary: " + err.Error()}, nil
}
ctx, cancel := context.WithTimeout(a.contextOrBackground(), 30*time.Second)
defer cancel()
out, err := a.runASCCombinedOutput(ctx, ascPath, "apps", "list", "--paginate", "--output", "json")
if err != nil {
return ListAppsResponse{Error: strings.TrimSpace(string(out))}, nil
}
rawApps, err := parseAppsListOutput(out)
if err != nil {
return ListAppsResponse{Error: "Failed to parse apps list: " + err.Error()}, nil
}
apps := make([]AppInfo, len(rawApps))
for i, raw := range rawApps {
apps[i] = AppInfo{
ID: raw.ID,
Name: raw.Attributes.Name,
BundleID: raw.Attributes.BundleID,
SKU: raw.Attributes.SKU,
}
}
return ListAppsResponse{Apps: apps}, nil
}
退出码处理
package cmd
import (
"errors"
"net/http"
"github.com/rudrankriyam/App-Store-Connect-CLI/internal/asc"
)
const (
ExitSuccess = 0
ExitError = 1
ExitUsage = 2
ExitAuth = 3
ExitNotFound = 4
ExitConflict = 5
)
func ExitCodeFromError(err error) int {
if err == nil {
return ExitSuccess
}
if errors.Is(err, flag.ErrHelp) {
return ExitUsage
}
if errors.Is(err, shared.ErrMissingAuth) ||
errors.Is(err, asc.ErrUnauthorized) {
return ExitAuth
}
if errors.Is(err, asc.ErrNotFound) {
return ExitNotFound
}
return ExitError
}
ASC Studio 桌面应用主程序
package main
import (
"embed"
"log"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
wmac "github.com/wailsapp/wails/v2/pkg/options/mac"
)
//go:embed all:frontend/dist
var studioAssets embed.FS
func main() {
app, err := NewApp()
if err != nil {
log.Fatalf("create ASC Studio app: %v", err)
}
err = wails.Run(&options.App{
Title: "ASC Studio",
Width: 1480,
Height: 980,
MinWidth: 1180,
MinHeight: 760,
BackgroundColour: options.NewRGBA(0, 0, 0, 0),
AssetServer: &assetserver.Options{
Assets: studioAssets,
},
OnStartup: app.startup,
OnShutdown: app.shutdown,
Bind: []interface{}{
app,
},
Mac: &wmac.Options{
WindowIsTranslucent: true,
WebviewIsTransparent: true,
TitleBar: &wmac.TitleBar{
TitlebarAppearsTransparent: true,
HideTitle: true,
FullSizeContent: true,
UseToolbar: true,
HideToolbarSeparator: true,
},
},
})
if err != nil {
log.Fatalf("run ASC Studio: %v", err)
}
}
TGiNfWQzQkhWj1CdHlp8M4lvZ97v+nAITbGjlcP0sUUxZisnCv9F/782QDFb4VXm