@i4ctime/q-ring 0.3.2 → 0.4.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 +196 -6
- package/dist/{chunk-F4SPZ774.js → chunk-6IQ5SFLI.js} +298 -6
- package/dist/chunk-6IQ5SFLI.js.map +1 -0
- package/dist/{chunk-3WTTWJYU.js → chunk-IGNU622R.js} +337 -5
- package/dist/chunk-IGNU622R.js.map +1 -0
- package/dist/{dashboard-X3ONQFLV.js → dashboard-32PCZF7D.js} +2 -2
- package/dist/{dashboard-QQWKOOI5.js → dashboard-HVIQO6NT.js} +2 -2
- package/dist/index.js +675 -10
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +580 -9
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-3WTTWJYU.js.map +0 -1
- package/dist/chunk-F4SPZ774.js.map +0 -1
- /package/dist/{dashboard-QQWKOOI5.js.map → dashboard-32PCZF7D.js.map} +0 -0
- /package/dist/{dashboard-X3ONQFLV.js.map → dashboard-HVIQO6NT.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -4,19 +4,28 @@ import {
|
|
|
4
4
|
collapseEnvironment,
|
|
5
5
|
deleteSecret,
|
|
6
6
|
detectAnomalies,
|
|
7
|
+
disableHook,
|
|
8
|
+
disentangleSecrets,
|
|
9
|
+
enableHook,
|
|
7
10
|
entangleSecrets,
|
|
8
11
|
exportSecrets,
|
|
12
|
+
fireHooks,
|
|
9
13
|
getEnvelope,
|
|
10
14
|
getSecret,
|
|
15
|
+
hasSecret,
|
|
16
|
+
listHooks,
|
|
11
17
|
listSecrets,
|
|
12
18
|
logAudit,
|
|
13
19
|
queryAudit,
|
|
20
|
+
readProjectConfig,
|
|
21
|
+
registerHook,
|
|
22
|
+
removeHook,
|
|
14
23
|
setSecret,
|
|
15
24
|
tunnelCreate,
|
|
16
25
|
tunnelDestroy,
|
|
17
26
|
tunnelList,
|
|
18
27
|
tunnelRead
|
|
19
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-IGNU622R.js";
|
|
20
29
|
|
|
21
30
|
// src/cli/commands.ts
|
|
22
31
|
import { Command } from "commander";
|
|
@@ -207,7 +216,9 @@ function runHealthScan(config = {}) {
|
|
|
207
216
|
`EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
|
|
208
217
|
);
|
|
209
218
|
if (cfg.autoRotate) {
|
|
210
|
-
const
|
|
219
|
+
const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
|
|
220
|
+
const prefix = entry.envelope?.meta.rotationPrefix;
|
|
221
|
+
const newValue = generateSecret({ format: fmt, prefix });
|
|
211
222
|
setSecret(entry.key, newValue, {
|
|
212
223
|
scope: entry.scope,
|
|
213
224
|
projectPath: cfg.projectPaths[0],
|
|
@@ -221,6 +232,14 @@ function runHealthScan(config = {}) {
|
|
|
221
232
|
source: "agent",
|
|
222
233
|
detail: "auto-rotated by agent (expired)"
|
|
223
234
|
});
|
|
235
|
+
fireHooks({
|
|
236
|
+
action: "rotate",
|
|
237
|
+
key: entry.key,
|
|
238
|
+
scope: entry.scope,
|
|
239
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
240
|
+
source: "agent"
|
|
241
|
+
}, entry.envelope?.meta.tags).catch(() => {
|
|
242
|
+
});
|
|
224
243
|
}
|
|
225
244
|
} else if (decay.isStale) {
|
|
226
245
|
report.stale++;
|
|
@@ -353,6 +372,265 @@ function teleportUnpack(encoded, passphrase) {
|
|
|
353
372
|
return JSON.parse(decrypted.toString("utf8"));
|
|
354
373
|
}
|
|
355
374
|
|
|
375
|
+
// src/core/import.ts
|
|
376
|
+
import { readFileSync } from "fs";
|
|
377
|
+
function parseDotenv(content) {
|
|
378
|
+
const result = /* @__PURE__ */ new Map();
|
|
379
|
+
const lines = content.split(/\r?\n/);
|
|
380
|
+
for (let i = 0; i < lines.length; i++) {
|
|
381
|
+
const line = lines[i].trim();
|
|
382
|
+
if (!line || line.startsWith("#")) continue;
|
|
383
|
+
const eqIdx = line.indexOf("=");
|
|
384
|
+
if (eqIdx === -1) continue;
|
|
385
|
+
const key = line.slice(0, eqIdx).trim();
|
|
386
|
+
let value = line.slice(eqIdx + 1).trim();
|
|
387
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
388
|
+
value = value.slice(1, -1);
|
|
389
|
+
}
|
|
390
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\").replace(/\\"/g, '"');
|
|
391
|
+
if (value.includes("#") && !line.includes('"') && !line.includes("'")) {
|
|
392
|
+
value = value.split("#")[0].trim();
|
|
393
|
+
}
|
|
394
|
+
if (key) result.set(key, value);
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
function importDotenv(filePathOrContent, options = {}) {
|
|
399
|
+
let content;
|
|
400
|
+
try {
|
|
401
|
+
content = readFileSync(filePathOrContent, "utf8");
|
|
402
|
+
} catch {
|
|
403
|
+
content = filePathOrContent;
|
|
404
|
+
}
|
|
405
|
+
const pairs = parseDotenv(content);
|
|
406
|
+
const result = {
|
|
407
|
+
imported: [],
|
|
408
|
+
skipped: [],
|
|
409
|
+
total: pairs.size
|
|
410
|
+
};
|
|
411
|
+
for (const [key, value] of pairs) {
|
|
412
|
+
if (options.skipExisting && hasSecret(key, {
|
|
413
|
+
scope: options.scope,
|
|
414
|
+
projectPath: options.projectPath,
|
|
415
|
+
source: options.source ?? "cli"
|
|
416
|
+
})) {
|
|
417
|
+
result.skipped.push(key);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (options.dryRun) {
|
|
421
|
+
result.imported.push(key);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const setOpts = {
|
|
425
|
+
scope: options.scope ?? "global",
|
|
426
|
+
projectPath: options.projectPath ?? process.cwd(),
|
|
427
|
+
source: options.source ?? "cli"
|
|
428
|
+
};
|
|
429
|
+
setSecret(key, value, setOpts);
|
|
430
|
+
result.imported.push(key);
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/core/validate.ts
|
|
436
|
+
import { request as httpsRequest } from "https";
|
|
437
|
+
import { request as httpRequest } from "http";
|
|
438
|
+
function makeRequest(url, headers, timeoutMs = 1e4) {
|
|
439
|
+
return new Promise((resolve, reject) => {
|
|
440
|
+
const parsedUrl = new URL(url);
|
|
441
|
+
const reqFn = parsedUrl.protocol === "https:" ? httpsRequest : httpRequest;
|
|
442
|
+
const req = reqFn(
|
|
443
|
+
url,
|
|
444
|
+
{ method: "GET", headers, timeout: timeoutMs },
|
|
445
|
+
(res) => {
|
|
446
|
+
let body = "";
|
|
447
|
+
res.on("data", (chunk) => body += chunk);
|
|
448
|
+
res.on(
|
|
449
|
+
"end",
|
|
450
|
+
() => resolve({ statusCode: res.statusCode ?? 0, body })
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
req.on("error", reject);
|
|
455
|
+
req.on("timeout", () => {
|
|
456
|
+
req.destroy();
|
|
457
|
+
reject(new Error("Request timed out"));
|
|
458
|
+
});
|
|
459
|
+
req.end();
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
var ProviderRegistry = class {
|
|
463
|
+
providers = /* @__PURE__ */ new Map();
|
|
464
|
+
register(provider) {
|
|
465
|
+
this.providers.set(provider.name, provider);
|
|
466
|
+
}
|
|
467
|
+
get(name) {
|
|
468
|
+
return this.providers.get(name);
|
|
469
|
+
}
|
|
470
|
+
detectProvider(value, hints) {
|
|
471
|
+
if (hints?.provider) {
|
|
472
|
+
return this.providers.get(hints.provider);
|
|
473
|
+
}
|
|
474
|
+
for (const provider of this.providers.values()) {
|
|
475
|
+
if (provider.prefixes) {
|
|
476
|
+
for (const pfx of provider.prefixes) {
|
|
477
|
+
if (value.startsWith(pfx)) return provider;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return void 0;
|
|
482
|
+
}
|
|
483
|
+
listProviders() {
|
|
484
|
+
return [...this.providers.values()];
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
var openaiProvider = {
|
|
488
|
+
name: "openai",
|
|
489
|
+
description: "OpenAI API key validation",
|
|
490
|
+
prefixes: ["sk-"],
|
|
491
|
+
async validate(value) {
|
|
492
|
+
const start = Date.now();
|
|
493
|
+
try {
|
|
494
|
+
const { statusCode } = await makeRequest(
|
|
495
|
+
"https://api.openai.com/v1/models?limit=1",
|
|
496
|
+
{
|
|
497
|
+
Authorization: `Bearer ${value}`,
|
|
498
|
+
"User-Agent": "q-ring-validator/1.0"
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
const latencyMs = Date.now() - start;
|
|
502
|
+
if (statusCode === 200)
|
|
503
|
+
return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "openai" };
|
|
504
|
+
if (statusCode === 401)
|
|
505
|
+
return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "openai" };
|
|
506
|
+
if (statusCode === 429)
|
|
507
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "openai" };
|
|
508
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "openai" };
|
|
509
|
+
} catch (err) {
|
|
510
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "openai" };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
var stripeProvider = {
|
|
515
|
+
name: "stripe",
|
|
516
|
+
description: "Stripe API key validation",
|
|
517
|
+
prefixes: ["sk_live_", "sk_test_", "rk_live_", "rk_test_", "pk_live_", "pk_test_"],
|
|
518
|
+
async validate(value) {
|
|
519
|
+
const start = Date.now();
|
|
520
|
+
try {
|
|
521
|
+
const { statusCode } = await makeRequest(
|
|
522
|
+
"https://api.stripe.com/v1/balance",
|
|
523
|
+
{
|
|
524
|
+
Authorization: `Bearer ${value}`,
|
|
525
|
+
"User-Agent": "q-ring-validator/1.0"
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
const latencyMs = Date.now() - start;
|
|
529
|
+
if (statusCode === 200)
|
|
530
|
+
return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "stripe" };
|
|
531
|
+
if (statusCode === 401)
|
|
532
|
+
return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "stripe" };
|
|
533
|
+
if (statusCode === 429)
|
|
534
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "stripe" };
|
|
535
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "stripe" };
|
|
536
|
+
} catch (err) {
|
|
537
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "stripe" };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
var githubProvider = {
|
|
542
|
+
name: "github",
|
|
543
|
+
description: "GitHub token validation",
|
|
544
|
+
prefixes: ["ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"],
|
|
545
|
+
async validate(value) {
|
|
546
|
+
const start = Date.now();
|
|
547
|
+
try {
|
|
548
|
+
const { statusCode } = await makeRequest(
|
|
549
|
+
"https://api.github.com/user",
|
|
550
|
+
{
|
|
551
|
+
Authorization: `token ${value}`,
|
|
552
|
+
"User-Agent": "q-ring-validator/1.0",
|
|
553
|
+
Accept: "application/vnd.github+json"
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
const latencyMs = Date.now() - start;
|
|
557
|
+
if (statusCode === 200)
|
|
558
|
+
return { valid: true, status: "valid", message: "Token is valid", latencyMs, provider: "github" };
|
|
559
|
+
if (statusCode === 401)
|
|
560
|
+
return { valid: false, status: "invalid", message: "Invalid or expired token", latencyMs, provider: "github" };
|
|
561
|
+
if (statusCode === 403)
|
|
562
|
+
return { valid: false, status: "invalid", message: "Token lacks required permissions", latencyMs, provider: "github" };
|
|
563
|
+
if (statusCode === 429)
|
|
564
|
+
return { valid: true, status: "error", message: "Rate limited \u2014 token may be valid", latencyMs, provider: "github" };
|
|
565
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "github" };
|
|
566
|
+
} catch (err) {
|
|
567
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "github" };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
var awsProvider = {
|
|
572
|
+
name: "aws",
|
|
573
|
+
description: "AWS access key validation (checks key format only \u2014 full STS validation requires secret key + region)",
|
|
574
|
+
prefixes: ["AKIA", "ASIA"],
|
|
575
|
+
async validate(value) {
|
|
576
|
+
const start = Date.now();
|
|
577
|
+
const latencyMs = Date.now() - start;
|
|
578
|
+
if (/^(AKIA|ASIA)[A-Z0-9]{16}$/.test(value)) {
|
|
579
|
+
return { valid: true, status: "unknown", message: "Valid AWS access key format (STS validation requires secret key)", latencyMs, provider: "aws" };
|
|
580
|
+
}
|
|
581
|
+
return { valid: false, status: "invalid", message: "Invalid AWS access key format", latencyMs, provider: "aws" };
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
var httpProvider = {
|
|
585
|
+
name: "http",
|
|
586
|
+
description: "Generic HTTP endpoint validation",
|
|
587
|
+
async validate(value, url) {
|
|
588
|
+
const start = Date.now();
|
|
589
|
+
if (!url) {
|
|
590
|
+
return { valid: false, status: "unknown", message: "No validation URL configured", latencyMs: 0, provider: "http" };
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
const { statusCode } = await makeRequest(url, {
|
|
594
|
+
Authorization: `Bearer ${value}`,
|
|
595
|
+
"User-Agent": "q-ring-validator/1.0"
|
|
596
|
+
});
|
|
597
|
+
const latencyMs = Date.now() - start;
|
|
598
|
+
if (statusCode >= 200 && statusCode < 300)
|
|
599
|
+
return { valid: true, status: "valid", message: `Endpoint returned ${statusCode}`, latencyMs, provider: "http" };
|
|
600
|
+
if (statusCode === 401 || statusCode === 403)
|
|
601
|
+
return { valid: false, status: "invalid", message: `Authentication failed (${statusCode})`, latencyMs, provider: "http" };
|
|
602
|
+
return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "http" };
|
|
603
|
+
} catch (err) {
|
|
604
|
+
return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "http" };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
var registry = new ProviderRegistry();
|
|
609
|
+
registry.register(openaiProvider);
|
|
610
|
+
registry.register(stripeProvider);
|
|
611
|
+
registry.register(githubProvider);
|
|
612
|
+
registry.register(awsProvider);
|
|
613
|
+
registry.register(httpProvider);
|
|
614
|
+
async function validateSecret(value, opts) {
|
|
615
|
+
const provider = opts?.provider ? registry.get(opts.provider) : registry.detectProvider(value);
|
|
616
|
+
if (!provider) {
|
|
617
|
+
return {
|
|
618
|
+
valid: false,
|
|
619
|
+
status: "unknown",
|
|
620
|
+
message: "No provider detected \u2014 set a provider in the manifest or secret metadata",
|
|
621
|
+
latencyMs: 0,
|
|
622
|
+
provider: "none"
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
if (provider.name === "http" && opts?.validationUrl) {
|
|
626
|
+
return provider.validate(value, opts.validationUrl);
|
|
627
|
+
}
|
|
628
|
+
return provider.validate(value);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/cli/commands.ts
|
|
632
|
+
import { writeFileSync } from "fs";
|
|
633
|
+
|
|
356
634
|
// src/utils/prompt.ts
|
|
357
635
|
import { createInterface } from "readline";
|
|
358
636
|
async function promptSecret(message) {
|
|
@@ -419,8 +697,8 @@ function buildOpts(cmd) {
|
|
|
419
697
|
function createProgram() {
|
|
420
698
|
const program2 = new Command().name("qring").description(
|
|
421
699
|
`${c.bold("q-ring")} ${c.dim("\u2014 quantum keyring for AI coding tools")}`
|
|
422
|
-
).version("0.
|
|
423
|
-
program2.command("set <key> [value]").description("Store a secret (with optional quantum metadata)").option("-g, --global", "Store in global scope").option("-p, --project", "Store in project scope (uses cwd)").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Set value for a specific environment (superposition)").option("--ttl <seconds>", "Time-to-live in seconds (quantum decay)", parseInt).option("--expires <iso>", "Expiry timestamp (ISO 8601)").option("--description <desc>", "Human-readable description").option("--tags <tags>", "Comma-separated tags").action(async (key, value, cmd) => {
|
|
700
|
+
).version("0.4.0");
|
|
701
|
+
program2.command("set <key> [value]").description("Store a secret (with optional quantum metadata)").option("-g, --global", "Store in global scope").option("-p, --project", "Store in project scope (uses cwd)").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Set value for a specific environment (superposition)").option("--ttl <seconds>", "Time-to-live in seconds (quantum decay)", parseInt).option("--expires <iso>", "Expiry timestamp (ISO 8601)").option("--description <desc>", "Human-readable description").option("--tags <tags>", "Comma-separated tags").option("--rotation-format <format>", "Format for auto-rotation (api-key, password, uuid, hex, base64, alphanumeric, token)").option("--rotation-prefix <prefix>", "Prefix for auto-rotation (e.g. sk-)").action(async (key, value, cmd) => {
|
|
424
702
|
const opts = buildOpts(cmd);
|
|
425
703
|
if (!value) {
|
|
426
704
|
value = await promptSecret(`${SYMBOLS.key} Enter value for ${c.bold(key)}: `);
|
|
@@ -434,7 +712,9 @@ function createProgram() {
|
|
|
434
712
|
ttlSeconds: cmd.ttl,
|
|
435
713
|
expiresAt: cmd.expires,
|
|
436
714
|
description: cmd.description,
|
|
437
|
-
tags: cmd.tags?.split(",").map((t) => t.trim())
|
|
715
|
+
tags: cmd.tags?.split(",").map((t) => t.trim()),
|
|
716
|
+
rotationFormat: cmd.rotationFormat,
|
|
717
|
+
rotationPrefix: cmd.rotationPrefix
|
|
438
718
|
};
|
|
439
719
|
if (cmd.env) {
|
|
440
720
|
const existing = getEnvelope(key, opts);
|
|
@@ -478,9 +758,29 @@ function createProgram() {
|
|
|
478
758
|
process.exit(1);
|
|
479
759
|
}
|
|
480
760
|
});
|
|
481
|
-
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("--project-path <path>", "Explicit project path").option("--show-decay", "Show decay/expiry status").action((cmd) => {
|
|
761
|
+
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("--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) => {
|
|
482
762
|
const opts = buildOpts(cmd);
|
|
483
|
-
|
|
763
|
+
let entries = listSecrets(opts);
|
|
764
|
+
if (cmd.tag) {
|
|
765
|
+
entries = entries.filter(
|
|
766
|
+
(e) => e.envelope?.meta.tags?.includes(cmd.tag)
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
if (cmd.expired) {
|
|
770
|
+
entries = entries.filter((e) => e.decay?.isExpired);
|
|
771
|
+
}
|
|
772
|
+
if (cmd.stale) {
|
|
773
|
+
entries = entries.filter(
|
|
774
|
+
(e) => e.decay?.isStale && !e.decay?.isExpired
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
if (cmd.filter) {
|
|
778
|
+
const regex = new RegExp(
|
|
779
|
+
"^" + cmd.filter.replace(/\*/g, ".*") + "$",
|
|
780
|
+
"i"
|
|
781
|
+
);
|
|
782
|
+
entries = entries.filter((e) => regex.test(e.key));
|
|
783
|
+
}
|
|
484
784
|
if (entries.length === 0) {
|
|
485
785
|
console.log(c.dim("No secrets found"));
|
|
486
786
|
return;
|
|
@@ -593,11 +893,200 @@ function createProgram() {
|
|
|
593
893
|
}
|
|
594
894
|
console.log();
|
|
595
895
|
});
|
|
596
|
-
program2.command("export").description("Export secrets as .env or JSON (collapses superposition)").option("-f, --format <format>", "Output format: env or json", "env").option("-g, --global", "Export global scope only").option("-p, --project", "Export project scope only").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force environment for collapse").action((cmd) => {
|
|
896
|
+
program2.command("export").description("Export secrets as .env or JSON (collapses superposition)").option("-f, --format <format>", "Output format: env or json", "env").option("-g, --global", "Export global scope only").option("-p, --project", "Export project scope only").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Force environment for collapse").option("-k, --keys <keys>", "Comma-separated key names to export").option("-t, --tags <tags>", "Comma-separated tags to filter by").action((cmd) => {
|
|
597
897
|
const opts = buildOpts(cmd);
|
|
598
|
-
const output = exportSecrets({
|
|
898
|
+
const output = exportSecrets({
|
|
899
|
+
...opts,
|
|
900
|
+
format: cmd.format,
|
|
901
|
+
keys: cmd.keys?.split(",").map((k) => k.trim()),
|
|
902
|
+
tags: cmd.tags?.split(",").map((t) => t.trim())
|
|
903
|
+
});
|
|
599
904
|
process.stdout.write(output + "\n");
|
|
600
905
|
});
|
|
906
|
+
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) => {
|
|
907
|
+
const opts = buildOpts(cmd);
|
|
908
|
+
const result = importDotenv(file, {
|
|
909
|
+
scope: opts.scope,
|
|
910
|
+
projectPath: opts.projectPath,
|
|
911
|
+
source: "cli",
|
|
912
|
+
skipExisting: cmd.skipExisting,
|
|
913
|
+
dryRun: cmd.dryRun
|
|
914
|
+
});
|
|
915
|
+
if (cmd.dryRun) {
|
|
916
|
+
console.log(
|
|
917
|
+
`
|
|
918
|
+
${SYMBOLS.package} ${c.bold("Dry run")} \u2014 would import ${result.imported.length} of ${result.total} secrets:
|
|
919
|
+
`
|
|
920
|
+
);
|
|
921
|
+
for (const key of result.imported) {
|
|
922
|
+
console.log(` ${SYMBOLS.key} ${c.bold(key)}`);
|
|
923
|
+
}
|
|
924
|
+
if (result.skipped.length > 0) {
|
|
925
|
+
console.log(`
|
|
926
|
+
${c.dim(`Skipped (existing): ${result.skipped.join(", ")}`)}`);
|
|
927
|
+
}
|
|
928
|
+
} else {
|
|
929
|
+
console.log(
|
|
930
|
+
`${SYMBOLS.check} ${c.green("imported")} ${result.imported.length} secret(s) from ${c.bold(file)}`
|
|
931
|
+
);
|
|
932
|
+
if (result.skipped.length > 0) {
|
|
933
|
+
console.log(
|
|
934
|
+
c.dim(` skipped ${result.skipped.length} existing: ${result.skipped.join(", ")}`)
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
console.log();
|
|
939
|
+
});
|
|
940
|
+
program2.command("check").description("Validate project secrets against .q-ring.json manifest").option("--project-path <path>", "Project path (defaults to cwd)").action((cmd) => {
|
|
941
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
942
|
+
const config = readProjectConfig(projectPath);
|
|
943
|
+
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
944
|
+
console.error(
|
|
945
|
+
c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
|
|
946
|
+
);
|
|
947
|
+
console.log(
|
|
948
|
+
c.dim(' Add a "secrets" field to your .q-ring.json to define required secrets.')
|
|
949
|
+
);
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
console.log(
|
|
953
|
+
c.bold(`
|
|
954
|
+
${SYMBOLS.shield} Project secret manifest check
|
|
955
|
+
`)
|
|
956
|
+
);
|
|
957
|
+
let present = 0;
|
|
958
|
+
let missing = 0;
|
|
959
|
+
let expiredCount = 0;
|
|
960
|
+
let staleCount = 0;
|
|
961
|
+
for (const [key, manifest] of Object.entries(config.secrets)) {
|
|
962
|
+
const result = getEnvelope(key, { projectPath, source: "cli" });
|
|
963
|
+
if (!result) {
|
|
964
|
+
if (manifest.required !== false) {
|
|
965
|
+
missing++;
|
|
966
|
+
console.log(
|
|
967
|
+
` ${c.red(SYMBOLS.cross)} ${c.bold(key)} ${c.red("MISSING")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
968
|
+
);
|
|
969
|
+
} else {
|
|
970
|
+
console.log(
|
|
971
|
+
` ${c.dim(SYMBOLS.cross)} ${c.bold(key)} ${c.dim("optional, not set")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const decay = checkDecay(result.envelope);
|
|
977
|
+
if (decay.isExpired) {
|
|
978
|
+
expiredCount++;
|
|
979
|
+
console.log(
|
|
980
|
+
` ${c.red(SYMBOLS.warning)} ${c.bold(key)} ${c.bgRed(c.white(" EXPIRED "))} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
981
|
+
);
|
|
982
|
+
} else if (decay.isStale) {
|
|
983
|
+
staleCount++;
|
|
984
|
+
console.log(
|
|
985
|
+
` ${c.yellow(SYMBOLS.warning)} ${c.bold(key)} ${c.yellow(`stale (${decay.lifetimePercent}%)`)} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
986
|
+
);
|
|
987
|
+
} else {
|
|
988
|
+
present++;
|
|
989
|
+
console.log(
|
|
990
|
+
` ${c.green(SYMBOLS.check)} ${c.bold(key)} ${c.green("OK")} ${manifest.description ? c.dim(`\u2014 ${manifest.description}`) : ""}`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const total = Object.keys(config.secrets).length;
|
|
995
|
+
console.log(
|
|
996
|
+
`
|
|
997
|
+
${c.bold(`${total} declared`)} ${c.green(`${present} present`)} ${c.yellow(`${staleCount} stale`)} ${c.red(`${expiredCount} expired`)} ${c.red(`${missing} missing`)}`
|
|
998
|
+
);
|
|
999
|
+
if (missing > 0) {
|
|
1000
|
+
console.log(
|
|
1001
|
+
`
|
|
1002
|
+
${c.red("Project is NOT ready \u2014 missing required secrets.")}`
|
|
1003
|
+
);
|
|
1004
|
+
} else if (expiredCount > 0) {
|
|
1005
|
+
console.log(
|
|
1006
|
+
`
|
|
1007
|
+
${c.yellow("Project has expired secrets that need rotation.")}`
|
|
1008
|
+
);
|
|
1009
|
+
} else {
|
|
1010
|
+
console.log(
|
|
1011
|
+
`
|
|
1012
|
+
${c.green(`${SYMBOLS.check} Project is ready \u2014 all required secrets present.`)}`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
console.log();
|
|
1016
|
+
if (missing > 0) process.exit(1);
|
|
1017
|
+
});
|
|
1018
|
+
program2.command("validate [key]").description("Test if a secret is actually valid with its target service").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--project-path <path>", "Explicit project path").option("--provider <name>", "Force a specific provider (openai, stripe, github, aws, http)").option("--all", "Validate all secrets that have a detectable provider").option("--manifest", "Only validate manifest-declared secrets (with --all)").option("--list-providers", "List all available providers").action(async (key, cmd) => {
|
|
1019
|
+
if (cmd.listProviders) {
|
|
1020
|
+
console.log(c.bold(`
|
|
1021
|
+
${SYMBOLS.shield} Available validation providers
|
|
1022
|
+
`));
|
|
1023
|
+
for (const p of registry.listProviders()) {
|
|
1024
|
+
const prefixes = p.prefixes?.length ? c.dim(` (${p.prefixes.join(", ")})`) : "";
|
|
1025
|
+
console.log(` ${c.cyan(p.name.padEnd(10))} ${p.description}${prefixes}`);
|
|
1026
|
+
}
|
|
1027
|
+
console.log();
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (!key && !cmd.all) {
|
|
1031
|
+
console.error(c.red(`${SYMBOLS.cross} Provide a key name or use --all`));
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
}
|
|
1034
|
+
const opts = buildOpts(cmd);
|
|
1035
|
+
if (cmd.all) {
|
|
1036
|
+
let entries = listSecrets(opts);
|
|
1037
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
1038
|
+
if (cmd.manifest) {
|
|
1039
|
+
const config = readProjectConfig(projectPath);
|
|
1040
|
+
if (config?.secrets) {
|
|
1041
|
+
const manifestKeys = new Set(Object.keys(config.secrets));
|
|
1042
|
+
entries = entries.filter((e) => manifestKeys.has(e.key));
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
console.log(c.bold(`
|
|
1046
|
+
${SYMBOLS.shield} Validating secrets
|
|
1047
|
+
`));
|
|
1048
|
+
let validated = 0;
|
|
1049
|
+
let skipped = 0;
|
|
1050
|
+
for (const entry of entries) {
|
|
1051
|
+
const value2 = getSecret(entry.key, { ...opts, scope: entry.scope });
|
|
1052
|
+
if (!value2) {
|
|
1053
|
+
skipped++;
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
const provHint2 = entry.envelope?.meta.provider ?? cmd.provider;
|
|
1057
|
+
const result2 = await validateSecret(value2, { provider: provHint2 });
|
|
1058
|
+
if (result2.status === "unknown") {
|
|
1059
|
+
skipped++;
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
validated++;
|
|
1063
|
+
const icon2 = result2.status === "valid" ? c.green(SYMBOLS.check) : result2.status === "invalid" ? c.red(SYMBOLS.cross) : c.yellow(SYMBOLS.warning);
|
|
1064
|
+
const statusText = result2.status === "valid" ? c.green("valid") : result2.status === "invalid" ? c.red("invalid") : c.yellow("error");
|
|
1065
|
+
console.log(
|
|
1066
|
+
` ${icon2} ${c.bold(entry.key.padEnd(24))} ${statusText} ${c.dim(`(${result2.provider}, ${result2.latencyMs}ms)`)}${result2.status !== "valid" ? ` ${c.dim("\u2014 " + result2.message)}` : ""}`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
console.log(`
|
|
1070
|
+
${c.dim(`${validated} validated, ${skipped} skipped (no provider)`)}
|
|
1071
|
+
`);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const value = getSecret(key, opts);
|
|
1075
|
+
if (!value) {
|
|
1076
|
+
console.error(c.red(`${SYMBOLS.cross} Secret "${key}" not found`));
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
const envelope = getEnvelope(key, opts);
|
|
1080
|
+
const provHint = envelope?.envelope.meta.provider ?? cmd.provider;
|
|
1081
|
+
const result = await validateSecret(value, { provider: provHint });
|
|
1082
|
+
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");
|
|
1083
|
+
console.log(`
|
|
1084
|
+
${icon} ${c.bold(key)} ${result.status} ${c.dim(`(${result.provider}, ${result.latencyMs}ms)`)}`);
|
|
1085
|
+
if (result.message && result.status !== "valid") {
|
|
1086
|
+
console.log(` ${c.dim(result.message)}`);
|
|
1087
|
+
}
|
|
1088
|
+
console.log();
|
|
1089
|
+
});
|
|
601
1090
|
program2.command("env").description("Show detected environment (wavefunction collapse context)").option("--project-path <path>", "Project path for detection").action((cmd) => {
|
|
602
1091
|
const result = collapseEnvironment({
|
|
603
1092
|
projectPath: cmd.projectPath ?? process.cwd()
|
|
@@ -657,6 +1146,24 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
|
|
|
657
1146
|
);
|
|
658
1147
|
}
|
|
659
1148
|
);
|
|
1149
|
+
program2.command("disentangle <sourceKey> <targetKey>").description("Unlink two entangled secrets").option("-g, --global", "Both in global scope").option("--source-project <path>", "Source project path").option("--target-project <path>", "Target project path").action(
|
|
1150
|
+
(sourceKey, targetKey, cmd) => {
|
|
1151
|
+
const sourceOpts = {
|
|
1152
|
+
scope: cmd.sourceProject ? "project" : "global",
|
|
1153
|
+
projectPath: cmd.sourceProject ?? process.cwd(),
|
|
1154
|
+
source: "cli"
|
|
1155
|
+
};
|
|
1156
|
+
const targetOpts = {
|
|
1157
|
+
scope: cmd.targetProject ? "project" : "global",
|
|
1158
|
+
projectPath: cmd.targetProject ?? process.cwd(),
|
|
1159
|
+
source: "cli"
|
|
1160
|
+
};
|
|
1161
|
+
disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts);
|
|
1162
|
+
console.log(
|
|
1163
|
+
`${SYMBOLS.link} ${c.yellow("disentangled")} ${c.bold(sourceKey)} ${SYMBOLS.arrow} ${c.bold(targetKey)}`
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
);
|
|
660
1167
|
const tunnel = program2.command("tunnel").description("Ephemeral in-memory secrets (quantum tunneling)");
|
|
661
1168
|
tunnel.command("create <value>").description("Create a tunneled secret (returns tunnel ID)").option("--ttl <seconds>", "Auto-expire after N seconds", parseInt).option("--max-reads <n>", "Self-destruct after N reads", parseInt).action((value, cmd) => {
|
|
662
1169
|
const id = tunnelCreate(value, {
|
|
@@ -884,8 +1391,166 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
884
1391
|
}
|
|
885
1392
|
console.log();
|
|
886
1393
|
});
|
|
1394
|
+
const hook = program2.command("hook").description("Manage secret change hooks (callbacks on write/delete/rotate)");
|
|
1395
|
+
hook.command("add").description("Register a new hook").option("--key <key>", "Trigger on exact key match").option("--key-pattern <pattern>", "Trigger on key glob pattern (e.g. DB_*)").option("--tag <tag>", "Trigger on secrets with this tag").option("--scope <scope>", "Trigger only for this scope (global or project)").option("--action <actions>", "Comma-separated actions: write,delete,rotate", "write,delete,rotate").option("--exec <command>", "Shell command to execute").option("--url <url>", "HTTP URL to POST to").option("--signal-target <target>", "Process name or PID to signal").option("--signal-name <signal>", "Signal to send (default: SIGHUP)", "SIGHUP").option("--description <desc>", "Human-readable description").action((cmd) => {
|
|
1396
|
+
let type;
|
|
1397
|
+
if (cmd.exec) type = "shell";
|
|
1398
|
+
else if (cmd.url) type = "http";
|
|
1399
|
+
else if (cmd.signalTarget) type = "signal";
|
|
1400
|
+
else {
|
|
1401
|
+
console.error(c.red(`${SYMBOLS.cross} Specify --exec, --url, or --signal-target`));
|
|
1402
|
+
process.exit(1);
|
|
1403
|
+
}
|
|
1404
|
+
if (!cmd.key && !cmd.keyPattern && !cmd.tag) {
|
|
1405
|
+
console.error(c.red(`${SYMBOLS.cross} Specify at least one match criterion: --key, --key-pattern, or --tag`));
|
|
1406
|
+
process.exit(1);
|
|
1407
|
+
}
|
|
1408
|
+
const actions = cmd.action.split(",").map((a) => a.trim());
|
|
1409
|
+
const entry = registerHook({
|
|
1410
|
+
type,
|
|
1411
|
+
match: {
|
|
1412
|
+
key: cmd.key,
|
|
1413
|
+
keyPattern: cmd.keyPattern,
|
|
1414
|
+
tag: cmd.tag,
|
|
1415
|
+
scope: cmd.scope,
|
|
1416
|
+
action: actions
|
|
1417
|
+
},
|
|
1418
|
+
command: cmd.exec,
|
|
1419
|
+
url: cmd.url,
|
|
1420
|
+
signal: cmd.signalTarget ? { target: cmd.signalTarget, signal: cmd.signalName } : void 0,
|
|
1421
|
+
description: cmd.description,
|
|
1422
|
+
enabled: true
|
|
1423
|
+
});
|
|
1424
|
+
console.log(`${SYMBOLS.check} ${c.green("registered")} hook ${c.bold(entry.id)} (${type})`);
|
|
1425
|
+
if (cmd.key) console.log(c.dim(` key: ${cmd.key}`));
|
|
1426
|
+
if (cmd.keyPattern) console.log(c.dim(` pattern: ${cmd.keyPattern}`));
|
|
1427
|
+
if (cmd.tag) console.log(c.dim(` tag: ${cmd.tag}`));
|
|
1428
|
+
});
|
|
1429
|
+
hook.command("list").alias("ls").description("List all registered hooks").action(() => {
|
|
1430
|
+
const hooks = listHooks();
|
|
1431
|
+
if (hooks.length === 0) {
|
|
1432
|
+
console.log(c.dim("No hooks registered"));
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
console.log(c.bold(`
|
|
1436
|
+
${SYMBOLS.zap} Registered hooks (${hooks.length})
|
|
1437
|
+
`));
|
|
1438
|
+
for (const h of hooks) {
|
|
1439
|
+
const status = h.enabled ? c.green("on") : c.red("off");
|
|
1440
|
+
const matchParts = [];
|
|
1441
|
+
if (h.match.key) matchParts.push(`key=${h.match.key}`);
|
|
1442
|
+
if (h.match.keyPattern) matchParts.push(`pattern=${h.match.keyPattern}`);
|
|
1443
|
+
if (h.match.tag) matchParts.push(`tag=${h.match.tag}`);
|
|
1444
|
+
if (h.match.scope) matchParts.push(`scope=${h.match.scope}`);
|
|
1445
|
+
if (h.match.action?.length) matchParts.push(`actions=${h.match.action.join(",")}`);
|
|
1446
|
+
const target = h.type === "shell" ? h.command : h.type === "http" ? h.url : h.signal ? `${h.signal.target} (${h.signal.signal ?? "SIGHUP"})` : "?";
|
|
1447
|
+
console.log(` ${c.bold(h.id)} [${status}] ${c.cyan(h.type)} ${c.dim(matchParts.join(" "))}`);
|
|
1448
|
+
console.log(` ${c.dim("\u2192")} ${target}${h.description ? ` ${c.dim(`\u2014 ${h.description}`)}` : ""}`);
|
|
1449
|
+
}
|
|
1450
|
+
console.log();
|
|
1451
|
+
});
|
|
1452
|
+
hook.command("remove <id>").alias("rm").description("Remove a hook by ID").action((id) => {
|
|
1453
|
+
if (removeHook(id)) {
|
|
1454
|
+
console.log(`${SYMBOLS.check} ${c.green("removed")} hook ${c.bold(id)}`);
|
|
1455
|
+
} else {
|
|
1456
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
1457
|
+
process.exit(1);
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
hook.command("enable <id>").description("Enable a hook").action((id) => {
|
|
1461
|
+
if (enableHook(id)) {
|
|
1462
|
+
console.log(`${SYMBOLS.check} ${c.green("enabled")} hook ${c.bold(id)}`);
|
|
1463
|
+
} else {
|
|
1464
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
1465
|
+
process.exit(1);
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
hook.command("disable <id>").description("Disable a hook").action((id) => {
|
|
1469
|
+
if (disableHook(id)) {
|
|
1470
|
+
console.log(`${SYMBOLS.check} ${c.yellow("disabled")} hook ${c.bold(id)}`);
|
|
1471
|
+
} else {
|
|
1472
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
1473
|
+
process.exit(1);
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
hook.command("test <id>").description("Dry-run a hook with a mock payload").action(async (id) => {
|
|
1477
|
+
const hooks = listHooks();
|
|
1478
|
+
const h = hooks.find((hook2) => hook2.id === id);
|
|
1479
|
+
if (!h) {
|
|
1480
|
+
console.error(c.red(`${SYMBOLS.cross} Hook "${id}" not found`));
|
|
1481
|
+
process.exit(1);
|
|
1482
|
+
}
|
|
1483
|
+
console.log(c.dim(`Testing hook ${id} (${h.type})...
|
|
1484
|
+
`));
|
|
1485
|
+
const payload = {
|
|
1486
|
+
action: "write",
|
|
1487
|
+
key: h.match.key ?? "TEST_KEY",
|
|
1488
|
+
scope: h.match.scope ?? "global",
|
|
1489
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1490
|
+
source: "cli"
|
|
1491
|
+
};
|
|
1492
|
+
const results = await fireHooks(payload);
|
|
1493
|
+
const result = results.find((r) => r.hookId === id);
|
|
1494
|
+
if (result) {
|
|
1495
|
+
const icon = result.success ? c.green(SYMBOLS.check) : c.red(SYMBOLS.cross);
|
|
1496
|
+
console.log(` ${icon} ${result.message}`);
|
|
1497
|
+
} else {
|
|
1498
|
+
console.log(c.yellow(` ${SYMBOLS.warning} Hook did not match the test payload`));
|
|
1499
|
+
}
|
|
1500
|
+
console.log();
|
|
1501
|
+
});
|
|
1502
|
+
program2.command("env:generate").description("Generate a .env file from the project manifest (.q-ring.json)").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) => {
|
|
1503
|
+
const projectPath = cmd.projectPath ?? process.cwd();
|
|
1504
|
+
const config = readProjectConfig(projectPath);
|
|
1505
|
+
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
1506
|
+
console.error(
|
|
1507
|
+
c.red(`${SYMBOLS.cross} No secrets manifest found in .q-ring.json`)
|
|
1508
|
+
);
|
|
1509
|
+
process.exit(1);
|
|
1510
|
+
}
|
|
1511
|
+
const opts = buildOpts(cmd);
|
|
1512
|
+
const lines = [];
|
|
1513
|
+
const warnings = [];
|
|
1514
|
+
for (const [key, manifest] of Object.entries(config.secrets)) {
|
|
1515
|
+
const value = getSecret(key, { ...opts, projectPath, source: "cli" });
|
|
1516
|
+
if (value === null) {
|
|
1517
|
+
if (manifest.required !== false) {
|
|
1518
|
+
warnings.push(`MISSING (required): ${key}`);
|
|
1519
|
+
}
|
|
1520
|
+
lines.push(`# ${key}= ${manifest.description ? `# ${manifest.description}` : ""}`);
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
const result = getEnvelope(key, { projectPath, source: "cli" });
|
|
1524
|
+
if (result) {
|
|
1525
|
+
const decay = checkDecay(result.envelope);
|
|
1526
|
+
if (decay.isExpired) {
|
|
1527
|
+
warnings.push(`EXPIRED: ${key}`);
|
|
1528
|
+
} else if (decay.isStale) {
|
|
1529
|
+
warnings.push(`STALE (${decay.lifetimePercent}%): ${key}`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
1533
|
+
lines.push(`${key}="${escaped}"`);
|
|
1534
|
+
}
|
|
1535
|
+
const output = lines.join("\n") + "\n";
|
|
1536
|
+
if (cmd.output) {
|
|
1537
|
+
writeFileSync(cmd.output, output);
|
|
1538
|
+
console.log(
|
|
1539
|
+
`${SYMBOLS.check} ${c.green("generated")} ${c.bold(cmd.output)} (${Object.keys(config.secrets).length} keys)`
|
|
1540
|
+
);
|
|
1541
|
+
} else {
|
|
1542
|
+
process.stdout.write(output);
|
|
1543
|
+
}
|
|
1544
|
+
if (warnings.length > 0 && process.stderr.isTTY) {
|
|
1545
|
+
console.error();
|
|
1546
|
+
for (const w of warnings) {
|
|
1547
|
+
console.error(` ${c.yellow(SYMBOLS.warning)} ${w}`);
|
|
1548
|
+
}
|
|
1549
|
+
console.error();
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
887
1552
|
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) => {
|
|
888
|
-
const { startDashboardServer } = await import("./dashboard-
|
|
1553
|
+
const { startDashboardServer } = await import("./dashboard-HVIQO6NT.js");
|
|
889
1554
|
const { exec } = await import("child_process");
|
|
890
1555
|
const { platform } = await import("os");
|
|
891
1556
|
const port = Number(cmd.port);
|