@anov/gis-skills-cli 0.0.0-stage → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +637 -0
  2. package/INSTALL.md +481 -0
  3. package/LICENSE +15 -0
  4. package/README.md +148 -2
  5. package/RELEASING.md +162 -0
  6. package/bin/anov-gis-skills.mjs +470 -0
  7. package/lib/mcp-config.mjs +273 -0
  8. package/package.json +43 -4
  9. package/skills/anovgis-analysis/SKILL.md +585 -0
  10. package/skills/anovgis-effects/SKILL.md +260 -0
  11. package/skills/anovgis-imagery-terrain/SKILL.md +505 -0
  12. package/skills/anovgis-layers/SKILL.md +627 -0
  13. package/skills/anovgis-materials/SKILL.md +635 -0
  14. package/skills/anovgis-models-3dtiles/SKILL.md +448 -0
  15. package/skills/anovgis-pitfalls/SKILL.md +561 -0
  16. package/skills/anovgis-popup-widget/SKILL.md +474 -0
  17. package/skills/anovgis-primitives/SKILL.md +482 -0
  18. package/skills/anovgis-vector-graphics/SKILL.md +602 -0
  19. package/skills/anovgis-viewer/SKILL.md +618 -0
  20. package/skills/using-anovgis/DEMO-MAP.md +64 -0
  21. package/skills/using-anovgis/DEMO-SKILL-MAP.md +248 -0
  22. package/skills/using-anovgis/DOMAINS.md +183 -0
  23. package/skills/using-anovgis/REFERENCE.md +114 -0
  24. package/skills/using-anovgis/SKILL.md +392 -0
  25. package/skills/using-anovgis/templates/README.md +154 -0
  26. package/skills/using-anovgis/templates/combo1-vue3-vite.md +295 -0
  27. package/skills/using-anovgis/templates/combo2-vue2-cli.md +119 -0
  28. package/skills/using-anovgis/templates/combo3-html-cdn.md +107 -0
  29. package/skills/using-anovgis/templates/combo4-vanilla-webpack.md +91 -0
  30. package/skills/using-anovgis/templates/wizard-profiles.md +105 -0
  31. package/vendor/cesiumjs-skills/LICENSE +201 -0
  32. package/vendor/cesiumjs-skills/NOTICE +18 -0
  33. package/vendor/cesiumjs-skills/skills/cesiumjs-3d-tiles/SKILL.md +465 -0
  34. package/vendor/cesiumjs-skills/skills/cesiumjs-camera/SKILL.md +500 -0
  35. package/vendor/cesiumjs-skills/skills/cesiumjs-core-utilities/SKILL.md +403 -0
  36. package/vendor/cesiumjs-skills/skills/cesiumjs-custom-shader/REFERENCE.md +397 -0
  37. package/vendor/cesiumjs-skills/skills/cesiumjs-custom-shader/SKILL.md +351 -0
  38. package/vendor/cesiumjs-skills/skills/cesiumjs-custom-shader/examples/01-diffuse-tint.js +38 -0
  39. package/vendor/cesiumjs-skills/skills/cesiumjs-custom-shader/examples/02-texture-swap.js +42 -0
  40. package/vendor/cesiumjs-skills/skills/cesiumjs-custom-shader/examples/03-feature-id-tileset.js +29 -0
  41. package/vendor/cesiumjs-skills/skills/cesiumjs-custom-shader/examples/04-translucent-override.js +37 -0
  42. package/vendor/cesiumjs-skills/skills/cesiumjs-custom-shader/examples/05-vertex-displacement.js +49 -0
  43. package/vendor/cesiumjs-skills/skills/cesiumjs-custom-shader/examples/06-metadata-ramp.js +34 -0
  44. package/vendor/cesiumjs-skills/skills/cesiumjs-custom-shader/examples/07-voxel-shader.js +27 -0
  45. package/vendor/cesiumjs-skills/skills/cesiumjs-custom-shader/examples/README.md +19 -0
  46. package/vendor/cesiumjs-skills/skills/cesiumjs-custom-shader/examples/_sandcastle-template.html +9 -0
  47. package/vendor/cesiumjs-skills/skills/cesiumjs-entities/SKILL.md +389 -0
  48. package/vendor/cesiumjs-skills/skills/cesiumjs-imagery/SKILL.md +471 -0
  49. package/vendor/cesiumjs-skills/skills/cesiumjs-interaction/SKILL.md +423 -0
  50. package/vendor/cesiumjs-skills/skills/cesiumjs-materials-shaders/SKILL.md +292 -0
  51. package/vendor/cesiumjs-skills/skills/cesiumjs-models-particles/SKILL.md +423 -0
  52. package/vendor/cesiumjs-skills/skills/cesiumjs-primitives/SKILL.md +537 -0
  53. package/vendor/cesiumjs-skills/skills/cesiumjs-spatial-math/SKILL.md +358 -0
  54. package/vendor/cesiumjs-skills/skills/cesiumjs-terrain-environment/SKILL.md +401 -0
  55. package/vendor/cesiumjs-skills/skills/cesiumjs-time-properties/SKILL.md +353 -0
  56. package/vendor/cesiumjs-skills/skills/cesiumjs-viewer-setup/SKILL.md +434 -0
  57. package/vendor/cesiumjs-skills/skills/using-cesiumjs-skills/SKILL.md +35 -0
