envpkt 0.1.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 ADDED
@@ -0,0 +1,2254 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { Cond, Left, List, Option, Right, Try } from "functype";
5
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
6
+ import { TypeCompiler } from "@sinclair/typebox/compiler";
7
+ import { TomlDate, parse, stringify } from "smol-toml";
8
+ import { FormatRegistry, Type } from "@sinclair/typebox";
9
+ import { execFileSync } from "node:child_process";
10
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
13
+
14
+ //#region src/core/audit.ts
15
+ const MS_PER_DAY = 864e5;
16
+ const WARN_BEFORE_DAYS = 30;
17
+ const daysBetween = (from, to) => Math.floor((to.getTime() - from.getTime()) / MS_PER_DAY);
18
+ const parseDate = (dateStr) => {
19
+ const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00Z");
20
+ return Number.isNaN(d.getTime()) ? Option(void 0) : Option(d);
21
+ };
22
+ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
23
+ const issues = [];
24
+ const created = Option(meta?.created).flatMap(parseDate);
25
+ const expires = Option(meta?.expires).flatMap(parseDate);
26
+ const rotationUrl = Option(meta?.rotation_url);
27
+ const purpose = Option(meta?.purpose);
28
+ const service = Option(meta?.service);
29
+ const daysRemaining = expires.map((exp) => daysBetween(today, exp));
30
+ const daysSinceCreated = created.map((c) => daysBetween(c, today));
31
+ const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
32
+ const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
33
+ const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
34
+ const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key);
35
+ const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
36
+ if (isExpired) issues.push("Secret has expired");
37
+ if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
38
+ if (isStale) issues.push("Secret is stale (no rotation detected)");
39
+ if (isMissing) issues.push("Key not found in fnox");
40
+ if (isMissingMetadata) {
41
+ if (requireExpiration && expires.isNone()) issues.push("Missing required expiration date");
42
+ if (requireService && service.isNone()) issues.push("Missing required service");
43
+ }
44
+ return {
45
+ key,
46
+ service,
47
+ status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").else("healthy"),
48
+ days_remaining: daysRemaining,
49
+ rotation_url: rotationUrl,
50
+ purpose,
51
+ created: Option(meta?.created),
52
+ expires: Option(meta?.expires),
53
+ issues: List(issues)
54
+ };
55
+ };
56
+ const computeAudit = (config, fnoxKeys, today) => {
57
+ const now = today ?? /* @__PURE__ */ new Date();
58
+ const lifecycle = config.lifecycle ?? {};
59
+ const staleWarningDays = lifecycle.stale_warning_days ?? 90;
60
+ const requireExpiration = lifecycle.require_expiration ?? false;
61
+ const requireService = lifecycle.require_service ?? false;
62
+ const keys = fnoxKeys ?? /* @__PURE__ */ new Set();
63
+ const metaKeys = new Set(Object.keys(config.meta));
64
+ const secrets = List(Object.entries(config.meta).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
65
+ const orphaned = keys.size > 0 ? [...metaKeys].filter((k) => !keys.has(k)).length : 0;
66
+ const total = secrets.size;
67
+ const expired = secrets.count((s) => s.status === "expired");
68
+ const missing = secrets.count((s) => s.status === "missing");
69
+ const missing_metadata = secrets.count((s) => s.status === "missing_metadata");
70
+ const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
71
+ const stale = secrets.count((s) => s.status === "stale");
72
+ const healthy = secrets.count((s) => s.status === "healthy");
73
+ return {
74
+ status: Cond.of().when(expired > 0 || missing > 0, "critical").elseWhen(expiring_soon > 0 || stale > 0 || missing_metadata > 0, "degraded").else("healthy"),
75
+ secrets,
76
+ total,
77
+ healthy,
78
+ expiring_soon,
79
+ expired,
80
+ stale,
81
+ missing,
82
+ missing_metadata,
83
+ orphaned,
84
+ agent: config.agent
85
+ };
86
+ };
87
+
88
+ //#endregion
89
+ //#region src/core/schema.ts
90
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
91
+ const URI_RE = /^https?:\/\/.+/;
92
+ if (!FormatRegistry.Has("date")) FormatRegistry.Set("date", (v) => DATE_RE.test(v));
93
+ if (!FormatRegistry.Has("uri")) FormatRegistry.Set("uri", (v) => URI_RE.test(v));
94
+ const ConsumerType = Type.Union([
95
+ Type.Literal("agent"),
96
+ Type.Literal("service"),
97
+ Type.Literal("developer"),
98
+ Type.Literal("ci")
99
+ ], { description: "Classification of the agent's consumer type" });
100
+ const AgentIdentitySchema = Type.Object({
101
+ name: Type.String({ description: "Agent display name" }),
102
+ consumer: Type.Optional(ConsumerType),
103
+ description: Type.Optional(Type.String({ description: "Agent description or role" })),
104
+ capabilities: Type.Optional(Type.Array(Type.String(), { description: "List of capabilities this agent provides" })),
105
+ expires: Type.Optional(Type.String({
106
+ format: "date",
107
+ description: "Agent credential expiration date (YYYY-MM-DD)"
108
+ })),
109
+ services: Type.Optional(Type.Array(Type.String(), { description: "Service dependencies for this agent" })),
110
+ identity: Type.Optional(Type.String({ description: "Path to encrypted agent key file (relative to config directory)" })),
111
+ recipient: Type.Optional(Type.String({ description: "Agent's age public key for encryption" })),
112
+ secrets: Type.Optional(Type.Array(Type.String(), { description: "Secret keys this agent needs from the catalog" }))
113
+ }, { description: "Identity and capabilities of the AI agent using this envpkt" });
114
+ const SecretMetaSchema = Type.Object({
115
+ service: Type.Optional(Type.String({ description: "Service or system this secret authenticates to" })),
116
+ expires: Type.Optional(Type.String({
117
+ format: "date",
118
+ description: "Date the secret expires (YYYY-MM-DD)"
119
+ })),
120
+ rotation_url: Type.Optional(Type.String({
121
+ format: "uri",
122
+ description: "URL or reference for secret rotation procedure"
123
+ })),
124
+ purpose: Type.Optional(Type.String({ description: "Why this secret exists and what it enables" })),
125
+ capabilities: Type.Optional(Type.Array(Type.String(), { description: "What operations this secret grants (e.g. read, write, admin)" })),
126
+ created: Type.Optional(Type.String({
127
+ format: "date",
128
+ description: "Date the secret was provisioned (YYYY-MM-DD)"
129
+ })),
130
+ rotates: Type.Optional(Type.String({ description: "Rotation schedule (e.g. '90d', 'quarterly')" })),
131
+ rate_limit: Type.Optional(Type.String({ description: "Rate limit or quota info (e.g. '1000/min')" })),
132
+ model_hint: Type.Optional(Type.String({ description: "Suggested model or tier for this credential" })),
133
+ source: Type.Optional(Type.String({ description: "Where the secret value originates (e.g. 'vault', 'ci')" })),
134
+ required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
135
+ tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
136
+ }, { description: "Metadata about a single secret" });
137
+ const LifecycleConfigSchema = Type.Object({
138
+ stale_warning_days: Type.Optional(Type.Number({
139
+ default: 90,
140
+ description: "Days since creation to consider a secret stale"
141
+ })),
142
+ require_expiration: Type.Optional(Type.Boolean({
143
+ default: false,
144
+ description: "Require expires on all secrets"
145
+ })),
146
+ require_service: Type.Optional(Type.Boolean({
147
+ default: false,
148
+ description: "Require service on all secrets"
149
+ }))
150
+ }, { description: "Policy configuration for credential lifecycle management" });
151
+ const CallbackConfigSchema = Type.Object({
152
+ on_expiring: Type.Optional(Type.String({ description: "Command or webhook to invoke when secrets are expiring" })),
153
+ on_expired: Type.Optional(Type.String({ description: "Command or webhook to invoke when secrets have expired" })),
154
+ on_audit_fail: Type.Optional(Type.String({ description: "Command or webhook on audit failure" }))
155
+ }, { description: "Automation callbacks for lifecycle events" });
156
+ const ToolsConfigSchema = Type.Record(Type.String(), Type.Unknown(), { description: "Tool integration configuration — open namespace for third-party extensions" });
157
+ const EnvpktConfigSchema = Type.Object({
158
+ version: Type.Number({
159
+ description: "Schema version number",
160
+ default: 1
161
+ }),
162
+ catalog: Type.Optional(Type.String({ description: "Path to shared secret catalog (relative to this config file)" })),
163
+ agent: Type.Optional(AgentIdentitySchema),
164
+ meta: Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" }),
165
+ lifecycle: Type.Optional(LifecycleConfigSchema),
166
+ callbacks: Type.Optional(CallbackConfigSchema),
167
+ tools: Type.Optional(ToolsConfigSchema)
168
+ }, {
169
+ $id: "envpkt",
170
+ title: "envpkt configuration",
171
+ description: "Credential lifecycle and fleet management configuration for AI agents"
172
+ });
173
+
174
+ //#endregion
175
+ //#region src/core/config.ts
176
+ const CONFIG_FILENAME$2 = "envpkt.toml";
177
+ const ENV_VAR_CONFIG = "ENVPKT_CONFIG";
178
+ const compiledSchema = TypeCompiler.Compile(EnvpktConfigSchema);
179
+ /** Recursively convert TomlDate instances to ISO date strings */
180
+ const normalizeDates = (obj) => {
181
+ if (obj instanceof TomlDate) return obj.toISOString().split("T")[0];
182
+ if (Array.isArray(obj)) return obj.map(normalizeDates);
183
+ if (obj !== null && typeof obj === "object") return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, normalizeDates(v)]));
184
+ return obj;
185
+ };
186
+ /** Find envpkt.toml in the given directory */
187
+ const findConfigPath = (dir) => {
188
+ const candidate = join(dir, CONFIG_FILENAME$2);
189
+ return existsSync(candidate) ? Option(candidate) : Option(void 0);
190
+ };
191
+ /** Read a config file, returning Either<ConfigError, string> */
192
+ const readConfigFile = (path) => {
193
+ if (!existsSync(path)) return Left({
194
+ _tag: "FileNotFound",
195
+ path
196
+ });
197
+ return Try(() => readFileSync(path, "utf-8")).fold((err) => Left({
198
+ _tag: "ReadError",
199
+ message: String(err)
200
+ }), (content) => Right(content));
201
+ };
202
+ /** Ensure required fields have defaults for valid configs (e.g. agent configs with catalog may omit meta) */
203
+ const applyDefaults = (data) => {
204
+ if (data !== null && typeof data === "object" && !Array.isArray(data)) {
205
+ const obj = data;
206
+ if (!("meta" in obj)) return {
207
+ ...obj,
208
+ meta: {}
209
+ };
210
+ }
211
+ return data;
212
+ };
213
+ /** Parse a TOML string, returning Either<ConfigError, unknown> */
214
+ const parseToml = (raw) => Try(() => parse(raw)).fold((err) => Left({
215
+ _tag: "ParseError",
216
+ message: String(err)
217
+ }), (data) => Right(applyDefaults(normalizeDates(data))));
218
+ /** Validate parsed data against the TypeBox schema */
219
+ const validateConfig = (data) => {
220
+ if (compiledSchema.Check(data)) return Right(data);
221
+ return Left({
222
+ _tag: "ValidationError",
223
+ errors: List([...compiledSchema.Errors(data)].map((e) => `${e.path}: ${e.message}`))
224
+ });
225
+ };
226
+ /** Load and validate an envpkt.toml from a file path */
227
+ const loadConfig = (path) => readConfigFile(path).flatMap(parseToml).flatMap(validateConfig);
228
+ /**
229
+ * Resolve config path via priority chain:
230
+ * 1. Explicit flag path
231
+ * 2. ENVPKT_CONFIG env var
232
+ * 3. CWD discovery
233
+ */
234
+ const resolveConfigPath = (flagPath, envVar, cwd) => {
235
+ if (flagPath) {
236
+ const resolved = resolve(flagPath);
237
+ return existsSync(resolved) ? Right(resolved) : Left({
238
+ _tag: "FileNotFound",
239
+ path: resolved
240
+ });
241
+ }
242
+ const envPath = envVar ?? process.env[ENV_VAR_CONFIG];
243
+ if (envPath) {
244
+ const resolved = resolve(envPath);
245
+ return existsSync(resolved) ? Right(resolved) : Left({
246
+ _tag: "FileNotFound",
247
+ path: resolved
248
+ });
249
+ }
250
+ const dir = cwd ?? process.cwd();
251
+ return findConfigPath(dir).fold(() => Left({
252
+ _tag: "FileNotFound",
253
+ path: join(dir, CONFIG_FILENAME$2)
254
+ }), (path) => Right(path));
255
+ };
256
+
257
+ //#endregion
258
+ //#region src/core/catalog.ts
259
+ /** Load and validate a catalog file, mapping ConfigError → CatalogError */
260
+ const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
261
+ if (err._tag === "FileNotFound") return Left({
262
+ _tag: "CatalogNotFound",
263
+ path: err.path
264
+ });
265
+ return Left({
266
+ _tag: "CatalogLoadError",
267
+ message: `${err._tag}: ${"message" in err ? err.message : String(err)}`
268
+ });
269
+ }, (config) => Right(config));
270
+ /** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
271
+ const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
272
+ const resolved = {};
273
+ for (const key of agentSecrets) {
274
+ const catalogEntry = catalogMeta[key];
275
+ if (!catalogEntry) return Left({
276
+ _tag: "SecretNotInCatalog",
277
+ key,
278
+ catalogPath
279
+ });
280
+ const agentOverride = agentMeta[key];
281
+ if (agentOverride) resolved[key] = {
282
+ ...catalogEntry,
283
+ ...agentOverride
284
+ };
285
+ else resolved[key] = catalogEntry;
286
+ }
287
+ return Right(resolved);
288
+ };
289
+ /** Resolve an agent config against its catalog (if any), producing a flat self-contained config */
290
+ const resolveConfig = (agentConfig, agentConfigDir) => {
291
+ if (!agentConfig.catalog) return Right({
292
+ config: agentConfig,
293
+ merged: [],
294
+ overridden: [],
295
+ warnings: []
296
+ });
297
+ if (!agentConfig.agent?.secrets || agentConfig.agent.secrets.length === 0) return Left({
298
+ _tag: "MissingSecretsList",
299
+ message: "Config has 'catalog' but agent.secrets is missing — declare which catalog secrets this agent needs"
300
+ });
301
+ const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
302
+ const agentSecrets = agentConfig.agent.secrets;
303
+ return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentConfig.meta, catalogConfig.meta, agentSecrets, catalogPath).map((resolvedMeta) => {
304
+ const merged = [];
305
+ const overridden = [];
306
+ const warnings = [];
307
+ for (const key of agentSecrets) {
308
+ merged.push(key);
309
+ if (agentConfig.meta[key]) overridden.push(key);
310
+ }
311
+ const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
312
+ const agentIdentity = agentConfig.agent ? (() => {
313
+ const { secrets: _secrets, ...rest } = agentConfig.agent;
314
+ return rest;
315
+ })() : void 0;
316
+ return {
317
+ config: {
318
+ ...agentWithoutCatalog,
319
+ agent: agentIdentity ? {
320
+ ...agentIdentity,
321
+ name: agentIdentity.name
322
+ } : void 0,
323
+ meta: resolvedMeta
324
+ },
325
+ catalogPath,
326
+ merged,
327
+ overridden,
328
+ warnings
329
+ };
330
+ }));
331
+ };
332
+
333
+ //#endregion
334
+ //#region src/cli/output.ts
335
+ const RESET = "\x1B[0m";
336
+ const BOLD = "\x1B[1m";
337
+ const DIM = "\x1B[2m";
338
+ const RED = "\x1B[31m";
339
+ const GREEN = "\x1B[32m";
340
+ const YELLOW = "\x1B[33m";
341
+ const CYAN = "\x1B[36m";
342
+ const statusColor = (status) => {
343
+ switch (status) {
344
+ case "healthy": return GREEN;
345
+ case "degraded": return YELLOW;
346
+ case "critical": return RED;
347
+ }
348
+ };
349
+ const statusIcon$1 = (status) => {
350
+ switch (status) {
351
+ case "healthy": return `${GREEN}✓${RESET}`;
352
+ case "degraded": return `${YELLOW}⚠${RESET}`;
353
+ case "critical": return `${RED}✗${RESET}`;
354
+ }
355
+ };
356
+ const secretStatusIcon = (status) => {
357
+ switch (status) {
358
+ case "healthy": return `${GREEN}✓${RESET}`;
359
+ case "expiring_soon": return `${YELLOW}⚠${RESET}`;
360
+ case "expired": return `${RED}✗${RESET}`;
361
+ case "stale": return `${YELLOW}○${RESET}`;
362
+ case "missing": return `${RED}?${RESET}`;
363
+ case "missing_metadata": return `${YELLOW}!${RESET}`;
364
+ default: return " ";
365
+ }
366
+ };
367
+ const formatSecretRow = (secret) => {
368
+ const icon = secretStatusIcon(secret.status);
369
+ const days = secret.days_remaining.fold(() => "", (d) => `${d}d`);
370
+ const rotation = secret.rotation_url.fold(() => "", (url) => `${DIM}${url}${RESET}`);
371
+ const svc = secret.service.fold(() => secret.key, (s) => s);
372
+ return ` ${icon} ${BOLD}${secret.key}${RESET} ${DIM}(${svc})${RESET} ${secret.status} ${days} ${rotation}`.trimEnd();
373
+ };
374
+ const formatAudit = (audit) => {
375
+ const color = statusColor(audit.status);
376
+ return [
377
+ `${statusIcon$1(audit.status)} ${BOLD}${color}${audit.status.toUpperCase()}${RESET} — ${audit.total} secrets`,
378
+ [
379
+ ` ${GREEN}${audit.healthy}${RESET} healthy`,
380
+ audit.expiring_soon > 0 ? ` ${YELLOW}${audit.expiring_soon}${RESET} expiring soon` : null,
381
+ audit.expired > 0 ? ` ${RED}${audit.expired}${RESET} expired` : null,
382
+ audit.stale > 0 ? ` ${YELLOW}${audit.stale}${RESET} stale` : null,
383
+ audit.missing > 0 ? ` ${RED}${audit.missing}${RESET} missing` : null,
384
+ audit.missing_metadata > 0 ? ` ${YELLOW}${audit.missing_metadata}${RESET} missing metadata` : null,
385
+ audit.orphaned > 0 ? ` ${YELLOW}${audit.orphaned}${RESET} orphaned` : null
386
+ ].filter(Boolean).join("\n"),
387
+ audit.secrets.filter((s) => s.status !== "healthy").map(formatSecretRow).toArray().join("\n")
388
+ ].filter((s) => s.length > 0).join("\n\n");
389
+ };
390
+ const formatAuditJson = (audit) => JSON.stringify({
391
+ status: audit.status,
392
+ total: audit.total,
393
+ healthy: audit.healthy,
394
+ expiring_soon: audit.expiring_soon,
395
+ expired: audit.expired,
396
+ stale: audit.stale,
397
+ missing: audit.missing,
398
+ missing_metadata: audit.missing_metadata,
399
+ orphaned: audit.orphaned,
400
+ secrets: audit.secrets.map((s) => ({
401
+ key: s.key,
402
+ service: s.service.fold(() => null, (sv) => sv),
403
+ status: s.status,
404
+ days_remaining: s.days_remaining.fold(() => null, (d) => d),
405
+ rotation_url: s.rotation_url.fold(() => null, (u) => u),
406
+ purpose: s.purpose.fold(() => null, (p) => p),
407
+ issues: s.issues.toArray()
408
+ })).toArray()
409
+ }, null, 2);
410
+ const formatFleetJson = (fleet) => JSON.stringify({
411
+ status: fleet.status,
412
+ total_agents: fleet.total_agents,
413
+ total_secrets: fleet.total_secrets,
414
+ expired: fleet.expired,
415
+ expiring_soon: fleet.expiring_soon,
416
+ agents: fleet.agents.map((a) => ({
417
+ path: a.path,
418
+ name: a.agent?.name ?? null,
419
+ consumer: a.agent?.consumer ?? null,
420
+ description: a.agent?.description ?? null,
421
+ status: a.audit.status,
422
+ secrets: a.audit.total
423
+ })).toArray()
424
+ }, null, 2);
425
+ const formatError = (error) => {
426
+ const tag = error._tag;
427
+ switch (tag) {
428
+ case "FileNotFound": return `${RED}Error:${RESET} Config file not found: ${error.path}`;
429
+ case "ParseError": return `${RED}Error:${RESET} Failed to parse TOML: ${error.message}`;
430
+ case "ValidationError": return `${RED}Error:${RESET} Config validation failed:\n${String(error.errors)}`;
431
+ case "ReadError": return `${RED}Error:${RESET} Could not read file: ${error.message}`;
432
+ case "AgeNotFound": return `${RED}Error:${RESET} age CLI not found: ${error.message}`;
433
+ case "DecryptFailed": return `${RED}Error:${RESET} Decrypt failed: ${error.message}`;
434
+ case "IdentityNotFound": return `${RED}Error:${RESET} Identity file not found: ${error.path}`;
435
+ case "AuditFailed": return `${RED}Error:${RESET} Audit failed: ${error.message}`;
436
+ case "CatalogNotFound": return `${RED}Error:${RESET} Catalog not found: ${error.path}`;
437
+ case "CatalogLoadError": return `${RED}Error:${RESET} Catalog load error: ${error.message}`;
438
+ case "SecretNotInCatalog": return `${RED}Error:${RESET} Secret "${error.key}" not found in catalog: ${error.path}`;
439
+ case "MissingSecretsList": return `${RED}Error:${RESET} ${error.message}`;
440
+ default: return `${RED}Error:${RESET} ${error.message ?? tag}`;
441
+ }
442
+ };
443
+ const exitCodeForAudit = (audit) => {
444
+ switch (audit.status) {
445
+ case "healthy": return 0;
446
+ case "degraded": return 1;
447
+ case "critical": return 2;
448
+ }
449
+ };
450
+ const confidenceIcon = (confidence) => {
451
+ switch (confidence) {
452
+ case "high": return `${GREEN}●${RESET}`;
453
+ case "medium": return `${YELLOW}◐${RESET}`;
454
+ case "low": return `${DIM}○${RESET}`;
455
+ }
456
+ };
457
+ const formatScanTable = (scan) => {
458
+ return [
459
+ `${BOLD}Scan Results${RESET} — ${scan.discovered.size} credential(s) found in ${scan.total_scanned} env vars`,
460
+ [
461
+ scan.high_confidence > 0 ? ` ${GREEN}${scan.high_confidence}${RESET} high confidence` : null,
462
+ scan.medium_confidence > 0 ? ` ${YELLOW}${scan.medium_confidence}${RESET} medium confidence` : null,
463
+ scan.low_confidence > 0 ? ` ${DIM}${scan.low_confidence}${RESET} low confidence` : null
464
+ ].filter(Boolean).join("\n"),
465
+ scan.discovered.map((m) => {
466
+ const icon = confidenceIcon(m.confidence);
467
+ const svc = m.service.fold(() => `${DIM}unknown${RESET}`, (s) => s);
468
+ return ` ${icon} ${BOLD}${m.envVar}${RESET} → ${CYAN}${svc}${RESET} ${DIM}(${m.matchedBy})${RESET}`;
469
+ }).toArray().join("\n")
470
+ ].filter((s) => s.length > 0).join("\n\n");
471
+ };
472
+ const formatScanJson = (scan) => JSON.stringify({
473
+ total_scanned: scan.total_scanned,
474
+ discovered: scan.discovered.size,
475
+ high_confidence: scan.high_confidence,
476
+ medium_confidence: scan.medium_confidence,
477
+ low_confidence: scan.low_confidence,
478
+ matches: scan.discovered.map((m) => ({
479
+ envVar: m.envVar,
480
+ service: m.service.fold(() => null, (s) => s),
481
+ confidence: m.confidence,
482
+ matchedBy: m.matchedBy
483
+ })).toArray()
484
+ }, null, 2);
485
+ const driftIcon = (status) => {
486
+ switch (status) {
487
+ case "tracked": return `${GREEN}✓${RESET}`;
488
+ case "missing_from_env": return `${RED}✗${RESET}`;
489
+ case "untracked": return `${YELLOW}?${RESET}`;
490
+ default: return " ";
491
+ }
492
+ };
493
+ const formatCheckTable = (check) => {
494
+ const clean = check.is_clean;
495
+ return [
496
+ `${clean ? `${GREEN}✓${RESET}` : `${YELLOW}⚠${RESET}`} ${BOLD}${clean ? `${GREEN}CLEAN${RESET}` : `${YELLOW}DRIFT DETECTED${RESET}`}${RESET}`,
497
+ [
498
+ ` ${GREEN}${check.tracked_and_present}${RESET} tracked and present`,
499
+ check.missing_from_env > 0 ? ` ${RED}${check.missing_from_env}${RESET} missing from env` : null,
500
+ check.untracked_credentials > 0 ? ` ${YELLOW}${check.untracked_credentials}${RESET} untracked credentials` : null
501
+ ].filter(Boolean).join("\n"),
502
+ check.entries.filter((e) => e.status !== "tracked").map((e) => {
503
+ const di = driftIcon(e.status);
504
+ const svc = e.service.fold(() => "", (s) => ` ${DIM}(${s})${RESET}`);
505
+ const conf = e.confidence.fold(() => "", (c) => ` ${confidenceIcon(c)}`);
506
+ return ` ${di} ${BOLD}${e.envVar}${RESET}${svc} ${e.status}${conf}`;
507
+ }).toArray().join("\n")
508
+ ].filter((s) => s.length > 0).join("\n\n");
509
+ };
510
+ const formatCheckJson = (check) => JSON.stringify({
511
+ is_clean: check.is_clean,
512
+ tracked_and_present: check.tracked_and_present,
513
+ missing_from_env: check.missing_from_env,
514
+ untracked_credentials: check.untracked_credentials,
515
+ entries: check.entries.map((e) => ({
516
+ envVar: e.envVar,
517
+ service: e.service.fold(() => null, (s) => s),
518
+ status: e.status,
519
+ confidence: e.confidence.fold(() => null, (c) => c)
520
+ })).toArray()
521
+ }, null, 2);
522
+ const formatAuditMinimal = (audit) => {
523
+ if (audit.status === "healthy") return `${GREEN}✓${RESET} ${audit.total} secrets healthy`;
524
+ const parts = [];
525
+ if (audit.expired > 0) parts.push(`${audit.expired} expired`);
526
+ if (audit.expiring_soon > 0) parts.push(`${audit.expiring_soon} expiring`);
527
+ if (audit.stale > 0) parts.push(`${audit.stale} stale`);
528
+ if (audit.missing > 0) parts.push(`${audit.missing} missing`);
529
+ return `${audit.status === "critical" ? `${RED}✗${RESET}` : `${YELLOW}⚠${RESET}`} ${parts.join(", ")}`;
530
+ };
531
+
532
+ //#endregion
533
+ //#region src/cli/commands/audit.ts
534
+ const runAudit = (options) => {
535
+ resolveConfigPath(options.config).fold((err) => {
536
+ console.error(formatError(err));
537
+ process.exit(2);
538
+ }, (path) => {
539
+ loadConfig(path).fold((err) => {
540
+ console.error(formatError(err));
541
+ process.exit(2);
542
+ }, (rawConfig) => {
543
+ resolveConfig(rawConfig, dirname(path)).fold((err) => {
544
+ console.error(formatError(err));
545
+ process.exit(2);
546
+ }, (resolveResult) => {
547
+ if (resolveResult.catalogPath) console.log(`${DIM}Catalog: ${CYAN}${resolveResult.catalogPath}${RESET}`);
548
+ runAuditOnConfig(resolveResult.config, options);
549
+ });
550
+ });
551
+ });
552
+ };
553
+ const runAuditOnConfig = (config, options) => {
554
+ const audit = computeAudit(config);
555
+ let filtered = audit;
556
+ if (options.status) {
557
+ const statusFilter = options.status;
558
+ const filteredSecrets = audit.secrets.filter((s) => s.status === statusFilter);
559
+ filtered = {
560
+ ...audit,
561
+ secrets: filteredSecrets
562
+ };
563
+ }
564
+ if (options.expiring !== void 0) {
565
+ const days = options.expiring;
566
+ const filteredSecrets = filtered.secrets.filter((s) => s.days_remaining.fold(() => false, (d) => d >= 0 && d <= days));
567
+ filtered = {
568
+ ...filtered,
569
+ secrets: filteredSecrets
570
+ };
571
+ }
572
+ if (options.format === "json") console.log(formatAuditJson(filtered));
573
+ else if (options.format === "minimal") console.log(formatAuditMinimal(filtered));
574
+ else console.log(formatAudit(filtered));
575
+ const code = options.strict ? exitCodeForAudit(audit) : audit.status === "critical" ? 2 : 0;
576
+ process.exit(code);
577
+ };
578
+
579
+ //#endregion
580
+ //#region src/core/patterns.ts
581
+ const EXCLUDED_VARS = new Set([
582
+ "PATH",
583
+ "HOME",
584
+ "USER",
585
+ "SHELL",
586
+ "TERM",
587
+ "LANG",
588
+ "LC_ALL",
589
+ "LC_CTYPE",
590
+ "DISPLAY",
591
+ "EDITOR",
592
+ "VISUAL",
593
+ "PAGER",
594
+ "HOSTNAME",
595
+ "LOGNAME",
596
+ "MAIL",
597
+ "OLDPWD",
598
+ "PWD",
599
+ "SHLVL",
600
+ "TMPDIR",
601
+ "TZ",
602
+ "XDG_CACHE_HOME",
603
+ "XDG_CONFIG_HOME",
604
+ "XDG_DATA_HOME",
605
+ "XDG_RUNTIME_DIR",
606
+ "XDG_SESSION_TYPE",
607
+ "NODE_ENV",
608
+ "NODE_PATH",
609
+ "NODE_OPTIONS",
610
+ "NVM_DIR",
611
+ "NVM_BIN",
612
+ "NVM_INC",
613
+ "NVM_CD_FLAGS",
614
+ "NPM_CONFIG_PREFIX",
615
+ "GOPATH",
616
+ "GOROOT",
617
+ "CARGO_HOME",
618
+ "RUSTUP_HOME",
619
+ "JAVA_HOME",
620
+ "ANDROID_HOME",
621
+ "PYENV_ROOT",
622
+ "VIRTUAL_ENV",
623
+ "CONDA_PREFIX",
624
+ "CONDA_DEFAULT_ENV",
625
+ "MANPATH",
626
+ "INFOPATH",
627
+ "LESS",
628
+ "LSCOLORS",
629
+ "LS_COLORS",
630
+ "COLORTERM",
631
+ "TERM_PROGRAM",
632
+ "TERM_PROGRAM_VERSION",
633
+ "TERM_SESSION_ID",
634
+ "ITERM_SESSION_ID",
635
+ "ITERM_PROFILE",
636
+ "SSH_AUTH_SOCK",
637
+ "SSH_AGENT_PID",
638
+ "GPG_TTY",
639
+ "GNUPGHOME",
640
+ "DBUS_SESSION_BUS_ADDRESS",
641
+ "WAYLAND_DISPLAY",
642
+ "ZDOTDIR",
643
+ "ZSH",
644
+ "HISTFILE",
645
+ "HISTSIZE",
646
+ "SAVEHIST",
647
+ "_",
648
+ "__CF_USER_TEXT_ENCODING",
649
+ "Apple_PubSub_Socket_Render",
650
+ "COMMAND_MODE",
651
+ "SECURITYSESSIONID",
652
+ "LaunchInstanceID",
653
+ "PNPM_HOME",
654
+ "BUN_INSTALL",
655
+ "FNM_DIR",
656
+ "FNM_MULTISHELL_PATH",
657
+ "FNM_VERSION_FILE_STRATEGY",
658
+ "FNM_LOGLEVEL",
659
+ "FNM_NODE_DIST_MIRROR",
660
+ "FNM_ARCH",
661
+ "VOLTA_HOME"
662
+ ]);
663
+ const EXACT_NAME_PATTERNS = [
664
+ {
665
+ kind: "name",
666
+ pattern: "OPENAI_API_KEY",
667
+ service: "openai",
668
+ confidence: "high",
669
+ description: "OpenAI API key"
670
+ },
671
+ {
672
+ kind: "name",
673
+ pattern: "OPENAI_ORG_ID",
674
+ service: "openai",
675
+ confidence: "high",
676
+ description: "OpenAI org ID"
677
+ },
678
+ {
679
+ kind: "name",
680
+ pattern: "ANTHROPIC_API_KEY",
681
+ service: "anthropic",
682
+ confidence: "high",
683
+ description: "Anthropic API key"
684
+ },
685
+ {
686
+ kind: "name",
687
+ pattern: "AWS_ACCESS_KEY_ID",
688
+ service: "aws",
689
+ confidence: "high",
690
+ description: "AWS access key ID"
691
+ },
692
+ {
693
+ kind: "name",
694
+ pattern: "AWS_SECRET_ACCESS_KEY",
695
+ service: "aws",
696
+ confidence: "high",
697
+ description: "AWS secret access key"
698
+ },
699
+ {
700
+ kind: "name",
701
+ pattern: "AWS_SESSION_TOKEN",
702
+ service: "aws",
703
+ confidence: "high",
704
+ description: "AWS session token"
705
+ },
706
+ {
707
+ kind: "name",
708
+ pattern: "GOOGLE_APPLICATION_CREDENTIALS",
709
+ service: "gcp",
710
+ confidence: "high",
711
+ description: "Google Cloud service account path"
712
+ },
713
+ {
714
+ kind: "name",
715
+ pattern: "GOOGLE_API_KEY",
716
+ service: "google",
717
+ confidence: "high",
718
+ description: "Google API key"
719
+ },
720
+ {
721
+ kind: "name",
722
+ pattern: "GCP_PROJECT_ID",
723
+ service: "gcp",
724
+ confidence: "medium",
725
+ description: "GCP project ID"
726
+ },
727
+ {
728
+ kind: "name",
729
+ pattern: "AZURE_CLIENT_ID",
730
+ service: "azure",
731
+ confidence: "high",
732
+ description: "Azure client ID"
733
+ },
734
+ {
735
+ kind: "name",
736
+ pattern: "AZURE_CLIENT_SECRET",
737
+ service: "azure",
738
+ confidence: "high",
739
+ description: "Azure client secret"
740
+ },
741
+ {
742
+ kind: "name",
743
+ pattern: "AZURE_TENANT_ID",
744
+ service: "azure",
745
+ confidence: "high",
746
+ description: "Azure tenant ID"
747
+ },
748
+ {
749
+ kind: "name",
750
+ pattern: "STRIPE_SECRET_KEY",
751
+ service: "stripe",
752
+ confidence: "high",
753
+ description: "Stripe secret key"
754
+ },
755
+ {
756
+ kind: "name",
757
+ pattern: "STRIPE_PUBLISHABLE_KEY",
758
+ service: "stripe",
759
+ confidence: "high",
760
+ description: "Stripe publishable key"
761
+ },
762
+ {
763
+ kind: "name",
764
+ pattern: "STRIPE_WEBHOOK_SECRET",
765
+ service: "stripe",
766
+ confidence: "high",
767
+ description: "Stripe webhook secret"
768
+ },
769
+ {
770
+ kind: "name",
771
+ pattern: "GITHUB_TOKEN",
772
+ service: "github",
773
+ confidence: "high",
774
+ description: "GitHub token"
775
+ },
776
+ {
777
+ kind: "name",
778
+ pattern: "GH_TOKEN",
779
+ service: "github",
780
+ confidence: "high",
781
+ description: "GitHub token (gh CLI)"
782
+ },
783
+ {
784
+ kind: "name",
785
+ pattern: "SLACK_BOT_TOKEN",
786
+ service: "slack",
787
+ confidence: "high",
788
+ description: "Slack bot token"
789
+ },
790
+ {
791
+ kind: "name",
792
+ pattern: "SLACK_SIGNING_SECRET",
793
+ service: "slack",
794
+ confidence: "high",
795
+ description: "Slack signing secret"
796
+ },
797
+ {
798
+ kind: "name",
799
+ pattern: "SLACK_WEBHOOK_URL",
800
+ service: "slack",
801
+ confidence: "high",
802
+ description: "Slack webhook URL"
803
+ },
804
+ {
805
+ kind: "name",
806
+ pattern: "TWILIO_ACCOUNT_SID",
807
+ service: "twilio",
808
+ confidence: "high",
809
+ description: "Twilio account SID"
810
+ },
811
+ {
812
+ kind: "name",
813
+ pattern: "TWILIO_AUTH_TOKEN",
814
+ service: "twilio",
815
+ confidence: "high",
816
+ description: "Twilio auth token"
817
+ },
818
+ {
819
+ kind: "name",
820
+ pattern: "SENDGRID_API_KEY",
821
+ service: "sendgrid",
822
+ confidence: "high",
823
+ description: "SendGrid API key"
824
+ },
825
+ {
826
+ kind: "name",
827
+ pattern: "SUPABASE_URL",
828
+ service: "supabase",
829
+ confidence: "high",
830
+ description: "Supabase project URL"
831
+ },
832
+ {
833
+ kind: "name",
834
+ pattern: "SUPABASE_ANON_KEY",
835
+ service: "supabase",
836
+ confidence: "high",
837
+ description: "Supabase anon key"
838
+ },
839
+ {
840
+ kind: "name",
841
+ pattern: "SUPABASE_SERVICE_ROLE_KEY",
842
+ service: "supabase",
843
+ confidence: "high",
844
+ description: "Supabase service role key"
845
+ },
846
+ {
847
+ kind: "name",
848
+ pattern: "DATABASE_URL",
849
+ service: "database",
850
+ confidence: "high",
851
+ description: "Database URL"
852
+ },
853
+ {
854
+ kind: "name",
855
+ pattern: "DATABASE_PASSWORD",
856
+ service: "database",
857
+ confidence: "high",
858
+ description: "Database password"
859
+ },
860
+ {
861
+ kind: "name",
862
+ pattern: "REDIS_URL",
863
+ service: "redis",
864
+ confidence: "high",
865
+ description: "Redis URL"
866
+ },
867
+ {
868
+ kind: "name",
869
+ pattern: "MONGODB_URI",
870
+ service: "mongodb",
871
+ confidence: "high",
872
+ description: "MongoDB URI"
873
+ },
874
+ {
875
+ kind: "name",
876
+ pattern: "DD_API_KEY",
877
+ service: "datadog",
878
+ confidence: "high",
879
+ description: "Datadog API key"
880
+ },
881
+ {
882
+ kind: "name",
883
+ pattern: "DD_APP_KEY",
884
+ service: "datadog",
885
+ confidence: "high",
886
+ description: "Datadog app key"
887
+ },
888
+ {
889
+ kind: "name",
890
+ pattern: "SENTRY_DSN",
891
+ service: "sentry",
892
+ confidence: "high",
893
+ description: "Sentry DSN"
894
+ },
895
+ {
896
+ kind: "name",
897
+ pattern: "SENTRY_AUTH_TOKEN",
898
+ service: "sentry",
899
+ confidence: "high",
900
+ description: "Sentry auth token"
901
+ },
902
+ {
903
+ kind: "name",
904
+ pattern: "VERCEL_TOKEN",
905
+ service: "vercel",
906
+ confidence: "high",
907
+ description: "Vercel token"
908
+ },
909
+ {
910
+ kind: "name",
911
+ pattern: "NETLIFY_AUTH_TOKEN",
912
+ service: "netlify",
913
+ confidence: "high",
914
+ description: "Netlify auth token"
915
+ },
916
+ {
917
+ kind: "name",
918
+ pattern: "CLOUDFLARE_API_TOKEN",
919
+ service: "cloudflare",
920
+ confidence: "high",
921
+ description: "Cloudflare API token"
922
+ },
923
+ {
924
+ kind: "name",
925
+ pattern: "CF_API_TOKEN",
926
+ service: "cloudflare",
927
+ confidence: "high",
928
+ description: "Cloudflare API token"
929
+ },
930
+ {
931
+ kind: "name",
932
+ pattern: "DOCKER_PASSWORD",
933
+ service: "docker",
934
+ confidence: "high",
935
+ description: "Docker password"
936
+ },
937
+ {
938
+ kind: "name",
939
+ pattern: "DOCKER_TOKEN",
940
+ service: "docker",
941
+ confidence: "high",
942
+ description: "Docker token"
943
+ },
944
+ {
945
+ kind: "name",
946
+ pattern: "NPM_TOKEN",
947
+ service: "npm",
948
+ confidence: "high",
949
+ description: "npm token"
950
+ },
951
+ {
952
+ kind: "name",
953
+ pattern: "HF_TOKEN",
954
+ service: "huggingface",
955
+ confidence: "high",
956
+ description: "Hugging Face token"
957
+ },
958
+ {
959
+ kind: "name",
960
+ pattern: "HUGGING_FACE_HUB_TOKEN",
961
+ service: "huggingface",
962
+ confidence: "high",
963
+ description: "Hugging Face Hub token"
964
+ },
965
+ {
966
+ kind: "name",
967
+ pattern: "COHERE_API_KEY",
968
+ service: "cohere",
969
+ confidence: "high",
970
+ description: "Cohere API key"
971
+ },
972
+ {
973
+ kind: "name",
974
+ pattern: "REPLICATE_API_TOKEN",
975
+ service: "replicate",
976
+ confidence: "high",
977
+ description: "Replicate API token"
978
+ },
979
+ {
980
+ kind: "name",
981
+ pattern: "PINECONE_API_KEY",
982
+ service: "pinecone",
983
+ confidence: "high",
984
+ description: "Pinecone API key"
985
+ },
986
+ {
987
+ kind: "name",
988
+ pattern: "LINEAR_API_KEY",
989
+ service: "linear",
990
+ confidence: "high",
991
+ description: "Linear API key"
992
+ }
993
+ ];
994
+ const SUFFIX_PATTERNS = [
995
+ {
996
+ suffix: "_API_KEY",
997
+ description: "API key"
998
+ },
999
+ {
1000
+ suffix: "_SECRET_KEY",
1001
+ description: "Secret key"
1002
+ },
1003
+ {
1004
+ suffix: "_SECRET",
1005
+ description: "Secret"
1006
+ },
1007
+ {
1008
+ suffix: "_TOKEN",
1009
+ description: "Token"
1010
+ },
1011
+ {
1012
+ suffix: "_PASSWORD",
1013
+ description: "Password"
1014
+ },
1015
+ {
1016
+ suffix: "_PASS",
1017
+ description: "Password"
1018
+ },
1019
+ {
1020
+ suffix: "_AUTH_TOKEN",
1021
+ description: "Auth token"
1022
+ },
1023
+ {
1024
+ suffix: "_ACCESS_TOKEN",
1025
+ description: "Access token"
1026
+ },
1027
+ {
1028
+ suffix: "_PRIVATE_KEY",
1029
+ description: "Private key"
1030
+ },
1031
+ {
1032
+ suffix: "_SIGNING_KEY",
1033
+ description: "Signing key"
1034
+ },
1035
+ {
1036
+ suffix: "_WEBHOOK_SECRET",
1037
+ description: "Webhook secret"
1038
+ },
1039
+ {
1040
+ suffix: "_DSN",
1041
+ description: "DSN"
1042
+ },
1043
+ {
1044
+ suffix: "_CONNECTION_STRING",
1045
+ description: "Connection string"
1046
+ }
1047
+ ];
1048
+ const VALUE_SHAPE_PATTERNS = [
1049
+ {
1050
+ prefix: "sk-ant-",
1051
+ service: "anthropic",
1052
+ description: "Anthropic API key"
1053
+ },
1054
+ {
1055
+ prefix: "sk-",
1056
+ service: "openai",
1057
+ description: "OpenAI API key"
1058
+ },
1059
+ {
1060
+ prefix: "sk_live_",
1061
+ service: "stripe",
1062
+ description: "Stripe live secret key"
1063
+ },
1064
+ {
1065
+ prefix: "sk_test_",
1066
+ service: "stripe",
1067
+ description: "Stripe test secret key"
1068
+ },
1069
+ {
1070
+ prefix: "pk_live_",
1071
+ service: "stripe",
1072
+ description: "Stripe live publishable key"
1073
+ },
1074
+ {
1075
+ prefix: "pk_test_",
1076
+ service: "stripe",
1077
+ description: "Stripe test publishable key"
1078
+ },
1079
+ {
1080
+ prefix: "whsec_",
1081
+ service: "stripe",
1082
+ description: "Stripe webhook secret"
1083
+ },
1084
+ {
1085
+ prefix: "AKIA",
1086
+ service: "aws",
1087
+ description: "AWS access key ID"
1088
+ },
1089
+ {
1090
+ prefix: "ghp_",
1091
+ service: "github",
1092
+ description: "GitHub personal access token"
1093
+ },
1094
+ {
1095
+ prefix: "gho_",
1096
+ service: "github",
1097
+ description: "GitHub OAuth token"
1098
+ },
1099
+ {
1100
+ prefix: "ghs_",
1101
+ service: "github",
1102
+ description: "GitHub server-to-server token"
1103
+ },
1104
+ {
1105
+ prefix: "ghu_",
1106
+ service: "github",
1107
+ description: "GitHub user-to-server token"
1108
+ },
1109
+ {
1110
+ prefix: "github_pat_",
1111
+ service: "github",
1112
+ description: "GitHub fine-grained PAT"
1113
+ },
1114
+ {
1115
+ prefix: "xoxb-",
1116
+ service: "slack",
1117
+ description: "Slack bot token"
1118
+ },
1119
+ {
1120
+ prefix: "xoxp-",
1121
+ service: "slack",
1122
+ description: "Slack user token"
1123
+ },
1124
+ {
1125
+ prefix: "xoxa-",
1126
+ service: "slack",
1127
+ description: "Slack app token"
1128
+ },
1129
+ {
1130
+ prefix: "xoxs-",
1131
+ service: "slack",
1132
+ description: "Slack legacy token"
1133
+ },
1134
+ {
1135
+ prefix: "SG.",
1136
+ service: "sendgrid",
1137
+ description: "SendGrid API key"
1138
+ },
1139
+ {
1140
+ prefix: "hf_",
1141
+ service: "huggingface",
1142
+ description: "Hugging Face token"
1143
+ },
1144
+ {
1145
+ prefix: "r8_",
1146
+ service: "replicate",
1147
+ description: "Replicate API token"
1148
+ },
1149
+ {
1150
+ prefix: "eyJ",
1151
+ service: "jwt",
1152
+ description: "JWT token"
1153
+ },
1154
+ {
1155
+ prefix: "postgres://",
1156
+ service: "postgresql",
1157
+ description: "PostgreSQL connection string"
1158
+ },
1159
+ {
1160
+ prefix: "postgresql://",
1161
+ service: "postgresql",
1162
+ description: "PostgreSQL connection string"
1163
+ },
1164
+ {
1165
+ prefix: "mysql://",
1166
+ service: "mysql",
1167
+ description: "MySQL connection string"
1168
+ },
1169
+ {
1170
+ prefix: "mongodb://",
1171
+ service: "mongodb",
1172
+ description: "MongoDB connection string"
1173
+ },
1174
+ {
1175
+ prefix: "mongodb+srv://",
1176
+ service: "mongodb",
1177
+ description: "MongoDB SRV connection string"
1178
+ },
1179
+ {
1180
+ prefix: "redis://",
1181
+ service: "redis",
1182
+ description: "Redis connection string"
1183
+ },
1184
+ {
1185
+ prefix: "rediss://",
1186
+ service: "redis",
1187
+ description: "Redis TLS connection string"
1188
+ },
1189
+ {
1190
+ prefix: "amqp://",
1191
+ service: "rabbitmq",
1192
+ description: "RabbitMQ connection string"
1193
+ },
1194
+ {
1195
+ prefix: "amqps://",
1196
+ service: "rabbitmq",
1197
+ description: "RabbitMQ TLS connection string"
1198
+ }
1199
+ ];
1200
+ /** Detect service from value prefix/shape */
1201
+ const matchValueShape = (value) => {
1202
+ for (const vp of VALUE_SHAPE_PATTERNS) if (value.startsWith(vp.prefix)) return Option({
1203
+ service: vp.service,
1204
+ description: vp.description
1205
+ });
1206
+ return Option(void 0);
1207
+ };
1208
+ /** Strip common suffixes and derive a service name from an env var name */
1209
+ const deriveServiceFromName = (name) => {
1210
+ const suffixes = [
1211
+ "_API_KEY",
1212
+ "_SECRET_KEY",
1213
+ "_ACCESS_KEY",
1214
+ "_PRIVATE_KEY",
1215
+ "_SIGNING_KEY",
1216
+ "_AUTH_TOKEN",
1217
+ "_ACCESS_TOKEN",
1218
+ "_WEBHOOK_SECRET",
1219
+ "_CONNECTION_STRING",
1220
+ "_SECRET",
1221
+ "_TOKEN",
1222
+ "_PASSWORD",
1223
+ "_PASS",
1224
+ "_KEY",
1225
+ "_DSN",
1226
+ "_URL",
1227
+ "_URI"
1228
+ ];
1229
+ let stripped = name;
1230
+ for (const suffix of suffixes) if (stripped.endsWith(suffix)) {
1231
+ stripped = stripped.slice(0, -suffix.length);
1232
+ break;
1233
+ }
1234
+ return stripped.toLowerCase().replace(/_/g, "-");
1235
+ };
1236
+ /** Match a single env var against all patterns */
1237
+ const matchEnvVar = (name, value) => {
1238
+ if (EXCLUDED_VARS.has(name)) return Option(void 0);
1239
+ for (const p of EXACT_NAME_PATTERNS) if (name === p.pattern) return Option({
1240
+ envVar: name,
1241
+ value,
1242
+ service: Option(p.service),
1243
+ confidence: p.confidence,
1244
+ matchedBy: `exact:${p.pattern}`
1245
+ });
1246
+ return matchValueShape(value).fold(() => {
1247
+ for (const sp of SUFFIX_PATTERNS) if (name.endsWith(sp.suffix)) return Option({
1248
+ envVar: name,
1249
+ value,
1250
+ service: Option(deriveServiceFromName(name)),
1251
+ confidence: "medium",
1252
+ matchedBy: `suffix:${sp.suffix}`
1253
+ });
1254
+ return Option(void 0);
1255
+ }, (vm) => Option({
1256
+ envVar: name,
1257
+ value,
1258
+ service: Option(vm.service),
1259
+ confidence: "high",
1260
+ matchedBy: `value:${vm.description}`
1261
+ }));
1262
+ };
1263
+ /** Scan full env, sorted by confidence (high first) then alphabetically */
1264
+ const scanEnv = (env) => {
1265
+ const results = [];
1266
+ for (const [name, value] of Object.entries(env)) {
1267
+ if (value === void 0 || value === "") continue;
1268
+ matchEnvVar(name, value).fold(() => {}, (m) => results.push(m));
1269
+ }
1270
+ const confidenceOrder = {
1271
+ high: 0,
1272
+ medium: 1,
1273
+ low: 2
1274
+ };
1275
+ results.sort((a, b) => {
1276
+ const conf = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
1277
+ if (conf !== 0) return conf;
1278
+ return a.envVar.localeCompare(b.envVar);
1279
+ });
1280
+ return results;
1281
+ };
1282
+
1283
+ //#endregion
1284
+ //#region src/core/env.ts
1285
+ /** Scan env for credentials, returning structured results */
1286
+ const envScan = (env, options) => {
1287
+ const allMatches = scanEnv(env);
1288
+ const discovered = options?.includeUnknown ? allMatches : allMatches.filter((m) => m.service.isSome());
1289
+ const total_scanned = Object.keys(env).length;
1290
+ const high_confidence = discovered.filter((m) => m.confidence === "high").length;
1291
+ const medium_confidence = discovered.filter((m) => m.confidence === "medium").length;
1292
+ const low_confidence = discovered.filter((m) => m.confidence === "low").length;
1293
+ return {
1294
+ discovered: List(discovered),
1295
+ total_scanned,
1296
+ high_confidence,
1297
+ medium_confidence,
1298
+ low_confidence
1299
+ };
1300
+ };
1301
+ /** Bidirectional drift detection between config and live environment */
1302
+ const envCheck = (config, env) => {
1303
+ const entries = [];
1304
+ const metaKeys = Object.keys(config.meta);
1305
+ const trackedSet = new Set(metaKeys);
1306
+ for (const key of metaKeys) {
1307
+ const meta = config.meta[key];
1308
+ const present = env[key] !== void 0 && env[key] !== "";
1309
+ entries.push({
1310
+ envVar: key,
1311
+ service: Option(meta?.service),
1312
+ status: present ? "tracked" : "missing_from_env",
1313
+ confidence: Option(void 0)
1314
+ });
1315
+ }
1316
+ const envMatches = scanEnv(env);
1317
+ for (const match of envMatches) if (!trackedSet.has(match.envVar)) entries.push({
1318
+ envVar: match.envVar,
1319
+ service: match.service,
1320
+ status: "untracked",
1321
+ confidence: Option(match.confidence)
1322
+ });
1323
+ const tracked_and_present = entries.filter((e) => e.status === "tracked").length;
1324
+ const missing_from_env = entries.filter((e) => e.status === "missing_from_env").length;
1325
+ const untracked_credentials = entries.filter((e) => e.status === "untracked").length;
1326
+ return {
1327
+ entries: List(entries),
1328
+ tracked_and_present,
1329
+ missing_from_env,
1330
+ untracked_credentials,
1331
+ is_clean: missing_from_env === 0 && untracked_credentials === 0
1332
+ };
1333
+ };
1334
+ const todayIso$1 = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1335
+ /** Generate TOML [meta.*] blocks from scan results, mirroring init.ts pattern */
1336
+ const generateTomlFromScan = (matches) => {
1337
+ const blocks = [];
1338
+ for (const match of matches) {
1339
+ const svc = match.service.fold(() => match.envVar.toLowerCase().replace(/_/g, "-"), (s) => s);
1340
+ blocks.push(`[meta.${match.envVar}]
1341
+ service = "${svc}"
1342
+ # purpose = "" # Why: what this secret enables
1343
+ # capabilities = [] # What operations this grants
1344
+ created = "${todayIso$1()}"
1345
+ # expires = "" # When: YYYY-MM-DD expiration date
1346
+ # rotation_url = "" # URL for rotation procedure
1347
+ # source = "" # Where the value originates (e.g. vault, ci)
1348
+ # tags = {}
1349
+ `);
1350
+ }
1351
+ return blocks.join("\n");
1352
+ };
1353
+
1354
+ //#endregion
1355
+ //#region src/cli/commands/env.ts
1356
+ const runEnvScan = (options) => {
1357
+ const scan = envScan(process.env, { includeUnknown: options.includeUnknown });
1358
+ if (scan.discovered.size === 0) {
1359
+ console.log(`${DIM}No credentials detected in environment.${RESET}`);
1360
+ process.exit(0);
1361
+ }
1362
+ if (options.format === "json") console.log(formatScanJson(scan));
1363
+ else console.log(formatScanTable(scan));
1364
+ if (options.write || options.dryRun) {
1365
+ const toml = generateTomlFromScan(scan.discovered.toArray());
1366
+ if (options.dryRun) {
1367
+ console.log(`\n${BOLD}Preview (--dry-run):${RESET}\n`);
1368
+ console.log(toml);
1369
+ return;
1370
+ }
1371
+ const configPath = join(process.cwd(), "envpkt.toml");
1372
+ if (existsSync(configPath)) {
1373
+ const existing = Try(() => readFileSync(configPath, "utf-8")).fold(() => "", (c) => c);
1374
+ const newEntries = scan.discovered.toArray().filter((m) => !existing.includes(`[meta.${m.envVar}]`));
1375
+ if (newEntries.length === 0) {
1376
+ console.log(`\n${GREEN}✓${RESET} All discovered credentials already tracked in envpkt.toml`);
1377
+ return;
1378
+ }
1379
+ const newToml = generateTomlFromScan(newEntries);
1380
+ Try(() => writeFileSync(configPath, existing.trimEnd() + "\n\n" + newToml, "utf-8")).fold((err) => {
1381
+ console.error(`\n${RED}Error:${RESET} Failed to write: ${err}`);
1382
+ process.exit(1);
1383
+ }, () => {
1384
+ console.log(`\n${GREEN}✓${RESET} Appended ${BOLD}${newEntries.length}${RESET} new entry/entries to ${CYAN}${configPath}${RESET}`);
1385
+ });
1386
+ } else {
1387
+ const header = `#:schema https://raw.githubusercontent.com/jordanburke/envpkt/main/schemas/envpkt.schema.json\n\nversion = 1\n\n[lifecycle]\nstale_warning_days = 90\n\n`;
1388
+ Try(() => writeFileSync(configPath, header + toml, "utf-8")).fold((err) => {
1389
+ console.error(`\n${RED}Error:${RESET} Failed to write: ${err}`);
1390
+ process.exit(1);
1391
+ }, () => {
1392
+ console.log(`\n${GREEN}✓${RESET} Created ${BOLD}envpkt.toml${RESET} with ${CYAN}${scan.discovered.size}${RESET} credential(s)`);
1393
+ });
1394
+ }
1395
+ }
1396
+ };
1397
+ const runEnvCheck = (options) => {
1398
+ resolveConfigPath(options.config).fold((err) => {
1399
+ console.error(formatError(err));
1400
+ process.exit(2);
1401
+ }, (path) => {
1402
+ loadConfig(path).fold((err) => {
1403
+ console.error(formatError(err));
1404
+ process.exit(2);
1405
+ }, (rawConfig) => {
1406
+ resolveConfig(rawConfig, dirname(path)).fold((err) => {
1407
+ console.error(formatError(err));
1408
+ process.exit(2);
1409
+ }, (resolveResult) => {
1410
+ const check = envCheck(resolveResult.config, process.env);
1411
+ if (options.format === "json") console.log(formatCheckJson(check));
1412
+ else console.log(formatCheckTable(check));
1413
+ if (options.strict && !check.is_clean) process.exit(1);
1414
+ });
1415
+ });
1416
+ });
1417
+ };
1418
+
1419
+ //#endregion
1420
+ //#region src/fnox/detect.ts
1421
+ /** Check if fnox CLI is available on PATH */
1422
+ const fnoxAvailable = () => Try(() => {
1423
+ execFileSync("fnox", ["--version"], { stdio: "pipe" });
1424
+ return true;
1425
+ }).fold(() => false, (v) => v);
1426
+
1427
+ //#endregion
1428
+ //#region src/fnox/identity.ts
1429
+ /** Check if the age CLI is available on PATH */
1430
+ const ageAvailable = () => Try(() => {
1431
+ execFileSync("age", ["--version"], { stdio: "pipe" });
1432
+ return true;
1433
+ }).fold(() => false, (v) => v);
1434
+ /** Unwrap an encrypted agent key using age --decrypt */
1435
+ const unwrapAgentKey = (identityPath) => {
1436
+ if (!existsSync(identityPath)) return Left({
1437
+ _tag: "IdentityNotFound",
1438
+ path: identityPath
1439
+ });
1440
+ if (!ageAvailable()) return Left({
1441
+ _tag: "AgeNotFound",
1442
+ message: "age CLI not found on PATH"
1443
+ });
1444
+ return Try(() => execFileSync("age", ["--decrypt", identityPath], {
1445
+ stdio: [
1446
+ "pipe",
1447
+ "pipe",
1448
+ "pipe"
1449
+ ],
1450
+ encoding: "utf-8"
1451
+ })).fold((err) => Left({
1452
+ _tag: "DecryptFailed",
1453
+ message: `age decrypt failed: ${err}`
1454
+ }), (output) => Right(output.trim()));
1455
+ };
1456
+
1457
+ //#endregion
1458
+ //#region src/cli/commands/exec.ts
1459
+ const runExec = (args, options) => {
1460
+ if (args.length === 0) {
1461
+ console.error(`${RED}Error:${RESET} No command specified`);
1462
+ process.exit(2);
1463
+ return;
1464
+ }
1465
+ const skipAudit = options.skipAudit || options.check === false;
1466
+ const configData = resolveConfigPath(options.config).fold((err) => {
1467
+ console.error(formatError(err));
1468
+ process.exit(2);
1469
+ }, (path) => loadConfig(path).fold((err) => {
1470
+ console.error(formatError(err));
1471
+ process.exit(2);
1472
+ }, (config) => ({
1473
+ config,
1474
+ path
1475
+ })));
1476
+ if (!configData) return;
1477
+ const { config, path } = configData;
1478
+ const configDir = dirname(path);
1479
+ if (!skipAudit) {
1480
+ const audit = computeAudit(config);
1481
+ console.error(`${BOLD}envpkt${RESET} pre-flight audit ${path}`);
1482
+ console.error(formatAudit(audit));
1483
+ console.error("");
1484
+ if (options.strict && audit.status !== "healthy") {
1485
+ console.error(`${RED}Aborting:${RESET} --strict mode and audit status is ${audit.status}`);
1486
+ process.exit(exitCodeForAudit(audit));
1487
+ return;
1488
+ }
1489
+ if (audit.status === "critical" && !options.warnOnly) {
1490
+ console.error(`${RED}Aborting:${RESET} audit status is critical (use --warn-only to proceed)`);
1491
+ process.exit(exitCodeForAudit(audit));
1492
+ return;
1493
+ }
1494
+ if (audit.status === "critical" && options.warnOnly) console.error(`${YELLOW}Warning:${RESET} Proceeding despite critical audit status (--warn-only)`);
1495
+ }
1496
+ let agentKey;
1497
+ if (config.agent?.identity) unwrapAgentKey(resolve(configDir, config.agent.identity)).fold((err) => {
1498
+ console.error(`${YELLOW}Warning:${RESET} Agent key unwrap failed: ${err._tag}`);
1499
+ }, (key) => {
1500
+ agentKey = key;
1501
+ });
1502
+ if (!fnoxAvailable()) console.error(`${YELLOW}Warning:${RESET} fnox not available — running command without secret injection`);
1503
+ const env = { ...process.env };
1504
+ if (fnoxAvailable()) {
1505
+ const fnoxArgs = options.profile ? [
1506
+ "export",
1507
+ "--profile",
1508
+ options.profile
1509
+ ] : ["export"];
1510
+ const fnoxEnv = agentKey ? {
1511
+ ...process.env,
1512
+ FNOX_AGE_KEY: agentKey
1513
+ } : void 0;
1514
+ Try(() => execFileSync("fnox", fnoxArgs, {
1515
+ stdio: "pipe",
1516
+ encoding: "utf-8",
1517
+ env: fnoxEnv
1518
+ })).fold((err) => {
1519
+ console.error(`${YELLOW}Warning:${RESET} fnox export failed: ${err}`);
1520
+ }, (output) => {
1521
+ for (const line of output.split("\n")) {
1522
+ const eq = line.indexOf("=");
1523
+ if (eq > 0) {
1524
+ const key = line.slice(0, eq).trim();
1525
+ env[key] = line.slice(eq + 1).trim();
1526
+ }
1527
+ }
1528
+ });
1529
+ }
1530
+ const [cmd, ...cmdArgs] = args;
1531
+ try {
1532
+ execFileSync(cmd, cmdArgs, {
1533
+ env,
1534
+ stdio: "inherit"
1535
+ });
1536
+ } catch (err) {
1537
+ const exitCode = err.status ?? 1;
1538
+ process.exit(exitCode);
1539
+ }
1540
+ };
1541
+
1542
+ //#endregion
1543
+ //#region src/core/fleet.ts
1544
+ const CONFIG_FILENAME$1 = "envpkt.toml";
1545
+ const SKIP_DIRS = new Set([
1546
+ "node_modules",
1547
+ ".git",
1548
+ ".hg",
1549
+ ".svn",
1550
+ "dist",
1551
+ "build",
1552
+ "lib",
1553
+ ".claude",
1554
+ "__pycache__",
1555
+ "target",
1556
+ "out",
1557
+ "tmp",
1558
+ ".terraform",
1559
+ ".gradle",
1560
+ ".cargo",
1561
+ ".venv",
1562
+ ".next",
1563
+ ".cache",
1564
+ ".tox",
1565
+ "vendor",
1566
+ "coverage",
1567
+ ".nyc_output",
1568
+ ".turbo"
1569
+ ]);
1570
+ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
1571
+ if (currentDepth > maxDepth) return;
1572
+ const configPath = join(dir, CONFIG_FILENAME$1);
1573
+ if (Try(() => statSync(configPath).isFile()).fold(() => false, (v) => v)) yield configPath;
1574
+ if (currentDepth >= maxDepth) return;
1575
+ let entries = [];
1576
+ Try(() => readdirSync(dir, { withFileTypes: true })).fold(() => {}, (e) => {
1577
+ entries = e;
1578
+ });
1579
+ for (const entry of entries) if (entry.isDirectory() && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) yield* findEnvpktFiles(join(dir, entry.name), maxDepth, currentDepth + 1);
1580
+ }
1581
+ const scanFleet = (rootDir, options) => {
1582
+ const maxDepth = options?.maxDepth ?? 3;
1583
+ const agents = [];
1584
+ for (const configPath of findEnvpktFiles(rootDir, maxDepth)) loadConfig(configPath).fold(() => {}, (config) => {
1585
+ const audit = computeAudit(config);
1586
+ agents.push({
1587
+ path: configPath,
1588
+ agent: config.agent,
1589
+ 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),
1590
+ audit
1591
+ });
1592
+ });
1593
+ const agentList = List(agents);
1594
+ const total_agents = agentList.size;
1595
+ const total_secrets = agentList.toArray().reduce((acc, a) => acc + a.audit.total, 0);
1596
+ const expired = agentList.toArray().reduce((acc, a) => acc + a.audit.expired, 0);
1597
+ const expiring_soon = agentList.toArray().reduce((acc, a) => acc + a.audit.expiring_soon, 0);
1598
+ const criticalCount = agentList.count((a) => a.audit.status === "critical");
1599
+ const degradedCount = agentList.count((a) => a.audit.status === "degraded");
1600
+ return {
1601
+ status: Cond.of().when(criticalCount > 0, "critical").elseWhen(degradedCount > 0, "degraded").else("healthy"),
1602
+ agents: agentList,
1603
+ total_agents,
1604
+ total_secrets,
1605
+ expired,
1606
+ expiring_soon
1607
+ };
1608
+ };
1609
+
1610
+ //#endregion
1611
+ //#region src/cli/commands/fleet.ts
1612
+ const statusIcon = (status) => {
1613
+ switch (status) {
1614
+ case "healthy": return `${GREEN}✓${RESET}`;
1615
+ case "degraded": return `${YELLOW}⚠${RESET}`;
1616
+ case "critical": return `${RED}✗${RESET}`;
1617
+ }
1618
+ };
1619
+ const runFleet = (options) => {
1620
+ const fleet = scanFleet(resolve(options.dir ?? "."), { maxDepth: options.depth });
1621
+ if (options.format === "json") {
1622
+ console.log(formatFleetJson(fleet));
1623
+ process.exit(fleet.status === "critical" ? 2 : 0);
1624
+ return;
1625
+ }
1626
+ const statusFilter = options.status;
1627
+ const agents = statusFilter ? fleet.agents.filter((a) => a.audit.status === statusFilter) : fleet.agents;
1628
+ console.log(`${statusIcon(fleet.status)} ${BOLD}Fleet: ${fleet.status.toUpperCase()}${RESET} — ${fleet.total_agents} agents, ${fleet.total_secrets} secrets`);
1629
+ if (fleet.expired > 0) console.log(` ${RED}${fleet.expired}${RESET} expired`);
1630
+ if (fleet.expiring_soon > 0) console.log(` ${YELLOW}${fleet.expiring_soon}${RESET} expiring soon`);
1631
+ console.log("");
1632
+ for (const agent of agents) {
1633
+ const name = agent.agent?.name ? BOLD + agent.agent.name + RESET : DIM + agent.path + RESET;
1634
+ const icon = statusIcon(agent.audit.status);
1635
+ console.log(` ${icon} ${name} ${DIM}(${agent.audit.total} secrets)${RESET}`);
1636
+ }
1637
+ process.exit(fleet.status === "critical" ? 2 : 0);
1638
+ };
1639
+
1640
+ //#endregion
1641
+ //#region src/cli/commands/init.ts
1642
+ const CONFIG_FILENAME = "envpkt.toml";
1643
+ const todayIso = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1644
+ const generateSecretBlock = (key, service) => {
1645
+ return `[meta.${key}]
1646
+ service = "${service ?? key}"
1647
+ # purpose = "" # Why: what this secret enables
1648
+ # capabilities = [] # What operations this grants
1649
+ created = "${todayIso()}"
1650
+ # expires = "" # When: YYYY-MM-DD expiration date
1651
+ # rotation_url = "" # URL for rotation procedure
1652
+ # source = "" # Where the value originates (e.g. vault, ci)
1653
+ # tags = {}
1654
+ `;
1655
+ };
1656
+ const generateAgentSection = (name, capabilities, expires) => {
1657
+ return `[agent]
1658
+ name = "${name}"
1659
+ # consumer = "agent" # agent | service | developer | ci${capabilities ? `\ncapabilities = [${capabilities.split(",").map((c) => `"${c.trim()}"`).join(", ")}]` : ""}${expires ? `\nexpires = "${expires}"` : ""}
1660
+ `;
1661
+ };
1662
+ const generateTemplate = (options, fnoxKeys) => {
1663
+ const lines = [];
1664
+ lines.push(`#:schema https://raw.githubusercontent.com/jordanburke/envpkt/main/schemas/envpkt.schema.json`);
1665
+ lines.push(``);
1666
+ lines.push(`version = 1`);
1667
+ lines.push(``);
1668
+ if (options.catalog) {
1669
+ lines.push(`catalog = "${options.catalog}"`);
1670
+ lines.push(``);
1671
+ }
1672
+ if (options.agent && options.name) {
1673
+ lines.push(generateAgentSection(options.name, options.capabilities, options.expires));
1674
+ if (options.catalog) lines.push(`secrets = [] # Add catalog secret keys this agent needs`);
1675
+ lines.push(``);
1676
+ }
1677
+ if (!options.catalog) {
1678
+ lines.push(`# Lifecycle policy`);
1679
+ lines.push(`[lifecycle]`);
1680
+ lines.push(`stale_warning_days = 90`);
1681
+ lines.push(`# require_expiration = false`);
1682
+ lines.push(`# require_service = false`);
1683
+ lines.push(``);
1684
+ if (fnoxKeys && fnoxKeys.length > 0) {
1685
+ lines.push(`# Secrets detected from fnox.toml`);
1686
+ for (const key of fnoxKeys) lines.push(generateSecretBlock(key));
1687
+ } else {
1688
+ lines.push(`# Add your secret metadata below.`);
1689
+ lines.push(`# Each [meta.<key>] describes a secret your agent needs.`);
1690
+ lines.push(``);
1691
+ lines.push(generateSecretBlock("EXAMPLE_API_KEY", "example-service"));
1692
+ }
1693
+ } else {
1694
+ lines.push(`# Optional: override catalog metadata for specific secrets`);
1695
+ lines.push(`# [meta.KEY_NAME]`);
1696
+ lines.push(`# capabilities = ["read"] # narrows catalog's broader definition`);
1697
+ }
1698
+ return lines.join("\n");
1699
+ };
1700
+ const readFnoxKeys = (fnoxPath) => Try(() => readFileSync(fnoxPath, "utf-8")).fold((err) => Left({
1701
+ _tag: "ReadError",
1702
+ message: String(err)
1703
+ }), (content) => Try(() => parse(content)).fold((err) => Left({
1704
+ _tag: "ParseError",
1705
+ message: String(err)
1706
+ }), (data) => Right(Object.keys(data))));
1707
+ const formatConfigError = (err) => {
1708
+ switch (err._tag) {
1709
+ case "FileNotFound": return err.path;
1710
+ case "ParseError": return err.message;
1711
+ case "ReadError": return err.message;
1712
+ case "ValidationError": return err.errors.toArray().join(", ");
1713
+ }
1714
+ };
1715
+ const runInit = (dir, options) => {
1716
+ const outPath = join(dir, CONFIG_FILENAME);
1717
+ if (existsSync(outPath) && !options.force) {
1718
+ console.error(`${RED}Error:${RESET} ${CONFIG_FILENAME} already exists. Use --force to overwrite.`);
1719
+ process.exit(1);
1720
+ }
1721
+ let fnoxKeys;
1722
+ if (options.fromFnox) {
1723
+ const fnoxPath = options.fromFnox === "true" || options.fromFnox === "" ? join(dir, "fnox.toml") : options.fromFnox;
1724
+ if (!existsSync(fnoxPath)) {
1725
+ console.error(`${RED}Error:${RESET} fnox.toml not found at ${fnoxPath}`);
1726
+ process.exit(1);
1727
+ }
1728
+ readFnoxKeys(fnoxPath).fold((err) => {
1729
+ console.error(`${RED}Error:${RESET} Failed to read fnox.toml: ${formatConfigError(err)}`);
1730
+ process.exit(1);
1731
+ }, (keys) => {
1732
+ fnoxKeys = keys;
1733
+ });
1734
+ }
1735
+ const content = generateTemplate(options, fnoxKeys);
1736
+ Try(() => writeFileSync(outPath, content, "utf-8")).fold((err) => {
1737
+ console.error(`${RED}Error:${RESET} Failed to write ${CONFIG_FILENAME}: ${err}`);
1738
+ process.exit(1);
1739
+ }, () => {
1740
+ console.log(`${GREEN}✓${RESET} Created ${BOLD}${CONFIG_FILENAME}${RESET} in ${CYAN}${dir}${RESET}`);
1741
+ if (fnoxKeys) console.log(` Scaffolded ${fnoxKeys.length} secret(s) from fnox.toml`);
1742
+ console.log(` ${BOLD}Next:${RESET} Fill in metadata for each secret`);
1743
+ });
1744
+ };
1745
+
1746
+ //#endregion
1747
+ //#region src/core/format.ts
1748
+ const maskValue = (value) => {
1749
+ if (value.length > 8) return `${value.slice(0, 3)}${"•".repeat(5)}${value.slice(-4)}`;
1750
+ return "•".repeat(5);
1751
+ };
1752
+
1753
+ //#endregion
1754
+ //#region src/cli/commands/inspect.ts
1755
+ const printSecretMeta = (meta, indent) => {
1756
+ if (meta.purpose) console.log(`${indent}purpose: ${meta.purpose}`);
1757
+ if (meta.capabilities) console.log(`${indent}capabilities: ${DIM}${meta.capabilities.join(", ")}${RESET}`);
1758
+ const dateParts = [];
1759
+ if (meta.created) dateParts.push(`created: ${meta.created}`);
1760
+ if (meta.expires) dateParts.push(`expires: ${meta.expires}`);
1761
+ if (dateParts.length > 0) console.log(`${indent}${dateParts.join(" ")}`);
1762
+ const opsParts = [];
1763
+ if (meta.rotates) opsParts.push(`rotates: ${meta.rotates}`);
1764
+ if (meta.rate_limit) opsParts.push(`rate_limit: ${meta.rate_limit}`);
1765
+ if (opsParts.length > 0) console.log(`${indent}${opsParts.join(" ")}`);
1766
+ if (meta.source) console.log(`${indent}source: ${meta.source}`);
1767
+ if (meta.model_hint) console.log(`${indent}model_hint: ${meta.model_hint}`);
1768
+ if (meta.rotation_url) console.log(`${indent}rotation_url: ${DIM}${meta.rotation_url}${RESET}`);
1769
+ if (meta.required !== void 0) console.log(`${indent}required: ${meta.required}`);
1770
+ if (meta.tags) {
1771
+ const tagStr = Object.entries(meta.tags).map(([k, v]) => `${k}=${v}`).join(", ");
1772
+ console.log(`${indent}tags: ${tagStr}`);
1773
+ }
1774
+ };
1775
+ const printConfig = (config, path, resolveResult, opts) => {
1776
+ console.log(`${BOLD}envpkt.toml${RESET} ${DIM}(${path})${RESET}`);
1777
+ if (resolveResult?.catalogPath) console.log(`${DIM}Catalog: ${CYAN}${resolveResult.catalogPath}${RESET}`);
1778
+ console.log(`version: ${config.version}`);
1779
+ console.log("");
1780
+ if (config.agent) {
1781
+ console.log(`${BOLD}Agent:${RESET} ${config.agent.name}`);
1782
+ if (config.agent.consumer) console.log(` consumer: ${config.agent.consumer}`);
1783
+ if (config.agent.description) console.log(` description: ${config.agent.description}`);
1784
+ if (config.agent.capabilities) console.log(` capabilities: ${config.agent.capabilities.join(", ")}`);
1785
+ if (config.agent.expires) console.log(` expires: ${config.agent.expires}`);
1786
+ if (config.agent.services) console.log(` services: ${config.agent.services.join(", ")}`);
1787
+ if (config.agent.secrets) console.log(` secrets: ${config.agent.secrets.join(", ")}`);
1788
+ console.log("");
1789
+ }
1790
+ console.log(`${BOLD}Secrets:${RESET} ${Object.keys(config.meta).length}`);
1791
+ for (const [key, meta] of Object.entries(config.meta)) {
1792
+ const secretValue = opts?.secrets?.[key];
1793
+ const valueSuffix = secretValue !== void 0 ? ` = ${YELLOW}${(opts?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}${RESET}` : "";
1794
+ console.log(` ${BOLD}${key}${RESET} → ${meta.service ?? key}${valueSuffix}`);
1795
+ printSecretMeta(meta, " ");
1796
+ }
1797
+ if (config.lifecycle) {
1798
+ console.log("");
1799
+ console.log(`${BOLD}Lifecycle:${RESET}`);
1800
+ if (config.lifecycle.stale_warning_days !== void 0) console.log(` stale_warning_days: ${config.lifecycle.stale_warning_days}`);
1801
+ if (config.lifecycle.require_expiration !== void 0) console.log(` require_expiration: ${config.lifecycle.require_expiration}`);
1802
+ if (config.lifecycle.require_service !== void 0) console.log(` require_service: ${config.lifecycle.require_service}`);
1803
+ }
1804
+ if (resolveResult?.catalogPath) {
1805
+ console.log("");
1806
+ console.log(`${BOLD}Catalog Resolution:${RESET}`);
1807
+ console.log(` merged: ${resolveResult.merged.length} keys`);
1808
+ if (resolveResult.overridden.length > 0) console.log(` overridden: ${resolveResult.overridden.join(", ")}`);
1809
+ else console.log(` overridden: ${DIM}(none)${RESET}`);
1810
+ for (const w of resolveResult.warnings) console.log(` ${YELLOW}warning:${RESET} ${w}`);
1811
+ }
1812
+ };
1813
+ const runInspect = (options) => {
1814
+ resolveConfigPath(options.config).fold((err) => {
1815
+ console.error(formatError(err));
1816
+ process.exit(2);
1817
+ }, (path) => {
1818
+ loadConfig(path).fold((err) => {
1819
+ console.error(formatError(err));
1820
+ process.exit(2);
1821
+ }, (config) => {
1822
+ resolveConfig(config, dirname(path)).fold((err) => {
1823
+ console.error(formatError(err));
1824
+ process.exit(2);
1825
+ }, (resolveResult) => {
1826
+ const showResolved = options.resolved || !!resolveResult.catalogPath;
1827
+ const showConfig = showResolved ? resolveResult.config : config;
1828
+ if (options.format === "json") {
1829
+ console.log(JSON.stringify(showConfig, null, 2));
1830
+ return;
1831
+ }
1832
+ const printOpts = options.secrets ? {
1833
+ secrets: Object.fromEntries(Object.keys(showConfig.meta).filter((key) => process.env[key] !== void 0).map((key) => [key, process.env[key]])),
1834
+ secretDisplay: options.plaintext ? "plaintext" : "encrypted"
1835
+ } : void 0;
1836
+ printConfig(showConfig, path, showResolved ? resolveResult : void 0, printOpts);
1837
+ });
1838
+ });
1839
+ });
1840
+ };
1841
+
1842
+ //#endregion
1843
+ //#region src/mcp/resources.ts
1844
+ const loadConfigSafe = () => {
1845
+ return resolveConfigPath().fold(() => void 0, (path) => loadConfig(path).fold(() => void 0, (config) => ({
1846
+ config,
1847
+ path
1848
+ })));
1849
+ };
1850
+ const resourceDefinitions = [{
1851
+ uri: "envpkt://health",
1852
+ name: "Credential Health",
1853
+ description: "Current health status of the envpkt credential packet",
1854
+ mimeType: "application/json"
1855
+ }, {
1856
+ uri: "envpkt://capabilities",
1857
+ name: "Agent Capabilities",
1858
+ description: "Capabilities declared by the agent and per-secret capability grants",
1859
+ mimeType: "application/json"
1860
+ }];
1861
+ const readHealth = () => {
1862
+ const loaded = loadConfigSafe();
1863
+ if (!loaded) return { contents: [{
1864
+ uri: "envpkt://health",
1865
+ mimeType: "application/json",
1866
+ text: JSON.stringify({ error: "No envpkt.toml found" })
1867
+ }] };
1868
+ const { config, path } = loaded;
1869
+ const audit = computeAudit(config);
1870
+ return { contents: [{
1871
+ uri: "envpkt://health",
1872
+ mimeType: "application/json",
1873
+ text: JSON.stringify({
1874
+ path,
1875
+ status: audit.status,
1876
+ total: audit.total,
1877
+ healthy: audit.healthy,
1878
+ expiring_soon: audit.expiring_soon,
1879
+ expired: audit.expired,
1880
+ stale: audit.stale,
1881
+ missing: audit.missing
1882
+ }, null, 2)
1883
+ }] };
1884
+ };
1885
+ const readCapabilities = () => {
1886
+ const loaded = loadConfigSafe();
1887
+ if (!loaded) return { contents: [{
1888
+ uri: "envpkt://capabilities",
1889
+ mimeType: "application/json",
1890
+ text: JSON.stringify({ error: "No envpkt.toml found" })
1891
+ }] };
1892
+ const { config } = loaded;
1893
+ const agentCapabilities = config.agent?.capabilities ?? [];
1894
+ const secretCapabilities = {};
1895
+ for (const [key, meta] of Object.entries(config.meta)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
1896
+ return { contents: [{
1897
+ uri: "envpkt://capabilities",
1898
+ mimeType: "application/json",
1899
+ text: JSON.stringify({
1900
+ agent: config.agent ? {
1901
+ name: config.agent.name,
1902
+ consumer: config.agent.consumer,
1903
+ description: config.agent.description,
1904
+ capabilities: agentCapabilities
1905
+ } : null,
1906
+ secrets: secretCapabilities
1907
+ }, null, 2)
1908
+ }] };
1909
+ };
1910
+ const resourceHandlers = {
1911
+ "envpkt://health": readHealth,
1912
+ "envpkt://capabilities": readCapabilities
1913
+ };
1914
+ const readResource = (uri) => {
1915
+ const handler = resourceHandlers[uri];
1916
+ return handler?.();
1917
+ };
1918
+
1919
+ //#endregion
1920
+ //#region src/mcp/tools.ts
1921
+ const textResult = (text) => ({ content: [{
1922
+ type: "text",
1923
+ text
1924
+ }] });
1925
+ const errorResult = (message) => ({
1926
+ content: [{
1927
+ type: "text",
1928
+ text: message
1929
+ }],
1930
+ isError: true
1931
+ });
1932
+ const loadConfigForTool = (configPath) => {
1933
+ return resolveConfigPath(configPath).fold((err) => ({
1934
+ ok: false,
1935
+ result: errorResult(`Config error: ${err._tag} — ${err._tag === "FileNotFound" ? err.path : ""}`)
1936
+ }), (path) => loadConfig(path).fold((err) => ({
1937
+ ok: false,
1938
+ result: errorResult(`Config error: ${err._tag} — ${err._tag === "ValidationError" ? err.errors.toArray().join(", ") : ""}`)
1939
+ }), (config) => ({
1940
+ ok: true,
1941
+ config,
1942
+ path
1943
+ })));
1944
+ };
1945
+ const toolDefinitions = [
1946
+ {
1947
+ name: "getPacketHealth",
1948
+ description: "Get overall health status of the envpkt credential packet — returns audit results including secret statuses, expiration info, and issues",
1949
+ inputSchema: {
1950
+ type: "object",
1951
+ properties: { configPath: {
1952
+ type: "string",
1953
+ description: "Optional path to envpkt.toml"
1954
+ } }
1955
+ }
1956
+ },
1957
+ {
1958
+ name: "listCapabilities",
1959
+ description: "List capabilities declared by the agent and per-secret capabilities",
1960
+ inputSchema: {
1961
+ type: "object",
1962
+ properties: { configPath: {
1963
+ type: "string",
1964
+ description: "Optional path to envpkt.toml"
1965
+ } }
1966
+ }
1967
+ },
1968
+ {
1969
+ name: "getSecretMeta",
1970
+ description: "Get metadata for a specific secret by key name — returns service, purpose, expiration, provisioner, and other five-W details",
1971
+ inputSchema: {
1972
+ type: "object",
1973
+ properties: {
1974
+ key: {
1975
+ type: "string",
1976
+ description: "Secret key name to look up"
1977
+ },
1978
+ configPath: {
1979
+ type: "string",
1980
+ description: "Optional path to envpkt.toml"
1981
+ }
1982
+ },
1983
+ required: ["key"]
1984
+ }
1985
+ },
1986
+ {
1987
+ name: "checkExpiration",
1988
+ description: "Check expiration status of a specific secret — returns days remaining and whether it needs rotation",
1989
+ inputSchema: {
1990
+ type: "object",
1991
+ properties: {
1992
+ key: {
1993
+ type: "string",
1994
+ description: "Secret key name to check"
1995
+ },
1996
+ configPath: {
1997
+ type: "string",
1998
+ description: "Optional path to envpkt.toml"
1999
+ }
2000
+ },
2001
+ required: ["key"]
2002
+ }
2003
+ }
2004
+ ];
2005
+ const handleGetPacketHealth = (args) => {
2006
+ const loaded = loadConfigForTool(args.configPath);
2007
+ if (!loaded.ok) return loaded.result;
2008
+ const { config, path } = loaded;
2009
+ const audit = computeAudit(config);
2010
+ const secretDetails = audit.secrets.toArray().map((s) => ({
2011
+ key: s.key,
2012
+ service: s.service.fold(() => null, (sv) => sv),
2013
+ status: s.status,
2014
+ days_remaining: s.days_remaining.fold(() => null, (d) => d),
2015
+ rotation_url: s.rotation_url.fold(() => null, (u) => u),
2016
+ issues: s.issues.toArray()
2017
+ }));
2018
+ return textResult(JSON.stringify({
2019
+ path,
2020
+ status: audit.status,
2021
+ total: audit.total,
2022
+ healthy: audit.healthy,
2023
+ expiring_soon: audit.expiring_soon,
2024
+ expired: audit.expired,
2025
+ stale: audit.stale,
2026
+ missing: audit.missing,
2027
+ secrets: secretDetails
2028
+ }, null, 2));
2029
+ };
2030
+ const handleListCapabilities = (args) => {
2031
+ const loaded = loadConfigForTool(args.configPath);
2032
+ if (!loaded.ok) return loaded.result;
2033
+ const { config } = loaded;
2034
+ const agentCapabilities = config.agent?.capabilities ?? [];
2035
+ const secretCapabilities = {};
2036
+ for (const [key, meta] of Object.entries(config.meta)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
2037
+ return textResult(JSON.stringify({
2038
+ agent: config.agent ? {
2039
+ name: config.agent.name,
2040
+ consumer: config.agent.consumer,
2041
+ description: config.agent.description,
2042
+ capabilities: agentCapabilities
2043
+ } : null,
2044
+ secrets: secretCapabilities
2045
+ }, null, 2));
2046
+ };
2047
+ const handleGetSecretMeta = (args) => {
2048
+ const key = args.key;
2049
+ if (!key) return errorResult("Missing required argument: key");
2050
+ const loaded = loadConfigForTool(args.configPath);
2051
+ if (!loaded.ok) return loaded.result;
2052
+ const { config } = loaded;
2053
+ const meta = config.meta[key];
2054
+ if (!meta) return errorResult(`Secret not found: ${key}`);
2055
+ return textResult(JSON.stringify({
2056
+ key,
2057
+ ...meta
2058
+ }, null, 2));
2059
+ };
2060
+ const handleCheckExpiration = (args) => {
2061
+ const key = args.key;
2062
+ if (!key) return errorResult("Missing required argument: key");
2063
+ const loaded = loadConfigForTool(args.configPath);
2064
+ if (!loaded.ok) return loaded.result;
2065
+ const { config } = loaded;
2066
+ return computeAudit(config).secrets.find((s) => s.key === key).fold(() => errorResult(`Secret not found: ${key}`), (s) => textResult(JSON.stringify({
2067
+ key: s.key,
2068
+ status: s.status,
2069
+ days_remaining: s.days_remaining.fold(() => null, (d) => d),
2070
+ expires: s.expires.fold(() => null, (e) => e),
2071
+ rotation_url: s.rotation_url.fold(() => null, (u) => u),
2072
+ needs_rotation: s.status === "expired" || s.status === "expiring_soon",
2073
+ issues: s.issues.toArray()
2074
+ }, null, 2)));
2075
+ };
2076
+ const handlers = {
2077
+ getPacketHealth: handleGetPacketHealth,
2078
+ listCapabilities: handleListCapabilities,
2079
+ getSecretMeta: handleGetSecretMeta,
2080
+ checkExpiration: handleCheckExpiration
2081
+ };
2082
+ const callTool = (name, args) => {
2083
+ const handler = handlers[name];
2084
+ if (!handler) return errorResult(`Unknown tool: ${name}`);
2085
+ return handler(args);
2086
+ };
2087
+
2088
+ //#endregion
2089
+ //#region src/mcp/server.ts
2090
+ const createServer = () => {
2091
+ const server = new Server({
2092
+ name: "envpkt",
2093
+ version: "0.1.0"
2094
+ }, {
2095
+ capabilities: {
2096
+ tools: {},
2097
+ resources: {}
2098
+ },
2099
+ instructions: "envpkt provides credential lifecycle awareness for AI agents. Use tools to check health, capabilities, and secret metadata. No secret values are ever exposed."
2100
+ });
2101
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions.map((t) => ({
2102
+ name: t.name,
2103
+ description: t.description,
2104
+ inputSchema: t.inputSchema
2105
+ })) }));
2106
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2107
+ const { name, arguments: args } = request.params;
2108
+ return callTool(name, args ?? {});
2109
+ });
2110
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [...resourceDefinitions] }));
2111
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2112
+ const { uri } = request.params;
2113
+ const result = readResource(uri);
2114
+ if (!result) return { contents: [{
2115
+ uri,
2116
+ mimeType: "text/plain",
2117
+ text: `Resource not found: ${uri}`
2118
+ }] };
2119
+ return result;
2120
+ });
2121
+ return server;
2122
+ };
2123
+ const startServer = async () => {
2124
+ const server = createServer();
2125
+ const transport = new StdioServerTransport();
2126
+ await server.connect(transport);
2127
+ };
2128
+
2129
+ //#endregion
2130
+ //#region src/cli/commands/mcp.ts
2131
+ const runMcp = (_options) => {
2132
+ startServer().catch((err) => {
2133
+ console.error("MCP server error:", err);
2134
+ process.exit(1);
2135
+ });
2136
+ };
2137
+
2138
+ //#endregion
2139
+ //#region src/cli/commands/resolve.ts
2140
+ const runResolve = (options) => {
2141
+ resolveConfigPath(options.config).fold((err) => {
2142
+ console.error(formatError(err));
2143
+ process.exit(2);
2144
+ }, (configPath) => {
2145
+ loadConfig(configPath).fold((err) => {
2146
+ console.error(formatError(err));
2147
+ process.exit(2);
2148
+ }, (config) => {
2149
+ resolveConfig(config, dirname(configPath)).fold((err) => {
2150
+ console.error(formatError(err));
2151
+ process.exit(2);
2152
+ }, (result) => {
2153
+ const outputFormat = options.format ?? "toml";
2154
+ let content;
2155
+ if (outputFormat === "json") content = JSON.stringify(result.config, null, 2) + "\n";
2156
+ else content = `# Generated by envpkt resolve — do not edit\n${stringify(result.config)}\n`;
2157
+ if (options.dryRun) {
2158
+ console.log(`${DIM}# Dry run — would write:${RESET}`);
2159
+ console.log(content);
2160
+ } else if (options.output) {
2161
+ writeFileSync(options.output, content, "utf-8");
2162
+ console.log(`${GREEN}✓${RESET} Resolved config written to ${BOLD}${options.output}${RESET}`);
2163
+ } else process.stdout.write(content);
2164
+ if (result.catalogPath) {
2165
+ const summaryTarget = options.output ? process.stdout : process.stderr;
2166
+ 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");
2167
+ for (const w of result.warnings) summaryTarget.write(`${RED}Warning:${RESET} ${w}\n`);
2168
+ }
2169
+ });
2170
+ });
2171
+ });
2172
+ };
2173
+
2174
+ //#endregion
2175
+ //#region src/cli/commands/shell-hook.ts
2176
+ const ZSH_HOOK = `# envpkt shell hook — add to your .zshrc
2177
+ _envpkt_chpwd() {
2178
+ if [[ -f envpkt.toml ]]; then
2179
+ envpkt audit --format minimal 2>/dev/null
2180
+ fi
2181
+ }
2182
+
2183
+ if (( $+functions[add-zsh-hook] )); then
2184
+ autoload -Uz add-zsh-hook
2185
+ add-zsh-hook chpwd _envpkt_chpwd
2186
+ else
2187
+ autoload -Uz add-zsh-hook
2188
+ add-zsh-hook chpwd _envpkt_chpwd
2189
+ fi
2190
+ `;
2191
+ const BASH_HOOK = `# envpkt shell hook — add to your .bashrc
2192
+ _envpkt_prompt() {
2193
+ if [[ -f envpkt.toml ]]; then
2194
+ envpkt audit --format minimal 2>/dev/null
2195
+ fi
2196
+ }
2197
+
2198
+ if [[ ! "$PROMPT_COMMAND" == *"_envpkt_prompt"* ]]; then
2199
+ PROMPT_COMMAND="_envpkt_prompt;$PROMPT_COMMAND"
2200
+ fi
2201
+ `;
2202
+ const runShellHook = (shell) => {
2203
+ switch (shell) {
2204
+ case "zsh":
2205
+ console.log(ZSH_HOOK);
2206
+ break;
2207
+ case "bash":
2208
+ console.log(BASH_HOOK);
2209
+ break;
2210
+ default:
2211
+ console.error(`${RED}Error:${RESET} Unsupported shell: ${shell}. Use "zsh" or "bash".`);
2212
+ process.exit(1);
2213
+ }
2214
+ };
2215
+
2216
+ //#endregion
2217
+ //#region src/cli/index.ts
2218
+ const program = new Command();
2219
+ program.name("envpkt").description("Credential lifecycle and fleet management for AI agents").version("0.1.0");
2220
+ program.command("init").description("Initialize a new envpkt.toml in the current directory").option("--from-fnox [path]", "Scaffold from fnox.toml (optionally specify path)").option("--catalog <path>", "Path to shared secret catalog").option("--agent", "Include [agent] section").option("--name <name>", "Agent name (requires --agent)").option("--capabilities <caps>", "Comma-separated capabilities (requires --agent)").option("--expires <date>", "Agent credential expiration YYYY-MM-DD (requires --agent)").option("--force", "Overwrite existing envpkt.toml").action((options) => {
2221
+ runInit(process.cwd(), options);
2222
+ });
2223
+ program.command("audit").description("Audit credential health from envpkt.toml").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json | minimal", "table").option("--expiring <days>", "Show secrets expiring within N days", parseInt).option("--status <status>", "Filter by status: healthy | expiring_soon | expired | stale | missing").option("--strict", "Exit non-zero on any non-healthy secret").action((options) => {
2224
+ runAudit(options);
2225
+ });
2226
+ program.command("fleet").description("Scan directory tree for envpkt.toml files and aggregate health").option("-d, --dir <path>", "Root directory to scan", ".").option("--depth <n>", "Max directory depth", parseInt).option("--format <format>", "Output format: table | json", "table").option("--status <status>", "Filter agents by health status").action((options) => {
2227
+ runFleet(options);
2228
+ });
2229
+ program.command("inspect").description("Display structured view of envpkt.toml").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json", "table").option("--resolved", "Show resolved view (catalog merged)").option("--secrets", "Show secret values from environment (masked by default)").option("--plaintext", "Show secret values in plaintext (requires --secrets)").action((options) => {
2230
+ runInspect(options);
2231
+ });
2232
+ program.command("exec").description("Run pre-flight audit then execute a command with fnox-injected env").argument("<command...>", "Command to execute").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("--skip-audit", "Skip the pre-flight audit (alias: --no-check)").option("--no-check", "Skip the pre-flight audit").option("--warn-only", "Warn on critical audit but do not abort").option("--strict", "Abort on any non-healthy secret").action((args, options) => {
2233
+ runExec(args, options);
2234
+ });
2235
+ program.command("resolve").description("Resolve catalog references and output a flat, self-contained config").option("-c, --config <path>", "Path to envpkt.toml").option("-o, --output <path>", "Write resolved config to file (default: stdout)").option("--format <format>", "Output format: toml | json", "toml").option("--dry-run", "Show what would be resolved without writing").action((options) => {
2236
+ runResolve(options);
2237
+ });
2238
+ program.command("mcp").description("Start the envpkt MCP server (stdio transport)").option("-c, --config <path>", "Path to envpkt.toml").action((options) => {
2239
+ runMcp(options);
2240
+ });
2241
+ const env = program.command("env").description("Discover and check credentials in your shell environment");
2242
+ env.command("scan").description("Auto-discover credentials from process.env and scaffold TOML entries").option("--format <format>", "Output format: table | json", "table").option("--write", "Write discovered credentials to envpkt.toml").option("--dry-run", "Preview TOML that would be written (implies --write)").option("--include-unknown", "Include vars where service could not be inferred").action((options) => {
2243
+ runEnvScan(options);
2244
+ });
2245
+ env.command("check").description("Bidirectional drift detection between envpkt.toml and live environment").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json", "table").option("--strict", "Exit non-zero on any drift").action((options) => {
2246
+ runEnvCheck(options);
2247
+ });
2248
+ program.command("shell-hook").description("Output shell function for ambient credential warnings on cd").argument("<shell>", "Shell type: zsh | bash").action((shell) => {
2249
+ runShellHook(shell);
2250
+ });
2251
+ program.parse();
2252
+
2253
+ //#endregion
2254
+ export { };