envpkt 0.6.1 → 0.6.3

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.
Files changed (2) hide show
  1. package/dist/cli.js +315 -180
  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, Left, List, Option, Right, Try } from "functype";
7
6
  import { TypeCompiler } from "@sinclair/typebox/compiler";
7
+ import { Cond, Left, List, Option, Right, Try } from "functype";
8
8
  import { Env, Fs, Path } from "functype-os";
9
9
  import { TomlDate, parse, stringify } from "smol-toml";
10
10
  import { FormatRegistry, Type } from "@sinclair/typebox";
@@ -15,109 +15,10 @@ 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
117
18
  //#region src/core/schema.ts
118
- const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
19
+ const DATE_RE$1 = /^\d{4}-\d{2}-\d{2}$/;
119
20
  const URI_RE = /^https?:\/\/.+/;
120
- if (!FormatRegistry.Has("date")) FormatRegistry.Set("date", (v) => DATE_RE.test(v));
21
+ if (!FormatRegistry.Has("date")) FormatRegistry.Set("date", (v) => DATE_RE$1.test(v));
121
22
  if (!FormatRegistry.Has("uri")) FormatRegistry.Set("uri", (v) => URI_RE.test(v));
122
23
  const ConsumerType = Type.Union([
123
24
  Type.Literal("agent"),
@@ -359,83 +260,6 @@ const resolveConfigPath = (flagPath, envVar, cwd) => {
359
260
  }));
360
261
  };
361
262
 
362
- //#endregion
363
- //#region src/core/catalog.ts
364
- /** Load and validate a catalog file, mapping ConfigError → CatalogError */
365
- const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
366
- if (err._tag === "FileNotFound") return Left({
367
- _tag: "CatalogNotFound",
368
- path: err.path
369
- });
370
- return Left({
371
- _tag: "CatalogLoadError",
372
- message: `${err._tag}: ${"message" in err ? err.message : String(err)}`
373
- });
374
- }, (config) => Right(config));
375
- /** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
376
- const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
377
- const resolved = {};
378
- for (const key of agentSecrets) {
379
- const catalogEntry = catalogMeta[key];
380
- if (!catalogEntry) return Left({
381
- _tag: "SecretNotInCatalog",
382
- key,
383
- catalogPath
384
- });
385
- const agentOverride = agentMeta[key];
386
- if (agentOverride) resolved[key] = {
387
- ...catalogEntry,
388
- ...agentOverride
389
- };
390
- else resolved[key] = catalogEntry;
391
- }
392
- return Right(resolved);
393
- };
394
- /** Resolve an agent config against its catalog (if any), producing a flat self-contained config */
395
- const resolveConfig = (agentConfig, agentConfigDir) => {
396
- if (!agentConfig.catalog) return Right({
397
- config: agentConfig,
398
- merged: [],
399
- overridden: [],
400
- warnings: []
401
- });
402
- if (!agentConfig.identity?.secrets || agentConfig.identity.secrets.length === 0) return Left({
403
- _tag: "MissingSecretsList",
404
- message: "Config has 'catalog' but identity.secrets is missing — declare which catalog secrets this agent needs"
405
- });
406
- const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
407
- const agentSecrets = agentConfig.identity.secrets;
408
- const agentSecretEntries = agentConfig.secret ?? {};
409
- return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
410
- const merged = [];
411
- const overridden = [];
412
- const warnings = [];
413
- for (const key of agentSecrets) {
414
- merged.push(key);
415
- if (agentSecretEntries[key]) overridden.push(key);
416
- }
417
- const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
418
- const identityData = agentConfig.identity ? (() => {
419
- const { secrets: _secrets, ...rest } = agentConfig.identity;
420
- return rest;
421
- })() : void 0;
422
- return {
423
- config: {
424
- ...agentWithoutCatalog,
425
- identity: identityData ? {
426
- ...identityData,
427
- name: identityData.name
428
- } : void 0,
429
- secret: resolvedMeta
430
- },
431
- catalogPath,
432
- merged,
433
- overridden,
434
- warnings
435
- };
436
- }));
437
- };
438
-
439
263
  //#endregion
440
264
  //#region src/cli/output.ts
