From e1f423880ec6d22f4feb550c17542a2427ea8635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=B9=E5=84=92=E5=A3=AB=E5=AD=90?= <84996057@qq.com> Date: Wed, 16 Sep 2026 21:06:42 +0800 Subject: [PATCH] feat(cli): add minimal one-cli skill and installer --- .gitignore | 1 + ARCHITECTURE.md | 3 +- CHANGELOG.md | 26 +-- README.md | 11 +- Taskfile.yml | 5 +- .../content/blog/en/agent-skill-context.md | 4 +- .../content/blog/zh/agent-skill-context.md | 4 +- apps/docs/content/docs/en/ai-native.md | 4 +- apps/docs/content/docs/en/cli-overview.md | 4 + apps/docs/content/docs/en/create.md | 4 + apps/docs/content/docs/en/error-codes.md | 6 + apps/docs/content/docs/en/installation.md | 4 + apps/docs/content/docs/en/meta.json | 1 + apps/docs/content/docs/en/skills.md | 49 +++++ apps/docs/content/docs/zh/ai-native.md | 4 +- apps/docs/content/docs/zh/cli-overview.md | 4 + apps/docs/content/docs/zh/create.md | 4 + apps/docs/content/docs/zh/error-codes.md | 12 ++ apps/docs/content/docs/zh/installation.md | 4 + apps/docs/content/docs/zh/meta.json | 1 + apps/docs/content/docs/zh/skills.md | 49 +++++ packages/cli/internal/bootstrap/cli/root.go | 2 + .../cli/internal/bootstrap/cli/root_test.go | 4 +- .../modules/creation/workspace_content.go | 2 + .../modules/creation/workspace_files.go | 3 + .../modules/creation/workspace_files_test.go | 2 +- .../cli/internal/modules/skills/agents.go | 177 ++++++++++++++++ .../cli/internal/modules/skills/install.go | 93 +++++++++ .../internal/modules/skills/skills_test.go | 197 ++++++++++++++++++ .../cli/internal/platform/errors/codes.go | 7 + .../internal/platform/errors/codes_test.go | 1 + .../internal/platform/i18n/locales/en-US.json | 8 + .../internal/platform/i18n/locales/zh-CN.json | 8 + .../cli/internal/resources/bundled/bundled.go | 12 +- .../resources/bundled/bundled_test.go | 10 + .../internal/transport/cobra/skills/cmd.go | 94 +++++++++ .../cli/testdata/reference/help/skills.txt | 12 ++ .../reference/help/skills_install.txt | 26 +++ packages/cli/tests/e2e/e2e_helpers_test.go | 3 + packages/cli/tests/e2e/skills_test.go | 94 +++++++++ .../cli/tests/e2e/snapshot_e2e_add_test.go | 4 +- .../e2e/snapshot_e2e_create_preset_test.go | 2 +- .../cli/tests/e2e/snapshot_e2e_create_test.go | 23 +- .../cli/tests/e2e/snapshot_e2e_ux_test.go | 25 +-- packages/cli/tools/gen-error-codes/main.go | 5 + packages/cli/tools/sync-resources/main.go | 10 +- .../cli/tools/sync-resources/main_test.go | 7 +- .../cli/tools/verify-cli-references/main.go | 1 + skills/one-cli/SKILL.md | 25 +++ 49 files changed, 994 insertions(+), 67 deletions(-) create mode 100644 apps/docs/content/docs/en/skills.md create mode 100644 apps/docs/content/docs/zh/skills.md create mode 100644 packages/cli/internal/modules/skills/agents.go create mode 100644 packages/cli/internal/modules/skills/install.go create mode 100644 packages/cli/internal/modules/skills/skills_test.go create mode 100644 packages/cli/internal/transport/cobra/skills/cmd.go create mode 100644 packages/cli/testdata/reference/help/skills.txt create mode 100644 packages/cli/testdata/reference/help/skills_install.txt create mode 100644 packages/cli/tests/e2e/skills_test.go create mode 100644 skills/one-cli/SKILL.md diff --git a/.gitignore b/.gitignore index f0815b9..d67980f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ go-dist/ # fail until one of those tasks has run at least once. packages/cli/internal/resources/bundled/registry.json packages/cli/internal/resources/bundled/_templates/ +packages/cli/internal/resources/bundled/_skills/ packages/cli/internal/resources/bundled/_web/ # go-task diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6f087e1..c5a03e3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -119,10 +119,11 @@ packages/cli/internal/ development/ local development process orchestration environment/ dotenv/Infisical workflows and workspace setup preset/ pure preset encoding, parsing, and resolution + skills/ bundled one-cli skill installation into agent user directories platform/ errors/ helpui/ i18n/ output/ preferences/ process/ prompt/ updatecheck/ resources/ - bundled/ embedded templates, registry, and Dashboard + bundled/ embedded templates, registry, one-cli skill, and Dashboard transport/ cobra/ one directory per command family http/ local Dashboard API diff --git a/CHANGELOG.md b/CHANGELOG.md index c8dd164..1348eaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,24 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Removed (BREAKING — agent skills 管理与分发) - -One CLI 不再内置或安装 agent skills。删除 `one skills` / `one skills install`、 -agent 检测与安装路径注册表、共享 store / symlink 安装器,以及随二进制分发的 -`one-cli` / `one-migrate` 内容。公开 Go 包 `packages/cli/pkg/agentskills` 同时移除。 - -`one create` 的普通与 preset JSON 输出均移除 `skills` 字段,成功提示不再引导安装。 -依赖旧命令、字段或 Go 包的调用方需要移除对应逻辑;旧命令返回 `UNKNOWN_COMMAND`。 -工作区 agent 说明也不再自动生成,详见下方说明。 -升级不会删除用户机器上以前安装的 skills 或修改其他 AI 工具的配置。 - -### Removed (BREAKING — 自动生成 agent 说明) - -`one create`、preset 创建和 `one add` 不再生成或刷新 `AGENTS.md`、`CLAUDE.md` -及 `.one/` 目录;模板渲染也会跳过 `.one`,已有文件保持原样。移除内部说明生成器和 `one add` JSON 的 -`ai_guides` 字段。团队可自行维护说明文件,One CLI 继续维护项目和 manifest。 -模板目录中遗留的 10 份 `CLAUDE.md` 已移除;打包流程排除 agent 说明、skill 入口及 -`.one` / `.agents` 目录,并校验最终嵌入资源不包含这些内容。 +### Changed — 精简 One CLI skill 与显式安装 + +- `one skills install` 安装单个内置 `one-cli` skill:遵循 One Workspace Convention,并通过当前 CLI 的 help 查询命令。日常 CLI 升级无需同步更新 skill。 +- 支持交互选择、`--agent` / `-a` 和 `--yes`;非交互安装到检测到的 Agent,无目标时提示显式选择。支持 JSON / YAML / text 输出及离线安装。 +- skill 直接复制到 Agent 的用户级目录。重复安装替换 `one-cli` 目录;兼容旧符号链接,不修改旧共享 store 或其他 skills。 +- 普通与 preset 创建均生成简短的根 `AGENTS.md`,要求使用 skill,缺失时运行 `one skills install`。创建项目不自动安装 skill,`one add` 保留已有指引。 +- 旧的 `one-migrate`、详细 playbook、共享 store 安装器和公开 Go 包 `packages/cli/pkg/agentskills` 保持移除。旧机器上的其他安装不自动清理。 +- `create` / `add` 输出不恢复旧 `skills` / `ai_guides` 字段;模板仍跳过独立的 `CLAUDE.md`、`AGENTS.md` 和 `.one` 元数据。 ### Removed (BREAKING — `one serve` session token) diff --git a/README.md b/README.md index 0a70075..fdb7c19 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,15 @@ Full command docs live at [1cli.dev](https://1cli.dev). ## Work With AI Assistants -One CLI is designed to make AI-assisted project work less fragile. +Install the bundled `one-cli` skill for your coding agent: + +```bash +one skills install +``` + +Use `--agent ` to choose an agent directly, or `--yes` to install into every detected agent. Installation works offline and only writes the selected agents' user skills directories. + +New workspaces include an `AGENTS.md` asking agents to use this skill and run `one skills install` if it is missing. The [skill](./skills/one-cli/SKILL.md) stays small: follow [One Workspace Convention](https://github.com/1cli-team/one-workspace-convention), then consult `one --help` and the relevant command help. Ordinary CLI upgrades do not require reinstalling it. You can ask an assistant for project-level changes in natural language, for example: @@ -140,6 +148,7 @@ If you want to work on One CLI itself, the repository is organized like this: |---|---| | `packages/cli` | The One CLI app | | `packages/templates` | Starters used by `one add` | +| `skills/one-cli` | Minimal workspace guidance installed by `one skills install` | | `apps/docs` | Documentation website | | `apps/dashboard` | Local Workspace, Project, and Profile Dashboard opened by `one serve` | | `assets` | Brand assets, including the logo | diff --git a/Taskfile.yml b/Taskfile.yml index 0489b94..47c8635 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -89,6 +89,7 @@ tasks: - 'packages/cli/internal/**/*.go' - 'packages/cli/tools/verify-cli-references/**/*.go' - 'README.md' + - 'skills/**/*.md' - 'CONTRIBUTING.md' - 'apps/docs/content/docs/**/*.md' - 'apps/docs/content/docs/**/*.mdx' @@ -379,14 +380,14 @@ tasks: sync-bundled: deps: [sync-mise] - desc: Regenerate registry/_templates under packages/cli/internal/resources/bundled (go:embed sources) + desc: Regenerate registry/_templates/_skills under packages/cli/internal/resources/bundled (go:embed sources) run: once summary: | Go's embed cannot traverse `..` or follow symlinks, so we keep physical copies of canonical assets under packages/cli/internal/resources/bundled. The whole bundled tree is gitignored and regenerated on demand. - This task handles the cheap copies: registry.json, _templates/. + This task handles the cheap copies: registry.json, _templates/, _skills/. It runs implicitly as a dep of `vet` / `test` / `build`. The dashboard build (sync-web) is separate but identically wired — both have to run before any Go command will compile the bundled package. diff --git a/apps/docs/content/blog/en/agent-skill-context.md b/apps/docs/content/blog/en/agent-skill-context.md index 06eae4e..c46f27a 100644 --- a/apps/docs/content/blog/en/agent-skill-context.md +++ b/apps/docs/content/blog/en/agent-skill-context.md @@ -1,12 +1,12 @@ --- title: "Historical Design: One CLI Agent Skills" -description: "Historical notes on the agent skills design, retired when One CLI removed skill distribution and generated instructions." +description: "Historical notes on the earlier detailed agent skill design, now replaced by a minimal convention-and-help skill." date: "2026-05-12" author: "One CLI Team" tags: ["skill", "codex", "dependencies"] --- -> Historical design note: One CLI no longer bundles or installs agent skills. Use [workspace AI guidance](/en/docs/ai-native/) and the [command reference](/en/docs/cli-overview/) for the current workflow. +> Historical design note: the detailed playbooks described below have been replaced by a minimal `one-cli` skill. See [Skills](/en/docs/skills/) for the current `one skills install` workflow. ## A skill is not marketing copy diff --git a/apps/docs/content/blog/zh/agent-skill-context.md b/apps/docs/content/blog/zh/agent-skill-context.md index edef401..610a3af 100644 --- a/apps/docs/content/blog/zh/agent-skill-context.md +++ b/apps/docs/content/blog/zh/agent-skill-context.md @@ -1,12 +1,12 @@ --- title: "历史设计:One CLI Agent Skills" -description: "记录已退役的 agent skills 设计;One CLI 已移除 skill 分发与项目说明自动生成。" +description: "记录早期详细的 agent skills 设计;当前采用仅包含结构规范和 help 查询的精简 skill。" date: "2026-05-12" author: "One CLI Team" tags: ["skill", "codex", "dependencies"] --- -> 历史设计记录:One CLI 已移除 skills 的内置分发和安装功能。当前用法请参考 [AI 项目说明](/zh/docs/ai-native/)和[命令参考](/zh/docs/cli-overview/)。 +> 历史设计记录:下文的详细 playbook 已由精简的 `one-cli` skill 替代。当前 `one skills install` 用法见 [Skills](/zh/docs/skills/)。 ## skill 不是营销文档 diff --git a/apps/docs/content/docs/en/ai-native.md b/apps/docs/content/docs/en/ai-native.md index 9e45daf..04da7f5 100644 --- a/apps/docs/content/docs/en/ai-native.md +++ b/apps/docs/content/docs/en/ai-native.md @@ -97,9 +97,9 @@ The full catalogue is in [Error codes](/en/docs/error-codes/). ## Rule 3: Project Instructions Belong To The Team -One CLI does not create or update `AGENTS.md`, `CLAUDE.md`, or the `.one/` directory. Existing files remain untouched, including files generated by older releases. +New workspaces include a short root `AGENTS.md` requiring the `one-cli` skill. If it is missing, agents are directed to run `one skills install`. Ordinary and preset creation use the same instructions; subsequent `one add` calls preserve team edits. -Agents can read `one.manifest.json`, project README files, and command help to understand the workspace. Teams can maintain their own agent instructions when needed. +The skill follows [One Workspace Convention](https://github.com/1cli-team/one-workspace-convention) and directs agents to the installed CLI help. Agents can also read `one.manifest.json` and project README files. Skill installation is explicit and independent of workspace creation; see [Skills](./skills). ## Rule 4: Configuration And Credentials Have Boundaries diff --git a/apps/docs/content/docs/en/cli-overview.md b/apps/docs/content/docs/en/cli-overview.md index a6b9129..77657f1 100644 --- a/apps/docs/content/docs/en/cli-overview.md +++ b/apps/docs/content/docs/en/cli-overview.md @@ -204,3 +204,7 @@ one --help ``` `one --help` shows the six everyday tasks. Use `one help --all` for the complete command catalogue and `one --help` for exact flags. + +## `one skills install` + +Install or refresh the bundled `one-cli` skill for selected coding agents. Use `--agent ` (repeatable) for explicit targets, or `--yes` for all detected agents. Installation is offline, independent of the workspace, and repeatable. See [Skills](./skills). diff --git a/apps/docs/content/docs/en/create.md b/apps/docs/content/docs/en/create.md index 389c36b..09613b6 100644 --- a/apps/docs/content/docs/en/create.md +++ b/apps/docs/content/docs/en/create.md @@ -145,3 +145,7 @@ pnpm install | `WORKSPACE_NESTED_FORBIDDEN` | Do not create a workspace inside an existing workspace; use another directory or `one add` | Full table: [Error codes](/en/docs/error-codes/). + +## Agent instructions + +Ordinary and preset creation write a short root `AGENTS.md` requiring the `one-cli` skill and directing agents to `one skills install` when it is missing. Skill installation is a separate user-level operation. Later `one add` calls preserve this file, including team edits. diff --git a/apps/docs/content/docs/en/error-codes.md b/apps/docs/content/docs/en/error-codes.md index 3eb6c6b..70c7797 100644 --- a/apps/docs/content/docs/en/error-codes.md +++ b/apps/docs/content/docs/en/error-codes.md @@ -37,6 +37,12 @@ Field meanings: - **`error.context`**: structured data from the failure site. It often already contains the data needed for recovery. - **`error.remediation`**: recovery actions. Each item has `action`, `hint`, and sometimes `command`; agents should prefer these before guessing. +## Agent Skill Installation + +### `SKILLS_INSTALL_FAILED` + +The bundled `one-cli` skill could not be installed. Check the agent ID and destination permissions with `one skills install --help`. If some targets were installed before a failure, `context.installed_to` lists those directories. Retrying is safe. + ## Generic / Lifecycle Command-level failures, user cancellation, and internal serialization failures. diff --git a/apps/docs/content/docs/en/installation.md b/apps/docs/content/docs/en/installation.md index 0f3d85e..c2ac02b 100644 --- a/apps/docs/content/docs/en/installation.md +++ b/apps/docs/content/docs/en/installation.md @@ -178,3 +178,7 @@ For the full contributor flow, see [CONTRIBUTING.md](https://github.com/1cli-tea ## Installed? Go to [Quick start](/en/docs/quick-start/) and create your first workspace. + +## Agent skill + +Run `one skills install` to install the bundled `one-cli` skill into your coding agent's user skills directory. Use `one skills install --help` for target selection and supported agents. See [Skills](./skills). diff --git a/apps/docs/content/docs/en/meta.json b/apps/docs/content/docs/en/meta.json index e035ea7..d395e0b 100644 --- a/apps/docs/content/docs/en/meta.json +++ b/apps/docs/content/docs/en/meta.json @@ -6,6 +6,7 @@ "manifest", "templates", "ai-native", + "skills", "cli-overview", "create", "add", diff --git a/apps/docs/content/docs/en/skills.md b/apps/docs/content/docs/en/skills.md new file mode 100644 index 0000000..09cc06f --- /dev/null +++ b/apps/docs/content/docs/en/skills.md @@ -0,0 +1,49 @@ +--- +title: one skills install +description: Install the minimal One CLI skill for your coding agent. +--- + +The bundled `one-cli` skill has two responsibilities: follow [One Workspace Convention](https://github.com/1cli-team/one-workspace-convention), and discover supported workspace operations through the installed CLI's help. Command lists, flags, templates, and manifest schemas stay with the CLI rather than being copied into the skill. + +```bash +one skills install +one skills install --agent claude-code --agent codex +one skills install --yes -o json +``` + +## Target selection + +- `--agent ` / `-a ` selects a target explicitly, even before that agent has created its configuration directory. Repeat the option for multiple agents. +- With no explicit targets, an interactive terminal offers detected agents, all preselected. +- `--yes` / `-y` or non-interactive execution installs into all detected agents without prompting. +- If no targets are detected, the command fails and asks for an explicit `--agent`. +- `one skills` and `one skills install --help` only display help. The latter lists every supported agent ID. + +Detection uses user configuration directories. Agent IDs from the previous installer remain supported. `CODEX_HOME`, `CLAUDE_CONFIG_DIR`, `XDG_CONFIG_HOME`, and `VIBE_HOME` override their respective default roots when set to absolute directories. + +## Installation + +Installation copies the embedded `one-cli/SKILL.md` into the selected agents' user skills directories. It works offline and from any directory. Duplicate targets sharing a directory are written once. Success output lists the destination directories; the skill is in `one-cli/SKILL.md` below each destination. + +Repeating installation replaces that `one-cli` directory, including old reference files. Other skills stay intact. A legacy `one-cli` symlink is replaced with a direct copy without changing its shared store or other agents' links. If an installation fails after some targets succeed, the error's `context.installed_to` lists completed destinations; rerun the command after fixing the reported problem. + +After installation, load the installed skill. Restart the agent session if it does not discover new skills automatically. + +## Generated workspaces + +`one create`, including preset creation, writes a short root `AGENTS.md`: + +```markdown +# Development + +Use the `one-cli` skill when developing this workspace. +If it is not installed, run `one skills install` first. +``` + +Creation does not install skills into your home directory. `one add` preserves existing instructions. For an existing workspace, install the skill and add the same guidance to its `AGENTS.md`. + +## Updates and output + +Ordinary CLI upgrades do not require reinstalling this skill: it directs agents to `one --help`, `one help --all`, and the relevant command help. Reinstall only when the skill's guidance itself changes or when configuring another agent. + +Structured output uses `one-cli/skills-install/v1`, with `status`, `targets`, `installed_to`, and `skill_count` (one). Use the usual `-o json`, `-o yaml`, or `-o text` option. Installation failures use `SKILLS_INSTALL_FAILED`. diff --git a/apps/docs/content/docs/zh/ai-native.md b/apps/docs/content/docs/zh/ai-native.md index ed983d5..72d8246 100644 --- a/apps/docs/content/docs/zh/ai-native.md +++ b/apps/docs/content/docs/zh/ai-native.md @@ -97,9 +97,9 @@ agent 的处理顺序应该是: ## 规则三:项目说明由团队维护 -One CLI 不会创建或更新 `AGENTS.md`、`CLAUDE.md` 或 `.one/` 目录。已有文件保持原样,包括旧版本生成的文件。 +新工作区包含简短的根 `AGENTS.md`,要求使用 `one-cli` skill;缺失时运行 `one skills install`。普通创建与 preset 创建使用同一份指引,后续 `one add` 保留团队修改。 -agent 可以读取 `one.manifest.json`、项目 README 和命令帮助来理解工作区。团队如需专门的 agent 说明,可自行维护。 +Skill 要求遵循 [One Workspace Convention](https://github.com/1cli-team/one-workspace-convention),并查询当前 CLI 的 help。Agent 还可以读取 `one.manifest.json` 和项目 README。Skill 安装独立于项目创建,详见 [Skills](./skills)。 ## 规则四:配置和凭据有边界 diff --git a/apps/docs/content/docs/zh/cli-overview.md b/apps/docs/content/docs/zh/cli-overview.md index 4742abe..a434bd5 100644 --- a/apps/docs/content/docs/zh/cli-overview.md +++ b/apps/docs/content/docs/zh/cli-overview.md @@ -207,3 +207,7 @@ one --help ``` `one --help` 只展示六个日常核心任务;`one help --all` 展示完整命令;具体 flag 以 `one --help` 为准。 + +## `one skills install` + +为选定的 coding agent 安装或刷新内置 `one-cli` skill。使用可重复的 `--agent ` 指定目标,或用 `--yes` 安装到所有检测到的 Agent。支持离线执行,无需进入工作区,可重复安装。详见 [Skills](./skills)。 diff --git a/apps/docs/content/docs/zh/create.md b/apps/docs/content/docs/zh/create.md index 6c9b32a..aceacf8 100644 --- a/apps/docs/content/docs/zh/create.md +++ b/apps/docs/content/docs/zh/create.md @@ -152,3 +152,7 @@ one dev api | `WORKSPACE_NESTED_FORBIDDEN` | 拒绝在已有 workspace 里再 create;换目录或用 `one add` | 完整码表:[错误码大全](/zh/docs/error-codes/)。 + +## Agent 指引 + +普通创建和 preset 创建都会写入简短的根 `AGENTS.md`,要求使用 `one-cli` skill,缺失时运行 `one skills install`。Skill 安装是独立的用户级操作;后续 `one add` 保留这份文件及团队自行补充的内容。 diff --git a/apps/docs/content/docs/zh/error-codes.md b/apps/docs/content/docs/zh/error-codes.md index 8f62d8e..3f17be9 100644 --- a/apps/docs/content/docs/zh/error-codes.md +++ b/apps/docs/content/docs/zh/error-codes.md @@ -40,6 +40,18 @@ import { Callout } from "fumadocs-ui/components/callout"; 下面按命令域分组列出所有 code。 +## Agent skill 安装 + +内置 one-cli skill 的目标选择与用户级目录安装错误。 + +### `SKILLS_INSTALL_FAILED` + +The bundled one-cli skill could not be installed into the selected agents. + +**Remediation**: + +- `inspect-skill-install` — Check the target agent and directory permissions. Completed targets are listed in context.installed_to; retrying is safe.
运行:`one skills install --help` + ## 通用 / 生命周期 命令本身的失败、用户取消、内部序列化错误。 diff --git a/apps/docs/content/docs/zh/installation.md b/apps/docs/content/docs/zh/installation.md index ade2f56..cd7ef62 100644 --- a/apps/docs/content/docs/zh/installation.md +++ b/apps/docs/content/docs/zh/installation.md @@ -204,3 +204,7 @@ Windows 会创建 `~/.local/bin/one.exe`;如果系统不允许创建文件符 ## 装完了? 跳到 [快速开始](/zh/docs/quick-start/) 跑通第一个工作区。 + +## Agent skill + +运行 `one skills install`,将内置 `one-cli` skill 安装到 coding agent 的用户级 skills 目录。目标选择和支持的 Agent 见 `one skills install --help`,详见 [Skills](./skills)。 diff --git a/apps/docs/content/docs/zh/meta.json b/apps/docs/content/docs/zh/meta.json index c85a595..f2e4453 100644 --- a/apps/docs/content/docs/zh/meta.json +++ b/apps/docs/content/docs/zh/meta.json @@ -6,6 +6,7 @@ "manifest", "templates", "ai-native", + "skills", "cli-overview", "create", "add", diff --git a/apps/docs/content/docs/zh/skills.md b/apps/docs/content/docs/zh/skills.md new file mode 100644 index 0000000..cc3261b --- /dev/null +++ b/apps/docs/content/docs/zh/skills.md @@ -0,0 +1,49 @@ +--- +title: one skills install +description: 为 coding agent 安装精简的 One CLI skill。 +--- + +内置 `one-cli` skill 只有两个职责:遵循 [One Workspace Convention](https://github.com/1cli-team/one-workspace-convention),以及通过本机 CLI 的 help 发现工作区操作。命令列表、参数、模板和 manifest schema 由 CLI 维护,避免在 skill 中复制一份。 + +```bash +one skills install +one skills install --agent claude-code --agent codex +one skills install --yes -o json +``` + +## 选择安装目标 + +- `--agent ` / `-a ` 显式选择 Agent,即使该 Agent 尚未创建配置目录也可安装。支持重复指定。 +- 未指定目标时,交互终端列出检测到的 Agent,默认全部勾选。 +- 使用 `--yes` / `-y` 或处于非交互环境时,直接安装到所有检测到的 Agent。 +- 未检测到目标时,命令返回错误并提示显式使用 `--agent`。 +- `one skills` 和 `one skills install --help` 只显示帮助,后者列出所有支持的 Agent ID。 + +检测依据是用户配置目录,兼容旧安装器的 Agent ID。`CODEX_HOME`、`CLAUDE_CONFIG_DIR`、`XDG_CONFIG_HOME` 和 `VIBE_HOME` 可用绝对目录覆盖对应的默认位置。 + +## 安装行为 + +安装器将二进制内置的 `one-cli/SKILL.md` 复制到所选 Agent 的用户级 skills 目录,可离线运行,无需进入工作区。多个目标共用同一目录时只写入一次。成功输出列出目标目录,其下的 `one-cli/SKILL.md` 就是已安装的 skill。 + +重复安装会替换 `one-cli` 目录,包括旧参考资料。其他 skills 保持原样。旧的 `one-cli` 符号链接会替换为直接副本,链接指向的共享 store 及其他 Agent 的链接不受影响。如果部分目标成功后发生错误,错误的 `context.installed_to` 会列出已完成的目录;修复问题后可重新执行。 + +安装后加载本地 skill;Agent 若无法自动发现新 skill,请重新启动会话。 + +## 生成项目的指引 + +`one create`(包括 preset 创建)会写入简短的根 `AGENTS.md`: + +```markdown +# Development + +Use the `one-cli` skill when developing this workspace. +If it is not installed, run `one skills install` first. +``` + +创建项目不向用户目录安装 skill;后续 `one add` 保留已有指引。已有工作区可单独安装 skill,再将相同要求加入自己的 `AGENTS.md`。 + +## 更新与输出 + +日常 CLI 升级无需重新安装 skill:Agent 会查看 `one --help`、`one help --all` 和对应的命令帮助。仅在 skill 的指导原则变化或配置新的 Agent 时重新安装。 + +结构化输出沿用 `one-cli/skills-install/v1`,包含 `status`、`targets`、`installed_to` 和 `skill_count`(值为 1)。支持通用的 `-o json`、`-o yaml`、`-o text`。安装失败使用 `SKILLS_INSTALL_FAILED`。 diff --git a/packages/cli/internal/bootstrap/cli/root.go b/packages/cli/internal/bootstrap/cli/root.go index e918451..83c8fd2 100644 --- a/packages/cli/internal/bootstrap/cli/root.go +++ b/packages/cli/internal/bootstrap/cli/root.go @@ -41,6 +41,7 @@ import ( "github.com/torchstellar-team/one-cli/packages/cli/internal/transport/cobra/mise" "github.com/torchstellar-team/one-cli/packages/cli/internal/transport/cobra/run" "github.com/torchstellar-team/one-cli/packages/cli/internal/transport/cobra/serve" + "github.com/torchstellar-team/one-cli/packages/cli/internal/transport/cobra/skills" "github.com/torchstellar-team/one-cli/packages/cli/internal/transport/cobra/templates" ) @@ -75,6 +76,7 @@ func newRootCommand() *cobra.Command { Registry: deps.registry, Manifest: deps.manifest, Environments: deps.environments, }), templatescmd.Commands(), + skillscmd.Commands(), } for _, commands := range groups { root.AddCommand(commands...) diff --git a/packages/cli/internal/bootstrap/cli/root_test.go b/packages/cli/internal/bootstrap/cli/root_test.go index e65a691..d0b4183 100644 --- a/packages/cli/internal/bootstrap/cli/root_test.go +++ b/packages/cli/internal/bootstrap/cli/root_test.go @@ -159,7 +159,7 @@ func TestRootHelpDoesNotAdvertiseUnregisteredCommands(t *testing.T) { func TestIsKnownSubcommand(t *testing.T) { // Every command is assembled by the composition root. for _, name := range []string{ - "create", "templates", "add", + "create", "templates", "add", "skills", // Per-domain commands (post capability-interface refactor). "env", "container", "dev", "deploy", "ci", // configure owns the credential CRUD surface (renamed from @@ -175,7 +175,7 @@ func TestIsKnownSubcommand(t *testing.T) { // replaced by `one configure`, and `one profile` // renamed to `one configure`. for _, name := range []string{ - "doctor", "status", "unknown", "secrets", "skill", "skills", "prd", "design", + "doctor", "status", "unknown", "secrets", "skill", "prd", "design", "docker", "infisical", "dotenv", "procs", "compose", "k8s", "plugins", "setup", "profile", "", diff --git a/packages/cli/internal/modules/creation/workspace_content.go b/packages/cli/internal/modules/creation/workspace_content.go index f7f4c81..f0eb0d4 100644 --- a/packages/cli/internal/modules/creation/workspace_content.go +++ b/packages/cli/internal/modules/creation/workspace_content.go @@ -6,6 +6,8 @@ import ( // These are the verbatim file contents the scaffolder writes. +const agentsContent = "# Development\n\nUse the `one-cli` skill when developing this workspace.\nIf it is not installed, run `one skills install` first.\n" + const pnpmWorkspaceContent = `packages: - "apps/*" - "services/*" diff --git a/packages/cli/internal/modules/creation/workspace_files.go b/packages/cli/internal/modules/creation/workspace_files.go index 5afb24a..29ba0f9 100644 --- a/packages/cli/internal/modules/creation/workspace_files.go +++ b/packages/cli/internal/modules/creation/workspace_files.go @@ -38,6 +38,9 @@ func generateWorkspaceFiles(targetDir string, opts workspaceFilesOptions) error if err := os.WriteFile(filepath.Join(targetDir, ".gitignore"), []byte(gitignoreContent), 0o644); err != nil { return err } + if err := os.WriteFile(filepath.Join(targetDir, "AGENTS.md"), []byte(agentsContent), 0o644); err != nil { + return err + } return nil } diff --git a/packages/cli/internal/modules/creation/workspace_files_test.go b/packages/cli/internal/modules/creation/workspace_files_test.go index 0c5df6d..94a98f5 100644 --- a/packages/cli/internal/modules/creation/workspace_files_test.go +++ b/packages/cli/internal/modules/creation/workspace_files_test.go @@ -25,7 +25,7 @@ func TestGenerateWorkspaceFiles(t *testing.T) { t.Fatalf("generateWorkspaceFiles() = %v", err) } - for _, rel := range []string{"one.manifest.json", ".gitignore", "apps", "services", "packages"} { + for _, rel := range []string{"one.manifest.json", ".gitignore", "AGENTS.md", "apps", "services", "packages"} { if _, err := os.Stat(filepath.Join(target, rel)); err != nil { t.Fatalf("missing %s: %v", rel, err) } diff --git a/packages/cli/internal/modules/skills/agents.go b/packages/cli/internal/modules/skills/agents.go new file mode 100644 index 0000000..088da5c --- /dev/null +++ b/packages/cli/internal/modules/skills/agents.go @@ -0,0 +1,177 @@ +// Package skills installs the bundled one-cli skill into coding agents' user directories. +package skills + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/torchstellar-team/one-cli/packages/cli/internal/platform/userdirs" +) + +// Agent is a resolved user-level installation target. +type Agent struct { + ID string `json:"agent_id"` + DisplayName string `json:"display_name"` + GlobalPath string `json:"global_path"` +} + +type agentSpec struct { + id, name, skillsPath, detectPath string +} + +// Keep the previously supported IDs. Paths were checked against +// https://github.com/vercel-labs/skills/blob/main/src/agents.ts. +// Detection uses user configuration directories, independent of the workspace. +var agentSpecs = []agentSpec{ + {"aider-desk", "AiderDesk", ".aider-desk/skills", ".aider-desk"}, + {"amp", "Amp", ".config/agents/skills", ".config/amp"}, + {"kimi-cli", "Kimi Code CLI", ".agents/skills", ".kimi"}, + {"replit", "Replit", ".config/agents/skills", ""}, + {"universal", "Universal", ".config/agents/skills", ""}, + {"antigravity", "Antigravity", ".gemini/antigravity/skills", ".gemini/antigravity"}, + {"augment", "Augment", ".augment/skills", ".augment"}, + {"bob", "IBM Bob", ".bob/skills", ".bob"}, + {"claude-code", "Claude Code", ".claude/skills", ".claude"}, + {"openclaw", "OpenClaw", ".openclaw/skills", ".openclaw"}, + {"cline", "Cline", ".agents/skills", ".cline"}, + {"dexto", "Dexto", ".agents/skills", ".dexto"}, + {"warp", "Warp", ".agents/skills", ".warp"}, + {"codearts-agent", "CodeArts Agent", ".codeartsdoer/skills", ".codeartsdoer"}, + {"codebuddy", "CodeBuddy", ".codebuddy/skills", ".codebuddy"}, + {"codemaker", "Codemaker", ".codemaker/skills", ".codemaker"}, + {"codestudio", "Code Studio", ".codestudio/skills", ".codestudio"}, + {"codex", "Codex", ".codex/skills", ".codex"}, + {"command-code", "Command Code", ".commandcode/skills", ".commandcode"}, + {"continue", "Continue", ".continue/skills", ".continue"}, + {"cortex", "Cortex Code", ".snowflake/cortex/skills", ".snowflake/cortex"}, + {"crush", "Crush", ".config/crush/skills", ".config/crush"}, + {"cursor", "Cursor", ".cursor/skills", ".cursor"}, + {"deepagents", "Deep Agents", ".deepagents/agent/skills", ".deepagents"}, + {"devin", "Devin for Terminal", ".config/devin/skills", ".config/devin"}, + {"droid", "Droid", ".factory/skills", ".factory"}, + {"firebender", "Firebender", ".firebender/skills", ".firebender"}, + {"forgecode", "ForgeCode", ".forge/skills", ".forge"}, + {"gemini-cli", "Gemini CLI", ".gemini/skills", ".gemini"}, + {"github-copilot", "GitHub Copilot", ".copilot/skills", ".copilot"}, + {"goose", "Goose", ".config/goose/skills", ".config/goose"}, + {"junie", "Junie", ".junie/skills", ".junie"}, + {"iflow-cli", "iFlow CLI", ".iflow/skills", ".iflow"}, + {"kilo", "Kilo Code", ".kilo/skills", ".kilo"}, + {"kiro-cli", "Kiro CLI", ".kiro/skills", ".kiro"}, + {"kode", "Kode", ".kode/skills", ".kode"}, + {"mcpjam", "MCPJam", ".mcpjam/skills", ".mcpjam"}, + {"mistral-vibe", "Mistral Vibe", ".vibe/skills", ".vibe"}, + {"mux", "Mux", ".mux/skills", ".mux"}, + {"opencode", "OpenCode", ".config/opencode/skills", ".config/opencode"}, + {"openhands", "OpenHands", ".openhands/skills", ".openhands"}, + {"pi", "Pi", ".pi/agent/skills", ".pi"}, + {"qoder", "Qoder", ".qoder/skills", ".qoder"}, + {"qwen-code", "Qwen Code", ".qwen/skills", ".qwen"}, + {"rovodev", "Rovo Dev", ".rovodev/skills", ".rovodev"}, + {"roo", "Roo Code", ".roo/skills", ".roo"}, + {"tabnine-cli", "Tabnine CLI", ".tabnine/agent/skills", ".tabnine"}, + {"trae", "Trae", ".trae/skills", ".trae"}, + {"trae-cn", "Trae CN", ".trae-cn/skills", ".trae-cn"}, + {"windsurf", "Windsurf", ".codeium/windsurf/skills", ".codeium/windsurf"}, + {"zencoder", "Zencoder", ".zencoder/skills", ".zencoder"}, + {"neovate", "Neovate", ".neovate/skills", ".neovate"}, + {"pochi", "Pochi", ".pochi/skills", ".pochi"}, + {"adal", "AdaL", ".adal/skills", ".adal"}, +} + +// AgentIDs provides the same registry to help and target validation. +func AgentIDs() []string { + ids := make([]string, 0, len(agentSpecs)) + for _, spec := range agentSpecs { + ids = append(ids, spec.id) + } + return ids +} + +// ResolveTargets validates every explicit ID before any write. Without IDs it +// selects agents whose configuration directories exist; there is no fallback. +func ResolveTargets(ids []string) ([]Agent, error) { + home, err := userdirs.Home() + if err != nil { + return nil, err + } + available := make(map[string]agentSpec, len(agentSpecs)) + for _, spec := range agentSpecs { + available[spec.id] = spec + } + selected := make([]agentSpec, 0, len(agentSpecs)) + if len(ids) > 0 { + seen := map[string]bool{} + for _, id := range ids { + spec, ok := available[id] + if !ok { + return nil, fmt.Errorf("unknown agent %q; run one skills install --help", id) + } + if !seen[id] { + selected = append(selected, spec) + seen[id] = true + } + } + } else { + for _, spec := range agentSpecs { + if spec.detectPath == "" { + continue + } + marker, err := agentPath(home, spec.detectPath) + if err != nil { + return nil, err + } + info, err := os.Stat(marker) + if os.IsNotExist(err) { + continue + } + if err != nil { + return nil, fmt.Errorf("detect %s: %w", spec.id, err) + } + if info.IsDir() { + selected = append(selected, spec) + } + } + } + out := make([]Agent, 0, len(selected)) + for _, spec := range selected { + dest, err := agentPath(home, spec.skillsPath) + if err != nil { + return nil, err + } + out = append(out, Agent{ID: spec.id, DisplayName: spec.name, GlobalPath: dest}) + } + if len(out) == 0 { + return nil, fmt.Errorf("no target agents found; specify --agent (see one skills install --help)") + } + return out, nil +} + +// Honor the agent's own configuration root, including XDG on all platforms. +func agentPath(home, relative string) (string, error) { + for _, override := range []struct{ prefix, env string }{ + {".codex", "CODEX_HOME"}, {".claude", "CLAUDE_CONFIG_DIR"}, + {".config", "XDG_CONFIG_HOME"}, {".vibe", "VIBE_HOME"}, + } { + if relative != override.prefix && !strings.HasPrefix(relative, override.prefix+"/") { + continue + } + root := strings.TrimSpace(os.Getenv(override.env)) + if root == "" { + break + } + if root == "~" { + root = home + } else if strings.HasPrefix(root, "~/") { + root = filepath.Join(home, filepath.FromSlash(root[2:])) + } + if !filepath.IsAbs(root) { + return "", fmt.Errorf("%s must be an absolute directory", override.env) + } + suffix := strings.TrimPrefix(strings.TrimPrefix(relative, override.prefix), "/") + return filepath.Join(root, filepath.FromSlash(suffix)), nil + } + return filepath.Abs(filepath.Join(home, filepath.FromSlash(relative))) +} diff --git a/packages/cli/internal/modules/skills/install.go b/packages/cli/internal/modules/skills/install.go new file mode 100644 index 0000000..1c5b8b1 --- /dev/null +++ b/packages/cli/internal/modules/skills/install.go @@ -0,0 +1,93 @@ +package skills + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/torchstellar-team/one-cli/packages/cli/internal/resources/bundled" +) + +const Name = "one-cli" + +// Install copies the single bundled skill into each distinct target directory. +// It needs neither a workspace nor a network connection. On partial failure the +// returned paths identify the targets that were already installed successfully. +func Install(ctx context.Context, targets []Agent) ([]string, error) { + installed := []string{} + if len(targets) == 0 { + return installed, errors.New("no target agents selected") + } + seen := map[string]bool{} + for _, target := range targets { + if err := ctx.Err(); err != nil { + return installed, err + } + root := filepath.Clean(target.GlobalPath) + if !filepath.IsAbs(root) { + return installed, fmt.Errorf("agent %s: installation directory must be absolute", target.ID) + } + if seen[root] { + continue + } + if err := installAt(root); err != nil { + return installed, fmt.Errorf("install %s for %s at %s: %w", Name, target.ID, root, err) + } + seen[root] = true + installed = append(installed, root) + } + return installed, nil +} + +func installAt(root string) error { + if err := os.MkdirAll(root, 0o755); err != nil { + return err + } + // Stage a complete directory beside the destination. Renaming an old symlink + // moves the link itself; its shared store (and other agents using it) is never + // traversed or modified. Backups stay available until publication succeeds. + stage, err := os.MkdirTemp(root, ".one-cli-install-*") + if err != nil { + return err + } + cleanup := true + defer func() { + if cleanup { + _ = os.RemoveAll(stage) + } + }() + next := filepath.Join(stage, "next") + if err := os.Mkdir(next, 0o755); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(next, "SKILL.md"), bundled.OneCLISkill, 0o644); err != nil { + return err + } + dest := filepath.Join(root, Name) + backup := filepath.Join(stage, "previous") + hadPrevious := false + if info, err := os.Lstat(dest); err == nil { + if !info.IsDir() && info.Mode()&os.ModeSymlink == 0 { + return fmt.Errorf("destination is not a skill directory: %s", dest) + } + if err := os.Rename(dest, backup); err != nil { + return err + } + hadPrevious = true + } else if !os.IsNotExist(err) { + return err + } + if err := os.Rename(next, dest); err != nil { + if hadPrevious { + if restoreErr := os.Rename(backup, dest); restoreErr != nil { + // Preserve the only remaining old copy if restoration also fails. + cleanup = false + return fmt.Errorf("publish: %w; restore failed: %v; previous installation is at %s", err, restoreErr, backup) + } + } + return err + } + return nil +} diff --git a/packages/cli/internal/modules/skills/skills_test.go b/packages/cli/internal/modules/skills/skills_test.go new file mode 100644 index 0000000..ed343b3 --- /dev/null +++ b/packages/cli/internal/modules/skills/skills_test.go @@ -0,0 +1,197 @@ +package skills + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/torchstellar-team/one-cli/packages/cli/internal/resources/bundled" +) + +func isolatedHome(t *testing.T) string { + t.Helper() + home := t.TempDir() + t.Setenv("HOME", home) + for _, name := range []string{"CODEX_HOME", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME", "VIBE_HOME"} { + t.Setenv(name, "") + } + return home +} + +func writeFile(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { + t.Fatal(err) + } +} + +func TestResolveTargetsExplicitAndDetected(t *testing.T) { + home := isolatedHome(t) + if _, err := ResolveTargets(nil); err == nil { + t.Fatal("empty home must require an explicit target") + } + // A shared skills folder does not identify every agent that can read it. + writeFile(t, filepath.Join(home, ".agents", "skills", "other", "SKILL.md"), "other") + if _, err := ResolveTargets(nil); err == nil { + t.Fatal("shared skills folder must not trigger unrelated agents") + } + writeFile(t, filepath.Join(home, ".cursor", "settings.json"), "{}") + detected, err := ResolveTargets(nil) + if err != nil || len(detected) != 1 || detected[0].ID != "cursor" { + t.Fatalf("detected = %v, %v", detected, err) + } + explicit, err := ResolveTargets([]string{"claude-code", "codex", "codex"}) + if err != nil || len(explicit) != 2 { + t.Fatalf("explicit = %v, %v", explicit, err) + } + if explicit[1].GlobalPath != filepath.Join(home, ".codex", "skills") { + t.Fatalf("unexpected destination: %s", explicit[1].GlobalPath) + } + if _, err := ResolveTargets([]string{"cursor", "../../outside"}); err == nil { + t.Fatal("invalid target must fail before installation") + } +} + +func TestAgentRootsAndRegistry(t *testing.T) { + home := isolatedHome(t) + for _, tc := range []struct{ id, env, suffix string }{ + {"codex", "CODEX_HOME", "skills"}, + {"claude-code", "CLAUDE_CONFIG_DIR", "skills"}, + {"opencode", "XDG_CONFIG_HOME", "opencode/skills"}, + {"mistral-vibe", "VIBE_HOME", "skills"}, + } { + t.Run(tc.id, func(t *testing.T) { + root := filepath.Join(home, "custom", tc.id) + t.Setenv(tc.env, root) + targets, err := ResolveTargets([]string{tc.id}) + if err != nil || targets[0].GlobalPath != filepath.Join(root, filepath.FromSlash(tc.suffix)) { + t.Fatalf("targets = %v, %v", targets, err) + } + t.Setenv(tc.env, "relative/path") + if _, err := ResolveTargets([]string{tc.id}); err == nil { + t.Fatal("relative configuration override must fail") + } + }) + } + seen := map[string]bool{} + for _, id := range AgentIDs() { + if seen[id] { + t.Fatalf("duplicate agent %s", id) + } + seen[id] = true + targets, err := ResolveTargets([]string{id}) + if err != nil || len(targets) != 1 || !strings.HasPrefix(targets[0].GlobalPath, home+string(filepath.Separator)) { + t.Fatalf("invalid registry entry %s: %v, %v", id, targets, err) + } + } +} + +func TestInstallReplacesOnlyOneCLISkill(t *testing.T) { + home := isolatedHome(t) + targets, err := ResolveTargets([]string{"codex"}) + if err != nil { + t.Fatal(err) + } + root := targets[0].GlobalPath + other := filepath.Join(root, "other", "SKILL.md") + writeFile(t, other, "keep me") + writeFile(t, filepath.Join(root, Name, "references", "obsolete.md"), "old command reference") + for range 2 { + paths, err := Install(context.Background(), append(targets, targets[0])) + if err != nil || !reflect.DeepEqual(paths, []string{root}) { + t.Fatalf("install = %v, %v", paths, err) + } + body, err := os.ReadFile(filepath.Join(root, Name, "SKILL.md")) + if err != nil || string(body) != string(bundled.OneCLISkill) { + t.Fatalf("skill = %q, %v", body, err) + } + entries, err := os.ReadDir(filepath.Join(root, Name)) + if err != nil || len(entries) != 1 { + t.Fatalf("obsolete skill files remain: %v, %v", entries, err) + } + body, err = os.ReadFile(other) + if err != nil || string(body) != "keep me" { + t.Fatalf("other skill changed: %q, %v", body, err) + } + entries, err = os.ReadDir(root) + if err != nil || len(entries) != 2 { + t.Fatalf("unexpected installation leftovers: %v, %v", entries, err) + } + } + if _, err := os.Stat(filepath.Join(home, ".one")); !os.IsNotExist(err) { + t.Fatalf("unexpected shared store: %v", err) + } +} + +func TestInstallReplacesLegacySymlinkWithoutChangingStore(t *testing.T) { + for _, broken := range []bool{false, true} { + t.Run(map[bool]string{false: "existing", true: "broken"}[broken], func(t *testing.T) { + home := isolatedHome(t) + targets, err := ResolveTargets([]string{"cursor"}) + if err != nil { + t.Fatal(err) + } + root := targets[0].GlobalPath + if err := os.MkdirAll(root, 0o755); err != nil { + t.Fatal(err) + } + store := filepath.Join(home, ".one", "skills-store", "one-bundled", Name) + if !broken { + writeFile(t, filepath.Join(store, "SKILL.md"), "old shared copy") + } + if err := os.Symlink(store, filepath.Join(root, Name)); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + if _, err := Install(context.Background(), targets); err != nil { + t.Fatal(err) + } + info, err := os.Lstat(filepath.Join(root, Name)) + if err != nil || !info.IsDir() { + t.Fatalf("not a direct installation: %v, %v", info, err) + } + body, err := os.ReadFile(filepath.Join(store, "SKILL.md")) + if broken { + if !os.IsNotExist(err) { + t.Fatalf("broken store was created: %v", err) + } + } else if err != nil || string(body) != "old shared copy" { + t.Fatalf("legacy store changed: %q, %v", body, err) + } + }) + } +} + +func TestInstallReportsPartialFailureAndCancellation(t *testing.T) { + home := isolatedHome(t) + good := filepath.Join(home, "good") + bad := filepath.Join(home, "not-a-directory") + writeFile(t, bad, "keep me") + paths, err := Install(context.Background(), []Agent{{ID: "first", GlobalPath: good}, {ID: "second", GlobalPath: bad}}) + if err == nil || !reflect.DeepEqual(paths, []string{good}) { + t.Fatalf("partial result = %v, %v", paths, err) + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + untouched := filepath.Join(home, "cancelled") + if _, err := Install(ctx, []Agent{{ID: "third", GlobalPath: untouched}}); err == nil { + t.Fatal("cancelled install succeeded") + } + if _, err := os.Stat(untouched); !os.IsNotExist(err) { + t.Fatalf("cancelled install wrote files: %v", err) + } + root := filepath.Join(home, "collision") + writeFile(t, filepath.Join(root, Name), "not a skill directory") + if _, err := Install(context.Background(), []Agent{{ID: "collision", GlobalPath: root}}); err == nil { + t.Fatal("file collision must fail") + } + body, err := os.ReadFile(filepath.Join(root, Name)) + if err != nil || string(body) != "not a skill directory" { + t.Fatalf("collision lost data: %q, %v", body, err) + } +} diff --git a/packages/cli/internal/platform/errors/codes.go b/packages/cli/internal/platform/errors/codes.go index 6143634..a980245 100644 --- a/packages/cli/internal/platform/errors/codes.go +++ b/packages/cli/internal/platform/errors/codes.go @@ -15,6 +15,9 @@ type Code string // Code constants. Keep alphabetical within each group for grep-ability. const ( + // Bundled skill installation. + SKILLS_INSTALL_FAILED Code = "SKILLS_INSTALL_FAILED" + // Generic / lifecycle. ONE_CLI_ERROR Code = "ONE_CLI_ERROR" UNKNOWN_COMMAND Code = "UNKNOWN_COMMAND" @@ -177,6 +180,10 @@ type Definition struct { // from here, so any published error-code reference can be made // authoritative by template-rendering this map. var Codes = map[Code]Definition{ + SKILLS_INSTALL_FAILED: { + Summary: "The bundled one-cli skill could not be installed into the selected agents.", + Remediation: []output.Remediation{{Action: "inspect-skill-install", Hint: "Check the target agent and directory permissions. Completed targets are listed in context.installed_to; retrying is safe.", Command: "one skills install --help"}}, + }, RUNTIME_INVALID: {Summary: "The selected execution runtime is not builtin or mise."}, MISE_NOT_FOUND: {Summary: "The explicitly selected mise executable is unavailable."}, MISE_INSTALL_FAILED: {Summary: "One could not extract, verify, or prepare its bundled mise runtime."}, diff --git a/packages/cli/internal/platform/errors/codes_test.go b/packages/cli/internal/platform/errors/codes_test.go index e34ed86..29bc99b 100644 --- a/packages/cli/internal/platform/errors/codes_test.go +++ b/packages/cli/internal/platform/errors/codes_test.go @@ -15,6 +15,7 @@ func TestEveryCodeHasDefinition(t *testing.T) { // We can't enumerate constants by reflection, so we curate the list // here and rely on grep + this test together. New code = new line. allCodes := []cliErrors.Code{ + cliErrors.SKILLS_INSTALL_FAILED, cliErrors.ONE_CLI_ERROR, cliErrors.UNKNOWN_COMMAND, cliErrors.PROMPT_CANCELLED, diff --git a/packages/cli/internal/platform/i18n/locales/en-US.json b/packages/cli/internal/platform/i18n/locales/en-US.json index 39b29bb..1fdf8e6 100644 --- a/packages/cli/internal/platform/i18n/locales/en-US.json +++ b/packages/cli/internal/platform/i18n/locales/en-US.json @@ -1,4 +1,12 @@ { + "skills.short": "Install the One CLI agent skill", + "skills.install.short": "Install or refresh the bundled one-cli skill for selected agents", + "skills.install.long": "Installs only the bundled one-cli skill into each selected agent's user skills directory. Works offline and outside a workspace. Repeating the command replaces the one-cli directory, including old references; other skills and legacy symlink stores remain untouched.\n\nWith --agent, install to those agents directly. Otherwise select detected agents interactively (all preselected). With --yes or without a terminal, use all detected agents. If no agents are detected, specify --agent explicitly. CODEX_HOME, CLAUDE_CONFIG_DIR, XDG_CONFIG_HOME and VIBE_HOME are honored when set to absolute directories.\n\nAfter installation, load the skill from the reported directory; restart your agent session if it does not discover new skills automatically.", + "skills.flag.agent": "Target agent ID (repeatable; overrides auto-detection)", + "skills.flag.yes": "Install to all detected agents without prompting", + "skills.supported": "Supported agent IDs:", + "skills.select": "Select agents to install the one-cli skill for", + "skills.installed": "✓ Installed the one-cli skill", "root.short": "AI Native monorepo workspace orchestrator", "root.help": "\none — workspace development and deployment\n\nUSAGE\n one [options]\n\nEVERYDAY COMMANDS\n create Create a workspace\n add Add a project\n dev Start local development\n deploy Deploy a project\n env Manage environment variables\n configure Manage local connections and preferences\n\nCOMMON OPTIONS\n -o, --output Output format: json | yaml | text\n -h, --help Show help\n -v, --version Show version\n\nEXAMPLES\n one create demo\n one add\n one dev\n one deploy\n\nAll commands: one help --all\nCommand help: one --help\n", "root.help_all_title": "one — all commands", diff --git a/packages/cli/internal/platform/i18n/locales/zh-CN.json b/packages/cli/internal/platform/i18n/locales/zh-CN.json index 6e51dde..f581de7 100644 --- a/packages/cli/internal/platform/i18n/locales/zh-CN.json +++ b/packages/cli/internal/platform/i18n/locales/zh-CN.json @@ -1,4 +1,12 @@ { + "skills.short": "安装 One CLI 的 Agent skill", + "skills.install.short": "为选定的 Agent 安装或刷新内置 one-cli skill", + "skills.install.long": "仅将内置 one-cli skill 安装到所选 Agent 的用户级 skills 目录。可离线执行,无需进入工作区。重复执行会替换 one-cli 目录及旧参考资料;其他 skills 和旧符号链接指向的共享目录保持原样。\n\n指定 --agent 时直接安装到所选目标。否则交互选择检测到的 Agent,默认全部勾选。使用 --yes 或处于非交互环境时安装到所有检测到的 Agent。未检测到目标时请显式指定 --agent。支持 CODEX_HOME、CLAUDE_CONFIG_DIR、XDG_CONFIG_HOME 和 VIBE_HOME 的绝对目录配置。\n\n安装后可从输出的目录加载 skill;Agent 若无法自动发现新 skill,请重新启动会话。", + "skills.flag.agent": "目标 Agent ID(可重复,优先于自动检测)", + "skills.flag.yes": "安装到所有检测到的 Agent,不进行交互", + "skills.supported": "支持的 Agent ID:", + "skills.select": "选择要安装 one-cli skill 的 Agent", + "skills.installed": "✓ one-cli skill 已安装", "root.short": "AI Native 单体仓库编排器", "root.help": "\none — 工作区开发与部署工具\n\n用法\n one [options]\n\n日常命令\n create 创建新工作区\n add 添加项目\n dev 启动本地开发\n deploy 部署项目\n env 管理环境变量\n configure 管理本机连接和偏好设置\n\n常用选项\n -o, --output 输出格式:json | yaml | text\n -h, --help 显示帮助\n -v, --version 显示版本号\n\n示例\n one create demo\n one add\n one dev\n one deploy\n\n完整命令:one help --all\n命令帮助:one --help\n", "root.help_all_title": "one — 完整命令", diff --git a/packages/cli/internal/resources/bundled/bundled.go b/packages/cli/internal/resources/bundled/bundled.go index 0a8da00..5087784 100644 --- a/packages/cli/internal/resources/bundled/bundled.go +++ b/packages/cli/internal/resources/bundled/bundled.go @@ -1,15 +1,15 @@ // Package bundled exposes the assets the CLI ships with: the template -// registry, the templates themselves, +// registry, the templates themselves, the one-cli skill, // and the built `one serve` web UI. // // The files in this directory are physical copies of canonical sources -// elsewhere in the monorepo (packages/templates/, +// elsewhere in the monorepo (packages/templates/, skills/, // apps/dashboard/dist/). Go's embed directive cannot traverse upward // with "../" and rejects symlinks ("cannot embed irregular file"), so // the copies have to live inside this package directory. // // The whole tree is gitignored. Two tasks regenerate it: -// - `task sync-bundled` — copy packages/templates/ to registry.json / _templates/. +// - `task sync-bundled` — copy templates and the skill to registry.json / _templates/ / _skills/. // - `task sync-web` — pnpm install + vite build of // apps/dashboard/ → _web/. // @@ -53,6 +53,12 @@ var TemplatesFS embed.FS // template directory. const TemplatesRoot = "_templates" +// OneCLISkill is the complete, version-independent skill installed by +// `one skills install`. Its canonical source is skills/one-cli/SKILL.md. +// +//go:embed _skills/one-cli/SKILL.md +var OneCLISkill []byte + // WebDistFS is the built React UI for `one serve` (sources at web/, built // via `task build-web`, copied to internal/resources/bundled/_web by `task sync-bundled`). // internal/transport/http walks this filesystem to serve index.html + hashed assets. diff --git a/packages/cli/internal/resources/bundled/bundled_test.go b/packages/cli/internal/resources/bundled/bundled_test.go index c56d9db..3705864 100644 --- a/packages/cli/internal/resources/bundled/bundled_test.go +++ b/packages/cli/internal/resources/bundled/bundled_test.go @@ -241,3 +241,13 @@ func hasPrefix(xs []string, prefix string) bool { } return false } + +func TestOneCLISkillMatchesSource(t *testing.T) { + source, err := os.ReadFile(filepath.Join(repoRoot(t), "skills", "one-cli", "SKILL.md")) + if err != nil { + t.Fatal(err) + } + if len(source) == 0 || string(source) != string(bundled.OneCLISkill) { + t.Fatal("embedded skill differs from source; run task sync-bundled") + } +} diff --git a/packages/cli/internal/transport/cobra/skills/cmd.go b/packages/cli/internal/transport/cobra/skills/cmd.go new file mode 100644 index 0000000..b983323 --- /dev/null +++ b/packages/cli/internal/transport/cobra/skills/cmd.go @@ -0,0 +1,94 @@ +// Package skillscmd exposes installation of the bundled one-cli skill. +package skillscmd + +import ( + "fmt" + "io" + "strings" + + "github.com/spf13/cobra" + + "github.com/torchstellar-team/one-cli/packages/cli/internal/modules/skills" + cliErrors "github.com/torchstellar-team/one-cli/packages/cli/internal/platform/errors" + "github.com/torchstellar-team/one-cli/packages/cli/internal/platform/i18n" + "github.com/torchstellar-team/one-cli/packages/cli/internal/platform/output" + "github.com/torchstellar-team/one-cli/packages/cli/internal/platform/prompt" +) + +func Commands() []*cobra.Command { + parent := &cobra.Command{Use: "skills", Args: cobra.NoArgs} + i18n.MarkShort(parent, "skills.short") + var agents []string + var yes bool + install := &cobra.Command{ + Use: "install", Args: cobra.NoArgs, + Example: "one skills install\none skills install --agent claude-code --agent codex\none skills install --yes -o json", + RunE: func(cmd *cobra.Command, _ []string) error { + targets, err := selectTargets(agents, yes) + if err != nil { + return err + } + installed, err := skills.Install(cmd.Context(), targets) + if err != nil { + return cliErrors.New(cliErrors.SKILLS_INSTALL_FAILED, err.Error()).WithContext(map[string]any{"installed_to": installed}) + } + output.Emit(&result{ + Schema: "one-cli/skills-install/v1", Status: "completed", + Targets: targets, InstalledTo: installed, SkillCount: 1, + }) + return nil + }, + } + i18n.MarkShort(install, "skills.install.short") + i18n.MarkLong(install, "skills.install.long") + install.Flags().StringSliceVarP(&agents, "agent", "a", nil, "") + install.Flags().BoolVarP(&yes, "yes", "y", false, "") + i18n.MarkFlagUsage(install, "agent", "skills.flag.agent") + i18n.MarkFlagUsage(install, "yes", "skills.flag.yes") + // Render the IDs from the registry, so help and validation cannot drift. + install.SetHelpFunc(func(cmd *cobra.Command, args []string) { + cmd.Long = i18n.T("skills.install.long") + "\n\n" + i18n.T("skills.supported") + "\n" + strings.Join(skills.AgentIDs(), ", ") + parent.HelpFunc()(cmd, args) + }) + parent.AddCommand(install) + return []*cobra.Command{parent} +} + +func selectTargets(ids []string, yes bool) ([]skills.Agent, error) { + targets, err := skills.ResolveTargets(ids) + if err != nil { + return nil, cliErrors.New(cliErrors.SKILLS_INSTALL_FAILED, err.Error()) + } + if len(ids) > 0 || yes || !output.CanPrompt() { + return targets, nil + } + options := make([]prompt.Option[string], 0, len(targets)) + defaults := make([]string, 0, len(targets)) + for _, target := range targets { + options = append(options, prompt.Option[string]{Label: target.DisplayName, Description: target.GlobalPath, Value: target.ID}) + defaults = append(defaults, target.ID) + } + picked, err := prompt.MultiSelect(i18n.T("skills.select"), options, defaults) + if err != nil { + return nil, err + } + if len(picked) == 0 { + return nil, cliErrors.New(cliErrors.SKILLS_INSTALL_FAILED, "no target agents selected") + } + return skills.ResolveTargets(picked) +} + +type result struct { + Schema string `json:"schema"` + Status string `json:"status"` + Targets []skills.Agent `json:"targets"` + InstalledTo []string `json:"installed_to"` + SkillCount int `json:"skill_count"` +} + +func (r *result) RenderTTY(w io.Writer) { + fmt.Fprintln(w, i18n.T("skills.installed")) + for _, target := range r.Targets { + fmt.Fprintf(w, " %s: %s\n", target.DisplayName, target.GlobalPath) + } +} diff --git a/packages/cli/testdata/reference/help/skills.txt b/packages/cli/testdata/reference/help/skills.txt new file mode 100644 index 0000000..a8cbd0e --- /dev/null +++ b/packages/cli/testdata/reference/help/skills.txt @@ -0,0 +1,12 @@ + +DESCRIPTION +Install the One CLI agent skill + +USAGE + one skills [flags] + +SUBCOMMANDS + install Install or refresh the bundled one-cli skill for selected agents + +COMMON OPTIONS + -o, --output Output format: json | yaml | text (default: terminal-friendly text, JSON when piped) diff --git a/packages/cli/testdata/reference/help/skills_install.txt b/packages/cli/testdata/reference/help/skills_install.txt new file mode 100644 index 0000000..48df396 --- /dev/null +++ b/packages/cli/testdata/reference/help/skills_install.txt @@ -0,0 +1,26 @@ + +DESCRIPTION +Install or refresh the bundled one-cli skill for selected agents + +USAGE + one skills install [flags] + +EXAMPLES + one skills install + one skills install --agent claude-code --agent codex + one skills install --yes -o json + +TIPS +Installs only the bundled one-cli skill into each selected agent's user skills directory. Works offline and outside a workspace. Repeating the command replaces the one-cli directory, including old references; other skills and legacy symlink stores remain untouched. + +With --agent, install to those agents directly. Otherwise select detected agents interactively (all preselected). With --yes or without a terminal, use all detected agents. If no agents are detected, specify --agent explicitly. CODEX_HOME, CLAUDE_CONFIG_DIR, XDG_CONFIG_HOME and VIBE_HOME are honored when set to absolute directories. + +After installation, load the skill from the reported directory; restart your agent session if it does not discover new skills automatically. + +Supported agent IDs: +aider-desk, amp, kimi-cli, replit, universal, antigravity, augment, bob, claude-code, openclaw, cline, dexto, warp, codearts-agent, codebuddy, codemaker, codestudio, codex, command-code, continue, cortex, crush, cursor, deepagents, devin, droid, firebender, forgecode, gemini-cli, github-copilot, goose, junie, iflow-cli, kilo, kiro-cli, kode, mcpjam, mistral-vibe, mux, opencode, openhands, pi, qoder, qwen-code, rovodev, roo, tabnine-cli, trae, trae-cn, windsurf, zencoder, neovate, pochi, adal + +COMMON OPTIONS + -a, --agent Target agent ID (repeatable; overrides auto-detection) + -o, --output Output format: json | yaml | text (default: terminal-friendly text, JSON when piped) + -y, --yes Install to all detected agents without prompting diff --git a/packages/cli/tests/e2e/e2e_helpers_test.go b/packages/cli/tests/e2e/e2e_helpers_test.go index e349a02..e62e2ab 100644 --- a/packages/cli/tests/e2e/e2e_helpers_test.go +++ b/packages/cli/tests/e2e/e2e_helpers_test.go @@ -287,6 +287,9 @@ func isolateHome(t *testing.T, dir string) { t.Setenv("XDG_CACHE_HOME", filepath.Join(dir, ".cache")) t.Setenv("HOME", dir) t.Setenv("XDG_CONFIG_HOME", "") + for _, key := range []string{"CODEX_HOME", "CLAUDE_CONFIG_DIR", "VIBE_HOME"} { + t.Setenv(key, "") + } } // fileExists is a tiny convenience wrapper used in tree-shape assertions. diff --git a/packages/cli/tests/e2e/skills_test.go b/packages/cli/tests/e2e/skills_test.go new file mode 100644 index 0000000..747cd10 --- /dev/null +++ b/packages/cli/tests/e2e/skills_test.go @@ -0,0 +1,94 @@ +package cli_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/torchstellar-team/one-cli/packages/cli/internal/resources/bundled" +) + +func TestSkillsInstallExplicitOfflineAndRepeated(t *testing.T) { + home := t.TempDir() + isolateHome(t, home) + // The installer uses embedded bytes; unavailable proxies must not affect it. + t.Setenv("HTTPS_PROXY", "http://127.0.0.1:1") + t.Setenv("HTTP_PROXY", "http://127.0.0.1:1") + for range 2 { + stdout, stderr, code := runBinaryIn(t, home, "skills", "install", "--agent", "codex", "-a", "claude-code", "-a", "codex", "-o", "json") + if code != 0 || stderr != "" { + t.Fatalf("install failed: %d %s %s", code, stdout, stderr) + } + got := mustParseJSON(t, stdout) + if got["schema"] != "one-cli/skills-install/v1" || got["status"] != "completed" || got["skill_count"] != float64(1) { + t.Fatalf("unexpected result: %v", got) + } + if len(got["targets"].([]any)) != 2 || len(got["installed_to"].([]any)) != 2 { + t.Fatalf("duplicate targets: %v", got) + } + for _, dir := range []string{".codex", ".claude"} { + body, err := os.ReadFile(filepath.Join(home, dir, "skills", "one-cli", "SKILL.md")) + if err != nil || string(body) != string(bundled.OneCLISkill) { + t.Fatalf("incorrect installed skill: %v", err) + } + } + } + for _, dir := range []string{".one", ".cursor", "one.manifest.json", "AGENTS.md"} { + if fileExists(t, filepath.Join(home, dir)) { + t.Fatalf("install unexpectedly wrote %s", dir) + } + } +} + +func TestSkillsInstallDetectionAndErrors(t *testing.T) { + home := t.TempDir() + isolateHome(t, home) + for _, args := range [][]string{ + {"skills", "install", "--yes", "-o", "json"}, + {"skills", "install", "--agent", "cursor", "--agent", "invalid-agent", "-o", "json"}, + } { + _, stderr, code := runBinaryIn(t, home, args...) + if code != 1 { + t.Fatalf("expected failure: %d %s", code, stderr) + } + got := mustParseJSON(t, firstJSONLine(stderr)) + if got["error"].(map[string]any)["code"] != "SKILLS_INSTALL_FAILED" { + t.Fatalf("unexpected error: %v", got) + } + entries, err := os.ReadDir(home) + if err != nil || len(entries) != 0 { + t.Fatalf("target resolution wrote files: %v %v", entries, err) + } + } + if err := os.Mkdir(filepath.Join(home, ".cursor"), 0o755); err != nil { + t.Fatal(err) + } + // Non-TTY execution without --yes follows the same detection policy. + for _, args := range [][]string{{"skills", "install", "--yes", "-o", "json"}, {"skills", "install", "-o", "json"}} { + stdout, stderr, code := runBinaryIn(t, home, args...) + if code != 0 { + t.Fatalf("detected install failed: %d %s %s", code, stdout, stderr) + } + got := mustParseJSON(t, stdout) + targets := got["targets"].([]any) + if len(targets) != 1 || targets[0].(map[string]any)["agent_id"] != "cursor" { + t.Fatalf("wrong detection: %v", got) + } + } +} + +func TestSkillsHelpDoesNotInstall(t *testing.T) { + home := t.TempDir() + isolateHome(t, home) + for _, args := range [][]string{{"skills"}, {"skills", "install", "--help"}} { + stdout, stderr, code := runBinaryIn(t, home, args...) + if code != 0 || stderr != "" || !strings.Contains(stdout, "install") { + t.Fatalf("help failed: %d %s %s", code, stdout, stderr) + } + } + entries, err := os.ReadDir(home) + if err != nil || len(entries) != 0 { + t.Fatalf("help wrote files: %v %v", entries, err) + } +} diff --git a/packages/cli/tests/e2e/snapshot_e2e_add_test.go b/packages/cli/tests/e2e/snapshot_e2e_add_test.go index b53a44c..b1bd5ec 100644 --- a/packages/cli/tests/e2e/snapshot_e2e_add_test.go +++ b/packages/cli/tests/e2e/snapshot_e2e_add_test.go @@ -49,7 +49,7 @@ func TestSnapshot_E2E_Add_DefersDeploymentDefaults(t *testing.T) { t.Errorf("expected template file missing: %s", full) } } - assertNoAgentDocs(t, ws) + assertWorkspaceAgentDocs(t, ws) assertNoAgentDocs(t, svcDir) goModRaw, err := os.ReadFile(filepath.Join(svcDir, "go.mod")) if err != nil { @@ -274,7 +274,7 @@ func TestSnapshot_E2E_Add_GoLibTemplate(t *testing.T) { t.Errorf("expected go-lib artifact missing: %s", full) } } - assertNoAgentDocs(t, ws) + assertWorkspaceAgentDocs(t, ws) assertNoAgentDocs(t, libDir) // Dev-only go.mod must NOT leak into the rendered output. diff --git a/packages/cli/tests/e2e/snapshot_e2e_create_preset_test.go b/packages/cli/tests/e2e/snapshot_e2e_create_preset_test.go index 0a530e2..a032ac6 100644 --- a/packages/cli/tests/e2e/snapshot_e2e_create_preset_test.go +++ b/packages/cli/tests/e2e/snapshot_e2e_create_preset_test.go @@ -40,7 +40,7 @@ func TestSnapshot_E2E_Create_Preset_Fullstack(t *testing.T) { got := mustParseJSON(t, stdout) assertSnapshot(t, "create-preset-fullstack.json", got) - assertNoAgentDocs(t, target) + assertWorkspaceAgentDocs(t, target) for _, p := range presetFullstackPaths { full := filepath.Join(target, p) diff --git a/packages/cli/tests/e2e/snapshot_e2e_create_test.go b/packages/cli/tests/e2e/snapshot_e2e_create_test.go index 15a655f..f8ebffb 100644 --- a/packages/cli/tests/e2e/snapshot_e2e_create_test.go +++ b/packages/cli/tests/e2e/snapshot_e2e_create_test.go @@ -10,6 +10,7 @@ import ( "io/fs" "os" "path/filepath" + "strings" "testing" "github.com/torchstellar-team/one-cli/packages/cli/internal/core/workspace" @@ -41,6 +42,7 @@ var expectedScaffoldPaths = []string{ "services", "packages", "one.manifest.json", + "AGENTS.md", ".mise/conf.d/one.toml", "hk.pkl", ".config/one/hk.pkl", @@ -68,7 +70,7 @@ func TestSnapshot_E2E_Create_Default(t *testing.T) { t.Errorf("expected scaffold path missing: %s", full) } } - assertNoAgentDocs(t, target) + assertWorkspaceAgentDocs(t, target) // Manifest sanity. Schema is the current ManifestVersion. mf := readManifest(t, target) @@ -224,3 +226,22 @@ func TestSnapshot_E2E_Create_DefaultEnablesUniversalSet(t *testing.T) { } } } + +func assertWorkspaceAgentDocs(t *testing.T, root string) { + t.Helper() + body, err := os.ReadFile(filepath.Join(root, "AGENTS.md")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(body), "`one-cli` skill") || !strings.Contains(string(body), "one skills install") { + t.Fatalf("workspace guidance does not explain skill installation: %s", body) + } + for _, entry := range []string{"apps", "services", "packages"} { + assertNoAgentDocs(t, filepath.Join(root, entry)) + } + for _, entry := range []string{"CLAUDE.md", ".one", ".agents"} { + if _, err := os.Stat(filepath.Join(root, entry)); !os.IsNotExist(err) { + t.Fatalf("unexpected generated %s: %v", entry, err) + } + } +} diff --git a/packages/cli/tests/e2e/snapshot_e2e_ux_test.go b/packages/cli/tests/e2e/snapshot_e2e_ux_test.go index 6204be6..e5b52e6 100644 --- a/packages/cli/tests/e2e/snapshot_e2e_ux_test.go +++ b/packages/cli/tests/e2e/snapshot_e2e_ux_test.go @@ -69,29 +69,8 @@ func TestSnapshot_E2E_HelpDailyAndCompleteCatalogues(t *testing.T) { t.Errorf("complete help missing %q:\n%s", command, all) } } - if strings.Contains(all, "\n skills") { - t.Errorf("complete help advertises the removed skills command:\n%s", all) - } -} - -func TestRemovedSkillsCommand(t *testing.T) { - tmp := t.TempDir() - isolateHome(t, tmp) - _, stderr, code := runBinaryIn(t, tmp, "skills", "install", "--yes", "-o", "json") - if code != 1 { - t.Fatalf("removed command should fail: exit=%d stderr=%q", code, stderr) - } - envelope := mustParseJSON(t, firstJSONLine(stderr)) - err, ok := envelope["error"].(map[string]any) - if !ok || err["code"] != "UNKNOWN_COMMAND" { - t.Fatalf("expected UNKNOWN_COMMAND, got %s", pretty(envelope)) - } - entries, readErr := os.ReadDir(tmp) - if readErr != nil { - t.Fatal(readErr) - } - if len(entries) != 0 { - t.Fatalf("removed command created files in the isolated home: %v", entries) + if !strings.Contains(all, "\n skills") || !strings.Contains(all, "install") { + t.Errorf("complete help is missing skills install:\n%s", all) } } diff --git a/packages/cli/tools/gen-error-codes/main.go b/packages/cli/tools/gen-error-codes/main.go index 331c797..bf5957f 100644 --- a/packages/cli/tools/gen-error-codes/main.go +++ b/packages/cli/tools/gen-error-codes/main.go @@ -27,6 +27,11 @@ type group struct { } var groups = []group{ + { + title: "Agent skill 安装", + intro: "内置 one-cli skill 的目标选择与用户级目录安装错误。", + prefix: []string{"SKILLS_"}, + }, { title: "通用 / 生命周期", intro: "命令本身的失败、用户取消、内部序列化错误。", diff --git a/packages/cli/tools/sync-resources/main.go b/packages/cli/tools/sync-resources/main.go index 0a7d128..53ac13d 100644 --- a/packages/cli/tools/sync-resources/main.go +++ b/packages/cli/tools/sync-resources/main.go @@ -65,16 +65,22 @@ func syncBundled(root string) error { if err := copyFile(filepath.Join(templates, "registry.json"), filepath.Join(bundled, "registry.json")); err != nil { return err } - return replaceDir(root, templates, filepath.Join(bundled, "_templates"), func(rel string, entry fs.DirEntry) bool { + if err := replaceDir(root, templates, filepath.Join(bundled, "_templates"), func(rel string, entry fs.DirEntry) bool { if rel == "registry.json" { return false } - // Retired agent assets must not be distributed inside the binary. + // Workspace guidance and skills are distributed separately from templates. switch strings.TrimSuffix(entry.Name(), ".hbs") { case ".one", ".agents", "AGENTS.md", "CLAUDE.md", "SKILL.md": return false } return entry.Name() != "go.mod" + }); err != nil { + return err + } + return replaceDir(root, filepath.Join(root, "skills"), filepath.Join(bundled, "_skills"), func(rel string, entry fs.DirEntry) bool { + rel = filepath.ToSlash(rel) + return rel == "one-cli" && entry.IsDir() || rel == "one-cli/SKILL.md" && entry.Type().IsRegular() }) } diff --git a/packages/cli/tools/sync-resources/main_test.go b/packages/cli/tools/sync-resources/main_test.go index 5ad797b..7f37c1a 100644 --- a/packages/cli/tools/sync-resources/main_test.go +++ b/packages/cli/tools/sync-resources/main_test.go @@ -20,16 +20,19 @@ func TestSyncBundledCopiesCanonicalAssetsAndStripsNestedModules(t *testing.T) { writeTestFile(t, root, "packages/templates/go-api/"+rel, "retired agent guidance") } + writeTestFile(t, root, "skills/one-cli/SKILL.md", "one-cli skill") + writeTestFile(t, root, "skills/one-migrate/SKILL.md", "retired skill") + writeTestFile(t, root, "skills/one-cli/references/old.md", "retired reference") if err := syncBundled(root); err != nil { t.Fatalf("syncBundled: %v", err) } bundled := filepath.Join(root, "packages", "cli", "internal", "resources", "bundled") - for _, rel := range []string{"registry.json", "_templates/go-api/main.go"} { + for _, rel := range []string{"registry.json", "_templates/go-api/main.go", "_skills/one-cli/SKILL.md"} { if _, err := os.Stat(filepath.Join(bundled, filepath.FromSlash(rel))); err != nil { t.Errorf("expected %s: %v", rel, err) } } - for _, rel := range []string{"_templates/registry.json", "_templates/go-api/go.mod"} { + for _, rel := range []string{"_templates/registry.json", "_templates/go-api/go.mod", "_skills/one-migrate", "_skills/one-cli/references"} { if _, err := os.Stat(filepath.Join(bundled, filepath.FromSlash(rel))); !os.IsNotExist(err) { t.Errorf("expected %s to be stripped, stat err=%v", rel, err) } diff --git a/packages/cli/tools/verify-cli-references/main.go b/packages/cli/tools/verify-cli-references/main.go index 0c04c5a..7990e5c 100644 --- a/packages/cli/tools/verify-cli-references/main.go +++ b/packages/cli/tools/verify-cli-references/main.go @@ -126,6 +126,7 @@ func docFiles() ([]string, error) { } roots := []walkSpec{ {repoRel("apps", "docs", "content", "docs"), []string{".md", ".mdx"}}, + {repoRel("skills"), []string{".md"}}, } for _, w := range roots { if err := filepath.WalkDir(w.root, func(path string, d fs.DirEntry, err error) error { diff --git a/skills/one-cli/SKILL.md b/skills/one-cli/SKILL.md new file mode 100644 index 0000000..203a5bd --- /dev/null +++ b/skills/one-cli/SKILL.md @@ -0,0 +1,25 @@ +--- +name: one-cli +description: Develop and manage One CLI workspaces using One Workspace Convention and the installed CLI's help. Use when creating, extending, or working in a One CLI workspace. +--- + +# One CLI + +## Workspace structure + +Follow [One Workspace Convention](https://github.com/1cli-team/one-workspace-convention) +when creating or changing the workspace structure: + +- `apps/`: runnable applications. +- `services/`: backend services and workers. +- `packages/`: shared libraries and reusable modules. + +## Use the installed CLI + +Use One CLI for workspace operations it supports. Discover the current commands +with `one --help`; use `one help --all` when you need the complete catalogue. +Before running a command, read `one --help`, then the relevant +subcommand's help if needed. + +Choose commands and flags from that help. The installed CLI is the authority for +its current behavior; do not assume commands or options from an older version.