@peterxiaoyang/superspec 0.1.0 → 0.1.2
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/README.md +175 -24
- package/dist/src/cli.js +2 -2
- package/dist/src/cli_args.js +18 -12
- package/dist/src/gates.js +11 -16
- package/dist/src/i18n.d.ts +21 -0
- package/dist/src/i18n.js +659 -0
- package/dist/src/init_cli.d.ts +25 -0
- package/dist/src/init_cli.js +91 -15
- package/dist/src/openspec.d.ts +17 -0
- package/dist/src/openspec.js +89 -3
- package/dist/src/project_init.d.ts +28 -1
- package/dist/src/project_init.js +69 -21
- package/dist/src/util.d.ts +21 -1
- package/dist/src/util.js +105 -5
- package/package.json +1 -1
- package/templates/workflow/prompts/architect.md +62 -55
- package/templates/workflow/prompts/code-reviewer.md +84 -77
- package/templates/workflow/prompts/critic.md +42 -35
- package/templates/workflow/prompts/test-engineer.md +73 -66
- package/templates/workflow/prompts/verifier.md +33 -26
- package/templates/workflow/skills/superspec-apply/SKILL.md +23 -11
- package/templates/workflow/skills/superspec-archive/SKILL.md +14 -2
- package/templates/workflow/skills/superspec-explore/SKILL.md +32 -20
- package/templates/workflow/skills/superspec-propose/SKILL.md +39 -27
- package/templates/workflow/skills/superspec-review/SKILL.md +52 -40
package/dist/src/init_cli.d.ts
CHANGED
|
@@ -1,2 +1,27 @@
|
|
|
1
|
+
import { runCommand, type OpenspecCliProbe } from "./core.ts";
|
|
2
|
+
import { type InstallScope } from "./install_engine.ts";
|
|
3
|
+
type InitArgs = {
|
|
4
|
+
path: string;
|
|
5
|
+
codexHome: string;
|
|
6
|
+
mode: "install" | "update" | "uninstall";
|
|
7
|
+
scope: InstallScope | null;
|
|
8
|
+
dryRun: boolean;
|
|
9
|
+
force: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function maybe_install_missing_openspec(opts: {
|
|
12
|
+
cwd: string;
|
|
13
|
+
scope: InstallScope;
|
|
14
|
+
mode: InitArgs["mode"];
|
|
15
|
+
interactive?: boolean;
|
|
16
|
+
commandExistsFn?: (cmd: string, meta?: {
|
|
17
|
+
cwd?: string;
|
|
18
|
+
}) => boolean;
|
|
19
|
+
probeOpenspecFn?: (meta?: {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
}) => OpenspecCliProbe;
|
|
22
|
+
run?: typeof runCommand;
|
|
23
|
+
writeStderr?: (text: string) => void;
|
|
24
|
+
}): "not-needed" | "installed" | "skipped" | "failed";
|
|
1
25
|
export declare function main_init(argv?: string[]): number;
|
|
2
26
|
export declare function main_init_async(argv?: string[]): Promise<number>;
|
|
27
|
+
export {};
|
package/dist/src/init_cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { block, GuardError, printDecision, reason } from "./core.js";
|
|
2
|
-
import { project_init } from "./project_init.js";
|
|
1
|
+
import { block, commandExists, GuardError, printDecision, reason, runCommand, openspec_cli_probe, REQUIRED_OPENSPEC_MIN_VERSION, } from "./core.js";
|
|
2
|
+
import { forced_openspec_install_plan, project_init, recommended_openspec_install_plan } from "./project_init.js";
|
|
3
3
|
import { install_workflow, uninstall_workflow, update_workflow } from "./install_engine.js";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { system_failure_zh } from "./i18n.js";
|
|
7
8
|
function usage() {
|
|
8
9
|
return "usage: superspec init [-h] [--scope {project,user}] [--path PATH] [--codex-home PATH] [--create] [--update] [--uninstall] [--dry-run] [--force]\n";
|
|
9
10
|
}
|
|
10
11
|
function help() {
|
|
11
|
-
return `${usage()}\
|
|
12
|
+
return `${usage()}\n可选参数:\n -h, --help 显示帮助并退出\n --scope {project,user} 安装到当前项目的 .codex 目录,或安装到用户级 Codex 目录(默认:project)\n --project 等价于 --scope project\n --user 等价于 --scope user\n --global 兼容别名,等价于 --user\n --path PATH --scope project 时使用的项目根目录(默认:当前目录)\n --codex-home PATH --scope user 时使用的 Codex 用户目录(默认:$CODEX_HOME 或 ~/.codex)\n --create 兼容参数;init 默认就会创建缺失内容\n --update 按 manifest 更新 SuperSpec 管理的文件;用户改动文件保留,新的版本写入 *.new\n --uninstall 按 manifest 卸载 SuperSpec 管理的文件;.superspec 数据与既有/用户改动文件会保留\n --dry-run 配合 --uninstall 时只预览将删除的文件,不实际修改\n --force 安装时覆盖已有且内容不同的文件,并保留 *.bak 备份\n`;
|
|
12
13
|
}
|
|
13
14
|
function parse_init_argv(argv) {
|
|
14
15
|
const getValue = (flag) => {
|
|
@@ -17,19 +18,19 @@ function parse_init_argv(argv) {
|
|
|
17
18
|
return undefined;
|
|
18
19
|
const value = argv[idx + 1];
|
|
19
20
|
if (value === undefined || value.startsWith("--"))
|
|
20
|
-
throw new GuardError(`${flag}
|
|
21
|
+
throw new GuardError(`${flag} 缺少取值`);
|
|
21
22
|
return value;
|
|
22
23
|
};
|
|
23
24
|
const update = argv.includes("--update");
|
|
24
25
|
const uninstall = argv.includes("--uninstall");
|
|
25
26
|
if (update && uninstall)
|
|
26
|
-
throw new GuardError("--update
|
|
27
|
+
throw new GuardError("--update 与 --uninstall 不能同时使用");
|
|
27
28
|
const scopeFlag = getValue("--scope");
|
|
28
29
|
if (scopeFlag !== undefined && !["project", "user", "global"].includes(scopeFlag))
|
|
29
|
-
throw new GuardError("--scope
|
|
30
|
+
throw new GuardError("--scope 只能是 project 或 user");
|
|
30
31
|
const userShortcut = argv.includes("--user") || argv.includes("--global");
|
|
31
32
|
if (userShortcut && argv.includes("--project"))
|
|
32
|
-
throw new GuardError("--user/--global
|
|
33
|
+
throw new GuardError("--user/--global 与 --project 不能同时使用");
|
|
33
34
|
let scope = scopeFlag === "global" ? "user" : scopeFlag ?? null;
|
|
34
35
|
if (userShortcut)
|
|
35
36
|
scope = "user";
|
|
@@ -47,6 +48,20 @@ function parse_init_argv(argv) {
|
|
|
47
48
|
function joinHomeCodex() {
|
|
48
49
|
return `${homedir()}/.codex`;
|
|
49
50
|
}
|
|
51
|
+
function commandFailure(proc) {
|
|
52
|
+
const output = (proc.error?.message ?? (proc.stderr || proc.stdout)).trim();
|
|
53
|
+
if (output)
|
|
54
|
+
return system_failure_zh(output, `安装命令执行失败(退出状态码 ${proc.status ?? "未知"})。`);
|
|
55
|
+
if (proc.status !== null)
|
|
56
|
+
return `安装命令执行失败(退出状态码 ${proc.status})。`;
|
|
57
|
+
return "安装命令执行失败,请查看终端日志后重试。";
|
|
58
|
+
}
|
|
59
|
+
function shouldRetryOpenSpecInstallWithForce(proc) {
|
|
60
|
+
const output = `${proc.error?.message ?? ""}\n${proc.stderr}\n${proc.stdout}`;
|
|
61
|
+
const binConflict = /\bEEXIST\b|already exists|file exists|Refusing to delete|will not overwrite|would overwrite/iu.test(output);
|
|
62
|
+
const openspecBin = /\bopenspec(?:\.(?:cmd|ps1))?\b/iu.test(output);
|
|
63
|
+
return binConflict && openspecBin;
|
|
64
|
+
}
|
|
50
65
|
function engineDecision(gate, projectRoot, result, nextActions) {
|
|
51
66
|
if (result.problems.length > 0) {
|
|
52
67
|
const decision = block("project", gate, result.problems.map((item) => reason(`${gate}_failed`, item)));
|
|
@@ -68,12 +83,12 @@ async function promptInstallScope() {
|
|
|
68
83
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
69
84
|
try {
|
|
70
85
|
for (;;) {
|
|
71
|
-
const answer = (await rl.question("
|
|
72
|
-
if (answer === "" || answer === "project" || answer === "p" || answer === "1")
|
|
86
|
+
const answer = (await rl.question("请选择安装范围:project 还是 user?[project] ")).trim().toLowerCase();
|
|
87
|
+
if (answer === "" || answer === "project" || answer === "p" || answer === "1" || answer === "项目")
|
|
73
88
|
return "project";
|
|
74
|
-
if (answer === "user" || answer === "u" || answer === "2" || answer === "global" || answer === "g")
|
|
89
|
+
if (answer === "user" || answer === "u" || answer === "2" || answer === "global" || answer === "g" || answer === "用户")
|
|
75
90
|
return "user";
|
|
76
|
-
process.stderr.write("
|
|
91
|
+
process.stderr.write("请输入 project 或 user。\n");
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
finally {
|
|
@@ -83,6 +98,60 @@ async function promptInstallScope() {
|
|
|
83
98
|
function canPrompt() {
|
|
84
99
|
return Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
85
100
|
}
|
|
101
|
+
export function maybe_install_missing_openspec(opts) {
|
|
102
|
+
const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
|
|
103
|
+
if (opts.mode !== "install")
|
|
104
|
+
return "not-needed";
|
|
105
|
+
const run = opts.run ?? runCommand;
|
|
106
|
+
const probeOpenspecFn = opts.probeOpenspecFn ?? ((meta) => openspec_cli_probe({ cwd: meta?.cwd, commandExistsFn, run }));
|
|
107
|
+
const before = probeOpenspecFn({ cwd: opts.cwd });
|
|
108
|
+
if (before.ok)
|
|
109
|
+
return "not-needed";
|
|
110
|
+
const writeStderr = opts.writeStderr ?? ((text) => {
|
|
111
|
+
process.stderr.write(text);
|
|
112
|
+
});
|
|
113
|
+
const plan = recommended_openspec_install_plan({ cwd: opts.cwd, commandExistsFn });
|
|
114
|
+
if (plan === null) {
|
|
115
|
+
writeStderr(`${before.message}。SuperSpec 需要 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION},但未找到 npm、pnpm、yarn 或 bun,无法自动安装。\n`);
|
|
116
|
+
return "skipped";
|
|
117
|
+
}
|
|
118
|
+
writeStderr(`${before.message}。SuperSpec 需要 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION},将自动安装或升级。\n`);
|
|
119
|
+
writeStderr(`正在安装 OpenSpec CLI:${plan.rendered}\n`);
|
|
120
|
+
let proc = run(plan.cmd, plan.args, { cwd: opts.cwd, timeout: 300_000 });
|
|
121
|
+
if ((proc.error || proc.status !== 0) && shouldRetryOpenSpecInstallWithForce(proc)) {
|
|
122
|
+
const forcedPlan = forced_openspec_install_plan(plan);
|
|
123
|
+
writeStderr(`OpenSpec CLI 安装遇到全局 bin 冲突,正在覆盖重试:${forcedPlan.rendered}\n`);
|
|
124
|
+
proc = run(forcedPlan.cmd, forcedPlan.args, { cwd: opts.cwd, timeout: 300_000 });
|
|
125
|
+
}
|
|
126
|
+
if (proc.error || proc.status !== 0) {
|
|
127
|
+
writeStderr(`自动安装 OpenSpec CLI 失败:${commandFailure(proc)}\n`);
|
|
128
|
+
return "failed";
|
|
129
|
+
}
|
|
130
|
+
const after = probeOpenspecFn({ cwd: opts.cwd });
|
|
131
|
+
if (!after.ok) {
|
|
132
|
+
writeStderr(`安装命令已完成,但当前 PATH 里的 openspec 仍不可用:${after.message}。请重新打开终端或确认全局 bin 已在 PATH 中,然后重新运行 superspec init;Windows PowerShell 请运行 superspec.cmd init。\n`);
|
|
133
|
+
return "failed";
|
|
134
|
+
}
|
|
135
|
+
writeStderr("OpenSpec CLI 安装或升级完成,继续执行 superspec init。\n");
|
|
136
|
+
return "installed";
|
|
137
|
+
}
|
|
138
|
+
function openspecPreflightBlocked(args, scope, installResult) {
|
|
139
|
+
const targetRoot = scope === "user" ? args.codexHome : args.path;
|
|
140
|
+
const code = installResult === "skipped" ? "openspec_auto_install_unavailable" : "openspec_auto_install_failed";
|
|
141
|
+
const message = installResult === "skipped"
|
|
142
|
+
? `OpenSpec CLI 不满足 SuperSpec 要求,且未找到可用包管理器自动安装 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}。`
|
|
143
|
+
: `OpenSpec CLI 自动安装或升级 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION} 未完成。`;
|
|
144
|
+
const decision = block("project", "openspec_preflight", [reason(code, message)], {
|
|
145
|
+
next_actions: [`install or upgrade @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}, then rerun \`superspec init --scope ${scope}\``],
|
|
146
|
+
});
|
|
147
|
+
printDecision({
|
|
148
|
+
...decision,
|
|
149
|
+
project_root: args.path,
|
|
150
|
+
install_scope: scope,
|
|
151
|
+
install_root: targetRoot,
|
|
152
|
+
}, { command: "init" });
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
86
155
|
function run_init(args, scope) {
|
|
87
156
|
const targetRoot = scope === "user" ? args.codexHome : args.path;
|
|
88
157
|
const gatePrefix = scope === "user" ? "user" : "project";
|
|
@@ -107,7 +176,7 @@ function run_init(args, scope) {
|
|
|
107
176
|
}
|
|
108
177
|
summary.install_scope = scope;
|
|
109
178
|
summary.install_root = targetRoot;
|
|
110
|
-
printDecision(summary);
|
|
179
|
+
printDecision(summary, { command: "init" });
|
|
111
180
|
return summary.allowed ? 0 : 1;
|
|
112
181
|
}
|
|
113
182
|
export function main_init(argv = process.argv.slice(2)) {
|
|
@@ -117,12 +186,16 @@ export function main_init(argv = process.argv.slice(2)) {
|
|
|
117
186
|
return 0;
|
|
118
187
|
}
|
|
119
188
|
const args = parse_init_argv(argv);
|
|
120
|
-
|
|
189
|
+
const scope = args.scope ?? "project";
|
|
190
|
+
const openspecInstall = maybe_install_missing_openspec({ cwd: args.path, scope, mode: args.mode });
|
|
191
|
+
if (openspecInstall === "failed" || openspecInstall === "skipped")
|
|
192
|
+
return openspecPreflightBlocked(args, scope, openspecInstall);
|
|
193
|
+
return run_init(args, scope);
|
|
121
194
|
}
|
|
122
195
|
catch (err) {
|
|
123
196
|
const change = "project";
|
|
124
197
|
const errReason = err instanceof GuardError ? reason("guard_error", err.message) : reason("guard_internal_error", `${err.name}: ${err.message}`);
|
|
125
|
-
printDecision(block(change, "guard_error", [errReason]));
|
|
198
|
+
printDecision(block(change, "guard_error", [errReason]), { command: "init" });
|
|
126
199
|
return 2;
|
|
127
200
|
}
|
|
128
201
|
}
|
|
@@ -134,12 +207,15 @@ export async function main_init_async(argv = process.argv.slice(2)) {
|
|
|
134
207
|
}
|
|
135
208
|
const args = parse_init_argv(argv);
|
|
136
209
|
const scope = args.scope ?? (canPrompt() ? await promptInstallScope() : "project");
|
|
210
|
+
const openspecInstall = maybe_install_missing_openspec({ cwd: args.path, scope, mode: args.mode });
|
|
211
|
+
if (openspecInstall === "failed" || openspecInstall === "skipped")
|
|
212
|
+
return openspecPreflightBlocked(args, scope, openspecInstall);
|
|
137
213
|
return run_init(args, scope);
|
|
138
214
|
}
|
|
139
215
|
catch (err) {
|
|
140
216
|
const change = "project";
|
|
141
217
|
const errReason = err instanceof GuardError ? reason("guard_error", err.message) : reason("guard_internal_error", `${err.name}: ${err.message}`);
|
|
142
|
-
printDecision(block(change, "guard_error", [errReason]));
|
|
218
|
+
printDecision(block(change, "guard_error", [errReason]), { command: "init" });
|
|
143
219
|
return 2;
|
|
144
220
|
}
|
|
145
221
|
}
|
package/dist/src/openspec.d.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import type { JsonMap, Reason } from "./util.ts";
|
|
2
|
+
import { runCommand } from "./util.ts";
|
|
3
|
+
export declare const REQUIRED_OPENSPEC_MIN_VERSION = "1.4.1";
|
|
4
|
+
export type OpenspecCliProbe = {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
state: "ok" | "missing" | "invalid" | "too_old";
|
|
7
|
+
version: string | null;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function parse_openspec_version(raw: string): string | null;
|
|
11
|
+
export declare function compare_versions(actual: string, required: string): number;
|
|
12
|
+
export declare function openspec_cli_probe(opts?: {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
commandExistsFn?: (cmd: string, meta?: {
|
|
15
|
+
cwd?: string;
|
|
16
|
+
}) => boolean;
|
|
17
|
+
run?: typeof runCommand;
|
|
18
|
+
}): OpenspecCliProbe;
|
|
2
19
|
export declare function openspec_status(change: string): JsonMap;
|
|
3
20
|
export declare function openspec_validate(change: string): [boolean, string];
|
|
4
21
|
export declare function openspec_version(): string;
|
package/dist/src/openspec.js
CHANGED
|
@@ -1,6 +1,93 @@
|
|
|
1
1
|
import { dirname, join, resolve, sep } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { GATE_ALIASES, GATE_ROUTE, GuardError, ROUTE_ALIASES, ROUTE_ORDER, fingerprint_obj, isObject, reason, repr, runCommand, } from "./util.js";
|
|
3
|
+
import { REQUIRED_OPENSPEC_CLI_SURFACES, GATE_ALIASES, GATE_ROUTE, GuardError, ROUTE_ALIASES, ROUTE_ORDER, commandExists, fingerprint_obj, isObject, reason, repr, runCommand, } from "./util.js";
|
|
4
|
+
export const REQUIRED_OPENSPEC_MIN_VERSION = "1.4.1";
|
|
5
|
+
export function parse_openspec_version(raw) {
|
|
6
|
+
const match = raw.match(/\b(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?\b/);
|
|
7
|
+
return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
|
|
8
|
+
}
|
|
9
|
+
export function compare_versions(actual, required) {
|
|
10
|
+
const actualParts = actual.split(".").map((part) => Number.parseInt(part, 10));
|
|
11
|
+
const requiredParts = required.split(".").map((part) => Number.parseInt(part, 10));
|
|
12
|
+
for (let index = 0; index < Math.max(actualParts.length, requiredParts.length); index += 1) {
|
|
13
|
+
const actualPart = actualParts[index] ?? 0;
|
|
14
|
+
const requiredPart = requiredParts[index] ?? 0;
|
|
15
|
+
if (!Number.isFinite(actualPart))
|
|
16
|
+
return -1;
|
|
17
|
+
if (!Number.isFinite(requiredPart))
|
|
18
|
+
return 1;
|
|
19
|
+
if (actualPart > requiredPart)
|
|
20
|
+
return 1;
|
|
21
|
+
if (actualPart < requiredPart)
|
|
22
|
+
return -1;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
export function openspec_cli_probe(opts = {}) {
|
|
27
|
+
const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
|
|
28
|
+
const run = opts.run ?? runCommand;
|
|
29
|
+
if (!commandExistsFn("openspec", { cwd: opts.cwd })) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
state: "missing",
|
|
33
|
+
version: null,
|
|
34
|
+
message: "PATH 中缺少 OpenSpec CLI(openspec)",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const versionProc = run("openspec", ["--version"], { cwd: opts.cwd, timeout: 15_000 });
|
|
38
|
+
if (versionProc.error || versionProc.status !== 0) {
|
|
39
|
+
const output = (versionProc.error?.message ?? (versionProc.stderr || versionProc.stdout)).trim();
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
state: "invalid",
|
|
43
|
+
version: null,
|
|
44
|
+
message: `\`openspec --version\` 执行失败:${output}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const rawVersion = `${versionProc.stdout}${versionProc.stderr}`.trim();
|
|
48
|
+
if (/openspec[-_\s]*chinese/iu.test(rawVersion)) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
state: "invalid",
|
|
52
|
+
version: null,
|
|
53
|
+
message: "PATH 上的 openspec 来自 openspec-chinese,不是受支持的 @fission-ai/openspec CLI",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const version = parse_openspec_version(rawVersion);
|
|
57
|
+
if (version === null) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
state: "invalid",
|
|
61
|
+
version: null,
|
|
62
|
+
message: "PATH 上的 OpenSpec CLI(openspec)无法报告语义版本,可能不是受支持的 @fission-ai/openspec",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (compare_versions(version, REQUIRED_OPENSPEC_MIN_VERSION) < 0) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
state: "too_old",
|
|
69
|
+
version,
|
|
70
|
+
message: `openspec ${version} 低于 SuperSpec 要求的最低版本 ${REQUIRED_OPENSPEC_MIN_VERSION}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
for (const args of REQUIRED_OPENSPEC_CLI_SURFACES) {
|
|
74
|
+
const proc = run("openspec", [...args], { cwd: opts.cwd, timeout: 15_000 });
|
|
75
|
+
if (proc.error || proc.status !== 0) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
state: "invalid",
|
|
79
|
+
version,
|
|
80
|
+
message: `openspec ${version} 缺少必需的原生能力:\`openspec ${args.join(" ")}\` 执行失败`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
state: "ok",
|
|
87
|
+
version,
|
|
88
|
+
message: `openspec ${version} satisfies SuperSpec requirements`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
4
91
|
export function openspec_status(change) {
|
|
5
92
|
const proc = runCommand("openspec", ["status", "--change", change, "--json"], { timeout: 30_000 });
|
|
6
93
|
if (proc.error) {
|
|
@@ -28,8 +115,7 @@ export function openspec_version() {
|
|
|
28
115
|
if (proc.error || proc.status !== 0)
|
|
29
116
|
return "unknown";
|
|
30
117
|
const raw = (proc.stdout || proc.stderr).trim();
|
|
31
|
-
|
|
32
|
-
return match ? match[0] : raw || "unknown";
|
|
118
|
+
return (parse_openspec_version(raw) ?? raw) || "unknown";
|
|
33
119
|
}
|
|
34
120
|
export function openspec_status_shape_reasons(status) {
|
|
35
121
|
const problems = [];
|
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
import { type JsonMap } from "./core.ts";
|
|
1
|
+
import { type JsonMap, type OpenspecCliProbe } from "./core.ts";
|
|
2
|
+
export declare const OPENSPEC_NPM_PACKAGE = "@fission-ai/openspec";
|
|
3
|
+
export declare const OPENSPEC_INSTALL_DOC_URL = "https://github.com/Fission-AI/OpenSpec#readme";
|
|
4
|
+
export type OpenspecInstallPlan = {
|
|
5
|
+
manager: "npm" | "pnpm" | "yarn" | "bun";
|
|
6
|
+
cmd: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
rendered: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function recommended_openspec_install_plan(opts?: {
|
|
11
|
+
cwd?: string;
|
|
12
|
+
commandExistsFn?: (cmd: string, meta?: {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
}) => boolean;
|
|
15
|
+
}): OpenspecInstallPlan | null;
|
|
16
|
+
export declare function forced_openspec_install_plan(plan: OpenspecInstallPlan): OpenspecInstallPlan;
|
|
17
|
+
export declare function missing_openspec_cli_message(opts?: {
|
|
18
|
+
cwd?: string;
|
|
19
|
+
commandExistsFn?: (cmd: string, meta?: {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
}) => boolean;
|
|
22
|
+
}): string;
|
|
23
|
+
export declare function openspec_cli_requirement_message(probe: OpenspecCliProbe, opts?: {
|
|
24
|
+
cwd?: string;
|
|
25
|
+
commandExistsFn?: (cmd: string, meta?: {
|
|
26
|
+
cwd?: string;
|
|
27
|
+
}) => boolean;
|
|
28
|
+
}): string;
|
|
2
29
|
export declare function project_init(repoRootRaw?: string, opts?: {
|
|
3
30
|
force?: boolean;
|
|
4
31
|
}): JsonMap;
|
package/dist/src/project_init.js
CHANGED
|
@@ -1,16 +1,63 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { REQUIRED_SUPERSPEC_AGENT_ROLES, REQUIRED_OPENSPEC_CODEX_SKILLS, block, reason, read_agent_toml_name, read_skill_frontmatter_name,
|
|
3
|
+
import { REQUIRED_SUPERSPEC_AGENT_ROLES, REQUIRED_OPENSPEC_CODEX_SKILLS, REQUIRED_OPENSPEC_MIN_VERSION, block, commandExists, openspec_cli_probe, reason, read_agent_toml_name, read_skill_frontmatter_name, runCommand, } from "./core.js";
|
|
4
4
|
import { install_workflow } from "./install_engine.js";
|
|
5
|
+
import { system_failure_zh } from "./i18n.js";
|
|
6
|
+
export const OPENSPEC_NPM_PACKAGE = "@fission-ai/openspec";
|
|
7
|
+
export const OPENSPEC_INSTALL_DOC_URL = "https://github.com/Fission-AI/OpenSpec#readme";
|
|
5
8
|
const ROLE_DESCRIPTIONS = {
|
|
6
|
-
architect: "
|
|
7
|
-
critic: "
|
|
8
|
-
"test-engineer": "
|
|
9
|
-
"code-reviewer": "
|
|
10
|
-
verifier: "
|
|
9
|
+
architect: "系统设计、边界、接口与长期取舍",
|
|
10
|
+
critic: "严格审查计划、证据、假设与范围漂移",
|
|
11
|
+
"test-engineer": "测试策略、覆盖率与 RED/GREEN 证据审查",
|
|
12
|
+
"code-reviewer": "代码 / 规格 / 安全审查",
|
|
13
|
+
verifier: "最终完成证据与验证审查",
|
|
11
14
|
};
|
|
12
15
|
function commandFailure(proc) {
|
|
13
|
-
return (proc.error?.message ?? (proc.stderr || proc.stdout)).trim();
|
|
16
|
+
return system_failure_zh((proc.error?.message ?? (proc.stderr || proc.stdout)).trim(), proc.status !== null && proc.status !== undefined ? `命令执行失败(退出状态码 ${proc.status})。` : "命令执行失败,请查看终端日志后重试。");
|
|
17
|
+
}
|
|
18
|
+
function renderCommand(cmd, args) {
|
|
19
|
+
return [cmd, ...args].join(" ");
|
|
20
|
+
}
|
|
21
|
+
export function recommended_openspec_install_plan(opts = {}) {
|
|
22
|
+
const commandExistsFn = opts.commandExistsFn ?? ((cmd, meta) => commandExists(cmd, { cwd: meta?.cwd }));
|
|
23
|
+
const versionedPackage = `${OPENSPEC_NPM_PACKAGE}@latest`;
|
|
24
|
+
const candidates = [
|
|
25
|
+
{ manager: "npm", cmd: "npm", args: ["install", "-g", versionedPackage] },
|
|
26
|
+
{ manager: "pnpm", cmd: "pnpm", args: ["add", "-g", versionedPackage] },
|
|
27
|
+
{ manager: "yarn", cmd: "yarn", args: ["global", "add", versionedPackage] },
|
|
28
|
+
{ manager: "bun", cmd: "bun", args: ["add", "-g", versionedPackage] },
|
|
29
|
+
];
|
|
30
|
+
for (const candidate of candidates) {
|
|
31
|
+
if (!commandExistsFn(candidate.cmd, { cwd: opts.cwd }))
|
|
32
|
+
continue;
|
|
33
|
+
return { ...candidate, rendered: renderCommand(candidate.cmd, candidate.args) };
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function forced_openspec_install_plan(plan) {
|
|
38
|
+
const versionedPackage = `${OPENSPEC_NPM_PACKAGE}@latest`;
|
|
39
|
+
const forced = {
|
|
40
|
+
npm: ["install", "-g", "--force", versionedPackage],
|
|
41
|
+
pnpm: ["add", "-g", "--force", versionedPackage],
|
|
42
|
+
yarn: ["global", "add", versionedPackage, "--force"],
|
|
43
|
+
bun: ["add", "-g", "--force", versionedPackage],
|
|
44
|
+
};
|
|
45
|
+
return { ...plan, args: forced[plan.manager], rendered: renderCommand(plan.cmd, forced[plan.manager]) };
|
|
46
|
+
}
|
|
47
|
+
export function missing_openspec_cli_message(opts = {}) {
|
|
48
|
+
return openspec_cli_requirement_message({
|
|
49
|
+
ok: false,
|
|
50
|
+
state: "missing",
|
|
51
|
+
version: null,
|
|
52
|
+
message: "PATH 中缺少 OpenSpec CLI(openspec)",
|
|
53
|
+
}, opts);
|
|
54
|
+
}
|
|
55
|
+
export function openspec_cli_requirement_message(probe, opts = {}) {
|
|
56
|
+
const plan = recommended_openspec_install_plan(opts);
|
|
57
|
+
if (plan) {
|
|
58
|
+
return `${probe.message}。需要 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}。可先运行 \`${plan.rendered}\` 安装或升级,然后重新运行 \`superspec init --scope project\`。`;
|
|
59
|
+
}
|
|
60
|
+
return `${probe.message}。请先安装或升级 @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}(${OPENSPEC_INSTALL_DOC_URL}),然后重新运行 \`superspec init --scope project\`。`;
|
|
14
61
|
}
|
|
15
62
|
function openspecSkillProblems(repoRoot) {
|
|
16
63
|
const skillsRoot = join(repoRoot, ".codex", "skills");
|
|
@@ -34,9 +81,9 @@ function writeSuperSpecAgent(repoRoot, name) {
|
|
|
34
81
|
`description = "${ROLE_DESCRIPTIONS[name] ?? `superspec ${name} role`}"`,
|
|
35
82
|
'model_reasoning_effort = "high"',
|
|
36
83
|
'developer_instructions = """',
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
84
|
+
`你是仓库本地的 superspec ${name} native subagent。`,
|
|
85
|
+
"遵循分配给你的 superspec gate 证据任务,引用具体文件,并把未通过的问题上报主流程。",
|
|
86
|
+
"不要用主流程自审替代必须的角色证据。",
|
|
40
87
|
'"""',
|
|
41
88
|
"",
|
|
42
89
|
].join("\n"), "utf8");
|
|
@@ -51,16 +98,17 @@ function writeSuperSpecPrompt(repoRoot, name) {
|
|
|
51
98
|
'argument-hint: "superspec gate evidence task"',
|
|
52
99
|
"---",
|
|
53
100
|
"",
|
|
54
|
-
|
|
101
|
+
`你是仓库本地的 superspec ${name} 角色。`,
|
|
55
102
|
"",
|
|
56
|
-
"
|
|
103
|
+
"请基于具体文件证据审查提供的 superspec gate 上下文,输出简洁的通过 / 未通过报告,引用 source anchors 与 target refs,并且不要用主流程自审替代必须的 native-subagent 证据。",
|
|
57
104
|
"",
|
|
58
105
|
].join("\n"), "utf8");
|
|
59
106
|
return filePath;
|
|
60
107
|
}
|
|
61
108
|
function ensureOpenSpecCodex(repoRoot, actions) {
|
|
62
|
-
|
|
63
|
-
|
|
109
|
+
const probe = openspec_cli_probe({ cwd: repoRoot });
|
|
110
|
+
if (!probe.ok)
|
|
111
|
+
return [openspec_cli_requirement_message(probe, { cwd: repoRoot })];
|
|
64
112
|
let problems = openspecSkillProblems(repoRoot);
|
|
65
113
|
if (problems.length === 0) {
|
|
66
114
|
actions.push({ action: "openspec_codex_skills", status: "ok" });
|
|
@@ -71,10 +119,10 @@ function ensureOpenSpecCodex(repoRoot, actions) {
|
|
|
71
119
|
action: "openspec init --tools codex .",
|
|
72
120
|
status: init.status === 0 ? "updated" : "failed",
|
|
73
121
|
refs: problems,
|
|
74
|
-
detail: init.status === 0 ? undefined : (init
|
|
122
|
+
detail: init.status === 0 ? undefined : commandFailure(init),
|
|
75
123
|
});
|
|
76
124
|
if (init.error || init.status !== 0)
|
|
77
|
-
return [`openspec init
|
|
125
|
+
return [`openspec init 执行失败:${commandFailure(init)}`];
|
|
78
126
|
problems = openspecSkillProblems(repoRoot);
|
|
79
127
|
if (problems.length === 0)
|
|
80
128
|
return [];
|
|
@@ -83,12 +131,12 @@ function ensureOpenSpecCodex(repoRoot, actions) {
|
|
|
83
131
|
action: "openspec update --force .",
|
|
84
132
|
status: update.status === 0 ? "updated" : "failed",
|
|
85
133
|
refs: problems,
|
|
86
|
-
detail: update.status === 0 ? undefined : (update
|
|
134
|
+
detail: update.status === 0 ? undefined : commandFailure(update),
|
|
87
135
|
});
|
|
88
136
|
if (update.error || update.status !== 0)
|
|
89
|
-
return [`openspec update
|
|
137
|
+
return [`openspec update 执行失败:${commandFailure(update)}`];
|
|
90
138
|
problems = openspecSkillProblems(repoRoot);
|
|
91
|
-
return problems.length === 0 ? [] : [`OpenSpec
|
|
139
|
+
return problems.length === 0 ? [] : [`OpenSpec 配套技能文件仍然缺失或无效:${problems.join(", ")}`];
|
|
92
140
|
}
|
|
93
141
|
function ensureSuperSpecRoles(repoRoot, actions) {
|
|
94
142
|
const problems = [];
|
|
@@ -99,14 +147,14 @@ function ensureSuperSpecRoles(repoRoot, actions) {
|
|
|
99
147
|
created.push(writeSuperSpecAgent(repoRoot, name));
|
|
100
148
|
}
|
|
101
149
|
else if (read_agent_toml_name(agentPath) !== name) {
|
|
102
|
-
problems.push(`
|
|
150
|
+
problems.push(`agent 文件无效:${agentPath}`);
|
|
103
151
|
}
|
|
104
152
|
const promptPath = join(repoRoot, ".codex", "prompts", `${name}.md`);
|
|
105
153
|
if (!existsSync(promptPath) || !statSync(promptPath).isFile()) {
|
|
106
154
|
created.push(writeSuperSpecPrompt(repoRoot, name));
|
|
107
155
|
}
|
|
108
156
|
else if (!readFileSync(promptPath, "utf8").trim()) {
|
|
109
|
-
problems.push(`
|
|
157
|
+
problems.push(`prompt 文件为空:${promptPath}`);
|
|
110
158
|
}
|
|
111
159
|
}
|
|
112
160
|
actions.push({
|
package/dist/src/util.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WorkflowTermHint } from "./i18n.ts";
|
|
1
2
|
export declare const SCHEMA_VERSION = 1;
|
|
2
3
|
export declare const GUARD_VERSION = "superspec-guard@1";
|
|
3
4
|
export declare const CONFIG_FILENAME = "config.yaml";
|
|
@@ -12,6 +13,9 @@ export type Reason = {
|
|
|
12
13
|
code: string;
|
|
13
14
|
message: string;
|
|
14
15
|
refs: string[];
|
|
16
|
+
label_zh?: string;
|
|
17
|
+
hint_zh?: string;
|
|
18
|
+
message_zh?: string;
|
|
15
19
|
};
|
|
16
20
|
export type Decision = {
|
|
17
21
|
allowed: boolean;
|
|
@@ -24,6 +28,16 @@ export type Decision = {
|
|
|
24
28
|
block_reasons: Reason[];
|
|
25
29
|
next_allowed_actions: string[];
|
|
26
30
|
trust_warnings: string[];
|
|
31
|
+
actions?: JsonMap[];
|
|
32
|
+
decision_zh?: string;
|
|
33
|
+
gate_label_zh?: string;
|
|
34
|
+
gate_hint_zh?: string;
|
|
35
|
+
command?: string;
|
|
36
|
+
command_label_zh?: string;
|
|
37
|
+
command_hint_zh?: string;
|
|
38
|
+
next_allowed_actions_zh?: string[];
|
|
39
|
+
trust_warnings_zh?: string[];
|
|
40
|
+
workflow_terms_zh?: WorkflowTermHint[];
|
|
27
41
|
};
|
|
28
42
|
export type TaskInfo = {
|
|
29
43
|
task_id: string;
|
|
@@ -82,7 +96,12 @@ export declare function block(change: string, gate: string, reasons: Reason[], o
|
|
|
82
96
|
gate_summary?: Record<string, any>;
|
|
83
97
|
next_actions?: string[];
|
|
84
98
|
}): Decision;
|
|
85
|
-
export declare function
|
|
99
|
+
export declare function decorateDecision(decision: JsonMap, opts?: {
|
|
100
|
+
command?: string;
|
|
101
|
+
}): JsonMap;
|
|
102
|
+
export declare function printDecision(decision: JsonMap, opts?: {
|
|
103
|
+
command?: string;
|
|
104
|
+
}): void;
|
|
86
105
|
export declare function runCommand(cmd: string, args: string[], opts?: {
|
|
87
106
|
cwd?: string;
|
|
88
107
|
timeout?: number;
|
|
@@ -108,6 +127,7 @@ export declare function commandLookupInvocation(cmd: string, platform?: NodeJS.P
|
|
|
108
127
|
args: string[];
|
|
109
128
|
shell: boolean;
|
|
110
129
|
};
|
|
130
|
+
export declare function selectWindowsCommandCandidate(cmd: string, whereStdout: string): string;
|
|
111
131
|
export declare function windowsShellEscapeArg(arg: string): string;
|
|
112
132
|
export declare function windowsCmdShimInvocation(cmdPath: string, args: string[], comspec?: string): {
|
|
113
133
|
cmd: string;
|