sdd-flow-kit 1.3.6 → 1.3.11
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 +33 -0
- package/dist/cli.js +66 -0
- package/dist/core/agentRemediateInvoke.js +188 -0
- package/dist/core/playwrightEnsure.js +142 -0
- package/dist/core/prdCoverage.js +398 -0
- package/dist/core/prdParse.js +88 -0
- package/dist/core/prdRemediate.js +300 -0
- package/dist/core/prdRemediateState.js +135 -0
- package/dist/core/prdSemanticDiff.js +365 -0
- package/dist/core/sessionState.js +2 -0
- package/dist/core/writeNext.js +5 -0
- package/dist/steps/runDeliver.js +13 -0
- package/dist/steps/runGate.js +34 -1
- package/dist/steps/runPhaseAdvance.js +4 -0
- package/dist/steps/runPrdDiff.js +71 -0
- package/dist/steps/runPrdRemediate.js +49 -0
- package/dist/steps/runPrdReview.js +73 -0
- package/dist/steps/runValidate.js +78 -52
- package/dist/steps/startFlow.js +5 -0
- package/package.json +1 -1
- package/src/templates/artifacts/06-agent-skill.template.md +2 -2
- package/src/templates/artifacts/07-opsx-auto-chain.template.md +11 -1
- package/src/templates/artifacts/08-PRD/344/270/200/350/207/264/346/200/247/345/244/215/346/237/245/346/212/245/345/221/212.template.md +93 -0
- package/src/templates/prompts/08-prd-consistency-review-prompt.md +49 -0
- package/src/templates/rules/sdd-no-implement-until-apply.mdc.template +6 -2
package/README.md
CHANGED
|
@@ -80,6 +80,39 @@ node ./node_modules/sdd-flow-kit/dist/postinstall.js
|
|
|
80
80
|
|
|
81
81
|
#### 变更记录
|
|
82
82
|
|
|
83
|
+
**1.3.11** — PRD 自动修复改为「每项最多 3 次」
|
|
84
|
+
|
|
85
|
+
- 每轮只修 `prd-diff` 仍失败的剩余项;8 项已修好则下轮只盯剩下 2 项
|
|
86
|
+
- 单项重试时 prompt 附「上次错误代码 + 错误描述」摘要,避免重复同样错误
|
|
87
|
+
- 状态持久化:`.prd-remediate-state.json`;单项 3 次仍失败 → 写入 08 待人工确认
|
|
88
|
+
|
|
89
|
+
**1.3.10** — PRD 自动修复支持多 Agent
|
|
90
|
+
|
|
91
|
+
- `prd-remediate` 按项目安装环境自动选择 Cursor / Claude Code / Codex / OpenClaw
|
|
92
|
+
- Claude Code 安装(`.claude/skills`)时默认用 `claude -p` 执行修复,不限于 Cursor
|
|
93
|
+
- 可用 `--agent claude-code` 或 `SDD_FLOW_KIT_ENABLE_*_AUTORUN=0` 控制
|
|
94
|
+
|
|
95
|
+
**1.3.9** — PRD 不一致自动修复循环(最多 3 次)
|
|
96
|
+
|
|
97
|
+
- 新增 `prd-remediate`:`prd-diff` 失败 → 按 `09` 生成修复指引 → 调 AI 工具 `/opsx-apply` 修代码 → 再 `prd-diff`(**每项**最多 3 次)
|
|
98
|
+
- 3 次仍失败:自动写入 `08`「待人工确认汇总」,状态 `待人工确认(自动修复3次未果)`
|
|
99
|
+
- `validate` 内置上述循环;escalated 后 `needUserConfirm=true`
|
|
100
|
+
|
|
101
|
+
**1.3.8** — PRD 脚本语义 diff + 全量测试覆盖门禁
|
|
102
|
+
|
|
103
|
+
- 新增 `prd-diff`:机械提取 PRD 字面量,在 `src/`/`e2e/` 中搜索,生成 `09-PRD语义diff报告.md`
|
|
104
|
+
- `gate prd-coverage` 增强:校验 08 证据路径真实存在、08 结论与脚本 diff 一致、可验证 PRD 须有自动化测试 + E2E
|
|
105
|
+
- 解决「08 看起来合规但实际对比不准」:08 标「一致」但代码无 PRD 字面量 → gate 失败
|
|
106
|
+
- 解决「E2E 只覆盖主路径」:所有 `ui-verifiable` / `behavior-verifiable` PRD 原子须在测试中引用 PRD-ID 或字面量,且须有 E2E
|
|
107
|
+
- 所有声明 `e2e` 类型的 AC(含 P1/P2 边缘场景)均须有 e2e/ 下测试
|
|
108
|
+
|
|
109
|
+
**1.3.7** — 强制验收与 PRD 一致性复查
|
|
110
|
+
|
|
111
|
+
- `validate` 强制 E2E:缺 Playwright 时自动 `pnpm add -D @playwright/test` + `playwright install chromium`
|
|
112
|
+
- 新增 `gate prd-coverage`:最细粒度 PRD 原子全覆盖 + 08 报告合规 + P0 AC 测试映射
|
|
113
|
+
- 新增 `prd-review` 命令与 `08-PRD一致性复查报告.md` / 提示词(禁止揣摩、禁止兜底,不一致按 PRD 修复)
|
|
114
|
+
- `deliver` 前必须通过 `prd-coverage` 门禁
|
|
115
|
+
|
|
83
116
|
**1.3.3** — Confluence PRD 拉取修复(ADI 长标题页)
|
|
84
117
|
|
|
85
118
|
- `confluence-doc.py`:匹配时统一 `-`/`_`;ADI 增加 `ADI_V*结算` 关键词
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,9 @@ const runPhaseAdvance_1 = require("./steps/runPhaseAdvance");
|
|
|
17
17
|
const runPropose_1 = require("./steps/runPropose");
|
|
18
18
|
const runValidate_1 = require("./steps/runValidate");
|
|
19
19
|
const runDeliver_1 = require("./steps/runDeliver");
|
|
20
|
+
const runPrdReview_1 = require("./steps/runPrdReview");
|
|
21
|
+
const runPrdDiff_1 = require("./steps/runPrdDiff");
|
|
22
|
+
const runPrdRemediate_1 = require("./steps/runPrdRemediate");
|
|
20
23
|
const bootstrap_1 = require("./core/bootstrap");
|
|
21
24
|
const projectRoots_1 = require("./core/projectRoots");
|
|
22
25
|
const ALLOWED_AGENTS = ["cursor", "claude-code", "codex", "openclaw", "custom"];
|
|
@@ -60,6 +63,14 @@ function printHelp() {
|
|
|
60
63
|
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect docs-closed",
|
|
61
64
|
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect ac-ready --change <name>",
|
|
62
65
|
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect ac-signed",
|
|
66
|
+
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect prd-coverage --change <name>",
|
|
67
|
+
"",
|
|
68
|
+
"10) PRD 一致性复查(最细粒度,交付前强制):",
|
|
69
|
+
" npx sdd-flow-kit prd-review --project-root . --run-id <runId>",
|
|
70
|
+
" # 在 AI 工具执行 openspec/PRD/<runId>/08-PRD一致性复查提示词.md",
|
|
71
|
+
" npx sdd-flow-kit prd-diff --project-root . --run-id <runId>",
|
|
72
|
+
" npx sdd-flow-kit prd-remediate --project-root . --run-id <runId> --change <name>",
|
|
73
|
+
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect prd-coverage",
|
|
63
74
|
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect impl-allowed --change <name>",
|
|
64
75
|
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect validate-pass --change <name>",
|
|
65
76
|
"",
|
|
@@ -102,6 +113,7 @@ async function main() {
|
|
|
102
113
|
"docs-closed",
|
|
103
114
|
"ac-ready",
|
|
104
115
|
"ac-signed",
|
|
116
|
+
"prd-coverage",
|
|
105
117
|
"propose-ready",
|
|
106
118
|
"impl-allowed",
|
|
107
119
|
"validate-pass",
|
|
@@ -126,6 +138,9 @@ async function main() {
|
|
|
126
138
|
cmd !== "phase" &&
|
|
127
139
|
cmd !== "propose" &&
|
|
128
140
|
cmd !== "validate" &&
|
|
141
|
+
cmd !== "prd-review" &&
|
|
142
|
+
cmd !== "prd-diff" &&
|
|
143
|
+
cmd !== "prd-remediate" &&
|
|
129
144
|
cmd !== "deliver") {
|
|
130
145
|
printHelp();
|
|
131
146
|
process_1.default.exitCode = 1;
|
|
@@ -169,6 +184,9 @@ async function main() {
|
|
|
169
184
|
"phase",
|
|
170
185
|
"propose",
|
|
171
186
|
"validate",
|
|
187
|
+
"prd-review",
|
|
188
|
+
"prd-diff",
|
|
189
|
+
"prd-remediate",
|
|
172
190
|
"deliver",
|
|
173
191
|
]);
|
|
174
192
|
if (!skipBootstrap && !dryRun && bootstrapCommands.has(cmd)) {
|
|
@@ -268,6 +286,54 @@ async function main() {
|
|
|
268
286
|
process_1.default.exitCode = 1;
|
|
269
287
|
return;
|
|
270
288
|
}
|
|
289
|
+
if (cmd === "prd-review") {
|
|
290
|
+
const result = await (0, runPrdReview_1.runPrdReview)({
|
|
291
|
+
projectRoot,
|
|
292
|
+
runId,
|
|
293
|
+
dryRun,
|
|
294
|
+
});
|
|
295
|
+
// eslint-disable-next-line no-console
|
|
296
|
+
console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
|
|
297
|
+
if (!result.ok)
|
|
298
|
+
process_1.default.exitCode = 1;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (cmd === "prd-diff") {
|
|
302
|
+
const result = await (0, runPrdDiff_1.runPrdDiff)({
|
|
303
|
+
projectRoot,
|
|
304
|
+
runId,
|
|
305
|
+
dryRun,
|
|
306
|
+
});
|
|
307
|
+
// eslint-disable-next-line no-console
|
|
308
|
+
console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
|
|
309
|
+
if (!result.ok)
|
|
310
|
+
process_1.default.exitCode = 1;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (cmd === "prd-remediate") {
|
|
314
|
+
if (!changeName) {
|
|
315
|
+
// eslint-disable-next-line no-console
|
|
316
|
+
console.error("prd-remediate 需要 --change <kebab-name>");
|
|
317
|
+
process_1.default.exitCode = 1;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const result = await (0, runPrdRemediate_1.runPrdRemediate)({
|
|
321
|
+
projectRoot,
|
|
322
|
+
runId,
|
|
323
|
+
change: changeName,
|
|
324
|
+
agent,
|
|
325
|
+
dryRun,
|
|
326
|
+
});
|
|
327
|
+
// eslint-disable-next-line no-console
|
|
328
|
+
console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
|
|
329
|
+
if (result.needUserConfirm) {
|
|
330
|
+
// eslint-disable-next-line no-console
|
|
331
|
+
console.warn("⚠️ 自动修复 3 次未果,已写入 08 待人工确认汇总,需人工裁定后继续。");
|
|
332
|
+
}
|
|
333
|
+
if (!result.ok)
|
|
334
|
+
process_1.default.exitCode = 1;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
271
337
|
if (cmd === "deliver") {
|
|
272
338
|
if (!changeName) {
|
|
273
339
|
// eslint-disable-next-line no-console
|
|
@@ -0,0 +1,188 @@
|
|
|
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.isRemediateAutorunEnabled = isRemediateAutorunEnabled;
|
|
7
|
+
exports.resolveRemediateAgent = resolveRemediateAgent;
|
|
8
|
+
exports.invokeAgentOpsxRemediate = invokeAgentOpsxRemediate;
|
|
9
|
+
exports.agentRemediateHint = agentRemediateHint;
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const fs_1 = require("./fs");
|
|
13
|
+
const opsxDelivery_1 = require("./opsxDelivery");
|
|
14
|
+
const opsxDelivery_2 = require("./opsxDelivery");
|
|
15
|
+
function hasCommand(cmd) {
|
|
16
|
+
return (0, child_process_1.spawnSync)("which", [cmd], { stdio: "ignore" }).status === 0;
|
|
17
|
+
}
|
|
18
|
+
const AGENT_LABEL = {
|
|
19
|
+
cursor: "Cursor",
|
|
20
|
+
"claude-code": "Claude Code",
|
|
21
|
+
codex: "Codex",
|
|
22
|
+
openclaw: "OpenClaw",
|
|
23
|
+
custom: "Custom Agent",
|
|
24
|
+
};
|
|
25
|
+
/** PRD 自动修复:默认允许已选 agent 自动执行(可用环境变量关闭) */
|
|
26
|
+
function isRemediateAutorunEnabled(agent) {
|
|
27
|
+
if (process.env.SDD_FLOW_KIT_ENABLE_AGENT_AUTORUN === "0")
|
|
28
|
+
return false;
|
|
29
|
+
switch (agent) {
|
|
30
|
+
case "cursor":
|
|
31
|
+
return process.env.SDD_FLOW_KIT_ENABLE_CURSOR_AUTORUN !== "0";
|
|
32
|
+
case "claude-code":
|
|
33
|
+
return process.env.SDD_FLOW_KIT_ENABLE_CLAUDE_AUTORUN !== "0";
|
|
34
|
+
case "codex":
|
|
35
|
+
return process.env.SDD_FLOW_KIT_ENABLE_CODEX_AUTORUN !== "0";
|
|
36
|
+
case "openclaw":
|
|
37
|
+
return process.env.SDD_FLOW_KIT_ENABLE_OPENCLAW_AUTORUN !== "0";
|
|
38
|
+
default:
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** 根据项目已安装的 skills 目录解析修复用 agent */
|
|
43
|
+
async function resolveRemediateAgent(projectRoot, preferred) {
|
|
44
|
+
const pref = preferred && preferred !== "custom" ? preferred : undefined;
|
|
45
|
+
const resolved = await (0, opsxDelivery_1.resolveOpsxAgent)(projectRoot, pref !== null && pref !== void 0 ? pref : "claude-code");
|
|
46
|
+
if (resolved !== "custom" && hasAgentCli(resolved))
|
|
47
|
+
return resolved;
|
|
48
|
+
const candidates = ["claude-code", "cursor", "codex", "openclaw"];
|
|
49
|
+
for (const name of candidates) {
|
|
50
|
+
if (await hasAgentSkillDir(projectRoot, name) && hasAgentCli(name))
|
|
51
|
+
return name;
|
|
52
|
+
}
|
|
53
|
+
for (const name of candidates) {
|
|
54
|
+
if (hasAgentCli(name))
|
|
55
|
+
return name;
|
|
56
|
+
}
|
|
57
|
+
return pref !== null && pref !== void 0 ? pref : "claude-code";
|
|
58
|
+
}
|
|
59
|
+
async function hasAgentSkillDir(projectRoot, agent) {
|
|
60
|
+
const map = {
|
|
61
|
+
cursor: ".cursor/skills/sdd-flow-kit/SKILL.md",
|
|
62
|
+
"claude-code": ".claude/skills/sdd-flow-kit/SKILL.md",
|
|
63
|
+
codex: ".codex/skills/sdd-flow-kit/SKILL.md",
|
|
64
|
+
openclaw: ".openclaw/skills/sdd-flow-kit/SKILL.md",
|
|
65
|
+
};
|
|
66
|
+
const rel = map[agent];
|
|
67
|
+
if (!rel)
|
|
68
|
+
return false;
|
|
69
|
+
return (0, opsxDelivery_2.pathExists)(path_1.default.join(projectRoot, rel));
|
|
70
|
+
}
|
|
71
|
+
function hasAgentCli(agent) {
|
|
72
|
+
switch (agent) {
|
|
73
|
+
case "cursor":
|
|
74
|
+
return hasCommand("cursor");
|
|
75
|
+
case "claude-code":
|
|
76
|
+
return hasCommand("claude");
|
|
77
|
+
case "codex":
|
|
78
|
+
return hasCommand("codex");
|
|
79
|
+
case "openclaw":
|
|
80
|
+
return hasCommand("openclaw");
|
|
81
|
+
default:
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function runAgentCommand(agent, projectRoot, outputRoot, prompt) {
|
|
86
|
+
switch (agent) {
|
|
87
|
+
case "cursor":
|
|
88
|
+
return (0, child_process_1.spawnSync)("cursor", ["agent", "--print", "--trust", "--workspace", projectRoot, prompt], { cwd: outputRoot, encoding: "utf8", env: process.env });
|
|
89
|
+
case "claude-code":
|
|
90
|
+
return (0, child_process_1.spawnSync)("claude", ["-p", prompt], {
|
|
91
|
+
cwd: projectRoot,
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
env: process.env,
|
|
94
|
+
});
|
|
95
|
+
case "codex": {
|
|
96
|
+
const withExec = (0, child_process_1.spawnSync)("codex", ["exec", prompt], {
|
|
97
|
+
cwd: projectRoot,
|
|
98
|
+
encoding: "utf8",
|
|
99
|
+
env: process.env,
|
|
100
|
+
});
|
|
101
|
+
if (withExec.status === 0 || withExec.stdout || withExec.stderr)
|
|
102
|
+
return withExec;
|
|
103
|
+
return (0, child_process_1.spawnSync)("codex", ["-p", prompt], {
|
|
104
|
+
cwd: projectRoot,
|
|
105
|
+
encoding: "utf8",
|
|
106
|
+
env: process.env,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
case "openclaw": {
|
|
110
|
+
const withRun = (0, child_process_1.spawnSync)("openclaw", ["run", prompt], {
|
|
111
|
+
cwd: projectRoot,
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
env: process.env,
|
|
114
|
+
});
|
|
115
|
+
if (withRun.status === 0 || withRun.stdout || withRun.stderr)
|
|
116
|
+
return withRun;
|
|
117
|
+
return (0, child_process_1.spawnSync)("openclaw", ["-p", prompt], {
|
|
118
|
+
cwd: projectRoot,
|
|
119
|
+
encoding: "utf8",
|
|
120
|
+
env: process.env,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
return { status: 1, stdout: "", stderr: "unsupported agent" };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function writeRemediateLog(outputRoot, attempt, agent, label, result) {
|
|
128
|
+
var _a;
|
|
129
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, `PRD-REMEDIATE-LOG-第${attempt}次.md`), [
|
|
130
|
+
`# PRD 自动修复日志(第 ${attempt} 次)`,
|
|
131
|
+
"",
|
|
132
|
+
`agent: ${label} (${agent})`,
|
|
133
|
+
`exit: ${(_a = result.status) !== null && _a !== void 0 ? _a : "unknown"}`,
|
|
134
|
+
"",
|
|
135
|
+
"## stdout",
|
|
136
|
+
result.stdout || "",
|
|
137
|
+
"",
|
|
138
|
+
"## stderr",
|
|
139
|
+
result.stderr || "",
|
|
140
|
+
].join("\n"));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 调用已安装 AI 工具执行 /opsx-apply 修复(Cursor / Claude Code / Codex / OpenClaw)。
|
|
144
|
+
*/
|
|
145
|
+
async function invokeAgentOpsxRemediate(args) {
|
|
146
|
+
var _a, _b;
|
|
147
|
+
const agent = args.agent;
|
|
148
|
+
const label = (_a = AGENT_LABEL[agent]) !== null && _a !== void 0 ? _a : agent;
|
|
149
|
+
if (!isRemediateAutorunEnabled(agent)) {
|
|
150
|
+
await writeRemediateLog(args.outputRoot, args.attempt, agent, label, {
|
|
151
|
+
status: null,
|
|
152
|
+
stdout: "",
|
|
153
|
+
stderr: `${label} autorun 已关闭(环境变量)`,
|
|
154
|
+
});
|
|
155
|
+
return { ok: false, agent, detail: `${label} autorun 已关闭(环境变量)` };
|
|
156
|
+
}
|
|
157
|
+
if (!hasAgentCli(agent)) {
|
|
158
|
+
await writeRemediateLog(args.outputRoot, args.attempt, agent, label, {
|
|
159
|
+
status: null,
|
|
160
|
+
stdout: "",
|
|
161
|
+
stderr: `未检测到 ${label} CLI`,
|
|
162
|
+
});
|
|
163
|
+
return { ok: false, agent, detail: `未检测到 ${label} CLI,请手动执行 10-PRD自动修复指引` };
|
|
164
|
+
}
|
|
165
|
+
const result = runAgentCommand(agent, args.projectRoot, args.outputRoot, args.prompt);
|
|
166
|
+
await writeRemediateLog(args.outputRoot, args.attempt, agent, label, result);
|
|
167
|
+
if (result.status !== 0) {
|
|
168
|
+
return { ok: false, agent, detail: `${label} 执行退出码 ${(_b = result.status) !== null && _b !== void 0 ? _b : 1}` };
|
|
169
|
+
}
|
|
170
|
+
const complete = /PRD_REMEDIATE=COMPLETE/i.test(result.stdout || "");
|
|
171
|
+
return {
|
|
172
|
+
ok: complete,
|
|
173
|
+
agent,
|
|
174
|
+
detail: complete ? `${label} 修复完成` : `${label} 已执行但未输出 PRD_REMEDIATE=COMPLETE`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function agentRemediateHint(agent) {
|
|
178
|
+
switch (agent) {
|
|
179
|
+
case "claude-code":
|
|
180
|
+
return "在 Claude Code 中执行 **/opsx-apply**(或阅读本文件后让 claude -p 执行)";
|
|
181
|
+
case "codex":
|
|
182
|
+
return "在 Codex 中执行修复指引(或 `codex exec` 读本文件)";
|
|
183
|
+
case "openclaw":
|
|
184
|
+
return "在 OpenClaw 中执行修复指引";
|
|
185
|
+
default:
|
|
186
|
+
return "在 Cursor 中执行 **/opsx-apply**";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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.hasPlaywrightDependency = hasPlaywrightDependency;
|
|
7
|
+
exports.ensurePlaywright = ensurePlaywright;
|
|
8
|
+
exports.runMandatoryE2e = runMandatoryE2e;
|
|
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 packageJsonPatch_1 = require("./packageJsonPatch");
|
|
13
|
+
async function detectPackageManager(pkgRoot) {
|
|
14
|
+
try {
|
|
15
|
+
await promises_1.default.access(path_1.default.join(pkgRoot, "pnpm-lock.yaml"));
|
|
16
|
+
return "pnpm";
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* continue */
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
await promises_1.default.access(path_1.default.join(pkgRoot, "yarn.lock"));
|
|
23
|
+
return "yarn";
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* continue */
|
|
27
|
+
}
|
|
28
|
+
return "npm";
|
|
29
|
+
}
|
|
30
|
+
async function readPkg(pkgRoot) {
|
|
31
|
+
try {
|
|
32
|
+
const text = await promises_1.default.readFile(path_1.default.join(pkgRoot, "package.json"), "utf8");
|
|
33
|
+
return JSON.parse(text);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function hasPlaywrightDependency(pkgRoot) {
|
|
40
|
+
const pkg = await readPkg(pkgRoot);
|
|
41
|
+
if (!pkg)
|
|
42
|
+
return false;
|
|
43
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
44
|
+
return Boolean(deps["@playwright/test"] || deps.playwright);
|
|
45
|
+
}
|
|
46
|
+
function runCmd(cmd, args, cwd) {
|
|
47
|
+
var _a;
|
|
48
|
+
const ret = (0, child_process_1.spawnSync)(cmd, args, {
|
|
49
|
+
cwd,
|
|
50
|
+
stdio: "inherit",
|
|
51
|
+
shell: process.platform === "win32",
|
|
52
|
+
env: process.env,
|
|
53
|
+
});
|
|
54
|
+
if (ret.status !== 0) {
|
|
55
|
+
return { ok: false, detail: `${cmd} ${args.join(" ")} 退出码 ${(_a = ret.status) !== null && _a !== void 0 ? _a : 1}` };
|
|
56
|
+
}
|
|
57
|
+
return { ok: true, detail: "ok" };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 强制确保 Playwright 可用:缺依赖则安装,缺浏览器则 install chromium,缺 test:e2e 则写入脚本。
|
|
61
|
+
*/
|
|
62
|
+
async function ensurePlaywright(pkgRoot, dryRun = false) {
|
|
63
|
+
var _a;
|
|
64
|
+
const pkg = await readPkg(pkgRoot);
|
|
65
|
+
if (!pkg) {
|
|
66
|
+
return { ok: false, detail: "无法读取 package.json", installed: false };
|
|
67
|
+
}
|
|
68
|
+
const pm = await detectPackageManager(pkgRoot);
|
|
69
|
+
let installed = false;
|
|
70
|
+
if (!(await hasPlaywrightDependency(pkgRoot))) {
|
|
71
|
+
if (dryRun) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
detail: `dry-run: 将安装 @playwright/test(${pm})`,
|
|
75
|
+
installed: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const addArgs = pm === "pnpm"
|
|
79
|
+
? ["add", "-D", "@playwright/test"]
|
|
80
|
+
: pm === "yarn"
|
|
81
|
+
? ["add", "-D", "@playwright/test"]
|
|
82
|
+
: ["install", "-D", "@playwright/test"];
|
|
83
|
+
const add = runCmd(pm, addArgs, pkgRoot);
|
|
84
|
+
if (!add.ok) {
|
|
85
|
+
return { ok: false, detail: `安装 @playwright/test 失败: ${add.detail}`, installed: false };
|
|
86
|
+
}
|
|
87
|
+
installed = true;
|
|
88
|
+
}
|
|
89
|
+
if (!((_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a["test:e2e"])) {
|
|
90
|
+
if (!dryRun) {
|
|
91
|
+
await (0, packageJsonPatch_1.patchPackageJsonScripts)(path_1.default.join(pkgRoot, "package.json"), { "test:e2e": "playwright test" }, { minDependencyCount: 1, minBytes: 40, minSizeRatio: 0.3 });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (dryRun) {
|
|
95
|
+
return { ok: true, detail: "dry-run: Playwright 已就绪", installed };
|
|
96
|
+
}
|
|
97
|
+
const browsers = runCmd("npx", ["playwright", "install", "chromium"], pkgRoot);
|
|
98
|
+
if (!browsers.ok) {
|
|
99
|
+
return { ok: false, detail: `playwright install chromium 失败: ${browsers.detail}`, installed };
|
|
100
|
+
}
|
|
101
|
+
return { ok: true, detail: installed ? "已安装 @playwright/test 与 chromium" : "Playwright 已就绪", installed };
|
|
102
|
+
}
|
|
103
|
+
/** 强制执行 E2E(先 ensurePlaywright,再跑 test:e2e 或 playwright test) */
|
|
104
|
+
async function runMandatoryE2e(pkgRoot, dryRun = false) {
|
|
105
|
+
var _a, _b, _c;
|
|
106
|
+
const ensured = await ensurePlaywright(pkgRoot, dryRun);
|
|
107
|
+
if (!ensured.ok) {
|
|
108
|
+
return { ok: false, exitCode: 1, detail: ensured.detail };
|
|
109
|
+
}
|
|
110
|
+
if (dryRun) {
|
|
111
|
+
return { ok: true, exitCode: 0, detail: "dry-run: would run playwright test" };
|
|
112
|
+
}
|
|
113
|
+
const pkg = await readPkg(pkgRoot);
|
|
114
|
+
if ((_a = pkg === null || pkg === void 0 ? void 0 : pkg.scripts) === null || _a === void 0 ? void 0 : _a["test:e2e"]) {
|
|
115
|
+
const pm = await detectPackageManager(pkgRoot);
|
|
116
|
+
const runScript = pm === "yarn" ? ["run", "test:e2e"] : ["run", "test:e2e"];
|
|
117
|
+
const ret = (0, child_process_1.spawnSync)(pm, runScript, {
|
|
118
|
+
cwd: pkgRoot,
|
|
119
|
+
stdio: "inherit",
|
|
120
|
+
shell: process.platform === "win32",
|
|
121
|
+
env: { ...process.env, CI: "1" },
|
|
122
|
+
});
|
|
123
|
+
const code = (_b = ret.status) !== null && _b !== void 0 ? _b : 1;
|
|
124
|
+
return {
|
|
125
|
+
ok: code === 0,
|
|
126
|
+
exitCode: code,
|
|
127
|
+
detail: code === 0 ? "test:e2e 全通过" : `test:e2e 退出码 ${code}`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const ret = (0, child_process_1.spawnSync)("npx", ["playwright", "test"], {
|
|
131
|
+
cwd: pkgRoot,
|
|
132
|
+
stdio: "inherit",
|
|
133
|
+
shell: process.platform === "win32",
|
|
134
|
+
env: { ...process.env, CI: "1" },
|
|
135
|
+
});
|
|
136
|
+
const code = (_c = ret.status) !== null && _c !== void 0 ? _c : 1;
|
|
137
|
+
return {
|
|
138
|
+
ok: code === 0,
|
|
139
|
+
exitCode: code,
|
|
140
|
+
detail: code === 0 ? "playwright test 全通过" : `playwright test 退出码 ${code}`,
|
|
141
|
+
};
|
|
142
|
+
}
|