@birdie_moblie/open_spec 2.1.7

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 (186) hide show
  1. package/.agents/plugins/marketplace.json +13 -0
  2. package/.claude-plugin/marketplace.json +12 -0
  3. package/.claude-plugin/plugin.json +9 -0
  4. package/.codex-plugin/plugin.json +9 -0
  5. package/LICENSE +21 -0
  6. package/README.md +111 -0
  7. package/bin/openspec.js +3 -0
  8. package/dist/cli/commands/change.d.ts +3 -0
  9. package/dist/cli/commands/change.js +37 -0
  10. package/dist/cli/commands/ext.d.ts +3 -0
  11. package/dist/cli/commands/ext.js +196 -0
  12. package/dist/cli/commands/figma.d.ts +3 -0
  13. package/dist/cli/commands/figma.js +73 -0
  14. package/dist/cli/commands/init.d.ts +3 -0
  15. package/dist/cli/commands/init.js +71 -0
  16. package/dist/cli/commands/opsx.d.ts +3 -0
  17. package/dist/cli/commands/opsx.js +324 -0
  18. package/dist/cli/commands/usage.d.ts +3 -0
  19. package/dist/cli/commands/usage.js +21 -0
  20. package/dist/cli/output.d.ts +8 -0
  21. package/dist/cli/output.js +17 -0
  22. package/dist/cli/program.d.ts +4 -0
  23. package/dist/cli/program.js +42 -0
  24. package/dist/cli/register.d.ts +3 -0
  25. package/dist/cli/register.js +13 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +3 -0
  28. package/dist/config/load.d.ts +7 -0
  29. package/dist/config/load.js +55 -0
  30. package/dist/config/presets.d.ts +129 -0
  31. package/dist/config/presets.js +125 -0
  32. package/dist/config/schema.d.ts +281 -0
  33. package/dist/config/schema.js +178 -0
  34. package/dist/core/archive.d.ts +5 -0
  35. package/dist/core/archive.js +33 -0
  36. package/dist/core/change.d.ts +12 -0
  37. package/dist/core/change.js +69 -0
  38. package/dist/core/code-changes.d.ts +9 -0
  39. package/dist/core/code-changes.js +31 -0
  40. package/dist/core/flow.d.ts +6 -0
  41. package/dist/core/flow.js +99 -0
  42. package/dist/core/gates.d.ts +5 -0
  43. package/dist/core/gates.js +71 -0
  44. package/dist/core/instructions.d.ts +21 -0
  45. package/dist/core/instructions.js +102 -0
  46. package/dist/core/source-operations.d.ts +25 -0
  47. package/dist/core/source-operations.js +94 -0
  48. package/dist/core/sources.d.ts +33 -0
  49. package/dist/core/sources.js +234 -0
  50. package/dist/core/state.d.ts +15 -0
  51. package/dist/core/state.js +75 -0
  52. package/dist/core/status.d.ts +31 -0
  53. package/dist/core/status.js +22 -0
  54. package/dist/core/tasks.d.ts +15 -0
  55. package/dist/core/tasks.js +143 -0
  56. package/dist/core/todo.d.ts +31 -0
  57. package/dist/core/todo.js +114 -0
  58. package/dist/core/types.d.ts +297 -0
  59. package/dist/core/types.js +227 -0
  60. package/dist/core/verify.d.ts +229 -0
  61. package/dist/core/verify.js +391 -0
  62. package/dist/core/worker-evidence.d.ts +71 -0
  63. package/dist/core/worker-evidence.js +253 -0
  64. package/dist/core/worker.d.ts +22 -0
  65. package/dist/core/worker.js +478 -0
  66. package/dist/delivery/claude-install.d.ts +4 -0
  67. package/dist/delivery/claude-install.js +44 -0
  68. package/dist/delivery/distribution.d.ts +3 -0
  69. package/dist/delivery/distribution.js +53 -0
  70. package/dist/delivery/skills.d.ts +36 -0
  71. package/dist/delivery/skills.js +178 -0
  72. package/dist/figma/cache.d.ts +19 -0
  73. package/dist/figma/cache.js +115 -0
  74. package/dist/figma/claude.d.ts +14 -0
  75. package/dist/figma/claude.js +121 -0
  76. package/dist/figma/collect.d.ts +3 -0
  77. package/dist/figma/collect.js +57 -0
  78. package/dist/figma/index.d.ts +35 -0
  79. package/dist/figma/index.js +178 -0
  80. package/dist/figma/media.d.ts +5 -0
  81. package/dist/figma/media.js +77 -0
  82. package/dist/figma/response.d.ts +9 -0
  83. package/dist/figma/response.js +27 -0
  84. package/dist/hooks/command-context.d.ts +19 -0
  85. package/dist/hooks/command-context.js +33 -0
  86. package/dist/hooks/engine.d.ts +61 -0
  87. package/dist/hooks/engine.js +314 -0
  88. package/dist/hooks/plugin-runtime.d.ts +188 -0
  89. package/dist/hooks/plugin-runtime.js +198 -0
  90. package/dist/index.d.ts +99 -0
  91. package/dist/index.js +185 -0
  92. package/dist/marketplace/adapters.d.ts +202 -0
  93. package/dist/marketplace/adapters.js +203 -0
  94. package/dist/marketplace/builtin.d.ts +9 -0
  95. package/dist/marketplace/builtin.js +9 -0
  96. package/dist/marketplace/install.d.ts +38 -0
  97. package/dist/marketplace/install.js +108 -0
  98. package/dist/marketplace/registry.d.ts +71 -0
  99. package/dist/marketplace/registry.js +342 -0
  100. package/dist/marketplace/upgrade.d.ts +39 -0
  101. package/dist/marketplace/upgrade.js +104 -0
  102. package/dist/shared/command-log.d.ts +9 -0
  103. package/dist/shared/command-log.js +55 -0
  104. package/dist/shared/errors.d.ts +7 -0
  105. package/dist/shared/errors.js +14 -0
  106. package/dist/shared/exec.d.ts +16 -0
  107. package/dist/shared/exec.js +86 -0
  108. package/dist/shared/fs.d.ts +18 -0
  109. package/dist/shared/fs.js +93 -0
  110. package/dist/shared/git.d.ts +22 -0
  111. package/dist/shared/git.js +132 -0
  112. package/dist/shared/hash.d.ts +5 -0
  113. package/dist/shared/hash.js +29 -0
  114. package/dist/shared/paths.d.ts +16 -0
  115. package/dist/shared/paths.js +97 -0
  116. package/dist/shared/pkg.d.ts +3 -0
  117. package/dist/shared/pkg.js +23 -0
  118. package/dist/shared/version.d.ts +9 -0
  119. package/dist/shared/version.js +34 -0
  120. package/dist/usage/analyze.d.ts +5 -0
  121. package/dist/usage/analyze.js +212 -0
  122. package/dist/usage/claude.d.ts +9 -0
  123. package/dist/usage/claude.js +59 -0
  124. package/dist/usage/collect.d.ts +11 -0
  125. package/dist/usage/collect.js +71 -0
  126. package/dist/usage/doctor.d.ts +72 -0
  127. package/dist/usage/doctor.js +121 -0
  128. package/dist/usage/parse.d.ts +28 -0
  129. package/dist/usage/parse.js +345 -0
  130. package/dist/usage/types.d.ts +103 -0
  131. package/dist/usage/types.js +2 -0
  132. package/docs/getting-started.md +20 -0
  133. package/docs/guides/configuration.md +33 -0
  134. package/docs/guides/cost-benchmark.md +88 -0
  135. package/docs/guides/plugin-migration.md +65 -0
  136. package/docs/guides/smart-figma.md +51 -0
  137. package/docs/guides/source-reading.md +35 -0
  138. package/docs/guides/ui-conventions-template.md +39 -0
  139. package/docs/guides/worker-evidence.md +73 -0
  140. package/docs/guides/workflow.md +74 -0
  141. package/docs/maintainers/architecture.md +27 -0
  142. package/docs/maintainers/plugin-runtime-contract.md +55 -0
  143. package/package.json +78 -0
  144. package/plugin.json +9 -0
  145. package/presets/backend-service.yaml +19 -0
  146. package/presets/flutter-mobile/plugin-hooks.json +68 -0
  147. package/presets/flutter-mobile/ui-conventions.md +122 -0
  148. package/presets/flutter-mobile.yaml +205 -0
  149. package/presets/web-product.yaml +23 -0
  150. package/skills/opsx-apply/SKILL.md +35 -0
  151. package/skills/opsx-apply/nodes/blocked.md +7 -0
  152. package/skills/opsx-apply/nodes/code-task.md +29 -0
  153. package/skills/opsx-apply/nodes/gate-a.md +9 -0
  154. package/skills/opsx-apply/nodes/stop.md +5 -0
  155. package/skills/opsx-apply/nodes/task-build.md +50 -0
  156. package/skills/opsx-apply/nodes/task-repair.md +20 -0
  157. package/skills/opsx-apply/nodes/task-scout.md +28 -0
  158. package/skills/opsx-apply/nodes/task-verify.md +38 -0
  159. package/skills/opsx-archive/SKILL.md +24 -0
  160. package/skills/opsx-archive/nodes/archive.md +15 -0
  161. package/skills/opsx-explore/SKILL.md +20 -0
  162. package/skills/opsx-explore/nodes/explore.md +3 -0
  163. package/skills/opsx-fix-verify/SKILL.md +30 -0
  164. package/skills/opsx-fix-verify/nodes/repair.md +11 -0
  165. package/skills/opsx-fix-verify/nodes/reverify.md +3 -0
  166. package/skills/opsx-fix-verify/nodes/stop.md +3 -0
  167. package/skills/opsx-gate-decision/SKILL.md +20 -0
  168. package/skills/opsx-gate-decision/nodes/decide.md +15 -0
  169. package/skills/opsx-propose/SKILL.md +36 -0
  170. package/skills/opsx-propose/nodes/blocked.md +13 -0
  171. package/skills/opsx-propose/nodes/collect.md +35 -0
  172. package/skills/opsx-propose/nodes/parse.md +72 -0
  173. package/skills/opsx-propose/nodes/plan.md +65 -0
  174. package/skills/opsx-propose/nodes/stop.md +17 -0
  175. package/skills/opsx-propose/nodes/tech.md +77 -0
  176. package/skills/opsx-propose/scripts/read-json-source.mjs +22 -0
  177. package/skills/opsx-resume/SKILL.md +21 -0
  178. package/skills/opsx-resume/nodes/route.md +12 -0
  179. package/skills/opsx-source-update/SKILL.md +23 -0
  180. package/skills/opsx-source-update/nodes/update.md +14 -0
  181. package/skills/opsx-verify/SKILL.md +32 -0
  182. package/skills/opsx-verify/nodes/blocked.md +3 -0
  183. package/skills/opsx-verify/nodes/failed.md +3 -0
  184. package/skills/opsx-verify/nodes/gate-b.md +7 -0
  185. package/skills/opsx-verify/nodes/stop.md +5 -0
  186. package/skills/opsx-verify/nodes/verify.md +10 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "openspec-bridge",
