@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.
- package/.agents/plugins/marketplace.json +13 -0
- package/.claude-plugin/marketplace.json +12 -0
- package/.claude-plugin/plugin.json +9 -0
- package/.codex-plugin/plugin.json +9 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/openspec.js +3 -0
- package/dist/cli/commands/change.d.ts +3 -0
- package/dist/cli/commands/change.js +37 -0
- package/dist/cli/commands/ext.d.ts +3 -0
- package/dist/cli/commands/ext.js +196 -0
- package/dist/cli/commands/figma.d.ts +3 -0
- package/dist/cli/commands/figma.js +73 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +71 -0
- package/dist/cli/commands/opsx.d.ts +3 -0
- package/dist/cli/commands/opsx.js +324 -0
- package/dist/cli/commands/usage.d.ts +3 -0
- package/dist/cli/commands/usage.js +21 -0
- package/dist/cli/output.d.ts +8 -0
- package/dist/cli/output.js +17 -0
- package/dist/cli/program.d.ts +4 -0
- package/dist/cli/program.js +42 -0
- package/dist/cli/register.d.ts +3 -0
- package/dist/cli/register.js +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.js +55 -0
- package/dist/config/presets.d.ts +129 -0
- package/dist/config/presets.js +125 -0
- package/dist/config/schema.d.ts +281 -0
- package/dist/config/schema.js +178 -0
- package/dist/core/archive.d.ts +5 -0
- package/dist/core/archive.js +33 -0
- package/dist/core/change.d.ts +12 -0
- package/dist/core/change.js +69 -0
- package/dist/core/code-changes.d.ts +9 -0
- package/dist/core/code-changes.js +31 -0
- package/dist/core/flow.d.ts +6 -0
- package/dist/core/flow.js +99 -0
- package/dist/core/gates.d.ts +5 -0
- package/dist/core/gates.js +71 -0
- package/dist/core/instructions.d.ts +21 -0
- package/dist/core/instructions.js +102 -0
- package/dist/core/source-operations.d.ts +25 -0
- package/dist/core/source-operations.js +94 -0
- package/dist/core/sources.d.ts +33 -0
- package/dist/core/sources.js +234 -0
- package/dist/core/state.d.ts +15 -0
- package/dist/core/state.js +75 -0
- package/dist/core/status.d.ts +31 -0
- package/dist/core/status.js +22 -0
- package/dist/core/tasks.d.ts +15 -0
- package/dist/core/tasks.js +143 -0
- package/dist/core/todo.d.ts +31 -0
- package/dist/core/todo.js +114 -0
- package/dist/core/types.d.ts +297 -0
- package/dist/core/types.js +227 -0
- package/dist/core/verify.d.ts +229 -0
- package/dist/core/verify.js +391 -0
- package/dist/core/worker-evidence.d.ts +71 -0
- package/dist/core/worker-evidence.js +253 -0
- package/dist/core/worker.d.ts +22 -0
- package/dist/core/worker.js +478 -0
- package/dist/delivery/claude-install.d.ts +4 -0
- package/dist/delivery/claude-install.js +44 -0
- package/dist/delivery/distribution.d.ts +3 -0
- package/dist/delivery/distribution.js +53 -0
- package/dist/delivery/skills.d.ts +36 -0
- package/dist/delivery/skills.js +178 -0
- package/dist/figma/cache.d.ts +19 -0
- package/dist/figma/cache.js +115 -0
- package/dist/figma/claude.d.ts +14 -0
- package/dist/figma/claude.js +121 -0
- package/dist/figma/collect.d.ts +3 -0
- package/dist/figma/collect.js +57 -0
- package/dist/figma/index.d.ts +35 -0
- package/dist/figma/index.js +178 -0
- package/dist/figma/media.d.ts +5 -0
- package/dist/figma/media.js +77 -0
- package/dist/figma/response.d.ts +9 -0
- package/dist/figma/response.js +27 -0
- package/dist/hooks/command-context.d.ts +19 -0
- package/dist/hooks/command-context.js +33 -0
- package/dist/hooks/engine.d.ts +61 -0
- package/dist/hooks/engine.js +314 -0
- package/dist/hooks/plugin-runtime.d.ts +188 -0
- package/dist/hooks/plugin-runtime.js +198 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +185 -0
- package/dist/marketplace/adapters.d.ts +202 -0
- package/dist/marketplace/adapters.js +203 -0
- package/dist/marketplace/builtin.d.ts +9 -0
- package/dist/marketplace/builtin.js +9 -0
- package/dist/marketplace/install.d.ts +38 -0
- package/dist/marketplace/install.js +108 -0
- package/dist/marketplace/registry.d.ts +71 -0
- package/dist/marketplace/registry.js +342 -0
- package/dist/marketplace/upgrade.d.ts +39 -0
- package/dist/marketplace/upgrade.js +104 -0
- package/dist/shared/command-log.d.ts +9 -0
- package/dist/shared/command-log.js +55 -0
- package/dist/shared/errors.d.ts +7 -0
- package/dist/shared/errors.js +14 -0
- package/dist/shared/exec.d.ts +16 -0
- package/dist/shared/exec.js +86 -0
- package/dist/shared/fs.d.ts +18 -0
- package/dist/shared/fs.js +93 -0
- package/dist/shared/git.d.ts +22 -0
- package/dist/shared/git.js +132 -0
- package/dist/shared/hash.d.ts +5 -0
- package/dist/shared/hash.js +29 -0
- package/dist/shared/paths.d.ts +16 -0
- package/dist/shared/paths.js +97 -0
- package/dist/shared/pkg.d.ts +3 -0
- package/dist/shared/pkg.js +23 -0
- package/dist/shared/version.d.ts +9 -0
- package/dist/shared/version.js +34 -0
- package/dist/usage/analyze.d.ts +5 -0
- package/dist/usage/analyze.js +212 -0
- package/dist/usage/claude.d.ts +9 -0
- package/dist/usage/claude.js +59 -0
- package/dist/usage/collect.d.ts +11 -0
- package/dist/usage/collect.js +71 -0
- package/dist/usage/doctor.d.ts +72 -0
- package/dist/usage/doctor.js +121 -0
- package/dist/usage/parse.d.ts +28 -0
- package/dist/usage/parse.js +345 -0
- package/dist/usage/types.d.ts +103 -0
- package/dist/usage/types.js +2 -0
- package/docs/getting-started.md +20 -0
- package/docs/guides/configuration.md +33 -0
- package/docs/guides/cost-benchmark.md +88 -0
- package/docs/guides/plugin-migration.md +65 -0
- package/docs/guides/smart-figma.md +51 -0
- package/docs/guides/source-reading.md +35 -0
- package/docs/guides/ui-conventions-template.md +39 -0
- package/docs/guides/worker-evidence.md +73 -0
- package/docs/guides/workflow.md +74 -0
- package/docs/maintainers/architecture.md +27 -0
- package/docs/maintainers/plugin-runtime-contract.md +55 -0
- package/package.json +78 -0
- package/plugin.json +9 -0
- package/presets/backend-service.yaml +19 -0
- package/presets/flutter-mobile/plugin-hooks.json +68 -0
- package/presets/flutter-mobile/ui-conventions.md +122 -0
- package/presets/flutter-mobile.yaml +205 -0
- package/presets/web-product.yaml +23 -0
- package/skills/opsx-apply/SKILL.md +35 -0
- package/skills/opsx-apply/nodes/blocked.md +7 -0
- package/skills/opsx-apply/nodes/code-task.md +29 -0
- package/skills/opsx-apply/nodes/gate-a.md +9 -0
- package/skills/opsx-apply/nodes/stop.md +5 -0
- package/skills/opsx-apply/nodes/task-build.md +50 -0
- package/skills/opsx-apply/nodes/task-repair.md +20 -0
- package/skills/opsx-apply/nodes/task-scout.md +28 -0
- package/skills/opsx-apply/nodes/task-verify.md +38 -0
- package/skills/opsx-archive/SKILL.md +24 -0
- package/skills/opsx-archive/nodes/archive.md +15 -0
- package/skills/opsx-explore/SKILL.md +20 -0
- package/skills/opsx-explore/nodes/explore.md +3 -0
- package/skills/opsx-fix-verify/SKILL.md +30 -0
- package/skills/opsx-fix-verify/nodes/repair.md +11 -0
- package/skills/opsx-fix-verify/nodes/reverify.md +3 -0
- package/skills/opsx-fix-verify/nodes/stop.md +3 -0
- package/skills/opsx-gate-decision/SKILL.md +20 -0
- package/skills/opsx-gate-decision/nodes/decide.md +15 -0
- package/skills/opsx-propose/SKILL.md +36 -0
- package/skills/opsx-propose/nodes/blocked.md +13 -0
- package/skills/opsx-propose/nodes/collect.md +35 -0
- package/skills/opsx-propose/nodes/parse.md +72 -0
- package/skills/opsx-propose/nodes/plan.md +65 -0
- package/skills/opsx-propose/nodes/stop.md +17 -0
- package/skills/opsx-propose/nodes/tech.md +77 -0
- package/skills/opsx-propose/scripts/read-json-source.mjs +22 -0
- package/skills/opsx-resume/SKILL.md +21 -0
- package/skills/opsx-resume/nodes/route.md +12 -0
- package/skills/opsx-source-update/SKILL.md +23 -0
- package/skills/opsx-source-update/nodes/update.md +14 -0
- package/skills/opsx-verify/SKILL.md +32 -0
- package/skills/opsx-verify/nodes/blocked.md +3 -0
- package/skills/opsx-verify/nodes/failed.md +3 -0
- package/skills/opsx-verify/nodes/gate-b.md +7 -0
- package/skills/opsx-verify/nodes/stop.md +5 -0
- 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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|