@i4ctime/q-ring 0.11.7 → 0.13.0

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-A4RZVP3P.js";
45
+ } from "./chunk-SRESNRML.js";
46
46
 
47
47
  // src/cli/commands.ts
48
48
  import { Command, Help } from "commander";
@@ -137,8 +137,11 @@ function parseDotenv(content) {
137
137
  '"': '"'
138
138
  };
139
139
  value = value.replace(/\\([nrt"\\])/g, (_, ch) => escapeMap[ch] ?? ch);
140
- if (value.includes("#") && !line.includes('"') && !line.includes("'")) {
141
- value = value.split("#")[0].trim();
140
+ if (!line.includes('"') && !line.includes("'")) {
141
+ const commentMatch = value.match(/\s#/);
142
+ if (commentMatch && commentMatch.index !== void 0) {
143
+ value = value.slice(0, commentMatch.index).trim();
144
+ }
142
145
  }
143
146
  if (key) result.set(key, value);
144
147
  }
@@ -146,9 +149,14 @@ function parseDotenv(content) {
146
149
  }
147
150
  function importDotenv(filePathOrContent, options = {}) {
148
151
  let content;
149
- try {
150
- content = readFileSync(filePathOrContent, "utf8");
151
- } catch {
152
+ const source = options.source ?? "cli";
153
+ if (source === "cli") {
154
+ try {
155
+ content = readFileSync(filePathOrContent, "utf8");
156
+ } catch {
157
+ content = filePathOrContent;
158
+ }
159
+ } else {
152
160
  content = filePathOrContent;
153
161
  }
154
162
  const pairs = parseDotenv(content);
@@ -183,7 +191,40 @@ function importDotenv(filePathOrContent, options = {}) {
183
191
 
184
192
  // src/utils/prompt.ts
185
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
+ }
186
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
+ }
187
228
  const rl = createInterface({
188
229
  input: process.stdin,
189
230
  output: process.stderr,
@@ -191,7 +232,6 @@ async function promptSecret(message) {
191
232
  });
192
233
  return new Promise((resolve) => {
193
234
  process.stderr.write(message);
194
- const stdin = process.stdin;
195
235
  const wasRaw = stdin.isRaw;
196
236
  if (stdin.isTTY && stdin.setRawMode) {
197
237
  stdin.setRawMode(true);
@@ -226,6 +266,11 @@ function wantsJsonOutput(program2, cmd) {
226
266
  const g = program2.opts();
227
267
  return !!g.json;
228
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
+ }
229
274
  function safeStr(s) {
230
275
  return s == null ? "" : `${s}`;
231
276
  }
@@ -352,17 +397,36 @@ function registerSecretsCommands(program2) {
352
397
  const payload = wantsJsonOutput(program2, cmd) ? { ok: true, data: { key, value } } : { key, value };
353
398
  process.stdout.write(JSON.stringify(payload) + "\n");
354
399
  });
355
- 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) => {
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) => {
356
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
+ }
357
421
  const deleted = deleteSecret(key, opts);
358
- if (deleted) {
359
- console.log(`${SYMBOLS.check} ${c.green("deleted")} ${c.bold(key)}`);
360
- } else {
422
+ if (!deleted) {
361
423
  console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
362
424
  process.exit(1);
363
425
  }
426
+ if (emitJson(program2, cmd, { key, deleted: true })) return;
427
+ console.log(`${SYMBOLS.check} ${c.green("deleted")} ${c.bold(key)}`);
364
428
  });
365
- 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) => {
366
430
  const opts = buildOpts(cmd);
367
431
  let entries = listSecrets(opts);
368
432
  if (cmd.tag) {
@@ -381,6 +445,22 @@ function registerSecretsCommands(program2) {
381
445
  if (cmd.filter) {
382
446
  entries = filterSecretsByKeyGlob(entries, cmd.filter);
383
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
+ }
384
464
  if (entries.length === 0) {
385
465
  console.log(c.dim("No secrets found"));
386
466
  return;
@@ -426,7 +506,7 @@ function registerSecretsCommands(program2) {
426
506
  }
427
507
  console.log();
428
508
  });
429
- 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) => {
430
510
  const opts = buildOpts(cmd);
431
511
  const result = getEnvelope(key, opts);
432
512
  if (!result) {
@@ -435,6 +515,26 @@ function registerSecretsCommands(program2) {
435
515
  }
436
516
  const { envelope, scope } = result;
437
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
+ }
438
538
  const safeScope = safeStr(scope);
439
539
  const createdAt = safeStr(envelope.meta.createdAt);
440
540
  const updatedAt = safeStr(envelope.meta.updatedAt);
@@ -503,7 +603,7 @@ function registerSecretsCommands(program2) {
503
603
  });
504
604
  process.stdout.write(output + "\n");
505
605
  });
506
- 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) => {
507
607
  const opts = buildOpts(cmd);
508
608
  const result = importDotenv(file, {
509
609
  scope: opts.scope,
@@ -512,6 +612,9 @@ function registerSecretsCommands(program2) {
512
612
  skipExisting: cmd.skipExisting,
513
613
  dryRun: cmd.dryRun
514
614
  });
615
+ if (emitJson(program2, cmd, { file, dryRun: !!cmd.dryRun, ...result })) {
616
+ return;
617
+ }
515
618
  if (cmd.dryRun) {
516
619
  console.log(
517
620
  `
@@ -594,24 +697,28 @@ function generateSecret(opts = {}) {
594
697
  }
595
698
  case "password": {
596
699
  const len = opts.length ?? 24;
597
- let pw = randomString(PASSWORD_CHARS, len);
598
- const hasUpper = /[A-Z]/.test(pw);
599
- const hasLower = /[a-z]/.test(pw);
600
- const hasDigit = /[0-9]/.test(pw);
601
- const hasSpecial = /[^A-Za-z0-9]/.test(pw);
602
- if (!hasUpper) pw = replaceAt(pw, randomInt(len), randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1));
603
- if (!hasLower) pw = replaceAt(pw, randomInt(len), randomString("abcdefghijklmnopqrstuvwxyz", 1));
604
- if (!hasDigit) pw = replaceAt(pw, randomInt(len), randomString("0123456789", 1));
605
- if (!hasSpecial) pw = replaceAt(pw, randomInt(len), randomString("!@#$%^&*()-_=+", 1));
606
- return pw;
700
+ const classCharsets = [
701
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
702
+ "abcdefghijklmnopqrstuvwxyz",
703
+ "0123456789",
704
+ "!@#$%^&*()-_=+"
705
+ ];
706
+ const guaranteed = classCharsets.slice(0, Math.min(classCharsets.length, len)).map((cs) => randomString(cs, 1));
707
+ const remaining = Math.max(0, len - guaranteed.length);
708
+ const chars = [
709
+ ...guaranteed,
710
+ ...remaining > 0 ? randomString(PASSWORD_CHARS, remaining).split("") : []
711
+ ];
712
+ for (let i = chars.length - 1; i > 0; i--) {
713
+ const j = randomInt(i + 1);
714
+ [chars[i], chars[j]] = [chars[j], chars[i]];
715
+ }
716
+ return chars.join("");
607
717
  }
608
718
  default:
609
719
  return randomBytes(32).toString("hex");
610
720
  }
611
721
  }
612
- function replaceAt(str, index, char) {
613
- return str.slice(0, index) + char + str.slice(index + 1);
614
- }
615
722
  function estimateEntropy(secret) {
616
723
  const charsets = [
617
724
  { regex: /[a-z]/, size: 26 },
@@ -977,7 +1084,7 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
977
1084
  }
978
1085
  console.log();
979
1086
  });
980
- 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) => {
981
1088
  const projectPath = cmd.projectPath ?? process.cwd();
982
1089
  const config = readProjectConfig(projectPath);
983
1090
  if (!config?.secrets || Object.keys(config.secrets).length === 0) {
@@ -991,49 +1098,58 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
991
1098
  );
992
1099
  process.exit(1);
993
1100
  }
994
- console.log(
995
- c.bold(`
996
- ${SYMBOLS.shield} Project secret manifest check
997
- `)
998
- );
999
- let present = 0;
1000
- let missing = 0;
1001
- let expiredCount = 0;
1002
- let staleCount = 0;
1101
+ const statuses = [];
1003
1102
  for (const [key, manifest] of Object.entries(config.secrets)) {
1004
1103
  const result = getEnvelope(key, { projectPath, source: "cli" });
1005
1104
  if (!result) {
1006
- if (manifest.required !== false) {
1007
- missing++;
1008
- console.log(
1009
- ` ${c.red(SYMBOLS.cross)} ${c.bold(key)} ${c.red("MISSING")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1010
- );
1011
- } else {
1012
- console.log(
1013
- ` ${c.dim(SYMBOLS.cross)} ${c.bold(key)} ${c.dim("optional, not set")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1014
- );
1015
- }
1105
+ statuses.push({
1106
+ key,
1107
+ status: manifest.required !== false ? "missing" : "optional-not-set",
1108
+ description: manifest.description
1109
+ });
1016
1110
  continue;
1017
1111
  }
1018
1112
  const decay = checkDecay(result.envelope);
1019
- if (decay.isExpired) {
1020
- expiredCount++;
1021
- console.log(
1022
- ` ${c.red(SYMBOLS.warning)} ${c.bold(key)} ${c.bgRed(c.white(" EXPIRED "))} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1023
- );
1024
- } else if (decay.isStale) {
1025
- staleCount++;
1026
- console.log(
1027
- ` ${c.yellow(SYMBOLS.warning)} ${c.bold(key)} ${c.yellow(`stale (${decay.lifetimePercent}%)`)} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1028
- );
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}`);
1029
1149
  } else {
1030
- present++;
1031
- console.log(
1032
- ` ${c.green(SYMBOLS.check)} ${c.bold(key)} ${c.green("OK")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
1033
- );
1150
+ console.log(` ${c.green(SYMBOLS.check)} ${c.bold(s.key)} ${c.green("OK")} ${desc}`);
1034
1151
  }
1035
1152
  }
1036
- const total = Object.keys(config.secrets).length;
1037
1153
  console.log(
1038
1154
  `
1039
1155
  ${c.bold(`${total} declared`)} ${c.green(`${present} present`)} ${c.yellow(`${staleCount} stale`)} ${c.red(`${expiredCount} expired`)} ${c.red(`${missing} missing`)}`
@@ -1057,10 +1173,11 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
1057
1173
  console.log();
1058
1174
  if (missing > 0) process.exit(1);
1059
1175
  });
1060
- 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) => {
1061
1177
  const result = collapseEnvironment({
1062
1178
  projectPath: cmd.projectPath ?? process.cwd()
1063
1179
  });
1180
+ if (emitJson(program2, cmd, { environment: result })) return;
1064
1181
  if (result) {
1065
1182
  console.log(
1066
1183
  `${SYMBOLS.zap} ${c.bold("Collapsed environment:")} ${envBadge(result.env)} ${c.dim(`(source: ${result.source})`)}`
@@ -1075,9 +1192,14 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
1075
1192
  });
1076
1193
  program2.command("env:generate").description(
1077
1194
  "Generate a .env file from the project manifest (.q-ring.json)"
1078
- ).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) => {
1079
1196
  const projectPath = cmd.projectPath ?? process.cwd();
1080
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
+ }
1081
1203
  if (!config?.secrets || Object.keys(config.secrets).length === 0) {
1082
1204
  console.error(
1083
1205
  c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
@@ -1138,7 +1260,7 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
1138
1260
  ).option("--tags <tags>", "Comma-separated tags for all secrets").option(
1139
1261
  "--hook-exec <cmd>",
1140
1262
  "Shell command to run when any of these secrets change"
1141
- ).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) => {
1142
1264
  const opts = buildOpts(cmd);
1143
1265
  const prefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
1144
1266
  const tags = cmd.tags?.split(",").map((t) => t.trim()) ?? [
@@ -1151,6 +1273,15 @@ ${c.bold(" Recent Activity:")} (last ${context.recentActions.length})`
1151
1273
  } else {
1152
1274
  keyNames = [`${prefix}_API_KEY`, `${prefix}_API_SECRET`];
1153
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
+ }
1154
1285
  console.log(`
1155
1286
  ${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
1156
1287
  `);
@@ -1225,7 +1356,8 @@ var ALGORITHM = "aes-256-gcm";
1225
1356
  var KEY_LENGTH = 32;
1226
1357
  var IV_LENGTH = 12;
1227
1358
  var SALT_LENGTH = 32;
1228
- var PBKDF2_ITERATIONS = 1e5;
1359
+ var PBKDF2_ITERATIONS = 21e4;
1360
+ var LEGACY_PBKDF2_ITERATIONS = 1e5;
1229
1361
  var TeleportBundleSchema = z.object({
1230
1362
  v: z.literal(1),
1231
1363
  data: z.string(),
@@ -1233,7 +1365,8 @@ var TeleportBundleSchema = z.object({
1233
1365
  iv: z.string(),
1234
1366
  tag: z.string(),
1235
1367
  createdAt: z.string(),
1236
- count: z.number()
1368
+ count: z.number(),
1369
+ iter: z.number().optional()
1237
1370
  });
1238
1371
  var TeleportPayloadSchema = z.object({
1239
1372
  secrets: z.array(
@@ -1246,8 +1379,8 @@ var TeleportPayloadSchema = z.object({
1246
1379
  exportedAt: z.string(),
1247
1380
  exportedBy: z.string().optional()
1248
1381
  });
1249
- function deriveKey(passphrase, salt) {
1250
- return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
1382
+ function deriveKey(passphrase, salt, iterations = PBKDF2_ITERATIONS) {
1383
+ return pbkdf2Sync(passphrase, salt, iterations, KEY_LENGTH, "sha512");
1251
1384
  }
1252
1385
  function teleportPack(secrets, passphrase) {
1253
1386
  const payload = {
@@ -1257,7 +1390,7 @@ function teleportPack(secrets, passphrase) {
1257
1390
  const plaintext = JSON.stringify(payload);
1258
1391
  const salt = randomBytes2(SALT_LENGTH);
1259
1392
  const iv = randomBytes2(IV_LENGTH);
1260
- const key = deriveKey(passphrase, salt);
1393
+ const key = deriveKey(passphrase, salt, PBKDF2_ITERATIONS);
1261
1394
  const cipher = createCipheriv(ALGORITHM, key, iv);
1262
1395
  const encrypted = Buffer.concat([
1263
1396
  cipher.update(plaintext, "utf8"),
@@ -1271,7 +1404,8 @@ function teleportPack(secrets, passphrase) {
1271
1404
  iv: iv.toString("base64"),
1272
1405
  tag: tag.toString("base64"),
1273
1406
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1274
- count: secrets.length
1407
+ count: secrets.length,
1408
+ iter: PBKDF2_ITERATIONS
1275
1409
  };
1276
1410
  return Buffer.from(JSON.stringify(bundle)).toString("base64");
1277
1411
  }
@@ -1299,7 +1433,7 @@ function teleportUnpack(encoded, passphrase) {
1299
1433
  const iv = Buffer.from(bundle.iv, "base64");
1300
1434
  const tag = Buffer.from(bundle.tag, "base64");
1301
1435
  const encrypted = Buffer.from(bundle.data, "base64");
1302
- const key = deriveKey(passphrase, salt);
1436
+ const key = deriveKey(passphrase, salt, bundle.iter ?? LEGACY_PBKDF2_ITERATIONS);
1303
1437
  const decipher = createDecipheriv(ALGORITHM, key, iv);
1304
1438
  decipher.setAuthTag(tag);
1305
1439
  let decrypted;
@@ -1425,8 +1559,9 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
1425
1559
  process.exit(1);
1426
1560
  }
1427
1561
  });
1428
- 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) => {
1429
1563
  const tunnels = tunnelList();
1564
+ if (emitJson(program2, cmd, { tunnels })) return;
1430
1565
  if (tunnels.length === 0) {
1431
1566
  console.log(c.dim("No active tunnels"));
1432
1567
  return;
@@ -1487,7 +1622,7 @@ ${SYMBOLS.package} ${c.green("packed")} ${secrets.length} secret(s)`
1487
1622
  );
1488
1623
  }
1489
1624
  });
1490
- 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) => {
1491
1626
  if (!bundle) {
1492
1627
  const chunks = [];
1493
1628
  for await (const chunk of process.stdin) {
@@ -1498,28 +1633,9 @@ ${SYMBOLS.package} ${c.green("packed")} ${secrets.length} secret(s)`
1498
1633
  const passphrase = await promptSecret(
1499
1634
  `${SYMBOLS.lock} Enter passphrase for decryption: `
1500
1635
  );
1636
+ let payload;
1501
1637
  try {
1502
- const payload = teleportUnpack(bundle, passphrase);
1503
- if (cmd.dryRun) {
1504
- console.log(
1505
- `
1506
- ${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets):
1507
- `
1508
- );
1509
- for (const s of payload.secrets) {
1510
- console.log(
1511
- ` ${SYMBOLS.key} ${c.bold(s.key)} ${c.dim(`[${s.scope ?? "global"}]`)}`
1512
- );
1513
- }
1514
- return;
1515
- }
1516
- const opts = buildOpts(cmd);
1517
- for (const s of payload.secrets) {
1518
- setSecret(s.key, s.value, opts);
1519
- }
1520
- console.log(
1521
- `${SYMBOLS.check} ${c.green("imported")} ${payload.secrets.length} secret(s) from teleport bundle`
1522
- );
1638
+ payload = teleportUnpack(bundle, passphrase);
1523
1639
  } catch {
1524
1640
  console.error(
1525
1641
  c.red(
@@ -1528,6 +1644,35 @@ ${SYMBOLS.package} ${c.bold("Would import")} (${payload.secrets.length} secrets)
1528
1644
  );
1529
1645
  process.exit(1);
1530
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
+ );
1531
1676
  });
1532
1677
  }
1533
1678
 
@@ -1542,8 +1687,11 @@ function registerValidationCommands(program2) {
1542
1687
  ).option(
1543
1688
  "--manifest",
1544
1689
  "Only validate manifest-declared secrets (with --all)"
1545
- ).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) => {
1546
1691
  if (cmd.listProviders) {
1692
+ if (emitJson(program2, cmd, { providers: registry2.listProviders() })) {
1693
+ return;
1694
+ }
1547
1695
  console.log(
1548
1696
  c.bold(`
1549
1697
  ${SYMBOLS.shield} Available validation providers
@@ -1573,11 +1721,15 @@ function registerValidationCommands(program2) {
1573
1721
  entries = entries.filter((e) => manifestKeys.has(e.key));
1574
1722
  }
1575
1723
  }
1576
- console.log(c.bold(`
1724
+ const jsonMode = wantsJsonOutput(program2, cmd);
1725
+ if (!jsonMode) {
1726
+ console.log(c.bold(`
1577
1727
  ${SYMBOLS.shield} Validating secrets
1578
1728
  `));
1729
+ }
1579
1730
  let validated = 0;
1580
1731
  let skipped = 0;
1732
+ const jsonResults = [];
1581
1733
  for (const entry of entries) {
1582
1734
  const value2 = getSecret(entry.key, { ...opts, scope: entry.scope });
1583
1735
  if (!value2) {
@@ -1591,12 +1743,26 @@ function registerValidationCommands(program2) {
1591
1743
  continue;
1592
1744
  }
1593
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
+ }
1594
1756
  const icon2 = result2.status === "valid" ? c.green(SYMBOLS.check) : result2.status === "invalid" ? c.red(SYMBOLS.cross) : c.yellow(SYMBOLS.warning);
1595
1757
  const statusText = result2.status === "valid" ? c.green("valid") : result2.status === "invalid" ? c.red("invalid") : c.yellow("error");
1596
1758
  console.log(
1597
1759
  ` ${icon2} ${c.bold(entry.key.padEnd(24))} ${statusText} ${c.dim(`(${result2.provider}, ${result2.latencyMs}ms)`)}${result2.status !== "valid" ? ` ${c.dim("\u2014 " + result2.message)}` : ""}`
1598
1760
  );
1599
1761
  }
1762
+ if (jsonMode) {
1763
+ emitJson(program2, cmd, { results: jsonResults, validated, skipped });
1764
+ return;
1765
+ }
1600
1766
  console.log(
1601
1767
  `
1602
1768
  ${c.dim(`${validated} validated, ${skipped} skipped (no provider)`)}
@@ -1612,6 +1778,15 @@ function registerValidationCommands(program2) {
1612
1778
  const envelope = getEnvelope(key, opts);
1613
1779
  const provHint = envelope?.envelope.meta.provider ?? cmd.provider;
1614
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
+ }
1615
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");
1616
1791
  console.log(
1617
1792
  `
@@ -1624,7 +1799,7 @@ function registerValidationCommands(program2) {
1624
1799
  });
1625
1800
  program2.command("rotate <key>").description(
1626
1801
  "Attempt issuer-native rotation of a secret via its provider"
1627
- ).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) => {
1628
1803
  const opts = buildOpts(cmd);
1629
1804
  const value = getSecret(key, opts);
1630
1805
  if (!value) {
@@ -1638,6 +1813,16 @@ function registerValidationCommands(program2) {
1638
1813
  ...opts,
1639
1814
  scope: opts.scope ?? "global"
1640
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) {
1641
1826
  console.log(
1642
1827
  `${SYMBOLS.check} ${c.green("Rotated")} ${c.bold(key)} via ${result.provider}`
1643
1828
  );
@@ -1846,7 +2031,8 @@ async function execCommand(opts) {
1846
2031
  "dig",
1847
2032
  "nslookup"
1848
2033
  ]);
1849
- if (profile.allowNetwork === false && networkTools.has(opts.command)) {
2034
+ const commandBase = opts.command.split(/[\\/]/).pop() ?? opts.command;
2035
+ if (profile.allowNetwork === false && networkTools.has(commandBase)) {
1850
2036
  const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts.command}" is considered network-related`;
1851
2037
  if (opts.captureOutput) {
1852
2038
  return resolve({ code: 126, stdout: "", stderr: msg });
@@ -2157,15 +2343,23 @@ function registerToolingCommands(program2) {
2157
2343
  program2.command("scan [dir]").description("Scan a codebase for hardcoded secrets").option(
2158
2344
  "--fix",
2159
2345
  "Auto-replace hardcoded secrets with process.env references and store in q-ring"
2160
- ).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) => {
2161
2347
  const targetDir = dir ?? process.cwd();
2162
2348
  const fixMode = cmd.fix === true;
2163
- console.log(
2164
- `
2349
+ const jsonMode = wantsJsonOutput(program2, cmd);
2350
+ if (!jsonMode) {
2351
+ console.log(
2352
+ `
2165
2353
  ${SYMBOLS.eye} Scanning ${c.bold(targetDir)} for secrets...${fixMode ? c.yellow(" [--fix mode]") : ""}
2166
2354
  `
2167
- );
2355
+ );
2356
+ }
2168
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
+ }
2169
2363
  if (results.length === 0) {
2170
2364
  console.log(
2171
2365
  ` ${c.green(SYMBOLS.check)} No hardcoded secrets found. Awesome!
@@ -2186,6 +2380,14 @@ function registerToolingCommands(program2) {
2186
2380
  projectPath: opts.projectPath
2187
2381
  });
2188
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
+ }
2189
2391
  console.log(
2190
2392
  ` ${c.green(SYMBOLS.check)} Fixed ${fixedCount} secrets \u2014 replaced with process.env references and stored in q-ring.
2191
2393
  `
@@ -2211,13 +2413,17 @@ function registerToolingCommands(program2) {
2211
2413
  ).option(
2212
2414
  "--fix",
2213
2415
  "Replace hardcoded secrets with process.env references and store in q-ring"
2214
- ).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) => {
2215
2417
  const opts = buildOpts(cmd);
2216
2418
  const results = lintFiles(files, {
2217
2419
  fix: cmd.fix,
2218
2420
  scope: opts.scope,
2219
2421
  projectPath: opts.projectPath
2220
2422
  });
2423
+ if (emitJson(program2, cmd, { files, findings: results })) {
2424
+ if (results.some((r) => !r.fixed)) process.exitCode = 1;
2425
+ return;
2426
+ }
2221
2427
  if (results.length === 0) {
2222
2428
  console.log(
2223
2429
  `
@@ -2251,12 +2457,11 @@ function registerToolingCommands(program2) {
2251
2457
  }
2252
2458
  });
2253
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) => {
2254
- const { startDashboardServer } = await import("./dashboard-OXUD62RV.js");
2460
+ const { startDashboardServer } = await import("./dashboard-II2ILNXA.js");
2255
2461
  const { exec } = await import("child_process");
2256
2462
  const { platform } = await import("os");
2257
2463
  const port = Number(cmd.port);
2258
- const { close } = startDashboardServer({ port });
2259
- const url = `http://127.0.0.1:${port}`;
2464
+ const { close, url } = startDashboardServer({ port });
2260
2465
  console.log(
2261
2466
  `
2262
2467
  ${SYMBOLS.zap} ${c.bold("q-ring quantum status dashboard")}
@@ -2288,9 +2493,10 @@ function registerAuditCommands(program2) {
2288
2493
  program2.command("audit").description("View the audit log (observer effect)").option("-k, --key <key>", "Filter by key").option(
2289
2494
  "-a, --action <action>",
2290
2495
  "Filter by action (read, write, delete, etc.)"
2291
- ).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) => {
2292
2497
  if (cmd.anomalies) {
2293
2498
  const anomalies = detectAnomalies(cmd.key);
2499
+ if (emitJson(program2, cmd, { anomalies })) return;
2294
2500
  if (anomalies.length === 0) {
2295
2501
  console.log(`${SYMBOLS.shield} ${c.green("No anomalies detected")}`);
2296
2502
  return;
@@ -2311,6 +2517,7 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2311
2517
  action: cmd.action,
2312
2518
  limit: cmd.limit
2313
2519
  });
2520
+ if (emitJson(program2, cmd, { events })) return;
2314
2521
  if (events.length === 0) {
2315
2522
  console.log(c.dim("No audit events found"));
2316
2523
  return;
@@ -2334,8 +2541,12 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2334
2541
  }
2335
2542
  console.log();
2336
2543
  });
2337
- 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) => {
2338
2545
  const result = verifyAuditChain();
2546
+ if (emitJson(program2, cmd, result)) {
2547
+ if (!result.intact && result.totalEvents > 0) process.exitCode = 1;
2548
+ return;
2549
+ }
2339
2550
  if (result.totalEvents === 0) {
2340
2551
  console.log(c.dim(" No audit events to verify"));
2341
2552
  return;
@@ -2376,42 +2587,58 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2376
2587
  console.log(output);
2377
2588
  }
2378
2589
  });
2379
- 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) => {
2380
2591
  const opts = buildOpts(cmd);
2381
2592
  const entries = listSecrets(opts);
2382
- if (entries.length === 0) {
2383
- console.log(c.dim("No secrets to check"));
2384
- return;
2385
- }
2386
- console.log(c.bold(`
2387
- ${SYMBOLS.shield} Secret health report
2388
- `));
2593
+ const expiredKeys = [];
2594
+ const staleKeys = [];
2389
2595
  let healthy = 0;
2390
- let stale = 0;
2391
- let expired = 0;
2392
2596
  let noDecay = 0;
2393
2597
  for (const entry of entries) {
2394
2598
  if (!entry.decay || !entry.decay.timeRemaining) {
2395
2599
  noDecay++;
2396
- continue;
2397
- }
2398
- if (entry.decay.isExpired) {
2399
- expired++;
2400
- console.log(
2401
- ` ${c.red(SYMBOLS.cross)} ${c.bold(entry.key)} ${c.bgRed(c.white(" EXPIRED "))}`
2402
- );
2600
+ } else if (entry.decay.isExpired) {
2601
+ expiredKeys.push(entry.key);
2403
2602
  } else if (entry.decay.isStale) {
2404
- stale++;
2405
- console.log(
2406
- ` ${c.yellow(SYMBOLS.warning)} ${c.bold(entry.key)} ${c.yellow(`stale (${entry.decay.lifetimePercent}%, ${entry.decay.timeRemaining} left)`)}`
2407
- );
2603
+ staleKeys.push({
2604
+ key: entry.key,
2605
+ lifetimePercent: entry.decay.lifetimePercent,
2606
+ timeRemaining: entry.decay.timeRemaining
2607
+ });
2408
2608
  } else {
2409
2609
  healthy++;
2410
2610
  }
2411
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
+ }
2412
2639
  console.log(
2413
2640
  `
2414
- ${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`)}`
2415
2642
  );
2416
2643
  const anomalies = detectAnomalies();
2417
2644
  if (anomalies.length > 0) {
@@ -2575,8 +2802,9 @@ function registerHookCommands(program2) {
2575
2802
  if (cmd.keyPattern) console.log(c.dim(` pattern: ${cmd.keyPattern}`));
2576
2803
  if (cmd.tag) console.log(c.dim(` tag: ${cmd.tag}`));
2577
2804
  });
2578
- 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) => {
2579
2806
  const hooks = listHooks();
2807
+ if (emitJson(program2, cmd, { hooks })) return;
2580
2808
  if (hooks.length === 0) {
2581
2809
  console.log(c.dim("No hooks registered"));
2582
2810
  return;
@@ -2800,9 +3028,16 @@ function registerAgentCommands(program2) {
2800
3028
  remember(key, value);
2801
3029
  console.log(`${SYMBOLS.check} ${c.green("remembered")} ${c.bold(key)}`);
2802
3030
  });
2803
- 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) => {
2804
3032
  if (!key) {
2805
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
+ }
2806
3041
  if (entries.length === 0) {
2807
3042
  console.log(c.dim("Agent memory is empty."));
2808
3043
  return;
@@ -2827,8 +3062,17 @@ ${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
2827
3062
  console.log(safeStr(value));
2828
3063
  }
2829
3064
  });
2830
- 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) => {
2831
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
+ }
2832
3076
  clearMemory();
2833
3077
  console.log(`${SYMBOLS.check} ${c.yellow("cleared")} all agent memory`);
2834
3078
  return;
@@ -2842,13 +3086,10 @@ ${SYMBOLS.zap} ${c.bold("Agent Memory")} (${entries.length} entries)
2842
3086
  });
2843
3087
  program2.command("analyze").description(
2844
3088
  "Analyze secret usage patterns and provide optimization suggestions"
2845
- ).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) => {
2846
3090
  const opts = buildOpts(cmd);
2847
3091
  const entries = listSecrets({ ...opts, silent: true });
2848
3092
  const audit = queryAudit({ limit: 1e3 });
2849
- console.log(`
2850
- ${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
2851
- `);
2852
3093
  const accessMap = /* @__PURE__ */ new Map();
2853
3094
  for (const e of audit) {
2854
3095
  if (e.action === "read" && e.key) {
@@ -2856,6 +3097,39 @@ ${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
2856
3097
  }
2857
3098
  }
2858
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
+ `);
2859
3133
  if (sorted.length > 0) {
2860
3134
  console.log(` ${c.bold("Most accessed:")}`);
2861
3135
  for (const [key, count] of sorted.slice(0, 5)) {
@@ -2863,10 +3137,6 @@ ${SYMBOLS.zap} ${c.bold("Secret Usage Analysis")}
2863
3137
  }
2864
3138
  console.log();
2865
3139
  }
2866
- const neverAccessed = entries.filter((e) => {
2867
- const count = e.envelope?.meta.accessCount ?? 0;
2868
- return count === 0;
2869
- });
2870
3140
  if (neverAccessed.length > 0) {
2871
3141
  console.log(
2872
3142
  ` ${c.bold("Never accessed:")} ${c.yellow(neverAccessed.length.toString())} secrets`
@@ -3018,8 +3288,9 @@ function registerSecurityCommands(program2) {
3018
3288
  )
3019
3289
  );
3020
3290
  });
3021
- 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) => {
3022
3292
  const approvals = listApprovals();
3293
+ if (emitJson(program2, cmd, { approvals })) return;
3023
3294
  if (approvals.length === 0) {
3024
3295
  console.log(c.dim(" No approvals found"));
3025
3296
  return;
@@ -3100,6 +3371,290 @@ ${SYMBOLS.lock} Approval Tokens
3100
3371
  });
3101
3372
  }
3102
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
+
3103
3658
  // src/cli/commands.ts
3104
3659
  var COMMAND_GROUPS = [
3105
3660
  {
@@ -3108,6 +3663,7 @@ var COMMAND_GROUPS = [
3108
3663
  commands: [
3109
3664
  "set",
3110
3665
  "get",
3666
+ "has",
3111
3667
  "delete",
3112
3668
  "list",
3113
3669
  "inspect",
@@ -3139,7 +3695,7 @@ var COMMAND_GROUPS = [
3139
3695
  {
3140
3696
  name: "Dev Tooling",
3141
3697
  symbol: SYMBOLS.zap,
3142
- commands: ["exec", "scan", "lint", "status"]
3698
+ commands: ["exec", "scan", "lint", "status", "doctor", "completion"]
3143
3699
  },
3144
3700
  {
3145
3701
  name: "Audit & Health",
@@ -3281,10 +3837,21 @@ function createProgram() {
3281
3837
  registerHookCommands(program2);
3282
3838
  registerAgentCommands(program2);
3283
3839
  registerSecurityCommands(program2);
3840
+ registerDoctorCommand(program2);
3841
+ registerCompletionCommand(program2);
3284
3842
  return program2;
3285
3843
  }
3286
3844
 
3287
3845
  // src/index.ts
3288
3846
  var program = createProgram();
3289
- 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
+ }
3290
3857
  //# sourceMappingURL=index.js.map