envpkt 0.7.3 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { Command } from "commander";
6
6
  import { Cond, Either, Left, List, Option, Right, Try } from "functype";
@@ -24,17 +24,17 @@ const parseDate = (dateStr) => {
24
24
  };
25
25
  const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
26
26
  const issues = [];
27
- const created = Option(meta?.created).flatMap(parseDate);
28
- const expires = Option(meta?.expires).flatMap(parseDate);
29
- const rotationUrl = Option(meta?.rotation_url);
30
- const purpose = Option(meta?.purpose);
31
- const service = Option(meta?.service);
27
+ const created = Option(meta.created).flatMap(parseDate);
28
+ const expires = Option(meta.expires).flatMap(parseDate);
29
+ const rotationUrl = Option(meta.rotation_url);
30
+ const purpose = Option(meta.purpose);
31
+ const service = Option(meta.service);
32
32
  const daysRemaining = expires.map((exp) => daysBetween(today, exp));
33
33
  const daysSinceCreated = created.map((c) => daysBetween(c, today));
34
34
  const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
35
35
  const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
36
36
  const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
37
- const hasSealed = !!meta?.encrypted_value;
37
+ const hasSealed = !!meta.encrypted_value;
38
38
  const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
39
39
  const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
40
40
  if (isExpired) issues.push("Secret has expired");
@@ -52,8 +52,8 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
52
52
  days_remaining: daysRemaining,
53
53
  rotation_url: rotationUrl,
54
54
  purpose,
55
- created: Option(meta?.created),
56
- expires: Option(meta?.expires),
55
+ created: Option(meta.created),
56
+ expires: Option(meta.expires),
57
57
  issues: List(issues)
58
58
  };
59
59
  };
@@ -91,18 +91,17 @@ const computeAudit = (config, fnoxKeys, today) => {
91
91
  };
