@i4ctime/q-ring 0.12.0 → 0.13.1

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
@@ -42,7 +42,7 @@ import {
42
42
  tunnelList,
43
43
  tunnelRead,
44
44
  verifyAuditChain
45
- } from "./chunk-5TQNDJT3.js";
45
+ } from "./chunk-Z7UJFHVE.js";
46
46
 
47
47
  // src/cli/commands.ts
48
48
  import { Command, Help } from "commander";
@@ -191,7 +191,40 @@ function importDotenv(filePathOrContent, options = {}) {
191
191
 
192
192
  // src/utils/prompt.ts
193
193
  import { createInterface } from "readline";
194
+ async function confirm(message, { assumeYes = false } = {}) {
195
+ if (assumeYes) return true;
196
+ if (!process.stdin.isTTY) {
197
+ throw new Error(
198
+ "Confirmation required but stdin is not a terminal. Pass --yes to proceed non-interactively."
199
+ );
200
+ }
201
+ const rl = createInterface({
202
+ input: process.stdin,
203
+ output: process.stderr,
204
+ terminal: true
205
+ });
206
+ const answer = await new Promise((resolve) => {
207
+ rl.question(`${message} [y/N] `, resolve);
208
+ });
209
+ rl.close();
210
+ return /^y(es)?$/i.test(answer.trim());
211
+ }
194
212
  async function promptSecret(message) {
213
+ const stdin = process.stdin;
214
+ if (!stdin.isTTY) {
215
+ let data = "";
216
+ stdin.setEncoding("utf8");
217
+ for await (const chunk of stdin) {
218
+ data += chunk;
219
+ }
220
+ const value = data.replace(/\r?\n$/, "");
221
+ if (value.length === 0) {
222
+ throw new Error(
223
+ "No value provided: stdin is not a terminal and was empty. Pipe the value in, or pass it as an argument."
224
+ );
225
+ }
226
+ return value;
227
+ }
195
228
  const rl = createInterface({
196
229
  input: process.stdin,
197
230
  output: process.stderr,
@@ -199,7 +232,6 @@ async function promptSecret(message) {
199
232
  });
200
233
  return new Promise((resolve) => {
201
234
  process.stderr.write(message);
202
- const stdin = process.stdin;
203
235
  const wasRaw = stdin.isRaw;
204
236
  if (stdin.isTTY && stdin.setRawMode) {
205
237
  stdin.setRawMode(true);
@@ -234,6 +266,11 @@ function wantsJsonOutput(program2, cmd) {
234
266
  const g = program2.opts();
235
267
  return !!g.json;
236
268
  }
269
+ function emitJson(program2, cmd, data) {
270
+ if (!wantsJsonOutput(program2, cmd)) return false;
271
+ process.stdout.write(JSON.stringify({ ok: true, data }, null, 2) + "\n");
272
+ return true;
273
+ }
237
274
  function safeStr(s) {
238
275
  return s == null ? "" : `${s}`;
239
276
  }
@@ -360,17 +397,36 @@ function registerSecretsCommands(program2) {
360
397
  const payload = wantsJsonOutput(program2, cmd) ? { ok: true, data: { key, value } } : { key, value };
361
398
  process.stdout.write(JSON.stringify(payload) + "\n");
362
399
  });
363
- program2.command("delete <key>").alias("rm").description("Remove a secret from the keyring").option("-g, --global", "Delete from global scope only").option("-p, --project", "Delete from project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
400
+ program2.command("has <key>").description(
401
+ "Check whether a secret exists (exit 0 if present, 1 if not) \u2014 decay-aware"
402
+ ).option("-g, --global", "Look only in global scope").option("-p, --project", "Look only in project scope").option("--team <id>", "Look only in team scope").option("--org <id>", "Look only in org scope").option("--project-path <path>", "Explicit project path").option("-q, --quiet", "No output, exit code only (for scripts)").action((key, cmd) => {
364
403
  const opts = buildOpts(cmd);
404
+ const exists = hasSecret(key, opts);
405
+ if (!cmd.quiet) {
406
+ if (wantsJsonOutput(program2, cmd)) {
407
+ console.log(JSON.stringify({ ok: true, data: { key, exists } }));
408
+ } else {
409
+ console.log(exists ? "true" : "false");
410
+ }
411
+ }
412
+ if (!exists) process.exitCode = 1;
413
+ });
414
+ program2.command("delete <key>").alias("rm").description("Remove a secret from the keyring").option("-g, --global", "Delete from global scope only").option("-p, --project", "Delete from project scope only").option("--project-path <path>", "Explicit project path").option("-y, --yes", "Skip the confirmation prompt").option("--json", "Output as JSON").action(async (key, cmd) => {
415
+ const opts = buildOpts(cmd);
416
+ if (!await confirm(`Delete secret ${key}?`, { assumeYes: cmd.yes })) {
417
+ console.error(c.dim("Aborted."));
418
+ process.exitCode = 1;
419
+ return;
420
+ }
365
421
  const deleted = deleteSecret(key, opts);
366
- if (deleted) {
367
- console.log(`${SYMBOLS.check} ${c.green("deleted")} ${c.bold(key)}`);
368
- } else {
422
+ if (!deleted) {
369
423
  console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
370
424
  process.exit(1);
371
425
  }
426
+ if (emitJson(program2, cmd, { key, deleted: true })) return;
427
+ console.log(`${SYMBOLS.check} ${c.green("deleted")} ${c.bold(key)}`);
372
428
  });
373
- program2.command("list").alias("ls").description("List all secrets with quantum status indicators").option("-g, --global", "List global scope only").option("-p, --project", "List project scope only").option("--team <id>", "List team scope only").option("--org <id>", "List org scope only").option("--project-path <path>", "Explicit project path").option("--show-decay", "Show decay/expiry status").option("-t, --tag <tag>", "Filter by tag").option("--expired", "Show only expired secrets").option("--stale", "Show only stale secrets (75%+ decay)").option("-f, --filter <pattern>", "Glob pattern on key name").action((cmd) => {
429
+ program2.command("list").alias("ls").description("List all secrets with quantum status indicators").option("-g, --global", "List global scope only").option("-p, --project", "List project scope only").option("--team <id>", "List team scope only").option("--org <id>", "List org scope only").option("--project-path <path>", "Explicit project path").option("--show-decay", "Show decay/expiry status").option("-t, --tag <tag>", "Filter by tag").option("--expired", "Show only expired secrets").option("--stale", "Show only stale secrets (75%+ decay)").option("-f, --filter <pattern>", "Glob pattern on key name").option("--json", "Output as JSON").action((cmd) => {
374
430
  const opts = buildOpts(cmd);
375
431
  let entries = listSecrets(opts);
376
432
  if (cmd.tag) {
@@ -389,6 +445,22 @@ function registerSecretsCommands(program2) {
389
445
  if (cmd.filter) {
390
446
  entries = filterSecretsByKeyGlob(entries, cmd.filter);
391
447
  }
448
+ if (emitJson(
449
+ program2,
450
+ cmd,
451
+ entries.map((e) => ({
452
+ key: safeStr(e.key),
453
+ scope: safeStr(e.scope),
454
+ type: e.envelope?.states ? "superposition" : "collapsed",
455
+ environments: e.envelope?.states ? Object.keys(e.envelope.states).map(safeStr) : void 0,
456
+ tags: safeArr(e.envelope?.meta.tags),
457
+ accessCount: safeNum(e.envelope?.meta.accessCount),
458
+ entangledCount: safeNum(e.envelope?.meta.entangled?.length),
459
+ decay: e.decay ?? null
460
+ }))
461
+ )) {
462
+ return;
463
+ }
392
464
  if (entries.length === 0) {
393
465
  console.log(c.dim("No secrets found"));
394
466
  return;
@@ -434,7 +506,7 @@ function registerSecretsCommands(program2) {
434
506
  }
435
507
  console.log();
436
508
  });
437
- program2.command("inspect <key>").description("Show full quantum state of a secret").option("-g, --global", "Inspect global scope only").option("-p, --project", "Inspect project scope only").option("--project-path <path>", "Explicit project path").action((key, cmd) => {
509
+ program2.command("inspect <key>").description("Show full quantum state of a secret").option("-g, --global", "Inspect global scope only").option("-p, --project", "Inspect project scope only").option("--project-path <path>", "Explicit project path").option("--json", "Output as JSON").action((key, cmd) => {
438
510
  const opts = buildOpts(cmd);
439
511
  const result = getEnvelope(key, opts);
440
512
  if (!result) {
@@ -443,6 +515,26 @@ function registerSecretsCommands(program2) {
443
515
  }
444
516
  const { envelope, scope } = result;
445
517
  const decay = checkDecay(envelope);
518
+ if (emitJson(program2, cmd, {
519
+ key: safeStr(key),
520
+ scope: safeStr(scope),
521
+ type: envelope.states ? "superposition" : "collapsed",
522
+ environments: envelope.states ? Object.keys(envelope.states).map(safeStr) : void 0,
523
+ defaultEnv: envelope.defaultEnv ? safeStr(envelope.defaultEnv) : void 0,
524
+ createdAt: safeStr(envelope.meta.createdAt),
525
+ updatedAt: safeStr(envelope.meta.updatedAt),
526
+ accessCount: safeNum(envelope.meta.accessCount),
527
+ lastAccessedAt: envelope.meta.lastAccessedAt ? safeStr(envelope.meta.lastAccessedAt) : void 0,
528
+ description: envelope.meta.description ? safeStr(envelope.meta.description) : void 0,
529
+ tags: safeArr(envelope.meta.tags),
530
+ entangled: (envelope.meta.entangled ?? []).map((l) => ({
531
+ service: safeStr(l.service),
532
+ key: safeStr(l.key)
533
+ })),
534
+ decay
535
+ })) {
536
+ return;
537
+ }
446
538
  const safeScope = safeStr(scope);
447
539
  const createdAt = safeStr(envelope.meta.createdAt);
448
540
  const updatedAt = safeStr(envelope.meta.updatedAt);
@@ -511,7 +603,7 @@ function registerSecretsCommands(program2) {
511
603
  });
512
604
  process.stdout.write(output + "\n");
513
605
  });
514
- program2.command("import <file>").description("Import secrets from a .env file").option("-g, --global", "Import to global scope").option("-p, --project", "Import to project scope").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Environment context").option("--skip-existing", "Skip keys that already exist").option("--dry-run", "Preview what would be imported without saving").action((file, cmd) => {
606
+ program2.command("import <file>").description("Import secrets from a .env file").option("-g, --global", "Import to global scope").option("-p, --project", "Import to project scope").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Environment context").option("--skip-existing", "Skip keys that already exist").option("--dry-run", "Preview what would be imported without saving").option("--json", "Output as JSON").action((file, cmd) => {
515
607
  const opts = buildOpts(cmd);
516
608
  const result = importDotenv(file, {
517
609
  scope: opts.scope,
@@ -520,6 +612,9 @@ function registerSecretsCommands(program2) {
520
612
  skipExisting: cmd.skipExisting,
521
613
  dryRun: cmd.dryRun
522
614
  });
615
+ if (emitJson(program2, cmd, { file, dryRun: !!cmd.dryRun, ...result })) {
616
+ return;
617
+ }
523
618
  if (cmd.dryRun) {
524
619
  console.log(
525
620
  `
@@ -989,7 +1084,7 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
989
1084
  }
990
1085
  console.log();
991
1086
  });
992
- program2.command("check").description("Validate project secrets against .q-ring.json manifest").option("--project-path <path>", "Project path (defaults to cwd)").action((cmd) => {
1087
+ program2.command("check").description("Validate project secrets against .q-ring.json manifest").option("--project-path <path>", "Project path (defaults to cwd)").option("--json", "Output as JSON").action((cmd) => {
993
1088
  const projectPath = cmd.projectPath ?? process.cwd();
994
1089
  const config = readProjectConfig(projectPath);
995
1090
  if (!config?.secrets || Object.keys(config.secrets).length === 0) {
@@ -1003,49 +1098,58 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
1003
1098
  );
1004
1099
  process.exit(1);
1005
1100
  }
1006
- console.log(
1007
- c.bold(`
1008
- ${SYMBOLS.shield} Project secret manifest check
1009
- `)
1010
- );
1011
- let present = 0;
1012
- let missing = 0;
1013
- let expiredCount = 0;
1014
- let staleCount = 0;
1101
+ const statuses = [];
1015
1102
  for (const [key, manifest] of Object.entries(config.secrets)) {
1016
1103
  const result = getEnvelope(key, { projectPath, source: "cli" });
1017
1104
  if (!result) {
1018
- if (manifest.required !== false) {
1019
- missing++;
1020
- console.log(
1021
- ` ${c.red(SYMBOLS.cross)} ${c.bold(key)} ${c.red("MISSING")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1022
- );
1023
- } else {
1024
- console.log(
1025
- ` ${c.dim(SYMBOLS.cross)} ${c.bold(key)} ${c.dim("optional, not set")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1026
- );
1027
- }
1105
+ statuses.push({
1106
+ key,
1107
+ status: manifest.required !== false ? "missing" : "optional-not-set",
1108
+ description: manifest.description
1109
+ });
1028
1110
  continue;
1029
1111
  }
1030
1112
  const decay = checkDecay(result.envelope);
1031
- if (decay.isExpired) {
1032
- expiredCount++;
1033
- console.log(
1034
- ` ${c.red(SYMBOLS.warning)} ${c.bold(key)} ${c.bgRed(c.white(" EXPIRED "))} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1035
- );
1036
- } else if (decay.isStale) {
1037
- staleCount++;
1038
- console.log(
1039
- ` ${c.yellow(SYMBOLS.warning)} ${c.bold(key)} ${c.yellow(`stale (${decay.lifetimePercent}%)`)} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1040
- );
1113
+ statuses.push({
1114
+ key,
1115
+ status: decay.isExpired ? "expired" : decay.isStale ? "stale" : "ok",
1116
+ description: manifest.description,
1117
+ ...decay.isStale && !decay.isExpired ? { lifetimePercent: decay.lifetimePercent } : {}
1118
+ });
1119
+ }
1120
+ const present = statuses.filter((s) => s.status === "ok").length;
1121
+ const missing = statuses.filter((s) => s.status === "missing").length;
1122
+ const expiredCount = statuses.filter((s) => s.status === "expired").length;
1123
+ const staleCount = statuses.filter((s) => s.status === "stale").length;
1124
+ const total = Object.keys(config.secrets).length;
1125
+ if (emitJson(program2, cmd, {
1126
+ projectPath,
1127
+ secrets: statuses,
1128
+ summary: { total, present, missing, expired: expiredCount, stale: staleCount },
1129
+ ready: missing === 0
1130
+ })) {
1131
+ if (missing > 0) process.exitCode = 1;
1132
+ return;
1133
+ }
1134
+ console.log(
1135
+ c.bold(`
1136
+ ${SYMBOLS.shield} Project secret manifest check
1137
+ `)
1138
+ );
1139
+ for (const s of statuses) {
1140
+ const desc = s.description ? c.dim(`\u2014 ${s.description}`) : "";
1141
+ if (s.status === "missing") {
1142
+ console.log(` ${c.red(SYMBOLS.cross)} ${c.bold(s.key)} ${c.red("MISSING")} ${desc}`);
1143
+ } else if (s.status === "optional-not-set") {
1144
+ console.log(` ${c.dim(SYMBOLS.cross)} ${c.bold(s.key)} ${c.dim("optional, not set")} ${desc}`);
1145
+ } else if (s.status === "expired") {
1146
+ console.log(` ${c.red(SYMBOLS.warning)} ${c.bold(s.key)} ${c.bgRed(c.white(" EXPIRED "))} ${desc}`);
1147
+ } else if (s.status === "stale") {
1148
+ console.log(` ${c.yellow(SYMBOLS.warning)} ${c.bold(s.key)} ${c.yellow(`stale (${s.lifetimePercent}%)`)} ${desc}`);
1041
1149
  } else {
1042
- present++;
1043
- console.log(
1044
- ` ${c.green(SYMBOLS.check)} ${c.bold(key)} ${c.green("OK")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1045
- );
1150
+ console.log(` ${c.green(SYMBOLS.check)} ${c.bold(s.key)} ${c.green("OK")} ${desc}`);
1046
1151
  }
1047
1152
  }
1048
- const total = Object.keys(config.secrets).length;
1049
1153
  console.log(
1050
1154
  `
1051
1155
  ${c.bold(`${total} declared`)} ${c.green(`${present} present`)} ${c.yellow(`${staleCount} stale`)} ${c.red(`${expiredCount} expired`)} ${c.red(`${missing} missing`)}`
@@ -1069,10 +1173,11 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
1069
1173
  console.log();
1070
1174
  if (missing > 0) process.exit(1);
1071
1175
  });
1072
- program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
1176
+ program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").option("--json", "Output as JSON").action((cmd) => {
1073
1177
  const result = collapseEnvironment({
1074
1178
  projectPath: cmd.projectPath ?? process.cwd()
1075
1179
  });
1180
+ if (emitJson(program2, cmd, { environment: result })) return;
1076
1181
  if (result) {
1077
1182
  console.log(
1078
1183
  `${SYMBOLS.zap} ${c.bold("Collapsed environment:")} ${envBadge(result.env)} ${c.dim(`(source: ${result.source})`)}`
@@ -1087,9 +1192,14 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
1087
1192
  });
1088
1193
  program2.command("env:generate").description(
1089
1194
  "Generate a .env file from the project manifest (.q-ring.json)"
1090
- ).option("--project-path <path>", "Project path (defaults to cwd)").option("-o, --output <file>", "Output file path (defaults to stdout)").option("-e, --env <env>", "Force environment for superposition collapse").action((cmd) => {
1195
+ ).option("--project-path <path>", "Project path (defaults to cwd)").option("-o, --output <file>", "Output file path (defaults to stdout)").option("-e, --env <env>", "Force environment for superposition collapse").option("-y, --yes", "Overwrite an existing output file without asking").action(async (cmd) => {
1091
1196
  const projectPath = cmd.projectPath ?? process.cwd();
1092
1197
  const config = readProjectConfig(projectPath);
1198
+ if (cmd.output && existsSync(cmd.output) && !await confirm(`Overwrite existing ${cmd.output}?`, { assumeYes: cmd.yes })) {
1199
+ console.error(c.dim("Aborted."));
1200
+ process.exitCode = 1;
1201
+ return;
1202
+ }
1093
1203
  if (!config?.secrets || Object.keys(config.secrets).length === 0) {
1094
1204
  console.error(
1095
1205
  c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
@@ -1150,7 +1260,7 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
1150
1260
  ).option("--tags <tags>", "Comma-separated tags for all secrets").option(
1151
1261
  "--hook-exec <cmd>",
1152
1262
  "Shell command to run when any of these secrets change"
1153
- ).option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").action(async (name, cmd) => {
1263
+ ).option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").option("-y, --yes", "Skip confirmation when overwriting existing secrets").action(async (name, cmd) => {
1154
1264
  const opts = buildOpts(cmd);
1155
1265
  const prefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
1156
1266
  const tags = cmd.tags?.split(",").map((t) => t.trim()) ?? [
@@ -1163,6 +1273,15 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
1163
1273
  } else {
1164
1274
  keyNames = [`${prefix}_API_KEY`, `${prefix}_API_SECRET`];
1165
1275
  }
1276
+ const existing = keyNames.filter((k) => hasSecret(k, opts));
1277
+ if (existing.length > 0 && !await confirm(
1278
+ `${existing.length} secret(s) already exist and will be overwritten with generated values: ${existing.join(", ")}. Continue?`,
1279
+ { assumeYes: cmd.yes }
1280
+ )) {
1281
+ console.error(c.dim("Aborted."));
1282
+ process.exitCode = 1;
1283
+ return;
1284
+ }
1166
1285
  console.log(`
1167
1286
  ${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
1168
1287
  `);
@@ -1440,8 +1559,9 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
1440
1559
  process.exit(1);
1441
1560
  }
1442
1561
  });
1443
- tunnel.command("list").alias("ls").description("List active tunnels").action(() => {
1562
+ tunnel.command("list").alias("ls").description("List active tunnels").option("--json", "Output as JSON").action((cmd) => {
1444
1563
  const tunnels = tunnelList();
1564
+ if (emitJson(program2, cmd, { tunnels })) return;
1445
1565
  if (tunnels.length === 0) {
1446
1566
  console.log(c.dim("No active tunnels"));
1447
1567
  return;
@@ -1502,7 +1622,7 @@ ${SYMBOLS.package} ${c.green("packed")} ${secrets.length} secret(s)`
1502
1622
  );
1503
1623
  }
1504
1624
  });
1505
- tp.command("unpack [bundle]").description("Unpack and import secrets from an encrypted bundle").option("-g, --global", "Import to global scope").option("-p, --project", "Import to project scope").option("--project-path <path>", "Explicit project path").option("--dry-run", "Show what would be imported without saving").action(async (bundle, cmd) => {
1625
+ tp.command("unpack [bundle]").description("Unpack and import secrets from an encrypted bundle").option("-g, --global", "Import to global scope").option("-p, --project", "Import to project scope").option("--project-path <path>", "Explicit project path").option("--dry-run", "Show what would be imported without saving").option("-y, --yes", "Overwrite existing secrets without asking").action(async (bundle, cmd) => {
1506
1626
  if (!bundle) {
1507
1627
  const chunks = [];
1508
1628
  for await (const chunk of process.stdin) {
@@ -1513,28 +1633,9 @@ ${SYMBOLS.package} ${c.green("packed")} ${secrets.length} secret(s)`
1513
1633
  const passphrase = await promptSecret(
1514
1634
  `${SYMBOLS.lock} Enter passphrase for decryption: `
1515
1635
  );
1636
+ let payload;
1516
1637
  try {
1517
- const payload = teleportUnpack(bundle, passphrase);
1518
- if (cmd.dryRun) {
1519
- console.log(
1520
- `
1521
- ${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets):
1522
- `
1523
- );
1524
- for (const s of payload.secrets) {
1525
- console.log(
1526
- ` ${SYMBOLS.key} ${c.bold(s.key)} ${c.dim(`[${s.scope ?? "global"}]`)}`
1527
- );
1528
- }
1529
- return;
1530
- }
1531
- const opts = buildOpts(cmd);
1532
- for (const s of payload.secrets) {
1533
- setSecret(s.key, s.value, opts);
1534
- }
1535
- console.log(
1536
- `${SYMBOLS.check} ${c.green("imported")} ${payload.secrets.length} secret(s) from teleport bundle`
1537
- );
1638
+ payload = teleportUnpack(bundle, passphrase);
1538
1639
  } catch {
1539
1640
  console.error(
1540
1641
  c.red(
@@ -1543,6 +1644,35 @@ ${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets)
1543
1644
  );
1544
1645
  process.exit(1);
1545
1646
  }
1647
+ if (cmd.dryRun) {
1648
+ console.log(
1649
+ `
1650
+ ${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets):
1651
+ `
1652
+ );
1653
+ for (const s of payload.secrets) {
1654
+ console.log(
1655
+ ` ${SYMBOLS.key} ${c.bold(s.key)} ${c.dim(`[${s.scope ?? "global"}]`)}`
1656
+ );
1657
+ }
1658
+ return;
1659
+ }
1660
+ const opts = buildOpts(cmd);
1661
+ const existing = payload.secrets.map((s) => s.key).filter((k) => hasSecret(k, opts));
1662
+ if (existing.length > 0 && !await confirm(
1663
+ `${existing.length} existing secret(s) will be overwritten: ${existing.join(", ")}. Continue?`,
1664
+ { assumeYes: cmd.yes }
1665
+ )) {
1666
+ console.error(c.dim("Aborted."));
1667
+ process.exitCode = 1;
1668
+ return;
1669
+ }
1670
+ for (const s of payload.secrets) {
1671
+ setSecret(s.key, s.value, opts);
1672
+ }
1673
+ console.log(
1674
+ `${SYMBOLS.check} ${c.green("imported")} ${payload.secrets.length} secret(s) from teleport bundle`
1675
+ );
1546
1676
  });
1547
1677
  }
1548
1678
 
@@ -1557,8 +1687,11 @@ function registerValidationCommands(program2) {
1557
1687
  ).option(
1558
1688
  "--manifest",
1559
1689
  "Only validate manifest-declared secrets (with --all)"
1560
- ).option("--list-providers", "List all available providers").action(async (key, cmd) => {
1690
+ ).option("--list-providers", "List all available providers").option("--json", "Output as JSON").action(async (key, cmd) => {
1561
1691
  if (cmd.listProviders) {
1692
+ if (emitJson(program2, cmd, { providers: registry2.listProviders() })) {
1693
+ return;
1694
+ }
1562
1695
  console.log(
1563
1696
  c.bold(`
1564
1697
  ${SYMBOLS.shield} Available validation providers
@@ -1588,11 +1721,15 @@ function registerValidationCommands(program2) {
1588
1721
  entries = entries.filter((e) => manifestKeys.has(e.key));
1589
1722
  }
1590
1723
  }
1591
- console.log(c.bold(`
1724
+ const jsonMode = wantsJsonOutput(program2, cmd);
1725
+ if (!jsonMode) {
1726
+ console.log(c.bold(`
1592
1727
  ${SYMBOLS.shield} Validating secrets
1593
1728
  `));
1729
+ }
1594
1730
  let validated = 0;
1595
1731
  let skipped = 0;
1732
+ const jsonResults = [];
1596
1733
  for (const entry of entries) {
1597
1734
  const value2 = getSecret(entry.key, { ...opts, scope: entry.scope });
1598
1735
  if (!value2) {
@@ -1606,12 +1743,26 @@ function registerValidationCommands(program2) {
1606
1743
  continue;
1607
1744
  }
1608
1745
  validated++;
1746
+ if (jsonMode) {
1747
+ jsonResults.push({
1748
+ key: entry.key,
1749
+ status: result2.status,
1750
+ provider: result2.provider,
1751
+ latencyMs: result2.latencyMs,
1752
+ message: result2.status !== "valid" ? result2.message : void 0
1753
+ });
1754
+ continue;
1755
+ }
1609
1756
  const icon2 = result2.status === "valid" ? c.green(SYMBOLS.check) : result2.status === "invalid" ? c.red(SYMBOLS.cross) : c.yellow(SYMBOLS.warning);
1610
1757
  const statusText = result2.status === "valid" ? c.green("valid") : result2.status === "invalid" ? c.red("invalid") : c.yellow("error");
1611
1758
  console.log(
1612
1759
  ` ${icon2} ${c.bold(entry.key.padEnd(24))} ${statusText} ${c.dim(`(${result2.provider}, ${result2.latencyMs}ms)`)}${result2.status !== "valid" ? ` ${c.dim("\u2014 " + result2.message)}` : ""}`
1613
1760
  );
1614
1761
  }
1762
+ if (jsonMode) {
1763
+ emitJson(program2, cmd, { results: jsonResults, validated, skipped });
1764
+ return;
1765
+ }
1615
1766
  console.log(
1616
1767
  `
1617
1768
  ${c.dim(`${validated} validated, ${skipped} skipped (no provider)`)}
@@ -1627,6 +1778,15 @@ function registerValidationCommands(program2) {
1627
1778
  const envelope = getEnvelope(key, opts);
1628
1779
  const provHint = envelope?.envelope.meta.provider ?? cmd.provider;
1629
1780
  const result = await validateSecret(value, { provider: provHint });
1781
+ if (emitJson(program2, cmd, {
1782
+ key,
1783
+ status: result.status,
1784
+ provider: result.provider,
1785
+ latencyMs: result.latencyMs,
1786
+ message: result.message
1787
+ })) {
1788
+ return;
1789
+ }
1630
1790
  const icon = result.status === "valid" ? c.green(SYMBOLS.check) : result.status === "invalid" ? c.red(SYMBOLS.cross) : result.status === "error" ? c.yellow(SYMBOLS.warning) : c.dim("\u25CB");
1631
1791
  console.log(
1632
1792
  `
@@ -1639,7 +1799,7 @@ function registerValidationCommands(program2) {
1639
1799
  });
1640
1800
  program2.command("rotate <key>").description(
1641
1801
  "Attempt issuer-native rotation of a secret via its provider"
1642
- ).option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").option("--provider <name>", "Force a specific provider").action(async (key, cmd) => {
1802
+ ).option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").option("--provider <name>", "Force a specific provider").option("--json", "Output as JSON").action(async (key, cmd) => {
1643
1803
  const opts = buildOpts(cmd);
1644
1804
  const value = getSecret(key, opts);
1645
1805
  if (!value) {
@@ -1653,6 +1813,16 @@ function registerValidationCommands(program2) {
1653
1813
  ...opts,
1654
1814
  scope: opts.scope ?? "global"
1655
1815
  });
1816
+ }
1817
+ if (emitJson(program2, cmd, {
1818
+ key,
1819
+ rotated: !!(result.rotated && result.newValue),
1820
+ provider: result.provider,
1821
+ message: result.message
1822
+ })) {
1823
+ return;
1824
+ }
1825
+ if (result.rotated && result.newValue) {
1656
1826
  console.log(
1657
1827
  `${SYMBOLS.check} ${c.green("Rotated")} ${c.bold(key)} via ${result.provider}`
1658
1828
  );
@@ -2173,15 +2343,23 @@ function registerToolingCommands(program2) {
2173
2343
  program2.command("scan [dir]").description("Scan a codebase for hardcoded secrets").option(
2174
2344
  "--fix",
2175
2345
  "Auto-replace hardcoded secrets with process.env references and store in q-ring"
2176
- ).option("-g, --global", "Store fixed secrets in global scope").option("-p, --project", "Store fixed secrets in project scope").option("--project-path <path>", "Explicit project path").action((dir, cmd) => {
2346
+ ).option("-g, --global", "Store fixed secrets in global scope").option("-p, --project", "Store fixed secrets in project scope").option("--project-path <path>", "Explicit project path").option("--json", "Output as JSON").action((dir, cmd) => {
2177
2347
  const targetDir = dir ?? process.cwd();
2178
2348
  const fixMode = cmd.fix === true;
2179
- console.log(
2180
- `
2349
+ const jsonMode = wantsJsonOutput(program2, cmd);
2350
+ if (!jsonMode) {
2351
+ console.log(
2352
+ `
2181
2353
  ${SYMBOLS.eye} Scanning ${c.bold(targetDir)} for secrets...${fixMode ? c.yellow(" [--fix mode]") : ""}
2182
2354
  `
2183
- );
2355
+ );
2356
+ }
2184
2357
  const results = scanCodebase(targetDir);
2358
+ if (jsonMode && !fixMode) {
2359
+ emitJson(program2, cmd, { dir: targetDir, findings: results });
2360
+ if (results.length > 0) process.exitCode = 1;
2361
+ return;
2362
+ }
2185
2363
  if (results.length === 0) {
2186
2364
  console.log(
2187
2365
  ` ${c.green(SYMBOLS.check)} No hardcoded secrets found. Awesome!
@@ -2202,6 +2380,14 @@ function registerToolingCommands(program2) {
2202
2380
  projectPath: opts.projectPath
2203
2381
  });
2204
2382
  const fixedCount = lintResults.filter((r) => r.fixed).length;
2383
+ if (jsonMode) {
2384
+ emitJson(program2, cmd, {
2385
+ dir: targetDir,
2386
+ findings: results,
2387
+ fixed: fixedCount
2388
+ });
2389
+ return;
2390
+ }
2205
2391
  console.log(
2206
2392
  ` ${c.green(SYMBOLS.check)} Fixed ${fixedCount} secrets \u2014 replaced with process.env references and stored in q-ring.
2207
2393
  `
@@ -2227,13 +2413,17 @@ function registerToolingCommands(program2) {
2227
2413
  ).option(
2228
2414
  "--fix",
2229
2415
  "Replace hardcoded secrets with process.env references and store in q-ring"
2230
- ).option("-g, --global", "Store fixed secrets in global scope").option("-p, --project", "Store fixed secrets in project scope").option("--project-path <path>", "Explicit project path").action((files, cmd) => {
2416
+ ).option("-g, --global", "Store fixed secrets in global scope").option("-p, --project", "Store fixed secrets in project scope").option("--project-path <path>", "Explicit project path").option("--json", "Output as JSON").action((files, cmd) => {
2231
2417
  const opts = buildOpts(cmd);
2232
2418
  const results = lintFiles(files, {
2233
2419
  fix: cmd.fix,
2234
2420
  scope: opts.scope,
2235
2421
  projectPath: opts.projectPath
2236
2422
  });
2423
+ if (emitJson(program2, cmd, { files, findings: results })) {
2424
+ if (results.some((r) => !r.fixed)) process.exitCode = 1;
2425
+ return;
2426
+ }
2237
2427
  if (results.length === 0) {
2238
2428
  console.log(
2239
2429
  `
@@ -2267,7 +2457,7 @@ function registerToolingCommands(program2) {
2267
2457
  }
2268
2458
  });
2269
2459
  program2.command("status").description("Launch the quantum status dashboard in your browser").option("--port <port>", "Port to serve on", "9876").option("--no-open", "Don't auto-open the browser").action(async (cmd) => {
2270
- const { startDashboardServer } = await import("./dashboard-6XKC5BL4.js");
2460
+ const { startDashboardServer } = await import("./dashboard-2IGUR7PA.js");
2271
2461
  const { exec } = await import("child_process");
2272
2462
  const { platform } = await import("os");
2273
2463
  const port = Number(cmd.port);
@@ -2303,9 +2493,10 @@ function registerAuditCommands(program2) {
2303
2493
  program2.command("audit").description("View the audit log (observer effect)").option("-k, --key <key>", "Filter by key").option(
2304
2494
  "-a, --action <action>",
2305
2495
  "Filter by action (read, write, delete, etc.)"
2306
- ).option("-n, --limit <n>", "Number of events to show", parseInt, 20).option("--anomalies", "Detect access anomalies").action((cmd) => {
2496
+ ).option("-n, --limit <n>", "Number of events to show", parseInt, 20).option("--anomalies", "Detect access anomalies").option("--json", "Output as JSON").action((cmd) => {
2307
2497
  if (cmd.anomalies) {
2308
2498
  const anomalies = detectAnomalies(cmd.key);
2499
+ if (emitJson(program2, cmd, { anomalies })) return;
2309
2500
  if (anomalies.length === 0) {
2310
2501
  console.log(`${SYMBOLS.shield} ${c.green("No anomalies detected")}`);
2311
2502
  return;
@@ -2326,6 +2517,7 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2326
2517
  action: cmd.action,
2327
2518
  limit: cmd.limit
2328
2519
  });
2520
+ if (emitJson(program2, cmd, { events })) return;
2329
2521
  if (events.length === 0) {
2330
2522
  console.log(c.dim("No audit events found"));
2331
2523
  return;
@@ -2349,8 +2541,12 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2349
2541
  }
2350
2542
  console.log();
2351
2543
  });
2352
- program2.command("audit:verify").description("Verify the integrity of the audit hash chain").action(() => {
2544
+ program2.command("audit:verify").description("Verify the integrity of the audit hash chain").option("--json", "Output as JSON").action((cmd) => {
2353
2545
  const result = verifyAuditChain();
2546
+ if (emitJson(program2, cmd, result)) {
2547
+ if (!result.intact && result.totalEvents > 0) process.exitCode = 1;
2548
+ return;
2549
+ }
2354
2550
  if (result.totalEvents === 0) {
2355
2551
  console.log(c.dim(" No audit events to verify"));
2356
2552
  return;
@@ -2391,42 +2587,58 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2391
2587
  console.log(output);
2392
2588
  }
2393
2589
  });
2394
- program2.command("health").description("Check the health of all secrets").option("-g, --global", "Check global scope only").option("-p, --project", "Check project scope only").option("--project-path <path>", "Explicit project path").action((cmd) => {
2590
+ program2.command("health").description("Check the health of all secrets").option("-g, --global", "Check global scope only").option("-p, --project", "Check project scope only").option("--project-path <path>", "Explicit project path").option("--json", "Output as JSON").action((cmd) => {
2395
2591
  const opts = buildOpts(cmd);
2396
2592
  const entries = listSecrets(opts);
2397
- if (entries.length === 0) {
2398
- console.log(c.dim("No secrets to check"));
2399
- return;
2400
- }
2401
- console.log(c.bold(`
2402
- ${SYMBOLS.shield} Secret health report
2403
- `));
2593
+ const expiredKeys = [];
2594
+ const staleKeys = [];
2404
2595
  let healthy = 0;
2405
- let stale = 0;
2406
- let expired = 0;
2407
2596
  let noDecay = 0;
2408
2597
  for (const entry of entries) {
2409
2598
  if (!entry.decay || !entry.decay.timeRemaining) {
2410
2599
  noDecay++;
2411
- continue;
2412
- }
2413
- if (entry.decay.isExpired) {
2414
- expired++;
2415
- console.log(
2416
- ` ${c.red(SYMBOLS.cross)} ${c.bold(entry.key)} ${c.bgRed(c.white(" EXPIRED "))}`
2417
- );
2600
+ } else if (entry.decay.isExpired) {
2601
+ expiredKeys.push(entry.key);
2418
2602
  } else if (entry.decay.isStale) {
2419
- stale++;
2420
- console.log(
2421
- ` ${c.yellow(SYMBOLS.warning)} ${c.bold(entry.key)} ${c.yellow(`stale (${entry.decay.lifetimePercent}%, ${entry.decay.timeRemaining} left)`)}`
2422
- );
2603
+ staleKeys.push({
2604
+ key: entry.key,
2605
+ lifetimePercent: entry.decay.lifetimePercent,
2606
+ timeRemaining: entry.decay.timeRemaining
2607
+ });
2423
2608
  } else {
2424
2609
  healthy++;
2425
2610
  }
2426
2611
  }
2612
+ if (emitJson(program2, cmd, {
2613
+ total: entries.length,
2614
+ healthy,
2615
+ noDecay,
2616
+ expired: expiredKeys,
2617
+ stale: staleKeys,
2618
+ anomalies: entries.length === 0 ? [] : detectAnomalies()
2619
+ })) {
2620
+ return;
2621
+ }
2622
+ if (entries.length === 0) {
2623
+ console.log(c.dim("No secrets to check"));
2624
+ return;
2625
+ }
2626
+ console.log(c.bold(`
2627
+ ${SYMBOLS.shield} Secret health report
2628
+ `));
2629
+ for (const key of expiredKeys) {
2630
+ console.log(
2631
+ ` ${c.red(SYMBOLS.cross)} ${c.bold(key)} ${c.bgRed(c.white(" EXPIRED "))}`
2632
+ );
2633
+ }
2634
+ for (const s of staleKeys) {
2635
+ console.log(
2636
+ ` ${c.yellow(SYMBOLS.warning)} ${c.bold(s.key)} ${c.yellow(`stale (${s.lifetimePercent}%, ${s.timeRemaining} left)`)}`
2637
+ );
2638
+ }
2427
2639
  console.log(
2428
2640
  `
2429
- ${c.green(`${SYMBOLS.check} ${healthy} healthy`)} ${c.yellow(`${SYMBOLS.warning} ${stale} stale`)} ${c.red(`${SYMBOLS.cross} ${expired} expired`)} ${c.dim(`${noDecay} no decay`)}`
2641
+ ${c.green(`${SYMBOLS.check} ${healthy} healthy`)} ${c.yellow(`${SYMBOLS.warning} ${staleKeys.length} stale`)} ${c.red(`${SYMBOLS.cross} ${expiredKeys.length} expired`)} ${c.dim(`${noDecay} no decay`)}`
2430
2642
  );
2431
2643
  const anomalies = detectAnomalies();
2432
2644
  if (anomalies.length > 0) {
@@ -2590,8 +2802,9 @@ function registerHookCommands(program2) {
2590
2802
  if (cmd.keyPattern) console.log(c.dim(` pattern: ${cmd.keyPattern}`));
2591
2803
  if (cmd.tag) console.log(c.dim(` tag: ${cmd.tag}`));
2592
2804
  });
2593
- hook.command("list").alias("ls").description("List all registered hooks").action(() => {
2805
+ hook.command("list").alias("ls").description("List all registered hooks").option("--json", "Output as JSON").action((cmd) => {
2594
2806
  const hooks = listHooks();
2807
+ if (emitJson(program2, cmd, { hooks })) return;
2595
2808
  if (hooks.length === 0) {
2596
2809
  console.log(c.dim("No hooks registered"));
2597
2810
  return;
@@ -2815,9 +3028,16 @@ function registerAgentCommands(program2) {
2815
3028
  remember(key, value);
2816
3029
  console.log(`${SYMBOLS.check} ${c.green("remembered")} ${c.bold(key)}`);
2817
3030
  });
2818
- program2.command("recall [key]").description("Retrieve a value from agent memory, or list all keys").action((key) => {
3031
+ program2.command("recall [key]").description("Retrieve a value from agent memory, or list all keys").option("--json", "Output as JSON (list mode only)").action((key, cmd) => {
2819
3032
  if (!key) {
2820
3033
  const entries = listMemory();
3034
+ if (emitJson(
3035
+ program2,
3036
+ cmd,
3037
+ entries.map((e) => ({ key: e.key, updatedAt: e.updatedAt }))
3038
+ )) {
3039
+ return;
3040
+ }
2821
3041
  if (entries.length === 0) {
2822
3042
  console.log(c.dim("Agent memory is empty."));
2823
3043
  return;
@@ -2842,8 +3062,17 @@ ${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
2842
3062
  console.log(safeStr(value));
2843
3063
  }
2844
3064
  });
2845
- program2.command("forget <key>").description("Delete a key from agent memory").option("--all", "Clear all agent memory").action((key, cmd) => {
3065
+ program2.command("forget <key>").description("Delete a key from agent memory").option("--all", "Clear all agent memory").option("-y, --yes", "Skip the confirmation prompt").action(async (key, cmd) => {
2846
3066
  if (cmd.all) {
3067
+ const count = listMemory().length;
3068
+ if (!await confirm(
3069
+ `Clear ALL agent memory (${count} entr${count === 1 ? "y" : "ies"})? This cannot be undone.`,
3070
+ { assumeYes: cmd.yes }
3071
+ )) {
3072
+ console.error(c.dim("Aborted."));
3073
+ process.exitCode = 1;
3074
+ return;
3075
+ }
2847
3076
  clearMemory();
2848
3077
  console.log(`${SYMBOLS.check} ${c.yellow("cleared")} all agent memory`);
2849
3078
  return;
@@ -2857,13 +3086,10 @@ ${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
2857
3086
  });
2858
3087
  program2.command("analyze").description(
2859
3088
  "Analyze secret usage patterns and provide optimization suggestions"
2860
- ).option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--project-path <path>", "Explicit project path").action((cmd) => {
3089
+ ).option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--project-path <path>", "Explicit project path").option("--json", "Output as JSON").action((cmd) => {
2861
3090
  const opts = buildOpts(cmd);
2862
3091
  const entries = listSecrets({ ...opts, silent: true });
2863
3092
  const audit = queryAudit({ limit: 1e3 });
2864
- console.log(`
2865
- ${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
2866
- `);
2867
3093
  const accessMap = /* @__PURE__ */ new Map();
2868
3094
  for (const e of audit) {
2869
3095
  if (e.action === "read" && e.key) {
@@ -2871,6 +3097,39 @@ ${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
2871
3097
  }
2872
3098
  }
2873
3099
  const sorted = [...accessMap.entries()].sort((a, b) => b[1] - a[1]);
3100
+ const neverAccessed = entries.filter((e) => {
3101
+ const count = e.envelope?.meta.accessCount ?? 0;
3102
+ return count === 0;
3103
+ });
3104
+ const jsonExpired = entries.filter((e) => e.decay?.isExpired);
3105
+ const jsonStale = entries.filter(
3106
+ (e) => e.decay?.isStale && !e.decay?.isExpired
3107
+ );
3108
+ const jsonNoRotation = entries.filter(
3109
+ (e) => !e.envelope?.meta.rotationFormat && !e.decay?.isExpired
3110
+ );
3111
+ if (emitJson(program2, cmd, {
3112
+ mostAccessed: sorted.slice(0, 5).map(([key, reads]) => ({ key, reads })),
3113
+ neverAccessed: neverAccessed.map((e) => e.key),
3114
+ expired: jsonExpired.map((e) => e.key),
3115
+ stale: jsonStale.map((e) => ({
3116
+ key: e.key,
3117
+ timeRemaining: e.decay?.timeRemaining ?? null
3118
+ })),
3119
+ summary: {
3120
+ total: entries.length,
3121
+ active: entries.length - jsonExpired.length,
3122
+ expired: jsonExpired.length,
3123
+ stale: jsonStale.length,
3124
+ neverAccessed: neverAccessed.length,
3125
+ withRotationConfig: entries.length - jsonNoRotation.length
3126
+ }
3127
+ })) {
3128
+ return;
3129
+ }
3130
+ console.log(`
3131
+ ${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
3132
+ `);
2874
3133
  if (sorted.length > 0) {
2875
3134
  console.log(` ${c.bold("Most accessed:")}`);
2876
3135
  for (const [key, count] of sorted.slice(0, 5)) {
@@ -2878,10 +3137,6 @@ ${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
2878
3137
  }
2879
3138
  console.log();
2880
3139
  }
2881
- const neverAccessed = entries.filter((e) => {
2882
- const count = e.envelope?.meta.accessCount ?? 0;
2883
- return count === 0;
2884
- });
2885
3140
  if (neverAccessed.length > 0) {
2886
3141
  console.log(
2887
3142
  ` ${c.bold("Never accessed:")} ${c.yellow(neverAccessed.length.toString())} secrets`
@@ -3033,8 +3288,9 @@ function registerSecurityCommands(program2) {
3033
3288
  )
3034
3289
  );
3035
3290
  });
3036
- program2.command("approvals").description("List all approval tokens with verification status").action(() => {
3291
+ program2.command("approvals").description("List all approval tokens with verification status").option("--json", "Output as JSON").action((cmd) => {
3037
3292
  const approvals = listApprovals();
3293
+ if (emitJson(program2, cmd, { approvals })) return;
3038
3294
  if (approvals.length === 0) {
3039
3295
  console.log(c.dim(" No approvals found"));
3040
3296
  return;
@@ -3115,6 +3371,290 @@ ${SYMBOLS.lock} Approval Tokens
3115
3371
  });
3116
3372
  }
3117
3373
 
3374
+ // src/cli/commands/doctor.ts
3375
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5, rmSync } from "fs";
3376
+ import { join as join3, delimiter } from "path";
3377
+ import { homedir } from "os";
3378
+ function auditDir() {
3379
+ return process.env.QRING_AUDIT_DIR ?? join3(homedir(), ".config", "q-ring");
3380
+ }
3381
+ function checkNode() {
3382
+ const major = Number(process.versions.node.split(".")[0]);
3383
+ return {
3384
+ name: "node runtime",
3385
+ status: major >= 18 ? "ok" : "fail",
3386
+ detail: `v${process.versions.node}${major >= 18 ? "" : " \u2014 q-ring requires Node >= 18"}`
3387
+ };
3388
+ }
3389
+ async function checkKeyringBackend() {
3390
+ try {
3391
+ const { Entry } = await import("@napi-rs/keyring");
3392
+ const probe = new Entry("q-ring-doctor-probe", "PROBE");
3393
+ probe.setPassword("ok");
3394
+ const read = probe.getPassword();
3395
+ probe.deletePassword();
3396
+ if (read !== "ok") {
3397
+ return {
3398
+ name: "keyring backend",
3399
+ status: "fail",
3400
+ detail: "probe round-trip returned unexpected value"
3401
+ };
3402
+ }
3403
+ return {
3404
+ name: "keyring backend",
3405
+ status: "ok",
3406
+ detail: "write/read/delete round-trip succeeded"
3407
+ };
3408
+ } catch (err) {
3409
+ return {
3410
+ name: "keyring backend",
3411
+ status: "fail",
3412
+ detail: `${err instanceof Error ? err.message : String(err)} \u2014 on headless Linux, install and unlock a Secret Service (e.g. gnome-keyring)`
3413
+ };
3414
+ }
3415
+ }
3416
+ function checkAuditLog() {
3417
+ const dir = auditDir();
3418
+ try {
3419
+ const probeFile = join3(dir, ".doctor-probe");
3420
+ writeFileSync5(probeFile, "ok");
3421
+ rmSync(probeFile);
3422
+ } catch (err) {
3423
+ return {
3424
+ name: "audit log",
3425
+ status: "fail",
3426
+ detail: `${dir} not writable: ${err instanceof Error ? err.message : String(err)}`
3427
+ };
3428
+ }
3429
+ const chain = verifyAuditChain();
3430
+ if (chain.totalEvents === 0) {
3431
+ return { name: "audit log", status: "ok", detail: `${dir} writable, no events yet` };
3432
+ }
3433
+ if (!chain.intact) {
3434
+ return {
3435
+ name: "audit log",
3436
+ status: "fail",
3437
+ detail: `hash chain BROKEN at event #${chain.brokenAt} (${chain.validEvents}/${chain.totalEvents} valid) \u2014 see qring audit:verify`
3438
+ };
3439
+ }
3440
+ return {
3441
+ name: "audit log",
3442
+ status: "ok",
3443
+ detail: `${dir} writable, chain intact (${chain.totalEvents} events)`
3444
+ };
3445
+ }
3446
+ function checkManifest(projectPath) {
3447
+ const manifestPath = join3(projectPath, ".q-ring.json");
3448
+ if (!existsSync4(manifestPath)) {
3449
+ return {
3450
+ name: "project manifest",
3451
+ status: "warn",
3452
+ detail: `no .q-ring.json in ${projectPath} (optional \u2014 run qring wizard or /qring-setup-project)`
3453
+ };
3454
+ }
3455
+ try {
3456
+ const config = JSON.parse(readFileSync6(manifestPath, "utf8"));
3457
+ const declared = Object.keys(config.secrets ?? {}).length;
3458
+ const hasPolicy = !!config.policy;
3459
+ return {
3460
+ name: "project manifest",
3461
+ status: "ok",
3462
+ detail: `${declared} secret(s) declared${hasPolicy ? ", policy present" : ""}`
3463
+ };
3464
+ } catch (err) {
3465
+ return {
3466
+ name: "project manifest",
3467
+ status: "fail",
3468
+ detail: `.q-ring.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
3469
+ };
3470
+ }
3471
+ }
3472
+ function checkPolicy() {
3473
+ const summary = getPolicySummary();
3474
+ const active = [
3475
+ summary.hasMcpPolicy && "mcp",
3476
+ summary.hasExecPolicy && "exec",
3477
+ summary.hasSecretPolicy && "secrets"
3478
+ ].filter(Boolean);
3479
+ return {
3480
+ name: "governance policy",
3481
+ status: "ok",
3482
+ detail: active.length ? `active: ${active.join(", ")}` : 'none configured (add a "policy" section to .q-ring.json to enable)'
3483
+ };
3484
+ }
3485
+ function checkMcpBinary() {
3486
+ const exts = process.platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
3487
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
3488
+ if (!dir) continue;
3489
+ for (const ext of exts) {
3490
+ if (existsSync4(join3(dir, `qring-mcp${ext}`))) {
3491
+ return {
3492
+ name: "qring-mcp binary",
3493
+ status: "ok",
3494
+ detail: `found at ${join3(dir, `qring-mcp${ext}`)}`
3495
+ };
3496
+ }
3497
+ }
3498
+ }
3499
+ return {
3500
+ name: "qring-mcp binary",
3501
+ status: "warn",
3502
+ detail: "not on PATH \u2014 editor MCP configs that spawn `qring-mcp` will fail; install globally (pnpm add -g @i4ctime/q-ring)"
3503
+ };
3504
+ }
3505
+ function registerDoctorCommand(program2) {
3506
+ program2.command("doctor").description("Diagnose the q-ring installation (keyring, audit, manifest, MCP)").option("--project-path <path>", "Project path (defaults to cwd)").option("--json", "Output as JSON").action(async (cmd) => {
3507
+ const projectPath = cmd.projectPath ?? process.cwd();
3508
+ const checks = [
3509
+ checkNode(),
3510
+ await checkKeyringBackend(),
3511
+ checkAuditLog(),
3512
+ checkManifest(projectPath),
3513
+ checkPolicy(),
3514
+ checkMcpBinary()
3515
+ ];
3516
+ const failed = checks.filter((r) => r.status === "fail").length;
3517
+ const warned = checks.filter((r) => r.status === "warn").length;
3518
+ if (emitJson(program2, cmd, {
3519
+ version: PACKAGE_VERSION,
3520
+ projectPath,
3521
+ checks,
3522
+ healthy: failed === 0
3523
+ })) {
3524
+ if (failed > 0) process.exitCode = 1;
3525
+ return;
3526
+ }
3527
+ console.log(c.bold(`
3528
+ ${SYMBOLS.shield} qring doctor \u2014 v${PACKAGE_VERSION}
3529
+ `));
3530
+ for (const r of checks) {
3531
+ const icon = r.status === "ok" ? c.green(SYMBOLS.check) : r.status === "warn" ? c.yellow(SYMBOLS.warning) : c.red(SYMBOLS.cross);
3532
+ console.log(` ${icon} ${c.bold(r.name.padEnd(18))} ${c.dim(r.detail)}`);
3533
+ }
3534
+ console.log();
3535
+ if (failed > 0) {
3536
+ console.log(` ${c.red(`${failed} check(s) failed`)}${warned ? c.dim(`, ${warned} warning(s)`) : ""}
3537
+ `);
3538
+ process.exitCode = 1;
3539
+ } else if (warned > 0) {
3540
+ console.log(` ${c.yellow(`${warned} warning(s)`)} ${c.dim("\u2014 everything critical is healthy")}
3541
+ `);
3542
+ } else {
3543
+ console.log(` ${c.green("All checks passed.")}
3544
+ `);
3545
+ }
3546
+ });
3547
+ }
3548
+
3549
+ // src/cli/commands/completion.ts
3550
+ function collect(cmd) {
3551
+ return {
3552
+ name: cmd.name(),
3553
+ aliases: cmd.aliases(),
3554
+ description: cmd.description(),
3555
+ options: cmd.options.map((o) => ({ flag: o.long ?? o.short ?? "", description: o.description ?? "" })).filter((o) => o.flag.startsWith("--")),
3556
+ subcommands: cmd.commands.filter((s) => s.name() !== "help").map((s) => collect(s))
3557
+ };
3558
+ }
3559
+ var sanitize = (s) => s.replace(/['"`$\\]/g, "").replace(/[:[\]]/g, " ").replace(/\s+/g, " ").trim();
3560
+ function bashScript(cmds) {
3561
+ const names = cmds.flatMap((x) => [x.name, ...x.aliases]).join(" ");
3562
+ const cases = cmds.map((x) => {
3563
+ const label = [x.name, ...x.aliases].join("|");
3564
+ const opts = [...x.options.map((o) => o.flag), "--json"].join(" ");
3565
+ const subs = x.subcommands.map((s) => s.name).join(" ");
3566
+ const subCase = subs ? `
3567
+ if [ "$COMP_CWORD" -eq 2 ]; then COMPREPLY=( $(compgen -W "${subs}" -- "$cur") ); return; fi` : "";
3568
+ return ` ${label})${subCase}
3569
+ opts="${opts}" ;;`;
3570
+ }).join("\n");
3571
+ return `# bash completion for qring \u2014 generated by \`qring completion bash\`
3572
+ _qring_completions() {
3573
+ local cur cmd opts
3574
+ cur="\${COMP_WORDS[COMP_CWORD]}"
3575
+ cmd="\${COMP_WORDS[1]}"
3576
+ if [ "$COMP_CWORD" -eq 1 ]; then
3577
+ COMPREPLY=( $(compgen -W "${names}" -- "$cur") )
3578
+ return
3579
+ fi
3580
+ opts="--json --help"
3581
+ case "$cmd" in
3582
+ ${cases}
3583
+ esac
3584
+ case "$cur" in
3585
+ -*) COMPREPLY=( $(compgen -W "$opts --help" -- "$cur") ) ;;
3586
+ esac
3587
+ }
3588
+ complete -o default -F _qring_completions qring
3589
+ `;
3590
+ }
3591
+ function zshScript(cmds) {
3592
+ const items = cmds.map((x) => ` '${x.name}:${sanitize(x.description)}'`).join("\n");
3593
+ const cases = cmds.map((x) => {
3594
+ const label = [x.name, ...x.aliases].join("|");
3595
+ const words = [
3596
+ ...x.subcommands.map((s) => s.name),
3597
+ ...x.options.map((o) => o.flag),
3598
+ "--json"
3599
+ ].join(" ");
3600
+ return ` ${label}) compadd -- ${words} ;;`;
3601
+ }).join("\n");
3602
+ return `#compdef qring
3603
+ # zsh completion for qring \u2014 generated by \`qring completion zsh\`
3604
+ _qring() {
3605
+ local -a commands
3606
+ commands=(
3607
+ ${items}
3608
+ )
3609
+ if (( CURRENT == 2 )); then
3610
+ _describe 'command' commands
3611
+ return
3612
+ fi
3613
+ case $words[2] in
3614
+ ${cases}
3615
+ esac
3616
+ }
3617
+ _qring "$@"
3618
+ `;
3619
+ }
3620
+ function fishScript(cmds) {
3621
+ const lines = [
3622
+ "# fish completion for qring \u2014 generated by `qring completion fish`",
3623
+ "complete -c qring -f"
3624
+ ];
3625
+ const allNames = cmds.flatMap((x) => [x.name, ...x.aliases]);
3626
+ for (const x of cmds) {
3627
+ for (const name of [x.name, ...x.aliases]) {
3628
+ lines.push(
3629
+ `complete -c qring -n "not __fish_seen_subcommand_from ${allNames.join(" ")}" -a ${name} -d "${sanitize(x.description)}"`
3630
+ );
3631
+ }
3632
+ for (const s of x.subcommands) {
3633
+ lines.push(
3634
+ `complete -c qring -n "__fish_seen_subcommand_from ${x.name}" -a ${s.name} -d "${sanitize(s.description)}"`
3635
+ );
3636
+ }
3637
+ for (const o of [...x.options, { flag: "--json", description: "Output as JSON" }]) {
3638
+ lines.push(
3639
+ `complete -c qring -n "__fish_seen_subcommand_from ${x.name}" -l ${o.flag.slice(2)} -d "${sanitize(o.description)}"`
3640
+ );
3641
+ }
3642
+ }
3643
+ return lines.join("\n") + "\n";
3644
+ }
3645
+ function registerCompletionCommand(program2) {
3646
+ program2.command("completion <shell>").description("Print a shell completion script (bash, zsh, fish)").action((shell) => {
3647
+ const cmds = program2.commands.filter((s) => s.name() !== "help" && s.name() !== "completion").map((s) => collect(s));
3648
+ if (shell === "bash") process.stdout.write(bashScript(cmds));
3649
+ else if (shell === "zsh") process.stdout.write(zshScript(cmds));
3650
+ else if (shell === "fish") process.stdout.write(fishScript(cmds));
3651
+ else {
3652
+ console.error(c.red(`Unknown shell "${shell}" \u2014 expected bash, zsh, or fish`));
3653
+ process.exitCode = 1;
3654
+ }
3655
+ });
3656
+ }
3657
+
3118
3658
  // src/cli/commands.ts
3119
3659
  var COMMAND_GROUPS = [
3120
3660
  {
@@ -3123,6 +3663,7 @@ var COMMAND_GROUPS = [
3123
3663
  commands: [
3124
3664
  "set",
3125
3665
  "get",
3666
+ "has",
3126
3667
  "delete",
3127
3668
  "list",
3128
3669
  "inspect",
@@ -3154,7 +3695,7 @@ var COMMAND_GROUPS = [
3154
3695
  {
3155
3696
  name: "Dev Tooling",
3156
3697
  symbol: SYMBOLS.zap,
3157
- commands: ["exec", "scan", "lint", "status"]
3698
+ commands: ["exec", "scan", "lint", "status", "doctor", "completion"]
3158
3699
  },
3159
3700
  {
3160
3701
  name: "Audit & Health",
@@ -3296,10 +3837,21 @@ function createProgram() {
3296
3837
  registerHookCommands(program2);
3297
3838
  registerAgentCommands(program2);
3298
3839
  registerSecurityCommands(program2);
3840
+ registerDoctorCommand(program2);
3841
+ registerCompletionCommand(program2);
3299
3842
  return program2;
3300
3843
  }
3301
3844
 
3302
3845
  // src/index.ts
3303
3846
  var program = createProgram();
3304
- program.parse();
3847
+ try {
3848
+ await program.parseAsync();
3849
+ } catch (err) {
3850
+ const message = err instanceof Error ? err.message : String(err);
3851
+ console.error(c.red(`${SYMBOLS.cross} ${message}`));
3852
+ if (process.env.QRING_DEBUG && err instanceof Error && err.stack) {
3853
+ console.error(c.dim(err.stack));
3854
+ }
3855
+ process.exitCode = 1;
3856
+ }
3305
3857
  //# sourceMappingURL=index.js.map