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 +474 -433
- package/dist/index.d.ts +15 -6
- package/dist/index.js +292 -281
- package/package.json +1 -1
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
|
|
28
|
-
const expires = Option(meta
|
|
29
|
-
const rotationUrl = Option(meta
|
|
30
|
-
const purpose = Option(meta
|
|
31
|
-
const service = Option(meta
|
|
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
|
|
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
|
|
56
|
-
expires: Option(meta
|
|
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
|
-
|
|
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
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
|
392
|
-
|
|
393
|
-
...
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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
|
|
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 <=
|
|
719
|
-
}
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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: `
|
|
800
|
-
}), (
|
|
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
|
|
876
|
-
const
|
|
877
|
-
|
|
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
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
output
|
|
913
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
})
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
1087
|
-
|
|
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
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
}
|
|
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
|
|
1104
|
-
warnings.push(
|
|
1105
|
-
}, (
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1140
|
+
skipped.push(...remainingKeys);
|
|
1117
1141
|
}, (exported) => {
|
|
1118
|
-
|
|
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
|
-
|
|
1121
|
-
|
|
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
|
-
|
|
1153
|
+
skipped.push(...remainingKeys);
|
|
1127
1154
|
}
|
|
1128
|
-
if (inject)
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1802
|
-
confidence:
|
|
1803
|
-
matchedBy: `exact:${
|
|
1830
|
+
service: Option(exactMatch.service),
|
|
1831
|
+
confidence: exactMatch.confidence,
|
|
1832
|
+
matchedBy: `exact:${exactMatch.pattern}`
|
|
1804
1833
|
});
|
|
1805
1834
|
return matchValueShape(value).fold(() => {
|
|
1806
|
-
|
|
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
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
-
|
|
1891
|
+
const secretDriftEntries = metaKeys.map((key) => {
|
|
1866
1892
|
const meta = secretEntries[key];
|
|
1867
1893
|
const present = env[key] !== void 0 && env[key] !== "";
|
|
1868
|
-
|
|
1894
|
+
return {
|
|
1869
1895
|
envVar: key,
|
|
1870
|
-
service: Option(meta
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2384
|
+
boot.warnings.forEach((warning) => {
|
|
2385
|
+
console.error(`${YELLOW}Warning:${RESET} ${warning}`);
|
|
2386
|
+
});
|
|
2349
2387
|
const env = { ...process.env };
|
|
2350
|
-
|
|
2351
|
-
|
|
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
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
|
2576
|
-
if (meta.comment) console.log(`${indent}comment
|
|
2577
|
-
if (meta.capabilities) console.log(`${indent}
|
|
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(
|
|
2580
|
-
if (meta.expires) dateParts.push(
|
|
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(
|
|
2584
|
-
if (meta.rate_limit) opsParts.push(
|
|
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
|
|
2587
|
-
if (meta.model_hint) console.log(`${indent}model_hint
|
|
2588
|
-
if (meta.rotation_url) console.log(`${indent}rotation_url
|
|
2589
|
-
if (meta.required !== void 0) console.log(`${indent}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
|
|
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(
|
|
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
|
|
2603
|
-
if (config.identity.description) console.log(` description
|
|
2604
|
-
if (config.identity.capabilities) console.log(` capabilities
|
|
2605
|
-
if (config.identity.expires) console.log(` expires
|
|
2606
|
-
if (config.identity.services) console.log(` services
|
|
2607
|
-
if (config.identity.secrets) console.log(` secrets
|
|
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
|
-
|
|
2613
|
-
const
|
|
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
|
-
|
|
2625
|
-
console.log(` ${BOLD}${key}${RESET} = "${entry.value}"`);
|
|
2626
|
-
if (entry.purpose) console.log(` purpose
|
|
2627
|
-
if (entry.comment) console.log(` comment
|
|
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
|
|
2634
|
-
if (config.lifecycle.require_expiration !== void 0) console.log(` require_expiration
|
|
2635
|
-
if (config.lifecycle.require_service !== void 0) console.log(` 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
|
|
2641
|
-
if (resolveResult.overridden.length > 0) console.log(` overridden
|
|
2642
|
-
else console.log(`
|
|
2643
|
-
|
|
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))
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
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(() =>
|
|
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
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
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
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
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
|
-
|
|
2781
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2933
|
-
|
|
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
|
|
2999
|
+
const loaded = loadConfigForTool(configPathArg(args));
|
|
2949
3000
|
if (!loaded.ok) return loaded.result;
|
|
2950
3001
|
const { config } = loaded;
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
}
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
3070
|
-
|
|
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
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3130
|
+
[...remaining].forEach((key) => {
|
|
3131
|
+
if (key in exported) {
|
|
3132
|
+
result[key] = exported[key];
|
|
3133
|
+
remaining.delete(key);
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3087
3136
|
});
|
|
3088
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
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(
|
|
3180
|
+
output.push(pendingSeals.get(key));
|
|
3132
3181
|
output.push(`"""`);
|
|
3133
3182
|
output.push("");
|
|
3134
|
-
pendingSeals.delete(
|
|
3183
|
+
pendingSeals.delete(key);
|
|
3135
3184
|
}
|
|
3136
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
3210
|
+
output.push(pendingSeals.get(key));
|
|
3161
3211
|
output.push(`"""`);
|
|
3162
|
-
pendingSeals.delete(
|
|
3163
|
-
}
|
|
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
|
-
|
|
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 =
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
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]
|
|
3273
|
-
const unsealed = allKeys.filter((k) => !allSecretEntries[k]
|
|
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
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
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
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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}`);
|