@metasession.co/devaudit-cli 0.1.35 → 0.1.37
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 +114 -84
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/sdlc/files/_common/Implementation_Plan_TEMPLATE.md +126 -0
- package/sdlc/files/_common/governance/ai-disclosure.md.template +21 -4
- package/sdlc/files/_common/governance/dpia.md.template +20 -4
- package/sdlc/files/_common/governance/incident-report.md.template +36 -3
- package/sdlc/files/_common/governance/periodic-review.md.template +20 -3
- package/sdlc/files/_common/governance/ropa.md.template +22 -4
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +69 -0
- package/sdlc/files/_common/skills/governance-doc-author/SKILL.md +148 -0
- package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +84 -0
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +12 -1
- package/sdlc/files/ci/periodic-review.yml.template +54 -1
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.
|
|
58
|
+
version: "0.1.37"};
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
};
|
|
@@ -2145,68 +2145,19 @@ async function syncAll(projectPaths) {
|
|
|
2145
2145
|
async function syncTemplates(ctx) {
|
|
2146
2146
|
if (ctx.dryRun) {
|
|
2147
2147
|
return {
|
|
2148
|
-
step: "10/
|
|
2148
|
+
step: "10/11 Sync SDLC templates",
|
|
2149
2149
|
status: "planned",
|
|
2150
2150
|
message: `would run native syncProject() against ${ctx.projectPath}`
|
|
2151
2151
|
};
|
|
2152
2152
|
}
|
|
2153
2153
|
const report = await syncProject(ctx.projectPath);
|
|
2154
2154
|
return {
|
|
2155
|
-
step: "10/
|
|
2155
|
+
step: "10/11 Sync SDLC templates",
|
|
2156
2156
|
status: "ok",
|
|
2157
2157
|
message: `synced ${report.totalFilesSynced} files across ${report.sections.length} sections`,
|
|
2158
2158
|
data: { totalFilesSynced: report.totalFilesSynced }
|
|
2159
2159
|
};
|
|
2160
2160
|
}
|
|
2161
|
-
var STEP = "11/12 Bootstrap governance docs";
|
|
2162
|
-
var SOURCE_REL = "sdlc/files/_common/governance";
|
|
2163
|
-
var TARGET_REL = "compliance/governance";
|
|
2164
|
-
async function bootstrapGovernanceDocs(ctx) {
|
|
2165
|
-
const sourceDir = resolve(ctx.installerRoot, SOURCE_REL);
|
|
2166
|
-
const targetDir = resolve(ctx.projectPath, TARGET_REL);
|
|
2167
|
-
let templates = [];
|
|
2168
|
-
try {
|
|
2169
|
-
templates = (await readdir(sourceDir)).filter((name) => name.endsWith(".md.template")).sort();
|
|
2170
|
-
} catch (err) {
|
|
2171
|
-
return {
|
|
2172
|
-
step: STEP,
|
|
2173
|
-
status: "warn",
|
|
2174
|
-
message: `source directory not found: ${sourceDir} (${err.message})`
|
|
2175
|
-
};
|
|
2176
|
-
}
|
|
2177
|
-
if (templates.length === 0) {
|
|
2178
|
-
return { step: STEP, status: "warn", message: `no .md.template files in ${sourceDir}` };
|
|
2179
|
-
}
|
|
2180
|
-
if (ctx.dryRun) {
|
|
2181
|
-
return {
|
|
2182
|
-
step: STEP,
|
|
2183
|
-
status: "planned",
|
|
2184
|
-
message: `would copy ${templates.length} starter(s) to ${TARGET_REL}/ (skip-if-exists)`,
|
|
2185
|
-
data: { templates: [...templates] }
|
|
2186
|
-
};
|
|
2187
|
-
}
|
|
2188
|
-
await ensureDir(targetDir);
|
|
2189
|
-
const copied = [];
|
|
2190
|
-
const skipped = [];
|
|
2191
|
-
for (const template of templates) {
|
|
2192
|
-
const targetName = template.replace(/\.template$/, "");
|
|
2193
|
-
const targetPath = join(targetDir, targetName);
|
|
2194
|
-
if (await isFile(targetPath)) {
|
|
2195
|
-
skipped.push(targetName);
|
|
2196
|
-
continue;
|
|
2197
|
-
}
|
|
2198
|
-
const body = await readFile(join(sourceDir, template), "utf8");
|
|
2199
|
-
await writeFile(targetPath, body, "utf8");
|
|
2200
|
-
copied.push(targetName);
|
|
2201
|
-
}
|
|
2202
|
-
const detail = skipped.length === 0 ? `${copied.length} starter(s) copied to ${TARGET_REL}/` : `${copied.length} copied, ${skipped.length} kept (already on disk)`;
|
|
2203
|
-
return {
|
|
2204
|
-
step: STEP,
|
|
2205
|
-
status: "ok",
|
|
2206
|
-
message: `${detail} \u2014 STARTERS, edit before production (see docs/governance-templates.md)`,
|
|
2207
|
-
data: { copied, skipped, targetDir: TARGET_REL }
|
|
2208
|
-
};
|
|
2209
|
-
}
|
|
2210
2161
|
|
|
2211
2162
|
// src/install/done-report.ts
|
|
2212
2163
|
function doneReport(ctx, plan) {
|
|
@@ -2235,7 +2186,7 @@ function doneReport(ctx, plan) {
|
|
|
2235
2186
|
""
|
|
2236
2187
|
];
|
|
2237
2188
|
return {
|
|
2238
|
-
step: "
|
|
2189
|
+
step: "11/11 Done (developer mode)",
|
|
2239
2190
|
status: "ok",
|
|
2240
2191
|
message: lines2.join("\n"),
|
|
2241
2192
|
data: { mode: "developer" }
|
|
@@ -2262,7 +2213,7 @@ function doneReport(ctx, plan) {
|
|
|
2262
2213
|
""
|
|
2263
2214
|
];
|
|
2264
2215
|
return {
|
|
2265
|
-
step: "
|
|
2216
|
+
step: "11/11 Done",
|
|
2266
2217
|
status: "ok",
|
|
2267
2218
|
message: lines.join("\n"),
|
|
2268
2219
|
data: { nextBranch: branch, mode: "operator" }
|
|
@@ -2314,7 +2265,7 @@ async function runInstall(options) {
|
|
|
2314
2265
|
steps.push(await record(log, setGithubSecrets(ctx, plan, providerResolution.provider)));
|
|
2315
2266
|
} else {
|
|
2316
2267
|
const skipped = {
|
|
2317
|
-
step: "7/
|
|
2268
|
+
step: "7/11 Set GitHub secrets and variables",
|
|
2318
2269
|
status: "skipped",
|
|
2319
2270
|
message: providerResolution.reason ?? "no git provider available"
|
|
2320
2271
|
};
|
|
@@ -2326,7 +2277,7 @@ async function runInstall(options) {
|
|
|
2326
2277
|
steps.push(await record(log, configureBranchProtection(ctx, providerResolution.provider)));
|
|
2327
2278
|
} else {
|
|
2328
2279
|
const skipped = {
|
|
2329
|
-
step: "9/
|
|
2280
|
+
step: "9/11 Configure branch protection",
|
|
2330
2281
|
status: "skipped",
|
|
2331
2282
|
message: providerResolution.reason ?? "no git provider available"
|
|
2332
2283
|
};
|
|
@@ -2334,7 +2285,6 @@ async function runInstall(options) {
|
|
|
2334
2285
|
log.warn(`[${skipped.step}] SKIPPED ${skipped.message}`);
|
|
2335
2286
|
}
|
|
2336
2287
|
steps.push(await record(log, syncTemplates(ctx)));
|
|
2337
|
-
steps.push(await record(log, bootstrapGovernanceDocs(ctx)));
|
|
2338
2288
|
const done = doneReport(ctx, plan);
|
|
2339
2289
|
steps.push(done);
|
|
2340
2290
|
log.success(`[${done.step}]`);
|
|
@@ -2430,7 +2380,7 @@ async function record(log, p) {
|
|
|
2430
2380
|
}
|
|
2431
2381
|
function planSummary(plan) {
|
|
2432
2382
|
return {
|
|
2433
|
-
step: "3/
|
|
2383
|
+
step: "3/11 Configure",
|
|
2434
2384
|
status: "ok",
|
|
2435
2385
|
message: `slug=${plan.projectSlug} runtime=${plan.runtimeVersion}`,
|
|
2436
2386
|
data: { ...plan }
|
|
@@ -2522,6 +2472,80 @@ async function runUpdate(options) {
|
|
|
2522
2472
|
log.log("");
|
|
2523
2473
|
log.warn("Do NOT auto-commit \u2014 review the changes first.");
|
|
2524
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
|
+
}
|
|
2525
2549
|
|
|
2526
2550
|
// src/commands/stub.ts
|
|
2527
2551
|
function makeStub(info) {
|
|
@@ -2781,6 +2805,12 @@ async function main(argv) {
|
|
|
2781
2805
|
});
|
|
2782
2806
|
}
|
|
2783
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
|
+
});
|
|
2784
2814
|
program.command("doctor").description("Verify the local install: required tools on PATH, auth state, config validity").action(runDoctor);
|
|
2785
2815
|
program.command("status [path]").description("Show the consumer project's framework state").action(async (path) => {
|
|
2786
2816
|
await runStatus({ path });
|