sdd-flow-kit 1.1.0 → 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 CHANGED
@@ -73,6 +73,25 @@ npx sdd-flow-kit doctor --agent cursor
73
73
 
74
74
  #### 使用说明
75
75
 
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`。
94
+
76
95
  ##### 脚本门禁(v1.1+,**不依赖自然语言**)
77
96
 
78
97
  `run` 会在 `openspec/PRD/<runId>/NEXT.md` 写入**必须按顺序执行的命令**。AI 与人都以 **exit code** 为准:失败则不得进入下一阶段、不得改 `src/`。
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@ const runGate_1 = require("./steps/runGate");
16
16
  const runPhaseAdvance_1 = require("./steps/runPhaseAdvance");
17
17
  const runPropose_1 = require("./steps/runPropose");
18
18
  const runDeliver_1 = require("./steps/runDeliver");
19
+ const projectRoots_1 = require("./core/projectRoots");
19
20
  const ALLOWED_AGENTS = ["cursor", "claude-code", "codex", "openclaw", "custom"];
20
21
  function getArgValue(flag) {
21
22
  const idx = process_1.default.argv.indexOf(flag);
@@ -63,7 +64,9 @@ function printHelp() {
63
64
  " npx sdd-flow-kit deliver --project-root . --run-id <runId> --change <name>",
64
65
  "",
65
66
  "可选参数:",
66
- " --projectRoot <path> 目标项目根目录(默认当前目录)",
67
+ " --projectRoot <path> 目标项目根目录(默认当前目录 = 安装根,cd 到哪装到哪)",
68
+ " 环境变量 SDD_FLOW_KIT_INSTALL_ROOT 可覆盖安装根(绝对路径)",
69
+ " 环境变量 SDD_FLOW_KIT_LEGACY_MONOREPO=1 恢复「仓库根无 package.json 时落到 frontend/」",
67
70
  " --agent <name> cursor | claude-code | codex | openclaw | custom(默认 claude-code)",
68
71
  " --branch <name> 开发分支;不存在会自动创建",
69
72
  " --project <name> OMS | ADI | 欢盟 | AD Tools(默认 OMS,用于生成 docs/*-doc-skill)",
@@ -256,6 +259,8 @@ async function main() {
256
259
  return;
257
260
  }
258
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()})`);
259
264
  const result = await (0, setupProject_1.setupProject)({
260
265
  projectRoot,
261
266
  agent,
@@ -326,6 +331,8 @@ async function main() {
326
331
  console.log(`已生成 SDD 流程产物骨架:openspec/PRD/${scaffoldRunId}/`);
327
332
  return;
328
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()})`);
329
336
  const result = await (0, runFlow_1.runFlow)({
330
337
  product,
331
338
  version,
@@ -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 checks = await collectDeliverySetupChecks(projectRoot);
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(options.projectRoot);
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(options.projectRoot, options.agent);
112
- const { cmd, args } = buildOpsxWorkflowSetupCommand(options.projectRoot, agent);
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: options.projectRoot,
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(options.projectRoot);
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(options.projectRoot);
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(projectRoot) {
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(projectRoot, "openspec")),
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(projectRoot, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md");
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(projectRoot, ".cursor", "skills", "tdd-script", "SKILL.md");
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(projectRoot, ".cursor", "skills", "openspec-apply-change", "SKILL.md");
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(projectRoot, DELIVERY_SCRIPT_REL);
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(projectRoot, "package.json"), "utf8");
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(projectRoot, ".opsx", "config.json"), "utf8");
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(projectRoot, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md"));
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(projectRoot, "package.json"), "utf8");
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(projectRoot, "AGENTS.md");
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),
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveInstallRoot = resolveInstallRoot;
7
+ exports.isLegacyMonorepoMode = isLegacyMonorepoMode;
8
+ exports.resolveOpenspecRoot = resolveOpenspecRoot;
9
+ exports.resolvePackageRoot = resolvePackageRoot;
10
+ exports.listOpenspecPrdRoots = listOpenspecPrdRoots;
11
+ const promises_1 = __importDefault(require("fs/promises"));
12
+ const path_1 = __importDefault(require("path"));
13
+ async function pathExists(filePath) {
14
+ try {
15
+ await promises_1.default.access(filePath);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ /**
23
+ * 安装根:严格等于 CLI 的 projectRoot(默认 process.cwd())。
24
+ * 可通过 SDD_FLOW_KIT_INSTALL_ROOT 覆盖(绝对路径)。
25
+ * 原则:cd 到哪,package.json / .cursor/skills / opsx-workflow 就装到哪。
26
+ */
27
+ function resolveInstallRoot(projectRoot) {
28
+ var _a;
29
+ const fromEnv = (_a = process.env.SDD_FLOW_KIT_INSTALL_ROOT) === null || _a === void 0 ? void 0 : _a.trim();
30
+ if (fromEnv)
31
+ return path_1.default.resolve(fromEnv);
32
+ return path_1.default.resolve(projectRoot);
33
+ }
34
+ /** 是否启用旧版 monorepo 行为(在仓库根执行时自动落到 frontend/) */
35
+ function isLegacyMonorepoMode() {
36
+ return process.env.SDD_FLOW_KIT_LEGACY_MONOREPO === "1" || process.env.SDD_FLOW_KIT_LEGACY_MONOREPO === "true";
37
+ }
38
+ /**
39
+ * 从 installRoot 向上查找含 openspec/ 的目录(用于解析 PRD run、openspec CLI)。
40
+ * 找不到则退回 installRoot(将在其下新建 openspec/PRD)。
41
+ */
42
+ async function resolveOpenspecRoot(installRoot) {
43
+ let cur = path_1.default.resolve(installRoot);
44
+ for (let i = 0; i < 12; i += 1) {
45
+ if (await pathExists(path_1.default.join(cur, "openspec"))) {
46
+ return cur;
47
+ }
48
+ const parent = path_1.default.dirname(cur);
49
+ if (parent === cur)
50
+ break;
51
+ cur = parent;
52
+ }
53
+ return path_1.default.resolve(installRoot);
54
+ }
55
+ /**
56
+ * 交付/门禁用的包根:默认与 installRoot 相同。
57
+ * 仅当 SDD_FLOW_KIT_LEGACY_MONOREPO=1 且 installRoot 无 package.json 但存在 frontend/package.json 时回落到 frontend/。
58
+ */
59
+ async function resolvePackageRoot(installRoot) {
60
+ const root = resolveInstallRoot(installRoot);
61
+ if (!isLegacyMonorepoMode()) {
62
+ return root;
63
+ }
64
+ const pkgAtRoot = path_1.default.join(root, "package.json");
65
+ if (await pathExists(pkgAtRoot)) {
66
+ return root;
67
+ }
68
+ const fePkg = path_1.default.join(root, "frontend", "package.json");
69
+ if (await pathExists(fePkg)) {
70
+ return path_1.default.join(root, "frontend");
71
+ }
72
+ return root;
73
+ }
74
+ /**
75
+ * 收集自 installRoot 向上所有存在的 openspec/PRD 目录(用于 resolveRun)。
76
+ */
77
+ async function listOpenspecPrdRoots(startDir) {
78
+ const seen = new Set();
79
+ const out = [];
80
+ let cur = path_1.default.resolve(startDir);
81
+ for (let i = 0; i < 12; i += 1) {
82
+ const prdRoot = path_1.default.join(cur, "openspec", "PRD");
83
+ const key = prdRoot.toLowerCase();
84
+ if (!seen.has(key) && (await pathExists(prdRoot))) {
85
+ seen.add(key);
86
+ out.push(prdRoot);
87
+ }
88
+ const parent = path_1.default.dirname(cur);
89
+ if (parent === cur)
90
+ break;
91
+ cur = parent;
92
+ }
93
+ return out;
94
+ }
@@ -7,16 +7,15 @@ exports.resolveRunDir = resolveRunDir;
7
7
  exports.loadRunSession = loadRunSession;
8
8
  const promises_1 = __importDefault(require("fs/promises"));
9
9
  const path_1 = __importDefault(require("path"));
10
+ const projectRoots_1 = require("../core/projectRoots");
10
11
  const sessionState_1 = require("../core/sessionState");
11
12
  const fs_1 = require("../core/fs");
12
13
  /**
13
- * 解析 openspec/PRD 目录:优先 --run-id,否则取最近修改的 run 目录。
14
+ * 解析 openspec/PRD 目录:从 installRoot(cwd)向上查找;优先 --run-id,否则取最近修改的 run 目录。
14
15
  */
15
16
  async function resolveRunDir(projectRoot, runId) {
16
- const candidates = [
17
- path_1.default.join(projectRoot, "openspec", "PRD"),
18
- path_1.default.join(projectRoot, "frontend", "openspec", "PRD"),
19
- ];
17
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(projectRoot);
18
+ const candidates = await (0, projectRoots_1.listOpenspecPrdRoots)(installRoot);
20
19
  for (const prdRoot of candidates) {
21
20
  try {
22
21
  const entries = await promises_1.default.readdir(prdRoot, { withFileTypes: true });
@@ -9,14 +9,8 @@ const child_process_1 = require("child_process");
9
9
  const resolveRun_1 = require("./resolveRun");
10
10
  const runGate_1 = require("./runGate");
11
11
  const opsxDelivery_1 = require("../core/opsxDelivery");
12
+ const projectRoots_1 = require("../core/projectRoots");
12
13
  const fs_1 = require("../core/fs");
13
- async function resolvePackageRoot(projectRoot) {
14
- const fe = path_1.default.join(projectRoot, "frontend", "package.json");
15
- if (await (0, opsxDelivery_1.pathExists)(fe)) {
16
- return path_1.default.join(projectRoot, "frontend");
17
- }
18
- return projectRoot;
19
- }
20
14
  /**
21
15
  * 实现阶段交付:先 gate impl-allowed,再 ensure-opsx,再跑 opsx:delivery-pipeline,写入 .delivery-pass.json。
22
16
  */
@@ -36,10 +30,11 @@ async function runDeliver(options) {
36
30
  (0, runGate_1.printGateResult)(implGate);
37
31
  return { ok: false, detail: "impl-allowed gate 未通过" };
38
32
  }
39
- const pkgRoot = await resolvePackageRoot(options.projectRoot);
33
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
34
+ const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
40
35
  const agent = options.agent || "cursor";
41
36
  if (options.dryRun) {
42
- return { ok: true, detail: `dry-run: would deliver in ${pkgRoot}` };
37
+ return { ok: true, detail: `dry-run: would deliver in ${pkgRoot} (installRoot=${installRoot})` };
43
38
  }
44
39
  const ensured = await (0, opsxDelivery_1.ensureOpsxWorkflow)({
45
40
  projectRoot: pkgRoot,
@@ -8,6 +8,7 @@ exports.printGateResult = printGateResult;
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const prdArtifacts_1 = require("../core/prdArtifacts");
10
10
  const gitDiffGuard_1 = require("../core/gitDiffGuard");
11
+ const projectRoots_1 = require("../core/projectRoots");
11
12
  const openspecGate_1 = require("../core/openspecGate");
12
13
  const resolveRun_1 = require("./resolveRun");
13
14
  const fs_1 = require("../core/fs");
@@ -22,9 +23,28 @@ async function readActiveChange(projectRoot, session, override) {
22
23
  return override;
23
24
  if (session === null || session === void 0 ? void 0 : session.activeChange)
24
25
  return session.activeChange;
25
- const rootCandidates = [projectRoot, path_1.default.join(projectRoot, "frontend")];
26
- for (const root of rootCandidates) {
27
- const text = await (0, fs_1.readFileIfExists)(path_1.default.join(root, ".opsx-active-change"));
26
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(projectRoot);
27
+ const prdRoots = await (0, projectRoots_1.listOpenspecPrdRoots)(installRoot);
28
+ const dirs = [installRoot, ...prdRoots.map((p) => path_1.default.dirname(path_1.default.dirname(p)))];
29
+ const seen = new Set();
30
+ for (const root of dirs) {
31
+ const key = path_1.default.resolve(root);
32
+ if (seen.has(key))
33
+ continue;
34
+ seen.add(key);
35
+ const text = await (0, fs_1.readFileIfExists)(path_1.default.join(key, ".opsx-active-change"));
36
+ if (text) {
37
+ const line = text
38
+ .split("\n")
39
+ .map((l) => l.trim())
40
+ .find(Boolean);
41
+ if (line)
42
+ return line;
43
+ }
44
+ }
45
+ if ((0, projectRoots_1.isLegacyMonorepoMode)()) {
46
+ const fe = path_1.default.join(installRoot, "frontend", ".opsx-active-change");
47
+ const text = await (0, fs_1.readFileIfExists)(fe);
28
48
  if (text) {
29
49
  const line = text
30
50
  .split("\n")
@@ -36,11 +56,15 @@ async function readActiveChange(projectRoot, session, override) {
36
56
  }
37
57
  return null;
38
58
  }
39
- function resolveImplRoots(projectRoot) {
40
- const roots = [path_1.default.resolve(projectRoot)];
41
- const fe = path_1.default.join(projectRoot, "frontend");
42
- if (fe !== roots[0])
43
- roots.push(path_1.default.resolve(fe));
59
+ async function resolveImplRoots(projectRoot) {
60
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(projectRoot);
61
+ const roots = [installRoot];
62
+ if ((0, projectRoots_1.isLegacyMonorepoMode)()) {
63
+ const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
64
+ if (pkgRoot !== installRoot && !roots.includes(pkgRoot)) {
65
+ roots.push(pkgRoot);
66
+ }
67
+ }
44
68
  return roots;
45
69
  }
46
70
  /**
@@ -56,7 +80,7 @@ async function runGate(options) {
56
80
  const { resolved, session } = loaded;
57
81
  const outputRoot = resolved.outputRoot;
58
82
  const runId = resolved.runId;
59
- const implRoots = resolveImplRoots(options.projectRoot);
83
+ const implRoots = await resolveImplRoots(options.projectRoot);
60
84
  if (options.expect === "prd-fetched") {
61
85
  const prd = await (0, prdArtifacts_1.isPrdFetched)(outputRoot);
62
86
  checks.push({ name: "prd-fetched", ...prd });
@@ -115,10 +139,19 @@ async function runGate(options) {
115
139
  checks.push({ name: "active-change", ok: false, detail: "未指定 change(--change 或 .opsx-active-change)" });
116
140
  return fail(checks, options.expect, runId);
117
141
  }
118
- const roots = [options.projectRoot, path_1.default.join(options.projectRoot, "frontend")];
142
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
143
+ const openspecRoots = [
144
+ installRoot,
145
+ ...(await (0, projectRoots_1.listOpenspecPrdRoots)(installRoot)).map((p) => path_1.default.dirname(path_1.default.dirname(p))),
146
+ ];
147
+ const seen = new Set();
119
148
  let found = false;
120
- for (const root of roots) {
121
- if (await (0, openspecGate_1.changeDirExists)(root, changeName)) {
149
+ for (const root of openspecRoots) {
150
+ const key = path_1.default.resolve(root);
151
+ if (seen.has(key))
152
+ continue;
153
+ seen.add(key);
154
+ if (await (0, openspecGate_1.changeDirExists)(key, changeName)) {
122
155
  found = true;
123
156
  const st = (0, openspecGate_1.checkOpenspecApplyReady)(root, changeName);
124
157
  checks.push({ name: "openspec-apply-ready", ok: st.applyReady, detail: st.detail });
@@ -156,14 +189,32 @@ async function runGate(options) {
156
189
  checks.push({ name: "active-change", ok: false, detail: "未绑定 activeChange" });
157
190
  return fail(checks, options.expect, runId);
158
191
  }
159
- const roots = [options.projectRoot, path_1.default.join(options.projectRoot, "frontend")];
160
- for (const root of roots) {
161
- if (await (0, openspecGate_1.changeDirExists)(root, changeName)) {
162
- const st = (0, openspecGate_1.checkOpenspecApplyReady)(root, changeName);
192
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
193
+ const openspecRoots = [
194
+ installRoot,
195
+ ...(await (0, projectRoots_1.listOpenspecPrdRoots)(installRoot)).map((p) => path_1.default.dirname(path_1.default.dirname(p))),
196
+ ];
197
+ const seen = new Set();
198
+ let found = false;
199
+ for (const root of openspecRoots) {
200
+ const key = path_1.default.resolve(root);
201
+ if (seen.has(key))
202
+ continue;
203
+ seen.add(key);
204
+ if (await (0, openspecGate_1.changeDirExists)(key, changeName)) {
205
+ found = true;
206
+ const st = (0, openspecGate_1.checkOpenspecApplyReady)(key, changeName);
163
207
  checks.push({ name: "openspec-apply-ready", ok: st.applyReady, detail: st.detail });
164
208
  break;
165
209
  }
166
210
  }
211
+ if (!found) {
212
+ checks.push({
213
+ name: "change-dir",
214
+ ok: false,
215
+ detail: `openspec/changes/${changeName} 不存在`,
216
+ });
217
+ }
167
218
  return pass(checks, options.expect, runId);
168
219
  }
169
220
  if (options.expect === "shipped") {
@@ -172,10 +223,19 @@ async function runGate(options) {
172
223
  checks.push({ name: "active-change", ok: false, detail: "未指定 change" });
173
224
  return fail(checks, options.expect, runId);
174
225
  }
175
- const roots = [options.projectRoot, path_1.default.join(options.projectRoot, "frontend")];
176
- for (const root of roots) {
177
- if (await (0, openspecGate_1.changeDirExists)(root, changeName)) {
178
- const tasks = await (0, openspecGate_1.checkTasksAllDone)(root, changeName);
226
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
227
+ const openspecRoots = [
228
+ installRoot,
229
+ ...(await (0, projectRoots_1.listOpenspecPrdRoots)(installRoot)).map((p) => path_1.default.dirname(path_1.default.dirname(p))),
230
+ ];
231
+ const seen = new Set();
232
+ for (const root of openspecRoots) {
233
+ const key = path_1.default.resolve(root);
234
+ if (seen.has(key))
235
+ continue;
236
+ seen.add(key);
237
+ if (await (0, openspecGate_1.changeDirExists)(key, changeName)) {
238
+ const tasks = await (0, openspecGate_1.checkTasksAllDone)(key, changeName);
179
239
  checks.push({ name: "tasks-done", ...tasks });
180
240
  const marker = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, ".delivery-pass.json"));
181
241
  if (marker) {
@@ -7,6 +7,7 @@ exports.runPhaseAdvance = runPhaseAdvance;
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const runGate_1 = require("./runGate");
9
9
  const resolveRun_1 = require("./resolveRun");
10
+ const projectRoots_1 = require("../core/projectRoots");
10
11
  const sessionState_1 = require("../core/sessionState");
11
12
  const fs_1 = require("../core/fs");
12
13
  const gitDiffGuard_1 = require("../core/gitDiffGuard");
@@ -112,9 +113,8 @@ async function runPhaseAdvance(options) {
112
113
  };
113
114
  if (!options.dryRun) {
114
115
  await (0, sessionState_1.writeSession)(outputRoot, next);
115
- await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(options.projectRoot, ".opsx-active-change"), `${changeName}\n`);
116
- const fe = path_1.default.join(options.projectRoot, "frontend", ".opsx-active-change");
117
- await (0, fs_1.writeTextFileEnsuredDir)(fe, `${changeName}\n`).catch(() => undefined);
116
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
117
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(installRoot, ".opsx-active-change"), `${changeName}\n`);
118
118
  await writeNextMd(outputRoot, [
119
119
  "## 当前:阶段 C 完成 → 进入 D(实现)",
120
120
  "```bash",
@@ -139,9 +139,8 @@ async function runPhaseAdvance(options) {
139
139
  (0, runGate_1.printGateResult)(gate);
140
140
  return { ok: false, runId, message: "propose 未就绪,禁止进入实现" };
141
141
  }
142
- const gitRoot = (0, gitDiffGuard_1.resolveGitRoot)(path_1.default.join(options.projectRoot, "frontend") === path_1.default.join(options.projectRoot)
143
- ? options.projectRoot
144
- : path_1.default.join(options.projectRoot, "frontend"));
142
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
143
+ const gitRoot = (0, gitDiffGuard_1.resolveGitRoot)(installRoot);
145
144
  const head = (0, gitDiffGuard_1.gitDiffNameOnly)(gitRoot).head;
146
145
  const next = {
147
146
  ...session,
@@ -9,7 +9,7 @@ const child_process_1 = require("child_process");
9
9
  const resolveRun_1 = require("./resolveRun");
10
10
  const runGate_1 = require("./runGate");
11
11
  const fs_1 = require("../core/fs");
12
- const opsxDelivery_1 = require("../core/opsxDelivery");
12
+ const projectRoots_1 = require("../core/projectRoots");
13
13
  /**
14
14
  * 创建/校验 openspec change,并检查 apply-ready(不代替 AI 写 proposal 正文,但强制 gate)。
15
15
  */
@@ -27,14 +27,8 @@ async function runPropose(options) {
27
27
  (0, runGate_1.printGateResult)(docsGate);
28
28
  return { ok: false, detail: "docs-closed gate 未通过,禁止 propose" };
29
29
  }
30
- const roots = [options.projectRoot, path_1.default.join(options.projectRoot, "frontend")];
31
- let cwd = options.projectRoot;
32
- for (const r of roots) {
33
- if (await (0, opsxDelivery_1.pathExists)(path_1.default.join(r, "openspec"))) {
34
- cwd = r;
35
- break;
36
- }
37
- }
30
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
31
+ const cwd = await (0, projectRoots_1.resolveOpenspecRoot)(installRoot);
38
32
  if (options.dryRun) {
39
33
  return { ok: true, detail: `dry-run: would openspec new change ${options.change}` };
40
34
  }
@@ -11,6 +11,7 @@ const fs_1 = require("../core/fs");
11
11
  const readlinePrompt_1 = require("../core/readlinePrompt");
12
12
  const utils_1 = require("../core/utils");
13
13
  const opsxDelivery_1 = require("../core/opsxDelivery");
14
+ const projectRoots_1 = require("../core/projectRoots");
14
15
  const templates_1 = require("../core/templates");
15
16
  const EDITOR_CHOICES = ["claude-code", "cursor", "codex", "openclaw"];
16
17
  const PROJECT_CHOICES = ["OMS", "ADI", "欢盟", "AD Tools"];
@@ -219,14 +220,15 @@ async function installOpsxConfig(projectRoot, agent) {
219
220
  }
220
221
  async function setupProject(options) {
221
222
  var _a, _b, _c;
223
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
222
224
  const defaultAgent = (_a = options.agent) !== null && _a !== void 0 ? _a : "claude-code";
223
225
  const selectedAgent = options.yes ? defaultAgent : await askAgent(defaultAgent);
224
226
  const defaultKind = (_b = options.projectKind) !== null && _b !== void 0 ? _b : "OMS";
225
227
  const selectedKind = options.yes ? defaultKind : await askProjectKind(defaultKind);
226
- const defaultBranch = (_c = options.branch) !== null && _c !== void 0 ? _c : pickDefaultBranch(options.projectRoot);
228
+ const defaultBranch = (_c = options.branch) !== null && _c !== void 0 ? _c : pickDefaultBranch(installRoot);
227
229
  const selectedBranch = options.yes ? defaultBranch : await askBranch(defaultBranch);
228
- const plannedActions = [];
229
- const branches = listLocalBranches(options.projectRoot);
230
+ const plannedActions = [`installRoot=${installRoot}`];
231
+ const branches = listLocalBranches(installRoot);
230
232
  const branchExists = branches.includes(selectedBranch);
231
233
  let branchResult;
232
234
  if (options.light) {
@@ -240,9 +242,9 @@ async function setupProject(options) {
240
242
  : `would create and checkout branch: ${selectedBranch}`);
241
243
  }
242
244
  else {
243
- branchResult = ensureBranch(options.projectRoot, selectedBranch);
245
+ branchResult = ensureBranch(installRoot, selectedBranch);
244
246
  }
245
- let environmentReady = await (0, opsxDelivery_1.isOpsxApplyReady)(options.projectRoot);
247
+ let environmentReady = await (0, opsxDelivery_1.isOpsxApplyReady)(installRoot);
246
248
  let setupExecuted = false;
247
249
  let skillInstalled = false;
248
250
  let docsSkillInstalled = false;
@@ -255,7 +257,7 @@ async function setupProject(options) {
255
257
  }
256
258
  else if (!environmentReady) {
257
259
  const ensured = await (0, opsxDelivery_1.ensureOpsxWorkflow)({
258
- projectRoot: options.projectRoot,
260
+ projectRoot: installRoot,
259
261
  agent: selectedAgent,
260
262
  dryRun: options.dryRun,
261
263
  });
@@ -267,23 +269,24 @@ async function setupProject(options) {
267
269
  plannedActions.push("opsx apply stack already ready");
268
270
  }
269
271
  if (options.dryRun) {
270
- for (const skillPath of allSkillInstallPaths(options.projectRoot)) {
272
+ for (const skillPath of allSkillInstallPaths(installRoot)) {
271
273
  plannedActions.push(`would install skill: ${skillPath}`);
272
274
  }
273
275
  const { dir } = docSkillDirName(selectedKind);
274
- plannedActions.push(`would install docs skill: ${path_1.default.join(options.projectRoot, "docs", dir)}`);
275
- plannedActions.push(`would install opsx config: ${path_1.default.join(options.projectRoot, ".opsx", "config.json")}`);
276
- plannedActions.push(`would install cursor rule: ${path_1.default.join(options.projectRoot, ".cursor", "rules", "sdd-flow-kit-gates.mdc")}`);
276
+ plannedActions.push(`would install docs skill: ${path_1.default.join(installRoot, "docs", dir)}`);
277
+ plannedActions.push(`would install opsx config: ${path_1.default.join(installRoot, ".opsx", "config.json")}`);
278
+ plannedActions.push(`would install cursor rule: ${path_1.default.join(installRoot, ".cursor", "rules", "sdd-flow-kit-gates.mdc")}`);
277
279
  }
278
280
  else {
279
- await installAgentSkill(options.projectRoot);
280
- await installSddCursorRule(options.projectRoot);
281
+ await installAgentSkill(installRoot);
282
+ await installSddCursorRule(installRoot);
281
283
  skillInstalled = true;
282
- await installDocsSkill(options.projectRoot, selectedKind);
284
+ await installDocsSkill(installRoot, selectedKind);
283
285
  docsSkillInstalled = true;
284
- await installOpsxConfig(options.projectRoot, selectedAgent);
286
+ await installOpsxConfig(installRoot, selectedAgent);
285
287
  opsxConfigInstalled = true;
286
- const repaired = await (0, opsxDelivery_1.repairOpsxDeliveryScripts)(options.projectRoot);
288
+ const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
289
+ const repaired = await (0, opsxDelivery_1.repairOpsxDeliveryScripts)(pkgRoot);
287
290
  if (repaired) {
288
291
  plannedActions.push("repaired package.json opsx:delivery-pipeline scripts (poquan → opsx)");
289
292
  }
@@ -307,7 +310,7 @@ async function setupProject(options) {
307
310
  ...plannedActions.map((x) => `- ${x}`),
308
311
  "",
309
312
  ].join("\n");
310
- await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(options.projectRoot, ".sdd-flow-kit", "setup-summary.md"), summary);
313
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(installRoot, ".sdd-flow-kit", "setup-summary.md"), summary);
311
314
  return {
312
315
  agent: selectedAgent,
313
316
  projectKind: selectedKind,
@@ -11,6 +11,7 @@ const fs_1 = require("../core/fs");
11
11
  const templates_1 = require("../core/templates");
12
12
  const inferProject_1 = require("../core/inferProject");
13
13
  const sessionState_1 = require("../core/sessionState");
14
+ const projectRoots_1 = require("../core/projectRoots");
14
15
  const writeNext_1 = require("../core/writeNext");
15
16
  function renderTemplate(template, vars) {
16
17
  let out = template;
@@ -22,7 +23,8 @@ function renderTemplate(template, vars) {
22
23
  async function startFlowScaffold(options) {
23
24
  const now = new Date();
24
25
  const runId = `${(0, utils_1.formatYYYYMMDD)(now)}-${options.product}-${options.version}`;
25
- const outputRoot = path_1.default.join(options.projectRoot, "openspec", "PRD", runId);
26
+ const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
27
+ const outputRoot = path_1.default.join(installRoot, "openspec", "PRD", runId);
26
28
  const sourceDir = path_1.default.join(outputRoot, "source");
27
29
  await promises_1.default.mkdir(sourceDir, { recursive: true });
28
30
  // 1) Step1: doc-skill 指引(按产品解析脚本路径,支持 monorepo frontend)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-flow-kit",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "private": false,
5
5
  "description": "Cross-agent SDD automated development workflow kit (ADI/ADI-like).",
6
6
  "license": "MIT",