sdd-flow-kit 1.3.21 → 1.3.23

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/dist/cli.js CHANGED
@@ -27,15 +27,42 @@ const runGuard_1 = require("./steps/runGuard");
27
27
  const bootstrap_1 = require("./core/bootstrap");
28
28
  const projectRoots_1 = require("./core/projectRoots");
29
29
  const versionIntent_1 = require("./core/versionIntent");
30
+ const inferProject_1 = require("./core/inferProject");
30
31
  const ALLOWED_AGENTS = ["cursor", "claude-code", "codex", "openclaw", "custom"];
31
- function getArgValue(flag) {
32
+ /** 读取 flag 后的参数;allowMultiWord 时合并至下一个以 - 开头的 token(支持未加引号的 AD Tools)。 */
33
+ function getArgValue(flag, options) {
32
34
  const idx = process_1.default.argv.indexOf(flag);
33
35
  if (idx === -1)
34
36
  return null;
35
- const v = process_1.default.argv[idx + 1];
36
- if (!v)
37
+ const first = process_1.default.argv[idx + 1];
38
+ if (!first || first.startsWith("-"))
37
39
  return null;
38
- return v;
40
+ if (!(options === null || options === void 0 ? void 0 : options.allowMultiWord))
41
+ return first;
42
+ const parts = [first];
43
+ for (let i = idx + 2; i < process_1.default.argv.length; i += 1) {
44
+ const token = process_1.default.argv[i];
45
+ if (token.startsWith("-"))
46
+ break;
47
+ parts.push(token);
48
+ }
49
+ return parts.join(" ");
50
+ }
51
+ /** 统计 flag 在 argv 中占用的 token 数(含多词值),供 getFreeTextArgs 跳过。 */
52
+ function countFlagArgTokens(flag) {
53
+ const idx = process_1.default.argv.indexOf(flag);
54
+ if (idx === -1)
55
+ return 0;
56
+ if (flag === "--project") {
57
+ const raw = getArgValue("--project", { allowMultiWord: true });
58
+ if (!raw)
59
+ return 1;
60
+ return 1 + raw.split(" ").length;
61
+ }
62
+ const next = process_1.default.argv[idx + 1];
63
+ if (!next || next.startsWith("-"))
64
+ return 1;
65
+ return 2;
39
66
  }
40
67
  function getFreeTextArgs(startIndex) {
41
68
  const flagsWithValue = new Set([
@@ -64,7 +91,8 @@ function getFreeTextArgs(startIndex) {
64
91
  continue;
65
92
  }
66
93
  if (flagsWithValue.has(token)) {
67
- i += 1;
94
+ const consumed = countFlagArgTokens(token);
95
+ i += Math.max(consumed - 1, 0);
68
96
  continue;
69
97
  }
70
98
  if (token.startsWith("--"))
@@ -96,6 +124,7 @@ function printHelp() {
96
124
  "",
97
125
  "5) 环境健检:",
98
126
  " npx sdd-flow-kit doctor --agent cursor",
127
+ " npx sdd-flow-kit doctor --fix -y # 自动补齐 OPSX / Playwright E2E",
99
128
  "",
100
129
  "6) Apply 前确保 OPSX+TDD+Playwright(/opsx-apply 前必须):",
101
130
  " npx sdd-flow-kit ensure-opsx --agent cursor -y",
@@ -142,7 +171,7 @@ function printHelp() {
142
171
  " 环境变量 SDD_FLOW_KIT_LEGACY_MONOREPO=1 恢复「仓库根无 package.json 时落到 frontend/」",
143
172
  " --agent <name> cursor | claude-code | codex | openclaw | custom(默认 claude-code)",
144
173
  " --branch <name> 开发分支;不存在会自动创建",
145
- " --project <name> OMS | ADI | 欢盟 | AD Tools | BreakX(默认 OMS,用于生成 docs/*-doc-skill)",
174
+ " --project <name> OMS | ADI | 欢盟 | AD Tools | BreakX(默认 OMS;支持 \"AD Tools\" / AD Tools / adtools)",
146
175
  " -y 非交互模式(使用默认值)",
147
176
  " --dry-run 预演模式:仅检测并输出计划,不执行写入/安装/切分支",
148
177
  " --plan-json 输出机器可读 JSON 计划结果",
@@ -152,7 +181,7 @@ function printHelp() {
152
181
  ].join("\n"));
153
182
  }
154
183
  async function main() {
155
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
184
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
156
185
  const cmd = process_1.default.argv[2];
157
186
  if (!cmd || cmd === "--help" || cmd === "-h") {
158
187
  printHelp();
@@ -216,7 +245,9 @@ async function main() {
216
245
  ? ((_h = getArgValue("--text")) !== null && _h !== void 0 ? _h : getFreeTextArgs(3)).trim()
217
246
  : "";
218
247
  const guardIntent = rawGuardText ? (0, versionIntent_1.parseVersionIntent)(rawGuardText) : null;
219
- const projectKind = ((_k = (_j = getArgValue("--project")) !== null && _j !== void 0 ? _j : guardIntent === null || guardIntent === void 0 ? void 0 : guardIntent.product) !== null && _k !== void 0 ? _k : "OMS");
248
+ const rawProjectArg = getArgValue("--project", { allowMultiWord: true });
249
+ const resolvedProject = rawProjectArg ? (0, inferProject_1.resolveProjectKindInput)(rawProjectArg) : null;
250
+ const projectKind = (_j = resolvedProject !== null && resolvedProject !== void 0 ? resolvedProject : ((guardIntent === null || guardIntent === void 0 ? void 0 : guardIntent.product) ? (0, inferProject_1.resolveProjectKindInput)(guardIntent.product) : null)) !== null && _j !== void 0 ? _j : "OMS";
220
251
  if (!ALLOWED_AGENTS.includes(agentValue)) {
221
252
  // eslint-disable-next-line no-console
222
253
  console.error(`不支持的 --agent: ${agentValue}`);
@@ -225,10 +256,9 @@ async function main() {
225
256
  return;
226
257
  }
227
258
  const agent = agentValue;
228
- const allowedProjects = ["OMS", "ADI", "欢盟", "AD Tools", "BreakX"];
229
- if (!allowedProjects.includes(projectKind)) {
259
+ if (rawProjectArg && !resolvedProject) {
230
260
  // eslint-disable-next-line no-console
231
- console.error(`不支持的 --project: ${projectKind}(可选:${allowedProjects.join(" / ")})`);
261
+ console.error(`不支持的 --project: ${rawProjectArg}(可选:${inferProject_1.ALLOWED_PROJECTS.join(" / ")};别名示例:adtools)`);
232
262
  process_1.default.exitCode = 1;
233
263
  return;
234
264
  }
@@ -467,7 +497,7 @@ async function main() {
467
497
  process_1.default.exitCode = 1;
468
498
  return;
469
499
  }
470
- const from = ((_l = getArgValue("--from")) !== null && _l !== void 0 ? _l : "docs-done");
500
+ const from = ((_k = getArgValue("--from")) !== null && _k !== void 0 ? _k : "docs-done");
471
501
  if (!CHAIN_FROM.includes(from)) {
472
502
  // eslint-disable-next-line no-console
473
503
  console.error(`chain 需要 --from ${CHAIN_FROM.join("|")}`);
@@ -496,7 +526,7 @@ async function main() {
496
526
  process_1.default.exitCode = 1;
497
527
  return;
498
528
  }
499
- const modeArg = (_m = getArgValue("--execution-mode")) !== null && _m !== void 0 ? _m : getArgValue("--executionMode");
529
+ const modeArg = (_l = getArgValue("--execution-mode")) !== null && _l !== void 0 ? _l : getArgValue("--executionMode");
500
530
  const textArg = getArgValue("--text");
501
531
  const strictCommands = process_1.default.argv.includes("--strict-commands");
502
532
  const result = await (0, runSessionMode_1.runSessionMode)({
@@ -8,6 +8,7 @@ const path_1 = __importDefault(require("path"));
8
8
  const inferProject_1 = require("./inferProject");
9
9
  const packageJsonPatch_1 = require("./packageJsonPatch");
10
10
  const opsxDelivery_1 = require("./opsxDelivery");
11
+ const playwrightEnsure_1 = require("./playwrightEnsure");
11
12
  const projectRoots_1 = require("./projectRoots");
12
13
  const setupProject_1 = require("../steps/setupProject");
13
14
  /**
@@ -16,41 +17,66 @@ const setupProject_1 = require("../steps/setupProject");
16
17
  async function bootstrapProjectIfNeeded(options) {
17
18
  var _a;
18
19
  const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
20
+ const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
19
21
  const opsxReady = await (0, opsxDelivery_1.isOpsxApplyReady)(installRoot);
20
22
  const sddReady = await (0, projectRoots_1.isSddKitSkillInstalled)(installRoot);
21
- if (opsxReady && sddReady) {
23
+ const pwReady = await (0, playwrightEnsure_1.isPlaywrightEnvironmentReady)(pkgRoot);
24
+ if (opsxReady && sddReady && pwReady.ready) {
22
25
  return false;
23
26
  }
24
27
  if (options.dryRun) {
25
28
  return true;
26
29
  }
27
- const kind = (_a = options.projectKind) !== null && _a !== void 0 ? _a : (await (0, inferProject_1.inferProjectKind)(installRoot));
28
- const missing = [];
29
- if (!sddReady)
30
- missing.push("SDD skill");
31
- if (!opsxReady)
32
- missing.push("OPSX");
33
- // eslint-disable-next-line no-console
34
- console.log(`[sdd-flow-kit] 检测到 ${missing.join(" / ")} 未就绪,正在自动 bootstrap(等价 install -y)…`);
35
- const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
36
- const pnpmPatched = await (0, packageJsonPatch_1.ensurePnpmAllowSddFlowKitBuild)(path_1.default.join(pkgRoot, "package.json"));
37
- if (pnpmPatched) {
30
+ if (opsxReady && sddReady && !pwReady.ready) {
38
31
  // eslint-disable-next-line no-console
39
- console.log("[sdd-flow-kit] 已写入 pnpm.onlyBuiltDependencies: sdd-flow-kit;下次 pnpm install 将执行 postinstall");
32
+ console.log(`[sdd-flow-kit] 检测到 Playwright E2E 未就绪(${pwReady.missing.join("、")}),正在自动补齐…`);
33
+ const pw = await (0, playwrightEnsure_1.ensurePlaywrightEnvironment)(pkgRoot, false);
34
+ if (pw.actions.length > 0) {
35
+ for (const action of pw.actions) {
36
+ // eslint-disable-next-line no-console
37
+ console.log(`[sdd-flow-kit] - ${action}`);
38
+ }
39
+ }
40
40
  }
41
- await (0, setupProject_1.setupProject)({
42
- projectRoot: installRoot,
43
- agent: options.agent,
44
- yes: true,
45
- light: opsxReady,
46
- skipGit: true,
47
- projectKind: kind !== null && kind !== void 0 ? kind : "OMS",
48
- });
49
- if (!(await (0, opsxDelivery_1.isOpsxApplyReady)(installRoot))) {
50
- await (0, opsxDelivery_1.ensureOpsxWorkflow)({
41
+ else {
42
+ const kind = (_a = options.projectKind) !== null && _a !== void 0 ? _a : (await (0, inferProject_1.inferProjectKind)(installRoot));
43
+ const missing = [];
44
+ if (!sddReady)
45
+ missing.push("SDD skill");
46
+ if (!opsxReady)
47
+ missing.push("OPSX");
48
+ if (!pwReady.ready)
49
+ missing.push("Playwright E2E");
50
+ // eslint-disable-next-line no-console
51
+ console.log(`[sdd-flow-kit] 检测到 ${missing.join(" / ")} 未就绪,正在自动 bootstrap(等价 install -y)…`);
52
+ const pnpmPatched = await (0, packageJsonPatch_1.ensurePnpmAllowSddFlowKitBuild)(path_1.default.join(pkgRoot, "package.json"));
53
+ if (pnpmPatched) {
54
+ // eslint-disable-next-line no-console
55
+ console.log("[sdd-flow-kit] 已写入 pnpm.onlyBuiltDependencies: sdd-flow-kit;下次 pnpm install 将执行 postinstall");
56
+ }
57
+ await (0, setupProject_1.setupProject)({
51
58
  projectRoot: installRoot,
52
59
  agent: options.agent,
60
+ yes: true,
61
+ light: opsxReady,
62
+ skipGit: true,
63
+ projectKind: kind !== null && kind !== void 0 ? kind : "OMS",
53
64
  });
65
+ if (!(await (0, opsxDelivery_1.isOpsxApplyReady)(installRoot))) {
66
+ await (0, opsxDelivery_1.ensureOpsxWorkflow)({
67
+ projectRoot: installRoot,
68
+ agent: options.agent,
69
+ });
70
+ }
71
+ const pw = await (0, playwrightEnsure_1.ensurePlaywrightEnvironment)(pkgRoot, false);
72
+ if (pw.actions.length > 0) {
73
+ // eslint-disable-next-line no-console
74
+ console.log(`[sdd-flow-kit] Playwright: ${pw.detail}`);
75
+ for (const action of pw.actions) {
76
+ // eslint-disable-next-line no-console
77
+ console.log(`[sdd-flow-kit] - ${action}`);
78
+ }
79
+ }
54
80
  }
55
81
  const ready = await (0, opsxDelivery_1.isOpsxApplyReady)(installRoot);
56
82
  const skillReady = await (0, projectRoots_1.isSddKitSkillInstalled)(installRoot);
@@ -0,0 +1,64 @@
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.countE2eSpecFiles = countE2eSpecFiles;
7
+ exports.ensureE2eScaffold = ensureE2eScaffold;
8
+ const promises_1 = __importDefault(require("fs/promises"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const fs_1 = require("./fs");
11
+ const templates_1 = require("./templates");
12
+ const E2E_SPEC_RE = /\.spec\.(ts|js|mjs)$/i;
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
+ /** 统计 e2e/ 下可执行的 Playwright 用例文件数 */
23
+ async function countE2eSpecFiles(pkgRoot) {
24
+ const e2eDir = path_1.default.join(pkgRoot, "e2e");
25
+ if (!(await pathExists(e2eDir)))
26
+ return 0;
27
+ let count = 0;
28
+ async function walk(dir) {
29
+ const entries = await promises_1.default.readdir(dir, { withFileTypes: true });
30
+ for (const ent of entries) {
31
+ const full = path_1.default.join(dir, ent.name);
32
+ if (ent.isDirectory()) {
33
+ await walk(full);
34
+ }
35
+ else if (E2E_SPEC_RE.test(ent.name)) {
36
+ count += 1;
37
+ }
38
+ }
39
+ }
40
+ await walk(e2eDir);
41
+ return count;
42
+ }
43
+ /** 若项目尚无 e2e 下任何 .spec.ts,写入最小 smoke 用例,避免 Playwright 环境检测永远失败。 */
44
+ async function ensureE2eScaffold(pkgRoot, dryRun = false) {
45
+ const existing = await countE2eSpecFiles(pkgRoot);
46
+ if (existing > 0) {
47
+ return { ok: true, detail: `已有 ${existing} 个 e2e spec`, scaffolded: false };
48
+ }
49
+ const target = path_1.default.join(pkgRoot, "e2e", "smoke", "app-loads.spec.ts");
50
+ if (dryRun) {
51
+ return {
52
+ ok: false,
53
+ detail: `dry-run: 将写入 ${path_1.default.relative(pkgRoot, target)}`,
54
+ scaffolded: false,
55
+ };
56
+ }
57
+ const content = await (0, templates_1.loadTemplateText)("e2e-scaffold/smoke-app-loads.spec.ts.template");
58
+ await (0, fs_1.writeTextFileEnsuredDir)(target, content);
59
+ return {
60
+ ok: true,
61
+ detail: `已写入 smoke 用例: ${path_1.default.relative(pkgRoot, target)}`,
62
+ scaffolded: true,
63
+ };
64
+ }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PRODUCT_PREFIX_BY_KIND = exports.DOC_SKILL_BY_KIND = exports.ALLOWED_PROJECTS = void 0;
7
7
  exports.productToKind = productToKind;
8
+ exports.resolveProjectKindInput = resolveProjectKindInput;
8
9
  exports.resolveDocSkillDir = resolveDocSkillDir;
9
10
  exports.inferProjectKind = inferProjectKind;
10
11
  const promises_1 = __importDefault(require("fs/promises"));
@@ -38,6 +39,31 @@ function productToKind(product) {
38
39
  return "BreakX";
39
40
  return "OMS";
40
41
  }
42
+ /**
43
+ * 将 CLI `--project` 入参解析为规范项目类型。
44
+ * 支持精确名、别名(adtools / ad-tools)及带空格写法;无法识别时返回 null(避免 productToKind 默认回落 OMS)。
45
+ */
46
+ function resolveProjectKindInput(raw) {
47
+ const value = raw.trim();
48
+ if (!value)
49
+ return null;
50
+ if (exports.ALLOWED_PROJECTS.includes(value)) {
51
+ return value;
52
+ }
53
+ const upper = value.toUpperCase();
54
+ const compact = upper.replace(/[\s_-]+/g, "");
55
+ if (upper === "ADI" || upper.includes("ADINSIGHT"))
56
+ return "ADI";
57
+ if (upper === "OMS" || upper.includes("OPERATION"))
58
+ return "OMS";
59
+ if (value === "欢盟" || upper === "HUAN" || upper === "HUANMENG" || compact === "HM")
60
+ return "欢盟";
61
+ if (upper.includes("AD TOOLS") || compact === "ADTOOLS")
62
+ return "AD Tools";
63
+ if (compact === "BREAKX" || upper.includes("POQUAN") || value.includes("破圈"))
64
+ return "BreakX";
65
+ return null;
66
+ }
41
67
  /** 从 projectRoot 向上查找 docs/*-doc-skill(适配 frontend 子目录 + 根目录 docs) */
42
68
  async function resolveDocSkillDir(projectRoot, kind) {
43
69
  const dirName = exports.DOC_SKILL_BY_KIND[kind];
@@ -17,6 +17,7 @@ const promises_1 = __importDefault(require("fs/promises"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const packageJsonPatch_1 = require("./packageJsonPatch");
19
19
  const projectRoots_1 = require("./projectRoots");
20
+ const playwrightEnsure_1 = require("./playwrightEnsure");
20
21
  const opsxWorkflowPackage_1 = require("./opsxWorkflowPackage");
21
22
  function resolveOpsxWorkflowPackage() {
22
23
  return (0, opsxWorkflowPackage_1.resolveOpsxWorkflowPackageForCwd)(process.cwd());
@@ -249,19 +250,18 @@ async function collectDeliverySetupChecks(installRoot) {
249
250
  });
250
251
  }
251
252
  try {
252
- const pkgText = await promises_1.default.readFile(path_1.default.join(root, "package.json"), "utf8");
253
- const pkg = JSON.parse(pkgText);
254
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
255
- const hasPw = Boolean(deps["@playwright/test"] || deps.playwright);
253
+ const pwReady = await (0, playwrightEnsure_1.isPlaywrightEnvironmentReady)(root);
256
254
  checks.push({
257
- name: "playwright-dependency",
258
- ok: hasPw,
259
- detail: hasPw ? "@playwright/test 或 playwright 已声明" : "package.json 缺少 Playwright 依赖",
255
+ name: "playwright-e2e-environment",
256
+ ok: pwReady.ready,
257
+ detail: pwReady.ready
258
+ ? "Playwright 依赖、node_modules 与 e2e spec 已就绪"
259
+ : `缺失: ${pwReady.missing.join("、")}(install/bootstrap 会自动补齐,或 npx sdd-flow-kit doctor --fix)`,
260
260
  });
261
261
  }
262
262
  catch {
263
263
  checks.push({
264
- name: "playwright-dependency",
264
+ name: "playwright-e2e-environment",
265
265
  ok: false,
266
266
  detail: "无法读取 package.json 检查 Playwright",
267
267
  });
@@ -4,12 +4,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.hasPlaywrightDependency = hasPlaywrightDependency;
7
+ exports.isPlaywrightDependencyInstalled = isPlaywrightDependencyInstalled;
8
+ exports.isPlaywrightEnvironmentReady = isPlaywrightEnvironmentReady;
7
9
  exports.ensurePlaywright = ensurePlaywright;
10
+ exports.ensurePlaywrightEnvironment = ensurePlaywrightEnvironment;
8
11
  exports.runMandatoryE2e = runMandatoryE2e;
9
12
  const child_process_1 = require("child_process");
10
13
  const promises_1 = __importDefault(require("fs/promises"));
11
14
  const path_1 = __importDefault(require("path"));
15
+ const e2eScaffold_1 = require("./e2eScaffold");
12
16
  const packageJsonPatch_1 = require("./packageJsonPatch");
17
+ async function pathExists(filePath) {
18
+ try {
19
+ await promises_1.default.access(filePath);
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
13
26
  async function detectPackageManager(pkgRoot) {
14
27
  try {
15
28
  await promises_1.default.access(path_1.default.join(pkgRoot, "pnpm-lock.yaml"));
@@ -43,62 +56,222 @@ async function hasPlaywrightDependency(pkgRoot) {
43
56
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
44
57
  return Boolean(deps["@playwright/test"] || deps.playwright);
45
58
  }
59
+ /** 校验 node_modules 中是否已实际安装 @playwright/test(避免仅 package.json 声明但 pnpm store 不一致) */
60
+ async function isPlaywrightDependencyInstalled(pkgRoot) {
61
+ return pathExists(path_1.default.join(pkgRoot, "node_modules", "@playwright", "test", "package.json"));
62
+ }
46
63
  function runCmd(cmd, args, cwd) {
47
- var _a;
64
+ var _a, _b, _c;
48
65
  const ret = (0, child_process_1.spawnSync)(cmd, args, {
49
66
  cwd,
50
- stdio: "inherit",
67
+ stdio: "pipe",
68
+ encoding: "utf8",
51
69
  shell: process.platform === "win32",
52
70
  env: process.env,
53
71
  });
72
+ const output = `${(_a = ret.stdout) !== null && _a !== void 0 ? _a : ""}${(_b = ret.stderr) !== null && _b !== void 0 ? _b : ""}`;
54
73
  if (ret.status !== 0) {
55
- return { ok: false, detail: `${cmd} ${args.join(" ")} 退出码 ${(_a = ret.status) !== null && _a !== void 0 ? _a : 1}` };
74
+ return { ok: false, detail: `${cmd} ${args.join(" ")} 退出码 ${(_c = ret.status) !== null && _c !== void 0 ? _c : 1}`, output };
56
75
  }
57
- return { ok: true, detail: "ok" };
76
+ return { ok: true, detail: "ok", output };
77
+ }
78
+ function isPnpmStoreMismatch(output) {
79
+ return /ERR_PNPM_UNEXPECTED_STORE|Unexpected store location/i.test(output);
58
80
  }
59
81
  /**
60
- * 强制确保 Playwright 可用:缺依赖则安装,缺浏览器则 install chromium,缺 test:e2e 则写入脚本。
82
+ * pnpm 项目 node_modules 与 lock/store 不一致时先执行 install,修复后再 add Playwright。
61
83
  */
62
- async function ensurePlaywright(pkgRoot, dryRun = false) {
84
+ async function repairPnpmNodeModules(pkgRoot, dryRun) {
85
+ const pm = await detectPackageManager(pkgRoot);
86
+ if (pm !== "pnpm") {
87
+ return { ok: true, detail: "非 pnpm 项目,跳过 store 修复" };
88
+ }
89
+ if (!(await pathExists(path_1.default.join(pkgRoot, "pnpm-lock.yaml")))) {
90
+ return { ok: true, detail: "无 pnpm-lock.yaml,跳过 store 修复" };
91
+ }
92
+ const needsRepair = !(await pathExists(path_1.default.join(pkgRoot, "node_modules"))) ||
93
+ ((await hasPlaywrightDependency(pkgRoot)) && !(await isPlaywrightDependencyInstalled(pkgRoot)));
94
+ if (!needsRepair) {
95
+ return { ok: true, detail: "pnpm node_modules 已就绪" };
96
+ }
97
+ if (dryRun) {
98
+ return { ok: false, detail: "dry-run: 将执行 pnpm install 修复 node_modules/store" };
99
+ }
100
+ const install = runCmd("pnpm", ["install"], pkgRoot);
101
+ if (!install.ok) {
102
+ return { ok: false, detail: `pnpm install 失败: ${install.detail}` };
103
+ }
104
+ return { ok: true, detail: "已执行 pnpm install 修复 node_modules" };
105
+ }
106
+ async function installPlaywrightDependency(pkgRoot, pm, dryRun) {
107
+ if (dryRun) {
108
+ return { ok: false, detail: `dry-run: 将安装 @playwright/test(${pm})`, installed: false };
109
+ }
110
+ const addArgs = pm === "pnpm"
111
+ ? ["add", "-D", "@playwright/test"]
112
+ : pm === "yarn"
113
+ ? ["add", "-D", "@playwright/test"]
114
+ : ["install", "-D", "@playwright/test"];
115
+ let add = runCmd(pm, addArgs, pkgRoot);
116
+ if (!add.ok && pm === "pnpm" && isPnpmStoreMismatch(add.output)) {
117
+ const reinstall = runCmd("pnpm", ["install"], pkgRoot);
118
+ if (!reinstall.ok) {
119
+ return { ok: false, detail: `pnpm store 修复失败: ${reinstall.detail}`, installed: false };
120
+ }
121
+ add = runCmd(pm, addArgs, pkgRoot);
122
+ }
123
+ if (!add.ok) {
124
+ return { ok: false, detail: `安装 @playwright/test 失败: ${add.detail}`, installed: false };
125
+ }
126
+ return { ok: true, detail: "已安装 @playwright/test", installed: true };
127
+ }
128
+ async function installPlaywrightBrowsers(pkgRoot, dryRun) {
63
129
  var _a;
130
+ if (dryRun) {
131
+ return { ok: true, detail: "dry-run: 将安装 chromium 浏览器" };
132
+ }
133
+ const pkg = await readPkg(pkgRoot);
134
+ const script = (_a = pkg === null || pkg === void 0 ? void 0 : pkg.scripts) === null || _a === void 0 ? void 0 : _a["playwright:install"];
135
+ if (script) {
136
+ const pm = await detectPackageManager(pkgRoot);
137
+ const runScript = pm === "yarn" ? ["run", "playwright:install"] : ["run", "playwright:install"];
138
+ const viaScript = runCmd(pm, runScript, pkgRoot);
139
+ if (viaScript.ok) {
140
+ return { ok: true, detail: "已执行 playwright:install" };
141
+ }
142
+ }
143
+ const browsers = runCmd("pnpm", ["exec", "playwright", "install", "chromium"], pkgRoot);
144
+ if (browsers.ok) {
145
+ return { ok: true, detail: "已执行 pnpm exec playwright install chromium" };
146
+ }
147
+ const fallback = runCmd("npx", ["playwright", "install", "chromium"], pkgRoot);
148
+ if (!fallback.ok) {
149
+ return { ok: false, detail: `playwright install chromium 失败: ${fallback.detail}` };
150
+ }
151
+ return { ok: true, detail: "已执行 npx playwright install chromium" };
152
+ }
153
+ async function ensurePlaywrightScripts(pkgRoot, dryRun) {
154
+ var _a, _b, _c;
155
+ const pkg = await readPkg(pkgRoot);
156
+ if (!pkg)
157
+ return;
158
+ const updates = {};
159
+ if (!((_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a["test:e2e"])) {
160
+ updates["test:e2e"] = "playwright test";
161
+ }
162
+ if (!((_b = pkg.scripts) === null || _b === void 0 ? void 0 : _b["playwright:install"])) {
163
+ const pm = await detectPackageManager(pkgRoot);
164
+ updates["playwright:install"] =
165
+ pm === "pnpm" ? "pnpm exec playwright install chromium" : "npx playwright install chromium";
166
+ }
167
+ if (!((_c = pkg.scripts) === null || _c === void 0 ? void 0 : _c.e2e)) {
168
+ const pm = await detectPackageManager(pkgRoot);
169
+ updates.e2e = pm === "pnpm" ? "pnpm exec playwright test e2e --workers=1" : "npx playwright test e2e --workers=1";
170
+ }
171
+ if (Object.keys(updates).length === 0 || dryRun)
172
+ return;
173
+ await (0, packageJsonPatch_1.patchPackageJsonScripts)(path_1.default.join(pkgRoot, "package.json"), updates, {
174
+ minDependencyCount: 1,
175
+ minBytes: 40,
176
+ minSizeRatio: 0.3,
177
+ });
178
+ }
179
+ /** Playwright 依赖、浏览器与 e2e spec 是否满足交付前最低要求 */
180
+ async function isPlaywrightEnvironmentReady(pkgRoot) {
181
+ const missing = [];
182
+ if (!(await hasPlaywrightDependency(pkgRoot))) {
183
+ missing.push("@playwright/test 依赖未声明");
184
+ }
185
+ else if (!(await isPlaywrightDependencyInstalled(pkgRoot))) {
186
+ missing.push("@playwright/test 未安装到 node_modules");
187
+ }
188
+ if (!(await pathExists(path_1.default.join(pkgRoot, "playwright.config.ts"))) &&
189
+ !(await pathExists(path_1.default.join(pkgRoot, "playwright.config.js")))) {
190
+ missing.push("playwright.config.ts/js");
191
+ }
192
+ const specCount = await (0, e2eScaffold_1.countE2eSpecFiles)(pkgRoot);
193
+ if (specCount === 0) {
194
+ missing.push("e2e/**/*.spec.ts");
195
+ }
196
+ return { ready: missing.length === 0, missing };
197
+ }
198
+ /**
199
+ * 强制确保 Playwright 可用:pnpm install 修复 store、安装依赖、浏览器与 e2e smoke 脚手架。
200
+ */
201
+ async function ensurePlaywright(pkgRoot, dryRun = false) {
64
202
  const pkg = await readPkg(pkgRoot);
65
203
  if (!pkg) {
66
204
  return { ok: false, detail: "无法读取 package.json", installed: false };
67
205
  }
206
+ const repair = await repairPnpmNodeModules(pkgRoot, dryRun);
207
+ if (!repair.ok) {
208
+ return { ok: false, detail: repair.detail, installed: false };
209
+ }
68
210
  const pm = await detectPackageManager(pkgRoot);
69
211
  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);
212
+ if (!(await hasPlaywrightDependency(pkgRoot)) || !(await isPlaywrightDependencyInstalled(pkgRoot))) {
213
+ const add = await installPlaywrightDependency(pkgRoot, pm, dryRun);
84
214
  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 });
215
+ return { ok: false, detail: add.detail, installed: false };
92
216
  }
217
+ installed = installed || add.installed;
93
218
  }
94
- if (dryRun) {
95
- return { ok: true, detail: "dry-run: Playwright 已就绪", installed };
219
+ await ensurePlaywrightScripts(pkgRoot, dryRun);
220
+ const scaffold = await (0, e2eScaffold_1.ensureE2eScaffold)(pkgRoot, dryRun);
221
+ if (!scaffold.ok && dryRun) {
222
+ return { ok: false, detail: scaffold.detail, installed };
96
223
  }
97
- const browsers = runCmd("npx", ["playwright", "install", "chromium"], pkgRoot);
224
+ const browsers = await installPlaywrightBrowsers(pkgRoot, dryRun);
98
225
  if (!browsers.ok) {
99
- return { ok: false, detail: `playwright install chromium 失败: ${browsers.detail}`, installed };
226
+ return { ok: false, detail: browsers.detail, installed };
227
+ }
228
+ const ready = await isPlaywrightEnvironmentReady(pkgRoot);
229
+ if (!ready.ready && !dryRun) {
230
+ return {
231
+ ok: false,
232
+ detail: `Playwright 仍缺: ${ready.missing.join("、")}`,
233
+ installed,
234
+ };
235
+ }
236
+ return {
237
+ ok: true,
238
+ detail: installed ? "已安装 @playwright/test、chromium 与 e2e smoke" : "Playwright 已就绪",
239
+ installed,
240
+ };
241
+ }
242
+ /**
243
+ * install / bootstrap / doctor 统一入口:检测未就绪则自动执行 pnpm install → add @playwright/test → playwright:install → e2e scaffold。
244
+ */
245
+ async function ensurePlaywrightEnvironment(pkgRoot, dryRun = false) {
246
+ const before = await isPlaywrightEnvironmentReady(pkgRoot);
247
+ if (before.ready) {
248
+ return { ok: true, detail: "Playwright E2E 环境已就绪", actions: [] };
249
+ }
250
+ const actions = [`检测到缺失: ${before.missing.join("、")}`];
251
+ const result = await ensurePlaywright(pkgRoot, dryRun);
252
+ if (result.installed) {
253
+ actions.push("安装 @playwright/test");
254
+ }
255
+ if (result.ok) {
256
+ actions.push(result.detail);
257
+ }
258
+ else {
259
+ actions.push(result.detail);
260
+ return { ok: false, detail: result.detail, actions };
100
261
  }
101
- return { ok: true, detail: installed ? "已安装 @playwright/test 与 chromium" : "Playwright 已就绪", installed };
262
+ const after = await isPlaywrightEnvironmentReady(pkgRoot);
263
+ if (!after.ready && !dryRun) {
264
+ return {
265
+ ok: false,
266
+ detail: `Playwright 仍缺: ${after.missing.join("、")}`,
267
+ actions,
268
+ };
269
+ }
270
+ return {
271
+ ok: true,
272
+ detail: dryRun ? "dry-run: Playwright 环境将补齐" : "Playwright E2E 环境已自动补齐",
273
+ actions,
274
+ };
102
275
  }
103
276
  /** 强制执行 E2E(先 ensurePlaywright,再跑 test:e2e 或 playwright test) */
104
277
  async function runMandatoryE2e(pkgRoot, dryRun = false) {
@@ -11,6 +11,8 @@ const process_1 = __importDefault(require("process"));
11
11
  const nodeCompat_1 = require("../core/nodeCompat");
12
12
  const bootstrap_1 = require("../core/bootstrap");
13
13
  const opsxDelivery_1 = require("../core/opsxDelivery");
14
+ const playwrightEnsure_1 = require("../core/playwrightEnsure");
15
+ const projectRoots_1 = require("../core/projectRoots");
14
16
  function hasCommand(cmd) {
15
17
  const out = (0, child_process_1.spawnSync)("which", [cmd], { stdio: "ignore" });
16
18
  return out.status === 0;
@@ -35,6 +37,9 @@ function agentBin(agent) {
35
37
  return "openclaw";
36
38
  return null;
37
39
  }
40
+ async function refreshDeliveryChecks(projectRoot) {
41
+ return (0, opsxDelivery_1.collectDeliverySetupChecks)(projectRoot);
42
+ }
38
43
  async function runDoctor(options) {
39
44
  const checks = [];
40
45
  checks.push({
@@ -80,7 +85,7 @@ async function runDoctor(options) {
80
85
  detail: `${expectedDir} 目录存在性`,
81
86
  });
82
87
  }
83
- for (const c of await (0, opsxDelivery_1.collectDeliverySetupChecks)(options.projectRoot)) {
88
+ for (const c of await refreshDeliveryChecks(options.projectRoot)) {
84
89
  checks.push(c);
85
90
  }
86
91
  let ok = checks.every((c) => c.ok);
@@ -91,13 +96,38 @@ async function runDoctor(options) {
91
96
  "delivery-pipeline-script",
92
97
  "package-json-delivery-script",
93
98
  ].includes(c.name));
99
+ const playwrightFailed = checks.some((c) => !c.ok && c.name === "playwright-e2e-environment");
100
+ // Playwright 优先自动补齐,避免仅为 E2E 缺失触发完整 OPSX bootstrap
101
+ if (!ok && playwrightFailed && !options.dryRun) {
102
+ const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(options.projectRoot);
103
+ const pw = await (0, playwrightEnsure_1.ensurePlaywrightEnvironment)(pkgRoot, false);
104
+ if (pw.actions.length > 0) {
105
+ checks.push({
106
+ name: "ensure-playwright-environment",
107
+ ok: pw.ok,
108
+ detail: `${pw.detail} | ${pw.actions.join(" | ")}`,
109
+ });
110
+ }
111
+ const delivery = await refreshDeliveryChecks(options.projectRoot);
112
+ checks.length = 0;
113
+ checks.push({
114
+ name: "node-version",
115
+ ok: (0, nodeCompat_1.isNodeVersionSupported)(),
116
+ detail: `Node ${process_1.default.version}(要求 ${(0, nodeCompat_1.nodeVersionRequirementLabel)()},推荐 18/20 LTS)`,
117
+ }, {
118
+ name: "openspec-dir",
119
+ ok: await hasPath(path_1.default.join(options.projectRoot, "openspec")),
120
+ detail: "openspec 目录存在性",
121
+ }, ...delivery);
122
+ ok = checks.every((c) => c.ok);
123
+ }
94
124
  if (!ok && opsxStackFailed && !options.fix && !options.dryRun) {
95
125
  await (0, bootstrap_1.bootstrapProjectIfNeeded)({
96
126
  projectRoot: options.projectRoot,
97
127
  agent: options.agent,
98
128
  });
99
129
  checks.length = 0;
100
- for (const c of await (0, opsxDelivery_1.collectDeliverySetupChecks)(options.projectRoot)) {
130
+ for (const c of await refreshDeliveryChecks(options.projectRoot)) {
101
131
  checks.push(c);
102
132
  }
103
133
  ok = checks.every((c) => c.ok);
@@ -120,6 +150,20 @@ async function runDoctor(options) {
120
150
  });
121
151
  }
122
152
  ok = checks.every((c) => c.ok);
153
+ if (!ok && !options.dryRun) {
154
+ const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(options.projectRoot);
155
+ const pw = await (0, playwrightEnsure_1.ensurePlaywrightEnvironment)(pkgRoot, false);
156
+ checks.push({
157
+ name: "ensure-playwright-environment",
158
+ ok: pw.ok,
159
+ detail: pw.actions.length > 0 ? `${pw.detail} | ${pw.actions.join(" | ")}` : pw.detail,
160
+ });
161
+ checks.length = 0;
162
+ for (const c of await refreshDeliveryChecks(options.projectRoot)) {
163
+ checks.push(c);
164
+ }
165
+ ok = checks.every((c) => c.ok);
166
+ }
123
167
  }
124
168
  return { ok, checks };
125
169
  }
@@ -13,6 +13,7 @@ const readlinePrompt_1 = require("../core/readlinePrompt");
13
13
  const utils_1 = require("../core/utils");
14
14
  const opsxDelivery_1 = require("../core/opsxDelivery");
15
15
  const packageJsonPatch_1 = require("../core/packageJsonPatch");
16
+ const playwrightEnsure_1 = require("../core/playwrightEnsure");
16
17
  const projectRoots_1 = require("../core/projectRoots");
17
18
  const templates_1 = require("../core/templates");
18
19
  const EDITOR_CHOICES = ["claude-code", "cursor", "codex", "openclaw"];
@@ -393,6 +394,16 @@ async function setupProject(options) {
393
394
  if (repaired) {
394
395
  plannedActions.push("repaired package.json opsx:delivery-pipeline scripts (poquan → opsx)");
395
396
  }
397
+ if (!options.light) {
398
+ const pw = await (0, playwrightEnsure_1.ensurePlaywrightEnvironment)(pkgRoot, false);
399
+ plannedActions.push(...pw.actions);
400
+ if (pw.ok) {
401
+ plannedActions.push(`Playwright E2E: ${pw.detail}`);
402
+ }
403
+ else {
404
+ plannedActions.push(`Playwright E2E 未完全就绪: ${pw.detail};可执行 npx sdd-flow-kit doctor --fix -y`);
405
+ }
406
+ }
396
407
  }
397
408
  const summary = [
398
409
  "# SETUP-SUMMARY",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-flow-kit",
3
- "version": "1.3.21",
3
+ "version": "1.3.23",
4
4
  "private": false,
5
5
  "description": "Cross-agent SDD automated development workflow kit (ADI/ADI-like).",
6
6
  "license": "MIT",
@@ -0,0 +1,10 @@
1
+ import { test, expect } from "@playwright/test";
2
+
3
+ /**
4
+ * OPSX / sdd-flow-kit 自动写入的 smoke 用例:验证 dev 服可拉起且首页可访问。
5
+ * change 级验收请在本目录或子目录新增 *.spec.ts,并在 e2e-scope.config.json 绑定。
6
+ */
7
+ test("应用首页可加载 @smoke", async ({ page }) => {
8
+ await page.goto("/");
9
+ await expect(page).toHaveURL(/\//);
10
+ });