@metasession.co/devaudit-cli 0.1.29 → 0.1.31

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
@@ -8,6 +8,7 @@ import envPaths from 'env-paths';
8
8
  import { fileURLToPath, pathToFileURL } from 'url';
9
9
  import { validateManifest } from '@metasession.co/devaudit-plugin-sdk';
10
10
  import * as clack2 from '@clack/prompts';
11
+ import { readdir, readFile, writeFile } from 'fs/promises';
11
12
 
12
13
  var DEFAULT_OPTIONS = { json: false, verbose: false, noColor: false };
13
14
  var currentLogger = build(DEFAULT_OPTIONS);
@@ -54,7 +55,7 @@ function emitJsonResult(payload) {
54
55
 
55
56
  // package.json
56
57
  var package_default = {
57
- version: "0.1.29"};
58
+ version: "0.1.31"};
58
59
 
59
60
  // src/lib/version.ts
60
61
  var CLI_VERSION = package_default.version;
@@ -1046,7 +1047,7 @@ async function runAuthProbe(ctx) {
1046
1047
  const client = new DevAuditClient({ token: ctx.token, baseUrl: ctx.baseUrl });
1047
1048
  try {
1048
1049
  await client.listProjects();
1049
- return { step: "1/11 Authenticate", status: "ok", message: `PAT accepted at ${ctx.baseUrl}` };
1050
+ return { step: "1/12 Authenticate", status: "ok", message: `PAT accepted at ${ctx.baseUrl}` };
1050
1051
  } catch (err) {
1051
1052
  if (err instanceof DevAuditApiError && (err.status === 401 || err.status === 403)) {
1052
1053
  throw new Error(
@@ -1101,7 +1102,7 @@ async function detectStack(ctx) {
1101
1102
  }
1102
1103
  function ok(stack, wd) {
1103
1104
  return {
1104
- step: "2/11 Detect stack",
1105
+ step: "2/12 Detect stack",
1105
1106
  status: "ok",
1106
1107
  message: `stack=${stack} working_directory=${wd} host=railway`,
1107
1108
  data: { stack, workingDirectory: wd, host: "railway" }
@@ -1208,7 +1209,7 @@ var PYTHON_PATHS_IGNORE = [
1208
1209
  async function writeSdlcConfig(ctx, plan) {
1209
1210
  if (ctx.installMode === "developer") {
1210
1211
  return {
1211
- step: "4/11 Write sdlc-config.json",
1212
+ step: "4/12 Write sdlc-config.json",
1212
1213
  status: "skipped",
1213
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."
1214
1215
  };
@@ -1262,20 +1263,20 @@ async function writeSdlcConfig(ctx, plan) {
1262
1263
  if (ctx.dryRun) {
1263
1264
  const preserved = existing ? `preserves existing customizations (${Object.keys(existing).filter((k) => !(k in wizardOwned)).length} non-wizard fields)` : "fresh config";
1264
1265
  return {
1265
- step: "4/11 Write sdlc-config.json",
1266
+ step: "4/12 Write sdlc-config.json",
1266
1267
  status: "planned",
1267
1268
  message: `would write ${outPath} (stack=${plan.stack}, slug=${plan.projectSlug}) \u2014 ${preserved}`
1268
1269
  };
1269
1270
  }
1270
1271
  await promises.writeFile(outPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1271
- return { step: "4/11 Write sdlc-config.json", status: "ok", message: `wrote ${outPath}` };
1272
+ return { step: "4/12 Write sdlc-config.json", status: "ok", message: `wrote ${outPath}` };
1272
1273
  }
1273
1274
 
1274
1275
  // src/install/project.ts
1275
1276
  async function findOrCreateProject(ctx, plan) {
1276
1277
  if (ctx.dryRun) {
1277
1278
  return {
1278
- step: "5/11 Find or create DevAudit project",
1279
+ step: "5/12 Find or create DevAudit project",
1279
1280
  status: "planned",
1280
1281
  message: `would create or find project slug='${plan.projectSlug}' on ${ctx.baseUrl}`
1281
1282
  };
@@ -1285,7 +1286,7 @@ async function findOrCreateProject(ctx, plan) {
1285
1286
  if (existing) {
1286
1287
  plan.projectId = existing.id;
1287
1288
  return {
1288
- step: "5/11 Find or create DevAudit project",
1289
+ step: "5/12 Find or create DevAudit project",
1289
1290
  status: "ok",
1290
1291
  message: `project '${plan.projectSlug}' already exists (id ${existing.id.slice(0, 8)}\u2026) \u2014 skipping creation`,
1291
1292
  data: { projectId: existing.id, created: false }
@@ -1294,7 +1295,7 @@ async function findOrCreateProject(ctx, plan) {
1294
1295
  const created = await client.createProject(plan.projectSlug, plan.projectSlug);
1295
1296
  plan.projectId = created.id;
1296
1297
  return {
1297
- step: "5/11 Find or create DevAudit project",
1298
+ step: "5/12 Find or create DevAudit project",
1298
1299
  status: "ok",
1299
1300
  message: `project '${plan.projectSlug}' created (id ${created.id.slice(0, 8)}\u2026)`,
1300
1301
  data: { projectId: created.id, created: true }
@@ -1306,14 +1307,14 @@ var KEY_NAME = "Onboarding-issued";
1306
1307
  async function issueApiKey(ctx, plan) {
1307
1308
  if (ctx.installMode === "developer") {
1308
1309
  return {
1309
- step: "6/11 Issue project API key",
1310
+ step: "6/12 Issue project API key",
1310
1311
  status: "skipped",
1311
1312
  message: "developer mode \u2014 leaving the project's 'Onboarding-issued' API key untouched (the team key is already configured by the project operator)."
1312
1313
  };
1313
1314
  }
1314
1315
  if (ctx.dryRun) {
1315
1316
  return {
1316
- step: "6/11 Issue project API key",
1317
+ step: "6/12 Issue project API key",
1317
1318
  status: "planned",
1318
1319
  message: `would issue API key named '${KEY_NAME}' on project '${plan.projectSlug}' (if not already present)`
1319
1320
  };
@@ -1326,7 +1327,7 @@ async function issueApiKey(ctx, plan) {
1326
1327
  const live = existing.find((k) => k.name === KEY_NAME && k.revoked_at === null);
1327
1328
  if (live) {
1328
1329
  return {
1329
- step: "6/11 Issue project API key",
1330
+ step: "6/12 Issue project API key",
1330
1331
  status: "warn",
1331
1332
  message: `'${KEY_NAME}' API key already exists \u2014 revoke it in the portal and re-run, or set DEVAUDIT_API_KEY manually`
1332
1333
  };
@@ -1334,7 +1335,7 @@ async function issueApiKey(ctx, plan) {
1334
1335
  const issued = await client.issueApiKey(plan.projectId, KEY_NAME);
1335
1336
  plan.apiKey = issued.plainTextKey;
1336
1337
  return {
1337
- step: "6/11 Issue project API key",
1338
+ step: "6/12 Issue project API key",
1338
1339
  status: "ok",
1339
1340
  message: `issued (will be stored as repo secret DEVAUDIT_API_KEY)`
1340
1341
  };
@@ -1360,7 +1361,7 @@ function buildSkipped(plan) {
1360
1361
  async function setGithubSecrets(ctx, plan, provider) {
1361
1362
  if (ctx.installMode === "developer") {
1362
1363
  return {
1363
- step: "7/11 Set GitHub secrets and variables",
1364
+ step: "7/12 Set GitHub secrets and variables",
1364
1365
  status: "skipped",
1365
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."
1366
1367
  };
@@ -1369,7 +1370,7 @@ async function setGithubSecrets(ctx, plan, provider) {
1369
1370
  if (ctx.dryRun) {
1370
1371
  const summary = operations.map((op) => `${op.kind}:${op.name}`).join(", ");
1371
1372
  return {
1372
- step: "7/11 Set GitHub secrets and variables",
1373
+ step: "7/12 Set GitHub secrets and variables",
1373
1374
  status: "planned",
1374
1375
  message: `would set ${summary} via ${provider.name} provider`
1375
1376
  };
@@ -1383,7 +1384,7 @@ async function setGithubSecrets(ctx, plan, provider) {
1383
1384
  }
1384
1385
  const skipped = buildSkipped(plan);
1385
1386
  const detail = `${operations.length} item(s) set${skipped.length > 0 ? ` (skipped: ${skipped.join("; ")})` : ""}`;
1386
- return { step: "7/11 Set GitHub secrets and variables", status: "ok", message: detail };
1387
+ return { step: "7/12 Set GitHub secrets and variables", status: "ok", message: detail };
1387
1388
  }
1388
1389
  async function commandExists(cmd) {
1389
1390
  try {
@@ -1405,7 +1406,7 @@ async function bootstrapHooks(ctx, plan) {
1405
1406
  if (ctx.dryRun) {
1406
1407
  const action = plan.stack === "python" ? "pre-commit install" : "npx husky init";
1407
1408
  return {
1408
- step: "8/11 Bootstrap hook framework",
1409
+ step: "8/12 Bootstrap hook framework",
1409
1410
  status: "planned",
1410
1411
  message: `would run \`${action}\` in ${ctx.projectPath}`
1411
1412
  };
@@ -1416,29 +1417,29 @@ async function bootstrapHooks(ctx, plan) {
1416
1417
  async function bootstrapPython(ctx) {
1417
1418
  if (!await commandExists("pre-commit")) {
1418
1419
  return {
1419
- step: "8/11 Bootstrap hook framework",
1420
+ step: "8/12 Bootstrap hook framework",
1420
1421
  status: "warn",
1421
1422
  message: "pre-commit not on PATH \u2014 run `pip install pre-commit && pre-commit install` manually"
1422
1423
  };
1423
1424
  }
1424
1425
  await execa("pre-commit", ["install"], { cwd: ctx.projectPath, stdio: "inherit" });
1425
1426
  await execa("pre-commit", ["install", "--hook-type", "commit-msg"], { cwd: ctx.projectPath, stdio: "inherit" });
1426
- return { step: "8/11 Bootstrap hook framework", status: "ok", message: "pre-commit hooks installed" };
1427
+ return { step: "8/12 Bootstrap hook framework", status: "ok", message: "pre-commit hooks installed" };
1427
1428
  }
1428
1429
  async function bootstrapNode(ctx) {
1429
1430
  const huskyDir = join(ctx.projectPath, ".husky");
1430
1431
  if (await dirExists(huskyDir)) {
1431
- return { step: "8/11 Bootstrap hook framework", status: "ok", message: ".husky/ already exists" };
1432
+ return { step: "8/12 Bootstrap hook framework", status: "ok", message: ".husky/ already exists" };
1432
1433
  }
1433
1434
  if (!await commandExists("npx")) {
1434
1435
  return {
1435
- step: "8/11 Bootstrap hook framework",
1436
+ step: "8/12 Bootstrap hook framework",
1436
1437
  status: "warn",
1437
1438
  message: "npx not on PATH \u2014 run `npx husky init` manually"
1438
1439
  };
1439
1440
  }
1440
1441
  await execa("npx", ["husky", "init"], { cwd: ctx.projectPath, stdio: "inherit" });
1441
- return { step: "8/11 Bootstrap hook framework", status: "ok", message: ".husky/ bootstrapped" };
1442
+ return { step: "8/12 Bootstrap hook framework", status: "ok", message: ".husky/ bootstrapped" };
1442
1443
  }
1443
1444
 
1444
1445
  // src/install/branch-protection.ts
@@ -1450,7 +1451,7 @@ var REQUIRED_CHECKS = [
1450
1451
  async function configureBranchProtection(ctx, provider) {
1451
1452
  if (ctx.installMode === "developer") {
1452
1453
  return {
1453
- step: "9/11 Configure branch protection",
1454
+ step: "9/12 Configure branch protection",
1454
1455
  status: "skipped",
1455
1456
  message: "developer mode \u2014 leaving branch protection unchanged. Use --force-team-config to re-apply as the project operator."
1456
1457
  };
@@ -1460,7 +1461,7 @@ async function configureBranchProtection(ctx, provider) {
1460
1461
  meta = await provider.getRepoMeta(ctx.projectPath);
1461
1462
  } catch (err) {
1462
1463
  return {
1463
- step: "9/11 Configure branch protection",
1464
+ step: "9/12 Configure branch protection",
1464
1465
  status: "warn",
1465
1466
  message: `could not resolve git repo (${err.message}) \u2014 configure manually`
1466
1467
  };
@@ -1468,7 +1469,7 @@ async function configureBranchProtection(ctx, provider) {
1468
1469
  const repo = `${meta.owner}/${meta.name}`;
1469
1470
  if (ctx.dryRun) {
1470
1471
  return {
1471
- step: "9/11 Configure branch protection",
1472
+ step: "9/12 Configure branch protection",
1472
1473
  status: "planned",
1473
1474
  message: `would apply branch protection on ${repo}:${meta.defaultBranch} with checks=${JSON.stringify(REQUIRED_CHECKS)}`
1474
1475
  };
@@ -1476,13 +1477,13 @@ async function configureBranchProtection(ctx, provider) {
1476
1477
  const result = await provider.applyBranchProtection(ctx.projectPath, meta.defaultBranch, REQUIRED_CHECKS);
1477
1478
  if (result.applied) {
1478
1479
  return {
1479
- step: "9/11 Configure branch protection",
1480
+ step: "9/12 Configure branch protection",
1480
1481
  status: "ok",
1481
1482
  message: `required checks on ${meta.defaultBranch}: ${REQUIRED_CHECKS.join(", ")}`
1482
1483
  };
1483
1484
  }
1484
1485
  return {
1485
- step: "9/11 Configure branch protection",
1486
+ step: "9/12 Configure branch protection",
1486
1487
  status: "warn",
1487
1488
  message: `${result.message ?? "branch-protection apply failed"} \u2014 configure manually`
1488
1489
  };
@@ -1857,7 +1858,13 @@ var CI_TEMPLATES = [
1857
1858
  "check-release-approval.yml.template",
1858
1859
  "post-deploy-prod.yml.template",
1859
1860
  "compliance-evidence.yml.template",
1860
- "close-out-release.yml.template"
1861
+ "close-out-release.yml.template",
1862
+ // DevAudit-Installer#98 WS3: quarterly cron → auto-PR with the
1863
+ // periodic-review.md regenerated from local stats.
1864
+ "periodic-review.yml.template",
1865
+ // DevAudit-Installer#98 WS4: fires on `label:incident` issue close →
1866
+ // auto-PR with the issue exported to compliance/governance/.
1867
+ "incident-export.yml.template"
1861
1868
  ];
1862
1869
  var OLD_WORKFLOWS_TO_REMOVE = ["test-on-pr.yml", "check-uat-approval.yml"];
1863
1870
  function indentEnvBlock(env, indent) {
@@ -2121,19 +2128,68 @@ async function syncAll(projectPaths) {
2121
2128
  async function syncTemplates(ctx) {
2122
2129
  if (ctx.dryRun) {
2123
2130
  return {
2124
- step: "10/11 Sync SDLC templates",
2131
+ step: "10/12 Sync SDLC templates",
2125
2132
  status: "planned",
2126
2133
  message: `would run native syncProject() against ${ctx.projectPath}`
2127
2134
  };
2128
2135
  }
2129
2136
  const report = await syncProject(ctx.projectPath);
2130
2137
  return {
2131
- step: "10/11 Sync SDLC templates",
2138
+ step: "10/12 Sync SDLC templates",
2132
2139
  status: "ok",
2133
2140
  message: `synced ${report.totalFilesSynced} files across ${report.sections.length} sections`,
2134
2141
  data: { totalFilesSynced: report.totalFilesSynced }
2135
2142
  };
2136
2143
  }
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
+ }
2137
2193
 
2138
2194
  // src/install/done-report.ts
2139
2195
  function doneReport(ctx, plan) {
@@ -2162,7 +2218,7 @@ function doneReport(ctx, plan) {
2162
2218
  ""
2163
2219
  ];
2164
2220
  return {
2165
- step: "11/11 Done (developer mode)",
2221
+ step: "12/12 Done (developer mode)",
2166
2222
  status: "ok",
2167
2223
  message: lines2.join("\n"),
2168
2224
  data: { mode: "developer" }
@@ -2189,7 +2245,7 @@ function doneReport(ctx, plan) {
2189
2245
  ""
2190
2246
  ];
2191
2247
  return {
2192
- step: "11/11 Done",
2248
+ step: "12/12 Done",
2193
2249
  status: "ok",
2194
2250
  message: lines.join("\n"),
2195
2251
  data: { nextBranch: branch, mode: "operator" }
@@ -2241,7 +2297,7 @@ async function runInstall(options) {
2241
2297
  steps.push(await record(log, setGithubSecrets(ctx, plan, providerResolution.provider)));
2242
2298
  } else {
2243
2299
  const skipped = {
2244
- step: "7/11 Set GitHub secrets and variables",
2300
+ step: "7/12 Set GitHub secrets and variables",
2245
2301
  status: "skipped",
2246
2302
  message: providerResolution.reason ?? "no git provider available"
2247
2303
  };
@@ -2253,7 +2309,7 @@ async function runInstall(options) {
2253
2309
  steps.push(await record(log, configureBranchProtection(ctx, providerResolution.provider)));
2254
2310
  } else {
2255
2311
  const skipped = {
2256
- step: "9/11 Configure branch protection",
2312
+ step: "9/12 Configure branch protection",
2257
2313
  status: "skipped",
2258
2314
  message: providerResolution.reason ?? "no git provider available"
2259
2315
  };
@@ -2261,6 +2317,7 @@ async function runInstall(options) {
2261
2317
  log.warn(`[${skipped.step}] SKIPPED ${skipped.message}`);
2262
2318
  }
2263
2319
  steps.push(await record(log, syncTemplates(ctx)));
2320
+ steps.push(await record(log, bootstrapGovernanceDocs(ctx)));
2264
2321
  const done = doneReport(ctx, plan);
2265
2322
  steps.push(done);
2266
2323
  log.success(`[${done.step}]`);
@@ -2356,7 +2413,7 @@ async function record(log, p) {
2356
2413
  }
2357
2414
  function planSummary(plan) {
2358
2415
  return {
2359
- step: "3/11 Configure",
2416
+ step: "3/12 Configure",
2360
2417
  status: "ok",
2361
2418
  message: `slug=${plan.projectSlug} runtime=${plan.runtimeVersion}`,
2362
2419
  data: { ...plan }