package/RELEASING.md ADDED
@@ -0,0 +1,162 @@
1
+ # 发版与贡献
2
+
3
+ PR 到 GitLab `zhf/anov-gis-skills` 主仓。本文两件事:**改 skill 的规范**、**怎么发一个版**。
4
+
5
+ ---
6
+
7
+ ## 一、贡献规范
8
+
9
+ ### 新增 skill 要求
10
+
11
+ - 目录名格式:`anovgis-<name>`(如 `anovgis-three-layer`)
12
+ - `SKILL.md` 是入口,**本身 ≤ 400 行**;重的部分拆成同目录子文件(`REFERENCE.md` 速查表 / `DEMO-MAP.md` 场景路由 / `templates/` 工程骨架 / `examples/` 可跑示例),子文件仍是 skill 的一部分,靠 SKILL.md 里的指针跳转
13
+ - frontmatter 必填字段:`name`、`description`、`anov-gis-baseline`、`cesium-baseline`
14
+ - description 含 "Use when..." 短语列表 + 触发关键词
15
+ - 必含章节:Out-of-scope / Version baseline / Quickstart / 类速查表 / Snippet / 反模式
16
+ - 在 `skills/using-anovgis/DOMAINS.md` § 1 标注该 skill 拥有的类条目
17
+ - 在 `skills/using-anovgis/DEMO-SKILL-MAP.md` 标注覆盖的 demo
18
+ - 在 `skills/using-anovgis/SKILL.md` 路由表中添加该 skill
19
+
20
+ ### 修改现有 skill 要求
21
+
22
+ - 保持 description 关键词密度(每 50 字至少 1 个具体 API / demo 文件名关键词)
23
+ - 新增 snippet 必须带 `// 来源: ...` 注释
24
+ - Out-of-scope 章节变动要同步更新 DOMAINS.md § 3 反归属清单
25
+
26
+ ### 铁律
27
+
28
+ - **不要手工改版本号** —— `scripts/bump.mjs` 是唯一来源,`--check` 会把任何漏改 / 多改揪出来
29
+ - **不要在 skill 里写逃出所属 skills 树的相对链接** —— skill 会被拷到 `<agent>/skills/<name>/` 单个目录,逃出去的链接必断。`scripts/check-structure.mjs` 与 `scripts/build-npm.mjs` 都会拦(`skills/` 与 `vendor/cesiumjs-skills/skills/` 各算一棵树)
30
+ - **不要手改 `vendor/cesiumjs-skills/`** —— 那是上游 Apache-2.0 内容的原样副本,改动会在下次 `sync-cesium.mjs` 时被覆盖。要改就在上游改,再同步回来
31
+ - **不要给 SKILL.md frontmatter 加 `version:` 字段** —— 非 schema 键,`bump.mjs --check` 会拒绝
32
+ - **不要改 skill 目录名** —— 装到用户机器上后是按 `name` 字段寻址,目录名只是本地约定
33
+ - **SDK 已封装的能力不要用 Cesium.\* 自拼** —— 天地图 / 地形 / 模型等一律走 `@anov/gis` 的封装
34
+
35
+ ---
36
+
37
+ ## 二、语义化版本纪律
38
+
39
+ 团队在 `.claude/settings.json` 的 `enabledPlugins` 里用 `anov-gis-skills@anov-gis-skills@^0.3.0` 这种 **锁 minor** 的方式安装,版本号怎么涨直接决定用户收到什么:
40
+
41
+ | 变更类型 | 版本怎么涨 | 用户收到吗 |
42
+ |---|---|---|
43
+ | 修 bug、改 skill 内容、加 snippet | **patch**(0.2.x → 0.2.x+1) | ✅ 自动收到(`autoUpdate` 开着) |
44
+ | 新增 skill、**改 skill 名或目录结构** | **minor**(0.2.x → 0.3.0) | ❌ 不自动跟,需显式改安装串 |
45
+ | skill 目录内新增子文件(`REFERENCE.md` / `templates/` / `examples/`) | **patch** | skill 的 `name` 与顶层结构未变,对所有智能体的寻址方式无影响 |
46
+ | 同步 vendor 的 Cesium skill(上游更新) | **patch** | 用户重跑 `setup` 即拿到 |
47
+ | 改动 CLI 命令族 / 新增安装方式 | **minor** | 安装方式变了,需显式改文档与流程 |
48
+ | 破坏性契约变更 | **major**(0.x → 1.0.0) | ❌ 不自动跟 |
49
+
50
+ 因为 skill 名和目录结构从 v0.1.1 至今没变过,**日常发版只用 patch**。
51
+
52
+ **改版本号只有一个入口**:
53
+
54
+ ```bash
55
+ node scripts/bump.mjs patch # 例:0.2.15 → 0.2.16,同时插 CHANGELOG 条目
56
+ node scripts/bump.mjs --check # 校验所有版本位一致(发版前自检,不一致就 fail)
57
+ ```
58
+
59
+ 不要手工去改 `INSTALL.md` / `RELEASING.md` / `skills/using-anovgis/*.md` 里的版本号 —— `bump.mjs` 是唯一来源,`--check` 会把任何漏改 / 多改的地方揪出来(`skills/using-anovgis/DEMO-SKILL-MAP.md` 之前就长期停在 v0.2.0,正是这个闸门要防的事)。
60
+
61
+ ---
62
+
63
+ ## 三、发版流程
64
+
65
+ `@anov/gis` 升级到 1.9.x 后(未来),或任何一次 skill 内容变更后:
66
+
67
+ 1. 在 ANOV-GIS 仓库拉新版源码:`git pull`
68
+ 2. 对比 ANOV-GIS 仓库 `src/` 与本仓库各 `skills/*/SKILL.md`:
69
+ - 新增类 → 加到对应 skill 的速查表 + snippet
70
+ - 删除类 → 从 skill 移除
71
+ - 改名 / 移动 → 更新 DOMAINS.md 归属表
72
+ 3. 检查行号是否漂移:v1.8.4 → v1.8.6 抽样核对显示 DEF_OPTS 等顶部声明稳定,但新文件(如 `PositonEditor`/`TilesetEditor`)需要作为新章节追加
73
+ 4. 检查 DOMAINS.md「空白域待补」清单是否需要更新
74
+ 5. 跑 `grep -r "v1.8.6" skills/` 确认每个 skill 顶部 baseline 字段同步
75
+ 6. 跑 trigger 复测:5 个真实提问覆盖新 API
76
+ 7. **改 skill 内容 + 更新 CHANGELOG.md**
77
+ 8. **bump 版本号**:`node scripts/bump.mjs patch`(或 `minor` / `major` / `--set <版本>`)—— 一次改掉所有版本位
78
+ 9. **自检**:`node scripts/bump.mjs --check` —— 不一致就 exit 1
79
+ 10. **结构自检**:`node scripts/check-structure.mjs` —— 相对链接 / 锚点 / frontmatter / 章节完整性,不一致就 exit 1
80
+ 11. **npm 包自检**:`node scripts/build-npm.mjs` —— 校验两棵 skill 树的相对链接、无本机绝对路径、vendor 齐全(15 个 skill + LICENSE + NOTICE),不一致就 exit 1
81
+ 11b. **vendor 一致性**(改动过 Cesium skill 时):`node scripts/sync-cesium.mjs --check` —— vendor 与内网镜像不一致就 exit 1;需要更新时去掉 `--check` 重新同步,并核对 `vendor/cesiumjs-skills/NOTICE` 里的版本号
82
+ 12. **提交推送**:用 `npm run release` 时自动完成 —— 只 `git add` bump 改写的发版文件后 commit + push(`--no-commit` 可只改文件不提交)。手工流程则 `git add` 发版文件 + `git commit -m "chore: release v<X.Y.Z>"` + `git push`
83
+ 13. **打 tag**:用 `npm run release` 时自动打 `anov-gis-skills--v<版本>` 注解 tag 并推送;不走 release 时按 §四 手工打
84
+ 14. **发 npm 包**(方式 A 的分发源):`npm publish` —— 发到哪由仓库根 `.npmrc` 决定(内网 / 公网),见 §五
85
+ 15. **验证已发布的包**:`node scripts/verify-published.mjs` —— 确认可安装、内容与源码一致、CLI 能发现全部 skill、链接全解析,不一致就 exit 1
86
+
87
+ > v0.2.15 起,第 8 步之前的手工改版本号(各 manifest + DOMAINS.md + DEMO-SKILL-MAP.md)全部由 `scripts/bump.mjs` 代劳,避免版本漂移。
88
+ >
89
+ > 用 `npm run release` 时,第 8–13 步(bump · 版本校验 · 结构校验 · npm 包校验 · 提交推送 · 打 tag)会一并自动执行;两道结构校验在写文件之前跑,失败则什么都不会改。
90
+
91
+ ---
92
+
93
+ ## 四、tag
94
+
95
+ 本仓库**不跑 CI**(内网 GitLab 上没有 runner),没有流水线帮你打 tag —— `npm run release` 会自动打并推送,手工发版时用下面的命令。
96
+
97
+ **打 tag** —— 带版本约束的安装串(`"anov-gis-skills@anov-gis-skills@^0.3.0"`)靠这个 tag 解析版本,没有匹配 tag 会报 `no-matching-tag`:
98
+
99
+ ```bash
100
+ VERSION=$(node -p "require('./.claude-plugin/plugin.json').version")
101
+ git tag -a "anov-gis-skills--v${VERSION}" -m "release ${VERSION}"
102
+ git push origin "anov-gis-skills--v${VERSION}"
103
+ ```
104
+
105
+ 不带约束的安装(`"anov-gis-skills@anov-gis-skills": true`)只认 marketplace 里的版本号,**不依赖 tag** —— 但 tag 仍是不可变的发布点,建议每次发版都打。
106
+
107
+ > tag 已存在时跳过重复打,避免覆盖历史注解。
108
+
109
+ ---
110
+
111
+ ## 五、npm 包发版
112
+
113
+ 跟 tag 一样要人工执行。**默认发到公网 npm**(`https://registry.npmjs.org/`)—— 内网直连公网即可安装,用户不需要配任何 registry。
114
+
115
+ ```bash
116
+ npm publish
117
+ ```
118
+
119
+ > ⚠️ **发布目标由仓库根 `.npmrc` 控制**(与 ANOV-GIS 仓库 `@anov/gis` 的做法一致)。`.npmrc` 里 `registry=` 那行启用哪个就发到哪个;两行都注释掉则回退到全局默认。
120
+ >
121
+ > ⚠️ **`package.json` 里不能有 `publishConfig.registry`** —— 它会**覆盖** `.npmrc`,把这个开关锁死。`scripts/bump.mjs --check` 会拦。
122
+ >
123
+ > ⚠️ **npmjs.org 账号开了双因素认证** —— `npm publish` 要 OTP 验证码:`npm publish --otp=<6位码>`。没带 OTP 会报 `EOTP`。
124
+ >
125
+ > ⚠️ **同版本号不能重发**(内网 Verdaccio 报 `409 Conflict`,`--force` 也不行)。改了内容就得 bump 一个新版本再发。
126
+
127
+ ### 发版前自检
128
+
129
+ ```bash
130
+ node scripts/bump.mjs --check # 版本一致性(含 package.json 与 manifest 同步)
131
+ node scripts/check-structure.mjs # 结构 / 链接 / 锚点
132
+ node scripts/build-npm.mjs # 包内容(链接不逃出 skills/ 树、无本机路径)
133
+ ```
134
+
135
+ ### 发完验证
136
+
137
+ ```bash
138
+ node scripts/verify-published.mjs
139
+ ```
140
+
141
+ 验五件事:
142
+
143
+ | # | 验什么 | 失败意味着 |
144
+ |---|---|---|
145
+ | 1 | registry 上查得到该版本,元数据齐全(version / bin / engines / license) | 包没发上去,或发到了别的 registry |
146
+ | 2 | 能真的装下来(tarball 可下载可解包) | tarball 损坏 / registry 权限问题 |
147
+ | 3 | 装出来的 skill 与仓库源码**逐字节一致** | `files` 白名单漏了文件,或发了旧版本 |
148
+ | 4 | skills CLI 能发现**全部 12 个** skill | frontmatter 没被严格 YAML 解析(v0.2.17 踩过:只发现 10/12) |
149
+ | 5 | skill 内相对链接在拷贝安装语义下**全部可解析** | 有链接逃出 `skills/` 树,别的机器上会断 |
150
+
151
+ 出问题时会逐条列出并以**退出码 1** 结束,可以直接挂到流水线里。
152
+
153
+ 指定 registry / 版本:
154
+
155
+ ```bash
156
+ node scripts/verify-published.mjs --registry https://registry.npmjs.org/
157
+ node scripts/verify-published.mjs --version 0.2.19
158
+ ```
159
+
160
+ > ⚠️ **npm 包版本号与 plugin 版本号必须一致** —— `scripts/bump.mjs` 会同时改两处,`--check` 会把它们揪出来。别手工改 `package.json` 的 `version`。
161
+ >
162
+ > ⚠️ **公网包里的内网信息**:`skills/anovgis-imagery-terrain/SKILL.md` 的三处瓦片服务示例地址是内网真实服务,外部网络访问不到。README / package.json / CHANGELOG 里的内网 GitLab 地址已全部移除(改为占位说明)。
@@ -0,0 +1,470 @@
1
+ #!/usr/bin/env node
2
+ // @anov/gis-skills-cli —— 把 anov-gis 与 Cesium 两套 skill 装到 AI 编码智能体,
3
+ // 并把 anov-gis-mcp 的 MCP 条目写进各智能体的配置。
4
+ //
5
+ // anov-gis-skills setup 装全部 skill(+ 写 MCP 配置)
6
+ // anov-gis-skills mcp add 只写 MCP 配置
7
+ // anov-gis-skills mcp remove 删掉 MCP 配置
8
+ // anov-gis-skills mcp show 看当前配置状态
9
+ // anov-gis-skills doctor 自检
10
+ // anov-gis-skills uninstall 清理
11
+ //
12
+ // 底层用 vercel-labs 的 skills CLI(npm 包 `skills`)搬 skill —— 它管着 30+ 智能体的
13
+ // skills 目录映射。锁 1.5.18 而不是 latest:1.6.0+ 的 engines 要求 node >= 22.20.0,
14
+ // 而团队环境实测是 node 22.19.0,锁低版本才能在 node >= 18 全覆盖。
15
+ //
16
+ // MCP 配置由我们自己写(lib/mcp-config.mjs),不引第三方库 —— 只覆盖我们真正常用的
17
+ // 几个智能体,且只动自己那一个条目。
18
+
19
+ import { spawnSync } from "node:child_process";
20
+ import { readdirSync, existsSync, readFileSync } from "node:fs";
21
+ import { fileURLToPath } from "node:url";
22
+ import { dirname, join, resolve } from "node:path";
23
+ import { homedir } from "node:os";
24
+
25
+ import {
26
+ MCP_SERVER_NAME,
27
+ SUPPORTED_AGENTS,
28
+ agentTargets,
29
+ buildServerConfig,
30
+ collectEnv,
31
+ readServer,
32
+ removeServer,
33
+ writeServer,
34
+ } from "../lib/mcp-config.mjs";
35
+
36
+ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
37
+ const SKILLS_DIR = join(PKG_ROOT, "skills");
38
+ const CESIUM_DIR = join(PKG_ROOT, "vendor", "cesiumjs-skills", "skills");
39
+ const SKILLS_CLI = "skills@1.5.18";
40
+
41
+ const MCP_PACKAGE = "anov-gis-mcp";
42
+ const DEFAULT_MCP_AGENTS = ["claude-code", "codex"];
43
+
44
+ // 用 npx 跑时脚本在 npm 缓存的 _npx 目录里,此时 `anov-gis-skills` 不在 PATH 上,
45
+ // 提示语必须给回 npx 形式,否则用户会撞上「不是内部或外部命令」。
46
+ const CLI_CMD = /[\\/]_npx[\\/]/.test(process.argv[1] ?? "")
47
+ ? "npx @anov/gis-skills-cli"
48
+ : "anov-gis-skills";
49
+
50
+ // ---------------------------------------------------------------- 参数解析
51
+
52
+ function parseArgs(argv) {
53
+ const out = { _: [], agents: [], mcpAgents: [], mcpArgs: [], unknown: [] };
54
+ for (let i = 0; i < argv.length; i++) {
55
+ const a = argv[i];
56
+ if (a === "-g" || a === "--global") out.scope = "global";
57
+ else if (a === "--project") out.scope = "project";
58
+ else if (a === "--scope") out.scope = argv[++i];
59
+ else if (a === "-y" || a === "--yes") out.yes = true;
60
+ else if (a === "--print") out.print = true;
61
+ else if (a === "--no-mcp") out.noMcp = true;
62
+ else if (a === "--mcp") out.mcp = true;
63
+ else if (a === "-a" || a === "--agent") {
64
+ while (argv[++i] && !argv[i].startsWith("-")) out.agents.push(argv[i]);
65
+ } else if (a === "--mcp-agent") {
66
+ while (argv[i + 1] && !argv[i + 1].startsWith("-")) {
67
+ for (const v of argv[++i].split(",")) if (v.trim()) out.mcpAgents.push(v.trim());
68
+ }
69
+ } else if (a === "--mcp-mode") out.mcpMode = argv[++i];
70
+ else if (a === "--mcp-url") out.mcpUrl = argv[++i];
71
+ else if (a === "--mcp-command") out.mcpCommand = argv[++i];
72
+ else if (a === "--mcp-arg") {
73
+ while (argv[++i] && !argv[i].startsWith("-")) out.mcpArgs.push(argv[i]);
74
+ } else if (a === "-h" || a === "--help") out.help = true;
75
+ else if (a.startsWith("-")) out.unknown.push(a);
76
+ else out._.push(a);
77
+ }
78
+ return out;
79
+ }
80
+
81
+ const HELP = `${CLI_CMD} —— 装 skill + 接 MCP
82
+
83
+ 用法
84
+ ${CLI_CMD} setup 装全部 skill(默认顺带写 MCP 配置)
85
+ ${CLI_CMD} mcp add 只写 MCP 配置
86
+ ${CLI_CMD} mcp remove 删掉 MCP 配置
87
+ ${CLI_CMD} mcp show 看 MCP 配置状态
88
+ ${CLI_CMD} doctor 自检(skill 完整性 + MCP 配置)
89
+ ${CLI_CMD} uninstall 清理(只删本包与本 MCP 的东西)
90
+
91
+ 通用选项
92
+ --scope global|project 装到哪(默认 global = 用户级,任何项目都能用)
93
+ -g, --global 等价于 --scope global
94
+ --project 等价于 --scope project
95
+ -a, --agent <名字...> 只装给指定智能体(默认:共享目录 + 已装的 Claude Code)
96
+ -y, --yes 跳过所有交互
97
+ --print 只打印要写的内容,不落盘
98
+
99
+ MCP 选项
100
+ --mcp-agent <名字...> 写到哪个智能体(${SUPPORTED_AGENTS.join(" / ")};默认 claude-code + codex)
101
+ --mcp-mode stdio|http 连接方式(默认 stdio)
102
+ --mcp-url <地址> HTTP 模式的服务地址(stdio 模式不需要)
103
+ --mcp-command <命令> stdio 模式的启动命令(默认 npx)
104
+ --mcp-arg <参数...> stdio 模式的参数(默认 -y anov-gis-mcp --stdio)
105
+ --no-mcp setup 时不写 MCP 配置
106
+ `;
107
+
108
+ // ---------------------------------------------------------------- 基础工具
109
+
110
+ function skillNames(dir) {
111
+ if (!existsSync(dir)) return [];
112
+ return readdirSync(dir, { withFileTypes: true })
113
+ .filter((d) => d.isDirectory() && existsSync(join(dir, d.name, "SKILL.md")))
114
+ .map((d) => d.name)
115
+ .sort();
116
+ }
117
+
118
+ /** 本包提供的全部 skill:anov-gis 12 个 + Cesium 15 个。 */
119
+ function allSkillNames() {
120
+ return [...skillNames(SKILLS_DIR), ...skillNames(CESIUM_DIR)];
121
+ }
122
+
123
+ function runSkillsCLI(extra, { quiet = false } = {}) {
124
+ const r = spawnSync("npx", ["-y", SKILLS_CLI, ...extra], {
125
+ stdio: quiet ? "pipe" : "inherit",
126
+ shell: process.platform === "win32",
127
+ encoding: "utf8",
128
+ });
129
+ if (r.error) {
130
+ console.error(`✗ 无法启动 npx:${r.error.message}`);
131
+ console.error(" 若 npm registry 不可达,先确认:npm config get registry");
132
+ process.exit(1);
133
+ }
134
+ return r.status ?? 1;
135
+ }
136
+
137
+ /** skill 装到用户级还是项目级。 */
138
+ function scopeFlags(scope) {
139
+ return scope === "global" ? ["--global"] : [];
140
+ }
141
+
142
+ // 上游的自动检测会把 57 个 universal 系智能体全拉进来,其中 PromptScript 没有全局
143
+ // 目录却仍在名单里,每个 skill 刷一条 Failed。所以这里只显式点名:
144
+ // 共享目录 .agents/skills(universal 系智能体本来就读这里)+ 实际存在的 Claude Code。
145
+ function defaultSkillAgents() {
146
+ return existsSync(join(homedir(), ".claude")) ? ["claude-code"] : ["universal"];
147
+ }
148
+
149
+ // 传 [] 时不加 --agent:remove 的上游语义是从所有智能体删,正好用来清旧的 symlink。
150
+ function targetFlags(agents) {
151
+ return agents.length ? ["--agent", ...agents] : [];
152
+ }
153
+
154
+ // ---------------------------------------------------------------- setup
155
+
156
+ function cmdSetup(opts) {
157
+ const scope = opts.scope ?? "global";
158
+ const agents = opts.agents.length ? opts.agents : defaultSkillAgents();
159
+ const anov = skillNames(SKILLS_DIR);
160
+ const cesium = skillNames(CESIUM_DIR);
161
+
162
+ if (!anov.length) {
163
+ console.error("✗ 包内找不到 skill(skills/ 目录缺失或为空)");
164
+ process.exit(1);
165
+ }
166
+ if (!cesium.length) {
167
+ console.error("✗ 找不到 vendor/cesiumjs-skills/skills —— 重新发布本包,或手工同步 vendor");
168
+ process.exit(1);
169
+ }
170
+
171
+ console.log(`\n安装 ${anov.length + cesium.length} 个 skill`);
172
+ console.log(` anov-gis ${anov.length} 个`);
173
+ console.log(` cesiumjs ${cesium.length} 个`);
174
+ console.log(` 范围 ${scope === "global" ? "用户级(任何项目可用)" : "项目级(当前目录)"}`);
175
+ console.log(` 智能体 ${agents.join(", ")}`);
176
+
177
+ for (const [label, dir] of [["anov-gis", SKILLS_DIR], ["cesiumjs", CESIUM_DIR]]) {
178
+ console.log(`\n── ${label} ──`);
179
+ const code = runSkillsCLI(["add", dir, "--agent", ...agents, ...scopeFlags(scope), "-y"]);
180
+ if (code !== 0) {
181
+ console.error(`✗ ${label} 安装失败(退出码 ${code})`);
182
+ process.exit(code);
183
+ }
184
+ }
185
+
186
+ if (!opts.noMcp) {
187
+ const mode = opts.mcpMode ?? "stdio";
188
+ const config = buildServerConfig({
189
+ mode,
190
+ url: opts.mcpUrl,
191
+ command: opts.mcpCommand,
192
+ args: opts.mcpArgs.length ? opts.mcpArgs : undefined,
193
+ env: collectEnv(),
194
+ });
195
+ writeMcp(mcpAgents(opts), scope, config, { printOnly: opts.print, strict: opts.mcpAgents.length > 0 });
196
+ }
197
+
198
+ console.log("\n✓ 完成");
199
+ if (!opts.noMcp) {
200
+ console.log(` 验证:${CLI_CMD} doctor`);
201
+ }
202
+ }
203
+
204
+ // ---------------------------------------------------------------- MCP
205
+
206
+ /** 默认 Claude Code + Codex;`--mcp-agent` 指定时只用指定的那些。 */
207
+ function mcpAgents(opts) {
208
+ return opts.mcpAgents.length ? opts.mcpAgents : DEFAULT_MCP_AGENTS;
209
+ }
210
+
211
+ /**
212
+ * strict = 用户显式点名了智能体:不支持当前 scope 就直接报错。
213
+ * 默认集合则静默跳过(如 Codex 没有项目级配置),不算失败。
214
+ */
215
+ function mcpTargets(agents, scope, strict) {
216
+ const out = [];
217
+ for (const agent of agents) {
218
+ if (!SUPPORTED_AGENTS.includes(agent)) {
219
+ console.error(`✗ 不支持的智能体:${agent}(可用:${SUPPORTED_AGENTS.join(" / ")})`);
220
+ process.exit(2);
221
+ }
222
+ const targets = agentTargets(agent, scope);
223
+ if (!targets.length) {
224
+ if (strict) {
225
+ console.error(`✗ ${agent} 不支持 ${scope === "global" ? "用户级" : "项目级"}配置`);
226
+ process.exit(1);
227
+ }
228
+ continue;
229
+ }
230
+ out.push(...targets);
231
+ }
232
+ return out;
233
+ }
234
+
235
+ /** --print 的片段按目标格式输出:Codex 是 TOML,其余是 JSON。 */
236
+ function printTarget(target, config) {
237
+ console.log(`\n# ${target.label} → ${target.file}`);
238
+ if (target.key !== null) {
239
+ console.log(JSON.stringify({ [target.key]: { [MCP_SERVER_NAME]: config } }, null, 2));
240
+ return;
241
+ }
242
+ const toml = (v) =>
243
+ typeof v === "string"
244
+ ? `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
245
+ : Array.isArray(v)
246
+ ? `[${v.map(toml).join(", ")}]`
247
+ : String(v);
248
+ const lines = [`[mcp_servers.${MCP_SERVER_NAME}]`];
249
+ for (const [k, v] of Object.entries(config)) {
250
+ if (k !== "env") lines.push(`${k} = ${toml(v)}`);
251
+ }
252
+ if (config.env) {
253
+ lines.push("", `[mcp_servers.${MCP_SERVER_NAME}.env]`);
254
+ for (const [k, v] of Object.entries(config.env)) lines.push(`${k} = ${toml(v)}`);
255
+ }
256
+ console.log(lines.join("\n"));
257
+ }
258
+
259
+ function writeMcp(agents, scope, config, { printOnly = false, strict = false } = {}) {
260
+ for (const target of mcpTargets(agents, scope, strict)) {
261
+ if (printOnly) {
262
+ printTarget(target, config);
263
+ continue;
264
+ }
265
+ writeServer(target, MCP_SERVER_NAME, config);
266
+ console.log(` ✓ ${target.label}:${target.file} → ${MCP_SERVER_NAME}`);
267
+ }
268
+ }
269
+
270
+ function cmdMcp(opts) {
271
+ const action = opts._[0] ?? "show";
272
+ const scope = opts.scope ?? "global";
273
+ const agents = mcpAgents(opts);
274
+ const strict = opts.mcpAgents.length > 0;
275
+
276
+ if (action === "show") {
277
+ for (const target of mcpTargets(agents, scope, strict)) {
278
+ const existing = readServer(target, MCP_SERVER_NAME);
279
+ console.log(`\n${target.label} —— ${target.file}`);
280
+ if (!existing) {
281
+ console.log(" · 未配置");
282
+ } else {
283
+ console.log(` ✓ 已配置:${JSON.stringify(existing)}`);
284
+ }
285
+ }
286
+ return;
287
+ }
288
+
289
+ if (action === "remove") {
290
+ for (const target of mcpTargets(agents, scope, strict)) {
291
+ const r = removeServer(target, MCP_SERVER_NAME);
292
+ console.log(r.removed ? ` ✓ 已删除:${target.file}` : ` · 本来就没有:${target.file}`);
293
+ }
294
+ return;
295
+ }
296
+
297
+ if (action !== "add") {
298
+ console.error(`✗ 未知的 mcp 子命令:${action}(可用:add / remove / show)`);
299
+ process.exit(2);
300
+ }
301
+
302
+ const mode = opts.mcpMode ?? "stdio";
303
+ const config = buildServerConfig({
304
+ mode,
305
+ url: opts.mcpUrl,
306
+ command: opts.mcpCommand,
307
+ args: opts.mcpArgs.length ? opts.mcpArgs : undefined,
308
+ env: collectEnv(),
309
+ });
310
+ console.log(`\n接入 MCP(${mode})`);
311
+ writeMcp(agents, scope, config, { printOnly: opts.print, strict });
312
+ if (!opts.print) console.log("\n✓ 完成。重启智能体生效。");
313
+ }
314
+
315
+ // ---------------------------------------------------------------- doctor
316
+
317
+ function frontmatterOk(text) {
318
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
319
+ if (!m) return { ok: false, why: "缺 frontmatter" };
320
+ const name = /^name\s*:\s*(\S+)\s*$/m.exec(m[1]);
321
+ const desc = /^description\s*:/m.exec(m[1]);
322
+ if (!name) return { ok: false, why: "frontmatter 缺 name" };
323
+ if (!desc) return { ok: false, why: "frontmatter 缺 description" };
324
+ const descLine = m[1].split(/\r?\n/).find((l) => l.startsWith("description:"));
325
+ const val = descLine.slice("description:".length).trim();
326
+ if (!val.startsWith("'") && !val.startsWith('"') && val.includes(":")) {
327
+ return { ok: false, why: "description 未加引号却含冒号(严格 YAML 解析会失败)" };
328
+ }
329
+ return { ok: true, name: name[1] };
330
+ }
331
+
332
+ /** 智能体的 skills 目录(用户级 / 项目级),与 skills CLI 的落点一致。 */
333
+ function installedSkillDirs(scope) {
334
+ const home = homedir();
335
+ const bases =
336
+ scope === "global"
337
+ ? [
338
+ ["Claude Code", join(home, ".claude", "skills")],
339
+ ["Codex", join(home, ".codex", "skills")],
340
+ ["通用 / Cursor", join(home, ".agents", "skills")],
341
+ ]
342
+ : [
343
+ ["Claude Code", join(process.cwd(), ".claude", "skills")],
344
+ ["Codex / 通用", join(process.cwd(), ".agents", "skills")],
345
+ ];
346
+ return bases.filter(([, dir]) => existsSync(dir));
347
+ }
348
+
349
+ function cmdDoctor(opts) {
350
+ const scope = opts.scope ?? "global";
351
+ const expected = allSkillNames();
352
+ let problems = 0;
353
+
354
+ console.log(`\n检查 @anov/gis-skills-cli\n`);
355
+ console.log(`包内 skill 数:${expected.length}`);
356
+
357
+ for (const [label, dir] of installedSkillDirs(scope)) {
358
+ const present = expected.filter((n) => existsSync(join(dir, n, "SKILL.md")));
359
+ console.log(`\n── ${label} ── ${dir}`);
360
+ if (!present.length) {
361
+ console.log(" · 该目录没装本包的 skill(跳过)");
362
+ continue;
363
+ }
364
+ if (present.length === expected.length) {
365
+ console.log(` ✓ ${present.length} 个 skill 齐全`);
366
+ } else {
367
+ const missing = expected.filter((n) => !present.includes(n));
368
+ console.log(` ! 只装了 ${present.length}/${expected.length} 个,缺:${missing.join(", ")}`);
369
+ problems++;
370
+ }
371
+
372
+ let bad = 0;
373
+ for (const n of present) {
374
+ const r = frontmatterOk(readFileSync(join(dir, n, "SKILL.md"), "utf8"));
375
+ if (!r.ok) {
376
+ console.log(` ✗ ${n}: ${r.why}`);
377
+ bad++;
378
+ }
379
+ }
380
+ if (!bad && present.length) console.log(" ✓ frontmatter 全部可被严格 YAML 解析");
381
+ if (bad) problems++;
382
+ }
383
+
384
+ for (const agent of SUPPORTED_AGENTS) {
385
+ for (const target of agentTargets(agent, scope)) {
386
+ const existing = readServer(target, MCP_SERVER_NAME);
387
+ console.log(`\n── MCP · ${target.label} ── ${target.file}`);
388
+ if (existing) {
389
+ console.log(` ✓ 已配置(${existing.type ?? "unknown"})`);
390
+ } else {
391
+ console.log(` · 未配置 —— ${CLI_CMD} mcp add`);
392
+ }
393
+ }
394
+ }
395
+
396
+ console.log("");
397
+ if (problems) {
398
+ console.error(`✗ 发现 ${problems} 处问题 —— 重装:${CLI_CMD} setup\n`);
399
+ process.exit(1);
400
+ }
401
+ console.log("✓ 检查通过\n");
402
+ }
403
+
404
+ // ---------------------------------------------------------------- uninstall
405
+
406
+ function cmdUninstall(opts) {
407
+ const scope = opts.scope ?? "global";
408
+ const names = allSkillNames();
409
+ if (!names.length) {
410
+ console.error("✗ 找不到包内 skill 清单");
411
+ process.exit(1);
412
+ }
413
+
414
+ console.log(`\n移除 ${names.length} 个 skill(${scope === "global" ? "用户级" : "项目级"})`);
415
+ // 只按名字删本包提供的 skill —— skills remove --skill '*' 会连别的来源的一起删掉
416
+ const code = runSkillsCLI(["remove", ...names, ...targetFlags(opts.agents), ...scopeFlags(scope), "-y"]);
417
+ if (code !== 0) {
418
+ console.error(`✗ 移除 skill 失败(退出码 ${code})`);
419
+ process.exit(code);
420
+ }
421
+
422
+ const agents = mcpAgents(opts);
423
+ for (const target of mcpTargets(agents, scope, opts.mcpAgents.length > 0)) {
424
+ const r = removeServer(target, MCP_SERVER_NAME);
425
+ console.log(r.removed ? ` ✓ 已删除 MCP 条目:${target.file}` : ` · 没有 MCP 条目:${target.file}`);
426
+ }
427
+
428
+ console.log("\n✓ 清理完成");
429
+ }
430
+
431
+ // ---------------------------------------------------------------- main
432
+
433
+ function main() {
434
+ const argv = process.argv.slice(2);
435
+ const cmd = argv[0] && !argv[0].startsWith("-") ? argv[0] : "setup";
436
+ const opts = parseArgs(argv[0] && !argv[0].startsWith("-") ? argv.slice(1) : argv);
437
+
438
+ if (opts.help) {
439
+ console.log(HELP);
440
+ return;
441
+ }
442
+ if (opts.unknown.length) {
443
+ console.error(`✗ 未知选项:${opts.unknown.join(", ")}`);
444
+ console.error(" 用 anov-gis-skills --help 看用法");
445
+ process.exit(2);
446
+ }
447
+
448
+ switch (cmd) {
449
+ case "setup":
450
+ cmdSetup(opts);
451
+ break;
452
+ case "mcp":
453
+ cmdMcp(opts);
454
+ break;
455
+ case "doctor":
456
+ case "verify":
457
+ cmdDoctor(opts);
458
+ break;
459
+ case "uninstall":
460
+ case "remove":
461
+ cmdUninstall(opts);
462
+ break;
463
+ default:
464
+ console.error(`✗ 未知命令:${cmd}`);
465
+ console.error(" 用 anov-gis-skills --help 看用法");
466
+ process.exit(2);
467
+ }
468
+ }
469
+
470
+ main();