@metasession.co/devaudit-cli 0.1.21 → 0.1.23

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
@@ -54,7 +54,7 @@ function emitJsonResult(payload) {
54
54
 
55
55
  // package.json
56
56
  var package_default = {
57
- version: "0.1.21"};
57
+ version: "0.1.23"};
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 ctx = {
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(ctx);
2219
+ banner(tentativeCtx);
2133
2220
  const steps = [];
2134
- steps.push(await record(log, runAuthProbe(ctx)));
2221
+ steps.push(await record(log, runAuthProbe(tentativeCtx)));
2135
2222
  const plugins = options.plugins ?? (await discoverPlugins()).loaded;
2136
- if (plugins.length > 0 && !ctx.dryRun) {
2137
- const pluginCtx = await buildPluginContext({ projectPath: ctx.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(ctx);
2227
+ const { result: detectResult, detected } = await detectStack(tentativeCtx);
2141
2228
  steps.push(await record(log, Promise.resolve(detectResult)));
2142
- const plan = await collectPlan(ctx, detected);
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 (native TS implementation)").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)").action(async (path, cmdOpts, cmd) => {
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 runInstallCommand({
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 } : {},