92
92
  const computeEnvAudit = (config, env = process.env) => {
93
93
  const envEntries = config.env ?? {};
94
- const entries = [];
95
- for (const [key, entry] of Object.entries(envEntries)) {
94
+ const entries = Object.entries(envEntries).map(([key, entry]) => {
96
95
  const currentValue = env[key];
97
96
  const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
98
- entries.push({
97
+ return {
99
98
  key,
100
99
  defaultValue: entry.value,
101
100
  currentValue,
102
101
  status,
103
102
  purpose: entry.purpose
104
- });
105
- }
103
+ };
104
+ });
106
105
  return {
107
106
  entries,
108
107
  total: entries.length,
@@ -220,7 +219,7 @@ const normalizeDates = (obj) => {
220
219
  /** Expand ~ and $ENV_VAR / ${ENV_VAR} in a path string (silent — unresolved vars become "") */
221
220
  const expandPath = (p) => {
222
221
  return Path.expandTilde(p).replace(/\$\{(\w+)\}|\$(\w+)/g, (_, braced, bare) => {
223
- const name = braced ?? bare ?? "";
222
+ const name = Option(braced).fold(() => Option(bare).fold(() => "", (b) => b), (b) => b);
224
223
  return Env.getOrDefault(name, "");
225
224
  });
226
225
  };
@@ -255,10 +254,9 @@ const ENV_FALLBACK_PATHS = [
255
254
  ];
256
255
  /** Build discovery paths dynamically from Platform home and cloud storage detection */
257
256
  const buildSearchPaths = () => {
258
- const paths = [];
259
- for (const home of Platform.homeDirs().toArray()) paths.push(join(home, ".envpkt", CONFIG_FILENAME$2));
260
- for (const cloud of Platform.cloudStorageDirs().toArray()) paths.push(join(cloud.path, ".envpkt", CONFIG_FILENAME$2));
261
- return paths;
257
+ const homePaths = Platform.homeDirs().toArray().map((home) => join(home, ".envpkt", CONFIG_FILENAME$2));
258
+ const cloudPaths = Platform.cloudStorageDirs().toArray().map((cloud) => join(cloud.path, ".envpkt", CONFIG_FILENAME$2));
259
+ return [...homePaths, ...cloudPaths];
262
260
  };
263
261
  /** Discover config by checking CWD, then ENVPKT_SEARCH_PATH, then dynamic Platform paths */
264
262
  const discoverConfig = (cwd) => {
@@ -267,28 +265,24 @@ const discoverConfig = (cwd) => {
267
265
  path: cwdCandidate,
268
266
  source: "cwd"
269
267
  });
270
- const customPaths = Env.get("ENVPKT_SEARCH_PATH").fold(() => [], (v) => v.split(":").filter(Boolean));
271
- for (const template of customPaths) {
272
- const expanded = expandPath(template);
273
- if (!expanded || expanded.startsWith("/.envpkt")) continue;
274
- const matches = expandGlobPath(expanded);
275
- if (matches.length > 0) return Option({
276
- path: matches[0],
277
- source: "search"
278
- });
279
- }
280
- for (const p of buildSearchPaths()) if (Fs.existsSync(p)) return Option({
281
- path: p,
268
+ const customMatch = Env.get("ENVPKT_SEARCH_PATH").fold(() => [], (v) => v.split(":").filter(Boolean)).map((template) => ({
269
+ template,
270
+ expanded: expandPath(template)
271
+ })).filter(({ expanded }) => expanded !== "" && !expanded.startsWith("/.envpkt")).map(({ expanded }) => expandGlobPath(expanded)).find((matches) => matches.length > 0);
272
+ if (customMatch) return Option({
273
+ path: customMatch[0],
274
+ source: "search"
275
+ });
276
+ const platformMatch = buildSearchPaths().find((p) => Fs.existsSync(p));
277
+ if (platformMatch) return Option({
278
+ path: platformMatch,
279
+ source: "search"
280
+ });
281
+ const fallbackMatch = ENV_FALLBACK_PATHS.map((template) => expandPath(template)).filter((expanded) => expanded !== "" && !expanded.startsWith("/.envpkt")).find((expanded) => Fs.existsSync(expanded));
282
+ if (fallbackMatch) return Option({
283
+ path: fallbackMatch,
282
284
  source: "search"
283
285
  });
284
- for (const template of ENV_FALLBACK_PATHS) {
285
- const expanded = expandPath(template);
286
- if (!expanded || expanded.startsWith("/.envpkt")) continue;
287
- if (Fs.existsSync(expanded)) return Option({
288
- path: expanded,
289
- source: "search"
290
- });
291
- }
292
286
  return Option(void 0);
293
287
  };
294
288
  /** Read a config file, returning Either<ConfigError, string> */
@@ -380,22 +374,21 @@ const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
380
374
  }, (config) => Right(config));
381
375
  /** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
382
376
  const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
383
- const resolved = {};
384
- for (const key of agentSecrets) {
385
- const catalogEntry = catalogMeta[key];
386
- if (!catalogEntry) return Left({
377
+ return agentSecrets.reduce((acc, key) => acc.flatMap((resolved) => {
378
+ if (!(key in catalogMeta)) return Left({
387
379
  _tag: "SecretNotInCatalog",
388
380
  key,
389
381
  catalogPath
390
382
  });
391
- const agentOverride = agentMeta[key];
392
- if (agentOverride) resolved[key] = {
393
- ...catalogEntry,
394
- ...agentOverride
395
- };
396
- else resolved[key] = catalogEntry;
397
- }
398
- return Right(resolved);
383
+ const catalogEntry = catalogMeta[key];
384
+ return Right({
385
+ ...resolved,
386
+ [key]: key in agentMeta ? {
387
+ ...catalogEntry,
388
+ ...agentMeta[key]
389
+ } : catalogEntry
390
+ });
391
+ }), Right({}));
399
392
  };
400
393
  /** Resolve an agent config against its catalog (if any), producing a flat self-contained config */
401
394
  const resolveConfig = (agentConfig, agentConfigDir) => {
@@ -413,13 +406,9 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
413
406
  const agentSecrets = agentConfig.identity.secrets;
414
407
  const agentSecretEntries = agentConfig.secret ?? {};
415
408
  return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
416
- const merged = [];
417
- const overridden = [];
409
+ const merged = [...agentSecrets];
410
+ const overridden = agentSecrets.filter((key) => key in agentSecretEntries);
418
411
  const warnings = [];
419
- for (const key of agentSecrets) {
420
- merged.push(key);
421
- if (agentSecretEntries[key]) overridden.push(key);
422
- }
423
412
  const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
424
413
  const identityData = agentConfig.identity ? (() => {
425
414
  const { secrets: _secrets, ...rest } = agentConfig.identity;
@@ -449,6 +438,8 @@ const DIM = "\x1B[2m";
449
438
  const RED = "\x1B[31m";
450
439
  const GREEN = "\x1B[32m";
451
440
  const YELLOW = "\x1B[33m";
441
+ const BLUE = "\x1B[34m";
442
+ const MAGENTA = "\x1B[35m";
452
443
  const CYAN = "\x1B[36m";
453
444
  const statusColor = (status) => {
454
445
  switch (status) {
@@ -677,11 +668,11 @@ const formatEnvAuditTable = (config) => {
677
668
  return;
678
669
  }
679
670
  console.log(`\n${BOLD}Environment Defaults${RESET} (${envAudit.total} entries)`);
680
- for (const entry of envAudit.entries) {
671
+ envAudit.entries.forEach((entry) => {
681
672
  const statusIcon = entry.status === "default" ? `${GREEN}=${RESET}` : entry.status === "overridden" ? `${YELLOW}~${RESET}` : `${RED}!${RESET}`;
682
673
  const statusLabel = entry.status === "default" ? `${DIM}using default${RESET}` : entry.status === "overridden" ? `${YELLOW}overridden${RESET} (${entry.currentValue})` : `${RED}not set${RESET}`;
683
674
  console.log(` ${statusIcon} ${BOLD}${entry.key}${RESET} = "${entry.defaultValue}" ${statusLabel}`);
684
- }
675
+ });
685
676
  };
686
677
  const formatEnvAuditJson = (config) => {
687
678
  const envAudit = computeEnvAudit(config);
@@ -699,24 +690,24 @@ const runAuditOnConfig = (config, options) => {
699
690
  const secretEntries = config.secret ?? {};
700
691
  return {
701
692
  ...audit,
702
- secrets: audit.secrets.filter((s) => !!secretEntries[s.key]?.encrypted_value)
693
+ secrets: audit.secrets.filter((s) => !!secretEntries[s.key].encrypted_value)
703
694
  };
704
695
  })() : audit;
705
696
  const afterExternal = options.external ? (() => {
706
697
  const secretEntries = config.secret ?? {};
707
698
  return {
708
699
  ...afterSealed,
709
- secrets: afterSealed.secrets.filter((s) => !secretEntries[s.key]?.encrypted_value)
700
+ secrets: afterSealed.secrets.filter((s) => !secretEntries[s.key].encrypted_value)
710
701
  };
711
702
  })() : afterSealed;
712
703
  const afterStatus = options.status ? {
713
704
  ...afterExternal,
714
705
  secrets: afterExternal.secrets.filter((s) => s.status === options.status)
715
706
  } : afterExternal;
716
- const filtered = options.expiring !== void 0 ? {
707
+ const filtered = Option(options.expiring).fold(() => afterStatus, (expiring) => ({
717
708
  ...afterStatus,
718
- secrets: afterStatus.secrets.filter((s) => s.days_remaining.fold(() => false, (d) => d >= 0 && d <= options.expiring))
719
- } : afterStatus;
709
+ secrets: afterStatus.secrets.filter((s) => s.days_remaining.fold(() => false, (d) => d >= 0 && d <= expiring))
710
+ }));
720
711
  if (options.format === "json") console.log(formatAuditJson(filtered));
721
712
  else if (options.format === "minimal") console.log(formatAuditMinimal(filtered));
722
713
  else console.log(formatAudit(filtered));
@@ -747,13 +738,13 @@ const fnoxExport = (profile, agentKey) => {
747
738
  message: `fnox export failed: ${err}`
748
739
  }), (output) => {
749
740
  const entries = {};
750
- for (const line of output.split("\n")) {
741
+ output.split("\n").forEach((line) => {
751
742
  const eq = line.indexOf("=");
752
743
  if (eq > 0) {
753
744
  const key = line.slice(0, eq).trim();
754
745
  entries[key] = line.slice(eq + 1).trim();
755
746
  }
756
- }
747
+ });
757
748
  return Right(entries);
758
749
  });
759
750
  };
@@ -777,27 +768,38 @@ const ageAvailable = () => Try(() => {
777
768
  execFileSync("age", ["--version"], { stdio: "pipe" });
778
769
  return true;
779
770
  }).fold(() => false, (v) => v);
780
- /** Unwrap an encrypted agent key using age --decrypt */
771
+ /**
772
+ * Extract the secret key from an age identity file (plain or encrypted).
773
+ * - Plain identity files (from `age-keygen`) contain `AGE-SECRET-KEY-*` lines directly
774
+ * - Encrypted identity files need `age --decrypt` to unwrap
775
+ */
781
776
  const unwrapAgentKey = (identityPath) => {
782
777
  if (!existsSync(identityPath)) return Left({
783
778
  _tag: "IdentityNotFound",
784
779
  path: identityPath
785
780
  });
786
- if (!ageAvailable()) return Left({
787
- _tag: "AgeNotFound",
788
- message: "age CLI not found on PATH"
789
- });
790
- return Try(() => execFileSync("age", ["--decrypt", identityPath], {
791
- stdio: [
792
- "pipe",
793
- "pipe",
794
- "pipe"
795
- ],
796
- encoding: "utf-8"
797
- })).fold((err) => Left({
781
+ return Try(() => readFileSync(identityPath, "utf-8")).fold((err) => Left({
798
782
  _tag: "DecryptFailed",
799
- message: `age decrypt failed: ${err}`
800
- }), (output) => Right(output.trim()));
783
+ message: `Failed to read identity file: ${err}`
784
+ }), (content) => {
785
+ const secretKeyLine = content.split("\n").find((l) => l.startsWith("AGE-SECRET-KEY-"));
786
+ if (secretKeyLine) return Right(secretKeyLine.trim());
787
+ if (!ageAvailable()) return Left({
788
+ _tag: "AgeNotFound",
789
+ message: "age CLI not found on PATH"
790
+ });
791
+ return Try(() => execFileSync("age", ["--decrypt", identityPath], {
792
+ stdio: [
793
+ "pipe",
794
+ "pipe",
795
+ "pipe"
796
+ ],
797
+ encoding: "utf-8"
798
+ })).fold((err) => Left({
799
+ _tag: "DecryptFailed",
800
+ message: `age decrypt failed: ${err}`
801
+ }), (output) => Right(output.trim()));
802
+ });
801
803
  };
802
804
  //#endregion
803
805
  //#region src/fnox/parse.ts
@@ -823,14 +825,14 @@ const extractFnoxKeys = (config) => new Set(Object.keys(config.secrets));
823
825
  //#region src/core/keygen.ts
824
826
  /** Resolve the age identity file path: ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt */
825
827
  const resolveKeyPath = () => process.env["ENVPKT_AGE_KEY_FILE"] ?? join(homedir(), ".envpkt", "age-key.txt");
826
- /** Generate an age keypair and write to disk */
828
+ /** Generate an age keypair and write to disk. Refuses to overwrite if the file already exists. */
827
829
  const generateKeypair = (options) => {
828
830
  if (!ageAvailable()) return Left({
829
831
  _tag: "AgeNotFound",
830
832
  message: "age-keygen CLI not found on PATH. Install age: https://github.com/FiloSottile/age"
831
833
  });
832
834
  const outputPath = options?.outputPath ?? resolveKeyPath();
833
- if (existsSync(outputPath) && !options?.force) return Left({
835
+ if (existsSync(outputPath)) return Left({
834
836
  _tag: "KeyExists",
835
837
  path: outputPath
836
838
  });
@@ -872,46 +874,77 @@ const generateKeypair = (options) => {
872
874
  }));
873
875
  });
874
876
  };
875
- /** Update identity.recipient in an envpkt.toml file, preserving structure */
876
- const updateConfigRecipient = (configPath, recipient) => {
877
- return Try(() => readFileSync(configPath, "utf-8")).fold((err) => Left({
877
+ /** Update identity fields (recipient, key_file, name) in an envpkt.toml file, preserving structure */
878
+ const updateConfigIdentity = (configPath, options) => {
879
+ const readResult = Try(() => readFileSync(configPath, "utf-8"));
880
+ const fieldUpdaters = [
881
+ {
882
+ re: /^recipient\s*=/,
883
+ line: `recipient = "${options.recipient}"`
884
+ },
885
+ ...options.name ? [{
886
+ re: /^name\s*=/,
887
+ line: `name = "${options.name}"`
888
+ }] : [],
889
+ ...options.keyFile ? [{
890
+ re: /^key_file\s*=/,
891
+ line: `key_file = "${options.keyFile}"`
892
+ }] : []
893
+ ];
894
+ return readResult.fold((err) => Left({
878
895
  _tag: "ConfigUpdateError",
879
896
  message: `Failed to read config: ${err}`
880
897
  }), (raw) => {
881
898
  const lines = raw.split("\n");
882
- const output = [];
883
- let inIdentitySection = false;
884
- let recipientUpdated = false;
885
- let hasIdentitySection = false;
886
- for (const line of lines) {
887
- if (/^\[identity\]\s*$/.test(line)) {
888
- inIdentitySection = true;
889
- hasIdentitySection = true;
890
- output.push(line);
891
- continue;
892
- }
899
+ const updatedFields = /* @__PURE__ */ new Set();
900
+ const acc = lines.reduce((state, line) => {
901
+ if (/^\[identity\]\s*$/.test(line)) return {
902
+ ...state,
903
+ output: [...state.output, line],
904
+ inIdentitySection: true,
905
+ hasIdentitySection: true
906
+ };
893
907
  if (/^\[/.test(line) && !/^\[identity\]\s*$/.test(line)) {
894
- if (inIdentitySection && !recipientUpdated) {
895
- output.push(`recipient = "${recipient}"`);
896
- recipientUpdated = true;
897
- }
898
- inIdentitySection = false;
899
- output.push(line);
900
- continue;
908
+ const missing = state.inIdentitySection ? fieldUpdaters.filter((f) => !updatedFields.has(f.re.source)).map((f) => f.line) : [];
909
+ missing.forEach((l) => updatedFields.add(l));
910
+ return {
911
+ ...state,
912
+ output: [
913
+ ...state.output,
914
+ ...missing,
915
+ line
916
+ ],
917
+ inIdentitySection: false
918
+ };
901
919
  }
902
- if (inIdentitySection && /^recipient\s*=/.test(line)) {
903
- output.push(`recipient = "${recipient}"`);
904
- recipientUpdated = true;
905
- continue;
920
+ if (state.inIdentitySection) {
921
+ const match = fieldUpdaters.find((f) => f.re.test(line));
922
+ if (match) {
923
+ updatedFields.add(match.re.source);
924
+ return {
925
+ ...state,
926
+ output: [...state.output, match.line]
927
+ };
928
+ }
906
929
  }
907
- output.push(line);
908
- }
909
- if (inIdentitySection && !recipientUpdated) output.push(`recipient = "${recipient}"`);
910
- if (!hasIdentitySection) {
911
- output.push("");
912
- output.push("[identity]");
913
- output.push(`recipient = "${recipient}"`);
914
- }
930
+ return {
931
+ ...state,
932
+ output: [...state.output, line]
933
+ };
934
+ }, {
935
+ output: [],
936
+ inIdentitySection: false,
937
+ hasIdentitySection: false
938
+ });
939
+ const missingAtEof = acc.inIdentitySection ? fieldUpdaters.filter((f) => !updatedFields.has(f.re.source)).map((f) => f.line) : [];
940
+ const afterEof = [...acc.output, ...missingAtEof];
941
+ const identityLines = fieldUpdaters.map((f) => f.line);
942
+ const output = !acc.hasIdentitySection ? [
943
+ ...afterEof,
944
+ "",
945
+ "[identity]",
946
+ ...identityLines
947
+ ] : afterEof;
915
948
  return Try(() => writeFileSync(configPath, output.join("\n"))).fold((err) => Left({
916
949
  _tag: "ConfigUpdateError",
917
950
  message: `Failed to write config: ${err}`
@@ -975,27 +1008,23 @@ const sealSecrets = (meta, values, recipient) => {
975
1008
  _tag: "AgeNotFound",
976
1009
  message: "age CLI not found on PATH"
977
1010
  });
978
- const result = {};
979
- for (const [key, secretMeta] of Object.entries(meta)) {
980
- const plaintext = values[key];
981
- if (plaintext === void 0) {
982
- result[key] = secretMeta;
983
- continue;
984
- }
985
- const outcome = ageEncrypt(plaintext, recipient).fold((err) => Left({
1011
+ return Object.entries(meta).reduce((acc, [key, secretMeta]) => acc.flatMap((result) => {
1012
+ if (!(key in values)) return Right({
1013
+ ...result,
1014
+ [key]: secretMeta
1015
+ });
1016
+ return ageEncrypt(values[key], recipient).mapLeft((err) => ({
986
1017
  _tag: "EncryptFailed",
987
1018
  key,
988
1019
  message: err.message
989
- }), (ciphertext) => Right(ciphertext));
990
- const failed = outcome.fold((err) => err, () => void 0);
991
- if (failed) return Left(failed);
992
- const ciphertext = outcome.fold(() => "", (v) => v);
993
- result[key] = {
994
- ...secretMeta,
995
- encrypted_value: ciphertext
996
- };
997
- }
998
- return Right(result);
1020
+ })).map((ciphertext) => ({
1021
+ ...result,
1022
+ [key]: {
1023
+ ...secretMeta,
1024
+ encrypted_value: ciphertext
1025
+ }
1026
+ }));
1027
+ }), Right({}));
999
1028
  };
1000
1029
  /** Unseal secrets: decrypt encrypted_value for each meta entry that has one */
1001
1030
  const unsealSecrets = (meta, identityPath) => {
@@ -1003,19 +1032,14 @@ const unsealSecrets = (meta, identityPath) => {
1003
1032
  _tag: "AgeNotFound",
1004
1033
  message: "age CLI not found on PATH"
1005
1034
  });
1006
- const result = {};
1007
- for (const [key, secretMeta] of Object.entries(meta)) {
1008
- if (!secretMeta.encrypted_value) continue;
1009
- const outcome = ageDecrypt(secretMeta.encrypted_value, identityPath).fold((err) => Left({
1010
- _tag: "DecryptFailed",
1011
- key,
1012
- message: err.message
1013
- }), (plaintext) => Right(plaintext));
1014
- const failed = outcome.fold((err) => err, () => void 0);
1015
- if (failed) return Left(failed);
1016
- result[key] = outcome.fold(() => "", (v) => v);
1017
- }
1018
- return Right(result);
1035
+ return Object.entries(meta).filter(([, secretMeta]) => secretMeta.encrypted_value !== void 0 && secretMeta.encrypted_value !== "").reduce((acc, [key, secretMeta]) => acc.flatMap((result) => ageDecrypt(secretMeta.encrypted_value, identityPath).mapLeft((err) => ({
1036
+ _tag: "DecryptFailed",
1037
+ key,
1038
+ message: err.message
1039
+ })).map((plaintext) => ({
1040
+ ...result,
1041
+ [key]: plaintext
1042
+ }))), Right({}));
1019
1043
  };
1020
1044
  //#endregion
1021
1045
  //#region src/core/boot.ts
@@ -1030,15 +1054,13 @@ const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) =
1030
1054
  }));
1031
1055
  /** Resolve identity file path with explicit fallback control */
1032
1056
  const resolveIdentityFilePath = (config, configDir, useDefaultFallback) => {
1033
- if (config.identity?.key_file) return resolve(configDir, expandPath(config.identity.key_file));
1034
- if (!useDefaultFallback) return void 0;
1057
+ if (config.identity?.key_file) return Option(resolve(configDir, expandPath(config.identity.key_file)));
1058
+ if (!useDefaultFallback) return Option(void 0);
1035
1059
  const defaultPath = resolveKeyPath();
1036
- return existsSync(defaultPath) ? defaultPath : void 0;
1060
+ return existsSync(defaultPath) ? Option(defaultPath) : Option(void 0);
1037
1061
  };
1038
1062
  const resolveIdentityKey = (config, configDir) => {
1039
- const identityPath = resolveIdentityFilePath(config, configDir, false);
1040
- if (!identityPath) return Right(void 0);
1041
- return unwrapAgentKey(identityPath).fold((err) => Left(err), (key) => Right(key));
1063
+ return resolveIdentityFilePath(config, configDir, false).fold(() => Right(Option(void 0)), (path) => unwrapAgentKey(path).fold((err) => Left(err), (key) => Right(Option(key))));
1042
1064
  };
1043
1065
  const detectFnoxKeys = (configDir) => detectFnox(configDir).fold(() => /* @__PURE__ */ new Set(), (fnoxPath) => readFnoxConfig(fnoxPath).fold(() => /* @__PURE__ */ new Set(), (fnoxConfig) => extractFnoxKeys(fnoxConfig)));
1044
1066
  const checkExpiration = (audit, failOnExpired, warnOnly) => {
@@ -1066,10 +1088,8 @@ const looksLikeSecret = (value) => {
1066
1088
  return false;
1067
1089
  };
1068
1090
  const checkEnvMisclassification = (config) => {
1069
- const warnings = [];
1070
1091
  const envEntries = config.env ?? {};
1071
- for (const [key, entry] of Object.entries(envEntries)) if (looksLikeSecret(entry.value)) warnings.push(`[env.${key}] value looks like a secret — consider moving to [secret.${key}]`);
1072
- return warnings;
1092
+ return Object.entries(envEntries).filter(([, entry]) => looksLikeSecret(entry.value)).map(([key]) => `[env.${key}] value looks like a secret — consider moving to [secret.${key}]`);
1073
1093
  };
1074
1094
  /** Programmatic boot — returns Either<BootError, BootResult> */
1075
1095
  const bootSafe = (options) => {
@@ -1080,11 +1100,13 @@ const bootSafe = (options) => {
1080
1100
  return resolveAndLoad(opts).flatMap(({ config, configPath, configDir, configSource }) => {
1081
1101
  const secretEntries = config.secret ?? {};
1082
1102
  const metaKeys = Object.keys(secretEntries);
1083
- const hasSealedValues = metaKeys.some((k) => !!secretEntries[k]?.encrypted_value);
1103
+ const hasSealedValues = metaKeys.some((k) => !!secretEntries[k].encrypted_value);
1084
1104
  const identityKeyResult = resolveIdentityKey(config, configDir);
1085
- const identityKey = identityKeyResult.fold(() => void 0, (k) => k);
1086
- const identityKeyError = identityKeyResult.fold((err) => err, () => void 0);
1087
- if (identityKeyError && !hasSealedValues) return Left(identityKeyError);
1105
+ const identityKey = identityKeyResult.fold(() => Option(void 0), (k) => k);
1106
+ if (identityKeyResult.isLeft() && !hasSealedValues) return identityKeyResult.fold((err) => Left(err), () => Left({
1107
+ _tag: "ReadError",
1108
+ message: "unexpected"
1109
+ }));
1088
1110
  const audit = computeAudit(config, detectFnoxKeys(configDir));
1089
1111
  return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
1090
1112
  const secrets = {};
@@ -1092,40 +1114,47 @@ const bootSafe = (options) => {
1092
1114
  const skipped = [];
1093
1115
  warnings.push(...checkEnvMisclassification(config));
1094
1116
  const envEntries = config.env ?? {};
1095
- const envDefaults = {};
1096
- const overridden = [];
1097
- for (const [key, entry] of Object.entries(envEntries)) if (process.env[key] === void 0) {
1098
- envDefaults[key] = entry.value;
1099
- if (inject) process.env[key] = entry.value;
1100
- } else overridden.push(key);
1117
+ const envEntriesArr = Object.entries(envEntries);
1118
+ const envDefaults = Object.fromEntries(envEntriesArr.flatMap(([key, entry]) => Option(process.env[key]).fold(() => [[key, entry.value]], () => [])));
1119
+ const overridden = envEntriesArr.flatMap(([key]) => Option(process.env[key]).fold(() => [], () => [key]));
1120
+ if (inject) Object.entries(envDefaults).forEach(([key, value]) => {
1121
+ process.env[key] = value;
1122
+ });
1101
1123
  const sealedKeys = /* @__PURE__ */ new Set();
1102
1124
  const identityFilePath = resolveIdentityFilePath(config, configDir, true);
1103
- if (hasSealedValues && identityFilePath) unsealSecrets(secretEntries, identityFilePath).fold((err) => {
1104
- warnings.push(`Sealed value decryption failed: ${err.message}`);
1105
- }, (unsealed) => {
1106
- for (const [key, value] of Object.entries(unsealed)) {
1107
- secrets[key] = value;
1108
- injected.push(key);
1109
- sealedKeys.add(key);
1110
- }
1125
+ if (hasSealedValues) identityFilePath.fold(() => {
1126
+ warnings.push("Sealed values found but no identity file available for decryption");
1127
+ }, (idPath) => {
1128
+ unsealSecrets(secretEntries, idPath).fold((err) => {
1129
+ warnings.push(`Sealed value decryption failed: ${err.message}`);
1130
+ }, (unsealed) => {
1131
+ const unsealedEntries = Object.entries(unsealed);
1132
+ Object.assign(secrets, unsealed);
1133
+ injected.push(...unsealedEntries.map(([key]) => key));
1134
+ unsealedEntries.map(([key]) => key).forEach((key) => sealedKeys.add(key));
1135
+ });
1111
1136
  });
1112
- else if (hasSealedValues && !identityFilePath) warnings.push("Sealed values found but no identity file available for decryption");
1113
1137
  const remainingKeys = metaKeys.filter((k) => !sealedKeys.has(k));
1114
- if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey).fold((err) => {
1138
+ if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey.orUndefined()).fold((err) => {
1115
1139
  warnings.push(`fnox export failed: ${err.message}`);
1116
- for (const key of remainingKeys) skipped.push(key);
1140
+ skipped.push(...remainingKeys);
1117
1141
  }, (exported) => {
1118
- for (const key of remainingKeys) if (key in exported) {
1142
+ const found = remainingKeys.filter((key) => key in exported);
1143
+ const notFound = remainingKeys.filter((key) => !(key in exported));
1144
+ found.forEach((key) => {
1119
1145
  secrets[key] = exported[key];
1120
- injected.push(key);
1121
- } else skipped.push(key);
1146
+ });
1147
+ injected.push(...found);
1148
+ skipped.push(...notFound);
1122
1149
  });
1123
1150
  else {
1124
1151
  if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
1125
1152
  else warnings.push("fnox not available — unsealed secrets could not be resolved");
1126
- for (const key of remainingKeys) skipped.push(key);
1153
+ skipped.push(...remainingKeys);
1127
1154
  }
1128
- if (inject) for (const [key, value] of Object.entries(secrets)) process.env[key] = value;
1155
+ if (inject) Object.entries(secrets).forEach(([key, value]) => {
1156
+ process.env[key] = value;
1157
+ });
1129
1158
  return {
1130
1159
  audit,
1131
1160
  injected,
@@ -1763,11 +1792,10 @@ const VALUE_SHAPE_PATTERNS = [
1763
1792
  ];
1764
1793
  /** Detect service from value prefix/shape */
1765
1794
  const matchValueShape = (value) => {
1766
- for (const vp of VALUE_SHAPE_PATTERNS) if (value.startsWith(vp.prefix)) return Option({
1795
+ return Option(VALUE_SHAPE_PATTERNS.find((vp) => value.startsWith(vp.prefix))).map((vp) => ({
1767
1796
  service: vp.service,
1768
1797
  description: vp.description
1769
- });
1770
- return Option(void 0);
1798
+ }));
1771
1799
  };
1772
1800
  /** Strip common suffixes and derive a service name from an env var name */
1773
1801
  const deriveServiceFromName = (name) => {
@@ -1795,22 +1823,22 @@ const deriveServiceFromName = (name) => {
1795
1823
  /** Match a single env var against all patterns */
1796
1824
  const matchEnvVar = (name, value) => {
1797
1825
  if (EXCLUDED_VARS.has(name)) return Option(void 0);
1798
- for (const p of EXACT_NAME_PATTERNS) if (name === p.pattern) return Option({
1826
+ const exactMatch = EXACT_NAME_PATTERNS.find((p) => name === p.pattern);
1827
+ if (exactMatch) return Option({
1799
1828
  envVar: name,
1800
1829
  value,
1801
- service: Option(p.service),
1802
- confidence: p.confidence,
1803
- matchedBy: `exact:${p.pattern}`
1830
+ service: Option(exactMatch.service),
1831
+ confidence: exactMatch.confidence,
1832
+ matchedBy: `exact:${exactMatch.pattern}`
1804
1833
  });
1805
1834
  return matchValueShape(value).fold(() => {
1806
- for (const sp of SUFFIX_PATTERNS) if (name.endsWith(sp.suffix)) return Option({
1835
+ return Option(SUFFIX_PATTERNS.find((sp) => name.endsWith(sp.suffix))).map((sp) => ({
1807
1836
  envVar: name,
1808
1837
  value,
1809
1838
  service: Option(deriveServiceFromName(name)),
1810
1839
  confidence: "medium",
1811
1840
  matchedBy: `suffix:${sp.suffix}`
1812
- });
1813
- return Option(void 0);
1841
+ }));
1814
1842
  }, (vm) => Option({
1815
1843
  envVar: name,
1816
1844
  value,
@@ -1821,11 +1849,10 @@ const matchEnvVar = (name, value) => {
1821
1849
  };
1822
1850
  /** Scan full env, sorted by confidence (high first) then alphabetically */
1823
1851
  const scanEnv = (env) => {
1824
- const results = [];
1825
- for (const [name, value] of Object.entries(env)) {
1826
- if (value === void 0 || value === "") continue;
1827
- matchEnvVar(name, value).fold(() => {}, (m) => results.push(m));
1828
- }
1852
+ const results = Object.entries(env).flatMap(([name, value]) => {
1853
+ if (value === void 0 || value === "") return [];
1854
+ return matchEnvVar(name, value).fold(() => [], (m) => [m]);
1855
+ });
1829
1856
  const confidenceOrder = {
1830
1857
  high: 0,
1831
1858
  medium: 1,
@@ -1858,38 +1885,44 @@ const envScan = (env, options) => {
1858
1885
  };
1859
1886
  /** Bidirectional drift detection between config and live environment */
1860
1887
  const envCheck = (config, env) => {
1861
- const entries = [];
1862
1888
  const secretEntries = config.secret ?? {};
1863
1889
  const metaKeys = Object.keys(secretEntries);
1864
1890
  const trackedSet = new Set(metaKeys);
1865
- for (const key of metaKeys) {
1891
+ const secretDriftEntries = metaKeys.map((key) => {
1866
1892
  const meta = secretEntries[key];
1867
1893
  const present = env[key] !== void 0 && env[key] !== "";
1868
- entries.push({
1894
+ return {
1869
1895
  envVar: key,
1870
- service: Option(meta?.service),
1896
+ service: Option(meta.service),
1871
1897
  status: present ? "tracked" : "missing_from_env",
1872
1898
  confidence: Option(void 0)
1873
- });
1874
- }
1899
+ };
1900
+ });
1875
1901
  const envDefaults = config.env ?? {};
1876
- for (const key of Object.keys(envDefaults)) if (!trackedSet.has(key)) {
1902
+ const envDefaultEntries = Object.keys(envDefaults).filter((key) => {
1903
+ if (trackedSet.has(key)) return false;
1877
1904
  trackedSet.add(key);
1905
+ return true;
1906
+ }).map((key) => {
1878
1907
  const present = env[key] !== void 0 && env[key] !== "";
1879
- entries.push({
1908
+ return {
1880
1909
  envVar: key,
1881
1910
  service: Option(void 0),
1882
1911
  status: present ? "tracked" : "missing_from_env",
1883
1912
  confidence: Option(void 0)
1884
- });
1885
- }
1886
- const envMatches = scanEnv(env);
1887
- for (const match of envMatches) if (!trackedSet.has(match.envVar)) entries.push({
1913
+ };
1914
+ });
1915
+ const untrackedEntries = scanEnv(env).filter((match) => !trackedSet.has(match.envVar)).map((match) => ({
1888
1916
  envVar: match.envVar,
1889
1917
  service: match.service,
1890
1918
  status: "untracked",
1891
1919
  confidence: Option(match.confidence)
1892
- });
1920
+ }));
1921
+ const entries = [
1922
+ ...secretDriftEntries,
1923
+ ...envDefaultEntries,
1924
+ ...untrackedEntries
1925
+ ];
1893
1926
  const tracked_and_present = entries.filter((e) => e.status === "tracked").length;
1894
1927
  const missing_from_env = entries.filter((e) => e.status === "missing_from_env").length;
1895
1928
  const untracked_credentials = entries.filter((e) => e.status === "untracked").length;
@@ -1904,10 +1937,9 @@ const envCheck = (config, env) => {
1904
1937
  const todayIso$1 = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1905
1938
  /** Generate TOML [secret.*] blocks from scan results, mirroring init.ts pattern */
1906
1939
  const generateTomlFromScan = (matches) => {
1907
- const blocks = [];
1908
- for (const match of matches) {
1940
+ return matches.map((match) => {
1909
1941
  const svc = match.service.fold(() => match.envVar.toLowerCase().replace(/_/g, "-"), (s) => s);
1910
- blocks.push(`[secret.${match.envVar}]
1942
+ return `[secret.${match.envVar}]
1911
1943
  service = "${svc}"
1912
1944
  # purpose = "" # Why: what this secret enables
1913
1945
  # capabilities = [] # What operations this grants
@@ -1916,9 +1948,8 @@ created = "${todayIso$1()}"
1916
1948
  # rotation_url = "" # URL for rotation procedure
1917
1949
  # source = "" # Where the value originates (e.g. vault, ci)
1918
1950
  # tags = {}
1919
- `);
1920
- }
1921
- return blocks.join("\n");
1951
+ `;
1952
+ }).join("\n");
1922
1953
  };
1923
1954
  //#endregion
1924
1955
  //#region src/core/toml-edit.ts
@@ -1930,11 +1961,7 @@ const MULTILINE_OPEN = "\"\"\"";
1930
1961
  * Handles multiline `"""..."""` values when scanning for section boundaries.
1931
1962
  */
1932
1963
  const findSectionRange = (lines, sectionHeader) => {
1933
- let start = -1;
1934
- for (let i = 0; i < lines.length; i++) if (lines[i].trim() === sectionHeader) {
1935
- start = i;
1936
- break;
1937
- }
1964
+ const start = lines.findIndex((l) => l.trim() === sectionHeader);
1938
1965
  if (start === -1) return void 0;
1939
1966
  let end = lines.length;
1940
1967
  let inMultiline = false;
@@ -2056,7 +2083,8 @@ const updateSectionFields = (raw, sectionHeader, updates) => {
2056
2083
  }
2057
2084
  remaining.push(line);
2058
2085
  }
2059
- for (const [key, value] of Object.entries(updates)) if (value !== null && !updatedKeys.has(key)) remaining.push(`${key} = ${value}`);
2086
+ const newFields = Object.entries(updates).filter(([key, value]) => value !== null && !updatedKeys.has(key)).map(([key, value]) => `${key} = ${value}`);
2087
+ remaining.push(...newFields);
2060
2088
  const result = [
2061
2089
  ...before,
2062
2090
  ...remaining,
@@ -2156,13 +2184,21 @@ const runEnvExport = (options) => {
2156
2184
  }, (boot) => {
2157
2185
  const sourceMsg = formatConfigSource(boot.configPath, boot.configSource);
2158
2186
  if (sourceMsg) console.error(sourceMsg);
2159
- for (const warning of boot.warnings) console.error(`${YELLOW}Warning:${RESET} ${warning}`);
2160
- for (const [key, value] of Object.entries(boot.envDefaults)) console.log(`export ${key}='${shellEscape(value)}'`);
2187
+ boot.warnings.forEach((warning) => {
2188
+ console.error(`${YELLOW}Warning:${RESET} ${warning}`);
2189
+ });
2190
+ Object.entries(boot.envDefaults).forEach(([key, value]) => {
2191
+ console.log(`export ${key}='${shellEscape(value)}'`);
2192
+ });
2161
2193
  if (boot.overridden.length > 0) loadConfig(boot.configPath).fold(() => {}, (config) => {
2162
2194
  const envEntries = config.env ?? {};
2163
- for (const key of boot.overridden) if (key in envEntries) console.log(`export ${key}='${shellEscape(envEntries[key].value)}'`);
2195
+ boot.overridden.forEach((key) => {
2196
+ if (key in envEntries) console.log(`export ${key}='${shellEscape(envEntries[key].value)}'`);
2197
+ });
2198
+ });
2199
+ Object.entries(boot.secrets).forEach(([key, value]) => {
2200
+ console.log(`export ${key}='${shellEscape(value)}'`);
2164
2201
  });
2165
- for (const [key, value] of Object.entries(boot.secrets)) console.log(`export ${key}='${shellEscape(value)}'`);
2166
2202
  });
2167
2203
  };
2168
2204
  const buildEnvBlock = (name, value, options) => {
@@ -2179,7 +2215,7 @@ const buildEnvBlock = (name, value, options) => {
2179
2215
  return `${lines.join("\n")}\n`;
2180
2216
  };
2181
2217
  const withConfig$1 = (configFlag, fn) => {
2182
- resolveConfigPath(configFlag).fold((err) => {
2218
+ resolveConfigPath(configFlag.orUndefined()).fold((err) => {
2183
2219
  console.error(formatError(err));
2184
2220
  process.exit(2);
2185
2221
  }, ({ path: configPath, source }) => {
@@ -2215,7 +2251,7 @@ const runEnvAdd = (name, value, options) => {
2215
2251
  });
2216
2252
  };
2217
2253
  const runEnvEdit = (name, options) => {
2218
- withConfig$1(options.config, (configPath, raw) => {
2254
+ withConfig$1(Option(options.config), (configPath, raw) => {
2219
2255
  loadConfig(configPath).fold((err) => {
2220
2256
  console.error(formatError(err));
2221
2257
  process.exit(2);
@@ -2252,7 +2288,7 @@ const runEnvEdit = (name, options) => {
2252
2288
  });
2253
2289
  };
2254
2290
  const runEnvRm = (name, options) => {
2255
- withConfig$1(options.config, (configPath, raw) => {
2291
+ withConfig$1(Option(options.config), (configPath, raw) => {
2256
2292
  removeSection(raw, `[env.${name}]`).fold((err) => {
2257
2293
  console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
2258
2294
  process.exit(1);
@@ -2268,7 +2304,7 @@ const runEnvRm = (name, options) => {
2268
2304
  });
2269
2305
  };
2270
2306
  const runEnvRename = (oldName, newName, options) => {
2271
- withConfig$1(options.config, (configPath, raw) => {
2307
+ withConfig$1(Option(options.config), (configPath, raw) => {
2272
2308
  renameSection(raw, `[env.${oldName}]`, `[env.${newName}]`).fold((err) => {
2273
2309
  console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
2274
2310
  process.exit(1);
@@ -2345,20 +2381,24 @@ const runExec = (args, options) => {
2345
2381
  }
2346
2382
  if (boot.audit.status === "critical" && options.warnOnly) console.error(`${YELLOW}Warning:${RESET} Proceeding despite critical audit status (--warn-only)`);
2347
2383
  }
2348
- for (const warning of boot.warnings) console.error(`${YELLOW}Warning:${RESET} ${warning}`);
2384
+ boot.warnings.forEach((warning) => {
2385
+ console.error(`${YELLOW}Warning:${RESET} ${warning}`);
2386
+ });
2349
2387
  const env = { ...process.env };
2350
- for (const [key, value] of Object.entries(boot.envDefaults)) if (!(key in env)) env[key] = value;
2351
- for (const [key, value] of Object.entries(boot.secrets)) env[key] = value;
2388
+ Object.entries(boot.envDefaults).forEach(([key, value]) => {
2389
+ if (!(key in env)) env[key] = value;
2390
+ });
2391
+ Object.entries(boot.secrets).forEach(([key, value]) => {
2392
+ env[key] = value;
2393
+ });
2352
2394
  const [cmd, ...cmdArgs] = args;
2353
- try {
2354
- execFileSync(cmd, cmdArgs, {
2355
- env,
2356
- stdio: "inherit"
2357
- });
2358
- } catch (err) {
2395
+ Try(() => execFileSync(cmd, cmdArgs, {
2396
+ env,
2397
+ stdio: "inherit"
2398
+ })).fold((err) => {
2359
2399
  const exitCode = err.status ?? 1;
2360
2400
  process.exit(exitCode);
2361
- }
2401
+ }, () => {});
2362
2402
  };
2363
2403
  //#endregion
2364
2404
  //#region src/core/fleet.ts
@@ -2398,17 +2438,15 @@ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
2398
2438
  }
2399
2439
  const scanFleet = (rootDir, options) => {
2400
2440
  const maxDepth = options?.maxDepth ?? 3;
2401
- const agents = [];
2402
- for (const configPath of findEnvpktFiles(rootDir, maxDepth)) loadConfig(configPath).fold(() => {}, (config) => {
2441
+ const agentList = List(Array.from(findEnvpktFiles(rootDir, maxDepth)).flatMap((configPath) => loadConfig(configPath).fold(() => [], (config) => {
2403
2442
  const audit = computeAudit(config);
2404
- agents.push({
2443
+ return [{
2405
2444
  path: configPath,
2406
2445
  identity: config.identity,
2407
- min_expiry_days: audit.secrets.toArray().reduce((min, s) => s.days_remaining.fold(() => min, (d) => min === void 0 ? d : Math.min(min, d)), void 0),
2446
+ min_expiry_days: audit.secrets.toArray().reduce((min, s) => s.days_remaining.fold(() => min, (d) => min.fold(() => Option(d), (m) => Option(Math.min(m, d)))), Option(void 0)).orUndefined(),
2408
2447
  audit
2409
- });
2410
- });
2411
- const agentList = List(agents);
2448
+ }];
2449
+ })));
2412
2450
  const total_agents = agentList.size;
2413
2451
  const total_secrets = agentList.toArray().reduce((acc, a) => acc + a.audit.total, 0);
2414
2452
  const expired = agentList.toArray().reduce((acc, a) => acc + a.audit.expired, 0);
@@ -2440,17 +2478,16 @@ const runFleet = (options) => {
2440
2478
  process.exit(fleet.status === "critical" ? 2 : 0);
2441
2479
  return;
2442
2480
  }
2443
- const statusFilter = options.status;
2444
- const agents = statusFilter ? fleet.agents.filter((a) => a.audit.status === statusFilter) : fleet.agents;
2481
+ const agents = Option(options.status).fold(() => fleet.agents, (s) => fleet.agents.filter((a) => a.audit.status === s));
2445
2482
  console.log(`${statusIcon(fleet.status)} ${BOLD}Fleet: ${fleet.status.toUpperCase()}${RESET} — ${fleet.total_agents} agents, ${fleet.total_secrets} secrets`);
2446
2483
  if (fleet.expired > 0) console.log(` ${RED}${fleet.expired}${RESET} expired`);
2447
2484
  if (fleet.expiring_soon > 0) console.log(` ${YELLOW}${fleet.expiring_soon}${RESET} expiring soon`);
2448
2485
  console.log("");
2449
- for (const agent of agents) {
2486
+ agents.forEach((agent) => {
2450
2487
  const name = agent.identity?.name ? BOLD + agent.identity.name + RESET : DIM + agent.path + RESET;
2451
2488
  const icon = statusIcon(agent.audit.status);
2452
2489
  console.log(` ${icon} ${name} ${DIM}(${agent.audit.total} secrets)${RESET}`);
2453
- }
2490
+ });
2454
2491
  process.exit(fleet.status === "critical" ? 2 : 0);
2455
2492
  };
2456
2493
  //#endregion
@@ -2507,7 +2544,7 @@ const generateTemplate = (options, fnoxKeys) => {
2507
2544
  lines.push(``);
2508
2545
  if (fnoxKeys && fnoxKeys.length > 0) {
2509
2546
  lines.push(`# Secrets detected from fnox.toml`);
2510
- for (const key of fnoxKeys) lines.push(generateSecretBlock(key));
2547
+ lines.push(...fnoxKeys.map((key) => generateSecretBlock(key)));
2511
2548
  } else {
2512
2549
  lines.push(`# Add your secret metadata below.`);
2513
2550
  lines.push(`# Each [secret.<key>] describes a secret your agent needs.`);
@@ -2542,8 +2579,8 @@ const runInit = (dir, options) => {
2542
2579
  console.error(`${RED}Error:${RESET} ${CONFIG_FILENAME} already exists. Use --force to overwrite.`);
2543
2580
  process.exit(1);
2544
2581
  }
2545
- const fnoxKeys = options.fromFnox ? (() => {
2546
- const fnoxPath = options.fromFnox === "true" || options.fromFnox === "" ? join(dir, "fnox.toml") : options.fromFnox;
2582
+ const fnoxKeys = Option(options.fromFnox).map((fromFnox) => {
2583
+ const fnoxPath = fromFnox === "true" || fromFnox === "" ? join(dir, "fnox.toml") : fromFnox;
2547
2584
  if (!existsSync(fnoxPath)) {
2548
2585
  console.error(`${RED}Error:${RESET} fnox.toml not found at ${fnoxPath}`);
2549
2586
  process.exit(1);
@@ -2551,15 +2588,18 @@ const runInit = (dir, options) => {
2551
2588
  return readFnoxKeys(fnoxPath).fold((err) => {
2552
2589
  console.error(`${RED}Error:${RESET} Failed to read fnox.toml: ${formatConfigError(err)}`);
2553
2590
  process.exit(1);
2591
+ return [];
2554
2592
  }, (keys) => keys);
2555
- })() : void 0;
2556
- const content = generateTemplate(options, fnoxKeys);
2593
+ });
2594
+ const content = generateTemplate(options, fnoxKeys.orUndefined());
2557
2595
  Try(() => writeFileSync(outPath, content, "utf-8")).fold((err) => {
2558
2596
  console.error(`${RED}Error:${RESET} Failed to write ${CONFIG_FILENAME}: ${err}`);
2559
2597
  process.exit(1);
2560
2598
  }, () => {
2561
2599
  console.log(`${GREEN}✓${RESET} Created ${BOLD}${CONFIG_FILENAME}${RESET} in ${CYAN}${dir}${RESET}`);
2562
- if (fnoxKeys) console.log(` Scaffolded ${fnoxKeys.length} secret(s) from fnox.toml`);
2600
+ fnoxKeys.forEach((keys) => {
2601
+ console.log(` Scaffolded ${keys.length} secret(s) from fnox.toml`);
2602
+ });
2563
2603
  console.log(` ${BOLD}Next:${RESET} Fill in metadata for each secret`);
2564
2604
  });
2565
2605
  };
@@ -2572,75 +2612,76 @@ const maskValue = (value) => {
2572
2612
  //#endregion
2573
2613
  //#region src/cli/commands/inspect.ts
2574
2614
  const printSecretMeta = (meta, indent) => {
2575
- if (meta.purpose) console.log(`${indent}purpose: ${meta.purpose}`);
2576
- if (meta.comment) console.log(`${indent}comment: ${DIM}${meta.comment}${RESET}`);
2577
- if (meta.capabilities) console.log(`${indent}capabilities: ${DIM}${meta.capabilities.join(", ")}${RESET}`);
2615
+ if (meta.purpose) console.log(`${indent}${DIM}purpose:${RESET} ${meta.purpose}`);
2616
+ if (meta.comment) console.log(`${indent}${DIM}comment:${RESET} ${DIM}${meta.comment}${RESET}`);
2617
+ if (meta.capabilities) console.log(`${indent}${DIM}capabilities:${RESET} ${meta.capabilities.map((c) => `${MAGENTA}${c}${RESET}`).join(", ")}`);
2578
2618
  const dateParts = [];
2579
- if (meta.created) dateParts.push(`created: ${meta.created}`);
2580
- if (meta.expires) dateParts.push(`expires: ${meta.expires}`);
2619
+ if (meta.created) dateParts.push(`${DIM}created:${RESET} ${BLUE}${meta.created}${RESET}`);
2620
+ if (meta.expires) dateParts.push(`${DIM}expires:${RESET} ${YELLOW}${meta.expires}${RESET}`);
2581
2621
  if (dateParts.length > 0) console.log(`${indent}${dateParts.join(" ")}`);
2582
2622
  const opsParts = [];
2583
- if (meta.rotates) opsParts.push(`rotates: ${meta.rotates}`);
2584
- if (meta.rate_limit) opsParts.push(`rate_limit: ${meta.rate_limit}`);
2623
+ if (meta.rotates) opsParts.push(`${DIM}rotates:${RESET} ${CYAN}${meta.rotates}${RESET}`);
2624
+ if (meta.rate_limit) opsParts.push(`${DIM}rate_limit:${RESET} ${CYAN}${meta.rate_limit}${RESET}`);
2585
2625
  if (opsParts.length > 0) console.log(`${indent}${opsParts.join(" ")}`);
2586
- if (meta.source) console.log(`${indent}source: ${meta.source}`);
2587
- if (meta.model_hint) console.log(`${indent}model_hint: ${meta.model_hint}`);
2588
- if (meta.rotation_url) console.log(`${indent}rotation_url: ${DIM}${meta.rotation_url}${RESET}`);
2589
- if (meta.required !== void 0) console.log(`${indent}required: ${meta.required}`);
2626
+ if (meta.source) console.log(`${indent}${DIM}source:${RESET} ${BLUE}${meta.source}${RESET}`);
2627
+ if (meta.model_hint) console.log(`${indent}${DIM}model_hint:${RESET} ${MAGENTA}${meta.model_hint}${RESET}`);
2628
+ if (meta.rotation_url) console.log(`${indent}${DIM}rotation_url:${RESET} ${DIM}${meta.rotation_url}${RESET}`);
2629
+ if (meta.required !== void 0) console.log(`${indent}${DIM}required:${RESET} ${meta.required ? `${GREEN}true${RESET}` : `${DIM}false${RESET}`}`);
2590
2630
  if (meta.tags) {
2591
- const tagStr = Object.entries(meta.tags).map(([k, v]) => `${k}=${v}`).join(", ");
2592
- console.log(`${indent}tags: ${tagStr}`);
2631
+ const tagStr = Object.entries(meta.tags).map(([k, v]) => `${CYAN}${k}${RESET}=${DIM}${v}${RESET}`).join(", ");
2632
+ console.log(`${indent}${DIM}tags:${RESET} ${tagStr}`);
2593
2633
  }
2594
2634
  };
2595
2635
  const printConfig = (config, path, resolveResult, opts) => {
2596
2636
  console.log(`${BOLD}envpkt.toml${RESET} ${DIM}(${path})${RESET}`);
2597
2637
  if (resolveResult?.catalogPath) console.log(`${DIM}Catalog: ${CYAN}${resolveResult.catalogPath}${RESET}`);
2598
- console.log(`version: ${config.version}`);
2638
+ console.log(`${DIM}version:${RESET} ${config.version}`);
2599
2639
  console.log("");
2600
2640
  if (config.identity) {
2601
- console.log(`${BOLD}Identity:${RESET} ${config.identity.name}`);
2602
- if (config.identity.consumer) console.log(` consumer: ${config.identity.consumer}`);
2603
- if (config.identity.description) console.log(` description: ${config.identity.description}`);
2604
- if (config.identity.capabilities) console.log(` capabilities: ${config.identity.capabilities.join(", ")}`);
2605
- if (config.identity.expires) console.log(` expires: ${config.identity.expires}`);
2606
- if (config.identity.services) console.log(` services: ${config.identity.services.join(", ")}`);
2607
- if (config.identity.secrets) console.log(` secrets: ${config.identity.secrets.join(", ")}`);
2641
+ console.log(`${BOLD}Identity:${RESET} ${GREEN}${config.identity.name}${RESET}`);
2642
+ if (config.identity.consumer) console.log(` ${DIM}consumer:${RESET} ${MAGENTA}${config.identity.consumer}${RESET}`);
2643
+ if (config.identity.description) console.log(` ${DIM}description:${RESET} ${config.identity.description}`);
2644
+ if (config.identity.capabilities) console.log(` ${DIM}capabilities:${RESET} ${config.identity.capabilities.map((c) => `${MAGENTA}${c}${RESET}`).join(", ")}`);
2645
+ if (config.identity.expires) console.log(` ${DIM}expires:${RESET} ${YELLOW}${config.identity.expires}${RESET}`);
2646
+ if (config.identity.services) console.log(` ${DIM}services:${RESET} ${config.identity.services.map((s) => `${CYAN}${s}${RESET}`).join(", ")}`);
2647
+ if (config.identity.secrets) console.log(` ${DIM}secrets:${RESET} ${config.identity.secrets.map((s) => `${BOLD}${s}${RESET}`).join(", ")}`);
2608
2648
  console.log("");
2609
2649
  }
2610
2650
  const secretEntries = config.secret ?? {};
2611
2651
  console.log(`${BOLD}Secrets:${RESET} ${Object.keys(secretEntries).length}`);
2612
- for (const [key, meta] of Object.entries(secretEntries)) {
2613
- const secretValue = opts?.secrets?.[key];
2614
- const valueSuffix = secretValue !== void 0 ? ` = ${YELLOW}${(opts?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}${RESET}` : "";
2652
+ Object.entries(secretEntries).forEach(([key, meta]) => {
2653
+ const valueSuffix = Option(opts?.secrets?.[key]).fold(() => "", (secretValue) => ` = ${YELLOW}${(opts?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}${RESET}`);
2615
2654
  const sealedTag = meta.encrypted_value ? ` ${CYAN}[sealed]${RESET}` : "";
2616
2655
  console.log(` ${BOLD}${key}${RESET} → ${meta.service ?? key}${sealedTag}${valueSuffix}`);
2617
2656
  printSecretMeta(meta, " ");
2618
- }
2657
+ });
2619
2658
  const envEntries = config.env ?? {};
2620
2659
  const envKeys = Object.keys(envEntries);
2621
2660
  if (envKeys.length > 0) {
2622
2661
  console.log("");
2623
2662
  console.log(`${BOLD}Environment Defaults:${RESET} ${envKeys.length}`);
2624
- for (const [key, entry] of Object.entries(envEntries)) {
2625
- console.log(` ${BOLD}${key}${RESET} = "${entry.value}"`);
2626
- if (entry.purpose) console.log(` purpose: ${entry.purpose}`);
2627
- if (entry.comment) console.log(` comment: ${DIM}${entry.comment}${RESET}`);
2628
- }
2663
+ Object.entries(envEntries).forEach(([key, entry]) => {
2664
+ console.log(` ${BOLD}${key}${RESET} = ${GREEN}"${entry.value}"${RESET}`);
2665
+ if (entry.purpose) console.log(` ${DIM}purpose:${RESET} ${entry.purpose}`);
2666
+ if (entry.comment) console.log(` ${DIM}comment:${RESET} ${DIM}${entry.comment}${RESET}`);
2667
+ });
2629
2668
  }
2630
2669
  if (config.lifecycle) {
2631
2670
  console.log("");
2632
2671
  console.log(`${BOLD}Lifecycle:${RESET}`);
2633
- if (config.lifecycle.stale_warning_days !== void 0) console.log(` stale_warning_days: ${config.lifecycle.stale_warning_days}`);
2634
- if (config.lifecycle.require_expiration !== void 0) console.log(` require_expiration: ${config.lifecycle.require_expiration}`);
2635
- if (config.lifecycle.require_service !== void 0) console.log(` require_service: ${config.lifecycle.require_service}`);
2672
+ if (config.lifecycle.stale_warning_days !== void 0) console.log(` ${DIM}stale_warning_days:${RESET} ${YELLOW}${config.lifecycle.stale_warning_days}${RESET}`);
2673
+ if (config.lifecycle.require_expiration !== void 0) console.log(` ${DIM}require_expiration:${RESET} ${config.lifecycle.require_expiration ? `${GREEN}true${RESET}` : `${DIM}false${RESET}`}`);
2674
+ if (config.lifecycle.require_service !== void 0) console.log(` ${DIM}require_service:${RESET} ${config.lifecycle.require_service ? `${GREEN}true${RESET}` : `${DIM}false${RESET}`}`);
2636
2675
  }
2637
2676
  if (resolveResult?.catalogPath) {
2638
2677
  console.log("");
2639
2678
  console.log(`${BOLD}Catalog Resolution:${RESET}`);
2640
- console.log(` merged: ${resolveResult.merged.length} keys`);
2641
- if (resolveResult.overridden.length > 0) console.log(` overridden: ${resolveResult.overridden.join(", ")}`);
2642
- else console.log(` overridden: ${DIM}(none)${RESET}`);
2643
- for (const w of resolveResult.warnings) console.log(` ${YELLOW}warning:${RESET} ${w}`);
2679
+ console.log(` ${DIM}merged:${RESET} ${GREEN}${resolveResult.merged.length}${RESET} keys`);
2680
+ if (resolveResult.overridden.length > 0) console.log(` ${DIM}overridden:${RESET} ${resolveResult.overridden.map((k) => `${YELLOW}${k}${RESET}`).join(", ")}`);
2681
+ else console.log(` ${DIM}overridden: (none)${RESET}`);
2682
+ resolveResult.warnings.forEach((w) => {
2683
+ console.log(` ${YELLOW}warning:${RESET} ${w}`);
2684
+ });
2644
2685
  }
2645
2686
  };
2646
2687
  const runInspect = (options) => {
@@ -2690,15 +2731,33 @@ const runInspect = (options) => {
2690
2731
  };
2691
2732
  //#endregion
2692
2733
  //#region src/cli/commands/keygen.ts
2734
+ /** Shorten a path under $HOME to use ~ prefix */
2735
+ const tildeShorten = (p) => {
2736
+ const home = homedir();
2737
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
2738
+ };
2739
+ /** Derive a default identity name from the config path's parent directory */
2740
+ const deriveIdentityName = (configPath) => basename(dirname(resolve(configPath)));
2741
+ /**
2742
+ * Derive a project-specific key path from the config path.
2743
+ * - `envpkt.toml` → `~/.envpkt/<dir>-key.txt`
2744
+ * - `prod.envpkt.toml` → `~/.envpkt/<dir>-prod-key.txt`
2745
+ * - `foo.envpkt.toml` → `~/.envpkt/<dir>-foo-key.txt`
2746
+ */
2747
+ const deriveKeyPath = (configPath) => {
2748
+ const abs = resolve(configPath);
2749
+ const dir = basename(dirname(abs));
2750
+ const stem = basename(abs).replace(/\.envpkt\.toml$/, "").replace(/\.toml$/, "");
2751
+ const name = stem === "envpkt" || stem === "" ? dir : `${dir}-${stem}`;
2752
+ return join(homedir(), ".envpkt", `${name}-key.txt`);
2753
+ };
2693
2754
  const runKeygen = (options) => {
2694
- const outputPath = options.output ?? resolveKeyPath();
2695
- generateKeypair({
2696
- force: options.force,
2697
- outputPath
2698
- }).fold((err) => {
2755
+ const configPath = resolve(options.config ?? join(process.cwd(), "envpkt.toml"));
2756
+ generateKeypair({ outputPath: options.output ?? (options.global ? resolveKeyPath() : deriveKeyPath(configPath)) }).fold((err) => {
2699
2757
  if (err._tag === "KeyExists") {
2700
2758
  console.error(`${YELLOW}Warning:${RESET} Identity file already exists: ${CYAN}${err.path}${RESET}`);
2701
- console.error(`${DIM}Use --force to overwrite.${RESET}`);
2759
+ console.error(`${DIM}To replace it: remove the file first, then re-run keygen.${RESET}`);
2760
+ console.error(`${DIM}To use a different path: pass -o <path>.${RESET}`);
2702
2761
  process.exit(1);
2703
2762
  }
2704
2763
  console.error(formatError(err));
@@ -2707,16 +2766,24 @@ const runKeygen = (options) => {
2707
2766
  console.log(`${GREEN}Generated${RESET} age identity: ${CYAN}${identityPath}${RESET}`);
2708
2767
  console.log(`${BOLD}Recipient:${RESET} ${recipient}`);
2709
2768
  console.log("");
2710
- const configPath = resolve(options.config ?? join(process.cwd(), "envpkt.toml"));
2711
- if (existsSync(configPath)) updateConfigRecipient(configPath, recipient).fold((err) => {
2712
- console.error(`${YELLOW}Warning:${RESET} Could not update config: ${"message" in err ? err.message : err._tag}`);
2713
- console.log(`${DIM}Manually add to your envpkt.toml:${RESET}`);
2714
- console.log(` [identity]`);
2715
- console.log(` recipient = "${recipient}"`);
2716
- }, () => {
2717
- console.log(`${GREEN}Updated${RESET} ${CYAN}${configPath}${RESET} with identity.recipient`);
2718
- });
2719
- else {
2769
+ if (existsSync(configPath)) {
2770
+ const name = deriveIdentityName(configPath);
2771
+ const keyFile = tildeShorten(identityPath);
2772
+ updateConfigIdentity(configPath, {
2773
+ recipient,
2774
+ name,
2775
+ keyFile
2776
+ }).fold((err) => {
2777
+ console.error(`${YELLOW}Warning:${RESET} Could not update config: ${"message" in err ? err.message : err._tag}`);
2778
+ console.log(`${DIM}Manually add to your envpkt.toml:${RESET}`);
2779
+ console.log(` [identity]`);
2780
+ console.log(` name = "${name}"`);
2781
+ console.log(` recipient = "${recipient}"`);
2782
+ console.log(` key_file = "${keyFile}"`);
2783
+ }, () => {
2784
+ console.log(`${GREEN}Updated${RESET} ${CYAN}${configPath}${RESET} with identity (name, recipient, key_file)`);
2785
+ });
2786
+ } else {
2720
2787
  console.log(`${BOLD}Next steps:${RESET}`);
2721
2788
  console.log(` ${DIM}1.${RESET} envpkt init ${DIM}# create envpkt.toml${RESET}`);
2722
2789
  console.log(` ${DIM}2.${RESET} envpkt env scan --write ${DIM}# discover credentials${RESET}`);
@@ -2727,7 +2794,7 @@ const runKeygen = (options) => {
2727
2794
  //#endregion
2728
2795
  //#region src/mcp/resources.ts
2729
2796
  const loadConfigSafe = () => {
2730
- return resolveConfigPath().fold(() => void 0, ({ path }) => loadConfig(path).fold(() => void 0, (config) => ({
2797
+ return resolveConfigPath().fold(() => Option.none(), ({ path }) => loadConfig(path).fold(() => Option.none(), (config) => Option({
2731
2798
  config,
2732
2799
  path
2733
2800
  })));
@@ -2743,14 +2810,11 @@ const resourceDefinitions = [{
2743
2810
  description: "Capabilities declared by the agent and per-secret capability grants",
2744
2811
  mimeType: "application/json"
2745
2812
  }];
2746
- const readHealth = () => {
2747
- const loaded = loadConfigSafe();
2748
- if (!loaded) return { contents: [{
2749
- uri: "envpkt://health",
2750
- mimeType: "application/json",
2751
- text: JSON.stringify({ error: "No envpkt.toml found" })
2752
- }] };
2753
- const { config, path } = loaded;
2813
+ const readHealth = () => loadConfigSafe().fold(() => ({ contents: [{
2814
+ uri: "envpkt://health",
2815
+ mimeType: "application/json",
2816
+ text: JSON.stringify({ error: "No envpkt.toml found" })
2817
+ }] }), ({ config, path }) => {
2754
2818
  const audit = computeAudit(config);
2755
2819
  return { contents: [{
2756
2820
  uri: "envpkt://health",
@@ -2766,19 +2830,17 @@ const readHealth = () => {
2766
2830
  missing: audit.missing
2767
2831
  }, null, 2)
2768
2832
  }] };
2769
- };
2770
- const readCapabilities = () => {
2771
- const loaded = loadConfigSafe();
2772
- if (!loaded) return { contents: [{
2773
- uri: "envpkt://capabilities",
2774
- mimeType: "application/json",
2775
- text: JSON.stringify({ error: "No envpkt.toml found" })
2776
- }] };
2777
- const { config } = loaded;
2833
+ });
2834
+ const readCapabilities = () => loadConfigSafe().fold(() => ({ contents: [{
2835
+ uri: "envpkt://capabilities",
2836
+ mimeType: "application/json",
2837
+ text: JSON.stringify({ error: "No envpkt.toml found" })
2838
+ }] }), ({ config }) => {
2778
2839
  const agentCapabilities = config.identity?.capabilities ?? [];
2779
2840
  const secretCapabilities = {};
2780
- const secretEntries = config.secret ?? {};
2781
- for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
2841
+ Object.entries(config.secret ?? {}).forEach(([key, meta]) => {
2842
+ if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
2843
+ });
2782
2844
  return { contents: [{
2783
2845
  uri: "envpkt://capabilities",
2784
2846
  mimeType: "application/json",
@@ -2792,15 +2854,12 @@ const readCapabilities = () => {
2792
2854
  secrets: secretCapabilities
2793
2855
  }, null, 2)
2794
2856
  }] };
2795
- };
2857
+ });
2796
2858
  const resourceHandlers = {
2797
2859
  "envpkt://health": readHealth,
2798
2860
  "envpkt://capabilities": readCapabilities
2799
2861
  };
2800
- const readResource = (uri) => {
2801
- const handler = resourceHandlers[uri];
2802
- return handler?.();
2803
- };
2862
+ const readResource = (uri) => Option(resourceHandlers[uri]).map((handler) => handler());
2804
2863
  //#endregion
2805
2864
  //#region src/mcp/tools.ts
2806
2865
  const textResult = (text) => ({ content: [{
@@ -2815,7 +2874,7 @@ const errorResult = (message) => ({
2815
2874
  isError: true
2816
2875
  });
2817
2876
  const loadConfigForTool = (configPath) => {
2818
- return resolveConfigPath(configPath).fold((err) => ({
2877
+ return resolveConfigPath(configPath.fold(() => void 0, (v) => v)).fold((err) => ({
2819
2878
  ok: false,
2820
2879
  result: errorResult(`Config error: ${err._tag} — ${err._tag === "FileNotFound" ? err.path : ""}`)
2821
2880
  }), ({ path }) => loadConfig(path).fold((err) => ({
@@ -2898,8 +2957,9 @@ const toolDefinitions = [
2898
2957
  }
2899
2958
  }
2900
2959
  ];
2960
+ const configPathArg = (args) => Option(typeof args.configPath === "string" ? args.configPath : null);
2901
2961
  const handleGetPacketHealth = (args) => {
2902
- const loaded = loadConfigForTool(args.configPath);
2962
+ const loaded = loadConfigForTool(configPathArg(args));
2903
2963
  if (!loaded.ok) return loaded.result;
2904
2964
  const { config, path } = loaded;
2905
2965
  const audit = computeAudit(config);
@@ -2924,13 +2984,14 @@ const handleGetPacketHealth = (args) => {
2924
2984
  }, null, 2));
2925
2985
  };
2926
2986
  const handleListCapabilities = (args) => {
2927
- const loaded = loadConfigForTool(args.configPath);
2987
+ const loaded = loadConfigForTool(configPathArg(args));
2928
2988
  if (!loaded.ok) return loaded.result;
2929
2989
  const { config } = loaded;
2930
2990
  const agentCapabilities = config.identity?.capabilities ?? [];
2931
2991
  const secretCapabilities = {};
2932
- const secretEntries = config.secret ?? {};
2933
- for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
2992
+ Object.entries(config.secret ?? {}).forEach(([key, meta]) => {
2993
+ if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
2994
+ });
2934
2995
  return textResult(JSON.stringify({
2935
2996
  identity: config.identity ? {
2936
2997
  name: config.identity.name,
@@ -2945,21 +3006,21 @@ const handleListCapabilities = (args) => {
2945
3006
  const handleGetSecretMeta = (args) => {
2946
3007
  const key = args.key;
2947
3008
  if (!key) return errorResult("Missing required argument: key");
2948
- const loaded = loadConfigForTool(args.configPath);
3009
+ const loaded = loadConfigForTool(configPathArg(args));
2949
3010
  if (!loaded.ok) return loaded.result;
2950
3011
  const { config } = loaded;
2951
- const meta = (config.secret ?? {})[key];
2952
- if (!meta) return errorResult(`Secret not found: ${key}`);
2953
- const { encrypted_value: _, ...safeMeta } = meta;
2954
- return textResult(JSON.stringify({
2955
- key,
2956
- ...safeMeta
2957
- }, null, 2));
3012
+ return Option((config.secret ?? {})[key]).fold(() => errorResult(`Secret not found: ${key}`), (meta) => {
3013
+ const { encrypted_value: _, ...safeMeta } = meta;
3014
+ return textResult(JSON.stringify({
3015
+ key,
3016
+ ...safeMeta
3017
+ }, null, 2));
3018
+ });
2958
3019
  };
2959
3020
  const handleCheckExpiration = (args) => {
2960
3021
  const key = args.key;
2961
3022
  if (!key) return errorResult("Missing required argument: key");
2962
- const loaded = loadConfigForTool(args.configPath);
3023
+ const loaded = loadConfigForTool(configPathArg(args));
2963
3024
  if (!loaded.ok) return loaded.result;
2964
3025
  const { config } = loaded;
2965
3026
  return computeAudit(config).secrets.find((s) => s.key === key).fold(() => errorResult(`Secret not found: ${key}`), (s) => textResult(JSON.stringify({
@@ -2973,7 +3034,7 @@ const handleCheckExpiration = (args) => {
2973
3034
  }, null, 2)));
2974
3035
  };
2975
3036
  const handleGetEnvMeta = (args) => {
2976
- const loaded = loadConfigForTool(args.configPath);
3037
+ const loaded = loadConfigForTool(configPathArg(args));
2977
3038
  if (!loaded.ok) return loaded.result;
2978
3039
  const { config } = loaded;
2979
3040
  const envAudit = computeEnvAudit(config);
@@ -2986,11 +3047,7 @@ const handlers = {
2986
3047
  checkExpiration: handleCheckExpiration,
2987
3048
  getEnvMeta: handleGetEnvMeta
2988
3049
  };
2989
- const callTool = (name, args) => {
2990
- const handler = handlers[name];
2991
- if (!handler) return errorResult(`Unknown tool: ${name}`);
2992
- return handler(args);
2993
- };
3050
+ const callTool = (name, args) => Option(handlers[name]).fold(() => errorResult(`Unknown tool: ${name}`), (handler) => handler(args));
2994
3051
  //#endregion
2995
3052
  //#region src/mcp/server.ts
2996
3053
  const createServer = () => {
@@ -3016,13 +3073,11 @@ const createServer = () => {
3016
3073
  server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [...resourceDefinitions] }));
3017
3074
  server.setRequestHandler(ReadResourceRequestSchema, (request) => {
3018
3075
  const { uri } = request.params;
3019
- const result = readResource(uri);
3020
- if (!result) return { contents: [{
3076
+ return readResource(uri).fold(() => ({ contents: [{
3021
3077
  uri,
3022
3078
  mimeType: "text/plain",
3023
3079
  text: `Resource not found: ${uri}`
3024
- }] };
3025
- return result;
3080
+ }] }), (result) => result);
3026
3081
  });
3027
3082
  return server;
3028
3083
  };
@@ -3066,8 +3121,10 @@ const runResolve = (options) => {
3066
3121
  } else process.stdout.write(content);
3067
3122
  if (result.catalogPath) {
3068
3123
  const summaryTarget = options.output ? process.stdout : process.stderr;
3069
- summaryTarget.write(`\n${CYAN}Catalog:${RESET} ${result.catalogPath}\n${GREEN}Merged:${RESET} ${result.merged.length} key(s)${result.overridden.length > 0 ? ` ${YELLOW}(${result.overridden.length} overridden: ${result.overridden.join(", ")})${RESET}` : ""}\n`);
3070
- for (const w of result.warnings) summaryTarget.write(`${RED}Warning:${RESET} ${w}\n`);
3124
+ summaryTarget.write(`\n${BOLD}${CYAN}Catalog:${RESET} ${BLUE}${result.catalogPath}${RESET}\n${GREEN}Merged:${RESET} ${BOLD}${result.merged.length}${RESET} key(s)${result.overridden.length > 0 ? ` ${YELLOW}(${result.overridden.length} overridden: ${BOLD}${result.overridden.join(`${RESET}${YELLOW}, ${BOLD}`)}${RESET}${YELLOW})${RESET}` : ""}\n`);
3125
+ result.warnings.forEach((w) => {
3126
+ summaryTarget.write(`${RED}Warning:${RESET} ${w}\n`);
3127
+ });
3071
3128
  }
3072
3129
  });
3073
3130
  });
@@ -3080,18 +3137,20 @@ const resolveValues = async (keys, profile, agentKey) => {
3080
3137
  const result = {};
3081
3138
  const remaining = new Set(keys);
3082
3139
  if (fnoxAvailable()) fnoxExport(profile, agentKey).fold(() => {}, (exported) => {
3083
- for (const key of [...remaining]) if (key in exported) {
3084
- result[key] = exported[key];
3085
- remaining.delete(key);
3086
- }
3140
+ [...remaining].forEach((key) => {
3141
+ if (key in exported) {
3142
+ result[key] = exported[key];
3143
+ remaining.delete(key);
3144
+ }
3145
+ });
3087
3146
  });
3088
- for (const key of [...remaining]) {
3147
+ [...remaining].forEach((key) => {
3089
3148
  const envValue = process.env[key];
3090
3149
  if (envValue !== void 0 && envValue !== "") {
3091
3150
  result[key] = envValue;
3092
3151
  remaining.delete(key);
3093
3152
  }
3094
- }
3153
+ });
3095
3154
  if (remaining.size > 0 && process.stdin.isTTY) {
3096
3155
  const rl = createInterface({
3097
3156
  input: process.stdin,
@@ -3114,53 +3173,55 @@ const resolveValues = async (keys, profile, agentKey) => {
3114
3173
  const writeSealedToml = (configPath, sealedMeta) => {
3115
3174
  const lines = readFileSync(configPath, "utf-8").split("\n");
3116
3175
  const output = [];
3117
- let currentMetaKey;
3176
+ let currentMetaKey = Option.none();
3118
3177
  let insideMetaBlock = false;
3119
3178
  let hasEncryptedValue = false;
3120
3179
  const pendingSeals = /* @__PURE__ */ new Map();
3121
- for (const [key, meta] of Object.entries(sealedMeta)) if (meta.encrypted_value) pendingSeals.set(key, meta.encrypted_value);
3180
+ Object.entries(sealedMeta).forEach(([key, meta]) => {
3181
+ if (meta.encrypted_value) pendingSeals.set(key, meta.encrypted_value);
3182
+ });
3122
3183
  const metaSectionRe = /^\[secret\.(.+)\]\s*$/;
3123
3184
  const encryptedValueRe = /^encrypted_value\s*=/;
3124
3185
  const newSectionRe = /^\[/;
3125
- for (let i = 0; i < lines.length; i++) {
3126
- const line = lines[i];
3127
- const metaMatch = metaSectionRe.exec(line);
3128
- if (metaMatch) {
3129
- if (currentMetaKey && !hasEncryptedValue && pendingSeals.has(currentMetaKey)) {
3186
+ const flushPending = () => {
3187
+ currentMetaKey.forEach((key) => {
3188
+ if (!hasEncryptedValue && pendingSeals.has(key)) {
3130
3189
  output.push(`encrypted_value = """`);
3131
- output.push(pendingSeals.get(currentMetaKey));
3190
+ output.push(pendingSeals.get(key));
3132
3191
  output.push(`"""`);
3133
3192
  output.push("");
3134
- pendingSeals.delete(currentMetaKey);
3193
+ pendingSeals.delete(key);
3135
3194
  }
3136
- currentMetaKey = metaMatch[1];
3195
+ });
3196
+ };
3197
+ for (let i = 0; i < lines.length; i++) {
3198
+ const line = lines[i];
3199
+ const metaMatch = metaSectionRe.exec(line);
3200
+ if (metaMatch) {
3201
+ flushPending();
3202
+ currentMetaKey = Option(metaMatch[1]);
3137
3203
  insideMetaBlock = true;
3138
3204
  hasEncryptedValue = false;
3139
3205
  output.push(line);
3140
3206
  continue;
3141
3207
  }
3142
3208
  if (insideMetaBlock && newSectionRe.test(line) && !metaSectionRe.test(line)) {
3143
- if (currentMetaKey && !hasEncryptedValue && pendingSeals.has(currentMetaKey)) {
3144
- output.push(`encrypted_value = """`);
3145
- output.push(pendingSeals.get(currentMetaKey));
3146
- output.push(`"""`);
3147
- output.push("");
3148
- pendingSeals.delete(currentMetaKey);
3149
- }
3209
+ flushPending();
3150
3210
  insideMetaBlock = false;
3151
- currentMetaKey = void 0;
3211
+ currentMetaKey = Option.none();
3152
3212
  output.push(line);
3153
3213
  continue;
3154
3214
  }
3155
3215
  if (insideMetaBlock && encryptedValueRe.test(line)) {
3156
3216
  hasEncryptedValue = true;
3157
- const replacing = !!(currentMetaKey && pendingSeals.has(currentMetaKey));
3158
- if (replacing) {
3217
+ const replacing = currentMetaKey.fold(() => false, (key) => pendingSeals.has(key));
3218
+ if (replacing) currentMetaKey.forEach((key) => {
3159
3219
  output.push(`encrypted_value = """`);
3160
- output.push(pendingSeals.get(currentMetaKey));
3220
+ output.push(pendingSeals.get(key));
3161
3221
  output.push(`"""`);
3162
- pendingSeals.delete(currentMetaKey);
3163
- } else output.push(line);
3222
+ pendingSeals.delete(key);
3223
+ });
3224
+ else output.push(line);
3164
3225
  if (line.slice(line.indexOf("=") + 1).trim().includes("\"\"\"")) {
3165
3226
  while (i + 1 < lines.length && !lines[i + 1].includes("\"\"\"")) {
3166
3227
  if (!replacing) output.push(lines[i + 1]);
@@ -3175,12 +3236,7 @@ const writeSealedToml = (configPath, sealedMeta) => {
3175
3236
  }
3176
3237
  output.push(line);
3177
3238
  }
3178
- if (currentMetaKey && !hasEncryptedValue && pendingSeals.has(currentMetaKey)) {
3179
- output.push(`encrypted_value = """`);
3180
- output.push(pendingSeals.get(currentMetaKey));
3181
- output.push(`"""`);
3182
- pendingSeals.delete(currentMetaKey);
3183
- }
3239
+ flushPending();
3184
3240
  writeFileSync(configPath, output.join("\n"));
3185
3241
  };
3186
3242
  const runSeal = async (options) => {
@@ -3217,10 +3273,13 @@ const runSeal = async (options) => {
3217
3273
  console.error(`${DIM}Move these to [secret.*] only, or remove from [env.*] before sealing.${RESET}`);
3218
3274
  process.exit(2);
3219
3275
  }
3220
- const identityKey = config.identity.key_file ? unwrapAgentKey(resolve(configDir, expandPath(config.identity.key_file))).fold((err) => {
3221
- const msg = err._tag === "IdentityNotFound" ? `not found: ${err.path}` : err.message;
3222
- console.error(`${YELLOW}Warning:${RESET} Could not unwrap agent key: ${msg}`);
3223
- }, (k) => k) : void 0;
3276
+ const identityKey = Option(config.identity.key_file).flatMap((keyFile) => {
3277
+ return unwrapAgentKey(resolve(configDir, expandPath(keyFile))).fold((err) => {
3278
+ const msg = err._tag === "IdentityNotFound" ? `not found: ${err.path}` : err.message;
3279
+ console.error(`${YELLOW}Warning:${RESET} Could not unwrap agent key: ${msg}`);
3280
+ return Option.none();
3281
+ }, (k) => Option(k));
3282
+ });
3224
3283
  const editKeys = options.edit ? options.edit.split(",").map((k) => k.trim()).filter((k) => k.length > 0) : [];
3225
3284
  if (editKeys.length > 0) {
3226
3285
  const allSecretEntries = config.secret ?? {};
@@ -3269,8 +3328,8 @@ const runSeal = async (options) => {
3269
3328
  }
3270
3329
  const allSecretEntries = config.secret ?? {};
3271
3330
  const allKeys = Object.keys(allSecretEntries);
3272
- const alreadySealed = allKeys.filter((k) => allSecretEntries[k]?.encrypted_value);
3273
- const unsealed = allKeys.filter((k) => !allSecretEntries[k]?.encrypted_value);
3331
+ const alreadySealed = allKeys.filter((k) => allSecretEntries[k].encrypted_value);
3332
+ const unsealed = allKeys.filter((k) => !allSecretEntries[k].encrypted_value);
3274
3333
  if (!options.reseal && alreadySealed.length > 0) {
3275
3334
  if (unsealed.length === 0) {
3276
3335
  console.log(`${GREEN}✓${RESET} All ${BOLD}${alreadySealed.length}${RESET} secret(s) already sealed. Use ${CYAN}--reseal${RESET} to re-encrypt.`);
@@ -3303,13 +3362,13 @@ const runSeal = async (options) => {
3303
3362
  process.exit(2);
3304
3363
  return {};
3305
3364
  }, (d) => d);
3306
- const newValues = unsealed.length > 0 ? await resolveValues(unsealed, options.profile, identityKey) : {};
3365
+ const newValues = unsealed.length > 0 ? await resolveValues(unsealed, options.profile, identityKey.orUndefined()) : {};
3307
3366
  return {
3308
3367
  ...decrypted,
3309
3368
  ...newValues
3310
3369
  };
3311
3370
  }
3312
- return resolveValues(metaKeys, options.profile, identityKey);
3371
+ return resolveValues(metaKeys, options.profile, identityKey.orUndefined());
3313
3372
  })();
3314
3373
  const resolved = Object.keys(values).length;
3315
3374
  const skipped = metaKeys.length - resolved;
@@ -3382,7 +3441,7 @@ const buildFieldUpdates = (options) => {
3382
3441
  return updates;
3383
3442
  };
3384
3443
  const withConfig = (configFlag, fn) => {
3385
- resolveConfigPath(configFlag).fold((err) => {
3444
+ resolveConfigPath(configFlag.orUndefined()).fold((err) => {
3386
3445
  console.error(formatError(err));
3387
3446
  process.exit(2);
3388
3447
  }, ({ path: configPath, source }) => {
@@ -3426,7 +3485,7 @@ const runSecretEdit = (name, options) => {
3426
3485
  console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
3427
3486
  process.exit(1);
3428
3487
  }
3429
- withConfig(options.config, (configPath, raw) => {
3488
+ withConfig(Option(options.config), (configPath, raw) => {
3430
3489
  loadConfig(configPath).fold((err) => {
3431
3490
  console.error(formatError(err));
3432
3491
  process.exit(2);
@@ -3456,7 +3515,7 @@ const runSecretEdit = (name, options) => {
3456
3515
  });
3457
3516
  };
3458
3517
  const runSecretRm = (name, options) => {
3459
- withConfig(options.config, (configPath, raw) => {
3518
+ withConfig(Option(options.config), (configPath, raw) => {
3460
3519
  removeSection(raw, `[secret.${name}]`).fold((err) => {
3461
3520
  console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
3462
3521
  process.exit(1);
@@ -3472,7 +3531,7 @@ const runSecretRm = (name, options) => {
3472
3531
  });
3473
3532
  };
3474
3533
  const runSecretRename = (oldName, newName, options) => {
3475
- withConfig(options.config, (configPath, raw) => {
3534
+ withConfig(Option(options.config), (configPath, raw) => {
3476
3535
  renameSection(raw, `[secret.${oldName}]`, `[secret.${newName}]`).fold((err) => {
3477
3536
  console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
3478
3537
  process.exit(1);
@@ -3546,42 +3605,34 @@ const runShellHook = (shell) => {
3546
3605
  };
3547
3606
  //#endregion
3548
3607
  //#region src/cli/commands/upgrade.ts
3549
- const getCurrentVersion = () => {
3550
- try {
3551
- return execFileSync("npm", [
3552
- "list",
3553
- "-g",
3554
- "envpkt",
3555
- "--json"
3556
- ], {
3557
- encoding: "utf-8",
3558
- stdio: [
3559
- "pipe",
3560
- "pipe",
3561
- "pipe"
3562
- ]
3563
- }).match(/"envpkt":\s*\{\s*"version":\s*"([^"]+)"/)?.[1] ?? "unknown";
3564
- } catch {
3565
- return "unknown";
3566
- }
3567
- };
3608
+ const getCurrentVersion = () => Try(() => execFileSync("npm", [
3609
+ "list",
3610
+ "-g",
3611
+ "envpkt",
3612
+ "--json"
3613
+ ], {
3614
+ encoding: "utf-8",
3615
+ stdio: [
3616
+ "pipe",
3617
+ "pipe",
3618
+ "pipe"
3619
+ ]
3620
+ })).fold(() => "unknown", (output) => output.match(/"envpkt":\s*\{\s*"version":\s*"([^"]+)"/)?.[1] ?? "unknown");
3568
3621
  const runUpgrade = () => {
3569
3622
  const before = getCurrentVersion();
3570
3623
  console.log(`${DIM}Current version: ${before}${RESET}`);
3571
3624
  console.log(`${CYAN}Upgrading envpkt...${RESET}\n`);
3572
- try {
3573
- execFileSync("npm", [
3574
- "install",
3575
- "-g",
3576
- "envpkt@latest",
3577
- "--prefer-online"
3578
- ], {
3579
- stdio: "inherit",
3580
- encoding: "utf-8"
3581
- });
3582
- } catch {
3625
+ Try(() => execFileSync("npm", [
3626
+ "install",
3627
+ "-g",
3628
+ "envpkt@latest",
3629
+ "--prefer-online"
3630
+ ], {
3631
+ stdio: "inherit",
3632
+ encoding: "utf-8"
3633
+ })).fold(() => {
3583
3634
  console.error(`\n${RED}Error:${RESET} npm install failed. Trying with cache clean...`);
3584
- try {
3635
+ Try(() => {
3585
3636
  execFileSync("npm", [
3586
3637
  "cache",
3587
3638
  "clean",
@@ -3595,12 +3646,12 @@ const runUpgrade = () => {
3595
3646
  stdio: "inherit",
3596
3647
  encoding: "utf-8"
3597
3648
  });
3598
- } catch {
3649
+ }).fold(() => {
3599
3650
  console.error(`${RED}Error:${RESET} Upgrade failed. Try manually:`);
3600
3651
  console.error(` ${BOLD}sudo npm install -g envpkt@latest --prefer-online${RESET}`);
3601
3652
  process.exit(1);
3602
- }
3603
- }
3653
+ }, () => {});
3654
+ }, () => {});
3604
3655
  const after = getCurrentVersion();
3605
3656
  if (before === after && before !== "unknown") console.log(`\n${GREEN}✓${RESET} Already on latest version ${BOLD}${after}${RESET}`);
3606
3657
  else console.log(`\n${GREEN}✓${RESET} Upgraded ${YELLOW}${before}${RESET} → ${BOLD}${after}${RESET}`);
@@ -3620,7 +3671,7 @@ program.name("envpkt").description("Credential lifecycle and fleet management fo
3620
3671
  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) => {
3621
3672
  runInit(process.cwd(), options);
3622
3673
  });
3623
- program.command("keygen").description("Generate an age keypair for sealing secrets — run this before `seal` if you don't have a key yet").option("-c, --config <path>", "Path to envpkt.toml (updates identity.recipient if found)").option("--force", "Overwrite existing identity file").option("-o, --output <path>", "Output path for identity file (default: ~/.envpkt/age-key.txt)").action((options) => {
3674
+ program.command("keygen").description("Generate an age keypair for sealing secrets — run this before `seal` if you don't have a key yet").option("-c, --config <path>", "Path to envpkt.toml (updates identity.recipient if found)").option("-o, --output <path>", "Output path for identity file (default: ~/.envpkt/<project>-key.txt)").option("--global", "Write key to the shared default path (~/.envpkt/age-key.txt) instead of a project-specific one").action((options) => {
3624
3675
  runKeygen(options);
3625
3676
  });
3626
3677
  program.command("audit").description("Audit credential health from envpkt.toml (use --strict in CI pipelines to gate deploys)").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json | minimal", "table").option("--expiring <days>", "Show secrets expiring within N days", parseInt).option("--status <status>", "Filter by status: healthy | expiring_soon | expired | stale | missing").option("--strict", "Exit non-zero on any non-healthy secret").option("--all", "Show both secrets and env defaults").option("--env-only", "Show only env defaults (drift detection)").option("--sealed", "Show only secrets with encrypted_value").option("--external", "Show only secrets without encrypted_value").action((options) => {