diff --git a/CHANGELOG.md b/CHANGELOG.md index a257cae..6f67f2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ User-visible changes are listed here. Published binaries and versions are availa ## Unreleased +### Added + +- `agc skills add` installs the bundled, self-contained agc-cli skill for Copilot, Claude Code, or Codex. + ## v0.2.0 — 2026-10-07 ### Added diff --git a/README.en.md b/README.en.md index 3128c31..be1dc3c 100644 --- a/README.en.md +++ b/README.en.md @@ -46,7 +46,7 @@ Endpoint definitions include `affordances` command templates and `_links` REST r } ``` -JSON is the default; add `--output table` or `--output markdown` for people. `agc web-server` exposes a local REST API. Templates require parameters and currently do not check remote business state to determine submission readiness. More in the [agent and REST guide](docs/CLI_USAGE.en.md#json-and-agents). +JSON is the default; add `--output table` or `--output markdown` for people. Install the bundled skill with `agc skills add --agent copilot` (or `--agent all` for Copilot, Claude Code, and Codex). `agc web-server` exposes a local REST API. Templates require parameters and currently do not check remote business state to determine submission readiness. More in the [agent and REST guide](docs/CLI_USAGE.en.md#json-and-agents). ## What It Covers diff --git a/README.md b/README.md index 9066203..87cb68a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ agc publishing app-info-query --invoke --query appId=YOUR_APP_ID --query lang=zh } ``` -默认 JSON;人工浏览可加 `--output table` 或 `--output markdown`。`agc web-server` 提供本地 REST API。命令模板需要补齐参数,当前不会根据远程业务状态判断能否提交。详见 [Agent 与 REST 使用说明](docs/CLI_USAGE.md#json-与-agent-支持)。 +默认 JSON;人工浏览可加 `--output table` 或 `--output markdown`。运行 `agc skills add --agent copilot` 安装内置 skill,也可用 `--agent all` 安装到 Copilot、Claude Code 和 Codex。`agc web-server` 提供本地 REST API。命令模板需要补齐参数,当前不会根据远程业务状态判断能否提交。详见 [Agent 与 REST 使用说明](docs/CLI_USAGE.md#json-与-agent-支持)。 ## 功能覆盖 diff --git a/cmd/agc/command/assets/agc-cli/SKILL.md b/cmd/agc/command/assets/agc-cli/SKILL.md new file mode 100644 index 0000000..9480183 --- /dev/null +++ b/cmd/agc/command/assets/agc-cli/SKILL.md @@ -0,0 +1,139 @@ +--- +name: agc-cli +description: Use the agc CLI to manage AppGallery Connect workflows, discover API endpoints, configure profiles, and safely preview or invoke requests. +--- + +# agc-cli agent skill + +This skill is the complete operational guide for using `agc-cli`; it does not depend on other repository files being present. The executable is named `agc`. It is for Huawei AppGallery Connect (AGC), not Google Cloud or Huawei Cloud. + +## Safety rules + +- Never ask a user to paste credentials, private keys, or access tokens into chat. Do not print or expose secrets in command output, logs, files, issues, or reports. Treat raw authentication responses as sensitive: API Client output may contain `clientKey`. +- Do not perform a remote check or send an API request unless the user authorized that specific remote action. Requests are dry-run by default; preserve that default until the user explicitly approves execution. +- Before any update, submission, reply, or other mutation, inspect the endpoint, required parameters, request body, target app/project, and dry-run preview. Do not infer business intent or fill missing data with guesses. +- Do not repeat a mutation automatically after a timeout or ambiguous response. First determine whether the remote action may have succeeded. +- Endpoint registration and a successful HTTP response do not prove business prerequisites, approval, or publication. + +## Check the CLI and discover commands + +The CLI can be installed from a release archive or package manager, or with Go 1.22+: + +```bash +go install github.com/Createitv/agc-cli/cmd/agc@latest +agc version +``` + +Discover commands and endpoint contracts without credentials or network access: + +```bash +agc --help +agc capabilities --output table +agc endpoints --pretty +agc publishing endpoints --output table +agc publishing app-info-query --pretty +agc publishing app-info-query --help +agc openapi --pretty +``` + +The API families include `publishing`, `upload`, `provisioning`, `domains`, `testing`, `reports`, `projects`, `comments`, `pms`, `gameplay`, `game-items`, `resources`, and `cicd`. Discover the installed CLI's current registry rather than relying on a fixed endpoint count. + +An endpoint definition shows its method, path, parameter contract, official Huawei `sourceUrl`, REST links, and command affordances. Read the definition before constructing a request. Use its official reference for permissions, field semantics, and business prerequisites. Affordance strings are templates, not verified plans. + +## Global flags and output + +Global flags can be placed before the command: + +| Flag | Purpose | +| --- | --- | +| `--output json\|table\|markdown` | Choose output format; JSON is the default and preferred for agents/scripts. | +| `--pretty` | Pretty-print JSON. | +| `--timeout 60s` | Set request timeout; default is 60 seconds. | +| `--profile NAME` | Select a saved credential profile. | +| `--project DIR` | Select project context directory; default is `.`. | + +Parse JSON structurally and check command exit status. Do not assume every response has the same `data` shape. Preserve warnings and errors, and avoid echoing returned credentials, tokens, user data, or other sensitive values. Use table/markdown output only when it helps a person inspect listings. + +## Credentials and project context + +Credentials are stored locally, by default at `~/.agc/credentials.json`. `AGC_CREDENTIALS_PATH` overrides that path; the `auth` commands also accept `--credentials-path`. Profiles are selected in this order: explicit `--profile`, the project profile, globally active profile, or the only profile. + +For credential setup, have the user configure the secret locally or make it available through their approved secret manager. Do not request the secret value in chat. Examples (the values must already exist in the user's protected environment): + +```bash +agc auth login --service-account-file PATH --name production +agc auth login --client-id "$AGC_CLIENT_ID" --client-key "$AGC_CLIENT_KEY" --name staging +``` + +The CLI does not automatically read `AGC_CLIENT_ID` or `AGC_CLIENT_KEY`; shell expansion in the example supplies the values. Service Account JSON needs `key_id`, `private_key`, and `sub_account`; login stores its file path, so that local file must remain available. Avoid putting literal secrets in shell history or command transcripts. + +Useful local credential commands: + +```bash +agc auth list +agc --profile staging auth check +agc auth token +``` + +`auth check` inspects local credential configuration; it does not prove Huawei accepts it. `auth check --remote` contacts Huawei and may perform an app/project read to check permissions. `auth token` creates a real token. Use either only with explicit authorization, and never display or log their raw output. + +Bind a project to an app and optionally a default profile: + +```bash +agc init --app-id APP_ID --default-profile production +agc init --app-id APP_ID --project-id PROJECT_ID --package-name PACKAGE_NAME +``` + +This writes `.agc/project.json` under the selected `--project` directory. It selects project context/profile; it does not automatically supply every endpoint parameter. Pass `appId`, `projectId`, package name, and other required values explicitly when the endpoint contract requires them. `AGC_ACCESS_TOKEN` can provide an access token for endpoint requests; never print or expose it. + +## Inspect, preview, and invoke an endpoint + +Without `--invoke`, an endpoint command displays its definition. With `--invoke`, the CLI constructs a dry-run request by default. Only `--dry-run=false` sends it. + +| Flag | Usage | +| --- | --- | +| `--param key=value` | Repeat for path parameters. | +| `--query key=value` | Repeat for query parameters. | +| `--header key=value` | Repeat for required HTTP headers, such as `client_id`. | +| `--field key=value` | Repeat for JSON fields; values are encoded as strings. | +| `--body FILE` | Read a JSON object request body; takes precedence over `--field`. Use for arrays, objects, numbers, or booleans. | +| `--token TOKEN` | Explicit request token; handle as a secret. | +| `--out FILE` | Save raw response bytes to a file. | +| `--base-url URL` | Override the API base URL, primarily for an explicitly selected compatible service/test environment. | +| `--dry-run=false` | Send the request; use only after explicit authorization and review. | + +Example: inspect, then preview a read-only app information query: + +```bash +agc publishing app-info-query --pretty +agc publishing app-info-query --invoke \ + --query appId=APP_ID --query lang=en-US --pretty +``` + +The preview identifies the method, URL, and `dryRun: true`; it does not contact Huawei and does not validate all business fields. After reviewing the preview and confirming authorization, append `--dry-run=false` to send. Supply `--header client_id=CLIENT_ID` when the endpoint contract requires it; this header is not automatically populated. + +For JSON request bodies, prepare the file locally, review its contents, and keep secrets out of it: + +```bash +agc publishing app-info-update --invoke --body request.json --pretty +``` + +If the endpoint requires a parameter, the CLI reports a missing `--param`, `--query`, `--header`, or `--field` value. Consult the endpoint's displayed parameter contract instead of guessing. + +## Common workflows and limitations + +- **App release:** Discover and invoke the separate query, upload handoff, metadata, and submission endpoints. The CLI does not orchestrate a full release. Binary/multipart file transfer is not a complete built-in workflow; use a separate tool that implements Huawei's documented upload protocol. `--body` does not upload binaries. +- **Reports:** Some report endpoints return raw responses or download URLs. `--out` preserves response bytes; it does not convert them into CSV/Excel or necessarily fetch a second-stage download URL. +- **Local REST:** `agc web-server` starts a local REST API (default listen address `127.0.0.1:8421`). REST invocation defaults to dry-run and does not load saved CLI credential profiles. Real REST invocation requires an explicit token and required headers in the request. Non-loopback listening requires `AGC_SERVER_TOKEN`; do not expose the server or token. +- **OpenAPI:** `agc openapi --pretty` exports the local REST and endpoint invocation contract. +- **Skills:** `agc skills add --agent copilot|claude|codex|all` installs this skill into the selected project's `.github/skills`, `.claude/skills`, or `.agents/skills` directory. Existing files are not overwritten unless `--force` is supplied. + +## Troubleshooting + +- `agc: command not found`: install the CLI, ensure its binary directory is on `PATH`, and run `agc version`. +- `credential profile ... not found`: inspect local profile names and select the intended profile or project. +- `auth check` passes but an API call fails: local check is not remote verification; review credentials locally, account permissions, app/project ownership, required headers, and the endpoint contract. +- A command shows only an endpoint definition or preview: actual invocation requires `--invoke`; sending also requires `--dry-run=false`. +- `appId` is still required after `agc init`: project setup does not necessarily populate endpoint parameters; pass the required value explicitly. + +When reporting an error, include CLI version, OS/architecture, a redacted command, and the error. Never include credentials, tokens, private keys, or unredacted auth output. diff --git a/cmd/agc/command/root.go b/cmd/agc/command/root.go index 316f878..f5523d6 100644 --- a/cmd/agc/command/root.go +++ b/cmd/agc/command/root.go @@ -2,11 +2,13 @@ package command import ( "context" + _ "embed" "encoding/json" "fmt" "net" "net/http" "os" + "path/filepath" "strings" "time" @@ -53,6 +55,7 @@ func NewRootCommand() *cobra.Command { cmd.AddCommand(initCommand(opts)) cmd.AddCommand(webServerCommand()) cmd.AddCommand(docsCommand(opts)) + cmd.AddCommand(skillsCommand(opts)) for _, capability := range domain.DecoratedCapabilities() { cmd.AddCommand(moduleCommand(opts, capability)) } @@ -356,6 +359,91 @@ func webServerCommand() *cobra.Command { return cmd } +//go:embed assets/agc-cli/SKILL.md +var agcCLISkill []byte + +func skillsCommand(opts *options) *cobra.Command { + cmd := &cobra.Command{ + Use: "skills", + Short: "Install agent skills", + } + var agent string + var force bool + add := &cobra.Command{ + Use: "add", + Short: "Add the agc-cli skill to an agent", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + agentDirs := map[string]string{ + "copilot": ".github/skills", + "claude": ".claude/skills", + "codex": ".agents/skills", + } + agents := []string{agent} + if agent == "all" { + agents = []string{"copilot", "claude", "codex"} + } + for _, name := range agents { + if _, ok := agentDirs[name]; !ok { + return fmt.Errorf("unsupported agent %q; choose copilot, claude, codex, or all", name) + } + } + + type skillInstall struct { + Agent string `json:"agent"` + Path string `json:"path"` + } + paths := make([]string, 0, len(agents)) + for _, name := range agents { + path := filepath.Join(agentDirs[name], "agc-cli", "SKILL.md") + info, err := os.Lstat(filepath.Join(opts.project, path)) + if err == nil && !force { + return fmt.Errorf("skill already exists at %s; use --force to replace it", path) + } else if err == nil && info.IsDir() { + return fmt.Errorf("skill destination %s is a directory", path) + } else if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("check skill destination %s: %w", path, err) + } + paths = append(paths, path) + } + + installed := make([]skillInstall, 0, len(agents)) + for i, path := range paths { + destination := filepath.Join(opts.project, path) + if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil { + return fmt.Errorf("create skill directory %s: %w", filepath.Dir(destination), err) + } + if force { + if err := os.Remove(destination); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("replace skill at %s: %w", path, err) + } + } + file, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + if err != nil { + return fmt.Errorf("install skill at %s: %w", path, err) + } + _, writeErr := file.Write(agcCLISkill) + closeErr := file.Close() + if writeErr != nil { + _ = os.Remove(destination) + return fmt.Errorf("write skill at %s: %w", path, writeErr) + } + if closeErr != nil { + _ = os.Remove(destination) + return fmt.Errorf("close skill at %s: %w", path, closeErr) + } + installed = append(installed, skillInstall{Agent: agents[i], Path: filepath.ToSlash(path)}) + } + return output.Write(cmd.OutOrStdout(), domain.Envelope[[]skillInstall]{Data: installed}, output.Format(opts.output), opts.pretty) + }, + } + add.Flags().StringVar(&agent, "agent", "", "Target agent: copilot, claude, codex, or all") + add.Flags().BoolVar(&force, "force", false, "Replace an existing agc-cli skill") + _ = add.MarkFlagRequired("agent") + cmd.AddCommand(add) + return cmd +} + func docsCommand(opts *options) *cobra.Command { return &cobra.Command{ Use: "docs [module]", diff --git a/cmd/agc/command/root_test.go b/cmd/agc/command/root_test.go index 87c3c03..6890594 100644 --- a/cmd/agc/command/root_test.go +++ b/cmd/agc/command/root_test.go @@ -117,6 +117,97 @@ func TestVersionCommand(t *testing.T) { } } +func TestSkillsAddInstallsForAllAgents(t *testing.T) { + projectDir := t.TempDir() + out, err := execute("--project", projectDir, "skills", "add", "--agent", "all") + if err != nil { + t.Fatal(err) + } + var body domain.Envelope[[]struct { + Agent string `json:"agent"` + Path string `json:"path"` + }] + if err := json.Unmarshal([]byte(out), &body); err != nil { + t.Fatal(err) + } + want := map[string]string{ + "copilot": ".github/skills/agc-cli/SKILL.md", + "claude": ".claude/skills/agc-cli/SKILL.md", + "codex": ".agents/skills/agc-cli/SKILL.md", + } + if len(body.Data) != len(want) { + t.Fatalf("installed %d skills, want %d", len(body.Data), len(want)) + } + for _, skill := range body.Data { + if want[skill.Agent] != skill.Path { + t.Errorf("installed agent/path = %q/%q", skill.Agent, skill.Path) + } + content, err := os.ReadFile(filepath.Join(projectDir, filepath.FromSlash(skill.Path))) + if err != nil { + t.Fatal(err) + } + if string(content) != string(agcCLISkill) { + t.Errorf("%s skill differs from embedded skill", skill.Agent) + } + } +} + +func TestSkillsAddDoesNotReplaceUnlessForced(t *testing.T) { + projectDir := t.TempDir() + destination := filepath.Join(projectDir, ".claude", "skills", "agc-cli", "SKILL.md") + if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil { + t.Fatal(err) + } + const existing = "existing skill" + if err := os.WriteFile(destination, []byte(existing), 0644); err != nil { + t.Fatal(err) + } + if _, err := execute("--project", projectDir, "skills", "add", "--agent", "claude"); err == nil { + t.Fatal("expected existing skill error") + } + content, err := os.ReadFile(destination) + if err != nil { + t.Fatal(err) + } + if string(content) != existing { + t.Fatalf("existing skill changed to %q", content) + } + if _, err := execute("--project", projectDir, "skills", "add", "--agent", "claude", "--force"); err != nil { + t.Fatal(err) + } + content, err = os.ReadFile(destination) + if err != nil { + t.Fatal(err) + } + if string(content) != string(agcCLISkill) { + t.Fatal("forced skill does not match embedded content") + } +} + +func TestSkillsAddRejectsUnsupportedAgent(t *testing.T) { + if _, err := execute("skills", "add", "--agent", "unknown"); err == nil { + t.Fatal("expected unsupported agent error") + } +} + +func TestSkillsAddDoesNotReplaceDirectory(t *testing.T) { + projectDir := t.TempDir() + destination := filepath.Join(projectDir, ".agents", "skills", "agc-cli", "SKILL.md") + if err := os.MkdirAll(destination, 0755); err != nil { + t.Fatal(err) + } + if _, err := execute("--project", projectDir, "skills", "add", "--agent", "codex", "--force"); err == nil { + t.Fatal("expected directory destination error") + } + info, err := os.Stat(destination) + if err != nil { + t.Fatal(err) + } + if !info.IsDir() { + t.Fatal("destination directory was replaced") + } +} + func TestEveryRegisteredCommandHasHelp(t *testing.T) { root := NewRootCommand() for _, cmd := range root.Commands() { diff --git a/docs/CLI_USAGE.en.md b/docs/CLI_USAGE.en.md index 75ac173..e365998 100644 --- a/docs/CLI_USAGE.en.md +++ b/docs/CLI_USAGE.en.md @@ -233,6 +233,16 @@ agc publishing app-info-query --pretty Endpoint definitions include `_links` to local REST routes and `affordances` with next-command templates. Agents can inspect the definition and `sourceUrl`, fill in parameters, and preview requests. Templates do not confirm that business prerequisites are satisfied. +Install the bundled `agc-cli` skill into an agent's skill directory in the selected project: + +```bash +agc --project . skills add --agent copilot +# Or install for all supported agents: copilot, claude, and codex +agc --project . skills add --agent all +``` + +The command creates `.github/skills/agc-cli`, `.claude/skills/agc-cli`, or `.agents/skills/agc-cli` as appropriate. Existing skills are left unchanged unless `--force` is specified. Run `agc skills add --help` for details. + ## Web, REST, and OpenAPI Browse static endpoint references at [agccli.app](https://agccli.app/). For local REST data, start the API and Web development server from the source checkout: diff --git a/docs/CLI_USAGE.md b/docs/CLI_USAGE.md index a5b945e..e25fd08 100644 --- a/docs/CLI_USAGE.md +++ b/docs/CLI_USAGE.md @@ -233,6 +233,16 @@ agc publishing app-info-query --pretty 接口定义中的 `_links` 提供本地 REST 路由,`affordances` 提供后续命令模板。Agent 可先读取定义和 `sourceUrl`,补齐参数后预览请求。模板并不表示业务前置条件已经满足。 +可将内置的 `agc-cli` skill 安装到项目中的 Agent skill 目录: + +```bash +agc --project . skills add --agent copilot +# 或安装到所有支持的 Agent:copilot、claude 和 codex +agc --project . skills add --agent all +``` + +命令会按目标创建 `.github/skills/agc-cli`、`.claude/skills/agc-cli` 或 `.agents/skills/agc-cli`。默认不覆盖已有 skill;使用 `--force` 才会替换。运行 `agc skills add --help` 查看详情。 + ## Web、REST 与 OpenAPI [agccli.app](https://agccli.app/) 可浏览静态接口参考。要读取本地 REST 数据,在源码仓库启动服务和 Web 开发服务器: