sdd-flow-kit 1.0.16 → 1.0.22

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 CHANGED
@@ -15,9 +15,12 @@
15
15
 
16
16
  1. 进入你要开发的项目目录
17
17
  2. 安装:`pnpm add -D sdd-flow-kit `
18
- 检查是否安装 - `ls .cursor/skills/sdd-flow-kit/SKILL.md`
19
- - `ls .claude/skills/sdd-flow-kit/SKILL.md`
20
- - `ls docs/*-doc-skill/SKILL.md`
18
+ 检查是否安装(Cursor 用户看 `.cursor`,不是 `.claude`):
19
+ - `ls .cursor/skills/sdd-flow-kit/SKILL.md`
20
+ - `ls docs/adi-doc-skill/SKILL.md`(adInsight 应为 **adi**,不是 oms)
21
+ - Claude Code 用户:`ls .claude/skills/sdd-flow-kit/SKILL.md`
22
+
23
+ > 若在 `adInsight-web/frontend` 等子目录安装,工具会**向上识别**仓库名;仍不对时:`SDD_FLOW_KIT_PROJECT=ADI pnpm add -D sdd-flow-kit` 或 `npx sdd-flow-kit install --project ADI -y`
21
24
  3. 若 skill 未生成,执行手动兜底:
22
25
  - `node ./node_modules/sdd-flow-kit/dist/postinstall.js`(轻量,秒级)
23
26
  - 或 `npx sdd-flow-kit install -y`(完整环境)
@@ -61,6 +64,22 @@
61
64
 
62
65
  阶段口语(可选,非必须):`继续` / `做提案` → propose;`开始 apply` → 实现。
63
66
 
67
+ ##### `/opsx-apply` 与 TDD + Playwright 检查机制
68
+
69
+ | 环节 | 实现 | 说明 |
70
+ |------|------|------|
71
+ | 安装时 | `postinstall`(light) | 只写 sdd skill/rule;**不**自动跑 opsx-workflow(避免 `pnpm i` 卡住) |
72
+ | 完整安装 | `npx sdd-flow-kit install -y` | 缺什么跑什么 → `ensureOpsxWorkflow` → `npx @lixin5257xxx/opsx-workflow setup` |
73
+ | Apply 前 | `npx sdd-flow-kit ensure-opsx -y` | **阶段 D 第 0 步**;doctor 加 `--fix` 等价修复 |
74
+ | Apply 时 | Cursor 读 skill 链 + `pnpm run opsx:delivery-pipeline` | 由 `AGENTS.md` / `06-agent-skill` 约束 AI,非 Node 自动执行 |
75
+
76
+ ```bash
77
+ npx sdd-flow-kit ensure-opsx --agent cursor -y # /opsx-apply 前
78
+ npx sdd-flow-kit doctor --agent cursor --fix # 检查并尝试修复
79
+ ```
80
+
81
+ 常见遗漏:只 `pnpm add sdd-flow-kit` 未跑 `ensure-opsx`;或 AI 跳过 skill 直接改代码。
82
+
64
83
  1. `install`(等价 `setup`):提示选择开发环境(cursor/claude-code/codex/openclaw,默认 claude-code)与开发分支(无则自动创建),并检测/安装 openspec+superpower 基础环境,同时写入项目级 Skill 与 Cursor 门禁规则
65
84
  2. 安装完成后,本工具主要负责“流程环境与模板”落地;业务需求由你在目标 AI 工具中自然语言驱动
66
85
  3. `start`/`run`/`invoke` 属于进阶能力(脚本化或一键编排时使用)
package/dist/cli.js CHANGED
@@ -41,7 +41,11 @@ function printHelp() {
41
41
  " npx sdd-flow-kit invoke \"开发 XXX 1.2.3 需求\"",
42
42
  "",
43
43
  "5) 环境健检:",
44
- " npx sdd-flow-kit doctor --agent claude-code",
44
+ " npx sdd-flow-kit doctor --agent cursor",
45
+ "",
46
+ "6) Apply 前确保 OPSX+TDD+Playwright(/opsx-apply 前必须):",
47
+ " npx sdd-flow-kit ensure-opsx --agent cursor -y",
48
+ " (缺省时自动 npx @lixin5257xxx/opsx-workflow setup)",
45
49
  "",
46
50
  "可选参数:",
47
51
  " --projectRoot <path> 目标项目根目录(默认当前目录)",
@@ -67,6 +71,7 @@ async function main() {
67
71
  cmd !== "setup" &&
68
72
  cmd !== "install" &&
69
73
  cmd !== "doctor" &&
74
+ cmd !== "ensure-opsx" &&
70
75
  cmd !== "invoke") {
71
76
  printHelp();
72
77
  node_process_1.default.exitCode = 1;
@@ -96,10 +101,16 @@ async function main() {
96
101
  node_process_1.default.exitCode = 1;
97
102
  return;
98
103
  }
99
- if (cmd === "doctor") {
100
- const doctor = await (0, doctor_1.runDoctor)({ projectRoot: node_path_1.default.resolve(projectRoot), agent });
104
+ if (cmd === "doctor" || cmd === "ensure-opsx") {
105
+ const fix = cmd === "ensure-opsx" || node_process_1.default.argv.includes("--fix");
106
+ const doctor = await (0, doctor_1.runDoctor)({
107
+ projectRoot: node_path_1.default.resolve(projectRoot),
108
+ agent,
109
+ fix,
110
+ dryRun,
111
+ });
101
112
  // eslint-disable-next-line no-console
102
- console.log(`doctor=${doctor.ok ? "ok" : "failed"}, agent=${agent}`);
113
+ console.log(`${cmd}=${doctor.ok ? "ok" : "failed"}, agent=${agent}${fix ? ", fix=on" : ""}`);
103
114
  for (const check of doctor.checks) {
104
115
  // eslint-disable-next-line no-console
105
116
  console.log(`[${check.ok ? "OK" : "FAIL"}] ${check.name}: ${check.detail}`);
@@ -108,6 +119,15 @@ async function main() {
108
119
  // eslint-disable-next-line no-console
109
120
  console.log(JSON.stringify({ command: "doctor", projectRoot: node_path_1.default.resolve(projectRoot), result: doctor }, null, 2));
110
121
  }
122
+ if (cmd === "ensure-opsx" && !dryRun) {
123
+ await (0, setupProject_1.setupProject)({
124
+ projectRoot: node_path_1.default.resolve(projectRoot),
125
+ agent,
126
+ yes: true,
127
+ light: true,
128
+ projectKind: projectKind,
129
+ });
130
+ }
111
131
  if (!doctor.ok)
112
132
  node_process_1.default.exitCode = 1;
113
133
  return;
@@ -0,0 +1,152 @@
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.PRODUCT_PREFIX_BY_KIND = exports.DOC_SKILL_BY_KIND = exports.ALLOWED_PROJECTS = void 0;
7
+ exports.productToKind = productToKind;
8
+ exports.resolveDocSkillDir = resolveDocSkillDir;
9
+ exports.inferProjectKind = inferProjectKind;
10
+ const promises_1 = __importDefault(require("node:fs/promises"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ exports.ALLOWED_PROJECTS = ["OMS", "ADI", "欢盟", "AD Tools"];
13
+ exports.DOC_SKILL_BY_KIND = {
14
+ ADI: "adi-doc-skill",
15
+ OMS: "oms-doc-skill",
16
+ 欢盟: "huan-doc-skill",
17
+ "AD Tools": "ad-tools-doc-skill",
18
+ };
19
+ exports.PRODUCT_PREFIX_BY_KIND = {
20
+ ADI: "ADI",
21
+ OMS: "OMS",
22
+ 欢盟: "HUAN",
23
+ "AD Tools": "ADTOOLS",
24
+ };
25
+ function productToKind(product) {
26
+ const p = product.trim().toUpperCase();
27
+ if (p === "ADI" || p.includes("ADINSIGHT"))
28
+ return "ADI";
29
+ if (p === "OMS" || p.includes("OPERATION"))
30
+ return "OMS";
31
+ if (p === "欢盟" || p === "HUAN")
32
+ return "欢盟";
33
+ if (p.includes("AD TOOLS") || p === "ADTOOLS")
34
+ return "AD Tools";
35
+ return "OMS";
36
+ }
37
+ /** 从 projectRoot 向上查找 docs/*-doc-skill(适配 frontend 子目录 + 根目录 docs) */
38
+ async function resolveDocSkillDir(projectRoot, kind) {
39
+ const dirName = exports.DOC_SKILL_BY_KIND[kind];
40
+ let cur = node_path_1.default.resolve(projectRoot);
41
+ for (let i = 0; i < 8; i += 1) {
42
+ const candidate = node_path_1.default.join(cur, "docs", dirName);
43
+ if (await pathExists(node_path_1.default.join(candidate, "confluence-doc.py"))) {
44
+ const rel = node_path_1.default.relative(projectRoot, candidate);
45
+ return {
46
+ absPath: candidate,
47
+ relFromRoot: rel.startsWith("..") ? rel : node_path_1.default.join("docs", dirName),
48
+ };
49
+ }
50
+ const parent = node_path_1.default.dirname(cur);
51
+ if (parent === cur)
52
+ break;
53
+ cur = parent;
54
+ }
55
+ return {
56
+ absPath: node_path_1.default.join(projectRoot, "docs", dirName),
57
+ relFromRoot: node_path_1.default.join("docs", dirName),
58
+ };
59
+ }
60
+ async function pathExists(filePath) {
61
+ try {
62
+ await promises_1.default.access(filePath);
63
+ return true;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ function matchKindInText(text) {
70
+ const t = text.toLowerCase().replace(/_/g, "-");
71
+ if (t.includes("adinsight") || t.includes("adi-web") || /(^|\/|-)adi(-|\/|$)/.test(t)) {
72
+ return "ADI";
73
+ }
74
+ if (t.includes("operation-web") || t.includes("operation") || /(^|\/|-)oms(-|\/|$)/.test(t)) {
75
+ return "OMS";
76
+ }
77
+ if (t.includes("huan-union") || t.includes("huanunion") || /\bhm\b/.test(t)) {
78
+ return "欢盟";
79
+ }
80
+ if (t.includes("ad-tools") || t.includes("adtools")) {
81
+ return "AD Tools";
82
+ }
83
+ return null;
84
+ }
85
+ /** 从 projectRoot 向上查找已存在的 docs/*-doc-skill */
86
+ async function inferFromExistingDocSkill(projectRoot) {
87
+ let dir = projectRoot;
88
+ for (let i = 0; i < 8; i += 1) {
89
+ for (const kind of exports.ALLOWED_PROJECTS) {
90
+ const rel = node_path_1.default.join(dir, "docs", exports.DOC_SKILL_BY_KIND[kind]);
91
+ if (await pathExists(node_path_1.default.join(rel, "SKILL.md"))) {
92
+ return kind;
93
+ }
94
+ }
95
+ const parent = node_path_1.default.dirname(dir);
96
+ if (parent === dir)
97
+ break;
98
+ dir = parent;
99
+ }
100
+ return null;
101
+ }
102
+ /** 从目录路径(含父级)推断产品类型 */
103
+ function inferFromPathSegments(projectRoot) {
104
+ const resolved = node_path_1.default.resolve(projectRoot);
105
+ const parts = resolved.split(node_path_1.default.sep);
106
+ for (let i = parts.length; i > 0; i -= 1) {
107
+ const segment = parts.slice(0, i).join(node_path_1.default.sep);
108
+ const hit = matchKindInText(segment);
109
+ if (hit)
110
+ return hit;
111
+ }
112
+ return null;
113
+ }
114
+ async function inferFromPackageJson(dir) {
115
+ try {
116
+ const pkgText = await promises_1.default.readFile(node_path_1.default.join(dir, "package.json"), "utf8");
117
+ const pkg = JSON.parse(pkgText);
118
+ const blob = `${pkg.name ?? ""} ${pkg.description ?? ""}`;
119
+ return matchKindInText(blob);
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ /**
126
+ * 推断 OMS / ADI / 欢盟 / AD Tools。
127
+ * monorepo 子目录(如 adInsight-web/frontend)会向上扫描路径,避免误判为 OMS。
128
+ */
129
+ async function inferProjectKind(projectRoot) {
130
+ const fromEnv = (process.env.SDD_FLOW_KIT_PROJECT ?? "").trim();
131
+ if (exports.ALLOWED_PROJECTS.includes(fromEnv)) {
132
+ return fromEnv;
133
+ }
134
+ // 路径优先于已存在的 docs skill(避免 frontend 子目录曾误装 oms-doc-skill 后一直错下去)
135
+ const fromPath = inferFromPathSegments(projectRoot);
136
+ if (fromPath)
137
+ return fromPath;
138
+ let dir = projectRoot;
139
+ for (let i = 0; i < 6; i += 1) {
140
+ const fromPkg = await inferFromPackageJson(dir);
141
+ if (fromPkg)
142
+ return fromPkg;
143
+ const parent = node_path_1.default.dirname(dir);
144
+ if (parent === dir)
145
+ break;
146
+ dir = parent;
147
+ }
148
+ const fromExisting = await inferFromExistingDocSkill(projectRoot);
149
+ if (fromExisting)
150
+ return fromExisting;
151
+ return "OMS";
152
+ }
@@ -0,0 +1,248 @@
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.deliveryPipelineNpmScripts = deliveryPipelineNpmScripts;
7
+ exports.pathExists = pathExists;
8
+ exports.resolveOpsxAgent = resolveOpsxAgent;
9
+ exports.repairOpsxDeliveryScripts = repairOpsxDeliveryScripts;
10
+ exports.buildOpsxWorkflowSetupCommand = buildOpsxWorkflowSetupCommand;
11
+ exports.isOpsxApplyReady = isOpsxApplyReady;
12
+ exports.ensureOpsxWorkflow = ensureOpsxWorkflow;
13
+ exports.collectDeliverySetupChecks = collectDeliverySetupChecks;
14
+ const node_child_process_1 = require("node:child_process");
15
+ const promises_1 = __importDefault(require("node:fs/promises"));
16
+ const node_path_1 = __importDefault(require("node:path"));
17
+ const fs_1 = require("./fs");
18
+ const DELIVERY_SCRIPT_REL = ".cursor/skills/opsx-dev-delivery-pipeline/scripts/opsx-delivery-pipeline.sh";
19
+ function deliveryPipelineNpmScripts() {
20
+ const full = `bash ${DELIVERY_SCRIPT_REL}`;
21
+ const short = `OPSX_PIPELINE_MODE=short ${full}`;
22
+ return { full, short };
23
+ }
24
+ async function pathExists(filePath) {
25
+ try {
26
+ await promises_1.default.access(filePath);
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ /** 优先使用已安装 OPSX skills 的 editor 目录 */
34
+ async function resolveOpsxAgent(projectRoot, preferred) {
35
+ const deliverySkill = node_path_1.default.join(projectRoot, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md");
36
+ if (await pathExists(deliverySkill))
37
+ return "cursor";
38
+ const claudeDelivery = node_path_1.default.join(projectRoot, ".claude", "skills", "opsx-dev-delivery-pipeline", "SKILL.md");
39
+ if (await pathExists(claudeDelivery))
40
+ return "claude-code";
41
+ if (await pathExists(node_path_1.default.join(projectRoot, ".cursor")))
42
+ return "cursor";
43
+ if (await pathExists(node_path_1.default.join(projectRoot, ".claude")))
44
+ return "claude-code";
45
+ return preferred;
46
+ }
47
+ /** 将 package.json 中过期的 poquan-delivery-pipeline 脚本修正为 opsx-dev-delivery-pipeline */
48
+ async function repairOpsxDeliveryScripts(projectRoot) {
49
+ const pkgPath = node_path_1.default.join(projectRoot, "package.json");
50
+ if (!(await pathExists(pkgPath)))
51
+ return false;
52
+ const scriptOnDisk = node_path_1.default.join(projectRoot, DELIVERY_SCRIPT_REL);
53
+ if (!(await pathExists(scriptOnDisk)))
54
+ return false;
55
+ const { full, short } = deliveryPipelineNpmScripts();
56
+ const text = await promises_1.default.readFile(pkgPath, "utf8");
57
+ const pkg = JSON.parse(text);
58
+ if (!pkg.scripts)
59
+ pkg.scripts = {};
60
+ let changed = false;
61
+ const needsFix = (v) => !v || v.includes("poquan-dev-delivery-pipeline") || v.includes("poquan-delivery-pipeline");
62
+ if (needsFix(pkg.scripts["opsx:delivery-pipeline"])) {
63
+ pkg.scripts["opsx:delivery-pipeline"] = full;
64
+ changed = true;
65
+ }
66
+ if (needsFix(pkg.scripts["opsx:delivery-pipeline:short"])) {
67
+ pkg.scripts["opsx:delivery-pipeline:short"] = short;
68
+ changed = true;
69
+ }
70
+ if (!changed)
71
+ return false;
72
+ await (0, fs_1.writeTextFileEnsuredDir)(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
73
+ return true;
74
+ }
75
+ function hasCommand(cmd) {
76
+ return (0, node_child_process_1.spawnSync)("which", [cmd], { stdio: "ignore" }).status === 0;
77
+ }
78
+ function buildOpsxWorkflowSetupCommand(projectRoot, agent) {
79
+ const baseArgs = ["@lixin5257xxx/opsx-workflow", "setup"];
80
+ if (agent === "cursor") {
81
+ return {
82
+ cmd: "npx",
83
+ args: [...baseArgs, "--editor", "cursor", "--project", projectRoot, "-y"],
84
+ };
85
+ }
86
+ if (agent === "claude-code") {
87
+ return { cmd: "npx", args: [...baseArgs, "--editor", "claude-code", "-y"] };
88
+ }
89
+ if (agent === "openclaw") {
90
+ return { cmd: "npx", args: [...baseArgs, "--editor", "openclaw", "-y"] };
91
+ }
92
+ return { cmd: "npx", args: [...baseArgs, "--editor", "codex", "-y"] };
93
+ }
94
+ async function isOpsxApplyReady(projectRoot) {
95
+ const checks = await collectDeliverySetupChecks(projectRoot);
96
+ return checks.every((c) => c.ok);
97
+ }
98
+ /** 缺 openspec/TDD/Playwright 工作流时安装 @lixin5257xxx/opsx-workflow,并修复 delivery 脚本 */
99
+ async function ensureOpsxWorkflow(options) {
100
+ const plannedActions = [];
101
+ let checks = await collectDeliverySetupChecks(options.projectRoot);
102
+ if (checks.every((c) => c.ok)) {
103
+ return {
104
+ ready: true,
105
+ workflowInstalled: false,
106
+ repairedScripts: false,
107
+ checks,
108
+ plannedActions,
109
+ };
110
+ }
111
+ const agent = await resolveOpsxAgent(options.projectRoot, options.agent);
112
+ const { cmd, args } = buildOpsxWorkflowSetupCommand(options.projectRoot, agent);
113
+ plannedActions.push(`missing opsx apply stack → run: ${cmd} ${args.join(" ")}`);
114
+ if (options.dryRun) {
115
+ return {
116
+ ready: false,
117
+ workflowInstalled: false,
118
+ repairedScripts: false,
119
+ checks,
120
+ plannedActions,
121
+ };
122
+ }
123
+ const ret = (0, node_child_process_1.spawnSync)(cmd, args, {
124
+ cwd: options.projectRoot,
125
+ stdio: "inherit",
126
+ env: process.env,
127
+ });
128
+ if (ret.status !== 0) {
129
+ throw new Error(`opsx-workflow 安装失败:${cmd} ${args.join(" ")}`);
130
+ }
131
+ const repairedScripts = await repairOpsxDeliveryScripts(options.projectRoot);
132
+ if (repairedScripts) {
133
+ plannedActions.push("repaired package.json opsx:delivery-pipeline scripts");
134
+ }
135
+ checks = await collectDeliverySetupChecks(options.projectRoot);
136
+ return {
137
+ ready: checks.every((c) => c.ok),
138
+ workflowInstalled: true,
139
+ repairedScripts,
140
+ checks,
141
+ plannedActions,
142
+ };
143
+ }
144
+ async function collectDeliverySetupChecks(projectRoot) {
145
+ const checks = [];
146
+ checks.push({
147
+ name: "openspec-dir",
148
+ ok: await pathExists(node_path_1.default.join(projectRoot, "openspec")),
149
+ detail: "openspec/ 目录",
150
+ });
151
+ checks.push({
152
+ name: "openspec-cli",
153
+ ok: hasCommand("openspec"),
154
+ detail: "openspec CLI(npx openspec 或全局安装)",
155
+ });
156
+ const skillPath = node_path_1.default.join(projectRoot, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md");
157
+ checks.push({
158
+ name: "opsx-dev-delivery-pipeline-skill",
159
+ ok: await pathExists(skillPath),
160
+ detail: skillPath,
161
+ });
162
+ const tddPath = node_path_1.default.join(projectRoot, ".cursor", "skills", "tdd-script", "SKILL.md");
163
+ checks.push({
164
+ name: "tdd-script-skill",
165
+ ok: await pathExists(tddPath),
166
+ detail: tddPath,
167
+ });
168
+ const applyPath = node_path_1.default.join(projectRoot, ".cursor", "skills", "openspec-apply-change", "SKILL.md");
169
+ checks.push({
170
+ name: "openspec-apply-change-skill",
171
+ ok: await pathExists(applyPath),
172
+ detail: applyPath,
173
+ });
174
+ const pipelineScript = node_path_1.default.join(projectRoot, DELIVERY_SCRIPT_REL);
175
+ checks.push({
176
+ name: "delivery-pipeline-script",
177
+ ok: await pathExists(pipelineScript),
178
+ detail: pipelineScript,
179
+ });
180
+ try {
181
+ const pkgText = await promises_1.default.readFile(node_path_1.default.join(projectRoot, "package.json"), "utf8");
182
+ const pkg = JSON.parse(pkgText);
183
+ const cmd = pkg.scripts?.["opsx:delivery-pipeline"] ?? "";
184
+ const ok = cmd.includes("opsx-dev-delivery-pipeline") &&
185
+ cmd.includes("opsx-delivery-pipeline.sh") &&
186
+ !cmd.includes("poquan");
187
+ checks.push({
188
+ name: "package-json-delivery-script",
189
+ ok,
190
+ detail: ok ? cmd : `错误或缺失: ${cmd || "(empty)"}`,
191
+ });
192
+ }
193
+ catch {
194
+ checks.push({
195
+ name: "package-json-delivery-script",
196
+ ok: false,
197
+ detail: "无法读取 package.json",
198
+ });
199
+ }
200
+ try {
201
+ const cfgText = await promises_1.default.readFile(node_path_1.default.join(projectRoot, ".opsx", "config.json"), "utf8");
202
+ const cfg = JSON.parse(cfgText);
203
+ const skillsDir = cfg.skillsDir ?? "";
204
+ const hasCursorDelivery = await pathExists(node_path_1.default.join(projectRoot, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md"));
205
+ const nameOk = cfg.deliveryPipeline?.skillName === "opsx-dev-delivery-pipeline";
206
+ const dirOk = (hasCursorDelivery && skillsDir.includes(".cursor")) ||
207
+ (!hasCursorDelivery && (skillsDir.includes(".cursor") || skillsDir.includes(".claude")));
208
+ checks.push({
209
+ name: "opsx-config-skills-dir",
210
+ ok: nameOk && dirOk,
211
+ detail: hasCursorDelivery && !skillsDir.includes(".cursor")
212
+ ? `skills 在 .cursor 但 config 指向 ${skillsDir},请 npx sdd-flow-kit install -y`
213
+ : `skillsDir=${skillsDir}, skillName=${cfg.deliveryPipeline?.skillName ?? ""}`,
214
+ });
215
+ }
216
+ catch {
217
+ checks.push({
218
+ name: "opsx-config",
219
+ ok: false,
220
+ detail: ".opsx/config.json 缺失",
221
+ });
222
+ }
223
+ try {
224
+ const pkgText = await promises_1.default.readFile(node_path_1.default.join(projectRoot, "package.json"), "utf8");
225
+ const pkg = JSON.parse(pkgText);
226
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
227
+ const hasPw = Boolean(deps["@playwright/test"] || deps.playwright);
228
+ checks.push({
229
+ name: "playwright-dependency",
230
+ ok: hasPw,
231
+ detail: hasPw ? "@playwright/test 或 playwright 已声明" : "package.json 缺少 Playwright 依赖",
232
+ });
233
+ }
234
+ catch {
235
+ checks.push({
236
+ name: "playwright-dependency",
237
+ ok: false,
238
+ detail: "无法读取 package.json 检查 Playwright",
239
+ });
240
+ }
241
+ const agentsMd = node_path_1.default.join(projectRoot, "AGENTS.md");
242
+ checks.push({
243
+ name: "agents-md",
244
+ ok: await pathExists(agentsMd),
245
+ detail: "AGENTS.md(Apply/TDD/pipeline 编排 SSOT)",
246
+ });
247
+ return checks;
248
+ }
@@ -1,55 +1,55 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  const node_path_1 = __importDefault(require("node:path"));
7
- const promises_1 = __importDefault(require("node:fs/promises"));
8
40
  const node_process_1 = __importDefault(require("node:process"));
41
+ const inferProject_1 = require("./core/inferProject");
42
+ const opsxDelivery_1 = require("./core/opsxDelivery");
9
43
  const setupProject_1 = require("./steps/setupProject");
10
- const ALLOWED_PROJECTS = ["OMS", "ADI", "欢盟", "AD Tools"];
11
- async function inferProjectKind(projectRoot) {
12
- const fromEnv = (node_process_1.default.env.SDD_FLOW_KIT_PROJECT ?? "").trim();
13
- if (ALLOWED_PROJECTS.includes(fromEnv)) {
14
- return fromEnv;
15
- }
16
- const base = node_path_1.default.basename(projectRoot).toLowerCase();
17
- const hit = (s) => base.includes(s);
18
- if (hit("adi") || hit("adinsight"))
19
- return "ADI";
20
- if (hit("oms") || hit("operation"))
21
- return "OMS";
22
- if (hit("huan-union") || hit("hm"))
23
- return "欢盟";
24
- if (hit("ad-tools") || hit("adtools") || hit("tools"))
25
- return "AD Tools";
26
- try {
27
- const pkgText = await promises_1.default.readFile(node_path_1.default.join(projectRoot, "package.json"), "utf8");
28
- const pkg = JSON.parse(pkgText);
29
- const name = (pkg.name ?? "").toLowerCase();
30
- const has = (s) => name.includes(s);
31
- if (has("adi") || has("adinsight"))
32
- return "ADI";
33
- if (has("oms") || has("operation"))
34
- return "OMS";
35
- if (has("huan-union") || has("hm"))
36
- return "欢盟";
37
- if (has("ad-tools") || has("adtools") || has("tools"))
38
- return "AD Tools";
39
- }
40
- catch {
41
- // ignore and fallback
42
- }
43
- return "OMS";
44
- }
45
44
  async function inferAgent(projectRoot) {
46
45
  const fromEnv = (node_process_1.default.env.SDD_FLOW_KIT_AGENT ?? "").trim();
47
46
  const allowed = ["cursor", "claude-code", "codex", "openclaw"];
48
47
  if (allowed.includes(fromEnv))
49
48
  return fromEnv;
49
+ const fs = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
50
50
  const exists = async (rel) => {
51
51
  try {
52
- await promises_1.default.access(node_path_1.default.join(projectRoot, rel));
52
+ await fs.access(node_path_1.default.join(projectRoot, rel));
53
53
  return true;
54
54
  }
55
55
  catch {
@@ -75,7 +75,7 @@ async function main() {
75
75
  const projectRoot = initCwd ?? node_process_1.default.cwd();
76
76
  if (projectRoot === pkgDir)
77
77
  return;
78
- const projectKind = await inferProjectKind(projectRoot);
78
+ const projectKind = await (0, inferProject_1.inferProjectKind)(projectRoot);
79
79
  const agent = await inferAgent(projectRoot);
80
80
  // eslint-disable-next-line no-console
81
81
  console.log(`[sdd-flow-kit] postinstall (light) at: ${projectRoot}`);
@@ -89,8 +89,26 @@ async function main() {
89
89
  dryRun: false,
90
90
  projectKind,
91
91
  });
92
+ const repaired = await (0, opsxDelivery_1.repairOpsxDeliveryScripts)(projectRoot);
93
+ if (repaired) {
94
+ // eslint-disable-next-line no-console
95
+ console.log("[sdd-flow-kit] repaired package.json opsx:delivery-pipeline scripts");
96
+ }
97
+ const ready = await (0, opsxDelivery_1.isOpsxApplyReady)(projectRoot);
98
+ if (!ready) {
99
+ // eslint-disable-next-line no-console
100
+ console.warn("[sdd-flow-kit] OPSX/TDD/Playwright 工作流未就绪。/opsx-apply 前请执行: npx sdd-flow-kit ensure-opsx -y");
101
+ }
102
+ else {
103
+ // eslint-disable-next-line no-console
104
+ console.log("[sdd-flow-kit] opsx apply stack ready");
105
+ }
92
106
  // eslint-disable-next-line no-console
93
- console.log("[sdd-flow-kit] light setup done (skills/rules only). Full env: npx sdd-flow-kit install -y");
107
+ console.log("[sdd-flow-kit] light setup done (skills/rules). Full install: npx sdd-flow-kit install --project ADI -y");
108
+ if (agent === "cursor") {
109
+ // eslint-disable-next-line no-console
110
+ console.log("[sdd-flow-kit] Cursor skill: .cursor/skills/sdd-flow-kit/SKILL.md");
111
+ }
94
112
  }
95
113
  main().catch((err) => {
96
114
  // eslint-disable-next-line no-console
@@ -7,6 +7,7 @@ exports.runDoctor = runDoctor;
7
7
  const node_child_process_1 = require("node:child_process");
8
8
  const promises_1 = __importDefault(require("node:fs/promises"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
+ const opsxDelivery_1 = require("../core/opsxDelivery");
10
11
  function hasCommand(cmd) {
11
12
  const out = (0, node_child_process_1.spawnSync)("which", [cmd], { stdio: "ignore" });
12
13
  return out.status === 0;
@@ -71,6 +72,28 @@ async function runDoctor(options) {
71
72
  detail: `${expectedDir} 目录存在性`,
72
73
  });
73
74
  }
74
- const ok = checks.every((c) => c.ok);
75
+ for (const c of await (0, opsxDelivery_1.collectDeliverySetupChecks)(options.projectRoot)) {
76
+ checks.push(c);
77
+ }
78
+ let ok = checks.every((c) => c.ok);
79
+ if (!ok && options.fix) {
80
+ const ensured = await (0, opsxDelivery_1.ensureOpsxWorkflow)({
81
+ projectRoot: options.projectRoot,
82
+ agent: options.agent,
83
+ dryRun: options.dryRun,
84
+ });
85
+ checks.length = 0;
86
+ for (const c of ensured.checks) {
87
+ checks.push(c);
88
+ }
89
+ if (ensured.plannedActions.length > 0) {
90
+ checks.push({
91
+ name: "ensure-opsx-actions",
92
+ ok: ensured.ready,
93
+ detail: ensured.plannedActions.join(" | "),
94
+ });
95
+ }
96
+ ok = checks.every((c) => c.ok);
97
+ }
75
98
  return { ok, checks };
76
99
  }