envpkt 0.11.3 → 0.11.5
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 +209 -9
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -778,9 +778,13 @@ const runAuditOnConfig = (config, options) => {
|
|
|
778
778
|
...afterStatus,
|
|
779
779
|
secrets: afterStatus.secrets.filter((s) => s.days_remaining.fold(() => false, (d) => d >= 0 && d <= expiring))
|
|
780
780
|
}));
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
781
|
+
const sorted = options.sort ? {
|
|
782
|
+
...filtered,
|
|
783
|
+
secrets: filtered.secrets.sortBy((s) => s.key)
|
|
784
|
+
} : filtered;
|
|
785
|
+
if (options.format === "json") console.log(formatAuditJson(sorted));
|
|
786
|
+
else if (options.format === "minimal") console.log(formatAuditMinimal(sorted));
|
|
787
|
+
else console.log(formatAudit(sorted));
|
|
784
788
|
if (options.all) if (options.format === "json") console.log(formatEnvAuditJson(config));
|
|
785
789
|
else formatEnvAuditTable(config);
|
|
786
790
|
const code = options.strict ? exitCodeForAudit(audit) : audit.status === "critical" ? 2 : 0;
|
|
@@ -2360,6 +2364,145 @@ const updateSectionFields = (raw, sectionHeader, updates) => {
|
|
|
2360
2364
|
* Ensures proper spacing (double newline before the block).
|
|
2361
2365
|
*/
|
|
2362
2366
|
const appendSection = (raw, block) => `${raw.trimEnd()}\n\n${block}`;
|
|
2367
|
+
const ENV_HEADER_RE = /^\[env\.(.+)\]\s*$/;
|
|
2368
|
+
const SECRET_HEADER_RE = /^\[secret\.(.+)\]\s*$/;
|
|
2369
|
+
const ANY_HEADER_RE = /^\[.+\]\s*$/;
|
|
2370
|
+
/**
|
|
2371
|
+
* Find the end (exclusive) of a section starting at `start`, respecting
|
|
2372
|
+
* multiline `"""..."""` values so the scanner does not mistake content inside
|
|
2373
|
+
* a multiline string for a section header.
|
|
2374
|
+
*/
|
|
2375
|
+
const findSectionEnd = (lines, start) => {
|
|
2376
|
+
const initial = {
|
|
2377
|
+
end: lines.length,
|
|
2378
|
+
inMultiline: false,
|
|
2379
|
+
done: false
|
|
2380
|
+
};
|
|
2381
|
+
return List(lines.slice(start + 1)).zipWithIndex().foldLeft(initial)((state, entry) => scanSectionBoundary(state, entry[0], start + 1 + entry[1])).end;
|
|
2382
|
+
};
|
|
2383
|
+
/**
|
|
2384
|
+
* Walking backwards from `headerIdx`, return the index of the first line of the
|
|
2385
|
+
* "doc block" that should travel with this section. A doc block is a contiguous
|
|
2386
|
+
* run of `#`-comment lines *immediately* above the header (no blank line
|
|
2387
|
+
* between). A blank line acts as a paragraph break and stops the walk — so
|
|
2388
|
+
* `# Some heading\n\n[secret.X]` does NOT attach the heading to `[secret.X]`.
|
|
2389
|
+
*/
|
|
2390
|
+
const findPreambleStart = (lines, headerIdx) => {
|
|
2391
|
+
const stopOffset = [...lines.slice(0, headerIdx)].reverse().findIndex((l) => !l.trim().startsWith("#"));
|
|
2392
|
+
return stopOffset === -1 ? 0 : headerIdx - stopOffset;
|
|
2393
|
+
};
|
|
2394
|
+
const classifyHeader = (line, idx) => {
|
|
2395
|
+
if (!ANY_HEADER_RE.test(line)) return Option.none();
|
|
2396
|
+
const envMatch = line.match(ENV_HEADER_RE);
|
|
2397
|
+
if (envMatch) return Option({
|
|
2398
|
+
idx,
|
|
2399
|
+
kind: "env",
|
|
2400
|
+
key: envMatch[1]
|
|
2401
|
+
});
|
|
2402
|
+
const secretMatch = line.match(SECRET_HEADER_RE);
|
|
2403
|
+
if (secretMatch) return Option({
|
|
2404
|
+
idx,
|
|
2405
|
+
kind: "secret",
|
|
2406
|
+
key: secretMatch[1]
|
|
2407
|
+
});
|
|
2408
|
+
return Option({
|
|
2409
|
+
idx,
|
|
2410
|
+
kind: "other",
|
|
2411
|
+
key: ""
|
|
2412
|
+
});
|
|
2413
|
+
};
|
|
2414
|
+
const scanHeader = (state, line, idx) => {
|
|
2415
|
+
if (state.inMultiline) return line.includes(MULTILINE_OPEN) ? {
|
|
2416
|
+
...state,
|
|
2417
|
+
inMultiline: false
|
|
2418
|
+
} : state;
|
|
2419
|
+
if (line.includes(MULTILINE_OPEN)) return (line.slice(line.indexOf("=") + 1).trim().match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1 ? {
|
|
2420
|
+
...state,
|
|
2421
|
+
inMultiline: true
|
|
2422
|
+
} : state;
|
|
2423
|
+
return classifyHeader(line, idx).fold(() => state, (header) => ({
|
|
2424
|
+
...state,
|
|
2425
|
+
headers: [...state.headers, header]
|
|
2426
|
+
}));
|
|
2427
|
+
};
|
|
2428
|
+
const partitionSections = (raw) => {
|
|
2429
|
+
const lines = raw.split("\n");
|
|
2430
|
+
const { headers } = List(lines).zipWithIndex().foldLeft({
|
|
2431
|
+
headers: [],
|
|
2432
|
+
inMultiline: false
|
|
2433
|
+
})((state, entry) => scanHeader(state, entry[0], entry[1]));
|
|
2434
|
+
const envSecretHeaders = headers.filter((h) => h.kind === "env" || h.kind === "secret");
|
|
2435
|
+
const trueBodyRange = (headerIdx) => {
|
|
2436
|
+
const naiveEnd = findSectionEnd(lines, headerIdx);
|
|
2437
|
+
const lastContent = lines.slice(headerIdx, naiveEnd).findLastIndex((l) => {
|
|
2438
|
+
const t = l.trim();
|
|
2439
|
+
return t !== "" && !t.startsWith("#");
|
|
2440
|
+
});
|
|
2441
|
+
return {
|
|
2442
|
+
start: headerIdx,
|
|
2443
|
+
end: lastContent === -1 ? headerIdx + 1 : headerIdx + lastContent + 1
|
|
2444
|
+
};
|
|
2445
|
+
};
|
|
2446
|
+
const sections = envSecretHeaders.map((h) => {
|
|
2447
|
+
const headerIdx = h.idx;
|
|
2448
|
+
const preambleStart = findPreambleStart(lines, headerIdx);
|
|
2449
|
+
const body = lines.slice(headerIdx, trueBodyRange(headerIdx).end);
|
|
2450
|
+
const preamble = lines.slice(preambleStart, headerIdx);
|
|
2451
|
+
return {
|
|
2452
|
+
kind: h.kind,
|
|
2453
|
+
key: h.key,
|
|
2454
|
+
body,
|
|
2455
|
+
preamble
|
|
2456
|
+
};
|
|
2457
|
+
});
|
|
2458
|
+
const claimedRanges = envSecretHeaders.map((h) => {
|
|
2459
|
+
const headerIdx = h.idx;
|
|
2460
|
+
return {
|
|
2461
|
+
start: findPreambleStart(lines, headerIdx),
|
|
2462
|
+
end: trueBodyRange(headerIdx).end
|
|
2463
|
+
};
|
|
2464
|
+
});
|
|
2465
|
+
const isClaimed = (idx) => claimedRanges.some((r) => idx >= r.start && idx < r.end);
|
|
2466
|
+
return {
|
|
2467
|
+
preambleLines: lines.map((l, idx) => isClaimed(idx) ? null : l).filter((l) => l !== null),
|
|
2468
|
+
envSections: sections.filter((s) => s.kind === "env"),
|
|
2469
|
+
secretSections: sections.filter((s) => s.kind === "secret")
|
|
2470
|
+
};
|
|
2471
|
+
};
|
|
2472
|
+
const emitSection = (s) => {
|
|
2473
|
+
return `${s.preamble.length > 0 ? `${s.preamble.join("\n")}\n` : ""}${s.body.join("\n")}`;
|
|
2474
|
+
};
|
|
2475
|
+
/**
|
|
2476
|
+
* Reformat a TOML config with `[env.*]` and `[secret.*]` sections grouped and
|
|
2477
|
+
* alphabetized. Top-level content (version key, `[identity]`, `[lifecycle]`,
|
|
2478
|
+
* `[callbacks]`, `[tools]`, etc.) stays in its original position. Comment
|
|
2479
|
+
* doc-blocks immediately above a section header travel with that section.
|
|
2480
|
+
*
|
|
2481
|
+
* Pure — no I/O. Returns the raw input unchanged when there is no env or
|
|
2482
|
+
* secret content to reorder.
|
|
2483
|
+
*/
|
|
2484
|
+
const sortConfigToml = (raw) => {
|
|
2485
|
+
const { preambleLines, envSections, secretSections } = partitionSections(raw);
|
|
2486
|
+
if (envSections.length === 0 && secretSections.length === 0) return raw;
|
|
2487
|
+
const sortedEnv = [...envSections].sort((a, b) => a.key.localeCompare(b.key));
|
|
2488
|
+
const sortedSecret = [...secretSections].sort((a, b) => a.key.localeCompare(b.key));
|
|
2489
|
+
const trimTrailing = (xs) => {
|
|
2490
|
+
const lastNonBlank = xs.findLastIndex((l) => l.trim() !== "");
|
|
2491
|
+
return lastNonBlank === -1 ? [] : xs.slice(0, lastNonBlank + 1);
|
|
2492
|
+
};
|
|
2493
|
+
const collapseBlanks = (xs) => xs.reduce((acc, line) => {
|
|
2494
|
+
const isBlank = line.trim() === "";
|
|
2495
|
+
const prevBlank = acc.length > 0 && acc[acc.length - 1].trim() === "";
|
|
2496
|
+
if (isBlank && prevBlank) return acc;
|
|
2497
|
+
return [...acc, line];
|
|
2498
|
+
}, []);
|
|
2499
|
+
const preambleTrimmed = collapseBlanks(trimTrailing(preambleLines));
|
|
2500
|
+
const parts = [];
|
|
2501
|
+
if (preambleTrimmed.length > 0) parts.push(preambleTrimmed.join("\n"));
|
|
2502
|
+
if (sortedEnv.length > 0) parts.push(sortedEnv.map(emitSection).join("\n\n"));
|
|
2503
|
+
if (sortedSecret.length > 0) parts.push(sortedSecret.map(emitSection).join("\n\n"));
|
|
2504
|
+
return `${parts.join("\n\n")}\n`;
|
|
2505
|
+
};
|
|
2363
2506
|
//#endregion
|
|
2364
2507
|
//#region src/core/validate.ts
|
|
2365
2508
|
/**
|
|
@@ -3000,6 +3143,7 @@ const printSecretMeta = (meta, indent) => {
|
|
|
3000
3143
|
console.log(`${indent}${DIM}tags:${RESET} ${tagStr}`);
|
|
3001
3144
|
}
|
|
3002
3145
|
};
|
|
3146
|
+
const sortedIfRequested = (entries, sort) => sort ? [...entries].sort((a, b) => a[0].localeCompare(b[0])) : entries;
|
|
3003
3147
|
const printConfig = (config, path, resolveResult, opts) => {
|
|
3004
3148
|
console.log(`${BOLD}envpkt.toml${RESET} ${DIM}(${path})${RESET}`);
|
|
3005
3149
|
if (resolveResult?.catalogPath) console.log(`${DIM}Catalog: ${CYAN}${resolveResult.catalogPath}${RESET}`);
|
|
@@ -3017,7 +3161,7 @@ const printConfig = (config, path, resolveResult, opts) => {
|
|
|
3017
3161
|
}
|
|
3018
3162
|
const secretEntries = config.secret ?? {};
|
|
3019
3163
|
console.log(`${BOLD}Secrets:${RESET} ${Object.keys(secretEntries).length}`);
|
|
3020
|
-
Object.entries(secretEntries).forEach(([key, meta]) => {
|
|
3164
|
+
sortedIfRequested(Object.entries(secretEntries), opts?.sort).forEach(([key, meta]) => {
|
|
3021
3165
|
const valueSuffix = Option(opts?.secrets?.[key]).fold(() => "", (secretValue) => ` = ${YELLOW}${(opts?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}${RESET}`);
|
|
3022
3166
|
const sealedTag = meta.encrypted_value ? ` ${CYAN}[sealed]${RESET}` : "";
|
|
3023
3167
|
console.log(` ${BOLD}${key}${RESET} → ${meta.service ?? key}${sealedTag}${valueSuffix}`);
|
|
@@ -3028,7 +3172,7 @@ const printConfig = (config, path, resolveResult, opts) => {
|
|
|
3028
3172
|
if (envKeys.length > 0) {
|
|
3029
3173
|
console.log("");
|
|
3030
3174
|
console.log(`${BOLD}Environment Defaults:${RESET} ${envKeys.length}`);
|
|
3031
|
-
Object.entries(envEntries).forEach(([key, entry]) => {
|
|
3175
|
+
sortedIfRequested(Object.entries(envEntries), opts?.sort).forEach(([key, entry]) => {
|
|
3032
3176
|
console.log(` ${BOLD}${key}${RESET} = ${GREEN}"${entry.value}"${RESET}`);
|
|
3033
3177
|
if (entry.purpose) console.log(` ${DIM}purpose:${RESET} ${entry.purpose}`);
|
|
3034
3178
|
if (entry.comment) console.log(` ${DIM}comment:${RESET} ${DIM}${entry.comment}${RESET}`);
|
|
@@ -3092,7 +3236,10 @@ const runInspect = (options) => {
|
|
|
3092
3236
|
secretDisplay: options.plaintext ? "plaintext" : "encrypted"
|
|
3093
3237
|
};
|
|
3094
3238
|
})() : void 0;
|
|
3095
|
-
printConfig(showConfig, path, showResolved ? resolveResult : void 0,
|
|
3239
|
+
printConfig(showConfig, path, showResolved ? resolveResult : void 0, {
|
|
3240
|
+
...printOpts ?? {},
|
|
3241
|
+
sort: options.sort
|
|
3242
|
+
});
|
|
3096
3243
|
});
|
|
3097
3244
|
});
|
|
3098
3245
|
});
|
|
@@ -3703,6 +3850,12 @@ const runSeal = async (options) => {
|
|
|
3703
3850
|
console.error(`${DIM}Available keys: ${Object.keys(allSecretEntries).join(", ")}${RESET}`);
|
|
3704
3851
|
process.exit(2);
|
|
3705
3852
|
}
|
|
3853
|
+
const aliasKeys = editKeys.filter((k) => allSecretEntries[k].from_key !== void 0);
|
|
3854
|
+
if (aliasKeys.length > 0) {
|
|
3855
|
+
console.error(`${RED}Error:${RESET} Cannot seal alias entries: ${aliasKeys.join(", ")}`);
|
|
3856
|
+
console.error(`${DIM}Aliases reference another secret's value via from_key — seal the target instead.${RESET}`);
|
|
3857
|
+
process.exit(2);
|
|
3858
|
+
}
|
|
3706
3859
|
if (!process.stdin.isTTY) {
|
|
3707
3860
|
console.error(`${RED}Error:${RESET} --edit requires an interactive terminal`);
|
|
3708
3861
|
process.exit(2);
|
|
@@ -3741,9 +3894,14 @@ const runSeal = async (options) => {
|
|
|
3741
3894
|
return;
|
|
3742
3895
|
}
|
|
3743
3896
|
const allSecretEntries = config.secret ?? {};
|
|
3744
|
-
const allKeys = Object.keys(allSecretEntries);
|
|
3897
|
+
const allKeys = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key === void 0);
|
|
3898
|
+
const skippedAliases = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key !== void 0);
|
|
3745
3899
|
const alreadySealed = allKeys.filter((k) => allSecretEntries[k].encrypted_value);
|
|
3746
3900
|
const unsealed = allKeys.filter((k) => !allSecretEntries[k].encrypted_value);
|
|
3901
|
+
if (skippedAliases.length > 0) {
|
|
3902
|
+
const noun = skippedAliases.length === 1 ? "alias" : "aliases";
|
|
3903
|
+
console.error(`${DIM}Skipping ${skippedAliases.length} ${noun}: ${skippedAliases.join(", ")}${RESET}`);
|
|
3904
|
+
}
|
|
3747
3905
|
if (!options.reseal && alreadySealed.length > 0) {
|
|
3748
3906
|
if (unsealed.length === 0) {
|
|
3749
3907
|
console.log(`${GREEN}✓${RESET} All ${BOLD}${alreadySealed.length}${RESET} secret(s) already sealed. Use ${CYAN}--reseal${RESET} to re-encrypt.`);
|
|
@@ -4182,6 +4340,45 @@ const runShellHook = (shell) => {
|
|
|
4182
4340
|
}
|
|
4183
4341
|
};
|
|
4184
4342
|
//#endregion
|
|
4343
|
+
//#region src/cli/commands/sort.ts
|
|
4344
|
+
const SECTION_HEADER_RE = /^\[(env|secret)\.(.+)\]\s*$/;
|
|
4345
|
+
const countSections = (raw) => raw.split("\n").reduce((acc, line) => {
|
|
4346
|
+
const m = SECTION_HEADER_RE.exec(line);
|
|
4347
|
+
if (!m) return acc;
|
|
4348
|
+
return m[1] === "env" ? {
|
|
4349
|
+
...acc,
|
|
4350
|
+
env: acc.env + 1
|
|
4351
|
+
} : {
|
|
4352
|
+
...acc,
|
|
4353
|
+
secret: acc.secret + 1
|
|
4354
|
+
};
|
|
4355
|
+
}, {
|
|
4356
|
+
env: 0,
|
|
4357
|
+
secret: 0
|
|
4358
|
+
});
|
|
4359
|
+
const runSort = (options) => {
|
|
4360
|
+
resolveConfigPath(options.config).fold((err) => {
|
|
4361
|
+
console.error(formatError(err));
|
|
4362
|
+
process.exit(2);
|
|
4363
|
+
}, ({ path: configPath, source }) => {
|
|
4364
|
+
const sourceMsg = formatConfigSource(configPath, source);
|
|
4365
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
4366
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
4367
|
+
const sorted = sortConfigToml(raw);
|
|
4368
|
+
const counts = countSections(sorted);
|
|
4369
|
+
if (sorted === raw) {
|
|
4370
|
+
console.log(`${GREEN}✓${RESET} ${BOLD}Already sorted${RESET} — ${counts.env} env, ${counts.secret} secret`);
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
if (options.dryRun) {
|
|
4374
|
+
console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
|
|
4375
|
+
console.log(sorted);
|
|
4376
|
+
return;
|
|
4377
|
+
}
|
|
4378
|
+
writeIfValid(configPath, sorted, `${GREEN}✓${RESET} Sorted ${BOLD}${counts.env}${RESET} env and ${BOLD}${counts.secret}${RESET} secret entries in ${CYAN}${configPath}${RESET}`);
|
|
4379
|
+
});
|
|
4380
|
+
};
|
|
4381
|
+
//#endregion
|
|
4185
4382
|
//#region src/cli/commands/upgrade.ts
|
|
4186
4383
|
const getCurrentVersion = () => Try(() => execFileSync("npm", [
|
|
4187
4384
|
"list",
|
|
@@ -4438,7 +4635,7 @@ program.command("init").description("Initialize a new envpkt.toml in the current
|
|
|
4438
4635
|
program.command("keygen").description("Generate an age keypair for sealing secrets — run this before `seal` if you don't have a key yet").option("-c, --config <path>", "Path to envpkt.toml (updates identity.recipient if found)").option("-o, --output <path>", "Output path for identity file (default: ~/.envpkt/<project>-key.txt)").option("--global", "Write key to the shared default path (~/.envpkt/age-key.txt) instead of a project-specific one").action((options) => {
|
|
4439
4636
|
runKeygen(options);
|
|
4440
4637
|
});
|
|
4441
|
-
program.command("audit").description("Audit credential health from envpkt.toml (use --strict in CI pipelines to gate deploys)").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json | minimal", "table").option("--expiring <days>", "Show secrets expiring within N days", parseInt).option("--status <status>", "Filter by status: healthy | expiring_soon | expired | stale | missing").option("--strict", "Exit non-zero on any non-healthy secret").option("--all", "Show both secrets and env defaults").option("--env-only", "Show only env defaults (drift detection)").option("--sealed", "Show only secrets with encrypted_value").option("--external", "Show only secrets without encrypted_value").action((options) => {
|
|
4638
|
+
program.command("audit").description("Audit credential health from envpkt.toml (use --strict in CI pipelines to gate deploys)").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json | minimal", "table").option("--expiring <days>", "Show secrets expiring within N days", parseInt).option("--status <status>", "Filter by status: healthy | expiring_soon | expired | stale | missing").option("--strict", "Exit non-zero on any non-healthy secret").option("--all", "Show both secrets and env defaults").option("--env-only", "Show only env defaults (drift detection)").option("--sealed", "Show only secrets with encrypted_value").option("--external", "Show only secrets without encrypted_value").option("--sort", "Sort secrets alphabetically within each status bucket (no file mutation)").action((options) => {
|
|
4442
4639
|
runAudit(options);
|
|
4443
4640
|
});
|
|
4444
4641
|
program.command("fleet").description("Scan directory tree for envpkt.toml files and aggregate health (use in CI for fleet-wide monitoring)").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) => {
|
|
@@ -4447,7 +4644,7 @@ program.command("fleet").description("Scan directory tree for envpkt.toml files
|
|
|
4447
4644
|
program.command("validate").description("Verify envpkt.toml integrity — runs TOML syntax, schema, catalog, alias, and sealed-block structural checks").option("-c, --config <path>", "Path to envpkt.toml").option("--json", "Output structured JSON instead of human-readable text").action((options) => {
|
|
4448
4645
|
runValidate(options);
|
|
4449
4646
|
});
|
|
4450
|
-
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) => {
|
|
4647
|
+
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)").option("--sort", "Display secrets and env entries in alphabetical order (no file mutation)").action((options) => {
|
|
4451
4648
|
runInspect(options);
|
|
4452
4649
|
});
|
|
4453
4650
|
program.command("exec").description("Run pre-flight audit then execute a command with injected secrets (sealed → fnox → env cascade)").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) => {
|
|
@@ -4464,6 +4661,9 @@ program.command("mcp").description("Start the envpkt MCP server (stdio transport
|
|
|
4464
4661
|
});
|
|
4465
4662
|
registerSecretCommands(program);
|
|
4466
4663
|
registerEnvCommands(program);
|
|
4664
|
+
program.command("sort").description("Group [env.*] and [secret.*] sections and alphabetize within each region").option("-c, --config <path>", "Path to envpkt.toml").option("--dry-run", "Preview the result without writing").action((options) => {
|
|
4665
|
+
runSort(options);
|
|
4666
|
+
});
|
|
4467
4667
|
program.command("upgrade").description("Upgrade envpkt to the latest version (npm install -g envpkt@latest)").action(() => {
|
|
4468
4668
|
runUpgrade();
|
|
4469
4669
|
});
|