441
265
  const RESET = "\x1B[0m";
@@ -643,6 +467,285 @@ const formatConfigSource = (path, source) => {
643
467
  return `${DIM}envpkt: loaded ${path}${RESET}`;
644
468
  };
645
469
 
470
+ //#endregion
471
+ //#region src/cli/commands/add.ts
472
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
473
+ const buildSecretBlock = (name, options) => {
474
+ const lines = [`[secret.${name}]`];
475
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
476
+ if (options.service) lines.push(`service = "${options.service}"`);
477
+ if (options.purpose) lines.push(`purpose = "${options.purpose}"`);
478
+ if (options.comment) lines.push(`comment = "${options.comment}"`);
479
+ lines.push(`created = "${today}"`);
480
+ if (options.expires) lines.push(`expires = "${options.expires}"`);
481
+ if (options.rotates) lines.push(`rotates = "${options.rotates}"`);
482
+ if (options.rateLimit) lines.push(`rate_limit = "${options.rateLimit}"`);
483
+ if (options.modelHint) lines.push(`model_hint = "${options.modelHint}"`);
484
+ if (options.source) lines.push(`source = "${options.source}"`);
485
+ if (options.rotationUrl) lines.push(`rotation_url = "${options.rotationUrl}"`);
486
+ if (options.required) lines.push(`required = true`);
487
+ if (options.capabilities) {
488
+ const caps = options.capabilities.split(",").map((c) => `"${c.trim()}"`).join(", ");
489
+ lines.push(`capabilities = [${caps}]`);
490
+ }
491
+ if (options.tags) {
492
+ const pairs = options.tags.split(",").map((pair) => {
493
+ const [k, v] = pair.split("=").map((s) => s.trim());
494
+ return `${k} = "${v}"`;
495
+ });
496
+ lines.push(`tags = { ${pairs.join(", ")} }`);
497
+ }
498
+ return `${lines.join("\n")}\n`;
499
+ };
500
+ const runAdd = (name, options) => {
501
+ if (options.expires && !DATE_RE.test(options.expires)) {
502
+ console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
503
+ process.exit(1);
504
+ }
505
+ resolveConfigPath(options.config).fold((err) => {
506
+ console.error(formatError(err));
507
+ process.exit(2);
508
+ }, ({ path: configPath, source }) => {
509
+ const sourceMsg = formatConfigSource(configPath, source);
510
+ if (sourceMsg) console.error(sourceMsg);
511
+ loadConfig(configPath).fold((err) => {
512
+ console.error(formatError(err));
513
+ process.exit(2);
514
+ }, (config) => {
515
+ if (config.secret?.[name]) {
516
+ console.error(`${RED}Error:${RESET} Secret "${name}" already exists in ${configPath}`);
517
+ process.exit(1);
518
+ }
519
+ const block = buildSecretBlock(name, options);
520
+ if (options.dryRun) {
521
+ console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
522
+ console.log(block);
523
+ return;
524
+ }
525
+ writeFileSync(configPath, `${readFileSync(configPath, "utf-8").trimEnd()}\n\n${block}`, "utf-8");
526
+ console.log(`${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
527
+ });
528
+ });
529
+ };
530
+
531
+ //#endregion
532
+ //#region src/cli/commands/add-env.ts
533
+ const buildEnvBlock = (name, value, options) => {
534
+ const lines = [`[env.${name}]`, `value = "${value}"`];
535
+ if (options.purpose) lines.push(`purpose = "${options.purpose}"`);
536
+ if (options.comment) lines.push(`comment = "${options.comment}"`);
537
+ if (options.tags) {
538
+ const pairs = options.tags.split(",").map((pair) => {
539
+ const [k, v] = pair.split("=").map((s) => s.trim());
540
+ return `${k} = "${v}"`;
541
+ });
542
+ lines.push(`tags = { ${pairs.join(", ")} }`);
543
+ }
544
+ return `${lines.join("\n")}\n`;
545
+ };
546
+ const runAddEnv = (name, value, options) => {
547
+ resolveConfigPath(options.config).fold((err) => {
548
+ console.error(formatError(err));
549
+ process.exit(2);
550
+ }, ({ path: configPath, source }) => {
551
+ const sourceMsg = formatConfigSource(configPath, source);
552
+ if (sourceMsg) console.error(sourceMsg);
553
+ loadConfig(configPath).fold((err) => {
554
+ console.error(formatError(err));
555
+ process.exit(2);
556
+ }, (config) => {
557
+ if (config.env?.[name]) {
558
+ console.error(`${RED}Error:${RESET} Env entry "${name}" already exists in ${configPath}`);
559
+ process.exit(1);
560
+ }
561
+ const block = buildEnvBlock(name, value, options);
562
+ if (options.dryRun) {
563
+ console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
564
+ console.log(block);
565
+ return;
566
+ }
567
+ writeFileSync(configPath, `${readFileSync(configPath, "utf-8").trimEnd()}\n\n${block}`, "utf-8");
568
+ console.log(`${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
569
+ });
570
+ });
571
+ };
572
+
573
+ //#endregion
574
+ //#region src/core/audit.ts
575
+ const MS_PER_DAY = 864e5;
576
+ const WARN_BEFORE_DAYS = 30;
577
+ const daysBetween = (from, to) => Math.floor((to.getTime() - from.getTime()) / MS_PER_DAY);
578
+ const parseDate = (dateStr) => {
579
+ const d = /* @__PURE__ */ new Date(`${dateStr}T00:00:00Z`);
580
+ return Number.isNaN(d.getTime()) ? Option(void 0) : Option(d);
581
+ };
582
+ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
583
+ const issues = [];
584
+ const created = Option(meta?.created).flatMap(parseDate);
585
+ const expires = Option(meta?.expires).flatMap(parseDate);
586
+ const rotationUrl = Option(meta?.rotation_url);
587
+ const purpose = Option(meta?.purpose);
588
+ const service = Option(meta?.service);
589
+ const daysRemaining = expires.map((exp) => daysBetween(today, exp));
590
+ const daysSinceCreated = created.map((c) => daysBetween(c, today));
591
+ const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
592
+ const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
593
+ const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
594
+ const hasSealed = !!meta?.encrypted_value;
595
+ const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
596
+ const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
597
+ if (isExpired) issues.push("Secret has expired");
598
+ if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
599
+ if (isStale) issues.push("Secret is stale (no rotation detected)");
600
+ if (isMissing) issues.push("Key not found in fnox");
601
+ if (isMissingMetadata) {
602
+ if (requireExpiration && expires.isNone()) issues.push("Missing required expiration date");
603
+ if (requireService && service.isNone()) issues.push("Missing required service");
604
+ }
605
+ return {
606
+ key,
607
+ service,
608
+ status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").else("healthy"),
609
+ days_remaining: daysRemaining,
610
+ rotation_url: rotationUrl,
611
+ purpose,
612
+ created: Option(meta?.created),
613
+ expires: Option(meta?.expires),
614
+ issues: List(issues)
615
+ };
616
+ };
617
+ const computeAudit = (config, fnoxKeys, today) => {
618
+ const now = today ?? /* @__PURE__ */ new Date();
619
+ const lifecycle = config.lifecycle ?? {};
620
+ const staleWarningDays = lifecycle.stale_warning_days ?? 90;
621
+ const requireExpiration = lifecycle.require_expiration ?? false;
622
+ const requireService = lifecycle.require_service ?? false;
623
+ const keys = fnoxKeys ?? /* @__PURE__ */ new Set();
624
+ const secretEntries = config.secret ?? {};
625
+ const metaKeys = new Set(Object.keys(secretEntries));
626
+ const secrets = List(Object.entries(secretEntries).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
627
+ const orphaned = keys.size > 0 ? [...metaKeys].filter((k) => !keys.has(k)).length : 0;
628
+ const total = secrets.size;
629
+ const expired = secrets.count((s) => s.status === "expired");
630
+ const missing = secrets.count((s) => s.status === "missing");
631
+ const missing_metadata = secrets.count((s) => s.status === "missing_metadata");
632
+ const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
633
+ const stale = secrets.count((s) => s.status === "stale");
634
+ const healthy = secrets.count((s) => s.status === "healthy");
635
+ return {
636
+ status: Cond.of().when(expired > 0 || missing > 0, "critical").elseWhen(expiring_soon > 0 || stale > 0 || missing_metadata > 0, "degraded").else("healthy"),
637
+ secrets,
638
+ total,
639
+ healthy,
640
+ expiring_soon,
641
+ expired,
642
+ stale,
643
+ missing,
644
+ missing_metadata,
645
+ orphaned,
646
+ identity: config.identity
647
+ };
648
+ };
649
+ const computeEnvAudit = (config, env = process.env) => {
650
+ const envEntries = config.env ?? {};
651
+ const entries = [];
652
+ for (const [key, entry] of Object.entries(envEntries)) {
653
+ const currentValue = env[key];
654
+ const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
655
+ entries.push({
656
+ key,
657
+ defaultValue: entry.value,
658
+ currentValue,
659
+ status,
660
+ purpose: entry.purpose
661
+ });
662
+ }
663
+ return {
664
+ entries,
665
+ total: entries.length,
666
+ defaults_applied: entries.filter((e) => e.status === "default").length,
667
+ overridden: entries.filter((e) => e.status === "overridden").length,
668
+ missing: entries.filter((e) => e.status === "missing").length
669
+ };
670
+ };
671
+
672
+ //#endregion
673
+ //#region src/core/catalog.ts
674
+ /** Load and validate a catalog file, mapping ConfigError → CatalogError */
675
+ const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
676
+ if (err._tag === "FileNotFound") return Left({
677
+ _tag: "CatalogNotFound",
678
+ path: err.path
679
+ });
680
+ return Left({
681
+ _tag: "CatalogLoadError",
682
+ message: `${err._tag}: ${"message" in err ? err.message : String(err)}`
683
+ });
684
+ }, (config) => Right(config));
685
+ /** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
686
+ const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
687
+ const resolved = {};
688
+ for (const key of agentSecrets) {
689
+ const catalogEntry = catalogMeta[key];
690
+ if (!catalogEntry) return Left({
691
+ _tag: "SecretNotInCatalog",
692
+ key,
693
+ catalogPath
694
+ });
695
+ const agentOverride = agentMeta[key];
696
+ if (agentOverride) resolved[key] = {
697
+ ...catalogEntry,
698
+ ...agentOverride
699
+ };
700
+ else resolved[key] = catalogEntry;
701
+ }
702
+ return Right(resolved);
703
+ };
704
+ /** Resolve an agent config against its catalog (if any), producing a flat self-contained config */
705
+ const resolveConfig = (agentConfig, agentConfigDir) => {
706
+ if (!agentConfig.catalog) return Right({
707
+ config: agentConfig,
708
+ merged: [],
709
+ overridden: [],
710
+ warnings: []
711
+ });
712
+ if (!agentConfig.identity?.secrets || agentConfig.identity.secrets.length === 0) return Left({
713
+ _tag: "MissingSecretsList",
714
+ message: "Config has 'catalog' but identity.secrets is missing — declare which catalog secrets this agent needs"
715
+ });
716
+ const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
717
+ const agentSecrets = agentConfig.identity.secrets;
718
+ const agentSecretEntries = agentConfig.secret ?? {};
719
+ return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
720
+ const merged = [];
721
+ const overridden = [];
722
+ const warnings = [];
723
+ for (const key of agentSecrets) {
724
+ merged.push(key);
725
+ if (agentSecretEntries[key]) overridden.push(key);
726
+ }
727
+ const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
728
+ const identityData = agentConfig.identity ? (() => {
729
+ const { secrets: _secrets, ...rest } = agentConfig.identity;
730
+ return rest;
731
+ })() : void 0;
732
+ return {
733
+ config: {
734
+ ...agentWithoutCatalog,
735
+ identity: identityData ? {
736
+ ...identityData,
737
+ name: identityData.name
738
+ } : void 0,
739
+ secret: resolvedMeta
740
+ },
741
+ catalogPath,
742
+ merged,
743
+ overridden,
744
+ warnings
745
+ };
746
+ }));
747
+ };
748
+
646
749
  //#endregion
