@metasession.co/devaudit-cli 0.1.22 → 0.1.24
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 +201 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/sdlc/files/_common/0-project-setup.md +2 -0
- package/sdlc/files/_common/implementing-an-sdlc-issue.md +1 -1
- package/sdlc/files/_common/joining-an-existing-project.md +191 -0
- package/sdlc/files/ci/compliance-evidence.yml.template +12 -2
package/dist/index.js
CHANGED
|
@@ -54,7 +54,7 @@ function emitJsonResult(payload) {
|
|
|
54
54
|
|
|
55
55
|
// package.json
|
|
56
56
|
var package_default = {
|
|
57
|
-
version: "0.1.
|
|
57
|
+
version: "0.1.24"};
|
|
58
58
|
|
|
59
59
|
// src/lib/version.ts
|
|
60
60
|
var CLI_VERSION = package_default.version;
|
|
@@ -792,6 +792,14 @@ async function exists(path) {
|
|
|
792
792
|
return false;
|
|
793
793
|
}
|
|
794
794
|
}
|
|
795
|
+
async function isFile(path) {
|
|
796
|
+
try {
|
|
797
|
+
const stat = await promises.stat(path);
|
|
798
|
+
return stat.isFile();
|
|
799
|
+
} catch {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
795
803
|
async function isDir(path) {
|
|
796
804
|
try {
|
|
797
805
|
const stat = await promises.stat(path);
|
|
@@ -888,6 +896,25 @@ var GitHubProvider = class {
|
|
|
888
896
|
"Setting a GitHub repo secret without `gh` CLI requires sodium encryption (libsodium) \u2014 not implemented. Install `gh` CLI to use this command."
|
|
889
897
|
);
|
|
890
898
|
}
|
|
899
|
+
async hasSecret(cwd, name) {
|
|
900
|
+
if (this.preferGhCli && await ghAvailable()) {
|
|
901
|
+
const res2 = await execa("gh", ["secret", "list", "--json", "name"], { cwd, reject: false });
|
|
902
|
+
if (res2.exitCode !== 0) return false;
|
|
903
|
+
try {
|
|
904
|
+
const rows = JSON.parse(res2.stdout);
|
|
905
|
+
return rows.some((r) => r.name === name);
|
|
906
|
+
} catch {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const meta = await this.getRepoMeta(cwd);
|
|
911
|
+
if (!this.token) return false;
|
|
912
|
+
const res = await fetch(
|
|
913
|
+
`https://api.github.com/repos/${meta.owner}/${meta.name}/actions/secrets/${name}`,
|
|
914
|
+
{ headers: this.authHeaders() }
|
|
915
|
+
);
|
|
916
|
+
return res.ok;
|
|
917
|
+
}
|
|
891
918
|
async setVariable(cwd, name, value) {
|
|
892
919
|
if (this.preferGhCli && await ghAvailable()) {
|
|
893
920
|
await execa("gh", ["variable", "set", name, "--body", value], { cwd });
|
|
@@ -1179,6 +1206,13 @@ var PYTHON_PATHS_IGNORE = [
|
|
|
1179
1206
|
"sdlc-config.json"
|
|
1180
1207
|
];
|
|
1181
1208
|
async function writeSdlcConfig(ctx, plan) {
|
|
1209
|
+
if (ctx.installMode === "developer") {
|
|
1210
|
+
return {
|
|
1211
|
+
step: "4/11 Write sdlc-config.json",
|
|
1212
|
+
status: "skipped",
|
|
1213
|
+
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
|
+
}
|
|
1182
1216
|
const runtimeKey = plan.stack === "node" ? "node_version" : "python_version";
|
|
1183
1217
|
const pathsIgnore = plan.stack === "node" ? NODE_PATHS_IGNORE : PYTHON_PATHS_IGNORE;
|
|
1184
1218
|
const existing = await readSdlcConfig(ctx.projectPath) ?? null;
|
|
@@ -1270,6 +1304,13 @@ async function findOrCreateProject(ctx, plan) {
|
|
|
1270
1304
|
// src/install/api-key.ts
|
|
1271
1305
|
var KEY_NAME = "Onboarding-issued";
|
|
1272
1306
|
async function issueApiKey(ctx, plan) {
|
|
1307
|
+
if (ctx.installMode === "developer") {
|
|
1308
|
+
return {
|
|
1309
|
+
step: "6/11 Issue project API key",
|
|
1310
|
+
status: "skipped",
|
|
1311
|
+
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
|
+
}
|
|
1273
1314
|
if (ctx.dryRun) {
|
|
1274
1315
|
return {
|
|
1275
1316
|
step: "6/11 Issue project API key",
|
|
@@ -1317,6 +1358,13 @@ function buildSkipped(plan) {
|
|
|
1317
1358
|
return skipped;
|
|
1318
1359
|
}
|
|
1319
1360
|
async function setGithubSecrets(ctx, plan, provider) {
|
|
1361
|
+
if (ctx.installMode === "developer") {
|
|
1362
|
+
return {
|
|
1363
|
+
step: "7/11 Set GitHub secrets and variables",
|
|
1364
|
+
status: "skipped",
|
|
1365
|
+
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
|
+
}
|
|
1320
1368
|
const operations = buildOperations(ctx, plan);
|
|
1321
1369
|
if (ctx.dryRun) {
|
|
1322
1370
|
const summary = operations.map((op) => `${op.kind}:${op.name}`).join(", ");
|
|
@@ -1400,6 +1448,13 @@ var REQUIRED_CHECKS = [
|
|
|
1400
1448
|
"Quality Gates"
|
|
1401
1449
|
];
|
|
1402
1450
|
async function configureBranchProtection(ctx, provider) {
|
|
1451
|
+
if (ctx.installMode === "developer") {
|
|
1452
|
+
return {
|
|
1453
|
+
step: "9/11 Configure branch protection",
|
|
1454
|
+
status: "skipped",
|
|
1455
|
+
message: "developer mode \u2014 leaving branch protection unchanged. Use --force-team-config to re-apply as the project operator."
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1403
1458
|
let meta;
|
|
1404
1459
|
try {
|
|
1405
1460
|
meta = await provider.getRepoMeta(ctx.projectPath);
|
|
@@ -2082,6 +2137,37 @@ async function syncTemplates(ctx) {
|
|
|
2082
2137
|
|
|
2083
2138
|
// src/install/done-report.ts
|
|
2084
2139
|
function doneReport(ctx, plan) {
|
|
2140
|
+
if (ctx.installMode === "developer") {
|
|
2141
|
+
const lines2 = [
|
|
2142
|
+
"",
|
|
2143
|
+
` ${ctx.projectName} \u2014 local developer setup complete.`,
|
|
2144
|
+
"",
|
|
2145
|
+
" What ran:",
|
|
2146
|
+
" - Templates re-synced from DevAudit-Installer (SDLC/, scripts/, .husky/, \u2026).",
|
|
2147
|
+
" - Git hooks bootstrapped.",
|
|
2148
|
+
"",
|
|
2149
|
+
" What was deliberately skipped (developer mode):",
|
|
2150
|
+
" - sdlc-config.json (team config \u2014 already on disk).",
|
|
2151
|
+
" - 'Onboarding-issued' API key (team-owned by the project operator).",
|
|
2152
|
+
" - GitHub repo secrets (DEVAUDIT_USER_TOKEN, DEVAUDIT_API_KEY, DEVAUDIT_BASE_URL).",
|
|
2153
|
+
" - Branch protection rules.",
|
|
2154
|
+
"",
|
|
2155
|
+
" Verify your local install:",
|
|
2156
|
+
" devaudit auth status # confirm your personal mctok_\u2026 is valid",
|
|
2157
|
+
" devaudit status . # check framework files are present",
|
|
2158
|
+
" devaudit doctor # node \u226522, git, gh, jq, curl",
|
|
2159
|
+
"",
|
|
2160
|
+
" See SDLC/joining-an-existing-project.md for the full second-developer guide.",
|
|
2161
|
+
" Rotate team secrets only as the project operator: `devaudit install --force-team-config`.",
|
|
2162
|
+
""
|
|
2163
|
+
];
|
|
2164
|
+
return {
|
|
2165
|
+
step: "11/11 Done (developer mode)",
|
|
2166
|
+
status: "ok",
|
|
2167
|
+
message: lines2.join("\n"),
|
|
2168
|
+
data: { mode: "developer" }
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2085
2171
|
const branch = "feat/sdlc-onboarding";
|
|
2086
2172
|
const lines = [
|
|
2087
2173
|
"",
|
|
@@ -2106,7 +2192,7 @@ function doneReport(ctx, plan) {
|
|
|
2106
2192
|
step: "11/11 Done",
|
|
2107
2193
|
status: "ok",
|
|
2108
2194
|
message: lines.join("\n"),
|
|
2109
|
-
data: { nextBranch: branch }
|
|
2195
|
+
data: { nextBranch: branch, mode: "operator" }
|
|
2110
2196
|
};
|
|
2111
2197
|
}
|
|
2112
2198
|
|
|
@@ -2120,33 +2206,37 @@ async function runInstall(options) {
|
|
|
2120
2206
|
const projectName = basename(projectPath);
|
|
2121
2207
|
const auth = await resolveTokenForInstall(options);
|
|
2122
2208
|
const installerRoot = await resolveInstallerRoot();
|
|
2123
|
-
const
|
|
2209
|
+
const tentativeCtx = {
|
|
2124
2210
|
projectPath,
|
|
2125
2211
|
projectName,
|
|
2126
2212
|
installerRoot,
|
|
2127
2213
|
token: auth.token,
|
|
2128
2214
|
baseUrl: auth.baseUrl,
|
|
2129
2215
|
dryRun: Boolean(options.dryRun),
|
|
2130
|
-
nonInteractive: Boolean(options.nonInteractive)
|
|
2216
|
+
nonInteractive: Boolean(options.nonInteractive),
|
|
2217
|
+
installMode: "operator"
|
|
2131
2218
|
};
|
|
2132
|
-
banner(
|
|
2219
|
+
banner(tentativeCtx);
|
|
2133
2220
|
const steps = [];
|
|
2134
|
-
steps.push(await record(log, runAuthProbe(
|
|
2221
|
+
steps.push(await record(log, runAuthProbe(tentativeCtx)));
|
|
2135
2222
|
const plugins = options.plugins ?? (await discoverPlugins()).loaded;
|
|
2136
|
-
if (plugins.length > 0 && !
|
|
2137
|
-
const pluginCtx = await buildPluginContext({ projectPath:
|
|
2223
|
+
if (plugins.length > 0 && !tentativeCtx.dryRun) {
|
|
2224
|
+
const pluginCtx = await buildPluginContext({ projectPath: tentativeCtx.projectPath });
|
|
2138
2225
|
await runHook(plugins, "beforeInstall", pluginCtx);
|
|
2139
2226
|
}
|
|
2140
|
-
const { result: detectResult, detected } = await detectStack(
|
|
2227
|
+
const { result: detectResult, detected } = await detectStack(tentativeCtx);
|
|
2141
2228
|
steps.push(await record(log, Promise.resolve(detectResult)));
|
|
2142
|
-
const plan = await collectPlan(
|
|
2229
|
+
const plan = await collectPlan(tentativeCtx, detected);
|
|
2143
2230
|
const planStep = planSummary(plan);
|
|
2144
2231
|
steps.push(planStep);
|
|
2145
2232
|
log.success(`[${planStep.step}] ${planStep.message ?? ""}`);
|
|
2233
|
+
const providerResolution = await resolveProvider(options, tentativeCtx);
|
|
2234
|
+
const detection = await detectInstallMode(tentativeCtx, plan, providerResolution.provider, options);
|
|
2235
|
+
if (detection.notice) log.info(detection.notice);
|
|
2236
|
+
const ctx = { ...tentativeCtx, installMode: detection.mode };
|
|
2146
2237
|
steps.push(await record(log, writeSdlcConfig(ctx, plan)));
|
|
2147
2238
|
steps.push(await record(log, findOrCreateProject(ctx, plan)));
|
|
2148
2239
|
steps.push(await record(log, issueApiKey(ctx, plan)));
|
|
2149
|
-
const providerResolution = await resolveProvider(options, ctx);
|
|
2150
2240
|
if (providerResolution.provider) {
|
|
2151
2241
|
steps.push(await record(log, setGithubSecrets(ctx, plan, providerResolution.provider)));
|
|
2152
2242
|
} else {
|
|
@@ -2181,6 +2271,58 @@ async function runInstall(options) {
|
|
|
2181
2271
|
}
|
|
2182
2272
|
return { project: projectName, projectPath, dryRun: ctx.dryRun, steps };
|
|
2183
2273
|
}
|
|
2274
|
+
async function detectInstallMode(ctx, plan, provider, options) {
|
|
2275
|
+
if (options.forceTeamConfig) {
|
|
2276
|
+
return {
|
|
2277
|
+
mode: "operator",
|
|
2278
|
+
allBitsMatched: false,
|
|
2279
|
+
notice: "--force-team-config: running operator-mode (will rewrite repo secrets + branch protection)."
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
if (options.mode === "developer") {
|
|
2283
|
+
return {
|
|
2284
|
+
mode: "developer",
|
|
2285
|
+
allBitsMatched: true,
|
|
2286
|
+
notice: "mode=developer (pinned): destructive steps will skip \u2014 use `devaudit install --force-team-config` from the project's onboarding operator if you need to rotate team secrets."
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
if (options.mode === "operator") {
|
|
2290
|
+
return { mode: "operator", allBitsMatched: false };
|
|
2291
|
+
}
|
|
2292
|
+
if (ctx.dryRun) {
|
|
2293
|
+
return { mode: "operator", allBitsMatched: false };
|
|
2294
|
+
}
|
|
2295
|
+
const sdlcConfigExisted = await isFile(`${ctx.projectPath}/sdlc-config.json`);
|
|
2296
|
+
if (!sdlcConfigExisted) return { mode: "operator", allBitsMatched: false };
|
|
2297
|
+
let projectExists = false;
|
|
2298
|
+
let keyExists = false;
|
|
2299
|
+
try {
|
|
2300
|
+
const client = new DevAuditClient({ token: ctx.token, baseUrl: ctx.baseUrl });
|
|
2301
|
+
const existing = await client.getProjectBySlug(plan.projectSlug);
|
|
2302
|
+
if (existing) {
|
|
2303
|
+
projectExists = true;
|
|
2304
|
+
const keys = await client.listApiKeys(existing.id);
|
|
2305
|
+
keyExists = keys.some((k) => k.name === "Onboarding-issued" && k.revoked_at === null);
|
|
2306
|
+
}
|
|
2307
|
+
} catch {
|
|
2308
|
+
return { mode: "operator", allBitsMatched: false };
|
|
2309
|
+
}
|
|
2310
|
+
if (!projectExists || !keyExists) return { mode: "operator", allBitsMatched: false };
|
|
2311
|
+
let hasUserTokenSecret = false;
|
|
2312
|
+
if (provider) {
|
|
2313
|
+
try {
|
|
2314
|
+
hasUserTokenSecret = await provider.hasSecret(ctx.projectPath, "DEVAUDIT_USER_TOKEN");
|
|
2315
|
+
} catch {
|
|
2316
|
+
hasUserTokenSecret = false;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
if (!hasUserTokenSecret) return { mode: "operator", allBitsMatched: false };
|
|
2320
|
+
return {
|
|
2321
|
+
mode: "developer",
|
|
2322
|
+
allBitsMatched: true,
|
|
2323
|
+
notice: "developer mode auto-detected (project + Onboarding-issued key + DEVAUDIT_USER_TOKEN secret all present): destructive steps (4, 6, 7, 9) will skip. Use --force-team-config to rotate team secrets."
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2184
2326
|
async function resolveProvider(options, ctx) {
|
|
2185
2327
|
if (options.provider) return { provider: options.provider };
|
|
2186
2328
|
try {
|
|
@@ -2236,6 +2378,31 @@ async function runInstallCommand(options) {
|
|
|
2236
2378
|
const log = logger();
|
|
2237
2379
|
try {
|
|
2238
2380
|
await runInstall({
|
|
2381
|
+
...options.path !== void 0 ? { path: options.path } : {},
|
|
2382
|
+
...options.token !== void 0 ? { token: options.token } : {},
|
|
2383
|
+
...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {},
|
|
2384
|
+
...options.dryRun !== void 0 ? { dryRun: options.dryRun } : {},
|
|
2385
|
+
...options.yes !== void 0 ? { nonInteractive: options.yes } : {},
|
|
2386
|
+
...options.forceTeamConfig !== void 0 ? { forceTeamConfig: options.forceTeamConfig } : {}
|
|
2387
|
+
});
|
|
2388
|
+
} catch (err) {
|
|
2389
|
+
log.error(err.message);
|
|
2390
|
+
process.exit(1);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
async function runJoinCommand(options) {
|
|
2394
|
+
const log = logger();
|
|
2395
|
+
const projectPath = resolve(options.path ?? process.cwd());
|
|
2396
|
+
const sdlcConfigExists = await isFile(`${projectPath}/sdlc-config.json`);
|
|
2397
|
+
if (!sdlcConfigExists) {
|
|
2398
|
+
log.error(
|
|
2399
|
+
`No sdlc-config.json at ${projectPath}. This project hasn't been onboarded yet \u2014 the project operator should run \`devaudit install\`. See SDLC/joining-an-existing-project.md (synced into onboarded repos) for the second-developer guide.`
|
|
2400
|
+
);
|
|
2401
|
+
process.exit(7);
|
|
2402
|
+
}
|
|
2403
|
+
try {
|
|
2404
|
+
await runInstall({
|
|
2405
|
+
mode: "developer",
|
|
2239
2406
|
...options.path !== void 0 ? { path: options.path } : {},
|
|
2240
2407
|
...options.token !== void 0 ? { token: options.token } : {},
|
|
2241
2408
|
...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {},
|
|
@@ -2476,9 +2643,30 @@ async function main(argv) {
|
|
|
2476
2643
|
"DevAudit CLI \u2014 installs, syncs, and operates the Metasession SDLC across consumer projects."
|
|
2477
2644
|
).version(CLI_VERSION, "-V, --version");
|
|
2478
2645
|
applyCommonFlags(program);
|
|
2479
|
-
program.command("install [path]").description("Interactive onboarding for a consumer project (
|
|
2646
|
+
program.command("install [path]").description("Interactive onboarding for a consumer project (operator flow). On an already-onboarded project a second dev auto-routes to developer mode; use `devaudit join` for the explicit second-dev entry point.").option("--token <token>", "PAT to use (otherwise reads DEVAUDIT_USER_TOKEN env or ~/.config/devaudit/auth.json)").option("--base-url <url>", "override portal URL (defaults to DEVAUDIT_BASE_URL env or production)").option("--force-team-config", "Re-run the destructive steps (write sdlc-config, issue API key, set GH secrets, apply branch protection) even when dev-mode detection would have skipped them. The operator-only rotation lane.").action(
|
|
2647
|
+
async (path, cmdOpts, cmd) => {
|
|
2648
|
+
const globals = cmd.optsWithGlobals();
|
|
2649
|
+
await runInstallCommand({
|
|
2650
|
+
...path !== void 0 ? { path } : {},
|
|
2651
|
+
...cmdOpts.token !== void 0 ? { token: cmdOpts.token } : {},
|
|
2652
|
+
...cmdOpts.baseUrl !== void 0 ? { baseUrl: cmdOpts.baseUrl } : {},
|
|
2653
|
+
...cmdOpts.forceTeamConfig !== void 0 ? { forceTeamConfig: Boolean(cmdOpts.forceTeamConfig) } : {},
|
|
2654
|
+
...globals.dryRun !== void 0 ? { dryRun: Boolean(globals.dryRun) } : {},
|
|
2655
|
+
...globals.yes !== void 0 ? { yes: Boolean(globals.yes) } : {}
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
);
|
|
2659
|
+
program.command("join [path]").description(
|
|
2660
|
+
"Second-developer entry point: re-sync framework templates + run hook bootstrap locally on an already-onboarded project. Skips the operator-only steps (sdlc-config write, API key issuance, GitHub secret writes, branch protection) so the team CI token is never rotated. See SDLC/joining-an-existing-project.md."
|
|
2661
|
+
).option(
|
|
2662
|
+
"--token <token>",
|
|
2663
|
+
"PAT to use (otherwise reads DEVAUDIT_USER_TOKEN env or ~/.config/devaudit/auth.json)"
|
|
2664
|
+
).option(
|
|
2665
|
+
"--base-url <url>",
|
|
2666
|
+
"override portal URL (defaults to DEVAUDIT_BASE_URL env or production)"
|
|
2667
|
+
).action(async (path, cmdOpts, cmd) => {
|
|
2480
2668
|
const globals = cmd.optsWithGlobals();
|
|
2481
|
-
await
|
|
2669
|
+
await runJoinCommand({
|
|
2482
2670
|
...path !== void 0 ? { path } : {},
|
|
2483
2671
|
...cmdOpts.token !== void 0 ? { token: cmdOpts.token } : {},
|
|
2484
2672
|
...cmdOpts.baseUrl !== void 0 ? { baseUrl: cmdOpts.baseUrl } : {},
|