App Store Connect CLI - 轻量级命令行工具与桌面工作室

0 阅读3分钟

asc: App Store Connect CLI

Latest Release GitHub Stars Go Version License

一个快速、轻量级、可脚本化的非官方 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