sdd-flow-kit 1.1.0 → 1.2.1
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 +35 -0
- package/dist/cli.js +36 -15
- package/dist/core/bootstrap.js +58 -0
- package/dist/core/opsxDelivery.js +63 -36
- package/dist/core/opsxWorkflowPackage.js +23 -0
- package/dist/core/packageJsonPatch.js +89 -0
- package/dist/core/projectRoots.js +94 -0
- package/dist/postinstall.js +22 -18
- package/dist/steps/doctor.js +19 -0
- package/dist/steps/invokeFlow.js +18 -11
- package/dist/steps/resolveRun.js +4 -5
- package/dist/steps/runDeliver.js +4 -9
- package/dist/steps/runFlow.js +2 -0
- package/dist/steps/runGate.js +80 -20
- package/dist/steps/runPhaseAdvance.js +5 -6
- package/dist/steps/runPropose.js +3 -9
- package/dist/steps/setupProject.js +32 -20
- package/dist/steps/startFlow.js +3 -1
- package/package.json +1 -1
- package/src/templates/artifacts/06-agent-skill.template.md +31 -52
- package/src/templates/rules/sdd-no-implement-until-apply.mdc.template +7 -0
package/README.md
CHANGED
|
@@ -71,8 +71,43 @@ npx sdd-flow-kit doctor --agent cursor
|
|
|
71
71
|
> 完整环境(分支切换 + opsx-workflow)请另执行:`npx sdd-flow-kit install -y`
|
|
72
72
|
> 跳过 postinstall:`SDD_FLOW_KIT_SKIP_POSTINSTALL=1 pnpm add -D sdd-flow-kit`
|
|
73
73
|
|
|
74
|
+
##### `pnpm add` 后依赖全被删掉(`Packages: +1 -932`)
|
|
75
|
+
|
|
76
|
+
**原因**:`pnpm` 会按当前 `package.json` **同步** `node_modules`。若 manifest 里只剩 `sdd-flow-kit`(或文件被误保存成几行空壳),就会卸载其它所有包——**不是** `pnpm add` 单独删包,而是 manifest 已不完整。
|
|
77
|
+
|
|
78
|
+
**恢复**(在业务仓如 `adInsight-web/frontend`):
|
|
79
|
+
|
|
80
|
+
1. 用 Git 恢复完整 `package.json`:`git checkout -- package.json`
|
|
81
|
+
2. 重装:`pnpm install`
|
|
82
|
+
3. 再装 kit(建议 **≥ 1.1.2**,且必须在有完整 `package.json` 的目录执行):
|
|
83
|
+
- `cd frontend && pnpm add -D sdd-flow-kit@1.1.2`
|
|
84
|
+
- 或本地联调:`pnpm add -D file:../../sdd-flow-kit`
|
|
85
|
+
|
|
86
|
+
**v1.1.2+ 防护**:`repairOpsxDeliveryScripts` 仅合并 `scripts`,写入前检查依赖条数与文件体积;`postinstall` 不再改 `package.json`,且必须有 `INIT_CWD` 才运行。
|
|
87
|
+
|
|
88
|
+
**pnpm v10 构建脚本**:若出现 `Ignored build scripts: sdd-flow-kit`,执行 `pnpm approve-builds` 选中 `sdd-flow-kit`,或安装后手动:`node ./node_modules/sdd-flow-kit/dist/postinstall.js`
|
|
89
|
+
|
|
74
90
|
#### 使用说明
|
|
75
91
|
|
|
92
|
+
##### 安装根:cd 到哪,装到哪(v1.2+)
|
|
93
|
+
|
|
94
|
+
默认 **`installRoot = 你执行命令时的当前目录`**(`process.cwd()`),也可用 `--project-root` / `SDD_FLOW_KIT_INSTALL_ROOT` 指定。
|
|
95
|
+
|
|
96
|
+
| 你 cd 到 | 会安装到 |
|
|
97
|
+
|----------|----------|
|
|
98
|
+
| `adInsight-web/frontend` | `frontend/package.json`、`.cursor/skills`、`.opsx`、`pnpm run opsx:delivery-pipeline` |
|
|
99
|
+
| `adInsight-web`(仓库根) | 仓库根(需根目录也有 `package.json`,否则会失败) |
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cd /path/to/adInsight-web/frontend
|
|
103
|
+
npx sdd-flow-kit install --project ADI -y
|
|
104
|
+
npx sdd-flow-kit run --product ADI --version 2.3.2 -y
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**PRD / gate 的 `--project-root`**:与安装根相同;若 `openspec/` 在上级目录,工具会**向上查找**已有 `openspec/PRD`(不把安装写到上级)。
|
|
108
|
+
|
|
109
|
+
旧行为(在仓库根执行时自动落到 `frontend/`):设置 `SDD_FLOW_KIT_LEGACY_MONOREPO=1`。
|
|
110
|
+
|
|
76
111
|
##### 脚本门禁(v1.1+,**不依赖自然语言**)
|
|
77
112
|
|
|
78
113
|
`run` 会在 `openspec/PRD/<runId>/NEXT.md` 写入**必须按顺序执行的命令**。AI 与人都以 **exit code** 为准:失败则不得进入下一阶段、不得改 `src/`。
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,8 @@ const runGate_1 = require("./steps/runGate");
|
|
|
16
16
|
const runPhaseAdvance_1 = require("./steps/runPhaseAdvance");
|
|
17
17
|
const runPropose_1 = require("./steps/runPropose");
|
|
18
18
|
const runDeliver_1 = require("./steps/runDeliver");
|
|
19
|
+
const bootstrap_1 = require("./core/bootstrap");
|
|
20
|
+
const projectRoots_1 = require("./core/projectRoots");
|
|
19
21
|
const ALLOWED_AGENTS = ["cursor", "claude-code", "codex", "openclaw", "custom"];
|
|
20
22
|
function getArgValue(flag) {
|
|
21
23
|
const idx = process_1.default.argv.indexOf(flag);
|
|
@@ -34,9 +36,11 @@ function printHelp() {
|
|
|
34
36
|
"1) 生成流程产物骨架:",
|
|
35
37
|
" npx sdd-flow-kit start --product ADI --version 2.3.2",
|
|
36
38
|
"",
|
|
37
|
-
"2)
|
|
38
|
-
"
|
|
39
|
-
"
|
|
39
|
+
"2) 安装到当前目录(全量:SDD + OPSX + Superpowers,推荐):",
|
|
40
|
+
" cd <你的项目目录> # 如 adInsight-web/frontend",
|
|
41
|
+
" pnpm add -D sdd-flow-kit",
|
|
42
|
+
" npx sdd-flow-kit install --project ADI -y",
|
|
43
|
+
" 本地 OPSX: SDD_OPSX_WORKFLOW_PACKAGE=file:../../opsx-workflow npx sdd-flow-kit install -y",
|
|
40
44
|
"",
|
|
41
45
|
"3) 生成骨架并进入可执行模式(推荐):",
|
|
42
46
|
" npx sdd-flow-kit run --product ADI --version 2.3.2 --agent claude-code",
|
|
@@ -63,7 +67,9 @@ function printHelp() {
|
|
|
63
67
|
" npx sdd-flow-kit deliver --project-root . --run-id <runId> --change <name>",
|
|
64
68
|
"",
|
|
65
69
|
"可选参数:",
|
|
66
|
-
" --projectRoot <path>
|
|
70
|
+
" --projectRoot <path> 目标项目根目录(默认当前目录 = 安装根,cd 到哪装到哪)",
|
|
71
|
+
" 环境变量 SDD_FLOW_KIT_INSTALL_ROOT 可覆盖安装根(绝对路径)",
|
|
72
|
+
" 环境变量 SDD_FLOW_KIT_LEGACY_MONOREPO=1 恢复「仓库根无 package.json 时落到 frontend/」",
|
|
67
73
|
" --agent <name> cursor | claude-code | codex | openclaw | custom(默认 claude-code)",
|
|
68
74
|
" --branch <name> 开发分支;不存在会自动创建",
|
|
69
75
|
" --project <name> OMS | ADI | 欢盟 | AD Tools(默认 OMS,用于生成 docs/*-doc-skill)",
|
|
@@ -138,6 +144,26 @@ async function main() {
|
|
|
138
144
|
process_1.default.exitCode = 1;
|
|
139
145
|
return;
|
|
140
146
|
}
|
|
147
|
+
const skipBootstrap = process_1.default.env.SDD_FLOW_KIT_SKIP_BOOTSTRAP === "1";
|
|
148
|
+
const bootstrapCommands = new Set([
|
|
149
|
+
"doctor",
|
|
150
|
+
"ensure-opsx",
|
|
151
|
+
"run",
|
|
152
|
+
"invoke",
|
|
153
|
+
"install",
|
|
154
|
+
"setup",
|
|
155
|
+
"gate",
|
|
156
|
+
"phase",
|
|
157
|
+
"propose",
|
|
158
|
+
"deliver",
|
|
159
|
+
]);
|
|
160
|
+
if (!skipBootstrap && !dryRun && bootstrapCommands.has(cmd)) {
|
|
161
|
+
await (0, bootstrap_1.bootstrapProjectIfNeeded)({
|
|
162
|
+
projectRoot,
|
|
163
|
+
agent,
|
|
164
|
+
projectKind: projectKind,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
141
167
|
if (cmd === "gate") {
|
|
142
168
|
const expect = getArgValue("--expect");
|
|
143
169
|
if (!expect || !GATE_EXPECTS.includes(expect)) {
|
|
@@ -242,20 +268,13 @@ async function main() {
|
|
|
242
268
|
// eslint-disable-next-line no-console
|
|
243
269
|
console.log(JSON.stringify({ command: "doctor", projectRoot: path_1.default.resolve(projectRoot), result: doctor }, null, 2));
|
|
244
270
|
}
|
|
245
|
-
if (cmd === "ensure-opsx" && !dryRun) {
|
|
246
|
-
await (0, setupProject_1.setupProject)({
|
|
247
|
-
projectRoot,
|
|
248
|
-
agent,
|
|
249
|
-
yes: true,
|
|
250
|
-
light: true,
|
|
251
|
-
projectKind: projectKind,
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
271
|
if (!doctor.ok)
|
|
255
272
|
process_1.default.exitCode = 1;
|
|
256
273
|
return;
|
|
257
274
|
}
|
|
258
275
|
if (cmd === "setup" || cmd === "install") {
|
|
276
|
+
// eslint-disable-next-line no-console
|
|
277
|
+
console.log(`[sdd-flow-kit] 安装根 installRoot=${(0, projectRoots_1.resolveInstallRoot)(projectRoot)} (cwd=${process_1.default.cwd()})`);
|
|
259
278
|
const result = await (0, setupProject_1.setupProject)({
|
|
260
279
|
projectRoot,
|
|
261
280
|
agent,
|
|
@@ -292,7 +311,7 @@ async function main() {
|
|
|
292
311
|
const text = (fromFlag !== null && fromFlag !== void 0 ? fromFlag : raw).trim();
|
|
293
312
|
const parsed = (0, invokeFlow_1.parseInvokeText)(text);
|
|
294
313
|
if (!parsed) {
|
|
295
|
-
throw new Error("invoke
|
|
314
|
+
throw new Error("invoke 解析失败,示例:开发 ADI 2.3.2 需求 | 进行ADI_2.3.2版本开发");
|
|
296
315
|
}
|
|
297
316
|
const options = (0, invokeFlow_1.toRunOptions)({
|
|
298
317
|
parsed,
|
|
@@ -326,6 +345,8 @@ async function main() {
|
|
|
326
345
|
console.log(`已生成 SDD 流程产物骨架:openspec/PRD/${scaffoldRunId}/`);
|
|
327
346
|
return;
|
|
328
347
|
}
|
|
348
|
+
// eslint-disable-next-line no-console
|
|
349
|
+
console.log(`[sdd-flow-kit] 安装根 installRoot=${(0, projectRoots_1.resolveInstallRoot)(projectRoot)} (cwd=${process_1.default.cwd()})`);
|
|
329
350
|
const result = await (0, runFlow_1.runFlow)({
|
|
330
351
|
product,
|
|
331
352
|
version,
|
|
@@ -342,7 +363,7 @@ async function main() {
|
|
|
342
363
|
// eslint-disable-next-line no-console
|
|
343
364
|
console.log(`流程已初始化:openspec/PRD/${result.runId}/`);
|
|
344
365
|
// eslint-disable-next-line no-console
|
|
345
|
-
console.log(`请打开 openspec/PRD/${result.runId}/NEXT.md
|
|
366
|
+
console.log(`请打开 openspec/PRD/${result.runId}/NEXT.md,严格按 gate→phase→propose→deliver 执行;确认点前禁止改 src/。`);
|
|
346
367
|
// eslint-disable-next-line no-console
|
|
347
368
|
console.log(`agent=${result.adapterName}, cli=${result.adapterAvailable ? "detected" : "missing"}, autoExecute=${result.executed ? "yes" : "manual"}`);
|
|
348
369
|
// eslint-disable-next-line no-console
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.bootstrapProjectIfNeeded = bootstrapProjectIfNeeded;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const inferProject_1 = require("./inferProject");
|
|
9
|
+
const packageJsonPatch_1 = require("./packageJsonPatch");
|
|
10
|
+
const opsxDelivery_1 = require("./opsxDelivery");
|
|
11
|
+
const projectRoots_1 = require("./projectRoots");
|
|
12
|
+
const setupProject_1 = require("../steps/setupProject");
|
|
13
|
+
/**
|
|
14
|
+
* pnpm v10 常拦截 postinstall;首次跑 CLI 时补全 SDD + OPSX 全栈(不切分支)。
|
|
15
|
+
*/
|
|
16
|
+
async function bootstrapProjectIfNeeded(options) {
|
|
17
|
+
var _a;
|
|
18
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
19
|
+
if (await (0, opsxDelivery_1.isOpsxApplyReady)(installRoot)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (options.dryRun) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
const kind = (_a = options.projectKind) !== null && _a !== void 0 ? _a : (await (0, inferProject_1.inferProjectKind)(installRoot));
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.log("[sdd-flow-kit] 检测到 OPSX 未就绪,正在自动 bootstrap(等价 install -y)…");
|
|
28
|
+
const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
|
|
29
|
+
const pnpmPatched = await (0, packageJsonPatch_1.ensurePnpmAllowSddFlowKitBuild)(path_1.default.join(pkgRoot, "package.json"));
|
|
30
|
+
if (pnpmPatched) {
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.log("[sdd-flow-kit] 已写入 pnpm.onlyBuiltDependencies: sdd-flow-kit;下次 pnpm install 将执行 postinstall");
|
|
33
|
+
}
|
|
34
|
+
await (0, setupProject_1.setupProject)({
|
|
35
|
+
projectRoot: installRoot,
|
|
36
|
+
agent: options.agent,
|
|
37
|
+
yes: true,
|
|
38
|
+
light: false,
|
|
39
|
+
skipGit: true,
|
|
40
|
+
projectKind: kind !== null && kind !== void 0 ? kind : "ADI",
|
|
41
|
+
});
|
|
42
|
+
if (!(await (0, opsxDelivery_1.isOpsxApplyReady)(installRoot))) {
|
|
43
|
+
await (0, opsxDelivery_1.ensureOpsxWorkflow)({
|
|
44
|
+
projectRoot: installRoot,
|
|
45
|
+
agent: options.agent,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const ready = await (0, opsxDelivery_1.isOpsxApplyReady)(installRoot);
|
|
49
|
+
if (ready) {
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.log("[sdd-flow-kit] bootstrap 完成:SDD + OPSX 已就绪");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.warn("[sdd-flow-kit] bootstrap 后仍有检查未通过,请执行: npx sdd-flow-kit doctor --agent cursor");
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveOpsxWorkflowPackage = resolveOpsxWorkflowPackage;
|
|
6
7
|
exports.deliveryPipelineNpmScripts = deliveryPipelineNpmScripts;
|
|
7
8
|
exports.pathExists = pathExists;
|
|
8
9
|
exports.resolveOpsxAgent = resolveOpsxAgent;
|
|
@@ -14,7 +15,12 @@ exports.collectDeliverySetupChecks = collectDeliverySetupChecks;
|
|
|
14
15
|
const child_process_1 = require("child_process");
|
|
15
16
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
16
17
|
const path_1 = __importDefault(require("path"));
|
|
17
|
-
const
|
|
18
|
+
const packageJsonPatch_1 = require("./packageJsonPatch");
|
|
19
|
+
const projectRoots_1 = require("./projectRoots");
|
|
20
|
+
const opsxWorkflowPackage_1 = require("./opsxWorkflowPackage");
|
|
21
|
+
function resolveOpsxWorkflowPackage() {
|
|
22
|
+
return (0, opsxWorkflowPackage_1.resolveOpsxWorkflowPackageForCwd)(process.cwd());
|
|
23
|
+
}
|
|
18
24
|
const DELIVERY_SCRIPT_REL = ".cursor/skills/opsx-dev-delivery-pipeline/scripts/opsx-delivery-pipeline.sh";
|
|
19
25
|
function deliveryPipelineNpmScripts() {
|
|
20
26
|
const full = `bash ${DELIVERY_SCRIPT_REL}`;
|
|
@@ -44,7 +50,7 @@ async function resolveOpsxAgent(projectRoot, preferred) {
|
|
|
44
50
|
return "claude-code";
|
|
45
51
|
return preferred;
|
|
46
52
|
}
|
|
47
|
-
/** 将 package.json 中过期的 poquan
|
|
53
|
+
/** 将 package.json 中过期的 poquan 脚本修正为 opsx-dev-delivery-pipeline(仅合并 scripts) */
|
|
48
54
|
async function repairOpsxDeliveryScripts(projectRoot) {
|
|
49
55
|
const pkgPath = path_1.default.join(projectRoot, "package.json");
|
|
50
56
|
if (!(await pathExists(pkgPath)))
|
|
@@ -54,29 +60,35 @@ async function repairOpsxDeliveryScripts(projectRoot) {
|
|
|
54
60
|
return false;
|
|
55
61
|
const { full, short } = deliveryPipelineNpmScripts();
|
|
56
62
|
const text = await promises_1.default.readFile(pkgPath, "utf8");
|
|
57
|
-
|
|
63
|
+
let pkg;
|
|
64
|
+
try {
|
|
65
|
+
pkg = JSON.parse(text);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
58
70
|
if (!pkg.scripts)
|
|
59
71
|
pkg.scripts = {};
|
|
60
|
-
let changed = false;
|
|
61
72
|
const needsFix = (v) => !v || v.includes("poquan-dev-delivery-pipeline") || v.includes("poquan-delivery-pipeline");
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
73
|
+
const updates = {};
|
|
74
|
+
if (needsFix(pkg.scripts["opsx:delivery-pipeline"]))
|
|
75
|
+
updates["opsx:delivery-pipeline"] = full;
|
|
66
76
|
if (needsFix(pkg.scripts["opsx:delivery-pipeline:short"])) {
|
|
67
|
-
|
|
68
|
-
changed = true;
|
|
77
|
+
updates["opsx:delivery-pipeline:short"] = short;
|
|
69
78
|
}
|
|
70
|
-
if (
|
|
79
|
+
if (Object.keys(updates).length === 0)
|
|
71
80
|
return false;
|
|
72
|
-
await (0,
|
|
73
|
-
|
|
81
|
+
const result = await (0, packageJsonPatch_1.patchPackageJsonScripts)(pkgPath, updates, {
|
|
82
|
+
minDependencyCount: 2,
|
|
83
|
+
minBytes: 120,
|
|
84
|
+
});
|
|
85
|
+
return result.ok && result.changed;
|
|
74
86
|
}
|
|
75
87
|
function hasCommand(cmd) {
|
|
76
88
|
return (0, child_process_1.spawnSync)("which", [cmd], { stdio: "ignore" }).status === 0;
|
|
77
89
|
}
|
|
78
90
|
function buildOpsxWorkflowSetupCommand(projectRoot, agent) {
|
|
79
|
-
const baseArgs = [
|
|
91
|
+
const baseArgs = [resolveOpsxWorkflowPackage(), "setup"];
|
|
80
92
|
if (agent === "cursor") {
|
|
81
93
|
return {
|
|
82
94
|
cmd: "npx",
|
|
@@ -92,13 +104,16 @@ function buildOpsxWorkflowSetupCommand(projectRoot, agent) {
|
|
|
92
104
|
return { cmd: "npx", args: [...baseArgs, "--editor", "codex", "-y"] };
|
|
93
105
|
}
|
|
94
106
|
async function isOpsxApplyReady(projectRoot) {
|
|
95
|
-
const
|
|
107
|
+
const installRoot = await (0, projectRoots_1.resolvePackageRoot)((0, projectRoots_1.resolveInstallRoot)(projectRoot));
|
|
108
|
+
const checks = await collectDeliverySetupChecks(installRoot);
|
|
96
109
|
return checks.every((c) => c.ok);
|
|
97
110
|
}
|
|
98
111
|
/** 缺 openspec/TDD/Playwright 工作流时安装 @lixin5257xxx/opsx-workflow,并修复 delivery 脚本 */
|
|
99
112
|
async function ensureOpsxWorkflow(options) {
|
|
113
|
+
var _a;
|
|
114
|
+
const installRoot = await (0, projectRoots_1.resolvePackageRoot)((0, projectRoots_1.resolveInstallRoot)(options.projectRoot));
|
|
100
115
|
const plannedActions = [];
|
|
101
|
-
let checks = await collectDeliverySetupChecks(
|
|
116
|
+
let checks = await collectDeliverySetupChecks(installRoot);
|
|
102
117
|
if (checks.every((c) => c.ok)) {
|
|
103
118
|
return {
|
|
104
119
|
ready: true,
|
|
@@ -108,8 +123,8 @@ async function ensureOpsxWorkflow(options) {
|
|
|
108
123
|
plannedActions,
|
|
109
124
|
};
|
|
110
125
|
}
|
|
111
|
-
const agent = await resolveOpsxAgent(
|
|
112
|
-
const { cmd, args } = buildOpsxWorkflowSetupCommand(
|
|
126
|
+
const agent = await resolveOpsxAgent(installRoot, options.agent);
|
|
127
|
+
const { cmd, args } = buildOpsxWorkflowSetupCommand(installRoot, agent);
|
|
113
128
|
plannedActions.push(`missing opsx apply stack → run: ${cmd} ${args.join(" ")}`);
|
|
114
129
|
if (options.dryRun) {
|
|
115
130
|
return {
|
|
@@ -121,18 +136,21 @@ async function ensureOpsxWorkflow(options) {
|
|
|
121
136
|
};
|
|
122
137
|
}
|
|
123
138
|
const ret = (0, child_process_1.spawnSync)(cmd, args, {
|
|
124
|
-
cwd:
|
|
139
|
+
cwd: installRoot,
|
|
125
140
|
stdio: "inherit",
|
|
126
141
|
env: process.env,
|
|
127
142
|
});
|
|
128
143
|
if (ret.status !== 0) {
|
|
129
|
-
|
|
144
|
+
const hint = ((_a = process.env.SDD_OPSX_WORKFLOW_PACKAGE) === null || _a === void 0 ? void 0 : _a.trim()) ?
|
|
145
|
+
""
|
|
146
|
+
: " 本地 OPSX 可设: SDD_OPSX_WORKFLOW_PACKAGE=file:../../opsx-workflow";
|
|
147
|
+
throw new Error(`opsx-workflow 安装失败:${cmd} ${args.join(" ")}${hint}`);
|
|
130
148
|
}
|
|
131
|
-
const repairedScripts = await repairOpsxDeliveryScripts(
|
|
149
|
+
const repairedScripts = await repairOpsxDeliveryScripts(installRoot);
|
|
132
150
|
if (repairedScripts) {
|
|
133
151
|
plannedActions.push("repaired package.json opsx:delivery-pipeline scripts");
|
|
134
152
|
}
|
|
135
|
-
checks = await collectDeliverySetupChecks(
|
|
153
|
+
checks = await collectDeliverySetupChecks(installRoot);
|
|
136
154
|
return {
|
|
137
155
|
ready: checks.every((c) => c.ok),
|
|
138
156
|
workflowInstalled: true,
|
|
@@ -141,45 +159,54 @@ async function ensureOpsxWorkflow(options) {
|
|
|
141
159
|
plannedActions,
|
|
142
160
|
};
|
|
143
161
|
}
|
|
144
|
-
async function collectDeliverySetupChecks(
|
|
162
|
+
async function collectDeliverySetupChecks(installRoot) {
|
|
145
163
|
var _a, _b, _c, _d, _e, _f;
|
|
146
164
|
const checks = [];
|
|
165
|
+
const root = (0, projectRoots_1.resolveInstallRoot)(installRoot);
|
|
166
|
+
const openspecRoot = await (0, projectRoots_1.resolveOpenspecRoot)(root);
|
|
167
|
+
checks.push({
|
|
168
|
+
name: "install-root",
|
|
169
|
+
ok: true,
|
|
170
|
+
detail: `安装根=${root}`,
|
|
171
|
+
});
|
|
147
172
|
checks.push({
|
|
148
173
|
name: "openspec-dir",
|
|
149
|
-
ok: await pathExists(path_1.default.join(
|
|
150
|
-
detail: "openspec/
|
|
174
|
+
ok: await pathExists(path_1.default.join(openspecRoot, "openspec")),
|
|
175
|
+
detail: openspecRoot === root ? "openspec/ 目录(安装根)" : `openspec/ 目录(上级 ${openspecRoot})`,
|
|
151
176
|
});
|
|
177
|
+
const openspecOk = hasCommand("openspec") ||
|
|
178
|
+
(0, child_process_1.spawnSync)("npx", ["openspec", "--version"], { stdio: "ignore", cwd: root }).status === 0;
|
|
152
179
|
checks.push({
|
|
153
180
|
name: "openspec-cli",
|
|
154
|
-
ok:
|
|
155
|
-
detail: "openspec CLI
|
|
181
|
+
ok: openspecOk,
|
|
182
|
+
detail: openspecOk ? "openspec CLI 可用" : "请安装 openspec CLI 或执行 opsx-workflow setup",
|
|
156
183
|
});
|
|
157
|
-
const skillPath = path_1.default.join(
|
|
184
|
+
const skillPath = path_1.default.join(root, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md");
|
|
158
185
|
checks.push({
|
|
159
186
|
name: "opsx-dev-delivery-pipeline-skill",
|
|
160
187
|
ok: await pathExists(skillPath),
|
|
161
188
|
detail: skillPath,
|
|
162
189
|
});
|
|
163
|
-
const tddPath = path_1.default.join(
|
|
190
|
+
const tddPath = path_1.default.join(root, ".cursor", "skills", "tdd-script", "SKILL.md");
|
|
164
191
|
checks.push({
|
|
165
192
|
name: "tdd-script-skill",
|
|
166
193
|
ok: await pathExists(tddPath),
|
|
167
194
|
detail: tddPath,
|
|
168
195
|
});
|
|
169
|
-
const applyPath = path_1.default.join(
|
|
196
|
+
const applyPath = path_1.default.join(root, ".cursor", "skills", "openspec-apply-change", "SKILL.md");
|
|
170
197
|
checks.push({
|
|
171
198
|
name: "openspec-apply-change-skill",
|
|
172
199
|
ok: await pathExists(applyPath),
|
|
173
200
|
detail: applyPath,
|
|
174
201
|
});
|
|
175
|
-
const pipelineScript = path_1.default.join(
|
|
202
|
+
const pipelineScript = path_1.default.join(root, DELIVERY_SCRIPT_REL);
|
|
176
203
|
checks.push({
|
|
177
204
|
name: "delivery-pipeline-script",
|
|
178
205
|
ok: await pathExists(pipelineScript),
|
|
179
206
|
detail: pipelineScript,
|
|
180
207
|
});
|
|
181
208
|
try {
|
|
182
|
-
const pkgText = await promises_1.default.readFile(path_1.default.join(
|
|
209
|
+
const pkgText = await promises_1.default.readFile(path_1.default.join(root, "package.json"), "utf8");
|
|
183
210
|
const pkg = JSON.parse(pkgText);
|
|
184
211
|
const cmd = (_b = (_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a["opsx:delivery-pipeline"]) !== null && _b !== void 0 ? _b : "";
|
|
185
212
|
const ok = cmd.includes("opsx-dev-delivery-pipeline") &&
|
|
@@ -199,10 +226,10 @@ async function collectDeliverySetupChecks(projectRoot) {
|
|
|
199
226
|
});
|
|
200
227
|
}
|
|
201
228
|
try {
|
|
202
|
-
const cfgText = await promises_1.default.readFile(path_1.default.join(
|
|
229
|
+
const cfgText = await promises_1.default.readFile(path_1.default.join(root, ".opsx", "config.json"), "utf8");
|
|
203
230
|
const cfg = JSON.parse(cfgText);
|
|
204
231
|
const skillsDir = (_c = cfg.skillsDir) !== null && _c !== void 0 ? _c : "";
|
|
205
|
-
const hasCursorDelivery = await pathExists(path_1.default.join(
|
|
232
|
+
const hasCursorDelivery = await pathExists(path_1.default.join(root, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md"));
|
|
206
233
|
const nameOk = ((_d = cfg.deliveryPipeline) === null || _d === void 0 ? void 0 : _d.skillName) === "opsx-dev-delivery-pipeline";
|
|
207
234
|
const dirOk = (hasCursorDelivery && skillsDir.includes(".cursor")) ||
|
|
208
235
|
(!hasCursorDelivery && (skillsDir.includes(".cursor") || skillsDir.includes(".claude")));
|
|
@@ -222,7 +249,7 @@ async function collectDeliverySetupChecks(projectRoot) {
|
|
|
222
249
|
});
|
|
223
250
|
}
|
|
224
251
|
try {
|
|
225
|
-
const pkgText = await promises_1.default.readFile(path_1.default.join(
|
|
252
|
+
const pkgText = await promises_1.default.readFile(path_1.default.join(root, "package.json"), "utf8");
|
|
226
253
|
const pkg = JSON.parse(pkgText);
|
|
227
254
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
228
255
|
const hasPw = Boolean(deps["@playwright/test"] || deps.playwright);
|
|
@@ -239,7 +266,7 @@ async function collectDeliverySetupChecks(projectRoot) {
|
|
|
239
266
|
detail: "无法读取 package.json 检查 Playwright",
|
|
240
267
|
});
|
|
241
268
|
}
|
|
242
|
-
const agentsMd = path_1.default.join(
|
|
269
|
+
const agentsMd = path_1.default.join(root, "AGENTS.md");
|
|
243
270
|
checks.push({
|
|
244
271
|
name: "agents-md",
|
|
245
272
|
ok: await pathExists(agentsMd),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveOpsxWorkflowPackageForCwd = resolveOpsxWorkflowPackageForCwd;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/** 本地联调 SDD_OPSX_WORKFLOW_PACKAGE=file:../../opsx-workflow;不存在则回退 npm 包 */
|
|
10
|
+
function resolveOpsxWorkflowPackageForCwd(cwd) {
|
|
11
|
+
var _a;
|
|
12
|
+
const fromEnv = (_a = process.env.SDD_OPSX_WORKFLOW_PACKAGE) === null || _a === void 0 ? void 0 : _a.trim();
|
|
13
|
+
if (fromEnv === null || fromEnv === void 0 ? void 0 : fromEnv.startsWith("file:")) {
|
|
14
|
+
const localPath = path_1.default.resolve(cwd, fromEnv.slice("file:".length));
|
|
15
|
+
if (fs_1.default.existsSync(localPath)) {
|
|
16
|
+
return fromEnv;
|
|
17
|
+
}
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
console.warn(`[sdd-flow-kit] SDD_OPSX_WORKFLOW_PACKAGE=${fromEnv} 不存在 (${localPath}),改用 @lixin5257xxx/opsx-workflow`);
|
|
20
|
+
return "@lixin5257xxx/opsx-workflow";
|
|
21
|
+
}
|
|
22
|
+
return fromEnv || "@lixin5257xxx/opsx-workflow";
|
|
23
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.countPackageDependencies = countPackageDependencies;
|
|
7
|
+
exports.patchPackageJsonScripts = patchPackageJsonScripts;
|
|
8
|
+
exports.ensurePnpmAllowSddFlowKitBuild = ensurePnpmAllowSddFlowKitBuild;
|
|
9
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
10
|
+
const fs_1 = require("./fs");
|
|
11
|
+
function countPackageDependencies(pkg) {
|
|
12
|
+
const deps = pkg.dependencies;
|
|
13
|
+
const devDeps = pkg.devDependencies;
|
|
14
|
+
return Object.keys(deps !== null && deps !== void 0 ? deps : {}).length + Object.keys(devDeps !== null && devDeps !== void 0 ? devDeps : {}).length;
|
|
15
|
+
}
|
|
16
|
+
async function patchPackageJsonScripts(pkgPath, scriptUpdates, options = {}) {
|
|
17
|
+
var _a, _b, _c, _d;
|
|
18
|
+
const minDependencyCount = (_a = options.minDependencyCount) !== null && _a !== void 0 ? _a : 1;
|
|
19
|
+
const minBytes = (_b = options.minBytes) !== null && _b !== void 0 ? _b : 80;
|
|
20
|
+
const minSizeRatio = (_c = options.minSizeRatio) !== null && _c !== void 0 ? _c : 0.5;
|
|
21
|
+
let text;
|
|
22
|
+
try {
|
|
23
|
+
text = await promises_1.default.readFile(pkgPath, "utf8");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { ok: false, reason: "package.json 不存在或不可读" };
|
|
27
|
+
}
|
|
28
|
+
if (text.length < minBytes) {
|
|
29
|
+
return { ok: false, reason: `package.json 过小(${text.length} 字节)` };
|
|
30
|
+
}
|
|
31
|
+
let pkg;
|
|
32
|
+
try {
|
|
33
|
+
pkg = JSON.parse(text);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return { ok: false, reason: "package.json 不是合法 JSON" };
|
|
37
|
+
}
|
|
38
|
+
if (countPackageDependencies(pkg) < minDependencyCount) {
|
|
39
|
+
return { ok: false, reason: "dependencies 过少" };
|
|
40
|
+
}
|
|
41
|
+
const beforeKeys = Object.keys(pkg);
|
|
42
|
+
const scripts = (_d = pkg.scripts) !== null && _d !== void 0 ? _d : {};
|
|
43
|
+
let changed = false;
|
|
44
|
+
for (const [key, value] of Object.entries(scriptUpdates)) {
|
|
45
|
+
if (scripts[key] !== value) {
|
|
46
|
+
scripts[key] = value;
|
|
47
|
+
changed = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!changed)
|
|
51
|
+
return { ok: true, changed: false };
|
|
52
|
+
pkg.scripts = scripts;
|
|
53
|
+
for (const key of beforeKeys) {
|
|
54
|
+
if (!Object.keys(pkg).includes(key)) {
|
|
55
|
+
return { ok: false, reason: `会丢失字段: ${key}` };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const out = `${JSON.stringify(pkg, null, 2)}\n`;
|
|
59
|
+
if (out.length < text.length * minSizeRatio) {
|
|
60
|
+
return { ok: false, reason: "写入后体积异常缩小" };
|
|
61
|
+
}
|
|
62
|
+
await (0, fs_1.writeTextFileEnsuredDir)(pkgPath, out);
|
|
63
|
+
return { ok: true, changed: true };
|
|
64
|
+
}
|
|
65
|
+
async function ensurePnpmAllowSddFlowKitBuild(pkgPath) {
|
|
66
|
+
var _a, _b;
|
|
67
|
+
let text;
|
|
68
|
+
try {
|
|
69
|
+
text = await promises_1.default.readFile(pkgPath, "utf8");
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
let pkg;
|
|
75
|
+
try {
|
|
76
|
+
pkg = JSON.parse(text);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const pnpm = (_a = pkg.pnpm) !== null && _a !== void 0 ? _a : {};
|
|
82
|
+
const list = (_b = pnpm.onlyBuiltDependencies) !== null && _b !== void 0 ? _b : [];
|
|
83
|
+
if (list.includes("sdd-flow-kit"))
|
|
84
|
+
return false;
|
|
85
|
+
pnpm.onlyBuiltDependencies = [...list, "sdd-flow-kit"];
|
|
86
|
+
pkg.pnpm = pnpm;
|
|
87
|
+
await (0, fs_1.writeTextFileEnsuredDir)(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveInstallRoot = resolveInstallRoot;
|
|
7
|
+
exports.isLegacyMonorepoMode = isLegacyMonorepoMode;
|
|
8
|
+
exports.resolveOpenspecRoot = resolveOpenspecRoot;
|
|
9
|
+
exports.resolvePackageRoot = resolvePackageRoot;
|
|
10
|
+
exports.listOpenspecPrdRoots = listOpenspecPrdRoots;
|
|
11
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
async function pathExists(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
await promises_1.default.access(filePath);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 安装根:严格等于 CLI 的 projectRoot(默认 process.cwd())。
|
|
24
|
+
* 可通过 SDD_FLOW_KIT_INSTALL_ROOT 覆盖(绝对路径)。
|
|
25
|
+
* 原则:cd 到哪,package.json / .cursor/skills / opsx-workflow 就装到哪。
|
|
26
|
+
*/
|
|
27
|
+
function resolveInstallRoot(projectRoot) {
|
|
28
|
+
var _a;
|
|
29
|
+
const fromEnv = (_a = process.env.SDD_FLOW_KIT_INSTALL_ROOT) === null || _a === void 0 ? void 0 : _a.trim();
|
|
30
|
+
if (fromEnv)
|
|
31
|
+
return path_1.default.resolve(fromEnv);
|
|
32
|
+
return path_1.default.resolve(projectRoot);
|
|
33
|
+
}
|
|
34
|
+
/** 是否启用旧版 monorepo 行为(在仓库根执行时自动落到 frontend/) */
|
|
35
|
+
function isLegacyMonorepoMode() {
|
|
36
|
+
return process.env.SDD_FLOW_KIT_LEGACY_MONOREPO === "1" || process.env.SDD_FLOW_KIT_LEGACY_MONOREPO === "true";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 从 installRoot 向上查找含 openspec/ 的目录(用于解析 PRD run、openspec CLI)。
|
|
40
|
+
* 找不到则退回 installRoot(将在其下新建 openspec/PRD)。
|
|
41
|
+
*/
|
|
42
|
+
async function resolveOpenspecRoot(installRoot) {
|
|
43
|
+
let cur = path_1.default.resolve(installRoot);
|
|
44
|
+
for (let i = 0; i < 12; i += 1) {
|
|
45
|
+
if (await pathExists(path_1.default.join(cur, "openspec"))) {
|
|
46
|
+
return cur;
|
|
47
|
+
}
|
|
48
|
+
const parent = path_1.default.dirname(cur);
|
|
49
|
+
if (parent === cur)
|
|
50
|
+
break;
|
|
51
|
+
cur = parent;
|
|
52
|
+
}
|
|
53
|
+
return path_1.default.resolve(installRoot);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 交付/门禁用的包根:默认与 installRoot 相同。
|
|
57
|
+
* 仅当 SDD_FLOW_KIT_LEGACY_MONOREPO=1 且 installRoot 无 package.json 但存在 frontend/package.json 时回落到 frontend/。
|
|
58
|
+
*/
|
|
59
|
+
async function resolvePackageRoot(installRoot) {
|
|
60
|
+
const root = resolveInstallRoot(installRoot);
|
|
61
|
+
if (!isLegacyMonorepoMode()) {
|
|
62
|
+
return root;
|
|
63
|
+
}
|
|
64
|
+
const pkgAtRoot = path_1.default.join(root, "package.json");
|
|
65
|
+
if (await pathExists(pkgAtRoot)) {
|
|
66
|
+
return root;
|
|
67
|
+
}
|
|
68
|
+
const fePkg = path_1.default.join(root, "frontend", "package.json");
|
|
69
|
+
if (await pathExists(fePkg)) {
|
|
70
|
+
return path_1.default.join(root, "frontend");
|
|
71
|
+
}
|
|
72
|
+
return root;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 收集自 installRoot 向上所有存在的 openspec/PRD 目录(用于 resolveRun)。
|
|
76
|
+
*/
|
|
77
|
+
async function listOpenspecPrdRoots(startDir) {
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
const out = [];
|
|
80
|
+
let cur = path_1.default.resolve(startDir);
|
|
81
|
+
for (let i = 0; i < 12; i += 1) {
|
|
82
|
+
const prdRoot = path_1.default.join(cur, "openspec", "PRD");
|
|
83
|
+
const key = prdRoot.toLowerCase();
|
|
84
|
+
if (!seen.has(key) && (await pathExists(prdRoot))) {
|
|
85
|
+
seen.add(key);
|
|
86
|
+
out.push(prdRoot);
|
|
87
|
+
}
|
|
88
|
+
const parent = path_1.default.dirname(cur);
|
|
89
|
+
if (parent === cur)
|
|
90
|
+
break;
|
|
91
|
+
cur = parent;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|