@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/README.md +37 -6
- package/dist/{chunk-YWDKAZWS.js → chunk-Q3UCKJAJ.js} +30 -20
- package/dist/chunk-Q3UCKJAJ.js.map +1 -0
- package/dist/{chunk-5TQNDJT3.js → chunk-Z7UJFHVE.js} +30 -20
- package/dist/chunk-Z7UJFHVE.js.map +1 -0
- package/dist/dashboard-2IGUR7PA.js +658 -0
- package/dist/dashboard-2IGUR7PA.js.map +1 -0
- package/dist/dashboard-2UDYGGTT.js +658 -0
- package/dist/dashboard-2UDYGGTT.js.map +1 -0
- package/dist/index.js +669 -117
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +3 -3
- package/dist/mcp.js.map +1 -1
- package/package.json +22 -17
- package/dist/chunk-5TQNDJT3.js.map +0 -1
- package/dist/chunk-YWDKAZWS.js.map +0 -1
- package/dist/dashboard-6XKC5BL4.js +0 -630
- package/dist/dashboard-6XKC5BL4.js.map +0 -1
- package/dist/dashboard-HHJY73JX.js +0 -630
- package/dist/dashboard-HHJY73JX.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
tunnelList,
|
|
43
43
|
tunnelRead,
|
|
44
44
|
verifyAuditChain
|
|
45
|
-
} from "./chunk-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
2398
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
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} ${
|
|
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
|
-
|
|
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
|