3
+ "owner": {
4
+ "name": "OpenSpec Contributors"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "openspec",
9
+ "description": "OpenSpec 确定性产品规格工作流",
10
+ "source": { "source": "local", "path": "." }
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "openspec-bridge",
3
+ "owner": {
4
+ "name": "OpenSpec Contributors"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "openspec",
9
+ "source": "./"
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "openspec",
3
+ "description": "OpenSpec 确定性产品规格工作流",
4
+ "version": "2.1.7",
5
+ "author": {
6
+ "name": "OpenSpec Contributors"
7
+ },
8
+ "skills": "./skills/"
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "openspec",
3
+ "version": "2.1.7",
4
+ "description": "OpenSpec 确定性产品规格工作流",
5
+ "author": {
6
+ "name": "OpenSpec Contributors"
7
+ },
8
+ "skills": "./skills/"
9
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 OpenSpec Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # OpenSpec v2
2
+
3
+ OpenSpec 是面向 AI 编码协作的确定性产品规格工作流。v2 使用轻量 packet、统一 Hook、分层 Skill 和按 task category 初始化的 code Worker;每个 code task 拆成 scout → build → verify → repair 四个短命子代理阶段,控制会话只编排、不读代码与截图,宿主不支持 subagent 时主 agent 按同一组节点文件执行。
4
+
5
+ ## 工作流
6
+
7
+ ```text
8
+ init
9
+ -> new change
10
+ -> sources add / collect
11
+ -> parse -> tech(可跳过) -> plan
12
+ -> Gate-A
13
+ -> code workers
14
+ -> Gate-B
15
+ -> verify / fix-verify
16
+ -> Gate-C
17
+ -> openspec/archive/YYYY-MM-DD-<change>/
18
+ ```
19
+
20
+ Skill 入口与状态机节点的对应关系:
21
+
22
+ - `/opsx-propose`:collect → parse → tech → plan,停在 Gate-A
23
+ - `/opsx-apply`:批准 Gate-A 后逐个 code worker,停在 Gate-B
24
+ - `/opsx-verify`:批准 Gate-B 后跑 QA,停在 Gate-C
25
+ - `/opsx-fix-verify` / `/opsx-archive` / `/opsx-resume` / `/opsx-source-update` / `/opsx-gate-decision` / `/opsx-explore`
26
+
27
+ ## 快速开始
28
+
29
+ 需要 Node.js 22 或 24。本仓库通过 `package.json` 的 `packageManager` 字段钉住 pnpm 11。
30
+
31
+ ### 安装 pnpm
32
+
33
+ 先执行 `pnpm -v`。没有该命令时,已有 Node 的环境优先用 Corepack(会按仓库钉住的版本准备 pnpm):
34
+
35
+ ```bash
36
+ corepack enable
37
+ ```
38
+
39
+ 其他方式:
40
+
41
+ - macOS(Homebrew):`brew install pnpm`
42
+ - POSIX 独立脚本:`curl -fsSL https://get.pnpm.io/install.sh | sh -`(pnpm 11 的独立脚本不支持 Intel Mac,可用 Homebrew)
43
+ - npm:`npx get-pnpm`
44
+
45
+ 装完后新开一个终端,确认 `pnpm -v` 可用。
46
+
47
+ ### 构建并注册全局 `openspec` 命令
48
+
49
+ 在仓库根目录:
50
+
51
+ ```bash
52
+ pnpm install
53
+ pnpm run build
54
+ pnpm add -g .
55
+ ```
56
+
57
+ pnpm 11 已移除 `pnpm link --global`。把本地 CLI 注册成全局命令的方式是在包目录执行 `pnpm add -g .`:它读取 `package.json` 的 `bin.openspec`(`./bin/openspec.js`),之后任意目录都能直接跑 `openspec`。该入口依赖 `dist/`,必须先 `pnpm run build`。
58
+
59
+ 不想注册全局时,继续用 `node bin/openspec.js <命令>`。
60
+
61
+ ```bash
62
+ openspec init --tools claude,codex,cursor --preset flutter-mobile --yes
63
+ openspec new change improve-checkout
64
+ openspec sources add --change improve-checkout --notes "简化结算确认" --json
65
+ ```
66
+
67
+ ### 全局命令常见问题
68
+
69
+ **`pnpm link --global` 报错,或提示没有 `--global`。** pnpm 11 删除了该选项。改在仓库根目录执行 `pnpm add -g .`。卸载:`pnpm remove -g @birdie_moblie/open_spec`。
70
+
71
+ **`ERR_PNPM_NO_GLOBAL_BIN_DIR`。** 全局 bin 目录未创建,或不在 `PATH` 里。执行 `pnpm setup`,再 `source ~/.zshrc`(bash 则 `~/.bashrc`)或重开终端。从 pnpm 10 升到 11 后通常要再跑一次 setup。不要用 `sudo`(会写到 root 的家目录,而不是当前用户)。
72
+
73
+ **`openspec: command not found`。** `PATH` 没有包含 pnpm 全局 bin。看 `pnpm bin -g` 的输出,确认该目录出现在 `echo $PATH` 中。zsh 可能缓存了旧的命令位置,执行 `hash -r`。也可用 `ls "$(pnpm bin -g)/openspec"` 确认二进制是否已注册。
74
+
75
+ **能跑但版本不对,或 `which openspec` 指向别处。** `which -a openspec` 看优先级。以前用 `npm install -g` 或项目里的 `install-cli.mjs` 装过的副本可能排在前面。卸掉冲突的 npm 全局包,或让 `pnpm bin -g` 在 `PATH` 里更靠前。包名是 `@birdie_moblie/open_spec`,命令名仍是 `openspec`。
76
+
77
+ **`Cannot find module '.../dist/cli.js'`。** 还没 build,或 `dist/` 被删了。重新 `pnpm run build`。改 TypeScript 源码后也要再 build;一般不必重新 `pnpm add -g .`,除非改了 `bin` 字段。
78
+
79
+ ### 团队成员
80
+
81
+ 已初始化的业务项目里用安装脚本对齐 CLI 版本:
82
+
83
+ ```bash
84
+ node openspec/install-cli.mjs
85
+ openspec doctor --json
86
+ ```
87
+
88
+ 成本实验与只读用量分析见 [成本测试指南](docs/guides/cost-benchmark.md)。支持 Codex/Claude 日志去重、父子线程核算、明确费用口径,以及保留旧默认输出的精简协议。
89
+
90
+ `init --yes` 才会调用 Claude/Codex 的原生 marketplace;Cursor 返回需要在 UI 完成的 `pending_user_action`。CI 或其他非 TTY 环境未给确认时也返回 pending(退出码 2),`--skip-plugins` 会把跳过决定写入项目 runtime receipt。可用 `OPENSPEC_HOME` 隔离 marketplace cache。
91
+
92
+ ## 开发验证
93
+
94
+ ```bash
95
+ pnpm run lint
96
+ pnpm run build
97
+ pnpm run typecheck:test
98
+ pnpm test
99
+ ```
100
+
101
+ ## 文档
102
+
103
+ - [快速开始](docs/getting-started.md)
104
+ - [工作流](docs/guides/workflow.md)
105
+ - [项目配置](docs/guides/configuration.md)
106
+ - [架构](docs/maintainers/architecture.md)
107
+ - [Worker 回执](docs/guides/worker-evidence.md)
108
+ - [成本实验](docs/guides/cost-benchmark.md)
109
+ - [插件运行契约](docs/maintainers/plugin-runtime-contract.md)
110
+ - [插件与宿主迁移](docs/guides/plugin-migration.md)
111
+ - [版本沿革(V1 → V2)](docs/history/README.md)
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import '../dist/cli.js';
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerChange(program: Command): void;
3
+ //# sourceMappingURL=change.d.ts.map
@@ -0,0 +1,37 @@
1
+ import { createChange, listChangeNames, project, resolveChangeName, } from '../../index.js';
2
+ import { changeStatus } from '../../core/status.js';
3
+ import { emitJson, jsonMode } from '../output.js';
4
+ export function registerChange(program) {
5
+ const created = program
6
+ .command('new')
7
+ .description('创建资源');
8
+ created
9
+ .command('change <name>')
10
+ .description('创建新的 change')
11
+ .option('--description <text>', '可选说明')
12
+ .action((name, options, command) => {
13
+ const { projectRoot } = project();
14
+ const state = createChange(projectRoot, name, options.description);
15
+ emitJson(jsonMode(command), state, () => `已创建 change ${name}`);
16
+ });
17
+ program
18
+ .command('list')
19
+ .description('列出 change')
20
+ .action((options, command) => {
21
+ const { projectRoot } = project();
22
+ const names = listChangeNames(projectRoot);
23
+ emitJson(jsonMode(command.parent ?? command), { changes: names }, () => names.join('\n') || '(empty)');
24
+ void options;
25
+ });
26
+ program
27
+ .command('status')
28
+ .description('查看 change 状态与下一步')
29
+ .option('--change <name>', 'change 名称')
30
+ .action((options, command) => {
31
+ const { projectRoot, config } = project();
32
+ const name = resolveChangeName(projectRoot, options.change);
33
+ const payload = changeStatus(projectRoot, name, config);
34
+ emitJson(jsonMode(command), payload, () => `${name} @ ${payload.node} → ${payload.next.action}`);
35
+ });
36
+ }
37
+ //# sourceMappingURL=change.js.map
@@ -0,0 +1,3 @@
1
+ import { type Command } from 'commander';
2
+ export declare function registerExt(program: Command): void;
3
+ //# sourceMappingURL=ext.d.ts.map
@@ -0,0 +1,196 @@
1
+ import { Option } from 'commander';
2
+ import { costDoctor } from '../../usage/doctor.js';
3
+ import { addMarketplace, assertPluginsPresent, BUILTIN_MARKETPLACE, installPlugin, inspectHookEntries, inspectMarketplaceDependencies, listMarketplaces, listPresets, loadPreset, project, verifyDelivery, verifyInstallCli, VERSION, PACKAGE_NAME, } from '../../index.js';
4
+ import { keptMarketplaceRefs, mergePreset, scaffoldPreset } from '../../config/presets.js';
5
+ import { upgradeMarketplace } from '../../marketplace/upgrade.js';
6
+ import { saveConfig as writeConfig } from '../../config/load.js';
7
+ import { emitJson, jsonMode } from '../output.js';
8
+ import { AppError } from '../../shared/errors.js';
9
+ import { readInstallReceipt, writeInstallReceipt } from '../../marketplace/install.js';
10
+ const ENTRY_PENDING_CODES = new Set(['marketplace_not_locked', 'marketplace_cache_missing']);
11
+ export function registerExt(program) {
12
+ program
13
+ .command('doctor')
14
+ .description('检查项目就绪度')
15
+ .option('--cost', '只读检查成本测试环境与实际用量')
16
+ .addOption(new Option('--host <host>', '成本诊断宿主').choices(['codex', 'claude']).default('codex'))
17
+ .option('--session <path...>', '实际 JSONL 日志,可重复', (value, previous) => [...previous, value], [])
18
+ .option('--capability <name>', '实验显式要求的能力,可重复', (value, previous) => [...previous, value], [])
19
+ .option('--evidence <path>', '能力与隔离探针证据清单')
20
+ .option('--expect-model <model>', '固定模型')
21
+ .option('--expect-effort <effort>', '固定推理配置')
22
+ .action((options, command) => {
23
+ if (options.cost) {
24
+ const payload = costDoctor({ host: options.host, sessions: options.session, evidence: options.evidence,
25
+ model: options.expectModel, effort: options.expectEffort, capabilities: options.capability, cwd: process.cwd() });
26
+ emitJson(jsonMode(command), payload, () => payload.ok ? 'cost doctor ok' : `cost doctor gaps: ${payload.gaps.join(', ')}`);
27
+ if (!payload.ok)
28
+ process.exitCode = 2;
29
+ return;
30
+ }
31
+ const { projectRoot, config } = project();
32
+ const hosts = (config.tools ?? []);
33
+ const delivery = hosts.map((host) => ({ host, ...verifyDelivery(projectRoot, host) }));
34
+ const installer = verifyInstallCli(projectRoot, VERSION, PACKAGE_NAME);
35
+ const dependencies = inspectMarketplaceDependencies(projectRoot, config.marketplaces ?? [], config.plugins ?? []);
36
+ const entries = inspectHookEntries(projectRoot, config);
37
+ const installReceipt = readInstallReceipt(projectRoot);
38
+ const pluginDelivery = (config.plugins ?? []).flatMap((plugin) => hosts.map((host) => {
39
+ const selector = `${plugin.name}@${plugin.marketplace}`;
40
+ const record = installReceipt.find((item) => item.plugin === plugin.name
41
+ && item.marketplace === plugin.marketplace
42
+ && item.host === host);
43
+ const dependency = dependencies.find((item) => item.kind === 'plugin' && item.id === selector);
44
+ const ok = record?.status === 'installed'
45
+ && dependency?.ok === true
46
+ && record.digest === dependency.digest
47
+ && record.resolvedSha === dependency.resolvedSha;
48
+ return {
49
+ plugin: selector,
50
+ host,
51
+ status: record?.status ?? 'missing',
52
+ ok,
53
+ };
54
+ }));
55
+ const payload = {
56
+ ok: installer.ok
57
+ && delivery.every((item) => item.ok || hosts.length === 0)
58
+ && dependencies.every((item) => item.ok)
59
+ && pluginDelivery.every((item) => item.ok)
60
+ && entries.every((item) => item.ok),
61
+ projectRoot,
62
+ schema: config.schema,
63
+ builtinMarketplace: BUILTIN_MARKETPLACE,
64
+ delivery,
65
+ installer,
66
+ dependencies,
67
+ pluginDelivery,
68
+ entries,
69
+ };
70
+ emitJson(jsonMode(command), payload, () => payload.ok ? 'doctor ok' : 'doctor found issues');
71
+ if (!payload.ok) {
72
+ const integrityFailure = delivery.some((item) => item.status === 'failed' || (item.status === 'installed' && !item.ok))
73
+ || Boolean(installer.actual && !installer.ok)
74
+ || dependencies.some((item) => item.code?.includes('mismatch') || item.code?.includes('digest') || item.code === 'plugin_conflict'
75
+ || item.code === 'plugin_version_too_low')
76
+ || pluginDelivery.some((item) => item.status === 'failed' || (item.status === 'installed' && !item.ok))
77
+ // 未锁定/缓存缺失属于尚未安装(2);入口缺失或类型阶段不符是已安装内容与配置不兼容(1)。
78
+ || entries.some((item) => !item.ok && !ENTRY_PENDING_CODES.has(item.code ?? ''));
79
+ process.exitCode = integrityFailure ? 1 : 2;
80
+ }
81
+ void options;
82
+ });
83
+ const preset = program.command('preset').description('团队 preset');
84
+ preset.command('list').action((opts, command) => {
85
+ const items = listPresets().map((item) => ({ id: item.id, title: item.title, version: item.version }));
86
+ emitJson(jsonMode(command), { presets: items }, () => items.map((item) => item.id).join('\n'));
87
+ void opts;
88
+ });
89
+ preset.command('show <id>').action((id, opts, command) => {
90
+ emitJson(jsonMode(command), loadPreset(id), () => id);
91
+ void opts;
92
+ });
93
+ preset.command('apply <id>').action((id, opts, command) => {
94
+ const { projectRoot, config } = project();
95
+ const preset = loadPreset(id);
96
+ const keptMarketplaces = keptMarketplaceRefs(config, preset);
97
+ writeConfig(projectRoot, mergePreset(config, preset));
98
+ const scaffold = scaffoldPreset(projectRoot, preset);
99
+ emitJson(jsonMode(command), { id, scaffold, keptMarketplaces }, () => [
100
+ `applied ${id}`,
101
+ ...scaffold.map((item) => ` ${item.status === 'created' ? '生成' : '保留已有'} ${item.path}`),
102
+ ...keptMarketplaces.map((item) => ` 保留宿主 marketplace ${item.name}@${item.ref.slice(0, 12)}(preset 基线 ${item.presetRef.slice(0, 12)})`),
103
+ ].join('\n'));
104
+ void opts;
105
+ });
106
+ const marketplace = program.command('marketplace').description('插件市场');
107
+ marketplace.command('list').action((opts, command) => {
108
+ const { projectRoot } = project();
109
+ const items = listMarketplaces(projectRoot);
110
+ emitJson(jsonMode(command), { marketplaces: items }, () => items.map((item) => `${item.name}@${item.ref}`).join('\n'));
111
+ void opts;
112
+ });
113
+ marketplace
114
+ .command('add [repo]')
115
+ .option('--name <name>', '名称')
116
+ .option('--ref <ref>', 'git ref')
117
+ .option('--dir <path>', '本地目录')
118
+ .action((repo, options, command) => {
119
+ const { projectRoot } = project();
120
+ const record = addMarketplace({
121
+ projectRoot,
122
+ repo,
123
+ name: options.name,
124
+ ref: options.ref,
125
+ dir: options.dir,
126
+ });
127
+ emitJson(jsonMode(command), record, () => `added ${record.name}@${record.ref}`);
128
+ });
129
+ marketplace
130
+ .command('upgrade <name>')
131
+ .description('把宿主 config 中的远端 marketplace 升级到新的不可变 ref:校验插件版本与入口后重装,失败时保持旧版本')
132
+ .requiredOption('--ref <ref>', '完整 Git SHA 或 refs/tags/<tag>')
133
+ .option('--allow-active-changes', '存在进行中的 change 时仍升级;这些 change 的来源与回执需重新采集或派发')
134
+ .action((name, options, command) => {
135
+ const { projectRoot } = project();
136
+ const result = upgradeMarketplace({
137
+ projectRoot, name, ref: options.ref, allowActiveChanges: Boolean(options.allowActiveChanges),
138
+ });
139
+ emitJson(jsonMode(command), result, () => [
140
+ `upgraded ${result.marketplace}: ${result.from.ref.slice(0, 12)} → ${result.to.ref.slice(0, 12)}(${result.status})`,
141
+ ...result.plugins.map((item) => ` ${item.name} ${item.version ?? '未声明版本'}${item.minVersion ? `(最低 ${item.minVersion})` : ''}`),
142
+ ...result.issues.map((item) => ` 问题 ${item.scope}: ${item.error}`),
143
+ ' 按插件 Skill 核对宿主工具依赖后,运行 openspec doctor',
144
+ ].join('\n'));
145
+ if (result.status === 'partial')
146
+ process.exitCode = 1;
147
+ else if (result.status === 'pending_user_action')
148
+ process.exitCode = 2;
149
+ });
150
+ const plugin = program.command('plugin').description('安装 marketplace 插件');
151
+ plugin
152
+ .command('install <selector>')
153
+ .option('--host <host>', 'claude | codex | cursor', 'claude')
154
+ .action((selector, options, command) => {
155
+ const { projectRoot, config } = project();
156
+ const host = parseHost(options.host);
157
+ const at = String(selector).indexOf('@');
158
+ const name = at >= 0 ? selector.slice(0, at) : selector;
159
+ const marketplace = at >= 0 ? selector.slice(at + 1) : BUILTIN_MARKETPLACE.name;
160
+ if (!name || !marketplace)
161
+ throw new AppError('invalid_plugin_selector', `插件 selector 不合法: ${selector}`);
162
+ const declared = (config.plugins ?? []).find((item) => item.name === name && item.marketplace === marketplace);
163
+ assertPluginsPresent(projectRoot, [{ name, marketplace, minVersion: declared?.minVersion }]);
164
+ const result = installPlugin({
165
+ plugin: name,
166
+ marketplace,
167
+ host,
168
+ projectRoot,
169
+ });
170
+ const retained = readInstallReceipt(projectRoot).filter((item) => item.plugin !== name
171
+ || item.marketplace !== marketplace || item.host !== host);
172
+ writeInstallReceipt(projectRoot, [...retained, result]);
173
+ emitJson(jsonMode(command), result, () => `${result.status} ${selector}`);
174
+ if (result.status === 'failed')
175
+ process.exitCode = 1;
176
+ else if (result.status === 'pending_user_action')
177
+ process.exitCode = 2;
178
+ });
179
+ const configCmd = program.command('config').description('项目配置');
180
+ configCmd.command('path').action((opts, command) => {
181
+ const { projectRoot } = project();
182
+ emitJson(jsonMode(command), { path: `${projectRoot}/openspec/config.yaml` }, () => `${projectRoot}/openspec/config.yaml`);
183
+ void opts;
184
+ });
185
+ configCmd.command('get').action((opts, command) => {
186
+ const { config } = project();
187
+ emitJson(jsonMode(command), config, () => 'ok');
188
+ void opts;
189
+ });
190
+ }
191
+ function parseHost(value) {
192
+ if (!['claude', 'codex', 'cursor'].includes(value))
193
+ throw new AppError('invalid_host', `未知宿主: ${value}`);
194
+ return value;
195
+ }
196
+ //# sourceMappingURL=ext.js.map
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerFigma(sources: Command): void;
3
+ //# sourceMappingURL=figma.d.ts.map
@@ -0,0 +1,73 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { project } from '../../index.js';
5
+ import { artifactPath, artifactRel } from '../../core/change.js';
6
+ import { resolveInside } from '../../shared/paths.js';
7
+ import { figmaSource, projectMetadata, indexPage } from '../../figma/index.js';
8
+ import { claudeFigmaSettings, figmaHook } from '../../figma/claude.js';
9
+ import { imagePixelLimit } from '../../figma/media.js';
10
+ export function registerFigma(sources) {
11
+ const figma = sources.command('figma').description('内建 Smart Figma:页面索引与会话级官方 MCP 接入');
12
+ figma.command('index').requiredOption('--source <url>', 'Figma 来源 URL')
13
+ .requiredOption('--input <path>', '项目内完整 metadata JSON/XML 文件')
14
+ .option('--output <path>', '完整机器索引保存到项目内文件;stdout 只返回有界分页')
15
+ .action(options => {
16
+ const file = resolveInside(process.cwd(), options.input);
17
+ const text = readFileSync(file, 'utf8');
18
+ const response = text.trimStart().startsWith('<') ? text : JSON.parse(text);
19
+ const index = projectMetadata(response, figmaSource(options.source));
20
+ if (options.output) {
21
+ const output = resolveInside(process.cwd(), options.output);
22
+ mkdirSync(path.dirname(output), { recursive: true });
23
+ writeFileSync(output, JSON.stringify(index, null, 2) + '\n');
24
+ }
25
+ console.log(JSON.stringify({ indexPath: options.output ?? null, ...indexPage(index) }));
26
+ });
27
+ figma.command('show').requiredOption('--index <path>', '项目内页面索引文件')
28
+ .option('--offset <n>', '下一页偏移', '0')
29
+ .action(options => {
30
+ const index = JSON.parse(readFileSync(resolveInside(process.cwd(), options.index), 'utf8'));
31
+ console.log(JSON.stringify(indexPage({ ...index, unresolved: index.unresolved ?? [] }, Number(options.offset))));
32
+ });
33
+ figma.command('setup').requiredOption('--change <name>', 'change 名称')
34
+ .option('--min-image-pixels <n>', '当前提供商 PNG 最小总像素;省略则不限制', imagePixelLimit)
35
+ .description('生成此 change 的 Claude --settings 文件;不修改全局设置或认证')
36
+ .action(options => {
37
+ const { projectRoot } = project();
38
+ const cli = fileURLToPath(new URL('../../../bin/openspec.js', import.meta.url));
39
+ const settings = claudeFigmaSettings(cli, projectRoot, options.change, options.minImagePixels);
40
+ const relative = '.opsx/figma-claude-settings.json';
41
+ const file = artifactPath(projectRoot, options.change, relative);
42
+ mkdirSync(path.dirname(file), { recursive: true });
43
+ writeFileSync(file, JSON.stringify(settings, null, 2));
44
+ console.log(JSON.stringify({ settingsPath: artifactRel(options.change, relative),
45
+ instruction: '本次 Claude 会话通过 --settings 加载此文件;官方 Figma MCP 需已认证。索引是候选,coding 仍需详情与截图。' }));
46
+ });
47
+ figma.command('hook').requiredOption('--project-root <path>', '固定项目根目录')
48
+ .requiredOption('--change <name>', '固定 change')
49
+ .option('--min-image-pixels <n>', '当前提供商 PNG 最小总像素;省略则不限制', imagePixelLimit)
50
+ .action(async (options) => {
51
+ const chunks = [];
52
+ let bytes = 0;
53
+ for await (const chunk of process.stdin) {
54
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
55
+ bytes += buffer.length;
56
+ if (bytes > 20 * 1024 * 1024)
57
+ throw new Error('Smart Figma hook input exceeds 20 MiB');
58
+ chunks.push(buffer);
59
+ }
60
+ const event = JSON.parse(Buffer.concat(chunks).toString('utf8'));
61
+ try {
62
+ console.log(JSON.stringify(figmaHook(options.projectRoot, options.change, event, options.minImagePixels)));
63
+ }
64
+ catch (error) {
65
+ const message = `Smart Figma 接入失败,未形成可用索引: ${error.message}`;
66
+ const hookSpecificOutput = event.hook_event_name === 'PreToolUse'
67
+ ? { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: message }
68
+ : { hookEventName: 'PostToolUse', updatedMCPToolOutput: [{ type: 'text', text: JSON.stringify({ ok: false, error: message }) }] };
69
+ console.log(JSON.stringify({ hookSpecificOutput }));
70
+ }
71
+ });
72
+ }
73
+ //# sourceMappingURL=figma.js.map
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerInit(program: Command): void;
3
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1,71 @@
1
+ import path from 'node:path';
2
+ import { initProject, loadPreset } from '../../index.js';
3
+ import { AppError } from '../../shared/errors.js';
4
+ import { emitJson, jsonMode } from '../output.js';
5
+ import { configPathOf, loadConfig } from '../../config/load.js';
6
+ export function registerInit(program) {
7
+ program
8
+ .command('init [path]')
9
+ .description('初始化 OpenSpec 项目')
10
+ .option('--tools <list>', '宿主工具,逗号分隔:claude,codex,cursor')
11
+ .option('--preset <id>', '应用团队 preset')
12
+ .option('--yes', '非交互确认(安装 preset 插件)')
13
+ .option('--skip-plugins', '跳过 marketplace 插件安装')
14
+ .action(async (targetPath, options, command) => {
15
+ const tools = parseTools(options.tools);
16
+ let confirmed = Boolean(options.yes);
17
+ const projectRoot = path.resolve(targetPath ?? process.cwd());
18
+ if (!confirmed
19
+ && !options.skipPlugins
20
+ && needsConfirmation(projectRoot, tools, options.preset)
21
+ && process.stdin.isTTY
22
+ && process.stdout.isTTY) {
23
+ const { confirm } = await import('@inquirer/prompts');
24
+ confirmed = await confirm({ message: '使用宿主原生 marketplace 安装或更新 OpenSpec 及 preset 插件?' });
25
+ }
26
+ // 进度只在文本模式打印,且走 stderr:--json 的 stdout/stderr 都留给机器消费
27
+ const json = jsonMode(command);
28
+ const result = initProject({
29
+ cwd: targetPath,
30
+ tools,
31
+ preset: options.preset,
32
+ yes: confirmed,
33
+ skipPlugins: Boolean(options.skipPlugins),
34
+ onProgress: json ? undefined : (message) => process.stderr.write(`${message}\n`),
35
+ });
36
+ const unfilled = result.scaffold.filter((item) => item.status === 'created');
37
+ emitJson(json, result, () => [
38
+ `${result.status}: ${result.projectRoot}`,
39
+ ...result.issues.map((issue) => ` - ${issue.scope}: ${issue.error.trim().split('\n').join('\n ')}`),
40
+ ...(result.issues.length > 0 ? [' 详情见 openspec/.runtime/init.json;修复后重跑同一条 init 命令即可。'] : []),
41
+ ...(unfilled.length > 0
42
+ ? [' 下一步:填写以下模板并删除其中的 openspec:fill-me 标记,否则相关 Worker 会以 blocked 停下:',
43
+ ...unfilled.map((item) => ` - ${item.path}${item.description ? `(${item.description})` : ''}`)]
44
+ : []),
45
+ ].join('\n'));
46
+ if (result.status === 'partial')
47
+ process.exitCode = 1;
48
+ else if (result.status === 'pending_user_action')
49
+ process.exitCode = 2;
50
+ });
51
+ }
52
+ function needsConfirmation(projectRoot, tools, presetId) {
53
+ const existing = configPathOf(projectRoot) ? loadConfig(projectRoot) : null;
54
+ const preset = presetId ? loadPreset(presetId) : null;
55
+ return (tools ?? existing?.tools ?? []).length > 0
56
+ || (existing?.plugins ?? []).length > 0
57
+ || (existing?.marketplaces ?? []).length > 0
58
+ || (preset?.plugins ?? []).length > 0
59
+ || (preset?.marketplaces ?? []).length > 0;
60
+ }
61
+ function parseTools(raw) {
62
+ if (!raw)
63
+ return undefined;
64
+ const allowed = ['claude', 'codex', 'cursor'];
65
+ const values = raw.split(',').map((item) => item.trim()).filter(Boolean);
66
+ const invalid = values.filter((item) => !allowed.includes(item));
67
+ if (invalid.length > 0)
68
+ throw new AppError('invalid_host', `未知宿主: ${invalid.join(', ')}`);
69
+ return [...new Set(values)];
70
+ }
71
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1,3 @@
1
+ import { type Command } from 'commander';
2
+ export declare function registerOpsx(program: Command): void;
3
+ //# sourceMappingURL=opsx.d.ts.map