envpkt 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js 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
@@ -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,6 +2731,13 @@ 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)));
2693
2741
  const runKeygen = (options) => {
2694
2742
  const outputPath = options.output ?? resolveKeyPath();
2695
2743
  generateKeypair({
@@ -2708,15 +2756,24 @@ const runKeygen = (options) => {
2708
2756
  console.log(`${BOLD}Recipient:${RESET} ${recipient}`);
2709
2757
  console.log("");
2710
2758
  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 {
2759
+ if (existsSync(configPath)) {
2760
+ const name = deriveIdentityName(configPath);
2761
+ const keyFile = tildeShorten(identityPath);
2762
+ updateConfigIdentity(configPath, {
2763
+ recipient,
2764
+ name,
2765
+ keyFile
2766
+ }).fold((err) => {
2767
+ console.error(`${YELLOW}Warning:${RESET} Could not update config: ${"message" in err ? err.message : err._tag}`);
2768
+ console.log(`${DIM}Manually add to your envpkt.toml:${RESET}`);
2769
+ console.log(` [identity]`);
2770
+ console.log(` name = "${name}"`);
2771
+ console.log(` recipient = "${recipient}"`);
2772
+ console.log(` key_file = "${keyFile}"`);
2773
+ }, () => {
2774
+ console.log(`${GREEN}Updated${RESET} ${CYAN}${configPath}${RESET} with identity (name, recipient, key_file)`);
2775
+ });
2776
+ } else {
2720
2777
  console.log(`${BOLD}Next steps:${RESET}`);
2721
2778
  console.log(` ${DIM}1.${RESET} envpkt init ${DIM}# create envpkt.toml${RESET}`);
2722
2779
  console.log(` ${DIM}2.${RESET} envpkt env scan --write ${DIM}# discover credentials${RESET}`);
@@ -2727,7 +2784,7 @@ const runKeygen = (options) => {
2727
2784
  //#endregion
2728
2785
  //#region src/mcp/resources.ts
2729
2786
  const loadConfigSafe = () => {
2730
- return resolveConfigPath().fold(() => void 0, ({ path }) => loadConfig(path).fold(() => void 0, (config) => ({
2787
+ return resolveConfigPath().fold(() => Option.none(), ({ path }) => loadConfig(path).fold(() => Option.none(), (config) => Option({
2731
2788
  config,
2732
2789
  path
2733
2790
  })));
@@ -2743,14 +2800,11 @@ const resourceDefinitions = [{
2743
2800
  description: "Capabilities declared by the agent and per-secret capability grants",
2744
2801
  mimeType: "application/json"
2745
2802
  }];
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;
2803
+ const readHealth = () => loadConfigSafe().fold(() => ({ contents: [{
2804
+ uri: "envpkt://health",
2805
+ mimeType: "application/json",
2806
+ text: JSON.stringify({ error: "No envpkt.toml found" })
2807
+ }] }), ({ config, path }) => {
2754
2808
  const audit = computeAudit(config);
2755
2809
  return { contents: [{
2756
2810
  uri: "envpkt://health",
@@ -2766,19 +2820,17 @@ const readHealth = () => {
2766
2820
  missing: audit.missing
2767
2821
  }, null, 2)
2768
2822
  }] };
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;
2823
+ });
2824
+ const readCapabilities = () => loadConfigSafe().fold(() => ({ contents: [{
2825
+ uri: "envpkt://capabilities",
2826
+ mimeType: "application/json",
2827
+ text: JSON.stringify({ error: "No envpkt.toml found" })
2828
+ }] }), ({ config }) => {
2778
2829
  const agentCapabilities = config.identity?.capabilities ?? [];
2779
2830
  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;
2831
+ Object.entries(config.secret ?? {}).forEach(([key, meta]) => {
2832
+ if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
2833
+ });
2782
2834
  return { contents: [{
2783
2835
  uri: "envpkt://capabilities",
2784
2836
  mimeType: "application/json",
@@ -2792,15 +2844,12 @@ const readCapabilities = () => {
2792
2844
  secrets: secretCapabilities
2793
2845
  }, null, 2)
2794
2846
  }] };
2795
- };
2847
+ });
2796
2848
  const resourceHandlers = {
2797
2849
  "envpkt://health": readHealth,
2798
2850
  "envpkt://capabilities": readCapabilities
2799
2851
  };
2800
- const readResource = (uri) => {
2801
- const handler = resourceHandlers[uri];
2802
- return handler?.();
2803
- };
2852
+ const readResource = (uri) => Option(resourceHandlers[uri]).map((handler) => handler());
2804
2853
  //#endregion
2805
2854
  //#region src/mcp/tools.ts
2806
2855
  const textResult = (text) => ({ content: [{
@@ -2815,7 +2864,7 @@ const errorResult = (message) => ({
2815
2864
  isError: true
2816
2865
  });
2817
2866
  const loadConfigForTool = (configPath) => {
2818
- return resolveConfigPath(configPath).fold((err) => ({
2867
+ return resolveConfigPath(configPath.fold(() => void 0, (v) => v)).fold((err) => ({
2819
2868
  ok: false,
2820
2869
  result: errorResult(`Config error: ${err._tag} — ${err._tag === "FileNotFound" ? err.path : ""}`)
2821
2870
  }), ({ path }) => loadConfig(path).fold((err) => ({
@@ -2898,8 +2947,9 @@ const toolDefinitions = [
2898
2947
  }
2899
2948
  }
2900
2949
  ];
2950
+ const configPathArg = (args) => Option(typeof args.configPath === "string" ? args.configPath : null);
2901
2951
  const handleGetPacketHealth = (args) => {
2902
- const loaded = loadConfigForTool(args.configPath);
2952
+ const loaded = loadConfigForTool(configPathArg(args));
2903
2953
  if (!loaded.ok) return loaded.result;
2904
2954
  const { config, path } = loaded;
2905
2955
  const audit = computeAudit(config);
@@ -2924,13 +2974,14 @@ const handleGetPacketHealth = (args) => {
2924
2974
  }, null, 2));
2925
2975
  };
2926
2976
  const handleListCapabilities = (args) => {
2927
- const loaded = loadConfigForTool(args.configPath);
2977
+ const loaded = loadConfigForTool(configPathArg(args));
2928
2978
  if (!loaded.ok) return loaded.result;
2929
2979
  const { config } = loaded;
2930
2980
  const agentCapabilities = config.identity?.capabilities ?? [];
2931
2981
  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;
2982
+ Object.entries(config.secret ?? {}).forEach(([key, meta]) => {
2983
+ if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
2984
+ });
2934
2985
  return textResult(JSON.stringify({
2935
2986
  identity: config.identity ? {
2936
2987
  name: config.identity.name,
@@ -2945,21 +2996,21 @@ const handleListCapabilities = (args) => {
2945
2996
  const handleGetSecretMeta = (args) => {
2946
2997
  const key = args.key;
2947
2998
  if (!key) return errorResult("Missing required argument: key");
2948
- const loaded = loadConfigForTool(args.configPath);
2999
+ const loaded = loadConfigForTool(configPathArg(args));
2949
3000
  if (!loaded.ok) return loaded.result;
2950
3001
  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));
3002
+ return Option((config.secret ?? {})[key]).fold(() => errorResult(`Secret not found: ${key}`), (meta) => {
3003
+ const { encrypted_value: _, ...safeMeta } = meta;
3004
+ return textResult(JSON.stringify({
3005
+ key,
3006
+ ...safeMeta
3007
+ }, null, 2));
3008
+ });
2958
3009
  };
2959
3010
  const handleCheckExpiration = (args) => {
2960
3011
  const key = args.key;
2961
3012
  if (!key) return errorResult("Missing required argument: key");
2962
- const loaded = loadConfigForTool(args.configPath);
3013
+ const loaded = loadConfigForTool(configPathArg(args));
2963
3014
  if (!loaded.ok) return loaded.result;
2964
3015
  const { config } = loaded;
2965
3016
  return computeAudit(config).secrets.find((s) => s.key === key).fold(() => errorResult(`Secret not found: ${key}`), (s) => textResult(JSON.stringify({
@@ -2973,7 +3024,7 @@ const handleCheckExpiration = (args) => {
2973
3024
  }, null, 2)));
2974
3025
  };
2975
3026
  const handleGetEnvMeta = (args) => {
2976
- const loaded = loadConfigForTool(args.configPath);
3027
+ const loaded = loadConfigForTool(configPathArg(args));
2977
3028
  if (!loaded.ok) return loaded.result;
2978
3029
  const { config } = loaded;
2979
3030
  const envAudit = computeEnvAudit(config);
@@ -2986,11 +3037,7 @@ const handlers = {
2986
3037
  checkExpiration: handleCheckExpiration,
2987
3038
  getEnvMeta: handleGetEnvMeta
2988
3039
  };
2989
- const callTool = (name, args) => {
2990
- const handler = handlers[name];
2991
- if (!handler) return errorResult(`Unknown tool: ${name}`);
2992
- return handler(args);
2993
- };
3040
+ const callTool = (name, args) => Option(handlers[name]).fold(() => errorResult(`Unknown tool: ${name}`), (handler) => handler(args));
2994
3041
  //#endregion
2995
3042
  //#region src/mcp/server.ts
2996
3043
  const createServer = () => {
@@ -3016,13 +3063,11 @@ const createServer = () => {
3016
3063
  server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [...resourceDefinitions] }));
3017
3064
  server.setRequestHandler(ReadResourceRequestSchema, (request) => {
3018
3065
  const { uri } = request.params;
3019
- const result = readResource(uri);
3020
- if (!result) return { contents: [{
3066
+ return readResource(uri).fold(() => ({ contents: [{
3021
3067
  uri,
3022
3068
  mimeType: "text/plain",
3023
3069
  text: `Resource not found: ${uri}`
3024
- }] };
3025
- return result;
3070
+ }] }), (result) => result);
3026
3071
  });
3027
3072
  return server;
3028
3073
  };
@@ -3066,8 +3111,10 @@ const runResolve = (options) => {
3066
3111
  } else process.stdout.write(content);
3067
3112
  if (result.catalogPath) {
3068
3113
  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`);
3114
+ 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`);
3115
+ result.warnings.forEach((w) => {
3116
+ summaryTarget.write(`${RED}Warning:${RESET} ${w}\n`);
3117
+ });
3071
3118
  }
3072
3119
  });
3073
3120
  });
@@ -3080,18 +3127,20 @@ const resolveValues = async (keys, profile, agentKey) => {
3080
3127
  const result = {};
3081
3128
  const remaining = new Set(keys);
3082
3129
  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
- }
3130
+ [...remaining].forEach((key) => {
3131
+ if (key in exported) {
3132
+ result[key] = exported[key];
3133
+ remaining.delete(key);
3134
+ }
3135
+ });
3087
3136
  });
3088
- for (const key of [...remaining]) {
3137
+ [...remaining].forEach((key) => {
3089
3138
  const envValue = process.env[key];
3090
3139
  if (envValue !== void 0 && envValue !== "") {
3091
3140
  result[key] = envValue;
3092
3141
  remaining.delete(key);
3093
3142
  }
3094
- }
3143
+ });
3095
3144
  if (remaining.size > 0 && process.stdin.isTTY) {
3096
3145
  const rl = createInterface({
3097
3146
  input: process.stdin,
@@ -3114,53 +3163,55 @@ const resolveValues = async (keys, profile, agentKey) => {
3114
3163
  const writeSealedToml = (configPath, sealedMeta) => {
3115
3164
  const lines = readFileSync(configPath, "utf-8").split("\n");
3116
3165
  const output = [];
3117
- let currentMetaKey;
3166
+ let currentMetaKey = Option.none();
3118
3167
  let insideMetaBlock = false;
3119
3168
  let hasEncryptedValue = false;
3120
3169
  const pendingSeals = /* @__PURE__ */ new Map();
3121
- for (const [key, meta] of Object.entries(sealedMeta)) if (meta.encrypted_value) pendingSeals.set(key, meta.encrypted_value);
3170
+ Object.entries(sealedMeta).forEach(([key, meta]) => {
3171
+ if (meta.encrypted_value) pendingSeals.set(key, meta.encrypted_value);
3172
+ });
3122
3173
  const metaSectionRe = /^\[secret\.(.+)\]\s*$/;
3123
3174
  const encryptedValueRe = /^encrypted_value\s*=/;
3124
3175
  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)) {
3176
+ const flushPending = () => {
3177
+ currentMetaKey.forEach((key) => {
3178
+ if (!hasEncryptedValue && pendingSeals.has(key)) {
3130
3179
  output.push(`encrypted_value = """`);
3131
- output.push(pendingSeals.get(currentMetaKey));
3180
+ output.push(pendingSeals.get(key));
3132
3181
  output.push(`"""`);
3133
3182
  output.push("");
3134
- pendingSeals.delete(currentMetaKey);
3183
+ pendingSeals.delete(key);
3135
3184
  }
3136
- currentMetaKey = metaMatch[1];
3185
+ });
3186
+ };
3187
+ for (let i = 0; i < lines.length; i++) {
3188
+ const line = lines[i];
3189
+ const metaMatch = metaSectionRe.exec(line);
3190
+ if (metaMatch) {
3191
+ flushPending();
3192
+ currentMetaKey = Option(metaMatch[1]);
3137
3193
  insideMetaBlock = true;
3138
3194
  hasEncryptedValue = false;
3139
3195
  output.push(line);
3140
3196
  continue;
3141
3197
  }
3142
3198
  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
- }
3199
+ flushPending();
3150
3200
  insideMetaBlock = false;
3151
- currentMetaKey = void 0;
3201
+ currentMetaKey = Option.none();
3152
3202
  output.push(line);
3153
3203
  continue;
3154
3204
  }
3155
3205
  if (insideMetaBlock && encryptedValueRe.test(line)) {
3156
3206
  hasEncryptedValue = true;
3157
- const replacing = !!(currentMetaKey && pendingSeals.has(currentMetaKey));
3158
- if (replacing) {
3207
+ const replacing = currentMetaKey.fold(() => false, (key) => pendingSeals.has(key));
3208
+ if (replacing) currentMetaKey.forEach((key) => {
3159
3209
  output.push(`encrypted_value = """`);
3160
- output.push(pendingSeals.get(currentMetaKey));
3210
+ output.push(pendingSeals.get(key));
3161
3211
  output.push(`"""`);
3162
- pendingSeals.delete(currentMetaKey);
3163
- } else output.push(line);
3212
+ pendingSeals.delete(key);
3213
+ });
3214
+ else output.push(line);
3164
3215
  if (line.slice(line.indexOf("=") + 1).trim().includes("\"\"\"")) {
3165
3216
  while (i + 1 < lines.length && !lines[i + 1].includes("\"\"\"")) {
3166
3217
  if (!replacing) output.push(lines[i + 1]);
@@ -3175,12 +3226,7 @@ const writeSealedToml = (configPath, sealedMeta) => {
3175
3226
  }
3176
3227
  output.push(line);
3177
3228
  }
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
- }
3229
+ flushPending();
3184
3230
  writeFileSync(configPath, output.join("\n"));
3185
3231
  };
3186
3232
  const runSeal = async (options) => {
@@ -3217,10 +3263,13 @@ const runSeal = async (options) => {
3217
3263
  console.error(`${DIM}Move these to [secret.*] only, or remove from [env.*] before sealing.${RESET}`);
3218
3264
  process.exit(2);
3219
3265
  }
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;
3266
+ const identityKey = Option(config.identity.key_file).flatMap((keyFile) => {
3267
+ return unwrapAgentKey(resolve(configDir, expandPath(keyFile))).fold((err) => {
3268
+ const msg = err._tag === "IdentityNotFound" ? `not found: ${err.path}` : err.message;
3269
+ console.error(`${YELLOW}Warning:${RESET} Could not unwrap agent key: ${msg}`);
3270
+ return Option.none();
3271
+ }, (k) => Option(k));
3272
+ });
3224
3273
  const editKeys = options.edit ? options.edit.split(",").map((k) => k.trim()).filter((k) => k.length > 0) : [];
3225
3274
  if (editKeys.length > 0) {
3226
3275
  const allSecretEntries = config.secret ?? {};
@@ -3269,8 +3318,8 @@ const runSeal = async (options) => {
3269
3318
  }
3270
3319
  const allSecretEntries = config.secret ?? {};
3271
3320
  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);
3321
+ const alreadySealed = allKeys.filter((k) => allSecretEntries[k].encrypted_value);
3322
+ const unsealed = allKeys.filter((k) => !allSecretEntries[k].encrypted_value);
3274
3323
  if (!options.reseal && alreadySealed.length > 0) {
3275
3324
  if (unsealed.length === 0) {
3276
3325
  console.log(`${GREEN}✓${RESET} All ${BOLD}${alreadySealed.length}${RESET} secret(s) already sealed. Use ${CYAN}--reseal${RESET} to re-encrypt.`);
@@ -3303,13 +3352,13 @@ const runSeal = async (options) => {
3303
3352
  process.exit(2);
3304
3353
  return {};
3305
3354
  }, (d) => d);
3306
- const newValues = unsealed.length > 0 ? await resolveValues(unsealed, options.profile, identityKey) : {};
3355
+ const newValues = unsealed.length > 0 ? await resolveValues(unsealed, options.profile, identityKey.orUndefined()) : {};
3307
3356
  return {
3308
3357
  ...decrypted,
3309
3358
  ...newValues
3310
3359
  };
3311
3360
  }
3312
- return resolveValues(metaKeys, options.profile, identityKey);
3361
+ return resolveValues(metaKeys, options.profile, identityKey.orUndefined());
3313
3362
  })();
3314
3363
  const resolved = Object.keys(values).length;
3315
3364
  const skipped = metaKeys.length - resolved;
@@ -3382,7 +3431,7 @@ const buildFieldUpdates = (options) => {
3382
3431
  return updates;
3383
3432
  };
3384
3433
  const withConfig = (configFlag, fn) => {
3385
- resolveConfigPath(configFlag).fold((err) => {
3434
+ resolveConfigPath(configFlag.orUndefined()).fold((err) => {
3386
3435
  console.error(formatError(err));
3387
3436
  process.exit(2);
3388
3437
  }, ({ path: configPath, source }) => {
@@ -3426,7 +3475,7 @@ const runSecretEdit = (name, options) => {
3426
3475
  console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
3427
3476
  process.exit(1);
3428
3477
  }
3429
- withConfig(options.config, (configPath, raw) => {
3478
+ withConfig(Option(options.config), (configPath, raw) => {
3430
3479
  loadConfig(configPath).fold((err) => {
3431
3480
  console.error(formatError(err));
3432
3481
  process.exit(2);
@@ -3456,7 +3505,7 @@ const runSecretEdit = (name, options) => {
3456
3505
  });
3457
3506
  };
3458
3507
  const runSecretRm = (name, options) => {
3459
- withConfig(options.config, (configPath, raw) => {
3508
+ withConfig(Option(options.config), (configPath, raw) => {
3460
3509
  removeSection(raw, `[secret.${name}]`).fold((err) => {
3461
3510
  console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
3462
3511
  process.exit(1);
@@ -3472,7 +3521,7 @@ const runSecretRm = (name, options) => {
3472
3521
  });
3473
3522
  };
3474
3523
  const runSecretRename = (oldName, newName, options) => {
3475
- withConfig(options.config, (configPath, raw) => {
3524
+ withConfig(Option(options.config), (configPath, raw) => {
3476
3525
  renameSection(raw, `[secret.${oldName}]`, `[secret.${newName}]`).fold((err) => {
3477
3526
  console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
3478
3527
  process.exit(1);
@@ -3546,42 +3595,34 @@ const runShellHook = (shell) => {
3546
3595
  };
3547
3596
  //#endregion
3548
3597
  //#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
- };
3598
+ const getCurrentVersion = () => Try(() => execFileSync("npm", [
3599
+ "list",
3600
+ "-g",
3601
+ "envpkt",
3602
+ "--json"
3603
+ ], {
3604
+ encoding: "utf-8",
3605
+ stdio: [
3606
+ "pipe",
3607
+ "pipe",
3608
+ "pipe"
3609
+ ]
3610
+ })).fold(() => "unknown", (output) => output.match(/"envpkt":\s*\{\s*"version":\s*"([^"]+)"/)?.[1] ?? "unknown");
3568
3611
  const runUpgrade = () => {
3569
3612
  const before = getCurrentVersion();
3570
3613
  console.log(`${DIM}Current version: ${before}${RESET}`);
3571
3614
  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 {
3615
+ Try(() => execFileSync("npm", [
3616
+ "install",
3617
+ "-g",
3618
+ "envpkt@latest",
3619
+ "--prefer-online"
3620
+ ], {
3621
+ stdio: "inherit",
3622
+ encoding: "utf-8"
3623
+ })).fold(() => {
3583
3624
  console.error(`\n${RED}Error:${RESET} npm install failed. Trying with cache clean...`);
3584
- try {
3625
+ Try(() => {
3585
3626
  execFileSync("npm", [
3586
3627
  "cache",
3587
3628
  "clean",
@@ -3595,12 +3636,12 @@ const runUpgrade = () => {
3595
3636
  stdio: "inherit",
3596
3637
  encoding: "utf-8"
3597
3638
  });
3598
- } catch {
3639
+ }).fold(() => {
3599
3640
  console.error(`${RED}Error:${RESET} Upgrade failed. Try manually:`);
3600
3641
  console.error(` ${BOLD}sudo npm install -g envpkt@latest --prefer-online${RESET}`);
3601
3642
  process.exit(1);
3602
- }
3603
- }
3643
+ }, () => {});
3644
+ }, () => {});
3604
3645
  const after = getCurrentVersion();
3605
3646
  if (before === after && before !== "unknown") console.log(`\n${GREEN}✓${RESET} Already on latest version ${BOLD}${after}${RESET}`);
3606
3647
  else console.log(`\n${GREEN}✓${RESET} Upgraded ${YELLOW}${before}${RESET} → ${BOLD}${after}${RESET}`);