sdd-flow-kit 1.0.27 → 1.1.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 +51 -9
- package/dist/cli.js +148 -19
- package/dist/core/gitDiffGuard.js +83 -0
- package/dist/core/openspecGate.js +83 -0
- package/dist/core/opsxDelivery.js +29 -19
- package/dist/core/prdArtifacts.js +53 -0
- package/dist/core/projectRoots.js +94 -0
- package/dist/core/sessionState.js +55 -0
- package/dist/core/writeNext.js +54 -0
- package/dist/index.js +13 -1
- package/dist/steps/resolveRun.js +60 -0
- package/dist/steps/runDeliver.js +77 -0
- package/dist/steps/runGate.js +280 -0
- package/dist/steps/runPhaseAdvance.js +194 -0
- package/dist/steps/runPropose.js +76 -0
- package/dist/steps/setupProject.js +19 -16
- package/dist/steps/startFlow.js +13 -17
- package/package.json +1 -1
- package/src/templates/artifacts/06-agent-skill.template.md +49 -124
- package/src/templates/rules/sdd-no-implement-until-apply.mdc.template +32 -33
package/README.md
CHANGED
|
@@ -73,14 +73,55 @@ npx sdd-flow-kit doctor --agent cursor
|
|
|
73
73
|
|
|
74
74
|
#### 使用说明
|
|
75
75
|
|
|
76
|
-
#####
|
|
76
|
+
##### 安装根:cd 到哪,装到哪(v1.2+)
|
|
77
|
+
|
|
78
|
+
默认 **`installRoot = 你执行命令时的当前目录`**(`process.cwd()`),也可用 `--project-root` / `SDD_FLOW_KIT_INSTALL_ROOT` 指定。
|
|
79
|
+
|
|
80
|
+
| 你 cd 到 | 会安装到 |
|
|
81
|
+
|----------|----------|
|
|
82
|
+
| `adInsight-web/frontend` | `frontend/package.json`、`.cursor/skills`、`.opsx`、`pnpm run opsx:delivery-pipeline` |
|
|
83
|
+
| `adInsight-web`(仓库根) | 仓库根(需根目录也有 `package.json`,否则会失败) |
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cd /path/to/adInsight-web/frontend
|
|
87
|
+
npx sdd-flow-kit install --project ADI -y
|
|
88
|
+
npx sdd-flow-kit run --product ADI --version 2.3.2 -y
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**PRD / gate 的 `--project-root`**:与安装根相同;若 `openspec/` 在上级目录,工具会**向上查找**已有 `openspec/PRD`(不把安装写到上级)。
|
|
92
|
+
|
|
93
|
+
旧行为(在仓库根执行时自动落到 `frontend/`):设置 `SDD_FLOW_KIT_LEGACY_MONOREPO=1`。
|
|
77
94
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
##### 脚本门禁(v1.1+,**不依赖自然语言**)
|
|
96
|
+
|
|
97
|
+
`run` 会在 `openspec/PRD/<runId>/NEXT.md` 写入**必须按顺序执行的命令**。AI 与人都以 **exit code** 为准:失败则不得进入下一阶段、不得改 `src/`。
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# 门禁(失败 exit 1)
|
|
101
|
+
npx sdd-flow-kit gate --project-root . --run-id <runId> --expect prd-fetched
|
|
102
|
+
npx sdd-flow-kit gate --project-root . --run-id <runId> --expect docs-closed # 检测 git 是否误改 src
|
|
103
|
+
npx sdd-flow-kit gate --project-root . --run-id <runId> --expect propose-ready --change <name>
|
|
104
|
+
npx sdd-flow-kit gate --project-root . --run-id <runId> --expect impl-allowed --change <name>
|
|
105
|
+
npx sdd-flow-kit gate --project-root . --run-id <runId> --expect shipped --change <name>
|
|
106
|
+
|
|
107
|
+
# 阶段推进(更新 .session-state.json + NEXT.md)
|
|
108
|
+
npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to after-prd
|
|
109
|
+
npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to docs-done
|
|
110
|
+
npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to propose-done --change <name>
|
|
111
|
+
npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to impl --change <name>
|
|
112
|
+
npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to shipped --change <name>
|
|
113
|
+
|
|
114
|
+
# 提案 / 交付
|
|
115
|
+
npx sdd-flow-kit propose --project-root . --run-id <runId> --change <name>
|
|
116
|
+
npx sdd-flow-kit deliver --project-root . --run-id <runId> --change <name>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`docs-closed` 会在 git 仓库中扫描 `src/`、`frontend/src/` 等路径;文档阶段若已有实现层改动,**gate 失败**。
|
|
120
|
+
|
|
121
|
+
##### 需求澄清:用户无需背提示词
|
|
82
122
|
|
|
83
|
-
|
|
123
|
+
`install` 会写入 `.cursor/rules/sdd-flow-kit-gates.mdc` 与触发 Skill(已改为**脚本优先**)。
|
|
124
|
+
用户粘贴确认点后:先更新 01/02/03/04,再执行 `gate docs-closed` → `phase advance --to docs-done`;**禁止**跳过 gate 直接改代码。
|
|
84
125
|
|
|
85
126
|
##### `/opsx-apply` 与 TDD + Playwright 检查机制
|
|
86
127
|
|
|
@@ -105,9 +146,10 @@ npx sdd-flow-kit doctor --agent cursor --fix # 检查并尝试修复
|
|
|
105
146
|
|
|
106
147
|
#### 当前自动化边界
|
|
107
148
|
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
149
|
+
- 已支持:跨工具统一目录结构、提示词、报告模板、**NEXT.md 脚本清单**
|
|
150
|
+
- 已支持:**gate / phase / propose / deliver** 命令(exit code 机械门禁)
|
|
151
|
+
- 已支持:`ensure-opsx` + `openspec status` 校验 apply-ready
|
|
152
|
+
- 未支持:在 Cursor 内全自动跑完全流程(Cursor adapter 仍为 manual);可选 `SDD_FLOW_KIT_ENABLE_CLAUDE_AUTORUN=1` 给 Claude Code
|
|
111
153
|
|
|
112
154
|
#### npm 发包流程
|
|
113
155
|
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,11 @@ const utils_1 = require("./core/utils");
|
|
|
12
12
|
const setupProject_1 = require("./steps/setupProject");
|
|
13
13
|
const doctor_1 = require("./steps/doctor");
|
|
14
14
|
const invokeFlow_1 = require("./steps/invokeFlow");
|
|
15
|
+
const runGate_1 = require("./steps/runGate");
|
|
16
|
+
const runPhaseAdvance_1 = require("./steps/runPhaseAdvance");
|
|
17
|
+
const runPropose_1 = require("./steps/runPropose");
|
|
18
|
+
const runDeliver_1 = require("./steps/runDeliver");
|
|
19
|
+
const projectRoots_1 = require("./core/projectRoots");
|
|
15
20
|
const ALLOWED_AGENTS = ["cursor", "claude-code", "codex", "openclaw", "custom"];
|
|
16
21
|
function getArgValue(flag) {
|
|
17
22
|
const idx = process_1.default.argv.indexOf(flag);
|
|
@@ -45,10 +50,23 @@ function printHelp() {
|
|
|
45
50
|
"",
|
|
46
51
|
"6) Apply 前确保 OPSX+TDD+Playwright(/opsx-apply 前必须):",
|
|
47
52
|
" npx sdd-flow-kit ensure-opsx --agent cursor -y",
|
|
48
|
-
"
|
|
53
|
+
"",
|
|
54
|
+
"7) 脚本门禁(失败 exit 1,不依赖自然语言):",
|
|
55
|
+
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect prd-fetched",
|
|
56
|
+
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect docs-closed",
|
|
57
|
+
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect impl-allowed --change <name>",
|
|
58
|
+
"",
|
|
59
|
+
"8) 阶段推进(更新 session + NEXT.md):",
|
|
60
|
+
" npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to after-prd|docs-done|propose-done|impl|shipped",
|
|
61
|
+
"",
|
|
62
|
+
"9) 提案 / 交付:",
|
|
63
|
+
" npx sdd-flow-kit propose --project-root . --run-id <runId> --change <name>",
|
|
64
|
+
" npx sdd-flow-kit deliver --project-root . --run-id <runId> --change <name>",
|
|
49
65
|
"",
|
|
50
66
|
"可选参数:",
|
|
51
|
-
" --projectRoot <path>
|
|
67
|
+
" --projectRoot <path> 目标项目根目录(默认当前目录 = 安装根,cd 到哪装到哪)",
|
|
68
|
+
" 环境变量 SDD_FLOW_KIT_INSTALL_ROOT 可覆盖安装根(绝对路径)",
|
|
69
|
+
" 环境变量 SDD_FLOW_KIT_LEGACY_MONOREPO=1 恢复「仓库根无 package.json 时落到 frontend/」",
|
|
52
70
|
" --agent <name> cursor | claude-code | codex | openclaw | custom(默认 claude-code)",
|
|
53
71
|
" --branch <name> 开发分支;不存在会自动创建",
|
|
54
72
|
" --project <name> OMS | ADI | 欢盟 | AD Tools(默认 OMS,用于生成 docs/*-doc-skill)",
|
|
@@ -61,32 +79,53 @@ function printHelp() {
|
|
|
61
79
|
].join("\n"));
|
|
62
80
|
}
|
|
63
81
|
async function main() {
|
|
64
|
-
var _a, _b, _c, _d;
|
|
82
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
65
83
|
const cmd = process_1.default.argv[2];
|
|
66
84
|
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
67
85
|
printHelp();
|
|
68
86
|
return;
|
|
69
87
|
}
|
|
88
|
+
const GATE_EXPECTS = [
|
|
89
|
+
"prd-fetched",
|
|
90
|
+
"questions-open",
|
|
91
|
+
"docs-closed",
|
|
92
|
+
"propose-ready",
|
|
93
|
+
"impl-allowed",
|
|
94
|
+
"shipped",
|
|
95
|
+
];
|
|
96
|
+
const PHASE_TARGETS = [
|
|
97
|
+
"after-prd",
|
|
98
|
+
"docs-done",
|
|
99
|
+
"propose-done",
|
|
100
|
+
"impl",
|
|
101
|
+
"shipped",
|
|
102
|
+
];
|
|
70
103
|
if (cmd !== "start" &&
|
|
71
104
|
cmd !== "run" &&
|
|
72
105
|
cmd !== "setup" &&
|
|
73
106
|
cmd !== "install" &&
|
|
74
107
|
cmd !== "doctor" &&
|
|
75
108
|
cmd !== "ensure-opsx" &&
|
|
76
|
-
cmd !== "invoke"
|
|
109
|
+
cmd !== "invoke" &&
|
|
110
|
+
cmd !== "gate" &&
|
|
111
|
+
cmd !== "phase" &&
|
|
112
|
+
cmd !== "propose" &&
|
|
113
|
+
cmd !== "deliver") {
|
|
77
114
|
printHelp();
|
|
78
115
|
process_1.default.exitCode = 1;
|
|
79
116
|
return;
|
|
80
117
|
}
|
|
81
118
|
const product = getArgValue("--product");
|
|
82
119
|
const version = getArgValue("--version");
|
|
83
|
-
const projectRoot = (_a = getArgValue("--
|
|
84
|
-
const
|
|
120
|
+
const projectRoot = path_1.default.resolve((_b = (_a = getArgValue("--project-root")) !== null && _a !== void 0 ? _a : getArgValue("--projectRoot")) !== null && _b !== void 0 ? _b : process_1.default.cwd());
|
|
121
|
+
const runId = (_d = (_c = getArgValue("--run-id")) !== null && _c !== void 0 ? _c : getArgValue("--runId")) !== null && _d !== void 0 ? _d : undefined;
|
|
122
|
+
const changeName = (_e = getArgValue("--change")) !== null && _e !== void 0 ? _e : undefined;
|
|
123
|
+
const branch = (_f = getArgValue("--branch")) !== null && _f !== void 0 ? _f : undefined;
|
|
85
124
|
const yes = process_1.default.argv.includes("-y");
|
|
86
125
|
const dryRun = process_1.default.argv.includes("--dry-run");
|
|
87
126
|
const planJson = process_1.default.argv.includes("--plan-json");
|
|
88
|
-
const agentValue = (
|
|
89
|
-
const projectKind = ((
|
|
127
|
+
const agentValue = (_g = getArgValue("--agent")) !== null && _g !== void 0 ? _g : "claude-code";
|
|
128
|
+
const projectKind = ((_h = getArgValue("--project")) !== null && _h !== void 0 ? _h : "OMS");
|
|
90
129
|
if (!ALLOWED_AGENTS.includes(agentValue)) {
|
|
91
130
|
// eslint-disable-next-line no-console
|
|
92
131
|
console.error(`不支持的 --agent: ${agentValue}`);
|
|
@@ -102,10 +141,96 @@ async function main() {
|
|
|
102
141
|
process_1.default.exitCode = 1;
|
|
103
142
|
return;
|
|
104
143
|
}
|
|
144
|
+
if (cmd === "gate") {
|
|
145
|
+
const expect = getArgValue("--expect");
|
|
146
|
+
if (!expect || !GATE_EXPECTS.includes(expect)) {
|
|
147
|
+
// eslint-disable-next-line no-console
|
|
148
|
+
console.error(`gate 需要 --expect ${GATE_EXPECTS.join("|")}`);
|
|
149
|
+
process_1.default.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const result = await (0, runGate_1.runGate)({
|
|
153
|
+
projectRoot,
|
|
154
|
+
runId,
|
|
155
|
+
expect,
|
|
156
|
+
change: changeName,
|
|
157
|
+
});
|
|
158
|
+
(0, runGate_1.printGateResult)(result);
|
|
159
|
+
if (!result.ok)
|
|
160
|
+
process_1.default.exitCode = 1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (cmd === "phase") {
|
|
164
|
+
const sub = process_1.default.argv[3];
|
|
165
|
+
if (sub !== "advance") {
|
|
166
|
+
// eslint-disable-next-line no-console
|
|
167
|
+
console.error("用法: sdd-flow-kit phase advance --to <target> ...");
|
|
168
|
+
process_1.default.exitCode = 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const to = getArgValue("--to");
|
|
172
|
+
if (!to || !PHASE_TARGETS.includes(to)) {
|
|
173
|
+
// eslint-disable-next-line no-console
|
|
174
|
+
console.error(`需要 --to ${PHASE_TARGETS.join("|")}`);
|
|
175
|
+
process_1.default.exitCode = 1;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const result = await (0, runPhaseAdvance_1.runPhaseAdvance)({
|
|
179
|
+
projectRoot,
|
|
180
|
+
runId,
|
|
181
|
+
to,
|
|
182
|
+
change: changeName,
|
|
183
|
+
dryRun,
|
|
184
|
+
});
|
|
185
|
+
// eslint-disable-next-line no-console
|
|
186
|
+
console.log(result.ok ? `✅ ${result.message}` : `❌ ${result.message}`);
|
|
187
|
+
if (!result.ok)
|
|
188
|
+
process_1.default.exitCode = 1;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (cmd === "propose") {
|
|
192
|
+
if (!changeName) {
|
|
193
|
+
// eslint-disable-next-line no-console
|
|
194
|
+
console.error("propose 需要 --change <kebab-name>");
|
|
195
|
+
process_1.default.exitCode = 1;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const result = await (0, runPropose_1.runPropose)({
|
|
199
|
+
projectRoot,
|
|
200
|
+
runId,
|
|
201
|
+
change: changeName,
|
|
202
|
+
dryRun,
|
|
203
|
+
});
|
|
204
|
+
// eslint-disable-next-line no-console
|
|
205
|
+
console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
|
|
206
|
+
if (!result.ok)
|
|
207
|
+
process_1.default.exitCode = 1;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (cmd === "deliver") {
|
|
211
|
+
if (!changeName) {
|
|
212
|
+
// eslint-disable-next-line no-console
|
|
213
|
+
console.error("deliver 需要 --change <kebab-name>");
|
|
214
|
+
process_1.default.exitCode = 1;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const result = await (0, runDeliver_1.runDeliver)({
|
|
218
|
+
projectRoot,
|
|
219
|
+
runId,
|
|
220
|
+
change: changeName,
|
|
221
|
+
agent,
|
|
222
|
+
dryRun,
|
|
223
|
+
});
|
|
224
|
+
// eslint-disable-next-line no-console
|
|
225
|
+
console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
|
|
226
|
+
if (!result.ok)
|
|
227
|
+
process_1.default.exitCode = 1;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
105
230
|
if (cmd === "doctor" || cmd === "ensure-opsx") {
|
|
106
231
|
const fix = cmd === "ensure-opsx" || process_1.default.argv.includes("--fix");
|
|
107
232
|
const doctor = await (0, doctor_1.runDoctor)({
|
|
108
|
-
projectRoot
|
|
233
|
+
projectRoot,
|
|
109
234
|
agent,
|
|
110
235
|
fix,
|
|
111
236
|
dryRun,
|
|
@@ -122,7 +247,7 @@ async function main() {
|
|
|
122
247
|
}
|
|
123
248
|
if (cmd === "ensure-opsx" && !dryRun) {
|
|
124
249
|
await (0, setupProject_1.setupProject)({
|
|
125
|
-
projectRoot
|
|
250
|
+
projectRoot,
|
|
126
251
|
agent,
|
|
127
252
|
yes: true,
|
|
128
253
|
light: true,
|
|
@@ -134,8 +259,10 @@ async function main() {
|
|
|
134
259
|
return;
|
|
135
260
|
}
|
|
136
261
|
if (cmd === "setup" || cmd === "install") {
|
|
262
|
+
// eslint-disable-next-line no-console
|
|
263
|
+
console.log(`[sdd-flow-kit] 安装根 installRoot=${(0, projectRoots_1.resolveInstallRoot)(projectRoot)} (cwd=${process_1.default.cwd()})`);
|
|
137
264
|
const result = await (0, setupProject_1.setupProject)({
|
|
138
|
-
projectRoot
|
|
265
|
+
projectRoot,
|
|
139
266
|
agent,
|
|
140
267
|
branch,
|
|
141
268
|
yes,
|
|
@@ -172,10 +299,9 @@ async function main() {
|
|
|
172
299
|
if (!parsed) {
|
|
173
300
|
throw new Error("invoke 解析失败,请使用:开发 ADI 2.3.2 需求");
|
|
174
301
|
}
|
|
175
|
-
const resolvedRoot = path_1.default.resolve(projectRoot);
|
|
176
302
|
const options = (0, invokeFlow_1.toRunOptions)({
|
|
177
303
|
parsed,
|
|
178
|
-
projectRoot
|
|
304
|
+
projectRoot,
|
|
179
305
|
branch,
|
|
180
306
|
yes,
|
|
181
307
|
dryRun,
|
|
@@ -194,22 +320,23 @@ async function main() {
|
|
|
194
320
|
process_1.default.exitCode = 1;
|
|
195
321
|
return;
|
|
196
322
|
}
|
|
197
|
-
const
|
|
198
|
-
const runId = `${(0, utils_1.formatYYYYMMDD)(new Date())}-${product}-${version}`;
|
|
323
|
+
const scaffoldRunId = `${(0, utils_1.formatYYYYMMDD)(new Date())}-${product}-${version}`;
|
|
199
324
|
if (cmd === "start") {
|
|
200
325
|
await (0, startFlow_1.startFlowScaffold)({
|
|
201
326
|
product,
|
|
202
327
|
version,
|
|
203
|
-
projectRoot
|
|
328
|
+
projectRoot,
|
|
204
329
|
});
|
|
205
330
|
// eslint-disable-next-line no-console
|
|
206
|
-
console.log(`已生成 SDD 流程产物骨架:openspec/PRD/${
|
|
331
|
+
console.log(`已生成 SDD 流程产物骨架:openspec/PRD/${scaffoldRunId}/`);
|
|
207
332
|
return;
|
|
208
333
|
}
|
|
334
|
+
// eslint-disable-next-line no-console
|
|
335
|
+
console.log(`[sdd-flow-kit] 安装根 installRoot=${(0, projectRoots_1.resolveInstallRoot)(projectRoot)} (cwd=${process_1.default.cwd()})`);
|
|
209
336
|
const result = await (0, runFlow_1.runFlow)({
|
|
210
337
|
product,
|
|
211
338
|
version,
|
|
212
|
-
projectRoot
|
|
339
|
+
projectRoot,
|
|
213
340
|
agent,
|
|
214
341
|
branch,
|
|
215
342
|
yes,
|
|
@@ -222,6 +349,8 @@ async function main() {
|
|
|
222
349
|
// eslint-disable-next-line no-console
|
|
223
350
|
console.log(`流程已初始化:openspec/PRD/${result.runId}/`);
|
|
224
351
|
// eslint-disable-next-line no-console
|
|
352
|
+
console.log(`请打开 openspec/PRD/${result.runId}/NEXT.md,按脚本顺序执行 gate / phase / propose / deliver。`);
|
|
353
|
+
// eslint-disable-next-line no-console
|
|
225
354
|
console.log(`agent=${result.adapterName}, cli=${result.adapterAvailable ? "detected" : "missing"}, autoExecute=${result.executed ? "yes" : "manual"}`);
|
|
226
355
|
// eslint-disable-next-line no-console
|
|
227
356
|
console.log(`branch=${result.branch}, envSetup=${result.setupExecuted ? "installed" : "already-ready"}`);
|
|
@@ -233,7 +362,7 @@ async function main() {
|
|
|
233
362
|
// eslint-disable-next-line no-console
|
|
234
363
|
console.log(JSON.stringify({
|
|
235
364
|
command: "run",
|
|
236
|
-
projectRoot
|
|
365
|
+
projectRoot,
|
|
237
366
|
dryRun,
|
|
238
367
|
product,
|
|
239
368
|
version,
|
|
@@ -0,0 +1,83 @@
|
|
|
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.gitDiffNameOnly = gitDiffNameOnly;
|
|
7
|
+
exports.isImplPath = isImplPath;
|
|
8
|
+
exports.isAllowedDocPath = isAllowedDocPath;
|
|
9
|
+
exports.findImplChanges = findImplChanges;
|
|
10
|
+
exports.resolveGitRoot = resolveGitRoot;
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
/**
|
|
14
|
+
* 在 projectRoot 执行 git 命令;非 git 仓库时 isRepo=false(不阻断文档阶段)。
|
|
15
|
+
*/
|
|
16
|
+
function gitDiffNameOnly(projectRoot) {
|
|
17
|
+
const rev = (0, child_process_1.spawnSync)("git", ["rev-parse", "HEAD"], {
|
|
18
|
+
cwd: projectRoot,
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
});
|
|
21
|
+
if (rev.status !== 0) {
|
|
22
|
+
return { ok: true, isRepo: false, changedFiles: [], head: null };
|
|
23
|
+
}
|
|
24
|
+
const head = (rev.stdout || "").trim();
|
|
25
|
+
const diff = (0, child_process_1.spawnSync)("git", ["diff", "--name-only", "HEAD"], {
|
|
26
|
+
cwd: projectRoot,
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
});
|
|
29
|
+
const untracked = (0, child_process_1.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
30
|
+
cwd: projectRoot,
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
});
|
|
33
|
+
const changed = [
|
|
34
|
+
...(diff.stdout || "").split("\n").filter(Boolean),
|
|
35
|
+
...(untracked.stdout || "").split("\n").filter(Boolean),
|
|
36
|
+
];
|
|
37
|
+
const unique = Array.from(new Set(changed));
|
|
38
|
+
return { ok: true, isRepo: true, changedFiles: unique, head };
|
|
39
|
+
}
|
|
40
|
+
/** 业务实现路径:命中则视为「实现层改动」 */
|
|
41
|
+
const IMPL_PATH_PATTERNS = [
|
|
42
|
+
/^src\//,
|
|
43
|
+
/^e2e\//,
|
|
44
|
+
/^frontend\/src\//,
|
|
45
|
+
/^frontend\/e2e\//,
|
|
46
|
+
/^app\//,
|
|
47
|
+
/^pages\//,
|
|
48
|
+
/^components\//,
|
|
49
|
+
];
|
|
50
|
+
function isImplPath(filePath) {
|
|
51
|
+
const p = filePath.replace(/\\/g, "/");
|
|
52
|
+
return IMPL_PATH_PATTERNS.some((re) => re.test(p));
|
|
53
|
+
}
|
|
54
|
+
/** 仅允许 PRD / openspec 文档路径(阶段 B/C) */
|
|
55
|
+
const DOC_PATH_PATTERNS = [
|
|
56
|
+
/^openspec\/PRD\//,
|
|
57
|
+
/^openspec\/changes\//,
|
|
58
|
+
/^frontend\/openspec\/PRD\//,
|
|
59
|
+
/^frontend\/openspec\/changes\//,
|
|
60
|
+
/^\.opsx-active-change$/,
|
|
61
|
+
/^frontend\/\.opsx-active-change$/,
|
|
62
|
+
];
|
|
63
|
+
function isAllowedDocPath(filePath) {
|
|
64
|
+
const p = filePath.replace(/\\/g, "/");
|
|
65
|
+
if (DOC_PATH_PATTERNS.some((re) => re.test(p)))
|
|
66
|
+
return true;
|
|
67
|
+
if (p.endsWith("docs/") || /^docs\//.test(p))
|
|
68
|
+
return true;
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
function findImplChanges(changedFiles) {
|
|
72
|
+
return changedFiles.filter((f) => isImplPath(f) && !isAllowedDocPath(f));
|
|
73
|
+
}
|
|
74
|
+
function resolveGitRoot(projectRoot) {
|
|
75
|
+
const top = (0, child_process_1.spawnSync)("git", ["rev-parse", "--show-toplevel"], {
|
|
76
|
+
cwd: projectRoot,
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
});
|
|
79
|
+
if (top.status === 0 && top.stdout) {
|
|
80
|
+
return top.stdout.trim();
|
|
81
|
+
}
|
|
82
|
+
return path_1.default.resolve(projectRoot);
|
|
83
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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.checkOpenspecApplyReady = checkOpenspecApplyReady;
|
|
7
|
+
exports.checkTasksAllDone = checkTasksAllDone;
|
|
8
|
+
exports.changeDirExists = changeDirExists;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const fs_1 = require("./fs");
|
|
13
|
+
/**
|
|
14
|
+
* 解析 openspec status --json,判断 change 是否 apply-ready。
|
|
15
|
+
*/
|
|
16
|
+
function checkOpenspecApplyReady(projectRoot, changeName) {
|
|
17
|
+
const ret = (0, child_process_1.spawnSync)("openspec", ["status", "--change", changeName, "--json"], {
|
|
18
|
+
cwd: projectRoot,
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
});
|
|
21
|
+
if (ret.status !== 0) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
changeName,
|
|
25
|
+
applyReady: false,
|
|
26
|
+
detail: ret.stderr || ret.stdout || "openspec status 失败",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const json = JSON.parse(ret.stdout || "{}");
|
|
31
|
+
const requires = json.applyRequires || [];
|
|
32
|
+
const artifacts = json.artifacts || [];
|
|
33
|
+
const pending = requires.filter((id) => {
|
|
34
|
+
const art = artifacts.find((a) => a.id === id);
|
|
35
|
+
return !art || art.status !== "done";
|
|
36
|
+
});
|
|
37
|
+
const applyReady = pending.length === 0 && requires.length > 0;
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
changeName,
|
|
41
|
+
applyReady,
|
|
42
|
+
detail: applyReady
|
|
43
|
+
? "apply-ready"
|
|
44
|
+
: `待完成 artifacts: ${pending.join(", ") || requires.join(", ")}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
changeName,
|
|
51
|
+
applyReady: false,
|
|
52
|
+
detail: `无法解析 openspec status JSON: ${e instanceof Error ? e.message : String(e)}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 检查 tasks.md 是否全部勾选完成。
|
|
58
|
+
*/
|
|
59
|
+
async function checkTasksAllDone(projectRoot, changeName) {
|
|
60
|
+
const tasksPath = path_1.default.join(projectRoot, "openspec", "changes", changeName, "tasks.md");
|
|
61
|
+
const content = await (0, fs_1.readFileIfExists)(tasksPath);
|
|
62
|
+
if (!content) {
|
|
63
|
+
return { ok: false, detail: `缺少 tasks.md: ${tasksPath}` };
|
|
64
|
+
}
|
|
65
|
+
const open = content.match(/^- \[ \]/gm);
|
|
66
|
+
if (open && open.length > 0) {
|
|
67
|
+
return { ok: false, detail: `tasks.md 仍有 ${open.length} 项未完成` };
|
|
68
|
+
}
|
|
69
|
+
const hasTask = /- \[[xX]\]/.test(content);
|
|
70
|
+
if (!hasTask) {
|
|
71
|
+
return { ok: false, detail: "tasks.md 无已勾选任务" };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, detail: "tasks.md 已全部勾选" };
|
|
74
|
+
}
|
|
75
|
+
async function changeDirExists(projectRoot, changeName) {
|
|
76
|
+
try {
|
|
77
|
+
await promises_1.default.access(path_1.default.join(projectRoot, "openspec", "changes", changeName));
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -15,6 +15,7 @@ const child_process_1 = require("child_process");
|
|
|
15
15
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
16
16
|
const path_1 = __importDefault(require("path"));
|
|
17
17
|
const fs_1 = require("./fs");
|
|
18
|
+
const projectRoots_1 = require("./projectRoots");
|
|
18
19
|
const DELIVERY_SCRIPT_REL = ".cursor/skills/opsx-dev-delivery-pipeline/scripts/opsx-delivery-pipeline.sh";
|
|
19
20
|
function deliveryPipelineNpmScripts() {
|
|
20
21
|
const full = `bash ${DELIVERY_SCRIPT_REL}`;
|
|
@@ -92,13 +93,15 @@ function buildOpsxWorkflowSetupCommand(projectRoot, agent) {
|
|
|
92
93
|
return { cmd: "npx", args: [...baseArgs, "--editor", "codex", "-y"] };
|
|
93
94
|
}
|
|
94
95
|
async function isOpsxApplyReady(projectRoot) {
|
|
95
|
-
const
|
|
96
|
+
const installRoot = await (0, projectRoots_1.resolvePackageRoot)((0, projectRoots_1.resolveInstallRoot)(projectRoot));
|
|
97
|
+
const checks = await collectDeliverySetupChecks(installRoot);
|
|
96
98
|
return checks.every((c) => c.ok);
|
|
97
99
|
}
|
|
98
100
|
/** 缺 openspec/TDD/Playwright 工作流时安装 @lixin5257xxx/opsx-workflow,并修复 delivery 脚本 */
|
|
99
101
|
async function ensureOpsxWorkflow(options) {
|
|
102
|
+
const installRoot = await (0, projectRoots_1.resolvePackageRoot)((0, projectRoots_1.resolveInstallRoot)(options.projectRoot));
|
|
100
103
|
const plannedActions = [];
|
|
101
|
-
let checks = await collectDeliverySetupChecks(
|
|
104
|
+
let checks = await collectDeliverySetupChecks(installRoot);
|
|
102
105
|
if (checks.every((c) => c.ok)) {
|
|
103
106
|
return {
|
|
104
107
|
ready: true,
|
|
@@ -108,8 +111,8 @@ async function ensureOpsxWorkflow(options) {
|
|
|
108
111
|
plannedActions,
|
|
109
112
|
};
|
|
110
113
|
}
|
|
111
|
-
const agent = await resolveOpsxAgent(
|
|
112
|
-
const { cmd, args } = buildOpsxWorkflowSetupCommand(
|
|
114
|
+
const agent = await resolveOpsxAgent(installRoot, options.agent);
|
|
115
|
+
const { cmd, args } = buildOpsxWorkflowSetupCommand(installRoot, agent);
|
|
113
116
|
plannedActions.push(`missing opsx apply stack → run: ${cmd} ${args.join(" ")}`);
|
|
114
117
|
if (options.dryRun) {
|
|
115
118
|
return {
|
|
@@ -121,18 +124,18 @@ async function ensureOpsxWorkflow(options) {
|
|
|
121
124
|
};
|
|
122
125
|
}
|
|
123
126
|
const ret = (0, child_process_1.spawnSync)(cmd, args, {
|
|
124
|
-
cwd:
|
|
127
|
+
cwd: installRoot,
|
|
125
128
|
stdio: "inherit",
|
|
126
129
|
env: process.env,
|
|
127
130
|
});
|
|
128
131
|
if (ret.status !== 0) {
|
|
129
132
|
throw new Error(`opsx-workflow 安装失败:${cmd} ${args.join(" ")}`);
|
|
130
133
|
}
|
|
131
|
-
const repairedScripts = await repairOpsxDeliveryScripts(
|
|
134
|
+
const repairedScripts = await repairOpsxDeliveryScripts(installRoot);
|
|
132
135
|
if (repairedScripts) {
|
|
133
136
|
plannedActions.push("repaired package.json opsx:delivery-pipeline scripts");
|
|
134
137
|
}
|
|
135
|
-
checks = await collectDeliverySetupChecks(
|
|
138
|
+
checks = await collectDeliverySetupChecks(installRoot);
|
|
136
139
|
return {
|
|
137
140
|
ready: checks.every((c) => c.ok),
|
|
138
141
|
workflowInstalled: true,
|
|
@@ -141,45 +144,52 @@ async function ensureOpsxWorkflow(options) {
|
|
|
141
144
|
plannedActions,
|
|
142
145
|
};
|
|
143
146
|
}
|
|
144
|
-
async function collectDeliverySetupChecks(
|
|
147
|
+
async function collectDeliverySetupChecks(installRoot) {
|
|
145
148
|
var _a, _b, _c, _d, _e, _f;
|
|
146
149
|
const checks = [];
|
|
150
|
+
const root = (0, projectRoots_1.resolveInstallRoot)(installRoot);
|
|
151
|
+
const openspecRoot = await (0, projectRoots_1.resolveOpenspecRoot)(root);
|
|
152
|
+
checks.push({
|
|
153
|
+
name: "install-root",
|
|
154
|
+
ok: true,
|
|
155
|
+
detail: `安装根=${root}`,
|
|
156
|
+
});
|
|
147
157
|
checks.push({
|
|
148
158
|
name: "openspec-dir",
|
|
149
|
-
ok: await pathExists(path_1.default.join(
|
|
150
|
-
detail: "openspec/
|
|
159
|
+
ok: await pathExists(path_1.default.join(openspecRoot, "openspec")),
|
|
160
|
+
detail: openspecRoot === root ? "openspec/ 目录(安装根)" : `openspec/ 目录(上级 ${openspecRoot})`,
|
|
151
161
|
});
|
|
152
162
|
checks.push({
|
|
153
163
|
name: "openspec-cli",
|
|
154
164
|
ok: hasCommand("openspec"),
|
|
155
165
|
detail: "openspec CLI(npx openspec 或全局安装)",
|
|
156
166
|
});
|
|
157
|
-
const skillPath = path_1.default.join(
|
|
167
|
+
const skillPath = path_1.default.join(root, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md");
|
|
158
168
|
checks.push({
|
|
159
169
|
name: "opsx-dev-delivery-pipeline-skill",
|
|
160
170
|
ok: await pathExists(skillPath),
|
|
161
171
|
detail: skillPath,
|
|
162
172
|
});
|
|
163
|
-
const tddPath = path_1.default.join(
|
|
173
|
+
const tddPath = path_1.default.join(root, ".cursor", "skills", "tdd-script", "SKILL.md");
|
|
164
174
|
checks.push({
|
|
165
175
|
name: "tdd-script-skill",
|
|
166
176
|
ok: await pathExists(tddPath),
|
|
167
177
|
detail: tddPath,
|
|
168
178
|
});
|
|
169
|
-
const applyPath = path_1.default.join(
|
|
179
|
+
const applyPath = path_1.default.join(root, ".cursor", "skills", "openspec-apply-change", "SKILL.md");
|
|
170
180
|
checks.push({
|
|
171
181
|
name: "openspec-apply-change-skill",
|
|
172
182
|
ok: await pathExists(applyPath),
|
|
173
183
|
detail: applyPath,
|
|
174
184
|
});
|
|
175
|
-
const pipelineScript = path_1.default.join(
|
|
185
|
+
const pipelineScript = path_1.default.join(root, DELIVERY_SCRIPT_REL);
|
|
176
186
|
checks.push({
|
|
177
187
|
name: "delivery-pipeline-script",
|
|
178
188
|
ok: await pathExists(pipelineScript),
|
|
179
189
|
detail: pipelineScript,
|
|
180
190
|
});
|
|
181
191
|
try {
|
|
182
|
-
const pkgText = await promises_1.default.readFile(path_1.default.join(
|
|
192
|
+
const pkgText = await promises_1.default.readFile(path_1.default.join(root, "package.json"), "utf8");
|
|
183
193
|
const pkg = JSON.parse(pkgText);
|
|
184
194
|
const cmd = (_b = (_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a["opsx:delivery-pipeline"]) !== null && _b !== void 0 ? _b : "";
|
|
185
195
|
const ok = cmd.includes("opsx-dev-delivery-pipeline") &&
|
|
@@ -199,10 +209,10 @@ async function collectDeliverySetupChecks(projectRoot) {
|
|
|
199
209
|
});
|
|
200
210
|
}
|
|
201
211
|
try {
|
|
202
|
-
const cfgText = await promises_1.default.readFile(path_1.default.join(
|
|
212
|
+
const cfgText = await promises_1.default.readFile(path_1.default.join(root, ".opsx", "config.json"), "utf8");
|
|
203
213
|
const cfg = JSON.parse(cfgText);
|
|
204
214
|
const skillsDir = (_c = cfg.skillsDir) !== null && _c !== void 0 ? _c : "";
|
|
205
|
-
const hasCursorDelivery = await pathExists(path_1.default.join(
|
|
215
|
+
const hasCursorDelivery = await pathExists(path_1.default.join(root, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md"));
|
|
206
216
|
const nameOk = ((_d = cfg.deliveryPipeline) === null || _d === void 0 ? void 0 : _d.skillName) === "opsx-dev-delivery-pipeline";
|
|
207
217
|
const dirOk = (hasCursorDelivery && skillsDir.includes(".cursor")) ||
|
|
208
218
|
(!hasCursorDelivery && (skillsDir.includes(".cursor") || skillsDir.includes(".claude")));
|
|
@@ -222,7 +232,7 @@ async function collectDeliverySetupChecks(projectRoot) {
|
|
|
222
232
|
});
|
|
223
233
|
}
|
|
224
234
|
try {
|
|
225
|
-
const pkgText = await promises_1.default.readFile(path_1.default.join(
|
|
235
|
+
const pkgText = await promises_1.default.readFile(path_1.default.join(root, "package.json"), "utf8");
|
|
226
236
|
const pkg = JSON.parse(pkgText);
|
|
227
237
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
228
238
|
const hasPw = Boolean(deps["@playwright/test"] || deps.playwright);
|
|
@@ -239,7 +249,7 @@ async function collectDeliverySetupChecks(projectRoot) {
|
|
|
239
249
|
detail: "无法读取 package.json 检查 Playwright",
|
|
240
250
|
});
|
|
241
251
|
}
|
|
242
|
-
const agentsMd = path_1.default.join(
|
|
252
|
+
const agentsMd = path_1.default.join(root, "AGENTS.md");
|
|
243
253
|
checks.push({
|
|
244
254
|
name: "agents-md",
|
|
245
255
|
ok: await pathExists(agentsMd),
|