647
750
  //#region src/cli/commands/audit.ts
648
751
  const runAudit = (options) => {
@@ -2842,6 +2945,7 @@ const writeSealedToml = (configPath, sealedMeta) => {
2842
2945
  output.push(`encrypted_value = """`);
2843
2946
  output.push(pendingSeals.get(currentMetaKey));
2844
2947
  output.push(`"""`);
2948
+ output.push("");
2845
2949
  pendingSeals.delete(currentMetaKey);
2846
2950
  }
2847
2951
  currentMetaKey = metaMatch[1];
@@ -2855,6 +2959,7 @@ const writeSealedToml = (configPath, sealedMeta) => {
2855
2959
  output.push(`encrypted_value = """`);
2856
2960
  output.push(pendingSeals.get(currentMetaKey));
2857
2961
  output.push(`"""`);
2962
+ output.push("");
2858
2963
  pendingSeals.delete(currentMetaKey);
2859
2964
  }
2860
2965
  insideMetaBlock = false;
@@ -2869,6 +2974,7 @@ const writeSealedToml = (configPath, sealedMeta) => {
2869
2974
  output.push(`encrypted_value = """`);
2870
2975
  output.push(pendingSeals.get(currentMetaKey));
2871
2976
  output.push(`"""`);
2977
+ output.push("");
2872
2978
  pendingSeals.delete(currentMetaKey);
2873
2979
  } else output.push(line);
2874
2980
  if (line.slice(line.indexOf("=") + 1).trim().includes("\"\"\"")) {
@@ -2947,7 +3053,30 @@ const runSeal = async (options) => {
2947
3053
  const metaKeys = targetKeys;
2948
3054
  console.log(`${BOLD}Sealing ${metaKeys.length} secret(s)${RESET} with recipient ${CYAN}${recipient.slice(0, 20)}...${RESET}`);
2949
3055
  console.log("");
2950
- const values = await resolveValues(metaKeys, options.profile, identityKey);
3056
+ const values = await (async () => {
3057
+ if (options.reseal && alreadySealed.length > 0) {
3058
+ const identityPath = config.identity?.key_file ? resolve(configDir, expandPath(config.identity.key_file)) : void 0;
3059
+ if (!identityPath) {
3060
+ console.error(`${RED}Error:${RESET} identity.key_file is required for --reseal (needed to decrypt existing secrets)`);
3061
+ console.error("");
3062
+ console.error(`${DIM}Add to your envpkt.toml:${RESET}`);
3063
+ console.error(`${DIM} [identity]${RESET}`);
3064
+ console.error(`${DIM} key_file = "path/to/identity.txt"${RESET}`);
3065
+ process.exit(2);
3066
+ }
3067
+ const decrypted = unsealSecrets(Object.fromEntries(alreadySealed.map((k) => [k, allSecretEntries[k]])), identityPath).fold((err) => {
3068
+ console.error(`${RED}Error:${RESET} Failed to decrypt existing secrets: ${err.message}`);
3069
+ process.exit(2);
3070
+ return {};
3071
+ }, (d) => d);
3072
+ const newValues = unsealed.length > 0 ? await resolveValues(unsealed, options.profile, identityKey) : {};
3073
+ return {
3074
+ ...decrypted,
3075
+ ...newValues
3076
+ };
3077
+ }
3078
+ return resolveValues(metaKeys, options.profile, identityKey);
3079
+ })();
2951
3080
  const resolved = Object.keys(values).length;
2952
3081
  const skipped = metaKeys.length - resolved;
2953
3082
  if (resolved === 0) {
@@ -3020,6 +3149,12 @@ program.name("envpkt").description("Credential lifecycle and fleet management fo
3020
3149
  const pkgPath = findPkgJson(dirname(fileURLToPath(import.meta.url)));
3021
3150
  return pkgPath ? JSON.parse(readFileSync(pkgPath, "utf-8")).version : "0.0.0";
3022
3151
  })());
3152
+ 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) => {
3153
+ runAdd(name, options);
3154
+ });
3155
+ 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) => {
3156
+ runAddEnv(name, value, options);
3157
+ });
3023
3158
  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) => {
3024
3159
  runInit(process.cwd(), options);
3025
3160
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envpkt",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Credential lifecycle and fleet management for AI agents",
5
5
  "keywords": [
6
6
  "credentials",