envpkt 0.6.10 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +664 -317
- package/dist/index.d.ts +32 -1
- package/dist/index.js +152 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,8 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync,
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
+
import { Cond, Either, Left, List, Option, Right, Try } from "functype";
|
|
6
7
|
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
7
|
-
import { Cond, Left, List, Option, Right, Try } from "functype";
|
|
8
8
|
import { Env, Fs, Path, Platform } from "functype-os";
|
|
9
9
|
import { TomlDate, parse, stringify } from "smol-toml";
|
|
10
10
|
import { FormatRegistry, Type } from "@sinclair/typebox";
|
|
@@ -15,6 +15,105 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
15
15
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
16
16
|
import { createInterface } from "node:readline";
|
|
17
17
|
|
|
18
|
+
//#region src/core/audit.ts
|
|
19
|
+
const MS_PER_DAY = 864e5;
|
|
20
|
+
const WARN_BEFORE_DAYS = 30;
|
|
21
|
+
const daysBetween = (from, to) => Math.floor((to.getTime() - from.getTime()) / MS_PER_DAY);
|
|
22
|
+
const parseDate = (dateStr) => {
|
|
23
|
+
const d = /* @__PURE__ */ new Date(`${dateStr}T00:00:00Z`);
|
|
24
|
+
return Number.isNaN(d.getTime()) ? Option(void 0) : Option(d);
|
|
25
|
+
};
|
|
26
|
+
const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
|
|
27
|
+
const issues = [];
|
|
28
|
+
const created = Option(meta?.created).flatMap(parseDate);
|
|
29
|
+
const expires = Option(meta?.expires).flatMap(parseDate);
|
|
30
|
+
const rotationUrl = Option(meta?.rotation_url);
|
|
31
|
+
const purpose = Option(meta?.purpose);
|
|
32
|
+
const service = Option(meta?.service);
|
|
33
|
+
const daysRemaining = expires.map((exp) => daysBetween(today, exp));
|
|
34
|
+
const daysSinceCreated = created.map((c) => daysBetween(c, today));
|
|
35
|
+
const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
|
|
36
|
+
const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
|
|
37
|
+
const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
|
|
38
|
+
const hasSealed = !!meta?.encrypted_value;
|
|
39
|
+
const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
|
|
40
|
+
const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
|
|
41
|
+
if (isExpired) issues.push("Secret has expired");
|
|
42
|
+
if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
|
|
43
|
+
if (isStale) issues.push("Secret is stale (no rotation detected)");
|
|
44
|
+
if (isMissing) issues.push("Key not found in fnox");
|
|
45
|
+
if (isMissingMetadata) {
|
|
46
|
+
if (requireExpiration && expires.isNone()) issues.push("Missing required expiration date");
|
|
47
|
+
if (requireService && service.isNone()) issues.push("Missing required service");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
key,
|
|
51
|
+
service,
|
|
52
|
+
status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").else("healthy"),
|
|
53
|
+
days_remaining: daysRemaining,
|
|
54
|
+
rotation_url: rotationUrl,
|
|
55
|
+
purpose,
|
|
56
|
+
created: Option(meta?.created),
|
|
57
|
+
expires: Option(meta?.expires),
|
|
58
|
+
issues: List(issues)
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const computeAudit = (config, fnoxKeys, today) => {
|
|
62
|
+
const now = today ?? /* @__PURE__ */ new Date();
|
|
63
|
+
const lifecycle = config.lifecycle ?? {};
|
|
64
|
+
const staleWarningDays = lifecycle.stale_warning_days ?? 90;
|
|
65
|
+
const requireExpiration = lifecycle.require_expiration ?? false;
|
|
66
|
+
const requireService = lifecycle.require_service ?? false;
|
|
67
|
+
const keys = fnoxKeys ?? /* @__PURE__ */ new Set();
|
|
68
|
+
const secretEntries = config.secret ?? {};
|
|
69
|
+
const metaKeys = new Set(Object.keys(secretEntries));
|
|
70
|
+
const secrets = List(Object.entries(secretEntries).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
|
|
71
|
+
const orphaned = keys.size > 0 ? [...metaKeys].filter((k) => !keys.has(k)).length : 0;
|
|
72
|
+
const total = secrets.size;
|
|
73
|
+
const expired = secrets.count((s) => s.status === "expired");
|
|
74
|
+
const missing = secrets.count((s) => s.status === "missing");
|
|
75
|
+
const missing_metadata = secrets.count((s) => s.status === "missing_metadata");
|
|
76
|
+
const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
|
|
77
|
+
const stale = secrets.count((s) => s.status === "stale");
|
|
78
|
+
const healthy = secrets.count((s) => s.status === "healthy");
|
|
79
|
+
return {
|
|
80
|
+
status: Cond.of().when(expired > 0 || missing > 0, "critical").elseWhen(expiring_soon > 0 || stale > 0 || missing_metadata > 0, "degraded").else("healthy"),
|
|
81
|
+
secrets,
|
|
82
|
+
total,
|
|
83
|
+
healthy,
|
|
84
|
+
expiring_soon,
|
|
85
|
+
expired,
|
|
86
|
+
stale,
|
|
87
|
+
missing,
|
|
88
|
+
missing_metadata,
|
|
89
|
+
orphaned,
|
|
90
|
+
identity: config.identity
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
const computeEnvAudit = (config, env = process.env) => {
|
|
94
|
+
const envEntries = config.env ?? {};
|
|
95
|
+
const entries = [];
|
|
96
|
+
for (const [key, entry] of Object.entries(envEntries)) {
|
|
97
|
+
const currentValue = env[key];
|
|
98
|
+
const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
|
|
99
|
+
entries.push({
|
|
100
|
+
key,
|
|
101
|
+
defaultValue: entry.value,
|
|
102
|
+
currentValue,
|
|
103
|
+
status,
|
|
104
|
+
purpose: entry.purpose
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
entries,
|
|
109
|
+
total: entries.length,
|
|
110
|
+
defaults_applied: entries.filter((e) => e.status === "default").length,
|
|
111
|
+
overridden: entries.filter((e) => e.status === "overridden").length,
|
|
112
|
+
missing: entries.filter((e) => e.status === "missing").length
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
18
117
|
//#region src/core/schema.ts
|
|
19
118
|
const DATE_RE$1 = /^\d{4}-\d{2}-\d{2}$/;
|
|
20
119
|
const URI_RE = /^https?:\/\/.+/;
|
|
@@ -270,6 +369,83 @@ const resolveConfigPath = (flagPath, envVar, cwd) => {
|
|
|
270
369
|
}));
|
|
271
370
|
};
|
|
272
371
|
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/core/catalog.ts
|
|
374
|
+
/** Load and validate a catalog file, mapping ConfigError → CatalogError */
|
|
375
|
+
const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
|
|
376
|
+
if (err._tag === "FileNotFound") return Left({
|
|
377
|
+
_tag: "CatalogNotFound",
|
|
378
|
+
path: err.path
|
|
379
|
+
});
|
|
380
|
+
return Left({
|
|
381
|
+
_tag: "CatalogLoadError",
|
|
382
|
+
message: `${err._tag}: ${"message" in err ? err.message : String(err)}`
|
|
383
|
+
});
|
|
384
|
+
}, (config) => Right(config));
|
|
385
|
+
/** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
|
|
386
|
+
const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
|
|
387
|
+
const resolved = {};
|
|
388
|
+
for (const key of agentSecrets) {
|
|
389
|
+
const catalogEntry = catalogMeta[key];
|
|
390
|
+
if (!catalogEntry) return Left({
|
|
391
|
+
_tag: "SecretNotInCatalog",
|
|
392
|
+
key,
|
|
393
|
+
catalogPath
|
|
394
|
+
});
|
|
395
|
+
const agentOverride = agentMeta[key];
|
|
396
|
+
if (agentOverride) resolved[key] = {
|
|
397
|
+
...catalogEntry,
|
|
398
|
+
...agentOverride
|
|
399
|
+
};
|
|
400
|
+
else resolved[key] = catalogEntry;
|
|
401
|
+
}
|
|
402
|
+
return Right(resolved);
|
|
403
|
+
};
|
|
404
|
+
/** Resolve an agent config against its catalog (if any), producing a flat self-contained config */
|
|
405
|
+
const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
406
|
+
if (!agentConfig.catalog) return Right({
|
|
407
|
+
config: agentConfig,
|
|
408
|
+
merged: [],
|
|
409
|
+
overridden: [],
|
|
410
|
+
warnings: []
|
|
411
|
+
});
|
|
412
|
+
if (!agentConfig.identity?.secrets || agentConfig.identity.secrets.length === 0) return Left({
|
|
413
|
+
_tag: "MissingSecretsList",
|
|
414
|
+
message: "Config has 'catalog' but identity.secrets is missing — declare which catalog secrets this agent needs"
|
|
415
|
+
});
|
|
416
|
+
const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
|
|
417
|
+
const agentSecrets = agentConfig.identity.secrets;
|
|
418
|
+
const agentSecretEntries = agentConfig.secret ?? {};
|
|
419
|
+
return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
|
|
420
|
+
const merged = [];
|
|
421
|
+
const overridden = [];
|
|
422
|
+
const warnings = [];
|
|
423
|
+
for (const key of agentSecrets) {
|
|
424
|
+
merged.push(key);
|
|
425
|
+
if (agentSecretEntries[key]) overridden.push(key);
|
|
426
|
+
}
|
|
427
|
+
const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
|
|
428
|
+
const identityData = agentConfig.identity ? (() => {
|
|
429
|
+
const { secrets: _secrets, ...rest } = agentConfig.identity;
|
|
430
|
+
return rest;
|
|
431
|
+
})() : void 0;
|
|
432
|
+
return {
|
|
433
|
+
config: {
|
|
434
|
+
...agentWithoutCatalog,
|
|
435
|
+
identity: identityData ? {
|
|
436
|
+
...identityData,
|
|
437
|
+
name: identityData.name
|
|
438
|
+
} : void 0,
|
|
439
|
+
secret: resolvedMeta
|
|
440
|
+
},
|
|
441
|
+
catalogPath,
|
|
442
|
+
merged,
|
|
443
|
+
overridden,
|
|
444
|
+
warnings
|
|
445
|
+
};
|
|
446
|
+
}));
|
|
447
|
+
};
|
|
448
|
+
|
|
273
449
|
//#endregion
|
|
274
450
|
//#region src/cli/output.ts
|
|
275
451
|
const RESET = "\x1B[0m";
|
|
@@ -454,306 +630,27 @@ const formatCheckTable = (check) => {
|
|
|
454
630
|
const formatCheckJson = (check) => JSON.stringify({
|
|
455
631
|
is_clean: check.is_clean,
|
|
456
632
|
tracked_and_present: check.tracked_and_present,
|
|
457
|
-
missing_from_env: check.missing_from_env,
|
|
458
|
-
untracked_credentials: check.untracked_credentials,
|
|
459
|
-
entries: check.entries.map((e) => ({
|
|
460
|
-
envVar: e.envVar,
|
|
461
|
-
service: e.service.fold(() => null, (s) => s),
|
|
462
|
-
status: e.status,
|
|
463
|
-
confidence: e.confidence.fold(() => null, (c) => c)
|
|
464
|
-
})).toArray()
|
|
465
|
-
}, null, 2);
|
|
466
|
-
const formatAuditMinimal = (audit) => {
|
|
467
|
-
if (audit.status === "healthy") return `${GREEN}✓${RESET} ${audit.total} secrets healthy`;
|
|
468
|
-
const parts = [];
|
|
469
|
-
if (audit.expired > 0) parts.push(`${audit.expired} expired`);
|
|
470
|
-
if (audit.expiring_soon > 0) parts.push(`${audit.expiring_soon} expiring`);
|
|
471
|
-
if (audit.stale > 0) parts.push(`${audit.stale} stale`);
|
|
472
|
-
if (audit.missing > 0) parts.push(`${audit.missing} missing`);
|
|
473
|
-
return `${audit.status === "critical" ? `${RED}✗${RESET}` : `${YELLOW}⚠${RESET}`} ${parts.join(", ")}`;
|
|
474
|
-
};
|
|
475
|
-
const formatConfigSource = (path, source) => {
|
|
476
|
-
if (source === "cwd") return "";
|
|
477
|
-
return `${DIM}envpkt: loaded ${path}${RESET}`;
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
//#endregion
|
|
481
|
-
//#region src/cli/commands/add.ts
|
|
482
|
-
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
483
|
-
const buildSecretBlock = (name, options) => {
|
|
484
|
-
const lines = [`[secret.${name}]`];
|
|
485
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
486
|
-
if (options.service) lines.push(`service = "${options.service}"`);
|
|
487
|
-
if (options.purpose) lines.push(`purpose = "${options.purpose}"`);
|
|
488
|
-
if (options.comment) lines.push(`comment = "${options.comment}"`);
|
|
489
|
-
lines.push(`created = "${today}"`);
|
|
490
|
-
if (options.expires) lines.push(`expires = "${options.expires}"`);
|
|
491
|
-
if (options.rotates) lines.push(`rotates = "${options.rotates}"`);
|
|
492
|
-
if (options.rateLimit) lines.push(`rate_limit = "${options.rateLimit}"`);
|
|
493
|
-
if (options.modelHint) lines.push(`model_hint = "${options.modelHint}"`);
|
|
494
|
-
if (options.source) lines.push(`source = "${options.source}"`);
|
|
495
|
-
if (options.rotationUrl) lines.push(`rotation_url = "${options.rotationUrl}"`);
|
|
496
|
-
if (options.required) lines.push(`required = true`);
|
|
497
|
-
if (options.capabilities) {
|
|
498
|
-
const caps = options.capabilities.split(",").map((c) => `"${c.trim()}"`).join(", ");
|
|
499
|
-
lines.push(`capabilities = [${caps}]`);
|
|
500
|
-
}
|
|
501
|
-
if (options.tags) {
|
|
502
|
-
const pairs = options.tags.split(",").map((pair) => {
|
|
503
|
-
const [k, v] = pair.split("=").map((s) => s.trim());
|
|
504
|
-
return `${k} = "${v}"`;
|
|
505
|
-
});
|
|
506
|
-
lines.push(`tags = { ${pairs.join(", ")} }`);
|
|
507
|
-
}
|
|
508
|
-
return `${lines.join("\n")}\n`;
|
|
509
|
-
};
|
|
510
|
-
const runAdd = (name, options) => {
|
|
511
|
-
if (options.expires && !DATE_RE.test(options.expires)) {
|
|
512
|
-
console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
|
|
513
|
-
process.exit(1);
|
|
514
|
-
}
|
|
515
|
-
resolveConfigPath(options.config).fold((err) => {
|
|
516
|
-
console.error(formatError(err));
|
|
517
|
-
process.exit(2);
|
|
518
|
-
}, ({ path: configPath, source }) => {
|
|
519
|
-
const sourceMsg = formatConfigSource(configPath, source);
|
|
520
|
-
if (sourceMsg) console.error(sourceMsg);
|
|
521
|
-
loadConfig(configPath).fold((err) => {
|
|
522
|
-
console.error(formatError(err));
|
|
523
|
-
process.exit(2);
|
|
524
|
-
}, (config) => {
|
|
525
|
-
if (config.secret?.[name]) {
|
|
526
|
-
console.error(`${RED}Error:${RESET} Secret "${name}" already exists in ${configPath}`);
|
|
527
|
-
process.exit(1);
|
|
528
|
-
}
|
|
529
|
-
const block = buildSecretBlock(name, options);
|
|
530
|
-
if (options.dryRun) {
|
|
531
|
-
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
532
|
-
console.log(block);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
writeFileSync(configPath, `${readFileSync(configPath, "utf-8").trimEnd()}\n\n${block}`, "utf-8");
|
|
536
|
-
console.log(`${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
//#endregion
|
|
542
|
-
//#region src/cli/commands/add-env.ts
|
|
543
|
-
const buildEnvBlock = (name, value, options) => {
|
|
544
|
-
const lines = [`[env.${name}]`, `value = "${value}"`];
|
|
545
|
-
if (options.purpose) lines.push(`purpose = "${options.purpose}"`);
|
|
546
|
-
if (options.comment) lines.push(`comment = "${options.comment}"`);
|
|
547
|
-
if (options.tags) {
|
|
548
|
-
const pairs = options.tags.split(",").map((pair) => {
|
|
549
|
-
const [k, v] = pair.split("=").map((s) => s.trim());
|
|
550
|
-
return `${k} = "${v}"`;
|
|
551
|
-
});
|
|
552
|
-
lines.push(`tags = { ${pairs.join(", ")} }`);
|
|
553
|
-
}
|
|
554
|
-
return `${lines.join("\n")}\n`;
|
|
555
|
-
};
|
|
556
|
-
const runAddEnv = (name, value, options) => {
|
|
557
|
-
resolveConfigPath(options.config).fold((err) => {
|
|
558
|
-
console.error(formatError(err));
|
|
559
|
-
process.exit(2);
|
|
560
|
-
}, ({ path: configPath, source }) => {
|
|
561
|
-
const sourceMsg = formatConfigSource(configPath, source);
|
|
562
|
-
if (sourceMsg) console.error(sourceMsg);
|
|
563
|
-
loadConfig(configPath).fold((err) => {
|
|
564
|
-
console.error(formatError(err));
|
|
565
|
-
process.exit(2);
|
|
566
|
-
}, (config) => {
|
|
567
|
-
if (config.env?.[name]) {
|
|
568
|
-
console.error(`${RED}Error:${RESET} Env entry "${name}" already exists in ${configPath}`);
|
|
569
|
-
process.exit(1);
|
|
570
|
-
}
|
|
571
|
-
const block = buildEnvBlock(name, value, options);
|
|
572
|
-
if (options.dryRun) {
|
|
573
|
-
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
574
|
-
console.log(block);
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
writeFileSync(configPath, `${readFileSync(configPath, "utf-8").trimEnd()}\n\n${block}`, "utf-8");
|
|
578
|
-
console.log(`${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
|
|
579
|
-
});
|
|
580
|
-
});
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
//#endregion
|
|
584
|
-
//#region src/core/audit.ts
|
|
585
|
-
const MS_PER_DAY = 864e5;
|
|
586
|
-
const WARN_BEFORE_DAYS = 30;
|
|
587
|
-
const daysBetween = (from, to) => Math.floor((to.getTime() - from.getTime()) / MS_PER_DAY);
|
|
588
|
-
const parseDate = (dateStr) => {
|
|
589
|
-
const d = /* @__PURE__ */ new Date(`${dateStr}T00:00:00Z`);
|
|
590
|
-
return Number.isNaN(d.getTime()) ? Option(void 0) : Option(d);
|
|
591
|
-
};
|
|
592
|
-
const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
|
|
593
|
-
const issues = [];
|
|
594
|
-
const created = Option(meta?.created).flatMap(parseDate);
|
|
595
|
-
const expires = Option(meta?.expires).flatMap(parseDate);
|
|
596
|
-
const rotationUrl = Option(meta?.rotation_url);
|
|
597
|
-
const purpose = Option(meta?.purpose);
|
|
598
|
-
const service = Option(meta?.service);
|
|
599
|
-
const daysRemaining = expires.map((exp) => daysBetween(today, exp));
|
|
600
|
-
const daysSinceCreated = created.map((c) => daysBetween(c, today));
|
|
601
|
-
const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
|
|
602
|
-
const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
|
|
603
|
-
const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
|
|
604
|
-
const hasSealed = !!meta?.encrypted_value;
|
|
605
|
-
const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
|
|
606
|
-
const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
|
|
607
|
-
if (isExpired) issues.push("Secret has expired");
|
|
608
|
-
if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
|
|
609
|
-
if (isStale) issues.push("Secret is stale (no rotation detected)");
|
|
610
|
-
if (isMissing) issues.push("Key not found in fnox");
|
|
611
|
-
if (isMissingMetadata) {
|
|
612
|
-
if (requireExpiration && expires.isNone()) issues.push("Missing required expiration date");
|
|
613
|
-
if (requireService && service.isNone()) issues.push("Missing required service");
|
|
614
|
-
}
|
|
615
|
-
return {
|
|
616
|
-
key,
|
|
617
|
-
service,
|
|
618
|
-
status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").else("healthy"),
|
|
619
|
-
days_remaining: daysRemaining,
|
|
620
|
-
rotation_url: rotationUrl,
|
|
621
|
-
purpose,
|
|
622
|
-
created: Option(meta?.created),
|
|
623
|
-
expires: Option(meta?.expires),
|
|
624
|
-
issues: List(issues)
|
|
625
|
-
};
|
|
626
|
-
};
|
|
627
|
-
const computeAudit = (config, fnoxKeys, today) => {
|
|
628
|
-
const now = today ?? /* @__PURE__ */ new Date();
|
|
629
|
-
const lifecycle = config.lifecycle ?? {};
|
|
630
|
-
const staleWarningDays = lifecycle.stale_warning_days ?? 90;
|
|
631
|
-
const requireExpiration = lifecycle.require_expiration ?? false;
|
|
632
|
-
const requireService = lifecycle.require_service ?? false;
|
|
633
|
-
const keys = fnoxKeys ?? /* @__PURE__ */ new Set();
|
|
634
|
-
const secretEntries = config.secret ?? {};
|
|
635
|
-
const metaKeys = new Set(Object.keys(secretEntries));
|
|
636
|
-
const secrets = List(Object.entries(secretEntries).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
|
|
637
|
-
const orphaned = keys.size > 0 ? [...metaKeys].filter((k) => !keys.has(k)).length : 0;
|
|
638
|
-
const total = secrets.size;
|
|
639
|
-
const expired = secrets.count((s) => s.status === "expired");
|
|
640
|
-
const missing = secrets.count((s) => s.status === "missing");
|
|
641
|
-
const missing_metadata = secrets.count((s) => s.status === "missing_metadata");
|
|
642
|
-
const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
|
|
643
|
-
const stale = secrets.count((s) => s.status === "stale");
|
|
644
|
-
const healthy = secrets.count((s) => s.status === "healthy");
|
|
645
|
-
return {
|
|
646
|
-
status: Cond.of().when(expired > 0 || missing > 0, "critical").elseWhen(expiring_soon > 0 || stale > 0 || missing_metadata > 0, "degraded").else("healthy"),
|
|
647
|
-
secrets,
|
|
648
|
-
total,
|
|
649
|
-
healthy,
|
|
650
|
-
expiring_soon,
|
|
651
|
-
expired,
|
|
652
|
-
stale,
|
|
653
|
-
missing,
|
|
654
|
-
missing_metadata,
|
|
655
|
-
orphaned,
|
|
656
|
-
identity: config.identity
|
|
657
|
-
};
|
|
658
|
-
};
|
|
659
|
-
const computeEnvAudit = (config, env = process.env) => {
|
|
660
|
-
const envEntries = config.env ?? {};
|
|
661
|
-
const entries = [];
|
|
662
|
-
for (const [key, entry] of Object.entries(envEntries)) {
|
|
663
|
-
const currentValue = env[key];
|
|
664
|
-
const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
|
|
665
|
-
entries.push({
|
|
666
|
-
key,
|
|
667
|
-
defaultValue: entry.value,
|
|
668
|
-
currentValue,
|
|
669
|
-
status,
|
|
670
|
-
purpose: entry.purpose
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
return {
|
|
674
|
-
entries,
|
|
675
|
-
total: entries.length,
|
|
676
|
-
defaults_applied: entries.filter((e) => e.status === "default").length,
|
|
677
|
-
overridden: entries.filter((e) => e.status === "overridden").length,
|
|
678
|
-
missing: entries.filter((e) => e.status === "missing").length
|
|
679
|
-
};
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
//#endregion
|
|
683
|
-
//#region src/core/catalog.ts
|
|
684
|
-
/** Load and validate a catalog file, mapping ConfigError → CatalogError */
|
|
685
|
-
const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
|
|
686
|
-
if (err._tag === "FileNotFound") return Left({
|
|
687
|
-
_tag: "CatalogNotFound",
|
|
688
|
-
path: err.path
|
|
689
|
-
});
|
|
690
|
-
return Left({
|
|
691
|
-
_tag: "CatalogLoadError",
|
|
692
|
-
message: `${err._tag}: ${"message" in err ? err.message : String(err)}`
|
|
693
|
-
});
|
|
694
|
-
}, (config) => Right(config));
|
|
695
|
-
/** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
|
|
696
|
-
const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
|
|
697
|
-
const resolved = {};
|
|
698
|
-
for (const key of agentSecrets) {
|
|
699
|
-
const catalogEntry = catalogMeta[key];
|
|
700
|
-
if (!catalogEntry) return Left({
|
|
701
|
-
_tag: "SecretNotInCatalog",
|
|
702
|
-
key,
|
|
703
|
-
catalogPath
|
|
704
|
-
});
|
|
705
|
-
const agentOverride = agentMeta[key];
|
|
706
|
-
if (agentOverride) resolved[key] = {
|
|
707
|
-
...catalogEntry,
|
|
708
|
-
...agentOverride
|
|
709
|
-
};
|
|
710
|
-
else resolved[key] = catalogEntry;
|
|
711
|
-
}
|
|
712
|
-
return Right(resolved);
|
|
713
|
-
};
|
|
714
|
-
/** Resolve an agent config against its catalog (if any), producing a flat self-contained config */
|
|
715
|
-
const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
716
|
-
if (!agentConfig.catalog) return Right({
|
|
717
|
-
config: agentConfig,
|
|
718
|
-
merged: [],
|
|
719
|
-
overridden: [],
|
|
720
|
-
warnings: []
|
|
721
|
-
});
|
|
722
|
-
if (!agentConfig.identity?.secrets || agentConfig.identity.secrets.length === 0) return Left({
|
|
723
|
-
_tag: "MissingSecretsList",
|
|
724
|
-
message: "Config has 'catalog' but identity.secrets is missing — declare which catalog secrets this agent needs"
|
|
725
|
-
});
|
|
726
|
-
const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
|
|
727
|
-
const agentSecrets = agentConfig.identity.secrets;
|
|
728
|
-
const agentSecretEntries = agentConfig.secret ?? {};
|
|
729
|
-
return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
|
|
730
|
-
const merged = [];
|
|
731
|
-
const overridden = [];
|
|
732
|
-
const warnings = [];
|
|
733
|
-
for (const key of agentSecrets) {
|
|
734
|
-
merged.push(key);
|
|
735
|
-
if (agentSecretEntries[key]) overridden.push(key);
|
|
736
|
-
}
|
|
737
|
-
const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
|
|
738
|
-
const identityData = agentConfig.identity ? (() => {
|
|
739
|
-
const { secrets: _secrets, ...rest } = agentConfig.identity;
|
|
740
|
-
return rest;
|
|
741
|
-
})() : void 0;
|
|
742
|
-
return {
|
|
743
|
-
config: {
|
|
744
|
-
...agentWithoutCatalog,
|
|
745
|
-
identity: identityData ? {
|
|
746
|
-
...identityData,
|
|
747
|
-
name: identityData.name
|
|
748
|
-
} : void 0,
|
|
749
|
-
secret: resolvedMeta
|
|
750
|
-
},
|
|
751
|
-
catalogPath,
|
|
752
|
-
merged,
|
|
753
|
-
overridden,
|
|
754
|
-
warnings
|
|
755
|
-
};
|
|
756
|
-
}));
|
|
633
|
+
missing_from_env: check.missing_from_env,
|
|
634
|
+
untracked_credentials: check.untracked_credentials,
|
|
635
|
+
entries: check.entries.map((e) => ({
|
|
636
|
+
envVar: e.envVar,
|
|
637
|
+
service: e.service.fold(() => null, (s) => s),
|
|
638
|
+
status: e.status,
|
|
639
|
+
confidence: e.confidence.fold(() => null, (c) => c)
|
|
640
|
+
})).toArray()
|
|
641
|
+
}, null, 2);
|
|
642
|
+
const formatAuditMinimal = (audit) => {
|
|
643
|
+
if (audit.status === "healthy") return `${GREEN}✓${RESET} ${audit.total} secrets healthy`;
|
|
644
|
+
const parts = [];
|
|
645
|
+
if (audit.expired > 0) parts.push(`${audit.expired} expired`);
|
|
646
|
+
if (audit.expiring_soon > 0) parts.push(`${audit.expiring_soon} expiring`);
|
|
647
|
+
if (audit.stale > 0) parts.push(`${audit.stale} stale`);
|
|
648
|
+
if (audit.missing > 0) parts.push(`${audit.missing} missing`);
|
|
649
|
+
return `${audit.status === "critical" ? `${RED}✗${RESET}` : `${YELLOW}⚠${RESET}`} ${parts.join(", ")}`;
|
|
650
|
+
};
|
|
651
|
+
const formatConfigSource = (path, source) => {
|
|
652
|
+
if (source === "cwd") return "";
|
|
653
|
+
return `${DIM}envpkt: loaded ${path}${RESET}`;
|
|
757
654
|
};
|
|
758
655
|
|
|
759
656
|
//#endregion
|
|
@@ -2039,6 +1936,156 @@ created = "${todayIso$1()}"
|
|
|
2039
1936
|
return blocks.join("\n");
|
|
2040
1937
|
};
|
|
2041
1938
|
|
|
1939
|
+
//#endregion
|
|
1940
|
+
//#region src/core/toml-edit.ts
|
|
1941
|
+
const SECTION_RE = /^\[.+\]\s*$/;
|
|
1942
|
+
const MULTILINE_OPEN = "\"\"\"";
|
|
1943
|
+
/**
|
|
1944
|
+
* Find the line range [start, end) of a TOML section by its header string.
|
|
1945
|
+
* The range includes the header line through to (but not including) the next section header or EOF.
|
|
1946
|
+
* Handles multiline `"""..."""` values when scanning for section boundaries.
|
|
1947
|
+
*/
|
|
1948
|
+
const findSectionRange = (lines, sectionHeader) => {
|
|
1949
|
+
let start = -1;
|
|
1950
|
+
for (let i = 0; i < lines.length; i++) if (lines[i].trim() === sectionHeader) {
|
|
1951
|
+
start = i;
|
|
1952
|
+
break;
|
|
1953
|
+
}
|
|
1954
|
+
if (start === -1) return void 0;
|
|
1955
|
+
let end = lines.length;
|
|
1956
|
+
let inMultiline = false;
|
|
1957
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
1958
|
+
const line = lines[i];
|
|
1959
|
+
if (inMultiline) {
|
|
1960
|
+
if (line.includes(MULTILINE_OPEN)) inMultiline = false;
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
if (line.includes(MULTILINE_OPEN)) {
|
|
1964
|
+
if ((line.slice(line.indexOf("=") + 1).trim().match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1) inMultiline = true;
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
if (SECTION_RE.test(line)) {
|
|
1968
|
+
end = i;
|
|
1969
|
+
break;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return {
|
|
1973
|
+
start,
|
|
1974
|
+
end
|
|
1975
|
+
};
|
|
1976
|
+
};
|
|
1977
|
+
/** Check whether a section header exists in the raw TOML */
|
|
1978
|
+
const sectionExists = (lines, sectionHeader) => lines.some((l) => l.trim() === sectionHeader);
|
|
1979
|
+
/**
|
|
1980
|
+
* Remove a TOML section (e.g. `[secret.X]`) and all its fields through the next section or EOF.
|
|
1981
|
+
* Strips trailing blank lines left behind.
|
|
1982
|
+
*/
|
|
1983
|
+
const removeSection = (raw, sectionHeader) => {
|
|
1984
|
+
const lines = raw.split("\n");
|
|
1985
|
+
const range = findSectionRange(lines, sectionHeader);
|
|
1986
|
+
if (!range) return Either.left({
|
|
1987
|
+
_tag: "SectionNotFound",
|
|
1988
|
+
section: sectionHeader
|
|
1989
|
+
});
|
|
1990
|
+
let removeEnd = range.end;
|
|
1991
|
+
while (removeEnd > range.start && removeEnd - 1 >= range.start && lines[removeEnd - 1].trim() === "") removeEnd--;
|
|
1992
|
+
const before = lines.slice(0, range.start);
|
|
1993
|
+
const after = lines.slice(range.end);
|
|
1994
|
+
while (before.length > 0 && before[before.length - 1].trim() === "") before.pop();
|
|
1995
|
+
const result = [...before, ...after].join("\n");
|
|
1996
|
+
return Either.right(result);
|
|
1997
|
+
};
|
|
1998
|
+
/**
|
|
1999
|
+
* Rename a TOML section header (e.g. `[secret.OLD]` → `[secret.NEW]`).
|
|
2000
|
+
* Errors if old doesn't exist or new already exists.
|
|
2001
|
+
*/
|
|
2002
|
+
const renameSection = (raw, oldHeader, newHeader) => {
|
|
2003
|
+
const lines = raw.split("\n");
|
|
2004
|
+
if (!sectionExists(lines, oldHeader)) return Either.left({
|
|
2005
|
+
_tag: "SectionNotFound",
|
|
2006
|
+
section: oldHeader
|
|
2007
|
+
});
|
|
2008
|
+
if (sectionExists(lines, newHeader)) return Either.left({
|
|
2009
|
+
_tag: "SectionAlreadyExists",
|
|
2010
|
+
section: newHeader
|
|
2011
|
+
});
|
|
2012
|
+
const result = lines.map((line) => line.trim() === oldHeader ? newHeader : line).join("\n");
|
|
2013
|
+
return Either.right(result);
|
|
2014
|
+
};
|
|
2015
|
+
/**
|
|
2016
|
+
* Update, add, or remove fields within an existing TOML section.
|
|
2017
|
+
* - A string value replaces or adds the field
|
|
2018
|
+
* - A null value removes the field
|
|
2019
|
+
* Does NOT re-serialize — operates on raw text lines.
|
|
2020
|
+
*/
|
|
2021
|
+
const updateSectionFields = (raw, sectionHeader, updates) => {
|
|
2022
|
+
const lines = raw.split("\n");
|
|
2023
|
+
const range = findSectionRange(lines, sectionHeader);
|
|
2024
|
+
if (!range) return Either.left({
|
|
2025
|
+
_tag: "SectionNotFound",
|
|
2026
|
+
section: sectionHeader
|
|
2027
|
+
});
|
|
2028
|
+
const before = lines.slice(0, range.start + 1);
|
|
2029
|
+
const after = lines.slice(range.end);
|
|
2030
|
+
const sectionBody = lines.slice(range.start + 1, range.end);
|
|
2031
|
+
const remaining = [];
|
|
2032
|
+
const updatedKeys = /* @__PURE__ */ new Set();
|
|
2033
|
+
let inMultiline = false;
|
|
2034
|
+
let multilineKey = "";
|
|
2035
|
+
for (let i = 0; i < sectionBody.length; i++) {
|
|
2036
|
+
const line = sectionBody[i];
|
|
2037
|
+
if (inMultiline) {
|
|
2038
|
+
if (line.includes(MULTILINE_OPEN)) {
|
|
2039
|
+
inMultiline = false;
|
|
2040
|
+
if (updates[multilineKey] === null) continue;
|
|
2041
|
+
if (multilineKey in updates) continue;
|
|
2042
|
+
} else {
|
|
2043
|
+
if (updates[multilineKey] === null) continue;
|
|
2044
|
+
if (multilineKey in updates) continue;
|
|
2045
|
+
}
|
|
2046
|
+
remaining.push(line);
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
const eqIdx = line.indexOf("=");
|
|
2050
|
+
if (eqIdx > 0 && !line.trimStart().startsWith("#") && !line.trimStart().startsWith("[")) {
|
|
2051
|
+
const key = line.slice(0, eqIdx).trim();
|
|
2052
|
+
if (key in updates) {
|
|
2053
|
+
updatedKeys.add(key);
|
|
2054
|
+
const afterEquals = line.slice(eqIdx + 1).trim();
|
|
2055
|
+
if (afterEquals.includes(MULTILINE_OPEN)) {
|
|
2056
|
+
if ((afterEquals.match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1) {
|
|
2057
|
+
inMultiline = true;
|
|
2058
|
+
multilineKey = key;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
if (updates[key] === null) continue;
|
|
2062
|
+
remaining.push(`${key} = ${updates[key]}`);
|
|
2063
|
+
if (inMultiline) {
|
|
2064
|
+
for (let j = i + 1; j < sectionBody.length; j++) if (sectionBody[j].includes(MULTILINE_OPEN)) {
|
|
2065
|
+
i = j;
|
|
2066
|
+
inMultiline = false;
|
|
2067
|
+
break;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
remaining.push(line);
|
|
2074
|
+
}
|
|
2075
|
+
for (const [key, value] of Object.entries(updates)) if (value !== null && !updatedKeys.has(key)) remaining.push(`${key} = ${value}`);
|
|
2076
|
+
const result = [
|
|
2077
|
+
...before,
|
|
2078
|
+
...remaining,
|
|
2079
|
+
...after
|
|
2080
|
+
].join("\n");
|
|
2081
|
+
return Either.right(result);
|
|
2082
|
+
};
|
|
2083
|
+
/**
|
|
2084
|
+
* Append a new TOML section block to the end of the file.
|
|
2085
|
+
* Ensures proper spacing (double newline before the block).
|
|
2086
|
+
*/
|
|
2087
|
+
const appendSection = (raw, block) => `${raw.trimEnd()}\n\n${block}`;
|
|
2088
|
+
|
|
2042
2089
|
//#endregion
|
|
2043
2090
|
//#region src/cli/commands/env.ts
|
|
2044
2091
|
const printPostWriteGuidance = () => {
|
|
@@ -2131,6 +2178,148 @@ const runEnvExport = (options) => {
|
|
|
2131
2178
|
for (const [key, value] of Object.entries(boot.secrets)) console.log(`export ${key}='${shellEscape(value)}'`);
|
|
2132
2179
|
});
|
|
2133
2180
|
};
|
|
2181
|
+
const buildEnvBlock = (name, value, options) => {
|
|
2182
|
+
const lines = [`[env.${name}]`, `value = "${value}"`];
|
|
2183
|
+
if (options.purpose) lines.push(`purpose = "${options.purpose}"`);
|
|
2184
|
+
if (options.comment) lines.push(`comment = "${options.comment}"`);
|
|
2185
|
+
if (options.tags) {
|
|
2186
|
+
const pairs = options.tags.split(",").map((pair) => {
|
|
2187
|
+
const [k, v] = pair.split("=").map((s) => s.trim());
|
|
2188
|
+
return `${k} = "${v}"`;
|
|
2189
|
+
});
|
|
2190
|
+
lines.push(`tags = { ${pairs.join(", ")} }`);
|
|
2191
|
+
}
|
|
2192
|
+
return `${lines.join("\n")}\n`;
|
|
2193
|
+
};
|
|
2194
|
+
const withConfig$1 = (configFlag, fn) => {
|
|
2195
|
+
resolveConfigPath(configFlag).fold((err) => {
|
|
2196
|
+
console.error(formatError(err));
|
|
2197
|
+
process.exit(2);
|
|
2198
|
+
}, ({ path: configPath, source }) => {
|
|
2199
|
+
const sourceMsg = formatConfigSource(configPath, source);
|
|
2200
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
2201
|
+
fn(configPath, readFileSync(configPath, "utf-8"));
|
|
2202
|
+
});
|
|
2203
|
+
};
|
|
2204
|
+
const runEnvAdd = (name, value, options) => {
|
|
2205
|
+
resolveConfigPath(options.config).fold((err) => {
|
|
2206
|
+
console.error(formatError(err));
|
|
2207
|
+
process.exit(2);
|
|
2208
|
+
}, ({ path: configPath, source }) => {
|
|
2209
|
+
const sourceMsg = formatConfigSource(configPath, source);
|
|
2210
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
2211
|
+
loadConfig(configPath).fold((err) => {
|
|
2212
|
+
console.error(formatError(err));
|
|
2213
|
+
process.exit(2);
|
|
2214
|
+
}, (config) => {
|
|
2215
|
+
if (config.env?.[name]) {
|
|
2216
|
+
console.error(`${RED}Error:${RESET} Env entry "${name}" already exists in ${configPath}`);
|
|
2217
|
+
process.exit(1);
|
|
2218
|
+
}
|
|
2219
|
+
const block = buildEnvBlock(name, value, options);
|
|
2220
|
+
if (options.dryRun) {
|
|
2221
|
+
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
2222
|
+
console.log(block);
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
writeFileSync(configPath, appendSection(readFileSync(configPath, "utf-8"), block), "utf-8");
|
|
2226
|
+
console.log(`${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
|
|
2227
|
+
});
|
|
2228
|
+
});
|
|
2229
|
+
};
|
|
2230
|
+
const runEnvEdit = (name, options) => {
|
|
2231
|
+
withConfig$1(options.config, (configPath, raw) => {
|
|
2232
|
+
loadConfig(configPath).fold((err) => {
|
|
2233
|
+
console.error(formatError(err));
|
|
2234
|
+
process.exit(2);
|
|
2235
|
+
}, (config) => {
|
|
2236
|
+
if (!config.env?.[name]) {
|
|
2237
|
+
console.error(`${RED}Error:${RESET} Env entry "${name}" not found in ${configPath}`);
|
|
2238
|
+
process.exit(1);
|
|
2239
|
+
}
|
|
2240
|
+
const updates = {};
|
|
2241
|
+
if (options.value !== void 0) updates["value"] = `"${options.value}"`;
|
|
2242
|
+
if (options.purpose !== void 0) updates["purpose"] = `"${options.purpose}"`;
|
|
2243
|
+
if (options.comment !== void 0) updates["comment"] = `"${options.comment}"`;
|
|
2244
|
+
if (options.tags !== void 0) updates["tags"] = `{ ${options.tags.split(",").map((pair) => {
|
|
2245
|
+
const [k, v] = pair.split("=").map((s) => s.trim());
|
|
2246
|
+
return `${k} = "${v}"`;
|
|
2247
|
+
}).join(", ")} }`;
|
|
2248
|
+
if (Object.keys(updates).length === 0) {
|
|
2249
|
+
console.error(`${RED}Error:${RESET} No fields to update. Provide at least one --flag.`);
|
|
2250
|
+
process.exit(1);
|
|
2251
|
+
}
|
|
2252
|
+
updateSectionFields(raw, `[env.${name}]`, updates).fold((err) => {
|
|
2253
|
+
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
2254
|
+
process.exit(2);
|
|
2255
|
+
}, (updated) => {
|
|
2256
|
+
if (options.dryRun) {
|
|
2257
|
+
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
2258
|
+
console.log(updated);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
writeFileSync(configPath, updated, "utf-8");
|
|
2262
|
+
console.log(`${GREEN}✓${RESET} Updated ${BOLD}${name}${RESET} in ${CYAN}${configPath}${RESET}`);
|
|
2263
|
+
});
|
|
2264
|
+
});
|
|
2265
|
+
});
|
|
2266
|
+
};
|
|
2267
|
+
const runEnvRm = (name, options) => {
|
|
2268
|
+
withConfig$1(options.config, (configPath, raw) => {
|
|
2269
|
+
removeSection(raw, `[env.${name}]`).fold((err) => {
|
|
2270
|
+
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
2271
|
+
process.exit(1);
|
|
2272
|
+
}, (updated) => {
|
|
2273
|
+
if (options.dryRun) {
|
|
2274
|
+
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
2275
|
+
console.log(updated);
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
writeFileSync(configPath, updated, "utf-8");
|
|
2279
|
+
console.log(`${GREEN}✓${RESET} Removed ${BOLD}${name}${RESET} from ${CYAN}${configPath}${RESET}`);
|
|
2280
|
+
});
|
|
2281
|
+
});
|
|
2282
|
+
};
|
|
2283
|
+
const runEnvRename = (oldName, newName, options) => {
|
|
2284
|
+
withConfig$1(options.config, (configPath, raw) => {
|
|
2285
|
+
renameSection(raw, `[env.${oldName}]`, `[env.${newName}]`).fold((err) => {
|
|
2286
|
+
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
2287
|
+
process.exit(1);
|
|
2288
|
+
}, (updated) => {
|
|
2289
|
+
if (options.dryRun) {
|
|
2290
|
+
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
2291
|
+
console.log(updated);
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
writeFileSync(configPath, updated, "utf-8");
|
|
2295
|
+
console.log(`${GREEN}✓${RESET} Renamed ${BOLD}${oldName}${RESET} → ${BOLD}${newName}${RESET} in ${CYAN}${configPath}${RESET}`);
|
|
2296
|
+
});
|
|
2297
|
+
});
|
|
2298
|
+
};
|
|
2299
|
+
const registerEnvCommands = (program) => {
|
|
2300
|
+
const env = program.command("env").description("Manage environment defaults and discover credentials");
|
|
2301
|
+
env.command("scan").description("Auto-discover credentials from process.env and scaffold TOML entries — first step in the developer workflow").option("-c, --config <path>", "Path to envpkt.toml (write target for --write)").option("--format <format>", "Output format: table | json", "table").option("--write", "Write discovered credentials to envpkt.toml").option("--dry-run", "Preview TOML that would be written (implies --write)").option("--include-unknown", "Include vars where service could not be inferred").action((options) => {
|
|
2302
|
+
runEnvScan(options);
|
|
2303
|
+
});
|
|
2304
|
+
env.command("check").description("Bidirectional drift detection between envpkt.toml and live environment").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json", "table").option("--strict", "Exit non-zero on any drift").action((options) => {
|
|
2305
|
+
runEnvCheck(options);
|
|
2306
|
+
});
|
|
2307
|
+
env.command("export").description("Output export statements for eval-ing secrets into the current shell. Usage: eval \"$(envpkt env export)\"").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("--skip-audit", "Skip the pre-flight audit").action((options) => {
|
|
2308
|
+
runEnvExport(options);
|
|
2309
|
+
});
|
|
2310
|
+
env.command("add").description("Add a new environment default entry to envpkt.toml").argument("<name>", "Environment variable name").argument("<value>", "Default value").option("-c, --config <path>", "Path to envpkt.toml").option("--purpose <purpose>", "Why this env var exists").option("--comment <comment>", "Free-form annotation").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)").option("--dry-run", "Preview the TOML block without writing").action((name, value, options) => {
|
|
2311
|
+
runEnvAdd(name, value, options);
|
|
2312
|
+
});
|
|
2313
|
+
env.command("edit").description("Update fields on an existing env entry").argument("<name>", "Environment variable name to edit").option("-c, --config <path>", "Path to envpkt.toml").option("--value <value>", "New default value").option("--purpose <purpose>", "Why this env var exists").option("--comment <comment>", "Free-form annotation").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)").option("--dry-run", "Preview the changes without writing").action((name, options) => {
|
|
2314
|
+
runEnvEdit(name, options);
|
|
2315
|
+
});
|
|
2316
|
+
env.command("rm").description("Remove an env entry from envpkt.toml").argument("<name>", "Environment variable name to remove").option("-c, --config <path>", "Path to envpkt.toml").option("--dry-run", "Preview the result without writing").action((name, options) => {
|
|
2317
|
+
runEnvRm(name, options);
|
|
2318
|
+
});
|
|
2319
|
+
env.command("rename").description("Rename an env entry, preserving all fields").argument("<old>", "Current env variable name").argument("<new>", "New env variable name").option("-c, --config <path>", "Path to envpkt.toml").option("--dry-run", "Preview the result without writing").action((oldName, newName, options) => {
|
|
2320
|
+
runEnvRename(oldName, newName, options);
|
|
2321
|
+
});
|
|
2322
|
+
};
|
|
2134
2323
|
|
|
2135
2324
|
//#endregion
|
|
2136
2325
|
//#region src/cli/commands/exec.ts
|
|
@@ -3171,6 +3360,178 @@ const runSeal = async (options) => {
|
|
|
3171
3360
|
});
|
|
3172
3361
|
};
|
|
3173
3362
|
|
|
3363
|
+
//#endregion
|
|
3364
|
+
//#region src/cli/commands/secret.ts
|
|
3365
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
3366
|
+
const buildSecretBlock = (name, options) => {
|
|
3367
|
+
const lines = [`[secret.${name}]`];
|
|
3368
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3369
|
+
if (options.service) lines.push(`service = "${options.service}"`);
|
|
3370
|
+
if (options.purpose) lines.push(`purpose = "${options.purpose}"`);
|
|
3371
|
+
if (options.comment) lines.push(`comment = "${options.comment}"`);
|
|
3372
|
+
lines.push(`created = "${today}"`);
|
|
3373
|
+
if (options.expires) lines.push(`expires = "${options.expires}"`);
|
|
3374
|
+
if (options.rotates) lines.push(`rotates = "${options.rotates}"`);
|
|
3375
|
+
if (options.rateLimit) lines.push(`rate_limit = "${options.rateLimit}"`);
|
|
3376
|
+
if (options.modelHint) lines.push(`model_hint = "${options.modelHint}"`);
|
|
3377
|
+
if (options.source) lines.push(`source = "${options.source}"`);
|
|
3378
|
+
if (options.rotationUrl) lines.push(`rotation_url = "${options.rotationUrl}"`);
|
|
3379
|
+
if (options.required) lines.push(`required = true`);
|
|
3380
|
+
if (options.capabilities) {
|
|
3381
|
+
const caps = options.capabilities.split(",").map((c) => `"${c.trim()}"`).join(", ");
|
|
3382
|
+
lines.push(`capabilities = [${caps}]`);
|
|
3383
|
+
}
|
|
3384
|
+
if (options.tags) {
|
|
3385
|
+
const pairs = options.tags.split(",").map((pair) => {
|
|
3386
|
+
const [k, v] = pair.split("=").map((s) => s.trim());
|
|
3387
|
+
return `${k} = "${v}"`;
|
|
3388
|
+
});
|
|
3389
|
+
lines.push(`tags = { ${pairs.join(", ")} }`);
|
|
3390
|
+
}
|
|
3391
|
+
return `${lines.join("\n")}\n`;
|
|
3392
|
+
};
|
|
3393
|
+
const buildFieldUpdates = (options) => {
|
|
3394
|
+
const updates = {};
|
|
3395
|
+
if (options.service !== void 0) updates["service"] = `"${options.service}"`;
|
|
3396
|
+
if (options.purpose !== void 0) updates["purpose"] = `"${options.purpose}"`;
|
|
3397
|
+
if (options.comment !== void 0) updates["comment"] = `"${options.comment}"`;
|
|
3398
|
+
if (options.expires !== void 0) updates["expires"] = `"${options.expires}"`;
|
|
3399
|
+
if (options.rotates !== void 0) updates["rotates"] = `"${options.rotates}"`;
|
|
3400
|
+
if (options.rateLimit !== void 0) updates["rate_limit"] = `"${options.rateLimit}"`;
|
|
3401
|
+
if (options.modelHint !== void 0) updates["model_hint"] = `"${options.modelHint}"`;
|
|
3402
|
+
if (options.source !== void 0) updates["source"] = `"${options.source}"`;
|
|
3403
|
+
if (options.rotationUrl !== void 0) updates["rotation_url"] = `"${options.rotationUrl}"`;
|
|
3404
|
+
if (options.required !== void 0) updates["required"] = options.required ? "true" : "false";
|
|
3405
|
+
if (options.capabilities !== void 0) updates["capabilities"] = `[${options.capabilities.split(",").map((c) => `"${c.trim()}"`).join(", ")}]`;
|
|
3406
|
+
if (options.tags !== void 0) updates["tags"] = `{ ${options.tags.split(",").map((pair) => {
|
|
3407
|
+
const [k, v] = pair.split("=").map((s) => s.trim());
|
|
3408
|
+
return `${k} = "${v}"`;
|
|
3409
|
+
}).join(", ")} }`;
|
|
3410
|
+
return updates;
|
|
3411
|
+
};
|
|
3412
|
+
const withConfig = (configFlag, fn) => {
|
|
3413
|
+
resolveConfigPath(configFlag).fold((err) => {
|
|
3414
|
+
console.error(formatError(err));
|
|
3415
|
+
process.exit(2);
|
|
3416
|
+
}, ({ path: configPath, source }) => {
|
|
3417
|
+
const sourceMsg = formatConfigSource(configPath, source);
|
|
3418
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
3419
|
+
fn(configPath, readFileSync(configPath, "utf-8"));
|
|
3420
|
+
});
|
|
3421
|
+
};
|
|
3422
|
+
const runSecretAdd = (name, options) => {
|
|
3423
|
+
if (options.expires && !DATE_RE.test(options.expires)) {
|
|
3424
|
+
console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
|
|
3425
|
+
process.exit(1);
|
|
3426
|
+
}
|
|
3427
|
+
resolveConfigPath(options.config).fold((err) => {
|
|
3428
|
+
console.error(formatError(err));
|
|
3429
|
+
process.exit(2);
|
|
3430
|
+
}, ({ path: configPath, source }) => {
|
|
3431
|
+
const sourceMsg = formatConfigSource(configPath, source);
|
|
3432
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
3433
|
+
loadConfig(configPath).fold((err) => {
|
|
3434
|
+
console.error(formatError(err));
|
|
3435
|
+
process.exit(2);
|
|
3436
|
+
}, (config) => {
|
|
3437
|
+
if (config.secret?.[name]) {
|
|
3438
|
+
console.error(`${RED}Error:${RESET} Secret "${name}" already exists in ${configPath}`);
|
|
3439
|
+
process.exit(1);
|
|
3440
|
+
}
|
|
3441
|
+
const block = buildSecretBlock(name, options);
|
|
3442
|
+
if (options.dryRun) {
|
|
3443
|
+
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
3444
|
+
console.log(block);
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
writeFileSync(configPath, appendSection(readFileSync(configPath, "utf-8"), block), "utf-8");
|
|
3448
|
+
console.log(`${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
|
|
3449
|
+
});
|
|
3450
|
+
});
|
|
3451
|
+
};
|
|
3452
|
+
const runSecretEdit = (name, options) => {
|
|
3453
|
+
if (options.expires && !DATE_RE.test(options.expires)) {
|
|
3454
|
+
console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
|
|
3455
|
+
process.exit(1);
|
|
3456
|
+
}
|
|
3457
|
+
withConfig(options.config, (configPath, raw) => {
|
|
3458
|
+
loadConfig(configPath).fold((err) => {
|
|
3459
|
+
console.error(formatError(err));
|
|
3460
|
+
process.exit(2);
|
|
3461
|
+
}, (config) => {
|
|
3462
|
+
if (!config.secret?.[name]) {
|
|
3463
|
+
console.error(`${RED}Error:${RESET} Secret "${name}" not found in ${configPath}`);
|
|
3464
|
+
process.exit(1);
|
|
3465
|
+
}
|
|
3466
|
+
const updates = buildFieldUpdates(options);
|
|
3467
|
+
if (Object.keys(updates).length === 0) {
|
|
3468
|
+
console.error(`${RED}Error:${RESET} No fields to update. Provide at least one --flag.`);
|
|
3469
|
+
process.exit(1);
|
|
3470
|
+
}
|
|
3471
|
+
updateSectionFields(raw, `[secret.${name}]`, updates).fold((err) => {
|
|
3472
|
+
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
3473
|
+
process.exit(2);
|
|
3474
|
+
}, (updated) => {
|
|
3475
|
+
if (options.dryRun) {
|
|
3476
|
+
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
3477
|
+
console.log(updated);
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3480
|
+
writeFileSync(configPath, updated, "utf-8");
|
|
3481
|
+
console.log(`${GREEN}✓${RESET} Updated ${BOLD}${name}${RESET} in ${CYAN}${configPath}${RESET}`);
|
|
3482
|
+
});
|
|
3483
|
+
});
|
|
3484
|
+
});
|
|
3485
|
+
};
|
|
3486
|
+
const runSecretRm = (name, options) => {
|
|
3487
|
+
withConfig(options.config, (configPath, raw) => {
|
|
3488
|
+
removeSection(raw, `[secret.${name}]`).fold((err) => {
|
|
3489
|
+
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
3490
|
+
process.exit(1);
|
|
3491
|
+
}, (updated) => {
|
|
3492
|
+
if (options.dryRun) {
|
|
3493
|
+
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
3494
|
+
console.log(updated);
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
writeFileSync(configPath, updated, "utf-8");
|
|
3498
|
+
console.log(`${GREEN}✓${RESET} Removed ${BOLD}${name}${RESET} from ${CYAN}${configPath}${RESET}`);
|
|
3499
|
+
});
|
|
3500
|
+
});
|
|
3501
|
+
};
|
|
3502
|
+
const runSecretRename = (oldName, newName, options) => {
|
|
3503
|
+
withConfig(options.config, (configPath, raw) => {
|
|
3504
|
+
renameSection(raw, `[secret.${oldName}]`, `[secret.${newName}]`).fold((err) => {
|
|
3505
|
+
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
3506
|
+
process.exit(1);
|
|
3507
|
+
}, (updated) => {
|
|
3508
|
+
if (options.dryRun) {
|
|
3509
|
+
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
3510
|
+
console.log(updated);
|
|
3511
|
+
return;
|
|
3512
|
+
}
|
|
3513
|
+
writeFileSync(configPath, updated, "utf-8");
|
|
3514
|
+
console.log(`${GREEN}✓${RESET} Renamed ${BOLD}${oldName}${RESET} → ${BOLD}${newName}${RESET} in ${CYAN}${configPath}${RESET}`);
|
|
3515
|
+
});
|
|
3516
|
+
});
|
|
3517
|
+
};
|
|
3518
|
+
const addSecretFlags = (cmd) => cmd.option("--service <service>", "Service this secret authenticates to").option("--purpose <purpose>", "Why this secret exists").option("--comment <comment>", "Free-form annotation").option("--expires <date>", "Expiration date (YYYY-MM-DD)").option("--capabilities <caps>", "Comma-separated capabilities (e.g. read,write)").option("--rotates <schedule>", "Rotation schedule (e.g. 90d, quarterly)").option("--rate-limit <limit>", "Rate limit info (e.g. 1000/min)").option("--model-hint <hint>", "Suggested model or tier").option("--source <source>", "Where the value originates (e.g. vault, ci)").option("--rotation-url <url>", "URL for secret rotation procedure").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)");
|
|
3519
|
+
const registerSecretCommands = (program) => {
|
|
3520
|
+
const secret = program.command("secret").description("Manage secret entries in envpkt.toml");
|
|
3521
|
+
addSecretFlags(secret.command("add").description("Add a new secret entry to envpkt.toml").argument("<name>", "Secret name (becomes the env var key)").option("-c, --config <path>", "Path to envpkt.toml").option("--required", "Mark this secret as required").option("--dry-run", "Preview the TOML block without writing")).action((name, options) => {
|
|
3522
|
+
runSecretAdd(name, options);
|
|
3523
|
+
});
|
|
3524
|
+
addSecretFlags(secret.command("edit").description("Update metadata fields on an existing secret").argument("<name>", "Secret name to edit").option("-c, --config <path>", "Path to envpkt.toml").option("--required", "Mark this secret as required").option("--no-required", "Mark this secret as not required").option("--dry-run", "Preview the changes without writing")).action((name, options) => {
|
|
3525
|
+
runSecretEdit(name, options);
|
|
3526
|
+
});
|
|
3527
|
+
secret.command("rm").description("Remove a secret entry from envpkt.toml").argument("<name>", "Secret name to remove").option("-c, --config <path>", "Path to envpkt.toml").option("--dry-run", "Preview the result without writing").action((name, options) => {
|
|
3528
|
+
runSecretRm(name, options);
|
|
3529
|
+
});
|
|
3530
|
+
secret.command("rename").description("Rename a secret entry, preserving all metadata").argument("<old>", "Current secret name").argument("<new>", "New secret name").option("-c, --config <path>", "Path to envpkt.toml").option("--dry-run", "Preview the result without writing").action((oldName, newName, options) => {
|
|
3531
|
+
runSecretRename(oldName, newName, options);
|
|
3532
|
+
});
|
|
3533
|
+
};
|
|
3534
|
+
|
|
3174
3535
|
//#endregion
|
|
3175
3536
|
//#region src/cli/commands/shell-hook.ts
|
|
3176
3537
|
const ZSH_HOOK = `# envpkt shell hook — add to your .zshrc
|
|
@@ -3225,12 +3586,6 @@ program.name("envpkt").description("Credential lifecycle and fleet management fo
|
|
|
3225
3586
|
const pkgPath = findPkgJson(dirname(fileURLToPath(import.meta.url)));
|
|
3226
3587
|
return pkgPath ? JSON.parse(readFileSync(pkgPath, "utf-8")).version : "0.0.0";
|
|
3227
3588
|
})());
|
|
3228
|
-
program.command("add").description("Add a new secret entry to envpkt.toml").argument("<name>", "Secret name (becomes the env var key)").option("-c, --config <path>", "Path to envpkt.toml").option("--service <service>", "Service this secret authenticates to").option("--purpose <purpose>", "Why this secret exists").option("--comment <comment>", "Free-form annotation").option("--expires <date>", "Expiration date (YYYY-MM-DD)").option("--capabilities <caps>", "Comma-separated capabilities (e.g. read,write)").option("--rotates <schedule>", "Rotation schedule (e.g. 90d, quarterly)").option("--rate-limit <limit>", "Rate limit info (e.g. 1000/min)").option("--model-hint <hint>", "Suggested model or tier").option("--source <source>", "Where the value originates (e.g. vault, ci)").option("--required", "Mark this secret as required").option("--rotation-url <url>", "URL for secret rotation procedure").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)").option("--dry-run", "Preview the TOML block without writing").action((name, options) => {
|
|
3229
|
-
runAdd(name, options);
|
|
3230
|
-
});
|
|
3231
|
-
program.command("add-env").description("Add a new environment default entry to envpkt.toml").argument("<name>", "Environment variable name").argument("<value>", "Default value").option("-c, --config <path>", "Path to envpkt.toml").option("--purpose <purpose>", "Why this env var exists").option("--comment <comment>", "Free-form annotation").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)").option("--dry-run", "Preview the TOML block without writing").action((name, value, options) => {
|
|
3232
|
-
runAddEnv(name, value, options);
|
|
3233
|
-
});
|
|
3234
3589
|
program.command("init").description("Initialize a new envpkt.toml in the current directory").option("--from-fnox [path]", "Scaffold from fnox.toml (optionally specify path)").option("--catalog <path>", "Path to shared secret catalog").option("--identity", "Include [identity] section").option("--name <name>", "Identity name (requires --identity)").option("--capabilities <caps>", "Comma-separated capabilities (requires --identity)").option("--expires <date>", "Credential expiration YYYY-MM-DD (requires --identity)").option("--force", "Overwrite existing envpkt.toml").action((options) => {
|
|
3235
3590
|
runInit(process.cwd(), options);
|
|
3236
3591
|
});
|
|
@@ -3258,16 +3613,8 @@ program.command("seal").description("Encrypt secret values into envpkt.toml usin
|
|
|
3258
3613
|
program.command("mcp").description("Start the envpkt MCP server (stdio transport)").option("-c, --config <path>", "Path to envpkt.toml").action((options) => {
|
|
3259
3614
|
runMcp(options);
|
|
3260
3615
|
});
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
runEnvScan(options);
|
|
3264
|
-
});
|
|
3265
|
-
env.command("check").description("Bidirectional drift detection between envpkt.toml and live environment").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json", "table").option("--strict", "Exit non-zero on any drift").action((options) => {
|
|
3266
|
-
runEnvCheck(options);
|
|
3267
|
-
});
|
|
3268
|
-
env.command("export").description("Output export statements for eval-ing secrets into the current shell. Usage: eval \"$(envpkt env export)\"").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("--skip-audit", "Skip the pre-flight audit").action((options) => {
|
|
3269
|
-
runEnvExport(options);
|
|
3270
|
-
});
|
|
3616
|
+
registerSecretCommands(program);
|
|
3617
|
+
registerEnvCommands(program);
|
|
3271
3618
|
program.command("shell-hook").description("Output shell function for ambient credential warnings on cd — combine with env export for full setup").argument("<shell>", "Shell type: zsh | bash").action((shell) => {
|
|
3272
3619
|
runShellHook(shell);
|
|
3273
3620
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -307,6 +307,13 @@ type KeygenResult = {
|
|
|
307
307
|
readonly identityPath: string;
|
|
308
308
|
readonly configUpdated: boolean;
|
|
309
309
|
};
|
|
310
|
+
type TomlEditError = {
|
|
311
|
+
readonly _tag: "SectionNotFound";
|
|
312
|
+
readonly section: string;
|
|
313
|
+
} | {
|
|
314
|
+
readonly _tag: "SectionAlreadyExists";
|
|
315
|
+
readonly section: string;
|
|
316
|
+
};
|
|
310
317
|
//#endregion
|
|
311
318
|
//#region src/core/config.d.ts
|
|
312
319
|
/** Find envpkt.toml in the given directory */
|
|
@@ -458,6 +465,30 @@ declare const updateConfigRecipient: (configPath: string, recipient: string) =>
|
|
|
458
465
|
/** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
|
|
459
466
|
declare const resolveValues: (keys: ReadonlyArray<string>, profile?: string, agentKey?: string) => Promise<Record<string, string>>;
|
|
460
467
|
//#endregion
|
|
468
|
+
//#region src/core/toml-edit.d.ts
|
|
469
|
+
/**
|
|
470
|
+
* Remove a TOML section (e.g. `[secret.X]`) and all its fields through the next section or EOF.
|
|
471
|
+
* Strips trailing blank lines left behind.
|
|
472
|
+
*/
|
|
473
|
+
declare const removeSection: (raw: string, sectionHeader: string) => Either<TomlEditError, string>;
|
|
474
|
+
/**
|
|
475
|
+
* Rename a TOML section header (e.g. `[secret.OLD]` → `[secret.NEW]`).
|
|
476
|
+
* Errors if old doesn't exist or new already exists.
|
|
477
|
+
*/
|
|
478
|
+
declare const renameSection: (raw: string, oldHeader: string, newHeader: string) => Either<TomlEditError, string>;
|
|
479
|
+
/**
|
|
480
|
+
* Update, add, or remove fields within an existing TOML section.
|
|
481
|
+
* - A string value replaces or adds the field
|
|
482
|
+
* - A null value removes the field
|
|
483
|
+
* Does NOT re-serialize — operates on raw text lines.
|
|
484
|
+
*/
|
|
485
|
+
declare const updateSectionFields: (raw: string, sectionHeader: string, updates: Readonly<Record<string, string | null>>) => Either<TomlEditError, string>;
|
|
486
|
+
/**
|
|
487
|
+
* Append a new TOML section block to the end of the file.
|
|
488
|
+
* Ensures proper spacing (double newline before the block).
|
|
489
|
+
*/
|
|
490
|
+
declare const appendSection: (raw: string, block: string) => string;
|
|
491
|
+
//#endregion
|
|
461
492
|
//#region src/core/fleet.d.ts
|
|
462
493
|
declare const scanFleet: (rootDir: string, options?: {
|
|
463
494
|
maxDepth?: number;
|
|
@@ -515,4 +546,4 @@ type ToolDef = {
|
|
|
515
546
|
declare const toolDefinitions: readonly ToolDef[];
|
|
516
547
|
declare const callTool: (name: string, args: Record<string, unknown>) => CallToolResult;
|
|
517
548
|
//#endregion
|
|
518
|
-
export { type AgentIdentity, AgentIdentitySchema, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConfigSource, type ConsumerType, type CredentialPattern, type DriftEntry, type DriftStatus, type EnvAuditResult, type EnvDriftEntry, type EnvDriftStatus, type EnvMeta, EnvMetaSchema, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatPacketOptions, type HealthStatus, type Identity, type IdentityError, IdentitySchema, type KeygenError, type KeygenResult, type LifecycleConfig, LifecycleConfigSchema, type MatchResult, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, validateConfig };
|
|
549
|
+
export { type AgentIdentity, AgentIdentitySchema, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConfigSource, type ConsumerType, type CredentialPattern, type DriftEntry, type DriftStatus, type EnvAuditResult, type EnvDriftEntry, type EnvDriftStatus, type EnvMeta, EnvMetaSchema, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatPacketOptions, type HealthStatus, type Identity, type IdentityError, IdentitySchema, type KeygenError, type KeygenResult, type LifecycleConfig, LifecycleConfigSchema, type MatchResult, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type TomlEditError, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, updateSectionFields, validateConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FormatRegistry, Type } from "@sinclair/typebox";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
4
|
-
import { Cond, Left, List, None, Option, Right, Some, Try } from "functype";
|
|
4
|
+
import { Cond, Either, Left, List, None, Option, Right, Some, Try } from "functype";
|
|
5
5
|
import { Env, Fs, Path, Platform } from "functype-os";
|
|
6
6
|
import { TomlDate, parse } from "smol-toml";
|
|
7
7
|
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -1831,6 +1831,156 @@ const resolveValues = async (keys, profile, agentKey) => {
|
|
|
1831
1831
|
return result;
|
|
1832
1832
|
};
|
|
1833
1833
|
|
|
1834
|
+
//#endregion
|
|
1835
|
+
//#region src/core/toml-edit.ts
|
|
1836
|
+
const SECTION_RE = /^\[.+\]\s*$/;
|
|
1837
|
+
const MULTILINE_OPEN = "\"\"\"";
|
|
1838
|
+
/**
|
|
1839
|
+
* Find the line range [start, end) of a TOML section by its header string.
|
|
1840
|
+
* The range includes the header line through to (but not including) the next section header or EOF.
|
|
1841
|
+
* Handles multiline `"""..."""` values when scanning for section boundaries.
|
|
1842
|
+
*/
|
|
1843
|
+
const findSectionRange = (lines, sectionHeader) => {
|
|
1844
|
+
let start = -1;
|
|
1845
|
+
for (let i = 0; i < lines.length; i++) if (lines[i].trim() === sectionHeader) {
|
|
1846
|
+
start = i;
|
|
1847
|
+
break;
|
|
1848
|
+
}
|
|
1849
|
+
if (start === -1) return void 0;
|
|
1850
|
+
let end = lines.length;
|
|
1851
|
+
let inMultiline = false;
|
|
1852
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
1853
|
+
const line = lines[i];
|
|
1854
|
+
if (inMultiline) {
|
|
1855
|
+
if (line.includes(MULTILINE_OPEN)) inMultiline = false;
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
if (line.includes(MULTILINE_OPEN)) {
|
|
1859
|
+
if ((line.slice(line.indexOf("=") + 1).trim().match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1) inMultiline = true;
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
if (SECTION_RE.test(line)) {
|
|
1863
|
+
end = i;
|
|
1864
|
+
break;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return {
|
|
1868
|
+
start,
|
|
1869
|
+
end
|
|
1870
|
+
};
|
|
1871
|
+
};
|
|
1872
|
+
/** Check whether a section header exists in the raw TOML */
|
|
1873
|
+
const sectionExists = (lines, sectionHeader) => lines.some((l) => l.trim() === sectionHeader);
|
|
1874
|
+
/**
|
|
1875
|
+
* Remove a TOML section (e.g. `[secret.X]`) and all its fields through the next section or EOF.
|
|
1876
|
+
* Strips trailing blank lines left behind.
|
|
1877
|
+
*/
|
|
1878
|
+
const removeSection = (raw, sectionHeader) => {
|
|
1879
|
+
const lines = raw.split("\n");
|
|
1880
|
+
const range = findSectionRange(lines, sectionHeader);
|
|
1881
|
+
if (!range) return Either.left({
|
|
1882
|
+
_tag: "SectionNotFound",
|
|
1883
|
+
section: sectionHeader
|
|
1884
|
+
});
|
|
1885
|
+
let removeEnd = range.end;
|
|
1886
|
+
while (removeEnd > range.start && removeEnd - 1 >= range.start && lines[removeEnd - 1].trim() === "") removeEnd--;
|
|
1887
|
+
const before = lines.slice(0, range.start);
|
|
1888
|
+
const after = lines.slice(range.end);
|
|
1889
|
+
while (before.length > 0 && before[before.length - 1].trim() === "") before.pop();
|
|
1890
|
+
const result = [...before, ...after].join("\n");
|
|
1891
|
+
return Either.right(result);
|
|
1892
|
+
};
|
|
1893
|
+
/**
|
|
1894
|
+
* Rename a TOML section header (e.g. `[secret.OLD]` → `[secret.NEW]`).
|
|
1895
|
+
* Errors if old doesn't exist or new already exists.
|
|
1896
|
+
*/
|
|
1897
|
+
const renameSection = (raw, oldHeader, newHeader) => {
|
|
1898
|
+
const lines = raw.split("\n");
|
|
1899
|
+
if (!sectionExists(lines, oldHeader)) return Either.left({
|
|
1900
|
+
_tag: "SectionNotFound",
|
|
1901
|
+
section: oldHeader
|
|
1902
|
+
});
|
|
1903
|
+
if (sectionExists(lines, newHeader)) return Either.left({
|
|
1904
|
+
_tag: "SectionAlreadyExists",
|
|
1905
|
+
section: newHeader
|
|
1906
|
+
});
|
|
1907
|
+
const result = lines.map((line) => line.trim() === oldHeader ? newHeader : line).join("\n");
|
|
1908
|
+
return Either.right(result);
|
|
1909
|
+
};
|
|
1910
|
+
/**
|
|
1911
|
+
* Update, add, or remove fields within an existing TOML section.
|
|
1912
|
+
* - A string value replaces or adds the field
|
|
1913
|
+
* - A null value removes the field
|
|
1914
|
+
* Does NOT re-serialize — operates on raw text lines.
|
|
1915
|
+
*/
|
|
1916
|
+
const updateSectionFields = (raw, sectionHeader, updates) => {
|
|
1917
|
+
const lines = raw.split("\n");
|
|
1918
|
+
const range = findSectionRange(lines, sectionHeader);
|
|
1919
|
+
if (!range) return Either.left({
|
|
1920
|
+
_tag: "SectionNotFound",
|
|
1921
|
+
section: sectionHeader
|
|
1922
|
+
});
|
|
1923
|
+
const before = lines.slice(0, range.start + 1);
|
|
1924
|
+
const after = lines.slice(range.end);
|
|
1925
|
+
const sectionBody = lines.slice(range.start + 1, range.end);
|
|
1926
|
+
const remaining = [];
|
|
1927
|
+
const updatedKeys = /* @__PURE__ */ new Set();
|
|
1928
|
+
let inMultiline = false;
|
|
1929
|
+
let multilineKey = "";
|
|
1930
|
+
for (let i = 0; i < sectionBody.length; i++) {
|
|
1931
|
+
const line = sectionBody[i];
|
|
1932
|
+
if (inMultiline) {
|
|
1933
|
+
if (line.includes(MULTILINE_OPEN)) {
|
|
1934
|
+
inMultiline = false;
|
|
1935
|
+
if (updates[multilineKey] === null) continue;
|
|
1936
|
+
if (multilineKey in updates) continue;
|
|
1937
|
+
} else {
|
|
1938
|
+
if (updates[multilineKey] === null) continue;
|
|
1939
|
+
if (multilineKey in updates) continue;
|
|
1940
|
+
}
|
|
1941
|
+
remaining.push(line);
|
|
1942
|
+
continue;
|
|
1943
|
+
}
|
|
1944
|
+
const eqIdx = line.indexOf("=");
|
|
1945
|
+
if (eqIdx > 0 && !line.trimStart().startsWith("#") && !line.trimStart().startsWith("[")) {
|
|
1946
|
+
const key = line.slice(0, eqIdx).trim();
|
|
1947
|
+
if (key in updates) {
|
|
1948
|
+
updatedKeys.add(key);
|
|
1949
|
+
const afterEquals = line.slice(eqIdx + 1).trim();
|
|
1950
|
+
if (afterEquals.includes(MULTILINE_OPEN)) {
|
|
1951
|
+
if ((afterEquals.match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1) {
|
|
1952
|
+
inMultiline = true;
|
|
1953
|
+
multilineKey = key;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
if (updates[key] === null) continue;
|
|
1957
|
+
remaining.push(`${key} = ${updates[key]}`);
|
|
1958
|
+
if (inMultiline) {
|
|
1959
|
+
for (let j = i + 1; j < sectionBody.length; j++) if (sectionBody[j].includes(MULTILINE_OPEN)) {
|
|
1960
|
+
i = j;
|
|
1961
|
+
inMultiline = false;
|
|
1962
|
+
break;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
remaining.push(line);
|
|
1969
|
+
}
|
|
1970
|
+
for (const [key, value] of Object.entries(updates)) if (value !== null && !updatedKeys.has(key)) remaining.push(`${key} = ${value}`);
|
|
1971
|
+
const result = [
|
|
1972
|
+
...before,
|
|
1973
|
+
...remaining,
|
|
1974
|
+
...after
|
|
1975
|
+
].join("\n");
|
|
1976
|
+
return Either.right(result);
|
|
1977
|
+
};
|
|
1978
|
+
/**
|
|
1979
|
+
* Append a new TOML section block to the end of the file.
|
|
1980
|
+
* Ensures proper spacing (double newline before the block).
|
|
1981
|
+
*/
|
|
1982
|
+
const appendSection = (raw, block) => `${raw.trimEnd()}\n\n${block}`;
|
|
1983
|
+
|
|
1834
1984
|
//#endregion
|
|
1835
1985
|
//#region src/core/fleet.ts
|
|
1836
1986
|
const CONFIG_FILENAME = "envpkt.toml";
|
|
@@ -2217,4 +2367,4 @@ const startServer = async () => {
|
|
|
2217
2367
|
};
|
|
2218
2368
|
|
|
2219
2369
|
//#endregion
|
|
2220
|
-
export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, validateConfig };
|
|
2370
|
+
export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, updateSectionFields, validateConfig };
|