From 815e2da105685b430d8d423bb12e069eec19a4ae Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 14:45:24 +0800 Subject: [PATCH 01/14] docs(spec): design delegating skill install to npx skills Brainstorming output: replace self-built skill install (mysql-cli init / skill install + internal/agents + internal/skillscheck + bundle.go + scripts/install-skills.sh) with delegation to the vercel-labs/skills ecosystem via `npx skills add AllenMuu/mysql-cli`. mysql-cli stops self-building install/TUI; skill becomes a repo-side asset consumed by the 75+-agent skills ecosystem. --- ...6-07-27-skill-install-npx-skills-design.md | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-27-skill-install-npx-skills-design.md diff --git a/docs/superpowers/specs/2026-07-27-skill-install-npx-skills-design.md b/docs/superpowers/specs/2026-07-27-skill-install-npx-skills-design.md new file mode 100644 index 0000000..c56cf13 --- /dev/null +++ b/docs/superpowers/specs/2026-07-27-skill-install-npx-skills-design.md @@ -0,0 +1,159 @@ +# mysql-cli skill 安装接入 vercel-labs/skills 生态 + +> 设计日期:2026-07-27 +> 状态:待审 + +## 背景与动机 + +mysql-cli 现有三套自研 skill 安装路径: + +- `mysql-cli init`(`internal/cli/init.go`)-- 检测 7 种 agent,用 `internal/agents.Run` 把内嵌 skill 装到各 agent 原生格式;支持 `--agent/--project-dir/--no-global/--dry-run/--json`。 +- `mysql-cli skill install`(`internal/cli/skill.go`)-- 仅把 SKILL.md 复制到 `~/.claude/skills`(Claude 单格式)。 +- `scripts/install-skills.sh` -- shell 版,等价于 `init`。 + +用户最初诉求:给 skill 安装加**交互式选择页面**(选 agent)+ **scope 确认弹窗**(user/project),参考 Claude 插件安装体验。 + +调研发现:飞书 `larksuite/cli` **不自研安装 TUI**,skill 安装完全委托通用包管理器 [`vercel-labs/skills`](https://github.com/vercel-labs/skills)(`npx skills add`)。该工具是业界主流的 open agent skills 生态,支持 75+ agent,有成熟的交互式 TUI(`@clack/prompts`)。 + +**决策**:mysql-cli 不再自研 skill 安装/TUI,接入 vercel-labs/skills 生态,委托 `npx skills add`。 + +## 目标 + +1. mysql-cli 仓库的 `skills/` 目录对齐 vercel-labs/skills 规范,用户用 `npx skills add AllenMuu/mysql-cli` 安装 skill。 +2. 删除 mysql-cli 自研 skill 安装代码:`init` 命令、`skill` 子命令组、`internal/agents`、`internal/skillscheck`、`bundle.go`、`scripts/install-skills.sh`。 +3. 获得 vercel-labs/skills 的 75+ agent 支持 + 成熟交互式 TUI(multiselect agent / select scope / select installMode / confirm),**mysql-cli 自己不写任何 TUI**。 + +## 非目标 + +- 不改 mysql-cli 核心(查询/事务/schema/safety/config/conn/format/repl)。 +- 不改 skill 文件正文内容(只改其中"安装说明"段落)。 +- 不内联或合并 `mysql-shared`(保留 DRY 拆分)。 +- 不对接 `npx skills` 之外的通用包管理器。 + +## 关键调研结论(均核实自 vercel-labs/skills 源码) + +1. **规范极简**:`skills//SKILL.md` + YAML frontmatter(`---\n...\n---`)。mysql-cli 现状(`skills/mysql-{shared,query,schema}/SKILL.md`)已完全符合,frontmatter 的 `metadata.binary/requires/cliHelp` 等自定义字段会被忽略,无害。 +2. **首选发现机制**:仓库根 `.well-known/agent-skills/index.json`(`add.ts` line 556)。不加也能靠默认 `skills/*/SKILL.md` 扫描工作,但加了发现体验更好。 +3. **不处理 skill 间引用**:全仓 grep 无"skill A 引用 B 则连带装 B"逻辑;vercel-labs/skills 自己的 `skills/` 只有一个独立 `find-skills`,无 shared 先例。**`mysql-shared` 模式有断裂风险**:用户若只装 `mysql-query`,`../mysql-shared/SKILL.md` 引用会断。 +4. **交互栈**:`@clack/prompts`(轻量 prompt 库)+ `picocolors` + `@vercel/detect-agent`;仅"可搜索多选"(`src/prompts/search-multiselect.ts`)手写。这些都在 devDependencies,构建时打进二进制。 +5. **scope 二选一**:project(默认,`.//skills/`)vs global(`-g`,`~//skills/`)。与用户诉求一致。 +6. **installMode**:symlink(推荐,单一真相源)/ copy。 +7. **交互流程**:`intro -> multiselect skill -> multiselect agent -> select scope -> select installMode -> note 汇总 -> confirm -> 安装 -> note 结果 -> outro`,每步 `isCancel` 处理。 + +## 架构 + +``` +用户/agent + │ + ├─ 装skill: npx skills add AllenMuu/mysql-cli ──→ vercel-labs/skills 交互式 TUI + │ ↓ + │ 仓库 skills/mysql-{shared,query,schema}/SKILL.md (已就绪) + │ ↓ + │ 装到 .agents/skills/ + 各 agent symlink (75+ agent) + │ + └─ 跑查询: mysql-cli query/txn/schema/... (Go 单二进制,无 Node 依赖) +``` + +mysql-cli 二进制与 skill 安装**完全解耦**:二进制只负责查询/事务/schema,skill 是仓库侧资产,由 vercel-labs/skills 生态管理。 + +## 决策汇总 + +| # | 决策点 | 选择 | +|---|--------|------| +| 1 | 接入方式 | 删除 `skill` 子命令组,委托 `npx skills add`(原"替换默认行为"在 A 方案下演变为删除) | +| 2 | agent 选择交互 | 由 vercel-labs/skills 的 `skills add` 提供(75+ agent),mysql-cli 不实现 | +| 3 | scope 选择交互 | 同上,project/global 二选一由 `skills add` 提供 | +| 4 | project-dir 来源 | vercel-labs/skills 的 project scope 即 `.//skills/`(当前目录),一致 | +| 5 | 改造范围 | 接入生态,委托 npx | +| 6 | 方案变体 | A 激进废弃 | +| 7 | mysql-shared 引用 | 文档引导全装 + `.well-known` 声明 3 skill(保留 DRY 拆分) | +| 8 | `skill install` 子命令 | 完全删除(敲 `mysql-cli skill install` 报 unknown command) | + +> 决策 2/3/4(交互式 TUI 细节)在 A 方案下**由 vercel-labs/skills 提供**,mysql-cli 侧不实现 -- 这是 A 方案的核心红利:零 TUI 代码,直接复用生态成熟交互。 + +## 删除清单 + +### Go 代码 + +| 路径 | 说明 | 消费者核实 | +|------|------|-----------| +| `internal/cli/init.go` | `mysql-cli init` 命令,`agents` 包主消费者 | agents 唯一非测试消费者 | +| `internal/cli/init_test.go` | init 测试 | - | +| `internal/cli/skill.go` | `mysql-cli skill` 子命令组(list/version/check/install) | skillscheck/bundle 消费者 | +| `internal/cli/skill_test.go` | skill 测试 | - | +| `internal/agents/` | 整个包(agents.go/detect.go/install.go/merge.go + tests),7-agent 自研安装 | 删 init.go 后无消费者 | +| `internal/skillscheck/` | 整个包(skillscheck.go + test),版本同步检查 | 删 skill.go 后无消费者 | +| `bundle.go` | `//go:embed skills` + `SkillNames/SkillFile/SkillsFS`,根包 bundle | 删 init.go + skill.go 后无消费者 | + +### 子命令注册 + +- `internal/cli/cli.go`(或注册处)移除 `newInitCmd()` 与 `newSkillCmd()` 的 `AddCommand` 调用。 + +### 脚本 + +| 路径 | 说明 | +|------|------| +| `scripts/install-skills.sh` | shell 版自研安装 | +| `scripts/install-skills-test.sh` | 其自测 | + +### 保留(不动) + +- `internal/cli/version.go` 的 `mysql-cli version` 命令(仅删注释里对 `skill version` 的提及)。 +- `scripts/skill-format-check.sh` + `scripts/skill-format-check/` 测试目录 + `.github/workflows/skill-format-check.yml`(frontmatter 校验仍有用)。 +- `skill-template/skill-template.md`。 +- `skills/` 目录(仓库 skill 资产,接入生态的源)。 +- `internal/{config,conn,query,result,safety,schema,format,repl}` 全部核心。 +- `dist/npm/`(CLI 本身的 npm 分发,与 skill 安装无关)。 + +## 新增 + +### `.well-known/agent-skills/index.json`(仓库根) + +声明 3 个 skill,供 vercel-labs/skills 首选发现机制读取。字段参考 vercel-labs/skills 规范(name/source/description),并在描述中提示"建议全装以保证 mysql-shared 引用不断"。具体 schema 在实现时参照 vercel-labs/skills 的 `add.ts` 对该文件的解析逻辑确定。 + +## 文档迁移 + +- **README.md / README-zh.md**: + - 安装说明改为 `npx skills add AllenMuu/mysql-cli`(交互式)。 + - 非交互示例:`npx skills add AllenMuu/mysql-cli --skill '*' -a -g -y`(CI 友好)。 + - 说明 scope(project `.//skills/` vs global `~//skills/`)+ installMode(symlink/copy)。 + - **强调"建议全装 3 skill 以保证 mysql-shared 引用不断"**。 + - 无 Node 用户 fallback:手动复制 `skills/` 目录到 `~/.claude/skills/` 等。 +- **AGENTS.md**:"Skill 体系"章节重写,移除 `skill install`/`init`/`install-skills.sh`/`agents`/`skillscheck`/`bundle` 描述,改为生态接入说明。 +- **skill 文件内的安装说明**(mysql-shared/mysql-query/mysql-schema 的 SKILL.md):改 `npx skills add`。 +- **CHANGELOG.md**:记录本次 breaking change(`mysql-cli init` / `skill install` 移除,改用 `npx skills add`)。 + +## 测试策略 + +- 删 `internal/agents/*_test.go`、`internal/skillscheck/*_test.go`、`internal/cli/init_test.go`、`internal/cli/skill_test.go`。 +- `internal/cli/cli_test.go`、`commands_test.go`、`errors_test.go` 中涉及 init/skill 子命令的用例同步移除。 +- 保留 `scripts/skill-format-check/test.sh`(frontmatter 校验自测)。 +- 生产代码与测试等量删除,覆盖率应维持(项目目标 ≥80%)。 +- 新增验证(手动/CI):`npx skills add AllenMuu/mysql-cli --list` 能列出 3 skill;`--skill '*' -a claude-code -y --dry-run`(若有)能预演安装。 + +## 迁移影响与向后兼容 + +- **Breaking**:`mysql-cli init`、`mysql-cli skill install/list/version/check` 全部移除,敲这些命令报 unknown command。 +- **运行时依赖**:装 skill 需 Node.js/npx。mysql-cli 二进制本身仍是 Go 单二进制,查询/事务/schema 不需 Node。 +- **无 Node fallback**:手动复制仓库 `skills/` 目录到目标 agent 目录(README 说明)。 +- **版本真相源**:从 CLI 二进制内嵌迁移到仓库 frontmatter(skill 版本 = GitHub 上 SKILL.md 的 version 字段)。 + +## 风险与缓解 + +| 风险 | 影响 | 缓解 | +|------|------|------| +| shared 引用断裂(用户只装单个 skill) | mysql-query/schema 顶部 `../mysql-shared/SKILL.md` 失效 | `.well-known` 声明 + README 强调全装 + agent 非交互装默认 `--skill '*'` | +| npx 依赖 | 无 Node 环境无法装 skill | README 提供 manual copy fallback | +| 向后不兼容 | 旧脚本/文档引用 `mysql-cli init`/`skill install` 报错 | CHANGELOG 标注 breaking;README 迁移指引 | +| vercel-labs/skills 上游变更 | 发现机制/规范漂移 | 锁定 skills 版本范围;关注上游 | + +## 验证清单 + +- [ ] `go build ./...` 通过 +- [ ] `go vet ./...` 通过 +- [ ] `go test ./...` 通过(136 用例删减后仍全绿) +- [ ] `go test -cover ./...` ≥80% +- [ ] `./scripts/skill-format-check.sh skills/` 通过 +- [ ] `npx skills add AllenMuu/mysql-cli --list` 列出 3 skill +- [ ] `npx skills add AllenMuu/mysql-cli --skill '*' -a claude-code -y` 实测安装成功 +- [ ] README/AGENTS.md 无残留旧安装说明 From 190d9961c724f03439f1f7d1c015f5bb21afef48 Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 14:50:26 +0800 Subject: [PATCH 02/14] docs(plan): add implementation plan for npx skills delegation 11-task plan: delete init/skill subcommands + agents/skillscheck/bundle packages + install-skills.sh; add .well-known/agent-skills/index.json; migrate README/README-zh/AGENTS/CHANGELOG to npx skills add; final verification (build/vet/test/cover/skill-format-check/npx install). --- .claude/worktrees/feat+cli-help | 1 + .../2026-07-27-skill-install-npx-skills.md | 591 ++++++++++++++++++ 2 files changed, 592 insertions(+) create mode 160000 .claude/worktrees/feat+cli-help create mode 100644 docs/superpowers/plans/2026-07-27-skill-install-npx-skills.md diff --git a/.claude/worktrees/feat+cli-help b/.claude/worktrees/feat+cli-help new file mode 160000 index 0000000..28f3ac3 --- /dev/null +++ b/.claude/worktrees/feat+cli-help @@ -0,0 +1 @@ +Subproject commit 28f3ac3305e36cc978be7cfccd5906a247b6e2e1 diff --git a/docs/superpowers/plans/2026-07-27-skill-install-npx-skills.md b/docs/superpowers/plans/2026-07-27-skill-install-npx-skills.md new file mode 100644 index 0000000..479bc90 --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-skill-install-npx-skills.md @@ -0,0 +1,591 @@ +# mysql-cli skill 安装接入 vercel-labs/skills 生态 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 删除 mysql-cli 自研 skill 安装代码(`init` 命令、`skill` 子命令组、`internal/agents`、`internal/skillscheck`、`bundle.go`、`scripts/install-skills.sh`),改由 `npx skills add AllenMuu/mysql-cli`(vercel-labs/skills 生态)安装,仓库侧新增 `.well-known/agent-skills/index.json`。 + +**Architecture:** mysql-cli 二进制与 skill 安装解耦。二进制只保留查询/事务/schema 核心;skill 成为仓库侧资产,由 vercel-labs/skills 生态(75+ agent、成熟交互式 TUI)管理。mysql-cli 不再内嵌 skill、不写任何 TUI 代码。 + +**Tech Stack:** Go 1.22、cobra、vercel-labs/skills(npx)、JSON。 + +## Global Constraints + +- Go 1.22(`go.mod`);`go build ./...` / `go vet ./...` / `go test ./...` 必须全绿。 +- 项目测试覆盖率目标 ≥80%(`go test -cover ./...`)。 +- skill frontmatter 校验:`./scripts/skill-format-check.sh skills/` 必须通过。 +- Conventional commits;commit 不带 attribution(全局 settings 已禁)。 +- 远程是 GitHub(memory 记录:repo-is-github),`gh` 可用。 +- 删除顺序遵循依赖:先删消费者(`init.go`/`skill.go`),再删被依赖包(`agents`/`skillscheck`/`bundle`),保证每个任务后 `go build ./...` 通过。 +- `config init`(config 子命令)与本次删除的 `mysql-cli init`(skill 安装)是不同命令,前者保留不动。 + +## File Structure + +**删除:** +- `internal/cli/init.go` -- `mysql-cli init` 命令(`agents` 包主消费者) +- `internal/cli/init_test.go` -- init 测试 +- `internal/cli/skill.go` -- `mysql-cli skill` 子命令组(list/version/check/install) +- `internal/cli/skill_test.go` -- skill 测试 +- `internal/agents/` -- 整个包(agents.go/detect.go/install.go/merge.go + tests) +- `internal/skillscheck/` -- 整个包(skillscheck.go + test) +- `bundle.go` -- 根包 `//go:embed skills` + `SkillNames/SkillFile/SkillsFS` +- `scripts/install-skills.sh` + `scripts/install-skills-test.sh` + +**修改:** +- `internal/cli/root.go` -- 移除 `newSkillCmd()`/`newInitCmd()` 的 `AddCommand` 注册 +- `internal/cli/version.go` -- 注释去掉 `skill version` 提及 +- `README.md` / `README-zh.md` -- 安装说明改为 `npx skills add` +- `AGENTS.md` -- "Skill 体系"章节重写 +- `CHANGELOG.md` -- `[Unreleased] > Breaking` 加条目 + +**新增:** +- `.well-known/agent-skills/index.json` -- vercel-labs/skills 首选发现机制,声明 3 skill + +**保留不动:** `internal/{config,conn,query,result,safety,schema,format,repl}`、`internal/cli/version.go` 的 `version` 命令、`scripts/skill-format-check.sh` + `.github/workflows/skill-format-check.yml`、`skill-template/`、`skills/` 目录、`dist/npm/`。 + +--- + +### Task 1: 移除 `init` 与 `skill` 子命令注册及命令文件 + +**Files:** +- Modify: `internal/cli/root.go:102-116`(AddCommand 块) +- Delete: `internal/cli/init.go`, `internal/cli/init_test.go`, `internal/cli/skill.go`, `internal/cli/skill_test.go` + +**Interfaces:** +- Consumes: 无(这是删除任务,移除的是命令注册与实现) +- Produces: `internal/cli` 包不再导出 `newInitCmd`/`newSkillCmd`;后续任务可安全删除 `agents`/`skillscheck`/`bundle`(它们的消费者本任务已移除) + +- [ ] **Step 1: 移除 root.go 里的两个注册行** + +对 `internal/cli/root.go` 做一处 Edit,把 AddCommand 块中的 `newSkillCmd()` 与 `newInitCmd()` 两行删掉,保留 `newConfigCmd(g)`: + +old_string: +``` + newAnalyzeCmd(g), + newSkillCmd(), + newConfigCmd(g), + newInitCmd(), + newVersionCmd(), +``` +new_string: +``` + newAnalyzeCmd(g), + newConfigCmd(g), + newVersionCmd(), +``` + +- [ ] **Step 2: 删除命令文件与测试** + +Run: +```bash +git rm internal/cli/init.go internal/cli/init_test.go internal/cli/skill.go internal/cli/skill_test.go +``` +Expected: 四个文件被删除,`git status` 显示 `deleted:`。 + +- [ ] **Step 3: 验证编译** + +Run: `go build ./...` +Expected: 成功,无输出。`internal/agents`/`internal/skillscheck`/`bundle` 此时仍存在但已无消费者,自身可独立编译。 + +- [ ] **Step 4: 验证测试** + +Run: `go test ./...` +Expected: 全绿。`config_cmd_test.go` 里的 `config init` 测试不受影响(config 子命令保留)。 + +- [ ] **Step 5: 验证静态检查** + +Run: `go vet ./...` +Expected: 无告警。 + +- [ ] **Step 6: Commit** + +```bash +git add internal/cli/root.go +git commit -m "refactor(cli): remove init and skill subcommands + +Drops mysql-cli init (agents-based install) and the skill subcommand +group (list/version/check/install). Registration removed from root.go. +agents/skillscheck/bundle now have no consumers and will be deleted in +follow-up tasks. Skill install is delegated to npx skills add (see +spec 2026-07-27-skill-install-npx-skills-design)." +``` + +--- + +### Task 2: 删除 `internal/agents` 包 + +**Files:** +- Delete: `internal/agents/`(整个目录:agents.go, detect.go, install.go, merge.go, agents_test.go, detect_test.go, install_test.go, merge_test.go, testutil_test.go) + +**Interfaces:** +- Consumes: 无(Task 1 已移除唯一消费者 `init.go`) +- Produces: 无(纯删除) + +- [ ] **Step 1: 删除整个包** + +Run: +```bash +git rm -r internal/agents +``` +Expected: 目录及全部 9 个文件被删除。 + +- [ ] **Step 2: 验证无悬空引用** + +Run: `grep -rn "internal/agents" --include="*.go" . | grep -v '.claude/worktrees'` +Expected: 无输出(无任何 Go 文件再 import `internal/agents`)。 + +- [ ] **Step 3: 验证编译与测试** + +Run: `go build ./... && go vet ./... && go test ./...` +Expected: 全绿。 + +- [ ] **Step 4: Commit** + +```bash +git commit -m "refactor(agents): delete self-built agent install package + +internal/agents (7-agent detect/install/merge) is obsolete after +delegating skill install to npx skills add. No remaining consumers." +``` + +--- + +### Task 3: 删除 `internal/skillscheck` 包 + +**Files:** +- Delete: `internal/skillscheck/`(skillscheck.go, skillscheck_test.go) + +**Interfaces:** +- Consumes: 无(Task 1 已移除唯一消费者 `skill.go`) +- Produces: 无 + +- [ ] **Step 1: 删除整个包** + +Run: +```bash +git rm -r internal/skillscheck +``` +Expected: 目录及 2 个文件被删除。 + +- [ ] **Step 2: 验证无悬空引用** + +Run: `grep -rn "skillscheck" --include="*.go" . | grep -v '.claude/worktrees'` +Expected: 无输出。 + +- [ ] **Step 3: 验证编译与测试** + +Run: `go build ./... && go vet ./... && go test ./...` +Expected: 全绿。 + +- [ ] **Step 4: Commit** + +```bash +git commit -m "refactor(skillscheck): delete bundled-version sync check + +skillscheck compared installed skills against the binary-embedded +version. With skills no longer embedded and install delegated to +npx skills (which provides its own update/list), this package is +obsolete." +``` + +--- + +### Task 4: 删除 `bundle.go`(根包内嵌) + +**Files:** +- Delete: `bundle.go` + +**Interfaces:** +- Consumes: 无(Task 1 已移除消费者 `init.go`/`skill.go`;Task 3 已移除 `skillscheck`) +- Produces: 根目录不再有 Go 文件;`go build ./...` 会跳过根目录(无 `.go` 文件),正常构建 `cmd/` 与 `internal/` + +- [ ] **Step 1: 验证无悬空引用** + +Run: `grep -rn 'AllenMuu/mysql-cli"' --include="*.go" . | grep -v '.claude/worktrees' | grep -v _test.go` +Expected: 无输出(无生产代码再 import 根包 `bundle`)。 + +- [ ] **Step 2: 删除 bundle.go** + +Run: +```bash +git rm bundle.go +``` +Expected: 文件被删除。 + +- [ ] **Step 3: 验证编译与测试** + +Run: `go build ./... && go vet ./... && go test ./...` +Expected: 全绿。根目录无 `.go` 文件,`go build ./...` 跳过根目录,构建 `cmd/mysql-cli` 与 `internal/...`。 + +- [ ] **Step 4: Commit** + +```bash +git commit -m "refactor(bundle): delete embedded skills tree + +//go:embed skills was the install source for mysql-cli skill install. +With install delegated to npx skills add (source = GitHub repo), the +embedded tree is dead weight. Skill version truth moves to repo +skills/*/SKILL.md frontmatter." +``` + +--- + +### Task 5: 删除 `scripts/install-skills.sh` 及其自测,修正 version.go 注释 + +**Files:** +- Delete: `scripts/install-skills.sh`, `scripts/install-skills-test.sh` +- Modify: `internal/cli/version.go:18-20` + +**Interfaces:** +- Consumes: 无 +- Produces: 无 + +- [ ] **Step 1: 删除安装脚本** + +Run: +```bash +git rm scripts/install-skills.sh scripts/install-skills-test.sh +``` +Expected: 两个文件被删除。 + +- [ ] **Step 2: 修正 version.go 注释** + +对 `internal/cli/version.go` 做一处 Edit,去掉对已删除的 `skill version` 的提及: + +old_string: +``` +// newVersionCmd is the top-level `version` subcommand: it prints the binary +// version. This is distinct from `mysql-cli skill version`, which prints the +// versions of the bundled skills. +``` +new_string: +``` +// newVersionCmd is the top-level `version` subcommand: it prints the binary +// version (injected at release build time via GoReleaser ldflags). +``` + +- [ ] **Step 3: 验证编译与测试** + +Run: `go build ./... && go vet ./... && go test ./...` +Expected: 全绿。 + +- [ ] **Step 4: 验证 skill-format-check 仍可用** + +Run: `./scripts/skill-format-check.sh skills/` +Expected: 通过(skill-format-check 脚本本身保留,不依赖被删的 install-skills.sh)。 + +- [ ] **Step 5: Commit** + +```bash +git add internal/cli/version.go +git commit -m "chore(scripts): drop install-skills.sh + fix version.go comment + +Removes the shell-based installer (replaced by npx skills add). Drops +the stale reference to mysql-cli skill version in version.go comment." +``` + +--- + +### Task 6: 新增 `.well-known/agent-skills/index.json` + +**Files:** +- Create: `.well-known/agent-skills/index.json` + +**Interfaces:** +- Consumes: vercel-labs/skills 的发现逻辑(`add.ts` 优先读此文件) +- Produces: `npx skills add AllenMuu/mysql-cli` 能列出/发现 3 个 skill + +- [ ] **Step 1: 创建 index.json** + +写入 `.well-known/agent-skills/index.json`: + +```json +{ + "skills": [ + { + "name": "mysql-shared", + "description": "mysql-cli shared rules: config & datasource, global flags, output formats, error recovery, safety model, stable exit codes. Required by mysql-query and mysql-schema; install all three together.", + "path": "skills/mysql-shared/SKILL.md" + }, + { + "name": "mysql-query", + "description": "Run SQL with mysql-cli: SELECT, DML (INSERT/UPDATE/DELETE), DDL, multi-statement transactions. Read-only by default, JSON output, stable exit codes, tiered write gates.", + "path": "skills/mysql-query/SKILL.md" + }, + { + "name": "mysql-schema", + "description": "Explore MySQL schema with mysql-cli: tables, databases, sample, read, analyze. Read-only discovery.", + "path": "skills/mysql-schema/SKILL.md" + } + ] +} +``` + +> 注:字段名基于 vercel-labs/skills 惯例推断。若 Step 2 的 `--list` 不能识别,查 `vercel-labs/skills` 的 `src/add.ts` 对 `.well-known/agent-skills/index.json` 的解析逻辑调整字段名。即使此文件不被识别,vercel-labs/skills 仍会回退到默认 `skills/*/SKILL.md` 扫描,3 个 skill 仍可安装。 + +- [ ] **Step 2: 验证 vercel-labs/skills 能发现 3 个 skill** + +Run: `npx skills add AllenMuu/mysql-cli --list` +Expected: 列出 `mysql-shared`、`mysql-query`、`mysql-schema` 三个 skill(可能附带 description)。 + +若未列出:回退验证默认扫描是否工作 -- `npx skills add AllenMuu/mysql-cli --skill '*' -a claude-code -y --dry-run`(若有 dry-run)或观察 `--list` 是否走默认扫描。若默认扫描也不列出,检查仓库 `skills/*/SKILL.md` 结构是否完整(应已完整)。 + +- [ ] **Step 3: Commit** + +```bash +git add .well-known/agent-skills/index.json +git commit -m "feat(skills): add .well-known/agent-skills index + +Declares the 3 mysql-cli skills for vercel-labs/skills' preferred +discovery mechanism. Hints that all three should be installed together +(mysql-query/schema reference ../mysql-shared/SKILL.md)." +``` + +--- + +### Task 7: 重写 README.md 安装说明 + +**Files:** +- Modify: `README.md`(line 55-58, 120-140, 161, 310-325 等所有 `mysql-cli init`/`skill install`/`skill list`/`skill version`/`skill check`/`install-skills.sh` 引用) + +**Interfaces:** +- Consumes: 无 +- Produces: 用户文档对齐生态接入 + +- [ ] **Step 1: 替换安装说明段落** + +定位 README.md 中 line 120-140 附近的安装说明段落(标题大致为 "Option 0 - `mysql-cli init` (recommended...)" 起到 `skill install`/`install-skills.sh` 示例结束),用以下内容整体替换: + +```markdown +## Install Skills (AI Agents) + +mysql-cli ships skills for AI agents (Claude Code, Cursor, Codex, and 70+ more) +via the [vercel-labs/skills](https://github.com/vercel-labs/skills) ecosystem. + +```bash +npx skills add AllenMuu/mysql-cli +``` + +This opens an interactive picker: select agents, choose scope (project +`.//skills/` or global `~//skills/`), choose install method +(symlink recommended), and confirm. + +Non-interactive (CI / agents): + +```bash +npx skills add AllenMuu/mysql-cli --skill '*' -a claude-code -g -y +``` + +> **Install all three skills** (`mysql-shared`, `mysql-query`, `mysql-schema`). +> `mysql-query` and `mysql-schema` reference `../mysql-shared/SKILL.md`; installing +> only one breaks the shared-rules reference. + +**No Node.js?** Manually copy the `skills/` directory from this repo into your +agent's skill directory (e.g. `~/.claude/skills/`). +``` + +- [ ] **Step 2: 替换 Quick Start 里的 init 引用** + +README.md line 55-58 附近,把 `mysql-cli init # installs agent skills...` 一行及"Then run `mysql-cli init` to install skills"一句,改为: + +```markdown +npx skills add AllenMuu/mysql-cli # installs agent skills (interactive) +``` + +并删去/改写"Then run `mysql-cli init` to install skills"为"Then run `npx skills add AllenMuu/mysql-cli` to install skills"。 + +- [ ] **Step 3: 替换 Agent 路径表与 skill 子命令表** + +README.md line 310-316 的 agent 路径表里,把"Install"列的 `./scripts/install-skills.sh --agent ` 与 `mysql-cli skill install` 全部改为 `npx skills add AllenMuu/mysql-cli -a `(对 Cursor/Codex/OpenCode/Copilot/Windsurf/Aider 同理,agent 名用 vercel-labs/skills 的命名,如 `claude-code`/`cursor`/`codex`/`opencode`/`github-copilot`/`windsurf`/`aider`)。 + +README.md line 322-325 的 skill 子命令表(`mysql-cli skill list/version/check/install`)整段删除(这些子命令已不存在)。 + +- [ ] **Step 4: 清理残留引用** + +Run: `grep -n "install-skills\|mysql-cli init\|mysql-cli skill" README.md` +Expected: 无输出。若有残留,逐处替换为对应的 `npx skills add` 表述或删除。 + +- [ ] **Step 5: Commit** + +```bash +git add README.md +git commit -m "docs(readme): switch skill install docs to npx skills add + +Replaces mysql-cli init / skill install / install-skills.sh references +with npx skills add AllenMuu/mysql-cli. Notes the all-three install +requirement and the no-Node manual-copy fallback." +``` + +--- + +### Task 8: 重写 README-zh.md 安装说明 + +**Files:** +- Modify: `README-zh.md`(line 110-118, 139, 282-297 等引用) + +**Interfaces:** +- Consumes: 无 +- Produces: 中文文档对齐 + +- [ ] **Step 1: 替换安装说明段落** + +定位 README-zh.md line 110-118 附近的安装说明,用以下内容整体替换: + +```markdown +## 安装 Skill(AI Agent) + +mysql-cli 通过 [vercel-labs/skills](https://github.com/vercel-labs/skills) 生态为 AI agent(Claude Code、Cursor、Codex 等 70+ 种)提供 skill。 + +```bash +npx skills add AllenMuu/mysql-cli +``` + +会打开交互式选择:选 agent、选 scope(project `.//skills/` 或 global `~//skills/`)、选安装方式(推荐 symlink)、确认。 + +非交互(CI / agent): + +```bash +npx skills add AllenMuu/mysql-cli --skill '*' -a claude-code -g -y +``` + +> **务必安装全部 3 个 skill**(`mysql-shared`、`mysql-query`、`mysql-schema`)。 +> `mysql-query` 与 `mysql-schema` 顶部引用 `../mysql-shared/SKILL.md`,只装单个会导致引用断裂。 + +**无 Node.js?** 手动把仓库 `skills/` 目录复制到 agent 的 skill 目录(如 `~/.claude/skills/`)。 +``` + +- [ ] **Step 2: 替换 agent 路径表与 skill 子命令表** + +README-zh.md line 282-288 的 agent 路径表"安装"列改为 `npx skills add AllenMuu/mysql-cli -a `;line 294-297 的 skill 子命令表整段删除。 + +- [ ] **Step 3: 清理残留引用** + +Run: `grep -n "install-skills\|mysql-cli init\|mysql-cli skill" README-zh.md` +Expected: 无输出。若有残留逐处替换。 + +- [ ] **Step 4: Commit** + +```bash +git add README-zh.md +git commit -m "docs(readme-zh): 切换 skill 安装文档到 npx skills add" +``` + +--- + +### Task 9: 重写 AGENTS.md 的 Skill 体系章节 + +**Files:** +- Modify: `AGENTS.md`(line 52 的 `bundle` 条目 + "## Skill 体系"整节) + +**Interfaces:** +- Consumes: 无 +- Produces: 开发者文档对齐 + +- [ ] **Step 1: 移除架构图里的 bundle 条目** + +AGENTS.md line 52 附近,删除 `bundle` 那一条: +``` +- **`bundle`**(根包,`bundle.go`)- `//go:embed skills` 把 skill 定义嵌入二进制,是 `mysql-cli skill install` 零依赖安装的单一来源(与 `scripts/install-skills.sh` 共享 `skills/` 目录)。 +``` +同时移除架构图(line 33-41 附近)里 `cli(skill 子命令)─-> skillscheck ─-> bundle` 这一行依赖关系。 + +- [ ] **Step 2: 重写"## Skill 体系(对接 AI agent)"整节** + +把该节(从 `## Skill 体系` 到文件末尾或下一 `##` 之前)替换为: + +```markdown +## Skill 体系(对接 AI agent) + +mysql-cli 的 skill 不再自研安装,而是接入 [vercel-labs/skills](https://github.com/vercel-labs/skills) 生态。skill 是仓库侧资产,由通用 `skills` 包管理器安装到 75+ agent。 + +- **skill 文件**:`skills/mysql-{shared,query,schema}/SKILL.md`。`mysql-shared` 承载配置/安全模型/退出码/错误自修复,被 `mysql-query`/`mysql-schema` 顶部 `MUST Read` 引用(auto-load,DRY)。 +- **安装**:`npx skills add AllenMuu/mysql-cli`(交互式选 agent/scope/install method);非交互 `npx skills add AllenMuu/mysql-cli --skill '*' -a -g -y`。**务必全装 3 个 skill**,否则 `mysql-shared` 引用断裂。 +- **发现机制**:仓库根 `.well-known/agent-skills/index.json` 声明 3 skill(vercel-labs/skills 首选);不加也可走默认 `skills/*/SKILL.md` 扫描。 +- **格式校验**:`scripts/skill-format-check.sh` 校验 SKILL.md frontmatter(name/version/description/metadata + semver),CI `.github/workflows/skill-format-check.yml` PR 时强制。改 skill 后本地跑一遍。 +- **版本真相源**:skill 版本 = 仓库 `skills/*/SKILL.md` frontmatter 的 `version` 字段(不再二进制内嵌)。 +- **无 Node fallback**:手动复制仓库 `skills/` 目录到 agent skill 目录。 +``` + +- [ ] **Step 3: 清理残留引用** + +Run: `grep -n "install-skills\|mysql-cli init\|mysql-cli skill\|skillscheck\|bundle.go\|internal/agents" AGENTS.md` +Expected: 无输出(或仅剩新章节里无引用的说明文字)。逐处确认替换。 + +- [ ] **Step 4: Commit** + +```bash +git add AGENTS.md +git commit -m "docs(agents): rewrite skill section for npx skills ecosystem + +Removes bundle/agents/skillscheck/install-skills.sh references. Documents +npx skills add as the install path, .well-known discovery, all-three +install requirement, and repo frontmatter as version truth." +``` + +--- + +### Task 10: CHANGELOG.md 加 breaking 条目 + +**Files:** +- Modify: `CHANGELOG.md`(`[Unreleased] > ### Breaking` 段) + +**Interfaces:** +- Consumes: 无 +- Produces: 无 + +- [ ] **Step 1: 在 Breaking 段追加条目** + +在 `CHANGELOG.md` 的 `## [Unreleased]` > `### Breaking` 列表末尾追加: + +```markdown +- **skill 安装迁移至 vercel-labs/skills 生态**:`mysql-cli init`、`mysql-cli skill install/list/version/check` 及 `scripts/install-skills.sh` 全部移除。改用 `npx skills add AllenMuu/mysql-cli` 安装 skill(支持 75+ agent,交互式选 agent/scope/install method)。无 Node 环境可手动复制仓库 `skills/` 目录。skill 版本真相源从二进制内嵌迁移至仓库 `skills/*/SKILL.md` frontmatter。 +``` + +- [ ] **Step 2: Commit** + +```bash +git add CHANGELOG.md +git commit -m "docs(changelog): note skill install migration breaking change" +``` + +--- + +### Task 11: 最终验证 + +**Files:** +- 无修改,仅验证 + +**Interfaces:** +- Consumes: Task 1-10 全部完成 +- Produces: 改造完成的可发布状态 + +- [ ] **Step 1: 全量编译/静态检查/测试** + +Run: `go build ./... && go vet ./... && go test ./...` +Expected: 全绿。 + +- [ ] **Step 2: 覆盖率** + +Run: `go test -cover ./...` +Expected: 总覆盖率 ≥80%(删除的测试与生产代码等量,覆盖率应维持)。 + +- [ ] **Step 3: skill frontmatter 校验** + +Run: `./scripts/skill-format-check.sh skills/` +Expected: 通过。 + +- [ ] **Step 4: 端到端安装实测** + +Run: `npx skills add AllenMuu/mysql-cli --skill '*' -a claude-code -y` +Expected: 安装成功,3 个 skill 落到 `~/.claude/skills/`(或 `.agents/skills/` + symlink)。验证 `~/.claude/skills/mysql-shared/SKILL.md`、`mysql-query/SKILL.md`、`mysql-schema/SKILL.md` 均存在。 + +- [ ] **Step 5: 旧命令确认已移除** + +Run: `./mysql-cli skill install 2>&1; ./mysql-cli init 2>&1` +Expected: 报 `unknown command "skill"`/`unknown command "init"`(或 cobra 的等效错误)。 + +- [ ] **Step 6: 残留引用全局扫描** + +Run: `grep -rn "install-skills\|mysql-cli init\|mysql-cli skill\|internal/agents\|skillscheck\|bundle.go\|SkillNames\|SkillFile\|SkillsFS" --include="*.go" --include="*.md" --include="*.sh" . | grep -v '.claude/worktrees' | grep -v 'docs/superpowers/'` +Expected: 无输出(`docs/superpowers/` 下的 spec/plan 自身引用不算)。 + +- [ ] **Step 7: 若全部通过,无需额外 commit**(本任务纯验证)。若 Step 6 发现残留,回到对应任务修复后再 commit。 From 4668a3c50e5651ee6a6bfd5e0e3c9fae9af22a1e Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 14:55:48 +0800 Subject: [PATCH 03/14] refactor(cli): remove init and skill subcommands Drops mysql-cli init (agents-based install) and the skill subcommand group (list/version/check/install). Registration removed from root.go. agents/skillscheck/bundle now have no consumers and will be deleted in follow-up tasks. Skill install is delegated to npx skills add (see spec 2026-07-27-skill-install-npx-skills-design). --- internal/cli/errors.go | 4 - internal/cli/init.go | 125 ------------------ internal/cli/init_test.go | 251 ------------------------------------- internal/cli/root.go | 3 - internal/cli/skill.go | 176 -------------------------- internal/cli/skill_test.go | 59 --------- 6 files changed, 618 deletions(-) delete mode 100644 internal/cli/init.go delete mode 100644 internal/cli/init_test.go delete mode 100644 internal/cli/skill.go delete mode 100644 internal/cli/skill_test.go diff --git a/internal/cli/errors.go b/internal/cli/errors.go index 2c37bd7..6a4bc6c 100644 --- a/internal/cli/errors.go +++ b/internal/cli/errors.go @@ -13,8 +13,6 @@ import ( // mapError translates a core error into an exit code. func mapError(err error) int { switch { - case errors.Is(err, ErrInitAllFailed): - return ExitInitFailed case errors.Is(err, safety.ErrReadonlyViolation): return ExitReadonlyViolation case errors.Is(err, safety.ErrDDLRequiresWrite): @@ -69,8 +67,6 @@ func errorCodeName(code int) string { return "QUERY_TIMEOUT" case ExitConfigError: return "CONFIG_ERROR" - case ExitInitFailed: - return "INIT_FAILED" } return "UNKNOWN" } diff --git a/internal/cli/init.go b/internal/cli/init.go deleted file mode 100644 index 5acf9db..0000000 --- a/internal/cli/init.go +++ /dev/null @@ -1,125 +0,0 @@ -package cli - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "os" - "strings" - - bundle "github.com/AllenMuu/mysql-cli" - "github.com/AllenMuu/mysql-cli/internal/agents" - "github.com/spf13/cobra" -) - -// ErrInitAllFailed is returned when every selected agent install failed. -var ErrInitAllFailed = errors.New("all agent skill installs failed") - -func newInitCmd() *cobra.Command { - c := &cobra.Command{ - Use: "init", - Short: "Install bundled skills into detected AI agents", - Long: "Detect installed AI agents (Claude Code, Cursor, Codex, OpenCode, " + - "Copilot, Windsurf, Aider) and install the bundled mysql-cli skills " + - "into each in the agent's native format. Idempotent and re-runnable.", - Args: cobra.NoArgs, - } - c.Flags().String("agent", "auto", "agent selection: auto|all|comma list (claude,cursor,...)") - c.Flags().String("project-dir", "", "project root for project-level install") - c.Flags().Bool("no-global", false, "skip global install") - c.Flags().Bool("dry-run", false, "report without writing files") - c.Flags().BoolP("json", "j", false, "emit JSON instead of text") - - c.RunE = func(cmd *cobra.Command, args []string) error { - agentSel, _ := cmd.Flags().GetString("agent") - projectDir, _ := cmd.Flags().GetString("project-dir") - noGlobal, _ := cmd.Flags().GetBool("no-global") - dryRun, _ := cmd.Flags().GetBool("dry-run") - asJSON, _ := cmd.Flags().GetBool("json") - - home, err := os.UserHomeDir() - if err != nil || home == "" { - home, _ = os.Getwd() - } - fsys, err := bundle.SkillsFS() - if err != nil { - return err - } - names, err := bundle.SkillNames() - if err != nil { - return err - } - opts := agents.Options{ - Home: home, - ProjectDir: projectDir, - NoGlobal: noGlobal, - DryRun: dryRun, - FS: fsys, - Names: names, - } - results := agents.Run(agentSel, opts) - - var emitErr error - if asJSON { - emitErr = emitInitJSON(cmd.OutOrStdout(), results) - } else { - emitInitText(cmd.OutOrStdout(), results) - } - if emitErr != nil { - return emitErr - } - if allFailed(results) { - return ErrInitAllFailed - } - return nil - } - return c -} - -func allFailed(results []agents.InstallResult) bool { - if len(results) == 0 { - return false - } - for _, r := range results { - if r.Status != "error" { - return false - } - } - return true -} - -func emitInitText(w io.Writer, results []agents.InstallResult) { - fmt.Fprintln(w, "🔧 mysql-cli skill init") - for _, r := range results { - switch r.Status { - case "installed": - fmt.Fprintf(w, " ✅ %-10s %s\n", r.Agent, strings.Join(r.Paths, ", ")) - case "skipped": - fmt.Fprintf(w, " ⏭️ %-10s %s\n", r.Agent, r.Error) - case "error": - fmt.Fprintf(w, " ❌ %-10s %s\n", r.Agent, r.Error) - } - } -} - -func emitInitJSON(w io.Writer, results []agents.InstallResult) error { - type envelope struct { - Success bool `json:"success"` - Data map[string]any `json:"data"` - Error string `json:"error"` - } - env := envelope{ - Success: !allFailed(results), - Data: map[string]any{"agents": results}, - } - if !env.Success { - env.Error = "all agent skill installs failed" - } - out, err := json.MarshalIndent(env, "", " ") - if err != nil { - return err - } - fmt.Fprintln(w, string(out)) - return nil -} diff --git a/internal/cli/init_test.go b/internal/cli/init_test.go deleted file mode 100644 index defbdab..0000000 --- a/internal/cli/init_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package cli - -import ( - "bytes" - "encoding/json" - "path/filepath" - "testing" - - "github.com/AllenMuu/mysql-cli/internal/agents" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func runInit(t *testing.T, home string, args ...string) (int, string) { - t.Helper() - var buf bytes.Buffer - g := &Globals{Format: "json", out: &buf} - root := newRootCmd(g) - // isolate HOME so detection is deterministic - t.Setenv("HOME", home) - root.SetArgs(append([]string{"init"}, args...)) - code := ExitOK - if err := root.Execute(); err != nil { - code = mapError(err) - } - return code, buf.String() -} - -func TestInit_AutoDefaultClaude_JSON(t *testing.T) { - home := t.TempDir() - code, out := runInit(t, home, "-j") - require.Equal(t, ExitOK, code) - var env struct { - Success bool `json:"success"` - Data struct { - Agents []struct { - Agent string `json:"name"` - Status string `json:"status"` - } `json:"agents"` - } `json:"data"` - } - require.NoError(t, json.Unmarshal([]byte(out), &env)) - assert.True(t, env.Success) - require.Len(t, env.Data.Agents, 1) - assert.Equal(t, "claude", env.Data.Agents[0].Agent) - assert.Equal(t, "installed", env.Data.Agents[0].Status) - assert.FileExists(t, filepath.Join(home, ".claude", "skills", "mysql-shared", "SKILL.md")) -} - -func TestInit_DryRun_NoFiles(t *testing.T) { - home := t.TempDir() - code, _ := runInit(t, home, "--dry-run") - assert.Equal(t, ExitOK, code) - assert.NoDirExists(t, filepath.Join(home, ".claude")) -} - -func TestInit_NoGlobal_WithProjectDir(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - code, _ := runInit(t, home, "--agent", "claude", "--project-dir", proj, "--no-global") - assert.Equal(t, ExitOK, code) - assert.NoDirExists(t, filepath.Join(home, ".claude")) - assert.FileExists(t, filepath.Join(proj, ".claude", "skills", "mysql-query", "SKILL.md")) -} - -func TestInit_UnknownAgent_ExitNonZero(t *testing.T) { - home := t.TempDir() - code, _ := runInit(t, home, "--agent", "nope") - // unknown agent -> Install returns status "error" -> all failed -> ExitInitFailed - assert.Equal(t, ExitInitFailed, code) -} - -func TestInit_TextOutput(t *testing.T) { - home := t.TempDir() - code, out := runInit(t, home) - assert.Equal(t, ExitOK, code) - assert.Contains(t, out, "mysql-cli skill init") - assert.Contains(t, out, "claude") -} - -func TestAllFailed(t *testing.T) { - assert.False(t, allFailed(nil), "empty results should not be all-failed") - assert.False(t, allFailed([]agents.InstallResult{ - {Agent: "claude", Status: "installed"}, - {Agent: "cursor", Status: "error"}, - }), "mixed results should not be all-failed") - assert.False(t, allFailed([]agents.InstallResult{ - {Agent: "claude", Status: "skipped"}, - }), "skipped-only should not be all-failed") - assert.True(t, allFailed([]agents.InstallResult{ - {Agent: "nope", Status: "error", Error: "unknown agent"}, - }), "all-error should be all-failed") - assert.True(t, allFailed([]agents.InstallResult{ - {Agent: "a", Status: "error"}, - {Agent: "b", Status: "error"}, - }), "multiple all-error should be all-failed") -} - -func TestEmitInitText_AllStatuses(t *testing.T) { - var buf bytes.Buffer - emitInitText(&buf, []agents.InstallResult{ - {Agent: "claude", Status: "installed", Paths: []string{"/a/SKILL.md"}}, - {Agent: "cursor", Status: "skipped", Error: "project-only"}, - {Agent: "nope", Status: "error", Error: "unknown agent"}, - }) - out := buf.String() - assert.Contains(t, out, "mysql-cli skill init") - assert.Contains(t, out, "claude") - assert.Contains(t, out, "/a/SKILL.md") - assert.Contains(t, out, "cursor") - assert.Contains(t, out, "project-only") - assert.Contains(t, out, "nope") - assert.Contains(t, out, "unknown agent") -} - -func TestEmitInitJSON_AllFailed(t *testing.T) { - var buf bytes.Buffer - results := []agents.InstallResult{ - {Agent: "nope", Status: "error", Error: "unknown agent"}, - } - require.NoError(t, emitInitJSON(&buf, results)) - var env struct { - Success bool `json:"success"` - Error string `json:"error"` - } - require.NoError(t, json.Unmarshal(buf.Bytes(), &env)) - assert.False(t, env.Success) - assert.Equal(t, "all agent skill installs failed", env.Error) -} - -func TestEmitInitJSON_Success(t *testing.T) { - var buf bytes.Buffer - results := []agents.InstallResult{ - {Agent: "claude", Status: "installed", Paths: []string{"/a/SKILL.md"}}, - } - require.NoError(t, emitInitJSON(&buf, results)) - var env struct { - Success bool `json:"success"` - } - require.NoError(t, json.Unmarshal(buf.Bytes(), &env)) - assert.True(t, env.Success) -} - -func TestInit_AllAgents_SomeSkipped(t *testing.T) { - home := t.TempDir() - // --agent all includes project-only agents (cursor, codex, etc.) which - // return "skipped" without --project-dir; claude/aider still install. - code, out := runInit(t, home, "--agent", "all", "-j") - assert.Equal(t, ExitOK, code, "at least claude+aider install -> not all failed") - var env struct { - Data struct { - Agents []struct { - Agent string `json:"name"` - Status string `json:"status"` - } `json:"agents"` - } `json:"data"` - } - require.NoError(t, json.Unmarshal([]byte(out), &env)) - assert.GreaterOrEqual(t, len(env.Data.Agents), 1) - // at least claude should be installed - var hasInstalled bool - for _, a := range env.Data.Agents { - if a.Agent == "claude" && a.Status == "installed" { - hasInstalled = true - } - } - assert.True(t, hasInstalled, "claude should install under --agent all") -} - -func TestInit_ErrorCodeMapping(t *testing.T) { - // mapError -> ExitInitFailed (already exercised by UnknownAgent test, - // but assert the mapping directly). - assert.Equal(t, ExitInitFailed, mapError(ErrInitAllFailed)) - // errorCodeName covers the new INIT_FAILED case. - assert.Equal(t, "INIT_FAILED", errorCodeName(ExitInitFailed)) - // formatErr renders the new code in both formats. - jsonOut := formatErr(ErrInitAllFailed, "json") - assert.Contains(t, jsonOut, `"code":"INIT_FAILED"`) - assert.Contains(t, jsonOut, ErrInitAllFailed.Error()) - textOut := formatErr(ErrInitAllFailed, "table") - assert.Contains(t, textOut, "Error [INIT_FAILED]") - assert.Contains(t, textOut, ErrInitAllFailed.Error()) -} - -func TestInit_ProjectDirOnly_NoGlobal(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - // --no-global with --project-dir and --agent claude installs to proj only. - code, out := runInit(t, home, "--agent", "claude", "--project-dir", proj, "--no-global", "-j") - assert.Equal(t, ExitOK, code) - var env struct { - Success bool `json:"success"` - } - require.NoError(t, json.Unmarshal([]byte(out), &env)) - assert.True(t, env.Success) - assert.NoDirExists(t, filepath.Join(home, ".claude")) - assert.FileExists(t, filepath.Join(proj, ".claude", "skills", "mysql-shared", "SKILL.md")) -} - -func TestInit_InvalidGlobalFormat(t *testing.T) { - home := t.TempDir() - var buf bytes.Buffer - g := &Globals{Format: "json", out: &buf} - root := newRootCmd(g) - // override after newRootCmd so StringVarP default doesn't clobber it - g.Format = "invalid" - t.Setenv("HOME", home) - root.SetArgs([]string{"init", "-j"}) - code := ExitOK - if err := root.Execute(); err != nil { - code = mapError(err) - } - // invalid format -> PersistentPreRunE error -> ExitConfigError - assert.Equal(t, ExitConfigError, code) -} - -func TestInit_InvalidTimeout(t *testing.T) { - home := t.TempDir() - var buf bytes.Buffer - g := &Globals{Format: "json", out: &buf} - root := newRootCmd(g) - g.Timeout = "not-a-duration" - t.Setenv("HOME", home) - root.SetArgs([]string{"init", "-j"}) - code := ExitOK - if err := root.Execute(); err != nil { - code = mapError(err) - } - // invalid timeout -> PersistentPreRunE error -> ExitConfigError - assert.Equal(t, ExitConfigError, code) -} - -func TestInit_HomeFallback(t *testing.T) { - // Empty HOME triggers os.UserHomeDir error -> fallback to os.Getwd(). - // Use --dry-run so no files are written to the cwd. - t.Setenv("HOME", "") - var buf bytes.Buffer - g := &Globals{Format: "json", out: &buf} - root := newRootCmd(g) - root.SetArgs([]string{"init", "--dry-run", "-j"}) - code := ExitOK - if err := root.Execute(); err != nil { - code = mapError(err) - } - assert.Equal(t, ExitOK, code) - var env struct { - Success bool `json:"success"` - } - require.NoError(t, json.Unmarshal(buf.Bytes(), &env)) - assert.True(t, env.Success) -} diff --git a/internal/cli/root.go b/internal/cli/root.go index 58147f7..37c80cb 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -25,7 +25,6 @@ const ( ExitSQLError = 8 ExitQueryTimeout = 9 ExitConfigError = 10 - ExitInitFailed = 11 ) // Globals carries parsed global flags shared by all subcommands. @@ -109,9 +108,7 @@ func newRootCmd(g *Globals) *cobra.Command { newReadCmd(g), newExploreCmd(g), newAnalyzeCmd(g), - newSkillCmd(), newConfigCmd(g), - newInitCmd(), newVersionCmd(), ) // No subcommand -> interactive REPL (human debug; not the agent path). diff --git a/internal/cli/skill.go b/internal/cli/skill.go deleted file mode 100644 index 37300fb..0000000 --- a/internal/cli/skill.go +++ /dev/null @@ -1,176 +0,0 @@ -package cli - -import ( - "encoding/json" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - - bundle "github.com/AllenMuu/mysql-cli" - "github.com/AllenMuu/mysql-cli/internal/skillscheck" - "github.com/spf13/cobra" -) - -// newSkillCmd groups subcommands for managing the AI-agent skills bundled -// into the binary. It never touches a database. -func newSkillCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "skill", - Short: "Manage bundled AI-agent skills (list/check/install/version)", - } - cmd.AddCommand( - newSkillListCmd(), - newSkillVersionCmd(), - newSkillCheckCmd(), - newSkillInstallCmd(), - ) - return cmd -} - -func newSkillListCmd() *cobra.Command { - return &cobra.Command{ - Use: "list", - Short: "List skills bundled with this binary", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return printSkillVersions(cmd.OutOrStdout()) - }, - } -} - -func newSkillVersionCmd() *cobra.Command { - return &cobra.Command{ - Use: "version", - Short: "Print expected skill versions", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return printSkillVersions(cmd.OutOrStdout()) - }, - } -} - -func printSkillVersions(w io.Writer) error { - names, err := bundle.SkillNames() - if err != nil { - return err - } - for _, n := range names { - data, err := bundle.SkillFile(n) - if err != nil { - return err - } - fmt.Fprintf(w, "%s\t%s\n", n, skillscheck.ParseVersion(string(data))) - } - return nil -} - -func newSkillCheckCmd() *cobra.Command { - c := &cobra.Command{ - Use: "check [target-dir]", - Short: "Check installed skills against bundled versions", - Long: "Check installed skills under target-dir (default ~/.claude/skills) " + - "against the versions bundled with this binary. Emits a report and " + - "always exits 0; parse the JSON status field for programmatic use.", - Args: cobra.MaximumNArgs(1), - } - c.Flags().BoolP("json", "j", false, "emit JSON instead of a text table") - c.RunE = func(cmd *cobra.Command, args []string) error { - target := defaultSkillsTarget() - if len(args) == 1 { - target = args[0] - } - results, err := skillscheck.Check(target) - if err != nil { - return err - } - asJSON, _ := cmd.Flags().GetBool("json") - if asJSON { - out, err := json.MarshalIndent(results, "", " ") - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), string(out)) - return nil - } - w := cmd.OutOrStdout() - for _, r := range results { - ver := r.InstalledVer - if ver == "" { - ver = "-" - } - fmt.Fprintf(w, "%-16s %-8s installed=%-8s expected=%-8s %s\n", - r.Skill, r.Status, ver, r.ExpectedVer, r.Path) - } - return nil - } - return c -} - -func newSkillInstallCmd() *cobra.Command { - c := &cobra.Command{ - Use: "install [target-dir]", - Short: "Install bundled skills into target-dir (default ~/.claude/skills)", - Args: cobra.MaximumNArgs(1), - } - c.RunE = func(cmd *cobra.Command, args []string) error { - target := defaultSkillsTarget() - if len(args) == 1 { - target = args[0] - } - n, err := installSkills(target) - if err != nil { - return err - } - fmt.Fprintf(cmd.OutOrStdout(), "installed %d skill(s) into %s\n", n, target) - return nil - } - return c -} - -func defaultSkillsTarget() string { - home, err := os.UserHomeDir() - if err != nil || home == "" { - return filepath.Join(".claude", "skills") - } - return filepath.Join(home, ".claude", "skills") -} - -// installSkills copies every bundled skill into target, preserving the -// skills//... layout. Existing files are overwritten. -func installSkills(target string) (int, error) { - fsys, err := bundle.SkillsFS() - if err != nil { - return 0, err - } - names, err := bundle.SkillNames() - if err != nil { - return 0, err - } - if err := os.MkdirAll(target, 0o755); err != nil { - return 0, err - } - count := 0 - for _, name := range names { - err := fs.WalkDir(fsys, name, func(p string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - dst := filepath.Join(target, p) - if d.IsDir() { - return os.MkdirAll(dst, 0o755) - } - data, err := fs.ReadFile(fsys, p) - if err != nil { - return err - } - return os.WriteFile(dst, data, 0o644) - }) - if err != nil { - return count, err - } - count++ - } - return count, nil -} diff --git a/internal/cli/skill_test.go b/internal/cli/skill_test.go deleted file mode 100644 index adc300e..0000000 --- a/internal/cli/skill_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package cli - -import ( - "bytes" - "os" - "path/filepath" - "testing" - - "github.com/AllenMuu/mysql-cli/internal/skillscheck" - "github.com/stretchr/testify/assert" -) - -func TestPrintSkillVersions(t *testing.T) { - var buf bytes.Buffer - assert.NoError(t, printSkillVersions(&buf)) - out := buf.String() - for _, s := range []string{"mysql-shared", "mysql-query", "mysql-schema", "1.0.0"} { - assert.Contains(t, out, s) - } -} - -func TestInstallSkillsWritesFiles(t *testing.T) { - dir := t.TempDir() - n, err := installSkills(dir) - assert.NoError(t, err) - assert.GreaterOrEqual(t, n, 3) - for _, s := range []string{"mysql-shared", "mysql-query", "mysql-schema"} { - _, err := os.Stat(filepath.Join(dir, s, "SKILL.md")) - assert.NoError(t, err, "missing %s/SKILL.md", s) - } -} - -func TestInstallThenCheckAllOK(t *testing.T) { - dir := t.TempDir() - _, err := installSkills(dir) - assert.NoError(t, err) - results, err := skillscheck.Check(dir) - assert.NoError(t, err) - for _, r := range results { - assert.Equal(t, skillscheck.StatusOK, r.Status, "%s: %s", r.Skill, r.Status) - } -} - -func TestSkillListRunExitZero(t *testing.T) { - assert.Equal(t, ExitOK, Run([]string{"skill", "list"})) -} - -func TestSkillInstallRunExitZero(t *testing.T) { - dir := t.TempDir() - assert.Equal(t, ExitOK, Run([]string{"skill", "install", dir})) - _, err := os.Stat(filepath.Join(dir, "mysql-shared", "SKILL.md")) - assert.NoError(t, err) -} - -func TestSkillCheckRunExitZero(t *testing.T) { - dir := t.TempDir() - // Empty dir: every skill is missing, but check still exits 0. - assert.Equal(t, ExitOK, Run([]string{"skill", "check", dir})) -} From f87d9fdb9163fa1b19855a4b56f3c258273d7484 Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 15:02:47 +0800 Subject: [PATCH 04/14] refactor(agents): delete self-built agent install package internal/agents (7-agent detect/install/merge) is obsolete after delegating skill install to npx skills add. No remaining consumers. --- internal/agents/agents.go | 117 --------------- internal/agents/agents_test.go | 95 ------------ internal/agents/detect.go | 45 ------ internal/agents/detect_test.go | 68 --------- internal/agents/install.go | 242 ------------------------------- internal/agents/install_test.go | 164 --------------------- internal/agents/merge.go | 111 -------------- internal/agents/merge_test.go | 57 -------- internal/agents/testutil_test.go | 13 -- 9 files changed, 912 deletions(-) delete mode 100644 internal/agents/agents.go delete mode 100644 internal/agents/agents_test.go delete mode 100644 internal/agents/detect.go delete mode 100644 internal/agents/detect_test.go delete mode 100644 internal/agents/install.go delete mode 100644 internal/agents/install_test.go delete mode 100644 internal/agents/merge.go delete mode 100644 internal/agents/merge_test.go delete mode 100644 internal/agents/testutil_test.go diff --git a/internal/agents/agents.go b/internal/agents/agents.go deleted file mode 100644 index 90cd246..0000000 --- a/internal/agents/agents.go +++ /dev/null @@ -1,117 +0,0 @@ -// Package agents installs bundled mysql-cli skills into AI agents in each -// agent's native format. It is the Go port of scripts/install-skills.sh and -// depends only on the standard library; skill content is injected via fs.FS. -package agents - -import ( - "errors" - "fmt" - "strings" -) - -// Agent name constants. -const ( - Claude = "claude" - Cursor = "cursor" - Codex = "codex" - OpenCode = "opencode" - Copilot = "copilot" - Windsurf = "windsurf" - Aider = "aider" -) - -// AllAgents is the canonical ordered list, matching install-skills.sh. -var AllAgents = []string{Claude, Cursor, Codex, OpenCode, Copilot, Windsurf, Aider} - -// Selection constants for Run. -const ( - SelAuto = "auto" - SelAll = "all" -) - -// ValidAgent reports whether name is a known agent. -func ValidAgent(name string) bool { - for _, a := range AllAgents { - if a == name { - return true - } - } - return false -} - -// Install installs all skills for one agent per opts. -func Install(agent string, opts Options) InstallResult { - r := InstallResult{Agent: agent} - installers := map[string]func(Options) ([]string, error){ - Claude: installClaude, - Cursor: installCursor, - Codex: installCodex, - OpenCode: installOpenCode, - Copilot: installCopilot, - Windsurf: installWindsurf, - Aider: installAider, - } - fn, ok := installers[agent] - if !ok { - r.Status = "error" - r.Error = fmt.Sprintf("unknown agent %q", agent) - return r - } - paths, err := fn(opts) - r.Paths = paths - switch { - case err == nil && len(paths) > 0: - r.Status = "installed" - case err == nil && len(paths) == 0: - r.Status = "skipped" - case errors.Is(err, ErrProjectOnly): - r.Status = "skipped" - r.Error = err.Error() - default: - r.Status = "error" - r.Error = err.Error() - } - return r -} - -// parseList splits a comma-separated agent selection, trimming whitespace. -func parseList(sel string) []string { - var out []string - for _, p := range strings.Split(sel, ",") { - p = strings.TrimSpace(p) - if p != "" { - out = append(out, p) - } - } - return out -} - -// Run resolves the agent selection and installs skills for each. For SelAuto, -// agents are detected via Detect; if none detected, defaults to Claude -// (matching install-skills.sh). Detected reflects actual presence. -func Run(sel string, opts Options) []InstallResult { - present := Detect(opts.Home, opts.ProjectDir) - presentSet := map[string]bool{} - for _, a := range present { - presentSet[a] = true - } - var targets []string - switch sel { - case SelAll: - targets = append([]string(nil), AllAgents...) - case SelAuto: - targets = present - if len(targets) == 0 { - targets = []string{Claude} - } - default: - targets = parseList(sel) - } - results := make([]InstallResult, 0, len(targets)) - for _, a := range targets { - r := Install(a, opts) - r.Detected = presentSet[a] - results = append(results, r) - } - return results -} diff --git a/internal/agents/agents_test.go b/internal/agents/agents_test.go deleted file mode 100644 index e23b5b8..0000000 --- a/internal/agents/agents_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package agents - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestValidAgent(t *testing.T) { - assert.True(t, ValidAgent("claude")) - assert.False(t, ValidAgent("nope")) -} - -func TestInstall_UnknownAgent(t *testing.T) { - r := Install("nope", baseOpts(t.TempDir(), "")) - assert.Equal(t, "error", r.Status) - assert.Contains(t, r.Error, "unknown agent") -} - -func TestInstall_CopilotSkippedWithoutProjectDir(t *testing.T) { - r := Install(Copilot, baseOpts(t.TempDir(), "")) - assert.Equal(t, "skipped", r.Status) -} - -func TestInstall_ClaudeSuccess(t *testing.T) { - r := Install(Claude, baseOpts(t.TempDir(), "")) - assert.Equal(t, "installed", r.Status) - assert.NotEmpty(t, r.Paths) -} - -func TestRun_All(t *testing.T) { - home := t.TempDir() - requireDir(t, home, ".claude") // Claude present -> Detected must be true - res := Run(SelAll, baseOpts(home, "")) - assert.Len(t, res, len(AllAgents)) - // No --project-dir: copilot is project-only -> skipped; cursor has no - // ~/.cursor and no project dir -> writes nothing -> skipped. The rest - // install globally (non-empty paths). - for _, r := range res { - if r.Agent == Copilot || r.Agent == Cursor { - assert.Equal(t, "skipped", r.Status, r.Agent) - } else { - assert.Equal(t, "installed", r.Status, r.Agent) - } - } - // Lock the Detected field in SelAll mode: presentSet is consulted for - // every result, not just SelAuto. Claude is present (via ~/.claude); - // Copilot is not (no .github anywhere). If r.Detected = presentSet[a] - // were moved inside case SelAuto, claudeResult.Detected would be false. - var claudeResult, copilotResult *InstallResult - for i := range res { - switch res[i].Agent { - case Claude: - claudeResult = &res[i] - case Copilot: - copilotResult = &res[i] - } - } - assert.True(t, claudeResult.Detected, "Claude must be detected when ~/.claude exists") - assert.False(t, copilotResult.Detected, "Copilot must not be detected without .github") -} - -func TestRun_AutoDefaultsToClaudeWhenNoneDetected(t *testing.T) { - res := Run(SelAuto, baseOpts(t.TempDir(), t.TempDir())) - assert.Len(t, res, 1) - assert.Equal(t, Claude, res[0].Agent) - assert.False(t, res[0].Detected) // not actually present -} - -func TestRun_AutoDetectsPresent(t *testing.T) { - home := t.TempDir() - requireDir(t, home, ".claude") - res := Run(SelAuto, baseOpts(home, t.TempDir())) - assert.Len(t, res, 1) - assert.Equal(t, Claude, res[0].Agent) - assert.True(t, res[0].Detected) -} - -func TestRun_CommaList(t *testing.T) { - home := t.TempDir() - requireDir(t, home, ".claude") // Claude present, Cursor not -> lock Detected in comma-list mode - res := Run("claude,cursor", baseOpts(home, t.TempDir())) - assert.Len(t, res, 2) - assert.Equal(t, Claude, res[0].Agent) - assert.Equal(t, Cursor, res[1].Agent) - // Lock Detected in comma-list mode: presentSet is consulted per result - // regardless of selection mode. If r.Detected = presentSet[a] were - // moved inside case SelAuto, res[0].Detected would be false here. - assert.True(t, res[0].Detected, "Claude must be detected when ~/.claude exists") - assert.False(t, res[1].Detected, "Cursor must not be detected without ~/.cursor") -} - -func TestParseList(t *testing.T) { - assert.Equal(t, []string{Claude, Cursor}, parseList("claude, cursor ")) -} diff --git a/internal/agents/detect.go b/internal/agents/detect.go deleted file mode 100644 index 2039438..0000000 --- a/internal/agents/detect.go +++ /dev/null @@ -1,45 +0,0 @@ -package agents - -import ( - "os" - "path/filepath" -) - -// Detect returns the agents present on the system, mirroring -// install-skills.sh detect_agents(). projectDir may be "". -func Detect(home, projectDir string) []string { - var found []string - any := func(paths ...string) bool { - for _, p := range paths { - if p == "" { - continue - } - if _, err := os.Stat(p); err == nil { - return true - } - } - return false - } - if any(filepath.Join(home, ".claude"), filepath.Join(projectDir, ".claude")) { - found = append(found, Claude) - } - if any(filepath.Join(home, ".cursor"), filepath.Join(projectDir, ".cursor")) { - found = append(found, Cursor) - } - if any(filepath.Join(home, ".codex"), filepath.Join(projectDir, "AGENTS.md")) { - found = append(found, Codex) - } - if any(filepath.Join(home, ".config", "opencode"), filepath.Join(projectDir, ".opencode")) { - found = append(found, OpenCode) - } - if any(filepath.Join(projectDir, ".github")) { - found = append(found, Copilot) - } - if any(filepath.Join(projectDir, ".windsurfrules"), filepath.Join(home, ".codeium"), filepath.Join(home, ".windsurf")) { - found = append(found, Windsurf) - } - if any(filepath.Join(projectDir, ".aider.conf.yml"), filepath.Join(home, ".aider.conf.yml")) { - found = append(found, Aider) - } - return found -} diff --git a/internal/agents/detect_test.go b/internal/agents/detect_test.go deleted file mode 100644 index d247dce..0000000 --- a/internal/agents/detect_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package agents - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDetect_None(t *testing.T) { - tmp := t.TempDir() - assert.Empty(t, Detect(tmp, t.TempDir())) -} - -func TestDetect_ClaudeGlobal(t *testing.T) { - home := t.TempDir() - requireDir(t, home, ".claude") - assert.Equal(t, []string{Claude}, Detect(home, t.TempDir())) -} - -func TestDetect_CopilotProjectOnly(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - requireDir(t, proj, ".github") - got := Detect(home, proj) - assert.Contains(t, got, Copilot) - assert.NotContains(t, got, Claude) -} - -func TestDetect_AiderGlobal(t *testing.T) { - home := t.TempDir() - requireFile(t, home, ".aider.conf.yml", "read: [.aider.instructions.md]\n") - got := Detect(home, t.TempDir()) - assert.Contains(t, got, Aider) -} - -func TestDetect_AllPresent(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - requireDir(t, home, ".claude") - requireDir(t, home, ".cursor") - requireDir(t, home, ".codex") - requireDir(t, home, ".config/opencode") - requireDir(t, proj, ".github") - requireFile(t, proj, ".windsurfrules", "") - requireFile(t, home, ".aider.conf.yml", "") - got := Detect(home, proj) - assert.Equal(t, AllAgents, got) -} - -func requireDir(t *testing.T, parts ...string) { - t.Helper() - requireNoErr(t, os.MkdirAll(filepath.Join(parts...), 0o755)) -} -func requireFile(t *testing.T, parts ...string) { - t.Helper() - // last element is content, preceding elements are path components - path := filepath.Join(parts[:len(parts)-1]...) - content := parts[len(parts)-1] - requireNoErr(t, os.WriteFile(path, []byte(content), 0o644)) -} -func requireNoErr(t *testing.T, err error) { - t.Helper() - if err != nil { - t.Fatal(err) - } -} diff --git a/internal/agents/install.go b/internal/agents/install.go deleted file mode 100644 index 41ccab8..0000000 --- a/internal/agents/install.go +++ /dev/null @@ -1,242 +0,0 @@ -package agents - -import ( - "errors" - "io/fs" - "os" - "path/filepath" -) - -// ErrProjectOnly indicates an agent is project-only and --project-dir was not set. -var ErrProjectOnly = errors.New("agent is project-only; pass --project-dir") - -// Options controls install behavior. -type Options struct { - Home string // user home dir - ProjectDir string // project root ("" = no project-level install) - NoGlobal bool // skip global install - DryRun bool // report paths without writing - FS fs.FS // skills subtree (contains /SKILL.md) - Names []string // skill names to install -} - -// InstallResult is the outcome for one agent. -type InstallResult struct { - Agent string `json:"name"` - Detected bool `json:"detected"` - Paths []string `json:"paths"` - Status string `json:"status"` // installed | skipped | error - Error string `json:"error,omitempty"` -} - -// writeIfNotDryRun writes content to path, creating parent dirs. No-op on DryRun. -func writeIfNotDryRun(opts Options, path, content string) error { - if opts.DryRun { - return nil - } - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return err - } - return os.WriteFile(path, []byte(content), 0o644) -} - -// copySkillTree copies the embedded /... tree into dstDir, replacing any -// existing copy (idempotent). Mirrors `rm -rf; cp -r` in install_claude. -func copySkillTree(opts Options, dstDir, skill string) error { - if opts.DryRun { - return nil - } - if err := os.MkdirAll(dstDir, 0o755); err != nil { - return err - } - if err := os.RemoveAll(filepath.Join(dstDir, skill)); err != nil { - return err - } - return fs.WalkDir(opts.FS, skill, func(p string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - out := filepath.Join(dstDir, p) - if d.IsDir() { - return os.MkdirAll(out, 0o755) - } - data, err := fs.ReadFile(opts.FS, p) - if err != nil { - return err - } - return os.WriteFile(out, data, 0o644) - }) -} - -// installClaude copies skill trees to ~/.claude/skills and (if ProjectDir set) -// /.claude/skills. Mirrors install-skills.sh install_claude. -func installClaude(opts Options) ([]string, error) { - var paths []string - if opts.ProjectDir != "" { - t := filepath.Join(opts.ProjectDir, ".claude", "skills") - for _, s := range opts.Names { - if err := copySkillTree(opts, t, s); err != nil { - return paths, err - } - paths = append(paths, filepath.Join(t, s, "SKILL.md")) - } - } - if !opts.NoGlobal { - t := filepath.Join(opts.Home, ".claude", "skills") - for _, s := range opts.Names { - if err := copySkillTree(opts, t, s); err != nil { - return paths, err - } - paths = append(paths, filepath.Join(t, s, "SKILL.md")) - } - } - return paths, nil -} - -// installCursor writes .mdc rule files to /.cursor/rules and (if -// ~/.cursor exists) ~/.cursor/rules. Mirrors install-skills.sh install_cursor. -func installCursor(opts Options) ([]string, error) { - var paths []string - writeMDC := func(dir string) error { - for _, s := range opts.Names { - data, err := fs.ReadFile(opts.FS, s+"/SKILL.md") - if err != nil { - return err - } - p := filepath.Join(dir, s+".mdc") - if err := writeIfNotDryRun(opts, p, makeMDC(s, SkillBody(string(data)))); err != nil { - return err - } - paths = append(paths, p) - } - return nil - } - if opts.ProjectDir != "" { - if err := writeMDC(filepath.Join(opts.ProjectDir, ".cursor", "rules")); err != nil { - return paths, err - } - } - if !opts.NoGlobal { - gdir := filepath.Join(opts.Home, ".cursor") - if _, err := os.Stat(gdir); err == nil { // bash guard: only if ~/.cursor exists - if err := writeMDC(filepath.Join(gdir, "rules")); err != nil { - return paths, err - } - } - } - return paths, nil -} - -// writeMerged reads existing file at path (if any), replaces/appends the marked -// block with the merged skill bodies, and writes it back. No-op body on DryRun -// still records the path. Returns the path written. -func writeMerged(opts Options, path string) (string, error) { - merged, err := mergedBody(opts.FS, opts.Names) - if err != nil { - return path, err - } - existing, err := os.ReadFile(path) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return path, err - } - content := MergeInstructionFile(string(existing), merged) - if err := writeIfNotDryRun(opts, path, content); err != nil { - return path, err - } - return path, nil -} - -// installCodex writes the merged block to /AGENTS.md and ~/.codex/instructions.md. -func installCodex(opts Options) ([]string, error) { - var paths []string - if opts.ProjectDir != "" { - p, err := writeMerged(opts, filepath.Join(opts.ProjectDir, "AGENTS.md")) - paths = append(paths, p) - if err != nil { - return paths, err - } - } - if !opts.NoGlobal { - p, err := writeMerged(opts, filepath.Join(opts.Home, ".codex", "instructions.md")) - paths = append(paths, p) - if err != nil { - return paths, err - } - } - return paths, nil -} - -// installOpenCode writes the merged block to /.opencode/instructions.md -// and ~/.config/opencode/instructions.md. -func installOpenCode(opts Options) ([]string, error) { - var paths []string - if opts.ProjectDir != "" { - p, err := writeMerged(opts, filepath.Join(opts.ProjectDir, ".opencode", "instructions.md")) - paths = append(paths, p) - if err != nil { - return paths, err - } - } - if !opts.NoGlobal { - p, err := writeMerged(opts, filepath.Join(opts.Home, ".config", "opencode", "instructions.md")) - paths = append(paths, p) - if err != nil { - return paths, err - } - } - return paths, nil -} - -// installCopilot is project-only: writes /.github/copilot-instructions.md. -// Returns ErrProjectOnly if ProjectDir is empty. -func installCopilot(opts Options) ([]string, error) { - if opts.ProjectDir == "" { - return nil, ErrProjectOnly - } - p, err := writeMerged(opts, filepath.Join(opts.ProjectDir, ".github", "copilot-instructions.md")) - if err != nil { - return []string{p}, err - } - return []string{p}, nil -} - -// installWindsurf writes the merged block to /.windsurfrules and ~/.windsurfrules. -func installWindsurf(opts Options) ([]string, error) { - var paths []string - if opts.ProjectDir != "" { - p, err := writeMerged(opts, filepath.Join(opts.ProjectDir, ".windsurfrules")) - paths = append(paths, p) - if err != nil { - return paths, err - } - } - if !opts.NoGlobal { - p, err := writeMerged(opts, filepath.Join(opts.Home, ".windsurfrules")) - paths = append(paths, p) - if err != nil { - return paths, err - } - } - return paths, nil -} - -// installAider writes the merged block to /.aider.instructions.md and -// ~/.aider.instructions.md. (aider has a global install path, unlike copilot.) -func installAider(opts Options) ([]string, error) { - var paths []string - if opts.ProjectDir != "" { - p, err := writeMerged(opts, filepath.Join(opts.ProjectDir, ".aider.instructions.md")) - paths = append(paths, p) - if err != nil { - return paths, err - } - } - if !opts.NoGlobal { - p, err := writeMerged(opts, filepath.Join(opts.Home, ".aider.instructions.md")) - paths = append(paths, p) - if err != nil { - return paths, err - } - } - return paths, nil -} diff --git a/internal/agents/install_test.go b/internal/agents/install_test.go deleted file mode 100644 index fd41764..0000000 --- a/internal/agents/install_test.go +++ /dev/null @@ -1,164 +0,0 @@ -package agents - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func baseOpts(home, proj string) Options { - return Options{Home: home, ProjectDir: proj, FS: testFS(), Names: testNames} -} - -func TestInstallClaude_GlobalAndProject(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - paths, err := installClaude(baseOpts(home, proj)) - require.NoError(t, err) - for _, s := range testNames { - assert.FileExists(t, filepath.Join(home, ".claude", "skills", s, "SKILL.md")) - assert.FileExists(t, filepath.Join(proj, ".claude", "skills", s, "SKILL.md")) - } - assert.NotEmpty(t, paths) -} - -func TestInstallClaude_GlobalOnlyByDefault(t *testing.T) { - home := t.TempDir() - paths, err := installClaude(baseOpts(home, "")) - require.NoError(t, err) - assert.FileExists(t, filepath.Join(home, ".claude", "skills", "mysql-query", "SKILL.md")) - assert.NotEmpty(t, paths) // ProjectDir=="" short-circuits project install; no cwd check needed -} - -func TestInstallClaude_NoGlobal(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - o := baseOpts(home, proj) - o.NoGlobal = true - _, err := installClaude(o) - require.NoError(t, err) - assert.NoDirExists(t, filepath.Join(home, ".claude")) - assert.FileExists(t, filepath.Join(proj, ".claude", "skills", "mysql-shared", "SKILL.md")) -} - -func TestInstallClaude_DryRun(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - o := baseOpts(home, proj) - o.DryRun = true - paths, err := installClaude(o) - require.NoError(t, err) - // DryRun must report paths but write nothing. - assert.NotEmpty(t, paths) - assert.NoDirExists(t, filepath.Join(proj, ".claude")) - assert.NoDirExists(t, filepath.Join(home, ".claude")) -} - -func TestInstallCursor_MDCFiles(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - require.NoError(t, os.MkdirAll(filepath.Join(home, ".cursor"), 0o755)) - _, err := installCursor(baseOpts(home, proj)) - require.NoError(t, err) - body, _ := os.ReadFile(filepath.Join(proj, ".cursor", "rules", "mysql-query.mdc")) - assert.Contains(t, string(body), "description: Run SQL with mysql-cli") - assert.Contains(t, string(body), "globs: *.sql") - assert.FileExists(t, filepath.Join(home, ".cursor", "rules", "mysql-shared.mdc")) -} - -func TestInstallCursor_DryRun(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - // Create ~/.cursor so the global branch is entered (otherwise the - // existence guard would hide DryRun's effect on the global branch). - requireDir(t, home, ".cursor") - o := baseOpts(home, proj) - o.DryRun = true - paths, err := installCursor(o) - require.NoError(t, err) - // DryRun must report paths but write no .mdc files anywhere. - assert.NotEmpty(t, paths) - assert.NoDirExists(t, filepath.Join(proj, ".cursor", "rules")) - assert.NoDirExists(t, filepath.Join(home, ".cursor", "rules")) -} - -func TestInstallCursor_SkipsGlobalWhenCursorMissing(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - // Deliberately do NOT create ~/.cursor; NoGlobal=false. - o := baseOpts(home, proj) - paths, err := installCursor(o) - require.NoError(t, err) - // Project .mdc files must exist; global ~/.cursor/rules must not. - for _, s := range testNames { - assert.FileExists(t, filepath.Join(proj, ".cursor", "rules", s+".mdc")) - } - assert.NoDirExists(t, filepath.Join(home, ".cursor", "rules")) - assert.NotEmpty(t, paths) -} - -func TestInstallCursor_NoGlobal(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - // ~/.cursor exists but NoGlobal=true must still skip the global branch. - requireDir(t, home, ".cursor") - o := baseOpts(home, proj) - o.NoGlobal = true - paths, err := installCursor(o) - require.NoError(t, err) - for _, s := range testNames { - assert.FileExists(t, filepath.Join(proj, ".cursor", "rules", s+".mdc")) - } - assert.NoDirExists(t, filepath.Join(home, ".cursor", "rules")) - assert.NotEmpty(t, paths) -} - -func TestInstallCodex_MergesIdempotently(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - o := baseOpts(home, proj) - _, err := installCodex(o) - require.NoError(t, err) - p := filepath.Join(proj, "AGENTS.md") - first, _ := os.ReadFile(p) - // re-run: content stable - _, err = installCodex(o) - require.NoError(t, err) - second, _ := os.ReadFile(p) - assert.Equal(t, string(first), string(second)) - assert.Contains(t, string(first), beginMarker) - assert.Contains(t, string(first), "## mysql-cli skill: mysql-query") - assert.FileExists(t, filepath.Join(home, ".codex", "instructions.md")) -} - -func TestInstallCopilot_ProjectOnly_NeedsProjectDir(t *testing.T) { - home := t.TempDir() - _, err := installCopilot(baseOpts(home, "")) - assert.ErrorIs(t, err, ErrProjectOnly) -} - -func TestInstallCopilot_ProjectInstall(t *testing.T) { - proj := t.TempDir() - _, err := installCopilot(baseOpts(t.TempDir(), proj)) - require.NoError(t, err) - assert.FileExists(t, filepath.Join(proj, ".github", "copilot-instructions.md")) -} - -func TestInstallWindsurf_GlobalAndProject(t *testing.T) { - home := t.TempDir() - proj := t.TempDir() - _, err := installWindsurf(baseOpts(home, proj)) - require.NoError(t, err) - assert.FileExists(t, filepath.Join(home, ".windsurfrules")) - assert.FileExists(t, filepath.Join(proj, ".windsurfrules")) -} - -func TestInstallAider_GlobalWithoutProjectDir(t *testing.T) { - home := t.TempDir() - _, err := installAider(baseOpts(home, "")) - require.NoError(t, err) - assert.FileExists(t, filepath.Join(home, ".aider.instructions.md")) -} diff --git a/internal/agents/merge.go b/internal/agents/merge.go deleted file mode 100644 index 9c08a36..0000000 --- a/internal/agents/merge.go +++ /dev/null @@ -1,111 +0,0 @@ -package agents - -import ( - "fmt" - "io/fs" - "sort" - "strings" -) - -const ( - beginMarker = "" - endMarker = "" - updateNote = "" -) - -// cursorDescriptions mirrors the make_mdc description args in install-skills.sh. -var cursorDescriptions = map[string]string{ - "mysql-shared": "mysql-cli shared rules: config, datasource, safety model, exit codes, error recovery, output formats", - "mysql-query": "Run SQL with mysql-cli: SELECT query, txn, DML (INSERT/UPDATE/DELETE), DDL", - "mysql-schema": "Explore MySQL schema with mysql-cli: tables, databases, schema, sample, read, explore, analyze", -} - -// isFrontmatterDelim reports whether line is a "---" frontmatter delimiter -// (optional trailing whitespace), matching install-skills.sh /^---[[:space:]]*$/. -func isFrontmatterDelim(line string) bool { - return strings.TrimRight(line, " \t\r") == "---" -} - -// SkillBody returns the body of a SKILL.md: content after the second "---" -// frontmatter delimiter. Mirrors install-skills.sh skill_body(). -func SkillBody(content string) string { - lines := strings.Split(content, "\n") - for i, ln := range lines { - if isFrontmatterDelim(ln) { - // find the next delim after i - for j := i + 1; j < len(lines); j++ { - if isFrontmatterDelim(lines[j]) { - return strings.Join(lines[j+1:], "\n") - } - } - } - } - return "" -} - -// mergedBody concatenates all skill bodies in canonical (sorted) order, -// mirroring install-skills.sh skill_body_concat(). -func mergedBody(fsys fs.FS, names []string) (string, error) { - sorted := append([]string(nil), names...) - sort.Strings(sorted) - var b strings.Builder - for _, name := range sorted { - data, err := fs.ReadFile(fsys, name+"/SKILL.md") - if err != nil { - return "", fmt.Errorf("read skill %s: %w", name, err) - } - b.WriteString("\n## mysql-cli skill: ") - b.WriteString(name) - b.WriteString("\n\n") - b.WriteString(SkillBody(string(data))) - } - return b.String(), nil -} - -// stripMarkedBlock removes the begin..end marker block (inclusive) from content. -func stripMarkedBlock(content string) string { - lines := strings.Split(content, "\n") - var out []string - skip := false - for _, ln := range lines { - if ln == beginMarker { - skip = true - continue - } - if ln == endMarker && skip { - skip = false - continue - } - if !skip { - out = append(out, ln) - } - } - return strings.Join(out, "\n") -} - -// MergeInstructionFile returns instruction-file content after idempotently -// replacing the marked mysql-cli block with merged. Absent block => append. -// Properly idempotent (no whitespace accumulation; improves on the bash script). -func MergeInstructionFile(existing, merged string) string { - base := stripMarkedBlock(existing) - base = strings.TrimRight(base, "\n\r ") - merged = strings.TrimRight(merged, "\n\r ") - if base != "" { - base += "\n\n" - } - return base + beginMarker + "\n" + updateNote + "\n" + merged + "\n" + endMarker + "\n" -} - -// makeMDC renders a Cursor .mdc rule file from a skill name + SKILL.md body. -func makeMDC(skill, body string) string { - var b strings.Builder - b.WriteString("---\n") - b.WriteString("description: ") - b.WriteString(cursorDescriptions[skill]) - b.WriteString("\n") - b.WriteString("globs: *.sql\n") - b.WriteString("alwaysApply: false\n") - b.WriteString("---\n") - b.WriteString(body) - return b.String() -} diff --git a/internal/agents/merge_test.go b/internal/agents/merge_test.go deleted file mode 100644 index 6316528..0000000 --- a/internal/agents/merge_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package agents - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSkillBody(t *testing.T) { - in := "---\nname: x\nversion: 1.0.0\n---\n\nbody line 1\nbody line 2\n" - assert.Equal(t, "\nbody line 1\nbody line 2\n", SkillBody(in)) -} - -func TestSkillBody_NoFrontmatter(t *testing.T) { - assert.Equal(t, "", SkillBody("no delimiters here")) -} - -func TestMergedBody(t *testing.T) { - got, err := mergedBody(testFS(), testNames) - require.NoError(t, err) - // sorted order: mysql-query, mysql-schema, mysql-shared - assert.Contains(t, got, "## mysql-cli skill: mysql-query") - assert.Contains(t, got, "## mysql-cli skill: mysql-schema") - assert.Contains(t, got, "## mysql-cli skill: mysql-shared") - assert.Contains(t, got, "query body") - assert.Less(t, strings.Index(got, "mysql-query"), strings.Index(got, "mysql-schema")) -} - -func TestMergeInstructionFile_AppendWhenAbsent(t *testing.T) { - got := MergeInstructionFile("", "MERGED") - assert.Contains(t, got, beginMarker) - assert.Contains(t, got, endMarker) - assert.Contains(t, got, "MERGED") - assert.Contains(t, got, updateNote) -} - -func TestMergeInstructionFile_ReplacesExistingBlock(t *testing.T) { - existing := "user notes\n\n" + beginMarker + "\nold\n" + endMarker + "\n" - got := MergeInstructionFile(existing, "NEW") - assert.Contains(t, got, "user notes") - assert.Contains(t, got, "NEW") - assert.NotContains(t, got, "old") -} - -func TestMergeInstructionFile_Idempotent(t *testing.T) { - merged, _ := mergedBody(testFS(), testNames) - once := MergeInstructionFile("", merged) - twice := MergeInstructionFile(once, merged) - assert.Equal(t, once, twice, "re-running must not accumulate whitespace or duplicates") -} - -func TestMakeMDC(t *testing.T) { - got := makeMDC("mysql-query", "BODY") - assert.Equal(t, "---\ndescription: Run SQL with mysql-cli: SELECT query, txn, DML (INSERT/UPDATE/DELETE), DDL\nglobs: *.sql\nalwaysApply: false\n---\nBODY", got) -} diff --git a/internal/agents/testutil_test.go b/internal/agents/testutil_test.go deleted file mode 100644 index 465220a..0000000 --- a/internal/agents/testutil_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package agents - -import "testing/fstest" - -func testFS() fstest.MapFS { - return fstest.MapFS{ - "mysql-shared/SKILL.md": {Data: []byte("---\nname: mysql-shared\nversion: 1.0.0\n---\n\nshared body\n")}, - "mysql-query/SKILL.md": {Data: []byte("---\nname: mysql-query\nversion: 1.0.0\n---\n\nquery body\n")}, - "mysql-schema/SKILL.md": {Data: []byte("---\nname: mysql-schema\nversion: 1.0.0\n---\n\nschema body\n")}, - } -} - -var testNames = []string{"mysql-shared", "mysql-query", "mysql-schema"} From 50ef25db94ddf773b9006b8bfbd737ed2a82ee2e Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 15:23:19 +0800 Subject: [PATCH 05/14] refactor(skillscheck): delete bundled-version sync check skillscheck compared installed skills against the binary-embedded version. With skills no longer embedded and install delegated to npx skills (which provides its own update/list), this package is obsolete. --- internal/skillscheck/skillscheck.go | 84 ------------------------ internal/skillscheck/skillscheck_test.go | 74 --------------------- 2 files changed, 158 deletions(-) delete mode 100644 internal/skillscheck/skillscheck.go delete mode 100644 internal/skillscheck/skillscheck_test.go diff --git a/internal/skillscheck/skillscheck.go b/internal/skillscheck/skillscheck.go deleted file mode 100644 index ce1808b..0000000 --- a/internal/skillscheck/skillscheck.go +++ /dev/null @@ -1,84 +0,0 @@ -// Package skillscheck compares installed mysql-cli skills (e.g. under -// ~/.claude/skills) against the versions bundled into the binary, reporting -// missing or stale skills. It mirrors the skillscheck concept from -// larksuite/cli, adapted to mysql-cli's per-process, connection-less model: -// the check runs only when explicitly invoked via `mysql-cli skill check`, -// never on the hot query path. -package skillscheck - -import ( - "os" - "path/filepath" - "regexp" - - bundle "github.com/AllenMuu/mysql-cli" -) - -// Status values reported per skill. -const ( - StatusOK = "ok" // installed and version matches - StatusStale = "stale" // installed but version differs - StatusMissing = "missing" // not installed - StatusUnknown = "unknown" // installed but version unparseable -) - -var versionRe = regexp.MustCompile(`(?m)^version:\s*"?([0-9]+\.[0-9]+\.[0-9]+)`) - -// ParseVersion extracts a semver version from a SKILL.md frontmatter. -func ParseVersion(content string) string { - m := versionRe.FindStringSubmatch(content) - if len(m) >= 2 { - return m[1] - } - return "" -} - -// Result is the check outcome for one skill. -type Result struct { - Skill string `json:"skill"` - Installed bool `json:"installed"` - InstalledVer string `json:"installed_version,omitempty"` - ExpectedVer string `json:"expected_version"` - Status string `json:"status"` - Path string `json:"path"` -} - -// Check scans targetDir for each bundled skill and compares its installed -// version frontmatter against the bundled version. -func Check(targetDir string) ([]Result, error) { - names, err := bundle.SkillNames() - if err != nil { - return nil, err - } - results := make([]Result, 0, len(names)) - for _, name := range names { - r := Result{Skill: name, ExpectedVer: expectedVersion(name)} - r.Path = filepath.Join(targetDir, name, "SKILL.md") - data, err := os.ReadFile(r.Path) - if err != nil { - r.Status = StatusMissing - results = append(results, r) - continue - } - r.Installed = true - r.InstalledVer = ParseVersion(string(data)) - switch { - case r.InstalledVer == "": - r.Status = StatusUnknown - case r.ExpectedVer != "" && r.InstalledVer != r.ExpectedVer: - r.Status = StatusStale - default: - r.Status = StatusOK - } - results = append(results, r) - } - return results, nil -} - -func expectedVersion(skill string) string { - data, err := bundle.SkillFile(skill) - if err != nil { - return "" - } - return ParseVersion(string(data)) -} diff --git a/internal/skillscheck/skillscheck_test.go b/internal/skillscheck/skillscheck_test.go deleted file mode 100644 index df66a2f..0000000 --- a/internal/skillscheck/skillscheck_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package skillscheck - -import ( - "os" - "path/filepath" - "testing" - - bundle "github.com/AllenMuu/mysql-cli" -) - -func TestParseVersion(t *testing.T) { - cases := []struct{ in, want string }{ - {"---\nversion: 1.2.3\n---\n", "1.2.3"}, - {"---\nversion: \"0.9.0\"\n---\n", "0.9.0"}, - {"---\nname: x\nversion: 10.20.30\n---\n", "10.20.30"}, - {"---\nname: x\n---\n", ""}, - {"no frontmatter at all", ""}, - } - for _, c := range cases { - if got := ParseVersion(c.in); got != c.want { - t.Errorf("ParseVersion(%q) = %q, want %q", c.in, got, c.want) - } - } -} - -func TestCheck(t *testing.T) { - names, err := bundle.SkillNames() - if err != nil { - t.Fatalf("SkillNames: %v", err) - } - if len(names) < 3 { - t.Fatalf("need >=3 bundled skills, got %d", len(names)) - } - - dir := t.TempDir() - // names[0]: installed at expected version -> ok - // names[1]: installed at a bogus version -> stale - // names[2]: not installed -> missing - data0, err := bundle.SkillFile(names[0]) - if err != nil { - t.Fatalf("SkillFile: %v", err) - } - if err := os.MkdirAll(filepath.Join(dir, names[0]), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(dir, names[0], "SKILL.md"), data0, 0o644); err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(filepath.Join(dir, names[1]), 0o755); err != nil { - t.Fatal(err) - } - stale := "---\nname: " + names[1] + "\nversion: 0.0.1\ndescription: stale\nmetadata:\n binary: mysql-cli\n---\n# stale\n" - if err := os.WriteFile(filepath.Join(dir, names[1], "SKILL.md"), []byte(stale), 0o644); err != nil { - t.Fatal(err) - } - - results, err := Check(dir) - if err != nil { - t.Fatalf("Check: %v", err) - } - byName := map[string]Result{} - for _, r := range results { - byName[r.Skill] = r - } - if got := byName[names[0]].Status; got != StatusOK { - t.Errorf("names[0] status = %q, want %q", got, StatusOK) - } - if got := byName[names[1]].Status; got != StatusStale { - t.Errorf("names[1] status = %q, want %q", got, StatusStale) - } - if got := byName[names[2]].Status; got != StatusMissing { - t.Errorf("names[2] status = %q, want %q", got, StatusMissing) - } -} From 9138728d13d341b2dc426105927a18466b4b641e Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 15:26:36 +0800 Subject: [PATCH 06/14] refactor(bundle): delete embedded skills tree //go:embed skills was the install source for mysql-cli skill install. With install delegated to npx skills add (source = GitHub repo), the embedded tree is dead weight. Skill version truth moves to repo skills/*/SKILL.md frontmatter. --- bundle.go | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 bundle.go diff --git a/bundle.go b/bundle.go deleted file mode 100644 index c66590a..0000000 --- a/bundle.go +++ /dev/null @@ -1,42 +0,0 @@ -// Package bundle embeds the mysql-cli skill definitions (skills/*) into the -// binary so that `mysql-cli skill install` can install them with no external -// dependencies - no repo checkout, no package manager. The embedded tree is -// the single source of truth shared with scripts/install-skills.sh. -package bundle - -import ( - "embed" - "io/fs" - "sort" -) - -// Skills is the embedded skills/ directory tree. -// -//go:embed skills -var Skills embed.FS - -// SkillNames returns the sorted names of the embedded skill directories. -func SkillNames() ([]string, error) { - entries, err := Skills.ReadDir("skills") - if err != nil { - return nil, err - } - names := make([]string, 0, len(entries)) - for _, e := range entries { - if e.IsDir() { - names = append(names, e.Name()) - } - } - sort.Strings(names) - return names, nil -} - -// SkillFile returns the bytes of /SKILL.md. -func SkillFile(skill string) ([]byte, error) { - return Skills.ReadFile("skills/" + skill + "/SKILL.md") -} - -// SkillsFS returns the "skills" subtree, for walking during install. -func SkillsFS() (fs.FS, error) { - return fs.Sub(Skills, "skills") -} From 82941634bef28c8cfa43e74489bb2a59a63d6242 Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 15:28:53 +0800 Subject: [PATCH 07/14] chore(scripts): drop install-skills.sh + fix version.go comment Removes the shell-based installer (replaced by npx skills add). Drops the stale reference to mysql-cli skill version in version.go comment. --- internal/cli/version.go | 3 +- scripts/install-skills-test.sh | 86 ------------ scripts/install-skills.sh | 250 --------------------------------- 3 files changed, 1 insertion(+), 338 deletions(-) delete mode 100755 scripts/install-skills-test.sh delete mode 100755 scripts/install-skills.sh diff --git a/internal/cli/version.go b/internal/cli/version.go index 44fc266..b928519 100644 --- a/internal/cli/version.go +++ b/internal/cli/version.go @@ -16,8 +16,7 @@ import ( var version = "dev" // newVersionCmd is the top-level `version` subcommand: it prints the binary -// version. This is distinct from `mysql-cli skill version`, which prints the -// versions of the bundled skills. +// version (injected at release build time via GoReleaser ldflags). func newVersionCmd() *cobra.Command { return &cobra.Command{ Use: "version", diff --git a/scripts/install-skills-test.sh b/scripts/install-skills-test.sh deleted file mode 100755 index cd4c18c..0000000 --- a/scripts/install-skills-test.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# install-skills-test.sh - Tests for install-skills.sh (all 7 agents) -# ============================================================================= -# Verifies each agent installs the right files, the merged skill body is -# embedded, installation is idempotent, each --agent works standalone, -# and --no-global skips global paths. Uses a temp dir + temp HOME; never -# touches the real $HOME. -# ============================================================================= -set -o pipefail -HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SCRIPT="$HERE/install-skills.sh" -TMP="$(mktemp -d)" -trap 'rm -rf "$TMP"' EXIT - -pass=0; fail=0 -ok() { echo "PASS $1"; pass=$((pass + 1)); } -ko() { echo "FAIL $1"; fail=$((fail + 1)); } -assert_exists() { [[ -e "$1" ]] && ok "$2" || ko "$2 ($1 missing)"; } -assert_missing() { [[ ! -e "$1" ]] && ok "$2" || ko "$2 ($1 should not exist)"; } -assert_contains() { grep -qF "$2" "$1" 2>/dev/null && ok "$3" || ko "$3 ($1 missing content)"; } -assert_count() { - local n; n=$(grep -cF "$2" "$1" 2>/dev/null) || true - [[ "$n" == "$3" ]] && ok "$4 (count=$n)" || ko "$4 (count=$n, want $3)" -} - -# Run installer into a temp project (no global, to keep the real $HOME clean). -run() { bash "$SCRIPT" --agent "$1" --project-dir "$TMP/proj" --no-global >/dev/null 2>&1; } - -echo "== 1. all agents produce expected files ==" -run all -for s in mysql-shared mysql-query mysql-schema; do - assert_exists "$TMP/proj/.claude/skills/$s/SKILL.md" "claude $s" - assert_exists "$TMP/proj/.cursor/rules/$s.mdc" "cursor $s" - assert_contains "$TMP/proj/.cursor/rules/$s.mdc" "alwaysApply:" "cursor $s .mdc frontmatter" -done -assert_exists "$TMP/proj/AGENTS.md" "codex AGENTS.md" -assert_contains "$TMP/proj/AGENTS.md" "mysql-cli skill: begin" "codex marker" -assert_exists "$TMP/proj/.opencode/instructions.md" "opencode file" -assert_contains "$TMP/proj/.opencode/instructions.md" "mysql-cli skill: begin" "opencode marker" -assert_exists "$TMP/proj/.github/copilot-instructions.md" "copilot file" -assert_contains "$TMP/proj/.github/copilot-instructions.md" "mysql-cli skill: begin" "copilot marker" -assert_exists "$TMP/proj/.windsurfrules" "windsurf file" -assert_contains "$TMP/proj/.windsurfrules" "mysql-cli skill: begin" "windsurf marker" -assert_exists "$TMP/proj/.aider.instructions.md" "aider file" -assert_contains "$TMP/proj/.aider.instructions.md" "mysql-cli skill: begin" "aider marker" - -echo "" -echo "== 2. merged skill body is embedded ==" -assert_contains "$TMP/proj/AGENTS.md" "## mysql-cli skill: mysql-shared" "shared heading" -assert_contains "$TMP/proj/AGENTS.md" "## mysql-cli skill: mysql-query" "query heading" -assert_contains "$TMP/proj/AGENTS.md" "## mysql-cli skill: mysql-schema" "schema heading" -assert_contains "$TMP/proj/AGENTS.md" "READONLY_VIOLATION" "shared exit-code table" -assert_contains "$TMP/proj/AGENTS.md" "mysql-cli query" "query command ref" - -echo "" -echo "== 3. idempotent (re-run keeps one marker block) ==" -run all -for f in AGENTS.md .opencode/instructions.md .github/copilot-instructions.md .windsurfrules .aider.instructions.md; do - assert_count "$TMP/proj/$f" "mysql-cli skill: begin" 1 "idempotent $f" -done - -echo "" -echo "== 4. each --agent works standalone ==" -for agent in claude cursor codex opencode copilot windsurf aider; do - bash "$SCRIPT" --agent "$agent" --project-dir "$TMP/indiv" --no-global >/dev/null 2>&1 -done -assert_exists "$TMP/indiv/.claude/skills/mysql-shared/SKILL.md" "standalone claude" -assert_exists "$TMP/indiv/.cursor/rules/mysql-shared.mdc" "standalone cursor" -assert_exists "$TMP/indiv/AGENTS.md" "standalone codex" -assert_exists "$TMP/indiv/.opencode/instructions.md" "standalone opencode" -assert_exists "$TMP/indiv/.github/copilot-instructions.md" "standalone copilot" -assert_exists "$TMP/indiv/.windsurfrules" "standalone windsurf" -assert_exists "$TMP/indiv/.aider.instructions.md" "standalone aider" - -echo "" -echo "== 5. --no-global skips global; default writes global (temp HOME) ==" -rm -rf "$TMP/h1" "$TMP/h2" -HOME="$TMP/h1" bash "$SCRIPT" --agent codex --project-dir "$TMP/p1" --no-global >/dev/null 2>&1 -assert_missing "$TMP/h1/.codex/instructions.md" "no-global skips global codex" -HOME="$TMP/h2" bash "$SCRIPT" --agent codex --project-dir "$TMP/p2" >/dev/null 2>&1 -assert_exists "$TMP/h2/.codex/instructions.md" "default writes global codex" - -echo "" -echo "Results: $pass passed, $fail failed" -[[ "$fail" -eq 0 ]] && exit 0 || exit 1 diff --git a/scripts/install-skills.sh b/scripts/install-skills.sh deleted file mode 100755 index cd73281..0000000 --- a/scripts/install-skills.sh +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# mysql-cli Skill Installer - Multi-Agent Support -# ============================================================================= -# Installs mysql-cli skill definitions (mysql-shared / mysql-query / mysql-schema) -# for AI agents. Claude Code and Cursor use the native SKILL.md / .mdc formats; -# Codex, OpenCode, Copilot, Windsurf, and Aider receive the merged skill body -# appended (idempotently, between markers) to their instruction files. -# -# Usage: -# ./scripts/install-skills.sh [--agent ] [--project-dir ] [--no-global] -# -# Agents: -# auto - Auto-detect installed agents (default) -# claude - Claude Code (.claude/skills/ + ~/.claude/skills/) -# cursor - Cursor (.cursor/rules/*.mdc) -# codex - Codex CLI (AGENTS.md + ~/.codex/instructions.md) -# opencode - OpenCode (.opencode/instructions.md + ~/.config/opencode/instructions.md) -# copilot - GitHub Copilot (.github/copilot-instructions.md) -# windsurf - Windsurf (.windsurfrules + ~/.windsurfrules) -# aider - Aider (.aider.instructions.md; needs read: config - see hint) -# all - Install for every agent above -# -# Examples: -# ./scripts/install-skills.sh # auto-detect -# ./scripts/install-skills.sh --agent all --no-global # all agents, project only -# ./scripts/install-skills.sh --agent copilot --project-dir ~/my-project -# ============================================================================= - -set -euo pipefail -echo "⚠️ install-skills.sh is deprecated; use \`mysql-cli init\` instead." >&2 -echo " This script will be removed in a future release." >&2 -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -AGENT="auto" -PROJECT_DIR="" -NO_GLOBAL=0 - -SKILLS=(mysql-shared mysql-query mysql-schema) -BEGIN_MARKER="" -END_MARKER="" - -while [[ $# -gt 0 ]]; do - case "$1" in - --agent) AGENT="$2"; shift 2 ;; - --project-dir) PROJECT_DIR="$2"; shift 2 ;; - --no-global) NO_GLOBAL=1; shift ;; - -h|--help) sed -n '3,29p' "$0"; exit 0 ;; - *) echo "Unknown arg: $1" >&2; exit 1 ;; - esac -done - -if [[ -z "$PROJECT_DIR" ]]; then - PROJECT_DIR="$REPO_DIR" -fi - -# --- helpers ---------------------------------------------------------------- - -# Body of a SKILL.md = content after the second '---' frontmatter delimiter. -skill_body() { - awk 'BEGIN{c=0} /^---[[:space:]]*$/ {c++; if(c==2){f=1; next}} f' "$1" -} - -# Concatenate all three skill bodies with headings. -skill_body_concat() { - for skill in "${SKILLS[@]}"; do - echo "" - echo "## mysql-cli skill: ${skill}" - echo "" - skill_body "${REPO_DIR}/skills/${skill}/SKILL.md" - done -} - -# Write the merged skill body to an instruction file, idempotently. Re-running -# replaces the marked block in place instead of appending duplicates. -write_instruction_file() { - local file="$1" - mkdir -p "$(dirname "$file")" - if [[ -f "$file" ]] && grep -qF "$BEGIN_MARKER" "$file"; then - awk -v b="$BEGIN_MARKER" -v e="$END_MARKER" \ - '$0==b{f=1;next} $0==e{f=0;next} !f' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" - fi - { - echo "" - echo "$BEGIN_MARKER" - echo "" - skill_body_concat - echo "" - echo "$END_MARKER" - } >> "$file" -} - -# make_mdc -make_mdc() { - local src="$1" out="$2" desc="$3" - { - echo "---" - echo "description: $desc" - echo "globs: *.sql" - echo "alwaysApply: false" - echo "---" - skill_body "$src" - } > "$out" -} - -detect_agents() { - local found=() - [[ -d "$HOME/.claude" || -d "${PROJECT_DIR}/.claude" ]] && found+=("claude") - [[ -d "$HOME/.cursor" || -d "${PROJECT_DIR}/.cursor" ]] && found+=("cursor") - [[ -d "$HOME/.codex" || -f "${PROJECT_DIR}/AGENTS.md" ]] && found+=("codex") - [[ -d "$HOME/.config/opencode" || -d "${PROJECT_DIR}/.opencode" ]] && found+=("opencode") - [[ -d "${PROJECT_DIR}/.github" ]] && found+=("copilot") - [[ -f "${PROJECT_DIR}/.windsurfrules" || -d "$HOME/.codeium" || -d "$HOME/.windsurf" ]] && found+=("windsurf") - [[ -f "${PROJECT_DIR}/.aider.conf.yml" || -f "$HOME/.aider.conf.yml" ]] && found+=("aider") - echo "${found[@]}" -} - -# --- installers ------------------------------------------------------------- - -install_claude() { - local target="${PROJECT_DIR}/.claude/skills" - mkdir -p "$target" - for skill in "${SKILLS[@]}"; do - rm -rf "${target}/${skill}" - cp -r "${REPO_DIR}/skills/${skill}" "$target/" - done - echo " ✅ Claude Code (project): $target" - if [[ "$NO_GLOBAL" -eq 0 ]]; then - local gtarget="$HOME/.claude/skills" - mkdir -p "$gtarget" - for skill in "${SKILLS[@]}"; do - rm -rf "${gtarget}/${skill}" - cp -r "${REPO_DIR}/skills/${skill}" "$gtarget/" - done - echo " ✅ Claude Code (global): $gtarget" - fi -} - -install_cursor() { - local target="${PROJECT_DIR}/.cursor/rules" - mkdir -p "$target" - make_mdc "${REPO_DIR}/skills/mysql-shared/SKILL.md" "${target}/mysql-shared.mdc" \ - "mysql-cli shared rules: config, datasource, safety model, exit codes, error recovery, output formats" - make_mdc "${REPO_DIR}/skills/mysql-query/SKILL.md" "${target}/mysql-query.mdc" \ - "Run SQL with mysql-cli: SELECT query, txn, DML (INSERT/UPDATE/DELETE), DDL" - make_mdc "${REPO_DIR}/skills/mysql-schema/SKILL.md" "${target}/mysql-schema.mdc" \ - "Explore MySQL schema with mysql-cli: tables, databases, schema, sample, read, explore, analyze" - echo " ✅ Cursor (project): $target" - if [[ "$NO_GLOBAL" -eq 0 && -d "$HOME/.cursor" ]]; then - local gtarget="$HOME/.cursor/rules" - mkdir -p "$gtarget" - cp "${target}"/mysql-*.mdc "$gtarget/" - echo " ✅ Cursor (global): $gtarget" - fi -} - -install_codex() { - write_instruction_file "${PROJECT_DIR}/AGENTS.md" - echo " ✅ Codex CLI (project): ${PROJECT_DIR}/AGENTS.md" - if [[ "$NO_GLOBAL" -eq 0 ]]; then - mkdir -p "$HOME/.codex" - write_instruction_file "$HOME/.codex/instructions.md" - echo " ✅ Codex CLI (global): $HOME/.codex/instructions.md" - fi -} - -install_opencode() { - write_instruction_file "${PROJECT_DIR}/.opencode/instructions.md" - echo " ✅ OpenCode (project): ${PROJECT_DIR}/.opencode/instructions.md" - if [[ "$NO_GLOBAL" -eq 0 ]]; then - mkdir -p "$HOME/.config/opencode" - write_instruction_file "$HOME/.config/opencode/instructions.md" - echo " ✅ OpenCode (global): $HOME/.config/opencode/instructions.md" - fi - echo " ℹ️ OpenCode also reads AGENTS.md; adjust the path if your setup differs." -} - -install_copilot() { - write_instruction_file "${PROJECT_DIR}/.github/copilot-instructions.md" - echo " ✅ GitHub Copilot: ${PROJECT_DIR}/.github/copilot-instructions.md" - if [[ "$NO_GLOBAL" -eq 0 ]]; then - echo " ℹ️ Copilot has no global file; project-level only (per-repo)." - fi -} - -install_windsurf() { - write_instruction_file "${PROJECT_DIR}/.windsurfrules" - echo " ✅ Windsurf (project): ${PROJECT_DIR}/.windsurfrules" - if [[ "$NO_GLOBAL" -eq 0 ]]; then - write_instruction_file "$HOME/.windsurfrules" - echo " ✅ Windsurf (global): $HOME/.windsurfrules" - fi -} - -install_aider() { - local file="${PROJECT_DIR}/.aider.instructions.md" - write_instruction_file "$file" - echo " ✅ Aider (project): $file" - echo " ℹ️ Add to .aider.conf.yml: read: [.aider.instructions.md]" - echo " Or run: aider --read .aider.instructions.md" - if [[ "$NO_GLOBAL" -eq 0 ]]; then - local gfile="$HOME/.aider.instructions.md" - write_instruction_file "$gfile" - echo " ✅ Aider (global): $gfile" - echo " Add to ~/.aider.conf.yml: read: [~/.aider.instructions.md]" - fi -} - -# --- main ------------------------------------------------------------------- - -echo "🔧 mysql-cli Skill Installer" -echo " Agent: $AGENT" -echo " Project: $PROJECT_DIR" -echo "" - -run_install() { - case "$1" in - claude) install_claude ;; - cursor) install_cursor ;; - codex) install_codex ;; - opencode) install_opencode ;; - copilot) install_copilot ;; - windsurf) install_windsurf ;; - aider) install_aider ;; - esac -} - -case "$AGENT" in - auto) - detected="$(detect_agents)" - if [[ -z "$detected" ]]; then - echo " No agent detected; defaulting to Claude Code." - install_claude - else - for a in $detected; do run_install "$a"; done - fi - ;; - claude|cursor|codex|opencode|copilot|windsurf|aider) run_install "$AGENT" ;; - all) - for a in claude cursor codex opencode copilot windsurf aider; do run_install "$a"; done - ;; - *) - echo "❌ Unknown agent: $AGENT" >&2 - echo " Supported: auto, claude, cursor, codex, opencode, copilot, windsurf, aider, all" >&2 - exit 1 - ;; -esac - -echo "" -echo "✅ Done. Verify with: mysql-cli databases -f json" From edc977e5cf3326f09007e526b6654c63d35940d9 Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 15:32:28 +0800 Subject: [PATCH 08/14] feat(skills): add .well-known/agent-skills index Declares the 3 mysql-cli skills for vercel-labs/skills' preferred discovery mechanism. Hints that all three should be installed together (mysql-query/schema reference ../mysql-shared/SKILL.md). --- .well-known/agent-skills/index.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .well-known/agent-skills/index.json diff --git a/.well-known/agent-skills/index.json b/.well-known/agent-skills/index.json new file mode 100644 index 0000000..4fff0fc --- /dev/null +++ b/.well-known/agent-skills/index.json @@ -0,0 +1,19 @@ +{ + "skills": [ + { + "name": "mysql-shared", + "description": "mysql-cli shared rules: config & datasource, global flags, output formats, error recovery, safety model, stable exit codes. Required by mysql-query and mysql-schema; install all three together.", + "path": "skills/mysql-shared/SKILL.md" + }, + { + "name": "mysql-query", + "description": "Run SQL with mysql-cli: SELECT, DML (INSERT/UPDATE/DELETE), DDL, multi-statement transactions. Read-only by default, JSON output, stable exit codes, tiered write gates.", + "path": "skills/mysql-query/SKILL.md" + }, + { + "name": "mysql-schema", + "description": "Explore MySQL schema with mysql-cli: tables, databases, sample, read, analyze. Read-only discovery.", + "path": "skills/mysql-schema/SKILL.md" + } + ] +} \ No newline at end of file From ad8abfae30d457f092bde94b78336a6c2bdf3a1a Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 15:38:40 +0800 Subject: [PATCH 09/14] docs(readme): switch skill install docs to npx skills add Replaces mysql-cli init / skill install / install-skills.sh references with npx skills add AllenMuu/mysql-cli. Notes the all-three install requirement and the no-Node manual-copy fallback. --- README.md | 65 +++++++++++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 34f9add..306e8d2 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,10 @@ binary with **JSON by default** and **stable exit codes**, so any agent ```bash npx @allenmuu/mysql-cli install # installs the prebuilt binary to ~/.local/bin -mysql-cli init # installs agent skills into detected agents +npx skills add AllenMuu/mysql-cli # installs agent skills (interactive) ``` -The `npx` command downloads the prebuilt binary for your platform from GitHub Releases. Set `MYSQL_CLI_MIRROR` to use a download mirror. Then run `mysql-cli init` to install skills. +The `npx` command downloads the prebuilt binary for your platform from GitHub Releases. Set `MYSQL_CLI_MIRROR` to use a download mirror. Then run `npx skills add AllenMuu/mysql-cli` to install skills. **Option 2 - `go install`:** @@ -115,30 +115,29 @@ go install github.com/AllenMuu/mysql-cli/cmd/mysql-cli@latest **Step 2 - Install Agent Skills** -Choose one (all install the three skills). **Option 0 is the recommended approach**: - -**Option 0 - `mysql-cli init` (recommended, no repo clone needed):** +mysql-cli ships skills for AI agents (Claude Code, Cursor, Codex, and 70+ more) +via the [vercel-labs/skills](https://github.com/vercel-labs/skills) ecosystem. ```bash -mysql-cli init # auto-detect installed agents, install to global -mysql-cli init --agent all # install for all 7 agents -mysql-cli init --project-dir ~/my-project --no-global # project-level only -mysql-cli init -j # JSON output for agents +npx skills add AllenMuu/mysql-cli ``` -*Option A - installer script* (supports all agents below): +This opens an interactive picker: select agents, choose scope (project +`.//skills/` or global `~//skills/`), choose install method +(symlink recommended), and confirm. + +Non-interactive (CI / agents): ```bash -./scripts/install-skills.sh # auto-detect -./scripts/install-skills.sh --agent all --project-dir ~/my-project +npx skills add AllenMuu/mysql-cli --skill '*' -a claude-code -g -y ``` -*Option B - from the binary* (embeds skills, zero external deps): +> **Install all three skills** (`mysql-shared`, `mysql-query`, `mysql-schema`). +> `mysql-query` and `mysql-schema` reference `../mysql-shared/SKILL.md`; installing +> only one breaks the shared-rules reference. -```bash -mysql-cli skill install # -> ~/.claude/skills -mysql-cli skill install ~/my-project/.claude/skills -``` +**No Node.js?** Manually copy the `skills/` directory from this repo into your +agent's skill directory (e.g. `~/.claude/skills/`). **Step 3 - Configure a datasource** @@ -158,7 +157,6 @@ database = "app" **Step 4 - Verify & run** ```bash -mysql-cli skill check # confirm skills match the binary mysql-cli query "SELECT * FROM users LIMIT 10" # JSON by default ``` @@ -301,28 +299,19 @@ There are three skills, following the shared-skill pattern from `larksuite/cli`: ### Other agents `mysql-cli` works with **any agent that can run shell commands and parse -JSON**. The installer supports all seven agents below: Claude Code and Cursor -use the native SKILL.md / .mdc formats; the others receive the merged skill -body appended (idempotently) to their instruction files. +JSON**. The `npx skills add` installer (vercel-labs/skills) supports Claude +Code, Cursor, Codex, and 70+ more agents; it symlinks each skill into the +agent's skill directory. -| Agent | Config format | How to use `mysql-cli` | +| Agent | Config format | Install | | --- | --- | --- | -| **Claude Code** | `.claude/skills/*/SKILL.md` | `./scripts/install-skills.sh --agent claude` or `mysql-cli skill install` | -| **Cursor** | `.cursor/rules/*.mdc` | `./scripts/install-skills.sh --agent cursor` | -| **Codex CLI** | `AGENTS.md` | `./scripts/install-skills.sh --agent codex` | -| **OpenCode** | `.opencode/instructions.md` | `./scripts/install-skills.sh --agent opencode` | -| **GitHub Copilot** | `.github/copilot-instructions.md` | `./scripts/install-skills.sh --agent copilot` | -| **Windsurf** | `.windsurfrules` | `./scripts/install-skills.sh --agent windsurf` | -| **Aider** | `.aider.instructions.md` | `./scripts/install-skills.sh --agent aider` (then add `read:` to `.aider.conf.yml`) | - -### Skill management commands - -| Command | Description | -| --- | --- | -| `mysql-cli skill list` | List skills bundled with this binary | -| `mysql-cli skill version` | Print expected skill versions | -| `mysql-cli skill check [dir] [-j]` | Compare installed vs bundled versions (`ok`/`stale`/`missing`) | -| `mysql-cli skill install [dir]` | Install bundled skills into a directory | +| **Claude Code** | `.claude/skills/*/SKILL.md` | `npx skills add AllenMuu/mysql-cli -a claude-code` | +| **Cursor** | `.cursor/rules/*.mdc` | `npx skills add AllenMuu/mysql-cli -a cursor` | +| **Codex CLI** | `AGENTS.md` | `npx skills add AllenMuu/mysql-cli -a codex` | +| **OpenCode** | `.opencode/instructions.md` | `npx skills add AllenMuu/mysql-cli -a opencode` | +| **GitHub Copilot** | `.github/copilot-instructions.md` | `npx skills add AllenMuu/mysql-cli -a github-copilot` | +| **Windsurf** | `.windsurfrules` | `npx skills add AllenMuu/mysql-cli -a windsurf` | +| **Aider** | `.aider.instructions.md` | `npx skills add AllenMuu/mysql-cli -a aider` (then add `read:` to `.aider.conf.yml`) | ### Setup notes From 91444ffcb49cf0fbe5be952658413aed2d9a3588 Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 15:44:54 +0800 Subject: [PATCH 10/14] =?UTF-8?q?docs(readme-zh):=20=E5=88=87=E6=8D=A2=20s?= =?UTF-8?q?kill=20=E5=AE=89=E8=A3=85=E6=96=87=E6=A1=A3=E5=88=B0=20npx=20sk?= =?UTF-8?q?ills=20add?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-zh.md | 49 ++++++++++++++++++++----------------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/README-zh.md b/README-zh.md index 85e412b..b680576 100644 --- a/README-zh.md +++ b/README-zh.md @@ -102,22 +102,25 @@ go install github.com/AllenMuu/mysql-cli/cmd/mysql-cli@latest **第 2 步 - 安装 Agent Skills** -二选一(两种方式都会安装全部三个 skill): - -*方式 A - 安装脚本*(支持下列所有 agent): +通过 [vercel-labs/skills](https://github.com/vercel-labs/skills) 安装 skill(支持 70+ 种 agent): ```bash -./scripts/install-skills.sh # 自动检测 -./scripts/install-skills.sh --agent all --project-dir ~/my-project +npx skills add AllenMuu/mysql-cli ``` -*方式 B - 从二进制安装*(内嵌 skill,零外部依赖): +会打开交互式选择:选 agent、选 scope(project `.//skills/` 或 global `~//skills/`)、选安装方式(推荐 symlink)、确认。 + +非交互(CI / agent): ```bash -mysql-cli skill install # -> ~/.claude/skills -mysql-cli skill install ~/my-project/.claude/skills +npx skills add AllenMuu/mysql-cli --skill '*' -a claude-code -g -y ``` +> **务必安装全部 3 个 skill**(`mysql-shared`、`mysql-query`、`mysql-schema`)。 +> `mysql-query` 与 `mysql-schema` 顶部引用 `../mysql-shared/SKILL.md`,只装单个会导致引用断裂。 + +**无 Node.js?** 手动把仓库 `skills/` 目录复制到 agent 的 skill 目录(如 `~/.claude/skills/`)。 + **第 3 步 - 配置数据源** 写入 `~/.config/mysql-cli/config.toml`(完整格式见[配置](#配置)): @@ -136,7 +139,6 @@ database = "app" **第 4 步 - 验证并执行** ```bash -mysql-cli skill check # 确认 skill 与二进制版本一致 mysql-cli query "SELECT * FROM users LIMIT 10" # 默认 JSON 输出 ``` @@ -273,28 +275,17 @@ Skills 编码了触发条件、前置检查、命令参考、安全模型与错 ### 其他 agent -`mysql-cli` 兼容**任何能跑 shell 命令并解析 JSON 的 agent**。安装脚本支持下列全部七种 -agent:Claude Code 与 Cursor 使用原生 SKILL.md / .mdc 格式;其余 agent 会把合并后的 skill -正文(幂等地)追加到各自的指令文件。 +`mysql-cli` 兼容**任何能跑 shell 命令并解析 JSON 的 agent**。`npx skills add` 安装器(vercel-labs/skills)支持 Claude Code、Cursor、Codex 以及 70+ 种 agent;它以 symlink 方式安装每个 skill,因此更新仓库即可自动同步。 -| Agent | 配置格式 | 如何使用 `mysql-cli` | +| Agent | 配置格式 | 安装 | | --- | --- | --- | -| **Claude Code** | `.claude/skills/*/SKILL.md` | `./scripts/install-skills.sh --agent claude` 或 `mysql-cli skill install` | -| **Cursor** | `.cursor/rules/*.mdc` | `./scripts/install-skills.sh --agent cursor` | -| **Codex CLI** | `AGENTS.md` | `./scripts/install-skills.sh --agent codex` | -| **OpenCode** | `.opencode/instructions.md` | `./scripts/install-skills.sh --agent opencode` | -| **GitHub Copilot** | `.github/copilot-instructions.md` | `./scripts/install-skills.sh --agent copilot` | -| **Windsurf** | `.windsurfrules` | `./scripts/install-skills.sh --agent windsurf` | -| **Aider** | `.aider.instructions.md` | `./scripts/install-skills.sh --agent aider`(然后在 `.aider.conf.yml` 加 `read:`) | - -### Skill 管理命令 - -| 命令 | 说明 | -| --- | --- | -| `mysql-cli skill list` | 列出二进制内嵌的 skill | -| `mysql-cli skill version` | 打印期望的 skill 版本 | -| `mysql-cli skill check [dir] [-j]` | 对比已装版本与内嵌版本(`ok`/`stale`/`missing`) | -| `mysql-cli skill install [dir]` | 把内嵌 skill 安装到指定目录 | +| **Claude Code** | `.claude/skills/*/SKILL.md` | `npx skills add AllenMuu/mysql-cli -a claude-code` | +| **Cursor** | `.cursor/rules/*.mdc` | `npx skills add AllenMuu/mysql-cli -a cursor` | +| **Codex CLI** | `AGENTS.md` | `npx skills add AllenMuu/mysql-cli -a codex` | +| **OpenCode** | `.opencode/instructions.md` | `npx skills add AllenMuu/mysql-cli -a opencode` | +| **GitHub Copilot** | `.github/copilot-instructions.md` | `npx skills add AllenMuu/mysql-cli -a github-copilot` | +| **Windsurf** | `.windsurfrules` | `npx skills add AllenMuu/mysql-cli -a windsurf` | +| **Aider** | `.aider.instructions.md` | `npx skills add AllenMuu/mysql-cli -a aider`(然后在 `.aider.conf.yml` 加 `read:`) | ### 安装须知 From 79c9d816fe6789f40fb8fd0a1f79eba06ed67156 Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 15:49:34 +0800 Subject: [PATCH 11/14] docs(agents): rewrite skill section for npx skills ecosystem Removes bundle/agents/skillscheck/install-skills.sh references. Documents npx skills add as the install path, .well-known discovery, all-three install requirement, and repo frontmatter as version truth. --- AGENTS.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 104f669..c43d909 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,6 @@ cmd/mysql-cli/main -> cli(cobra 装配 + 退出码映射 + skill 子命令 │ └─-> schema ─-> result/safety └─ env/file 解析 repl(聚合 query+schema+format) format ← result - cli(skill 子命令)─-> skillscheck ─-> bundle(根包,//go:embed skills/) ``` - **`result`** - 共享 `Result{Columns, Rows, RowsAffected, LastInsertID}`,是 query/schema(生产者)与 format/cli(消费者)之间的中立契约。 @@ -49,8 +48,6 @@ cmd/mysql-cli/main -> cli(cobra 装配 + 退出码映射 + skill 子命令 - **`format`** - `result.Result` -> json/table/csv/tsv;JSON 严格信封 `{success,data,error:{code,message}}`。 - **`cli`** - cobra 子命令 + 全局 flag + `mapError` 把核心 error 翻译成退出码;含 `skill` 子命令(list/check/install/version)。 - **`repl`** - readline 交互壳,仅人类调试用,复用 query/schema/format。 -- **`bundle`**(根包,`bundle.go`)- `//go:embed skills` 把 skill 定义嵌入二进制,是 `mysql-cli skill install` 零依赖安装的单一来源(与 `scripts/install-skills.sh` 共享 `skills/` 目录)。 -- **`skillscheck`** - 对比已装 skill(如 `~/.claude/skills`)的 version frontmatter 与 bundle 内嵌版本,报 `ok/stale/missing/unknown`。仅 `mysql-cli skill check` 显式调用,**不走查询热路径**。 ## 关键约定(改代码前必读) @@ -76,12 +73,11 @@ cmd/mysql-cli/main -> cli(cobra 装配 + 退出码映射 + skill 子命令 ## Skill 体系(对接 AI agent) -skill 让 agent 零配置发现并正确调用 mysql-cli,设计参照 `larksuite/cli`(调研见 `docs/research-lark-cli.md`)。 +mysql-cli 的 skill 不再自研安装,而是接入 [vercel-labs/skills](https://github.com/vercel-labs/skills) 生态。skill 是仓库侧资产,由通用 `skills` 包管理器安装到 75+ agent。 -- **skill 文件**:`skills/mysql-{shared,query,schema}/SKILL.md`。`mysql-shared` 承载配置/安全模型/退出码/错误自修复,被 `mysql-query`/`mysql-schema` 顶部 `MUST Read` 引用(auto-load,DRY)。新增 skill 建 `skills/mysql-/SKILL.md`,参考 `skill-template/skill-template.md`。 -- **安装**(二选一): - - `./scripts/install-skills.sh` -- auto 检测已安装的 agent;原生支持 claude/cursor(SKILL.md/.mdc),codex/opencode/copilot/windsurf/aider 幂等追加合并 skill 到各自指令文件;支持 `--agent |all`、`--project-dir`、`--no-global`。 - - `mysql-cli skill install [target-dir]` -- 从二进制内嵌的 bundle 安装,零外部依赖(默认 `~/.claude/skills`)。 -- **版本同步检查**:`mysql-cli skill check [target-dir] [-j]` 对比已装 skill version 与内嵌版本,状态 `ok/stale/missing/unknown`,始终 exit 0(agent 解析 JSON `status` 字段)。 -- **格式校验**:`scripts/skill-format-check.sh` 校验 SKILL.md frontmatter(name/version/description/metadata + name 匹配目录 + semver),CI `.github/workflows/skill-format-check.yml` 在 PR 时强制。改 skill 后本地跑一遍。 -- **改动 skill 后**:skill 文件是 `bundle` 的 embed 源,改完 `go build` 重新嵌入;`scripts/install-skills.sh` 与 bundle 共享同一份 `skills/`,无需同步两份。 +- **skill 文件**:`skills/mysql-{shared,query,schema}/SKILL.md`。`mysql-shared` 承载配置/安全模型/退出码/错误自修复,被 `mysql-query`/`mysql-schema` 顶部 `MUST Read` 引用(auto-load,DRY)。 +- **安装**:`npx skills add AllenMuu/mysql-cli`(交互式选 agent/scope/install method);非交互 `npx skills add AllenMuu/mysql-cli --skill '*' -a -g -y`。**务必全装 3 个 skill**,否则 `mysql-shared` 引用断裂。 +- **发现机制**:仓库根 `.well-known/agent-skills/index.json` 声明 3 skill(vercel-labs/skills 首选);不加也可走默认 `skills/*/SKILL.md` 扫描。 +- **格式校验**:`scripts/skill-format-check.sh` 校验 SKILL.md frontmatter(name/version/description/metadata + semver),CI `.github/workflows/skill-format-check.yml` PR 时强制。改 skill 后本地跑一遍。 +- **版本真相源**:skill 版本 = 仓库 `skills/*/SKILL.md` frontmatter 的 `version` 字段(不再二进制内嵌)。 +- **无 Node fallback**:手动复制仓库 `skills/` 目录到 agent skill 目录。 From 9db8afa4d9f1c14627d8ae624c4bb320096dcd5c Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 15:52:53 +0800 Subject: [PATCH 12/14] docs(changelog): note skill install migration breaking change --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5920efa..60417b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Breaking - **SELECT 默认安全 cap 1000**:不带 LIMIT 的 SELECT 现在默认只返回 1000 行,`meta.truncated=true` 标记截断。需全表用 `--no-limit`;调默认值用 config `default_limit` 或 env `MYSQL_CLI_DEFAULT_LIMIT`;显式精确行数用 `--limit N`。动机:实测裸跑 4.4 万行表 = ~900 万 token(45 个 200K context 窗口),会当场撑爆 agent 会话。 - **SELECT 的 JSON 信封省略 `rows_affected`**(对 SELECT 恒为 0);改用 `meta.truncated`/`meta.limit`。DML/DDL 信封不变。 +- **skill 安装迁移至 vercel-labs/skills 生态**:`mysql-cli init`、`mysql-cli skill install/list/version/check` 及 `scripts/install-skills.sh` 全部移除。改用 `npx skills add AllenMuu/mysql-cli` 安装 skill(支持 75+ agent,交互式选 agent/scope/install method)。无 Node 环境可手动复制仓库 `skills/` 目录。skill 版本真相源从二进制内嵌迁移至仓库 `skills/*/SKILL.md` frontmatter。 ### Added - `--format jsonl`:每行一个 JSON 对象,比 json 紧凑,适合 agent。 From 1c25fbece335cd53cf87627246a47939c62a6b62 Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 15:55:33 +0800 Subject: [PATCH 13/14] docs: clean up stale skill command refs in dist/npm README + AGENTS --- AGENTS.md | 4 ++-- dist/npm/README.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c43d909..7101d82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ RUN_INTEGRATION=1 go test -tags=integration ./internal/integration/ -v 包严格单向依赖,`result` 是无依赖底层,避免循环引用: ``` -cmd/mysql-cli/main -> cli(cobra 装配 + 退出码映射 + skill 子命令) +cmd/mysql-cli/main -> cli(cobra 装配 + 退出码映射 + config 子命令) ↓ config ─-> conn ─-> query ─-> result │ │ └─-> safety(无依赖,纯逻辑) @@ -46,7 +46,7 @@ cmd/mysql-cli/main -> cli(cobra 装配 + 退出码映射 + skill 子命令 - **`query`** - `Execute`(读,走 `QueryContext`)、`ExecuteWrite`(单条 DML/DDL,包在事务里提交)、`ExecuteTxn`(多条原子事务)。每条语句都过 safety 闸门 + 多语句检测。 - **`schema`** - 只读探索命令(`schema/sample/tables/databases/read/explore/analyze`),对应原 MCP 的 `get_schema_info`/`get_table_sample`/`list_resources`/`read_resource`。所有标识符在拼接 SQL 前经 `safety.Validate*` 校验。 - **`format`** - `result.Result` -> json/table/csv/tsv;JSON 严格信封 `{success,data,error:{code,message}}`。 -- **`cli`** - cobra 子命令 + 全局 flag + `mapError` 把核心 error 翻译成退出码;含 `skill` 子命令(list/check/install/version)。 +- **`cli`** - cobra 子命令 + 全局 flag + `mapError` 把核心 error 翻译成退出码;含 `config` 子命令(init/list/global/project)。 - **`repl`** - readline 交互壳,仅人类调试用,复用 query/schema/format。 ## 关键约定(改代码前必读) diff --git a/dist/npm/README.md b/dist/npm/README.md index 1fdddc8..928d6bd 100644 --- a/dist/npm/README.md +++ b/dist/npm/README.md @@ -6,7 +6,7 @@ One-line install of the [mysql-cli](https://github.com/AllenMuu/mysql-cli) Go bi ```bash npx @allenmuu/mysql-cli install # installs the binary to ~/.local/bin -mysql-cli init # installs agent skills (auto-detected) +mysql-cli config init # initialises config (auto-detected) ``` No Go toolchain required. The `install` command downloads the prebuilt binary for your platform from GitHub Releases. @@ -14,8 +14,8 @@ No Go toolchain required. The `install` command downloads the prebuilt binary fo ## One-shot usage (no permanent install) ```bash -npx @allenmuu/mysql-cli init # install skills into detected agents -npx @allenmuu/mysql-cli skill check # check installed skill versions +npx @allenmuu/mysql-cli config init # initialises config (same as above) +npx @allenmuu/mysql-cli config list # list configured datasources npx @allenmuu/mysql-cli query "SELECT 1" -d mydb ``` From b89d303bdb1d8cc52f3a8cca0eb61da161b5802c Mon Sep 17 00:00:00 2001 From: AllenJ Date: Mon, 27 Jul 2026 16:01:19 +0800 Subject: [PATCH 14/14] fix(ci): drop deleted install-skills-test from skill-format-check workflow - Remove installer tests step (script deleted in 8294163) - Remove install-skills.sh and install-skills-test.sh from push/pull_request paths - Add trailing newline to .well-known/agent-skills/index.json --- .github/workflows/skill-format-check.yml | 7 ------- .well-known/agent-skills/index.json | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/skill-format-check.yml b/.github/workflows/skill-format-check.yml index ea4e08e..3d3c487 100644 --- a/.github/workflows/skill-format-check.yml +++ b/.github/workflows/skill-format-check.yml @@ -5,16 +5,12 @@ on: - "skills/**" - "scripts/skill-format-check/**" - "scripts/skill-format-check.sh" - - "scripts/install-skills.sh" - - "scripts/install-skills-test.sh" - ".github/workflows/skill-format-check.yml" pull_request: paths: - "skills/**" - "scripts/skill-format-check/**" - "scripts/skill-format-check.sh" - - "scripts/install-skills.sh" - - "scripts/install-skills-test.sh" - ".github/workflows/skill-format-check.yml" jobs: @@ -28,6 +24,3 @@ jobs: - name: Self-test (good/bad fixtures) run: ./scripts/skill-format-check/test.sh - - - name: Installer tests (7 agents) - run: ./scripts/install-skills-test.sh diff --git a/.well-known/agent-skills/index.json b/.well-known/agent-skills/index.json index 4fff0fc..6aa71e1 100644 --- a/.well-known/agent-skills/index.json +++ b/.well-known/agent-skills/index.json @@ -16,4 +16,4 @@ "path": "skills/mysql-schema/SKILL.md" } ] -} \ No newline at end of file +}