@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/README.md +45 -10
- package/dist/{chunk-MQM4DLPI.js → chunk-CWV3WTPF.js} +168 -95
- package/dist/chunk-CWV3WTPF.js.map +1 -0
- package/dist/{chunk-A4RZVP3P.js → chunk-SRESNRML.js} +163 -95
- package/dist/chunk-SRESNRML.js.map +1 -0
- package/dist/dashboard-II2ILNXA.js +658 -0
- package/dist/dashboard-II2ILNXA.js.map +1 -0
- package/dist/dashboard-RLMROWIU.js +658 -0
- package/dist/dashboard-RLMROWIU.js.map +1 -0
- package/dist/index.js +712 -145
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +49 -31
- package/dist/mcp.js.map +1 -1
- package/package.json +29 -14
- package/dist/chunk-A4RZVP3P.js.map +0 -1
- package/dist/chunk-MQM4DLPI.js.map +0 -1
- package/dist/dashboard-FSCJDLJX.js +0 -1313
- package/dist/dashboard-FSCJDLJX.js.map +0 -1
- package/dist/dashboard-OXUD62RV.js +0 -1313
- package/dist/dashboard-OXUD62RV.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-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 (
|
|
141
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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("
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
2383
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
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} ${
|
|
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
|
-
|
|
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
|