@metasession.co/devaudit-cli 0.1.34 → 0.1.36

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/index.js CHANGED
@@ -55,7 +55,7 @@ function emitJsonResult(payload) {
55
55
 
56
56
  // package.json
57
57
  var package_default = {
58
- version: "0.1.34"};
58
+ version: "0.1.36"};
59
59
 
60
60
  // src/lib/version.ts
61
61
  var CLI_VERSION = package_default.version;
@@ -1047,7 +1047,7 @@ async function runAuthProbe(ctx) {
1047
1047
  const client = new DevAuditClient({ token: ctx.token, baseUrl: ctx.baseUrl });
1048
1048
  try {
1049
1049
  await client.listProjects();
1050
- return { step: "1/12 Authenticate", status: "ok", message: `PAT accepted at ${ctx.baseUrl}` };
1050
+ return { step: "1/11 Authenticate", status: "ok", message: `PAT accepted at ${ctx.baseUrl}` };
1051
1051
  } catch (err) {
1052
1052
  if (err instanceof DevAuditApiError && (err.status === 401 || err.status === 403)) {
1053
1053
  throw new Error(
@@ -1102,7 +1102,7 @@ async function detectStack(ctx) {
1102
1102
  }
1103
1103
  function ok(stack, wd) {
1104
1104
  return {
1105
- step: "2/12 Detect stack",
1105
+ step: "2/11 Detect stack",
1106
1106
  status: "ok",
1107
1107
  message: `stack=${stack} working_directory=${wd} host=railway`,
1108
1108
  data: { stack, workingDirectory: wd, host: "railway" }
@@ -1209,7 +1209,7 @@ var PYTHON_PATHS_IGNORE = [
1209
1209
  async function writeSdlcConfig(ctx, plan) {
1210
1210
  if (ctx.installMode === "developer") {
1211
1211
  return {
1212
- step: "4/12 Write sdlc-config.json",
1212
+ step: "4/11 Write sdlc-config.json",
1213
1213
  status: "skipped",
1214
1214
  message: "developer mode \u2014 leaving sdlc-config.json untouched (the team config is already on disk from the project operator). Use --force-team-config if you need to refresh wizard-owned fields."
1215
1215
  };
@@ -1263,20 +1263,20 @@ async function writeSdlcConfig(ctx, plan) {
1263
1263
  if (ctx.dryRun) {
1264
1264
  const preserved = existing ? `preserves existing customizations (${Object.keys(existing).filter((k) => !(k in wizardOwned)).length} non-wizard fields)` : "fresh config";
1265
1265
  return {
1266
- step: "4/12 Write sdlc-config.json",
1266
+ step: "4/11 Write sdlc-config.json",
1267
1267
  status: "planned",
1268
1268
  message: `would write ${outPath} (stack=${plan.stack}, slug=${plan.projectSlug}) \u2014 ${preserved}`
1269
1269
  };
1270
1270
  }
1271
1271
  await promises.writeFile(outPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1272
- return { step: "4/12 Write sdlc-config.json", status: "ok", message: `wrote ${outPath}` };
1272
+ return { step: "4/11 Write sdlc-config.json", status: "ok", message: `wrote ${outPath}` };
1273
1273
  }
1274
1274
 
1275
1275
  // src/install/project.ts
1276
1276
  async function findOrCreateProject(ctx, plan) {
1277
1277
  if (ctx.dryRun) {
1278
1278
  return {
1279
- step: "5/12 Find or create DevAudit project",
1279
+ step: "5/11 Find or create DevAudit project",
1280
1280
  status: "planned",
1281
1281
  message: `would create or find project slug='${plan.projectSlug}' on ${ctx.baseUrl}`
1282
1282
  };
@@ -1286,7 +1286,7 @@ async function findOrCreateProject(ctx, plan) {
1286
1286
  if (existing) {
1287
1287
  plan.projectId = existing.id;
1288
1288
  return {
1289
- step: "5/12 Find or create DevAudit project",
1289
+ step: "5/11 Find or create DevAudit project",
1290
1290
  status: "ok",
1291
1291
  message: `project '${plan.projectSlug}' already exists (id ${existing.id.slice(0, 8)}\u2026) \u2014 skipping creation`,
1292
1292
  data: { projectId: existing.id, created: false }
@@ -1295,7 +1295,7 @@ async function findOrCreateProject(ctx, plan) {
1295
1295
  const created = await client.createProject(plan.projectSlug, plan.projectSlug);
1296
1296
  plan.projectId = created.id;
1297
1297
  return {
1298
- step: "5/12 Find or create DevAudit project",
1298
+ step: "5/11 Find or create DevAudit project",
1299
1299
  status: "ok",
1300
1300
  message: `project '${plan.projectSlug}' created (id ${created.id.slice(0, 8)}\u2026)`,
1301
1301
  data: { projectId: created.id, created: true }
@@ -1307,14 +1307,14 @@ var KEY_NAME = "Onboarding-issued";
1307
1307
  async function issueApiKey(ctx, plan) {
1308
1308
  if (ctx.installMode === "developer") {
1309
1309
  return {
1310
- step: "6/12 Issue project API key",
1310
+ step: "6/11 Issue project API key",
1311
1311
  status: "skipped",
1312
1312
  message: "developer mode \u2014 leaving the project's 'Onboarding-issued' API key untouched (the team key is already configured by the project operator)."
1313
1313
  };
1314
1314
  }
1315
1315
  if (ctx.dryRun) {
1316
1316
  return {
1317
- step: "6/12 Issue project API key",
1317
+ step: "6/11 Issue project API key",
1318
1318
  status: "planned",
1319
1319
  message: `would issue API key named '${KEY_NAME}' on project '${plan.projectSlug}' (if not already present)`
1320
1320
  };
@@ -1327,7 +1327,7 @@ async function issueApiKey(ctx, plan) {
1327
1327
  const live = existing.find((k) => k.name === KEY_NAME && k.revoked_at === null);
1328
1328
  if (live) {
1329
1329
  return {
1330
- step: "6/12 Issue project API key",
1330
+ step: "6/11 Issue project API key",
1331
1331
  status: "warn",
1332
1332
  message: `'${KEY_NAME}' API key already exists \u2014 revoke it in the portal and re-run, or set DEVAUDIT_API_KEY manually`
1333
1333
  };
@@ -1335,7 +1335,7 @@ async function issueApiKey(ctx, plan) {
1335
1335
  const issued = await client.issueApiKey(plan.projectId, KEY_NAME);
1336
1336
  plan.apiKey = issued.plainTextKey;
1337
1337
  return {
1338
- step: "6/12 Issue project API key",
1338
+ step: "6/11 Issue project API key",
1339
1339
  status: "ok",
1340
1340
  message: `issued (will be stored as repo secret DEVAUDIT_API_KEY)`
1341
1341
  };
@@ -1361,7 +1361,7 @@ function buildSkipped(plan) {
1361
1361
  async function setGithubSecrets(ctx, plan, provider) {
1362
1362
  if (ctx.installMode === "developer") {
1363
1363
  return {
1364
- step: "7/12 Set GitHub secrets and variables",
1364
+ step: "7/11 Set GitHub secrets and variables",
1365
1365
  status: "skipped",
1366
1366
  message: "developer mode \u2014 leaving DEVAUDIT_USER_TOKEN, DEVAUDIT_API_KEY, DEVAUDIT_BASE_URL, and the production-URL secret unchanged. Use --force-team-config to rotate them as the project operator."
1367
1367
  };
@@ -1370,7 +1370,7 @@ async function setGithubSecrets(ctx, plan, provider) {
1370
1370
  if (ctx.dryRun) {
1371
1371
  const summary = operations.map((op) => `${op.kind}:${op.name}`).join(", ");
1372
1372
  return {
1373
- step: "7/12 Set GitHub secrets and variables",
1373
+ step: "7/11 Set GitHub secrets and variables",
1374
1374
  status: "planned",
1375
1375
  message: `would set ${summary} via ${provider.name} provider`
1376
1376
  };
@@ -1384,7 +1384,7 @@ async function setGithubSecrets(ctx, plan, provider) {
1384
1384
  }
1385
1385
  const skipped = buildSkipped(plan);
1386
1386
  const detail = `${operations.length} item(s) set${skipped.length > 0 ? ` (skipped: ${skipped.join("; ")})` : ""}`;
1387
- return { step: "7/12 Set GitHub secrets and variables", status: "ok", message: detail };
1387
+ return { step: "7/11 Set GitHub secrets and variables", status: "ok", message: detail };
1388
1388
  }
1389
1389
  async function commandExists(cmd) {
1390
1390
  try {
@@ -1406,7 +1406,7 @@ async function bootstrapHooks(ctx, plan) {
1406
1406
  if (ctx.dryRun) {
1407
1407
  const action = plan.stack === "python" ? "pre-commit install" : "npx husky init";
1408
1408
  return {
1409
- step: "8/12 Bootstrap hook framework",
1409
+ step: "8/11 Bootstrap hook framework",
1410
1410
  status: "planned",
1411
1411
  message: `would run \`${action}\` in ${ctx.projectPath}`
1412
1412
  };
@@ -1417,29 +1417,29 @@ async function bootstrapHooks(ctx, plan) {
1417
1417
  async function bootstrapPython(ctx) {
1418
1418
  if (!await commandExists("pre-commit")) {
1419
1419
  return {
1420
- step: "8/12 Bootstrap hook framework",
1420
+ step: "8/11 Bootstrap hook framework",
1421
1421
  status: "warn",
1422
1422
  message: "pre-commit not on PATH \u2014 run `pip install pre-commit && pre-commit install` manually"
1423
1423
  };
1424
1424
  }
1425
1425
  await execa("pre-commit", ["install"], { cwd: ctx.projectPath, stdio: "inherit" });
1426
1426
  await execa("pre-commit", ["install", "--hook-type", "commit-msg"], { cwd: ctx.projectPath, stdio: "inherit" });
1427
- return { step: "8/12 Bootstrap hook framework", status: "ok", message: "pre-commit hooks installed" };
1427
+ return { step: "8/11 Bootstrap hook framework", status: "ok", message: "pre-commit hooks installed" };
1428
1428
  }
1429
1429
  async function bootstrapNode(ctx) {
1430
1430
  const huskyDir = join(ctx.projectPath, ".husky");
1431
1431
  if (await dirExists(huskyDir)) {
1432
- return { step: "8/12 Bootstrap hook framework", status: "ok", message: ".husky/ already exists" };
1432
+ return { step: "8/11 Bootstrap hook framework", status: "ok", message: ".husky/ already exists" };
1433
1433
  }
1434
1434
  if (!await commandExists("npx")) {
1435
1435
  return {
1436
- step: "8/12 Bootstrap hook framework",
1436
+ step: "8/11 Bootstrap hook framework",
1437
1437
  status: "warn",
1438
1438
  message: "npx not on PATH \u2014 run `npx husky init` manually"
1439
1439
  };
1440
1440
  }
1441
1441
  await execa("npx", ["husky", "init"], { cwd: ctx.projectPath, stdio: "inherit" });
1442
- return { step: "8/12 Bootstrap hook framework", status: "ok", message: ".husky/ bootstrapped" };
1442
+ return { step: "8/11 Bootstrap hook framework", status: "ok", message: ".husky/ bootstrapped" };
1443
1443
  }
1444
1444
 
1445
1445
  // src/install/branch-protection.ts
@@ -1451,7 +1451,7 @@ var REQUIRED_CHECKS = [
1451
1451
  async function configureBranchProtection(ctx, provider) {
1452
1452
  if (ctx.installMode === "developer") {
1453
1453
  return {
1454
- step: "9/12 Configure branch protection",
1454
+ step: "9/11 Configure branch protection",
1455
1455
  status: "skipped",
1456
1456
  message: "developer mode \u2014 leaving branch protection unchanged. Use --force-team-config to re-apply as the project operator."
1457
1457
  };
@@ -1461,7 +1461,7 @@ async function configureBranchProtection(ctx, provider) {
1461
1461
  meta = await provider.getRepoMeta(ctx.projectPath);
1462
1462
  } catch (err) {
1463
1463
  return {
1464
- step: "9/12 Configure branch protection",
1464
+ step: "9/11 Configure branch protection",
1465
1465
  status: "warn",
1466
1466
  message: `could not resolve git repo (${err.message}) \u2014 configure manually`
1467
1467
  };
@@ -1469,7 +1469,7 @@ async function configureBranchProtection(ctx, provider) {
1469
1469
  const repo = `${meta.owner}/${meta.name}`;
1470
1470
  if (ctx.dryRun) {
1471
1471
  return {
1472
- step: "9/12 Configure branch protection",
1472
+ step: "9/11 Configure branch protection",
1473
1473
  status: "planned",
1474
1474
  message: `would apply branch protection on ${repo}:${meta.defaultBranch} with checks=${JSON.stringify(REQUIRED_CHECKS)}`
1475
1475
  };
@@ -1477,13 +1477,13 @@ async function configureBranchProtection(ctx, provider) {
1477
1477
  const result = await provider.applyBranchProtection(ctx.projectPath, meta.defaultBranch, REQUIRED_CHECKS);
1478
1478
  if (result.applied) {
1479
1479
  return {
1480
- step: "9/12 Configure branch protection",
1480
+ step: "9/11 Configure branch protection",
1481
1481
  status: "ok",
1482
1482
  message: `required checks on ${meta.defaultBranch}: ${REQUIRED_CHECKS.join(", ")}`
1483
1483
  };
1484
1484
  }
1485
1485
  return {
1486
- step: "9/12 Configure branch protection",
1486
+ step: "9/11 Configure branch protection",
1487
1487
  status: "warn",
1488
1488
  message: `${result.message ?? "branch-protection apply failed"} \u2014 configure manually`
1489
1489
  };
@@ -1787,26 +1787,37 @@ async function syncSkills(ctx) {
1787
1787
  }
1788
1788
  return { name: "Claude Code skills", filesSynced: count, message: `${count} synced to .claude/skills/` };
1789
1789
  }
1790
+ var HELPER_FILES = ["evidence.ts", "evidence-shot-core.ts"];
1790
1791
  async function syncEvidenceHelper(ctx) {
1791
1792
  if (ctx.stack !== "node") {
1792
1793
  return { name: "E2E evidence helper", filesSynced: 0, skipped: true };
1793
1794
  }
1794
- const src = join(
1795
+ const srcDir = join(
1795
1796
  ctx.installerRoot,
1796
1797
  "sdlc",
1797
1798
  "files",
1798
1799
  "_common",
1799
1800
  "skills",
1800
1801
  "e2e-test-engineer",
1801
- "references",
1802
- "evidence.ts"
1802
+ "references"
1803
1803
  );
1804
- if (!await exists(src)) {
1805
- return { name: "E2E evidence helper", filesSynced: 0, skipped: true, message: "source not found" };
1804
+ let copied = 0;
1805
+ const missing = [];
1806
+ for (const fname of HELPER_FILES) {
1807
+ const src = join(srcDir, fname);
1808
+ if (!await exists(src)) {
1809
+ missing.push(fname);
1810
+ continue;
1811
+ }
1812
+ const dst = join(ctx.projectPath, "e2e", "helpers", fname);
1813
+ await copyFile(src, dst);
1814
+ copied += 1;
1806
1815
  }
1807
- const dst = join(ctx.projectPath, "e2e", "helpers", "evidence.ts");
1808
- await copyFile(src, dst);
1809
- return { name: "E2E evidence helper", filesSynced: 1, message: "synced to e2e/helpers/evidence.ts" };
1816
+ if (copied === 0) {
1817
+ return { name: "E2E evidence helper", filesSynced: 0, skipped: true, message: "no sources found" };
1818
+ }
1819
+ const message = missing.length > 0 ? `synced ${copied} to e2e/helpers/ (missing: ${missing.join(", ")})` : `synced to e2e/helpers/ (${HELPER_FILES.join(" + ")})`;
1820
+ return { name: "E2E evidence helper", filesSynced: copied, message };
1810
1821
  }
1811
1822
 
1812
1823
  // src/lib/templates.ts
@@ -1820,14 +1831,20 @@ function substituteTokens(content, tokens) {
1820
1831
  }
1821
1832
  function substituteBlocks(content, blocks) {
1822
1833
  if (Object.keys(blocks).length === 0) return content;
1823
- Object.keys(blocks).map((k) => `{{${k}}}`);
1824
- return content.split("\n").map((line) => {
1834
+ const out = [];
1835
+ for (const line of content.split("\n")) {
1836
+ let matched = false;
1825
1837
  for (const [key, replacement] of Object.entries(blocks)) {
1826
1838
  const needle = `{{${key}}}`;
1827
- if (line.includes(needle)) return replacement;
1839
+ if (line.includes(needle)) {
1840
+ matched = true;
1841
+ if (replacement.length > 0) out.push(replacement);
1842
+ break;
1843
+ }
1828
1844
  }
1829
- return line;
1830
- }).join("\n");
1845
+ if (!matched) out.push(line);
1846
+ }
1847
+ return out.join("\n");
1831
1848
  }
1832
1849
  function stripServicesBlock(content) {
1833
1850
  const lines = content.split("\n");
@@ -2128,68 +2145,19 @@ async function syncAll(projectPaths) {
2128
2145
  async function syncTemplates(ctx) {
2129
2146
  if (ctx.dryRun) {
2130
2147
  return {
2131
- step: "10/12 Sync SDLC templates",
2148
+ step: "10/11 Sync SDLC templates",
2132
2149
  status: "planned",
2133
2150
  message: `would run native syncProject() against ${ctx.projectPath}`
2134
2151
  };
2135
2152
  }
2136
2153
  const report = await syncProject(ctx.projectPath);
2137
2154
  return {
2138
- step: "10/12 Sync SDLC templates",
2155
+ step: "10/11 Sync SDLC templates",
2139
2156
  status: "ok",
2140
2157
  message: `synced ${report.totalFilesSynced} files across ${report.sections.length} sections`,
2141
2158
  data: { totalFilesSynced: report.totalFilesSynced }
2142
2159
  };
2143
2160
  }
2144
- var STEP = "11/12 Bootstrap governance docs";
2145
- var SOURCE_REL = "sdlc/files/_common/governance";
2146
- var TARGET_REL = "compliance/governance";
2147
- async function bootstrapGovernanceDocs(ctx) {
2148
- const sourceDir = resolve(ctx.installerRoot, SOURCE_REL);
2149
- const targetDir = resolve(ctx.projectPath, TARGET_REL);
2150
- let templates = [];
2151
- try {
2152
- templates = (await readdir(sourceDir)).filter((name) => name.endsWith(".md.template")).sort();
2153
- } catch (err) {
2154
- return {
2155
- step: STEP,
2156
- status: "warn",
2157
- message: `source directory not found: ${sourceDir} (${err.message})`
2158
- };
2159
- }
2160
- if (templates.length === 0) {
2161
- return { step: STEP, status: "warn", message: `no .md.template files in ${sourceDir}` };
2162
- }
2163
- if (ctx.dryRun) {
2164
- return {
2165
- step: STEP,
2166
- status: "planned",
2167
- message: `would copy ${templates.length} starter(s) to ${TARGET_REL}/ (skip-if-exists)`,
2168
- data: { templates: [...templates] }
2169
- };
2170
- }
2171
- await ensureDir(targetDir);
2172
- const copied = [];
2173
- const skipped = [];
2174
- for (const template of templates) {
2175
- const targetName = template.replace(/\.template$/, "");
2176
- const targetPath = join(targetDir, targetName);
2177
- if (await isFile(targetPath)) {
2178
- skipped.push(targetName);
2179
- continue;
2180
- }
2181
- const body = await readFile(join(sourceDir, template), "utf8");
2182
- await writeFile(targetPath, body, "utf8");
2183
- copied.push(targetName);
2184
- }
2185
- const detail = skipped.length === 0 ? `${copied.length} starter(s) copied to ${TARGET_REL}/` : `${copied.length} copied, ${skipped.length} kept (already on disk)`;
2186
- return {
2187
- step: STEP,
2188
- status: "ok",
2189
- message: `${detail} \u2014 STARTERS, edit before production (see docs/governance-templates.md)`,
2190
- data: { copied, skipped, targetDir: TARGET_REL }
2191
- };
2192
- }
2193
2161
 
2194
2162
  // src/install/done-report.ts
2195
2163
  function doneReport(ctx, plan) {
@@ -2218,7 +2186,7 @@ function doneReport(ctx, plan) {
2218
2186
  ""
2219
2187
  ];
2220
2188
  return {
2221
- step: "12/12 Done (developer mode)",
2189
+ step: "11/11 Done (developer mode)",
2222
2190
  status: "ok",
2223
2191
  message: lines2.join("\n"),
2224
2192
  data: { mode: "developer" }
@@ -2245,7 +2213,7 @@ function doneReport(ctx, plan) {
2245
2213
  ""
2246
2214
  ];
2247
2215
  return {
2248
- step: "12/12 Done",
2216
+ step: "11/11 Done",
2249
2217
  status: "ok",
2250
2218
  message: lines.join("\n"),
2251
2219
  data: { nextBranch: branch, mode: "operator" }
@@ -2297,7 +2265,7 @@ async function runInstall(options) {
2297
2265
  steps.push(await record(log, setGithubSecrets(ctx, plan, providerResolution.provider)));
2298
2266
  } else {
2299
2267
  const skipped = {
2300
- step: "7/12 Set GitHub secrets and variables",
2268
+ step: "7/11 Set GitHub secrets and variables",
2301
2269
  status: "skipped",
2302
2270
  message: providerResolution.reason ?? "no git provider available"
2303
2271
  };
@@ -2309,7 +2277,7 @@ async function runInstall(options) {
2309
2277
  steps.push(await record(log, configureBranchProtection(ctx, providerResolution.provider)));
2310
2278
  } else {
2311
2279
  const skipped = {
2312
- step: "9/12 Configure branch protection",
2280
+ step: "9/11 Configure branch protection",
2313
2281
  status: "skipped",
2314
2282
  message: providerResolution.reason ?? "no git provider available"
2315
2283
  };
@@ -2317,7 +2285,6 @@ async function runInstall(options) {
2317
2285
  log.warn(`[${skipped.step}] SKIPPED ${skipped.message}`);
2318
2286
  }
2319
2287
  steps.push(await record(log, syncTemplates(ctx)));
2320
- steps.push(await record(log, bootstrapGovernanceDocs(ctx)));
2321
2288
  const done = doneReport(ctx, plan);
2322
2289
  steps.push(done);
2323
2290
  log.success(`[${done.step}]`);
@@ -2413,7 +2380,7 @@ async function record(log, p) {
2413
2380
  }
2414
2381
  function planSummary(plan) {
2415
2382
  return {
2416
- step: "3/12 Configure",
2383
+ step: "3/11 Configure",
2417
2384
  status: "ok",
2418
2385
  message: `slug=${plan.projectSlug} runtime=${plan.runtimeVersion}`,
2419
2386
  data: { ...plan }
@@ -2505,6 +2472,80 @@ async function runUpdate(options) {
2505
2472
  log.log("");
2506
2473
  log.warn("Do NOT auto-commit \u2014 review the changes first.");
2507
2474
  }
2475
+ var STEP = "Bootstrap governance docs";
2476
+ var SOURCE_REL = "sdlc/files/_common/governance";
2477
+ var TARGET_REL = "compliance/governance";
2478
+ async function bootstrapGovernanceDocs(ctx) {
2479
+ const sourceDir = resolve(ctx.installerRoot, SOURCE_REL);
2480
+ const targetDir = resolve(ctx.projectPath, TARGET_REL);
2481
+ let templates = [];
2482
+ try {
2483
+ templates = (await readdir(sourceDir)).filter((name) => name.endsWith(".md.template")).sort();
2484
+ } catch (err) {
2485
+ return {
2486
+ step: STEP,
2487
+ status: "warn",
2488
+ message: `source directory not found: ${sourceDir} (${err.message})`
2489
+ };
2490
+ }
2491
+ if (templates.length === 0) {
2492
+ return { step: STEP, status: "warn", message: `no .md.template files in ${sourceDir}` };
2493
+ }
2494
+ if (ctx.dryRun) {
2495
+ return {
2496
+ step: STEP,
2497
+ status: "planned",
2498
+ message: `would copy ${templates.length} starter(s) to ${TARGET_REL}/ (skip-if-exists)`,
2499
+ data: { templates: [...templates] }
2500
+ };
2501
+ }
2502
+ await ensureDir(targetDir);
2503
+ const copied = [];
2504
+ const skipped = [];
2505
+ for (const template of templates) {
2506
+ const targetName = template.replace(/\.template$/, "");
2507
+ const targetPath = join(targetDir, targetName);
2508
+ if (await isFile(targetPath)) {
2509
+ skipped.push(targetName);
2510
+ continue;
2511
+ }
2512
+ const body = await readFile(join(sourceDir, template), "utf8");
2513
+ await writeFile(targetPath, body, "utf8");
2514
+ copied.push(targetName);
2515
+ }
2516
+ const detail = skipped.length === 0 ? `${copied.length} starter(s) copied to ${TARGET_REL}/` : `${copied.length} copied, ${skipped.length} kept (already on disk)`;
2517
+ return {
2518
+ step: STEP,
2519
+ status: "ok",
2520
+ message: `${detail} \u2014 STARTERS, edit before production (see docs/governance-templates.md)`,
2521
+ data: { copied, skipped, targetDir: TARGET_REL }
2522
+ };
2523
+ }
2524
+
2525
+ // src/commands/bootstrap-governance.ts
2526
+ async function runBootstrapGovernance(opts) {
2527
+ const projectPath = resolve(opts.path ?? process.cwd());
2528
+ const installerRoot = await resolveInstallerRoot();
2529
+ const log = logger();
2530
+ const result = await bootstrapGovernanceDocs({
2531
+ projectPath,
2532
+ installerRoot,
2533
+ dryRun: Boolean(opts.dryRun)
2534
+ });
2535
+ if (isJsonMode()) {
2536
+ emitJsonResult(result);
2537
+ return;
2538
+ }
2539
+ if (result.status === "ok") {
2540
+ log.success(`[${result.step}] ${result.message ?? ""}`);
2541
+ } else if (result.status === "warn") {
2542
+ log.warn(`[${result.step}] ${result.message ?? ""}`);
2543
+ } else if (result.status === "planned") {
2544
+ log.info(`[${result.step}] (dry-run) ${result.message ?? ""}`);
2545
+ } else {
2546
+ log.log(`[${result.step}] ${result.message ?? ""}`);
2547
+ }
2548
+ }
2508
2549
 
2509
2550
  // src/commands/stub.ts
2510
2551
  function makeStub(info) {
@@ -2764,6 +2805,12 @@ async function main(argv) {
2764
2805
  });
2765
2806
  }
2766
2807
  );
2808
+ program.command("bootstrap-governance [path]").description(
2809
+ "Copy governance starter templates (ropa, dpia, ai-disclosure, periodic-review, incident-report) into compliance/governance/. Opt-in since v0.1.36 \u2014 auto-seed during install was removed because the placeholders auto-uploaded as evidence on first CI push."
2810
+ ).action(async (path, _opts, cmd) => {
2811
+ const opts = cmd?.optsWithGlobals() ?? {};
2812
+ await runBootstrapGovernance({ path, dryRun: opts.dryRun });
2813
+ });
2767
2814
  program.command("doctor").description("Verify the local install: required tools on PATH, auth state, config validity").action(runDoctor);
2768
2815
  program.command("status [path]").description("Show the consumer project's framework state").action(async (path) => {
2769
2816
  await runStatus({ path });