@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,198 @@
1
+ import path from 'node:path';
2
+ import { closeSync, openSync, statSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { z } from 'zod';
5
+ import { exists, ensureDir, readJson, readText, writeJson, writeText } from '../shared/fs.js';
6
+ import { sha256, sha256File } from '../shared/hash.js';
7
+ import { resolveInside } from '../shared/paths.js';
8
+ import { checkoutFingerprint } from '../shared/git.js';
9
+ import { AppError } from '../shared/errors.js';
10
+ import { PluginIdentitySchema, resolveAdapterEntry, resolveHookEntry } from '../marketplace/adapters.js';
11
+ export const PluginBindingSchema = z.object({
12
+ use: z.string(), kind: z.enum(['adapter', 'hook']), operation: z.enum(['read', 'preview', 'hook']),
13
+ identity: PluginIdentitySchema, input: z.record(z.string(), z.unknown()).optional(),
14
+ inputFiles: z.array(z.string()).optional(), inputDescription: z.string().optional(),
15
+ }).strict();
16
+ export const PluginReceiptSchema = z.object({
17
+ binding: PluginBindingSchema, inputDigest: z.string(),
18
+ files: z.record(z.string(), z.string()), outputs: z.record(z.string(), z.string()),
19
+ workerId: z.string().optional(), stepId: z.string().optional(),
20
+ }).strict();
21
+ export const PluginEvidenceSchema = z.object({
22
+ path: z.string(), sha256: z.string(), binding: PluginBindingSchema,
23
+ workerId: z.string().optional(), stepId: z.string().optional(),
24
+ }).strict();
25
+ /** Keep dependency closures out of control responses and shared worker indexes. */
26
+ export function savePluginEvidence(root, outputDir, receipt) {
27
+ const file = resolveInside(outputDir, 'receipt.json');
28
+ writeJson(file, receipt);
29
+ return { path: path.relative(root, file), sha256: sha256File(file), binding: receipt.binding,
30
+ ...(receipt.workerId ? { workerId: receipt.workerId } : {}), ...(receipt.stepId ? { stepId: receipt.stepId } : {}) };
31
+ }
32
+ export function pluginEvidenceCurrent(root, reference) {
33
+ try {
34
+ const file = resolveInside(root, reference.path, 'plugin_evidence_outside_project');
35
+ if (sha256File(file) !== reference.sha256)
36
+ return false;
37
+ const receipt = PluginReceiptSchema.parse(readJson(file));
38
+ return JSON.stringify(receipt.binding) === JSON.stringify(reference.binding)
39
+ && receipt.workerId === reference.workerId && receipt.stepId === reference.stepId
40
+ && pluginReceiptCurrent(root, receipt);
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ export function pluginHook(projectRoot, hook) {
47
+ const use = hook.use;
48
+ const preview = hook.operation === 'preview';
49
+ const resolved = preview ? resolveAdapterEntry(projectRoot, use) : resolveHookEntry(projectRoot, use);
50
+ const entry = resolved.entry;
51
+ if (preview) {
52
+ if (!('operations' in entry) || entry.kind !== 'command' || !entry.operations.preview)
53
+ throw new AppError('adapter_operation_missing', `adapter 不支持 preview: ${use}`);
54
+ }
55
+ else if (!('stages' in entry) || entry.type !== hook.type || hook.stage === 'collect' || !entry.stages.includes(hook.stage)) {
56
+ throw new AppError('plugin_hook_mismatch', `插件 Hook 类型或阶段不符: ${use} (${hook.stage}/${hook.type})`);
57
+ }
58
+ const inputDescription = 'operations' in entry ? entry.operations.preview?.inputDescription : entry.inputDescription;
59
+ const binding = {
60
+ use, kind: preview ? 'adapter' : 'hook', operation: preview ? 'preview' : 'hook', identity: resolved.identity,
61
+ ...(hook.input ? { input: hook.input } : {}), ...(hook.inputFiles ? { inputFiles: hook.inputFiles } : {}),
62
+ ...(inputDescription ? { inputDescription } : {}),
63
+ };
64
+ if ('type' in entry && entry.type === 'skill') {
65
+ const skillPath = resolveInside(resolved.pluginRoot, entry.path, 'plugin_skill_outside_root');
66
+ if (!exists(skillPath) || !statSync(skillPath).isFile())
67
+ throw new AppError('plugin_skill_missing', `插件技能不存在: ${use}`);
68
+ return { binding, skillPath };
69
+ }
70
+ return { binding };
71
+ }
72
+ export function sameIdentity(a, b) {
73
+ return a.name === b.name && a.marketplace === b.marketplace && a.resolvedSha === b.resolvedSha && a.digest === b.digest;
74
+ }
75
+ export function pluginBindingCurrent(root, binding) {
76
+ try {
77
+ const resolved = binding.kind === 'adapter' ? resolveAdapterEntry(root, binding.use) : resolveHookEntry(root, binding.use);
78
+ return sameIdentity(binding.identity, resolved.identity);
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ export function resolvePluginCommand(root, binding) {
85
+ const resolved = binding.kind === 'adapter' ? resolveAdapterEntry(root, binding.use) : resolveHookEntry(root, binding.use);
86
+ if (!sameIdentity(binding.identity, resolved.identity))
87
+ throw new AppError('plugin_identity_changed', `锁定插件已变化,停止旧 Worker 命令后用 worker create --refresh 显式刷新: ${binding.use}`);
88
+ const entry = resolved.entry;
89
+ const operation = 'operations' in entry
90
+ ? binding.operation === 'hook' ? undefined : entry.operations[binding.operation]
91
+ : entry.type === 'command' ? entry : undefined;
92
+ if (!operation)
93
+ throw new AppError('plugin_operation_missing', `入口不支持 ${binding.operation}: ${binding.use}`);
94
+ for (const token of [operation.command, ...(operation.args ?? [])]) {
95
+ if (!token.includes('{pluginRoot}'))
96
+ continue;
97
+ if (!token.startsWith('{pluginRoot}/') || token.indexOf('{pluginRoot}', 1) !== -1)
98
+ throw new AppError('plugin_entry_path_invalid', 'pluginRoot 必须是独立文件参数');
99
+ const file = resolveInside(resolved.pluginRoot, token.slice('{pluginRoot}/'.length), 'plugin_entry_outside_root');
100
+ if (!exists(file) || !statSync(file).isFile())
101
+ throw new AppError('plugin_entry_missing', `插件入口文件不存在: ${token}`);
102
+ }
103
+ return { operation, pluginRoot: resolved.pluginRoot };
104
+ }
105
+ export function readPluginInput(root, binding, file) {
106
+ const dynamic = file ? z.record(z.string(), z.unknown()).parse(readJson(resolveInside(root, file, 'plugin_input_outside_project'))) : {};
107
+ return { input: { ...binding.input, ...dynamic }, files: [...(binding.inputFiles ?? []), ...(file ? [file] : [])] };
108
+ }
109
+ export function digestFiles(root, files) {
110
+ return Object.fromEntries([...new Set(files)].map((file) => {
111
+ const absolute = resolveInside(root, file, 'plugin_dependency_outside_project');
112
+ if (!exists(absolute) || !statSync(absolute).isFile())
113
+ throw new AppError('plugin_dependency_missing', `依赖文件不存在: ${file}`);
114
+ return [path.relative(root, absolute).replaceAll('\\', '/'), sha256File(absolute)];
115
+ }));
116
+ }
117
+ export function digestsMatch(root, files) {
118
+ try {
119
+ return Object.entries(files).every(([file, digest]) => digestFiles(root, [file])[file] === digest);
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
125
+ /** Commands run as argv with JSON stdin. Complete stdout/stderr never share a file. */
126
+ export function runPluginCommand(root, binding, context, inputFiles = []) {
127
+ const { operation, pluginRoot } = resolvePluginCommand(root, binding);
128
+ const substitute = (value) => value.replaceAll('{pluginRoot}', pluginRoot);
129
+ const checkoutBefore = binding.operation === 'hook' ? undefined : checkoutFingerprint(root);
130
+ const sourceFiles = [...(context.source ? [context.source] : []), ...(context.sources ?? [])]
131
+ .map((source) => path.relative(root, source.artifact));
132
+ const before = digestFiles(root, [...inputFiles, ...sourceFiles]);
133
+ ensureDir(context.outputDir);
134
+ const stdoutPath = resolveInside(context.outputDir, 'stdout.json');
135
+ const stderrPath = resolveInside(context.outputDir, 'stderr.log');
136
+ const inputPath = resolveInside(context.outputDir, 'input.json');
137
+ writeJson(inputPath, context);
138
+ const stdout = openSync(stdoutPath, 'w');
139
+ const stderr = openSync(stderrPath, 'w');
140
+ let result;
141
+ try {
142
+ result = spawnSync(substitute(operation.command), (operation.args ?? []).map(substitute), {
143
+ cwd: root, input: JSON.stringify(context), stdio: ['pipe', stdout, stderr],
144
+ timeout: operation.timeoutMs ?? 120000, killSignal: 'SIGKILL',
145
+ });
146
+ }
147
+ finally {
148
+ closeSync(stdout);
149
+ closeSync(stderr);
150
+ }
151
+ let ok = result.status === 0 && !result.error;
152
+ let error = result.error?.message ?? (result.signal ? `命令终止: ${result.signal}` : '');
153
+ let data;
154
+ let dependencies = [];
155
+ let artifacts = [];
156
+ try {
157
+ if (ok) {
158
+ const envelope = z.object({ data: z.unknown(), dependencies: z.array(z.string()).optional(), artifacts: z.array(z.string()).optional() }).strict().parse(readJson(stdoutPath));
159
+ if (!Object.hasOwn(envelope, 'data'))
160
+ throw new Error('stdout 缺少 data');
161
+ data = envelope.data;
162
+ dependencies = envelope.dependencies ?? [];
163
+ artifacts = (envelope.artifacts ?? []).map((file) => path.relative(root, resolveInside(context.outputDir, file, 'plugin_artifact_outside_output')));
164
+ }
165
+ // A reader may never alter its frozen source or the supplied input document.
166
+ if (checkoutBefore && checkoutFingerprint(root) !== checkoutBefore)
167
+ throw new Error('只读操作修改了宿主工作树');
168
+ if (binding.operation !== 'hook' && !digestsMatch(root, before))
169
+ throw new Error('只读操作修改了来源或输入文件');
170
+ digestFiles(root, dependencies);
171
+ digestFiles(root, artifacts);
172
+ }
173
+ catch (failure) {
174
+ ok = false;
175
+ error = String(failure);
176
+ dependencies = [];
177
+ artifacts = [];
178
+ }
179
+ if (error)
180
+ writeText(stderrPath, `${readText(stderrPath)}\n${error}\n`);
181
+ const receipt = {
182
+ binding, inputDigest: sha256(JSON.stringify(context.input)),
183
+ files: ok ? digestFiles(root, [...inputFiles, ...sourceFiles, ...dependencies]) : before,
184
+ outputs: digestFiles(root, [stdoutPath, stderrPath, inputPath, ...artifacts].map((file) => path.isAbsolute(file) ? path.relative(root, file) : file)),
185
+ ...(context.worker ?? {}),
186
+ };
187
+ return { ok, code: ok ? 0 : result.status || 1, receipt, data, stdoutPath, stderrPath };
188
+ }
189
+ export function pluginReceiptCurrent(root, receipt) {
190
+ try {
191
+ resolvePluginCommand(root, receipt.binding);
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ return digestsMatch(root, receipt.files) && digestsMatch(root, receipt.outputs);
197
+ }
198
+ //# sourceMappingURL=plugin-runtime.js.map
@@ -0,0 +1,99 @@
1
+ import { listPresets, loadPreset, type ScaffoldResult } from './config/presets.js';
2
+ import { effectiveTaskCategories, type ProjectConfig } from './config/schema.js';
3
+ import { createChange, listChangeNames, requireChange, resolveChangeName } from './core/change.js';
4
+ import { nextAction } from './core/flow.js';
5
+ import { buildInstructions } from './core/instructions.js';
6
+ import { addSources, collect, abortUpdate, parseSourceArg, parseSourceFileArg } from './core/sources.js';
7
+ import { createWorker, acceptWorker, answerWorker, cancelWorker } from './core/worker.js';
8
+ import { approveGate, rejectGate } from './core/gates.js';
9
+ import { runVerifyCommands, recordEvidence, writeReport, renderReportMarkdown, requestFix, changedFilesFromState } from './core/verify.js';
10
+ import { collectPendingWork, renderPendingWork } from './core/todo.js';
11
+ import { archiveChange } from './core/archive.js';
12
+ import { isKnownGate } from './core/state.js';
13
+ import { deliverSkills, verifyDelivery, verifyInstallCli, type HostId } from './delivery/skills.js';
14
+ import { addMarketplace, listMarketplaces, assertPluginsPresent, inspectMarketplaceDependencies } from './marketplace/registry.js';
15
+ import { installPlugin, type InstallResult } from './marketplace/install.js';
16
+ import { BUILTIN_MARKETPLACE } from './marketplace/builtin.js';
17
+ import { inspectHookEntries } from './hooks/engine.js';
18
+ import type { GateId, WorkflowName } from './core/types.js';
19
+ import { PACKAGE_NAME, VERSION } from './shared/version.js';
20
+ export { VERSION, PACKAGE_NAME };
21
+ export declare function project(cwd?: string): {
22
+ projectRoot: string;
23
+ config: {
24
+ schema: "spec-product";
25
+ taskCategories?: Record<string, {
26
+ notes?: string | undefined;
27
+ } | null> | undefined;
28
+ hooks?: {
29
+ id: string;
30
+ stage: "collect" | "parse" | "tech" | "plan" | "execute" | "verify";
31
+ type?: "command" | "skill" | "notes" | "file" | "check" | undefined;
32
+ capability?: "requirements" | "interfaces" | "design" | undefined;
33
+ use?: string | undefined;
34
+ operation?: "preview" | undefined;
35
+ input?: Record<string, unknown> | undefined;
36
+ inputFiles?: string[] | undefined;
37
+ name?: string | undefined;
38
+ notes?: string | undefined;
39
+ taskCategories?: string[] | undefined;
40
+ command?: string | undefined;
41
+ target?: "project" | "inScopeChangedFiles" | undefined;
42
+ include?: string[] | undefined;
43
+ exclude?: string[] | undefined;
44
+ allowEmpty?: boolean | undefined;
45
+ required?: boolean | undefined;
46
+ purpose?: string | undefined;
47
+ path?: string | undefined;
48
+ category?: string | undefined;
49
+ description?: string | undefined;
50
+ }[] | undefined;
51
+ verify?: {
52
+ reviewScope?: {
53
+ includeDirs?: string[] | undefined;
54
+ excludeDirs?: string[] | undefined;
55
+ } | undefined;
56
+ failurePolicy?: {
57
+ failOnHookFailure?: boolean | undefined;
58
+ failOnRequiredCommandFailure?: boolean | undefined;
59
+ } | undefined;
60
+ } | undefined;
61
+ plugins?: {
62
+ name: string;
63
+ marketplace: string;
64
+ minVersion?: string | undefined;
65
+ }[] | undefined;
66
+ marketplaces?: {
67
+ name: string;
68
+ repo: string;
69
+ ref: string;
70
+ }[] | undefined;
71
+ tools?: ("claude" | "codex" | "cursor")[] | undefined;
72
+ };
73
+ };
74
+ export declare function initProject(input: {
75
+ cwd?: string;
76
+ tools?: HostId[];
77
+ preset?: string;
78
+ yes?: boolean;
79
+ skipPlugins?: boolean;
80
+ /** 每个耗时步骤开始前调用一次,供 CLI 在终端反馈进度;缺省静默。 */
81
+ onProgress?: (message: string) => void;
82
+ }): {
83
+ projectRoot: string;
84
+ config: ProjectConfig;
85
+ delivery: ReturnType<typeof deliverSkills>;
86
+ plugins: string[];
87
+ pluginInstalls: InstallResult[];
88
+ status: 'complete' | 'pending_user_action' | 'partial' | 'skipped';
89
+ issues: Array<{
90
+ scope: string;
91
+ error: string;
92
+ }>;
93
+ scaffold: ScaffoldResult[];
94
+ };
95
+ export { createChange, listChangeNames, requireChange, resolveChangeName, nextAction, buildInstructions, addSources, collect, abortUpdate, parseSourceArg, parseSourceFileArg, createWorker, acceptWorker, answerWorker, cancelWorker, approveGate, rejectGate, isKnownGate, runVerifyCommands, recordEvidence, writeReport, renderReportMarkdown, requestFix, changedFilesFromState, archiveChange, collectPendingWork, renderPendingWork, listPresets, loadPreset, addMarketplace, listMarketplaces, installPlugin, verifyDelivery, verifyInstallCli, effectiveTaskCategories, BUILTIN_MARKETPLACE, assertPluginsPresent, inspectMarketplaceDependencies, inspectHookEntries, };
96
+ export type { HostId, GateId, WorkflowName, ProjectConfig };
97
+ export { analyzeUsage, renderUsageMarkdown } from './usage/analyze.js';
98
+ export type { UsageReport, UsageOptions, Tokens, RateCard, Billing } from './usage/types.js';
99
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,185 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { stringify as stringifyYaml } from 'yaml';
4
+ import { appendText, exists, writeText, ensureDir, writeJson } from './shared/fs.js';
5
+ import { findProjectRoot, loadConfig, saveConfig, defaultConfig, configPathOf } from './config/load.js';
6
+ import { keptMarketplaceRefs, listPresets, loadPreset, mergePreset, scaffoldPreset } from './config/presets.js';
7
+ import { effectiveTaskCategories } from './config/schema.js';
8
+ import { createChange, listChangeNames, requireChange, resolveChangeName } from './core/change.js';
9
+ import { nextAction } from './core/flow.js';
10
+ import { buildInstructions } from './core/instructions.js';
11
+ import { addSources, collect, abortUpdate, parseSourceArg, parseSourceFileArg } from './core/sources.js';
12
+ import { createWorker, acceptWorker, answerWorker, cancelWorker } from './core/worker.js';
13
+ import { approveGate, rejectGate } from './core/gates.js';
14
+ import { runVerifyCommands, recordEvidence, writeReport, renderReportMarkdown, requestFix, changedFilesFromState } from './core/verify.js';
15
+ import { collectPendingWork, renderPendingWork } from './core/todo.js';
16
+ import { archiveChange } from './core/archive.js';
17
+ import { isKnownGate } from './core/state.js';
18
+ import { deliverSkills, writeInstallCli, verifyDelivery, verifyInstallCli } from './delivery/skills.js';
19
+ import { addMarketplace, listMarketplaces, ensurePresetMarketplaces, assertPluginsPresent, inspectMarketplaceDependencies } from './marketplace/registry.js';
20
+ import { installPlugin, writeInstallReceipt } from './marketplace/install.js';
21
+ import { BUILTIN_MARKETPLACE } from './marketplace/builtin.js';
22
+ import { inspectHookEntries } from './hooks/engine.js';
23
+ import { OPENSPEC_DIR, resolveInside } from './shared/paths.js';
24
+ import { PACKAGE_NAME, VERSION } from './shared/version.js';
25
+ import { AppError, isAppError } from './shared/errors.js';
26
+ export { VERSION, PACKAGE_NAME };
27
+ export function project(cwd = process.cwd()) {
28
+ const projectRoot = findProjectRoot(cwd);
29
+ const config = loadConfig(projectRoot);
30
+ return { projectRoot, config };
31
+ }
32
+ export function initProject(input) {
33
+ const projectRoot = path.resolve(input.cwd ?? process.cwd());
34
+ const progress = input.onProgress ?? (() => { });
35
+ let scaffold = [];
36
+ ensureDir(resolveInside(projectRoot, path.posix.join(OPENSPEC_DIR, 'changes'), 'openspec_path_outside_project'));
37
+ ensureDir(resolveInside(projectRoot, path.posix.join(OPENSPEC_DIR, 'archive'), 'openspec_path_outside_project'));
38
+ const hadConfig = Boolean(configPathOf(projectRoot));
39
+ let config = hadConfig ? loadConfig(projectRoot) : defaultConfig();
40
+ if (input.tools !== undefined)
41
+ config = { ...config, tools: input.tools };
42
+ if (input.preset) {
43
+ progress(`应用 preset ${input.preset}`);
44
+ const preset = loadPreset(input.preset);
45
+ for (const kept of keptMarketplaceRefs(config, preset)) {
46
+ progress(` 保留宿主 marketplace ${kept.name}@${kept.ref.slice(0, 12)}(preset 基线 ${kept.presetRef.slice(0, 12)})`);
47
+ }
48
+ config = mergePreset(config, preset);
49
+ scaffold = scaffoldPreset(projectRoot, preset);
50
+ for (const item of scaffold) {
51
+ progress(item.status === 'created' ? ` 生成 ${item.path}(模板,需填写)` : ` 保留已有 ${item.path}`);
52
+ }
53
+ const lockedMarketplaces = (preset.marketplaces ?? []).map((item) => requireImmutableMarketplace(item));
54
+ writeText(resolveInside(projectRoot, path.posix.join(OPENSPEC_DIR, 'preset-lock.yaml'), 'preset_lock_outside_project'), stringifyYaml({
55
+ schemaVersion: preset.schemaVersion,
56
+ id: preset.id,
57
+ version: preset.version,
58
+ marketplaces: lockedMarketplaces,
59
+ plugins: preset.plugins ?? [],
60
+ }, { lineWidth: 0 }));
61
+ }
62
+ const presetPlugins = config.plugins ?? [];
63
+ const presetMarketplaces = (config.marketplaces ?? []).map((item) => requireImmutableMarketplace(item));
64
+ const rewriteConfig = !hadConfig || input.tools !== undefined || Boolean(input.preset);
65
+ if (rewriteConfig)
66
+ saveConfig(projectRoot, config);
67
+ writeInstallCli(projectRoot, VERSION, PACKAGE_NAME);
68
+ appendGitignore(projectRoot);
69
+ const hosts = config.tools ?? [];
70
+ const mode = input.skipPlugins ? 'skip' : input.yes ? 'install' : 'pending';
71
+ progress(rewriteConfig
72
+ ? `写入 openspec/config.yaml 与 install-cli.mjs`
73
+ : `刷新 install-cli.mjs 与宿主交付(保留已有 config.yaml)`);
74
+ if (hosts.length > 0)
75
+ progress(`交付 OpenSpec skills → ${hosts.join(', ')}(${mode})`);
76
+ const delivery = hosts.length > 0 ? deliverSkills(projectRoot, hosts, mode) : [];
77
+ const plugins = [];
78
+ const pluginInstalls = [];
79
+ const issues = [];
80
+ if (mode === 'install') {
81
+ for (const marketplace of presetMarketplaces) {
82
+ progress(`拉取 marketplace ${marketplace.name}@${marketplace.ref.slice(0, 12)}(${marketplace.repo})`);
83
+ try {
84
+ const [record] = ensurePresetMarketplaces(projectRoot, [marketplace]);
85
+ progress(` 已锁定 ${marketplace.name} → ${record?.resolvedSha.slice(0, 12) ?? marketplace.ref.slice(0, 12)}`);
86
+ }
87
+ catch (error) {
88
+ progress(` 失败: ${marketplace.name}`);
89
+ issues.push({ scope: `marketplace:${marketplace.name}`, error: errorMessage(error) });
90
+ }
91
+ }
92
+ for (const plugin of presetPlugins) {
93
+ try {
94
+ plugins.push(...assertPluginsPresent(projectRoot, [plugin]));
95
+ }
96
+ catch (error) {
97
+ issues.push({ scope: `plugin:${plugin.name}@${plugin.marketplace}`, error: errorMessage(error) });
98
+ continue;
99
+ }
100
+ for (const host of hosts) {
101
+ progress(`安装插件 ${plugin.name}@${plugin.marketplace} → ${host}`);
102
+ try {
103
+ const result = installPlugin({
104
+ plugin: plugin.name,
105
+ marketplace: plugin.marketplace,
106
+ host: host,
107
+ projectRoot,
108
+ });
109
+ pluginInstalls.push(result);
110
+ progress(` ${result.status}${result.status === 'failed' && result.error ? `: ${result.error}` : ''}`);
111
+ if (result.status === 'failed')
112
+ issues.push({ scope: `install:${plugin.name}@${host}`, error: result.error ?? '安装失败' });
113
+ }
114
+ catch (error) {
115
+ progress(` 失败: ${errorMessage(error)}`);
116
+ issues.push({ scope: `install:${plugin.name}@${host}`, error: errorMessage(error) });
117
+ }
118
+ }
119
+ }
120
+ if (pluginInstalls.length > 0)
121
+ writeInstallReceipt(projectRoot, pluginInstalls);
122
+ // 入口在安装时就解析一遍;marketplace 未锁定已在上面记为 issue,不重复记。
123
+ for (const entry of inspectHookEntries(projectRoot, config)) {
124
+ if (!entry.ok && entry.code !== 'marketplace_not_locked') {
125
+ issues.push({ scope: `entry:${entry.hook}`, error: `${entry.code}: ${entry.error}` });
126
+ }
127
+ }
128
+ }
129
+ for (const item of delivery) {
130
+ if (item.status === 'failed')
131
+ issues.push({ scope: `delivery:${item.host}`, error: item.error ?? '交付失败' });
132
+ }
133
+ const hasPending = delivery.some((item) => item.status === 'pending_user_action')
134
+ || pluginInstalls.some((item) => item.status === 'pending_user_action')
135
+ || (mode === 'pending' && (hosts.length > 0 || presetPlugins.length > 0 || presetMarketplaces.length > 0));
136
+ const status = issues.length > 0
137
+ ? 'partial'
138
+ : mode === 'skip'
139
+ ? 'skipped'
140
+ : hasPending
141
+ ? 'pending_user_action'
142
+ : 'complete';
143
+ const result = { projectRoot, config, delivery, plugins, pluginInstalls, status, issues, scaffold };
144
+ writeJson(resolveInside(projectRoot, path.posix.join(OPENSPEC_DIR, '.runtime/init.json'), 'runtime_path_outside_project'), {
145
+ recordedAt: new Date().toISOString(),
146
+ mode,
147
+ status,
148
+ pendingPlugins: mode === 'pending' ? presetPlugins.map((item) => `${item.name}@${item.marketplace}`) : [],
149
+ skippedPlugins: mode === 'skip' ? presetPlugins.map((item) => `${item.name}@${item.marketplace}`) : [],
150
+ delivery,
151
+ pluginInstalls,
152
+ issues,
153
+ scaffold,
154
+ });
155
+ return result;
156
+ }
157
+ export { createChange, listChangeNames, requireChange, resolveChangeName, nextAction, buildInstructions, addSources, collect, abortUpdate, parseSourceArg, parseSourceFileArg, createWorker, acceptWorker, answerWorker, cancelWorker, approveGate, rejectGate, isKnownGate, runVerifyCommands, recordEvidence, writeReport, renderReportMarkdown, requestFix, changedFilesFromState, archiveChange, collectPendingWork, renderPendingWork, listPresets, loadPreset, addMarketplace, listMarketplaces, installPlugin, verifyDelivery, verifyInstallCli, effectiveTaskCategories, BUILTIN_MARKETPLACE, assertPluginsPresent, inspectMarketplaceDependencies, inspectHookEntries, };
158
+ function appendGitignore(projectRoot) {
159
+ const file = resolveInside(projectRoot, '.gitignore', 'gitignore_outside_project');
160
+ const entries = [
161
+ 'openspec/changes/**/.opsx/',
162
+ 'openspec/.runtime/',
163
+ ];
164
+ if (!exists(file)) {
165
+ writeText(file, `${entries.join('\n')}\n`);
166
+ return;
167
+ }
168
+ const current = readFileSync(file, 'utf8');
169
+ const missing = entries.filter((entry) => !current.split(/\r?\n/).includes(entry));
170
+ if (missing.length > 0)
171
+ appendText(file, `${current.endsWith('\n') ? '' : '\n'}${missing.join('\n')}\n`);
172
+ }
173
+ /** issue 文本带上错误码,便于在 init 文本输出与回执中定位。 */
174
+ function errorMessage(error) {
175
+ if (isAppError(error))
176
+ return `${error.code}: ${error.message}`;
177
+ return error instanceof Error ? error.message : String(error);
178
+ }
179
+ function requireImmutableMarketplace(item) {
180
+ if (!item.ref)
181
+ throw new AppError('marketplace_ref_required', `marketplace 缺少不可变 ref: ${item.name}`);
182
+ return { ...item, ref: item.ref };
183
+ }
184
+ export { analyzeUsage, renderUsageMarkdown } from './usage/analyze.js';
185
+ //# sourceMappingURL=index.js.map