morphix-env 0.8.0 → 0.9.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/README.md CHANGED
@@ -90,6 +90,31 @@ This is leakage minimization, not a cryptographic boundary: a process that recei
90
90
 
91
91
  `mx-env run` without `--serve` is unchanged: values are injected into the environment and redacted from forwarded output.
92
92
 
93
+ ## Terminal shims
94
+
95
+ For commands you run constantly, shims remove the `mx-env run --` prefix entirely. Declare a command list and generate shims once:
96
+
97
+ ```jsonc
98
+ // ~/.mx-env/config.json (global) and/or mx-env.config.json (project)
99
+ { "shims": ["node", "pnpm"] }
100
+ ```
101
+
102
+ ```bash
103
+ mx-env shell-init # generate shims in ~/.mx-env/shims, print the PATH line
104
+ mx-env shell-init --write # also maintain a marked block in ~/.zshrc / ~/.bashrc
105
+ mx-env shell-init --remove # remove shims and the rc block
106
+ ```
107
+
108
+ After the PATH line is active, typing `pnpm dev` is exactly `mx-env run -- pnpm dev`: same composition, same redaction, same exit code. Undeclared commands are untouched.
109
+
110
+ - The effective list is the union of the global and project lists. A shim is only an entry hook: **what gets injected is always decided by the configuration in effect where the command runs**, so one `pnpm` shim serves every project's own profile.
111
+ - `run` removes the shims directory from the child's PATH before composing, so a tool invoked again inside the run (or by a provider plugin mid-composition) resolves to the real binary — injection happens exactly once, never recursively.
112
+ - `shell-init` is idempotent: re-running converges the directory to the declared list, `--write`/`--remove` use marked rc blocks and never touch your other lines or handmade files.
113
+ - Command names are validated (bare names only; mx-env's own command names are reserved). Invalid entries fail `shell-init` loudly but only warn in composition paths — a convenience layer never breaks `run`.
114
+ - Audit events record `via: "shim"` vs `"cli"` so the two channels stay distinguishable.
115
+
116
+ **Boundary.** Shims buy convenience, not concealment: injected values live in the child's environment and are visible to same-uid processes. When the consumer is an agent you do not trust with the values, use `run --serve` instead. macOS/Linux only; Windows keeps the explicit `run` form. Do not place files in `~/.mx-env/shims` by hand.
117
+
93
118
  ## Provider plugins
94
119
 
95
120
  Any executable on PATH named `mx-env-provider-<name>` is a provider — new remote stores need no changes to `config.ts` / `sources.ts` / `editor.ts`:
package/dist/SKILL.md CHANGED
@@ -71,8 +71,11 @@ mx-env doc # 生成本项目的 key→source 工
71
71
  mx-env audit [tail] # 值无关的本地审计事件
72
72
  mx-env skill install [--target global|project|trellis] [--force] # 安装本 Skill
73
73
  mx-env skill show # 查看各目标安装状态
74
+ mx-env shell-init [--write|--remove] # 为声明的命令生成 PATH shim(shims: ["node","pnpm"] 配置项)
74
75
  ```
75
76
 
77
+ **shim 消费指引**:配置里声明过 `shims` 且执行过 `shell-init` 的机器上,裸敲被声明命令(如 `pnpm dev`)等价于 `mx-env run -- pnpm dev`——你可以依赖它,无需加前缀。但 shim 是方便性不是隐藏:值仍在子进程 env。**需要值不进 env 的场景(高风险 agent 能力、不可信子进程树)必须显式用 `run --serve`**,不要依赖 shim。audit 里 `via: "shim"` 与 `"cli"` 可区分两种来路。
78
+
76
79
  `export` / `render` / `inject` / `run` 均支持 `--dry-run`(只报 key/变量名,不出值)。值永不出现在 shell 历史里:`eval "$(mx-env export --profile dev)"` 才是 export 的消费方式。
77
80
 
78
81
  ## 进入一个新项目的标准流程
package/dist/cli.js CHANGED
@@ -29482,9 +29482,9 @@ var require_jsx_runtime = __commonJS({
29482
29482
 
29483
29483
  // src/cli.ts
29484
29484
  var import_cross_spawn = __toESM(require("cross-spawn"));
29485
- var import_fs9 = require("fs");
29486
- var import_os6 = require("os");
29487
- var import_path9 = require("path");
29485
+ var import_fs10 = require("fs");
29486
+ var import_os7 = require("os");
29487
+ var import_path10 = require("path");
29488
29488
 
29489
29489
  // src/audit.ts
29490
29490
  var import_fs = require("fs");
@@ -37437,10 +37437,267 @@ async function startSecretServer(values) {
37437
37437
  };
37438
37438
  }
37439
37439
 
37440
- // src/template.ts
37441
- var import_child_process4 = require("child_process");
37440
+ // src/shims.ts
37442
37441
  var import_fs8 = require("fs");
37442
+ var import_os6 = require("os");
37443
37443
  var import_path8 = require("path");
37444
+ var SHIM_MARKER = "# mx-env shim \u2014 regenerated by `mx-env shell-init`; do not edit by hand.";
37445
+ var RESERVED_SHIM_NAMES = [
37446
+ "run",
37447
+ "fetch",
37448
+ "export",
37449
+ "render",
37450
+ "inject",
37451
+ "generate",
37452
+ "gen",
37453
+ "inspect",
37454
+ "audit",
37455
+ "edit",
37456
+ "doc",
37457
+ "skill",
37458
+ "shell-init",
37459
+ "help",
37460
+ "mx-env",
37461
+ "morphix-env",
37462
+ "__mx_editor"
37463
+ ];
37464
+ var SHIM_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
37465
+ function shimsDir(homeDir = (0, import_os6.homedir)()) {
37466
+ return (0, import_path8.join)(homeDir, ".mx-env", "shims");
37467
+ }
37468
+ function validateShimList(list, label) {
37469
+ if (list === void 0 || list === null) return [];
37470
+ if (!Array.isArray(list)) return [`${label}: "shims" must be an array of command names`];
37471
+ const errors = [];
37472
+ const seen = /* @__PURE__ */ new Set();
37473
+ for (const entry of list) {
37474
+ if (typeof entry !== "string" || !entry) {
37475
+ errors.push(`${label}: shim entries must be non-empty command names`);
37476
+ continue;
37477
+ }
37478
+ if (!SHIM_NAME_PATTERN.test(entry)) {
37479
+ errors.push(`${label}: shim entry "${entry}" must be a bare command name (letters, digits, ".", "_", "-"; no path separators or whitespace)`);
37480
+ continue;
37481
+ }
37482
+ const lower = entry.toLowerCase();
37483
+ if (RESERVED_SHIM_NAMES.includes(lower)) {
37484
+ errors.push(`${label}: shim entry "${entry}" conflicts with a reserved mx-env command name`);
37485
+ continue;
37486
+ }
37487
+ if (seen.has(lower)) {
37488
+ errors.push(`${label}: duplicate shim entry "${entry}" (comparison is case-insensitive)`);
37489
+ continue;
37490
+ }
37491
+ seen.add(lower);
37492
+ }
37493
+ return errors;
37494
+ }
37495
+ function resolveShimCommands(layers, includeGlobal = true) {
37496
+ const seen = /* @__PURE__ */ new Set();
37497
+ const commands = [];
37498
+ const push = (list) => {
37499
+ if (!Array.isArray(list)) return;
37500
+ for (const entry of list) {
37501
+ if (typeof entry !== "string") continue;
37502
+ const lower = entry.toLowerCase();
37503
+ if (seen.has(lower)) continue;
37504
+ seen.add(lower);
37505
+ commands.push(entry);
37506
+ }
37507
+ };
37508
+ if (includeGlobal) push(layers.global.config.shims);
37509
+ push(layers.project.config.shims);
37510
+ return commands;
37511
+ }
37512
+ function renderShimScript(command) {
37513
+ return `#!/bin/sh
37514
+ ${SHIM_MARKER}
37515
+ command -v mx-env >/dev/null 2>&1 || { echo "[mx-env] mx-env not on PATH; reinstall morphix-env or run 'mx-env shell-init --remove' to clean up this shim." >&2; exit 127; }
37516
+ MXENV_SHIM=1 exec mx-env run -- ${command} "$@"
37517
+ `;
37518
+ }
37519
+ function readIfExists(path) {
37520
+ try {
37521
+ return (0, import_fs8.readFileSync)(path, "utf8");
37522
+ } catch {
37523
+ return null;
37524
+ }
37525
+ }
37526
+ function writeShims(commands, homeDir = (0, import_os6.homedir)()) {
37527
+ const dir = shimsDir(homeDir);
37528
+ (0, import_fs8.mkdirSync)(dir, { recursive: true, mode: 448 });
37529
+ const wanted = new Set(commands.map((command) => command.toLowerCase()));
37530
+ const written = [];
37531
+ const removed = [];
37532
+ const skipped = [];
37533
+ for (const file of (0, import_fs8.readdirSync)(dir)) {
37534
+ const path = (0, import_path8.join)(dir, file);
37535
+ const content = readIfExists(path);
37536
+ if (content === null || !content.includes(SHIM_MARKER)) continue;
37537
+ if (!wanted.has(file.toLowerCase())) {
37538
+ (0, import_fs8.rmSync)(path);
37539
+ removed.push(file);
37540
+ }
37541
+ }
37542
+ for (const command of commands) {
37543
+ const path = (0, import_path8.join)(dir, command);
37544
+ const content = renderShimScript(command);
37545
+ let isSymlink = false;
37546
+ try {
37547
+ isSymlink = (0, import_fs8.lstatSync)(path).isSymbolicLink();
37548
+ } catch {
37549
+ }
37550
+ const existing = isSymlink ? null : readIfExists(path);
37551
+ if (!isSymlink && existing !== null && !existing.includes(SHIM_MARKER)) {
37552
+ skipped.push(command);
37553
+ continue;
37554
+ }
37555
+ if (isSymlink) (0, import_fs8.rmSync)(path);
37556
+ else if (existing === content) continue;
37557
+ (0, import_fs8.writeFileSync)(path, content, { mode: 493 });
37558
+ (0, import_fs8.chmodSync)(path, 493);
37559
+ written.push(command);
37560
+ }
37561
+ return { written, removed, skipped };
37562
+ }
37563
+ function removeAllShims(homeDir = (0, import_os6.homedir)()) {
37564
+ const dir = shimsDir(homeDir);
37565
+ if (!(0, import_fs8.existsSync)(dir)) return [];
37566
+ const removed = [];
37567
+ for (const file of (0, import_fs8.readdirSync)(dir)) {
37568
+ const path = (0, import_path8.join)(dir, file);
37569
+ const content = readIfExists(path);
37570
+ if (content !== null && content.includes(SHIM_MARKER)) {
37571
+ (0, import_fs8.rmSync)(path);
37572
+ removed.push(file);
37573
+ }
37574
+ }
37575
+ return removed;
37576
+ }
37577
+ function detectHandmadeReservedShims(homeDir = (0, import_os6.homedir)()) {
37578
+ const dir = shimsDir(homeDir);
37579
+ if (!(0, import_fs8.existsSync)(dir)) return [];
37580
+ const found = [];
37581
+ for (const file of (0, import_fs8.readdirSync)(dir)) {
37582
+ if (!RESERVED_SHIM_NAMES.includes(file.toLowerCase())) continue;
37583
+ const content = readIfExists((0, import_path8.join)(dir, file));
37584
+ if (content === null || !content.includes(SHIM_MARKER)) found.push(file);
37585
+ }
37586
+ return found;
37587
+ }
37588
+ var RC_BLOCK_BEGIN = "# >>> mx-env shims >>>";
37589
+ var RC_BLOCK_END = "# <<< mx-env shims <<<";
37590
+ function rcBlock(homeDir = (0, import_os6.homedir)()) {
37591
+ const dir = shimsDir(homeDir);
37592
+ if (/["$`\\]/.test(dir)) {
37593
+ throw new Error("[morphix-env] Shims directory path contains characters that are unsafe in a shell rc line.");
37594
+ }
37595
+ return `${RC_BLOCK_BEGIN}
37596
+ export PATH="${dir}:$PATH"
37597
+ ${RC_BLOCK_END}`;
37598
+ }
37599
+ function defaultRcPath(shell, homeDir = (0, import_os6.homedir)()) {
37600
+ const name = shell ? shell.split("/").pop() : "";
37601
+ if (name === "zsh") return (0, import_path8.join)(homeDir, ".zshrc");
37602
+ if (name === "bash") return (0, import_path8.join)(homeDir, ".bashrc");
37603
+ return null;
37604
+ }
37605
+ function writeFileAtomic(path, content) {
37606
+ const tmp = `${path}.mx-env-${process.pid}.tmp`;
37607
+ (0, import_fs8.writeFileSync)(tmp, content);
37608
+ (0, import_fs8.renameSync)(tmp, path);
37609
+ }
37610
+ function upsertRcBlock(rcPath, homeDir = (0, import_os6.homedir)()) {
37611
+ const existing = readIfExists(rcPath) ?? "";
37612
+ const block = rcBlock(homeDir);
37613
+ const pattern = new RegExp(`${escapeRegExp(RC_BLOCK_BEGIN)}[\\s\\S]*?${escapeRegExp(RC_BLOCK_END)}`, "g");
37614
+ if (pattern.test(existing)) {
37615
+ const replaced = existing.replace(pattern, block);
37616
+ if (replaced === existing) return "unchanged";
37617
+ writeFileAtomic(rcPath, replaced.endsWith("\n") ? replaced : `${replaced}
37618
+ `);
37619
+ return "updated";
37620
+ }
37621
+ const separator = existing.length && !existing.endsWith("\n") ? "\n" : "";
37622
+ writeFileAtomic(rcPath, `${existing}${separator}${block}
37623
+ `);
37624
+ return "added";
37625
+ }
37626
+ function removeRcBlock(rcPath) {
37627
+ const existing = readIfExists(rcPath);
37628
+ if (existing === null) return "absent";
37629
+ const pattern = new RegExp(`\\n?${escapeRegExp(RC_BLOCK_BEGIN)}[\\s\\S]*?${escapeRegExp(RC_BLOCK_END)}\\n?`, "g");
37630
+ if (!pattern.test(existing)) return "absent";
37631
+ writeFileAtomic(rcPath, existing.replace(pattern, "\n"));
37632
+ return "removed";
37633
+ }
37634
+ function escapeRegExp(text) {
37635
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37636
+ }
37637
+ function stripShimsFromPath(pathValue, homeDir = (0, import_os6.homedir)()) {
37638
+ if (!pathValue) return pathValue;
37639
+ const target = shimsDir(homeDir);
37640
+ let targetReal = target;
37641
+ try {
37642
+ targetReal = (0, import_fs8.realpathSync)(target);
37643
+ } catch {
37644
+ }
37645
+ const equal = (a, b) => process.platform === "darwin" ? a.toLowerCase() === b.toLowerCase() : a === b;
37646
+ const entries = pathValue.split(import_path8.delimiter).filter((entry) => {
37647
+ if (!entry) return true;
37648
+ const literal = entry.replace(/\/+$/, "");
37649
+ let resolved = literal;
37650
+ try {
37651
+ resolved = (0, import_fs8.realpathSync)(literal);
37652
+ } catch {
37653
+ }
37654
+ return !equal(literal, target) && !equal(resolved, targetReal);
37655
+ });
37656
+ return entries.join(import_path8.delimiter);
37657
+ }
37658
+ function sanitizeBootEnv(env = process.env, homeDir) {
37659
+ const originalPath = env.PATH;
37660
+ const viaShim = env.MXENV_SHIM === "1";
37661
+ try {
37662
+ const home = homeDir ?? (0, import_os6.homedir)();
37663
+ const stripped = stripShimsFromPath(originalPath, home);
37664
+ if (stripped !== originalPath) {
37665
+ if (stripped) env.PATH = stripped;
37666
+ else delete env.PATH;
37667
+ }
37668
+ } catch {
37669
+ }
37670
+ delete env.MXENV_SHIM;
37671
+ return { viaShim, originalPath };
37672
+ }
37673
+ function findShadowedCommands(commands, originalPath, homeDir = (0, import_os6.homedir)()) {
37674
+ if (!originalPath) return { absent: [...commands], shadowed: [] };
37675
+ const dirs = originalPath.split(import_path8.delimiter).filter(Boolean);
37676
+ const absent = [];
37677
+ const shadowed = [];
37678
+ for (const command of commands) {
37679
+ const shimPath = (0, import_path8.join)(shimsDir(homeDir), command);
37680
+ let resolvedTo = null;
37681
+ for (const dir of dirs) {
37682
+ const candidate = (0, import_path8.join)(dir, command);
37683
+ try {
37684
+ (0, import_fs8.accessSync)(candidate, import_fs8.constants.X_OK);
37685
+ resolvedTo = candidate;
37686
+ break;
37687
+ } catch {
37688
+ }
37689
+ }
37690
+ const samePath = (a, b) => process.platform === "darwin" ? a.toLowerCase() === b.toLowerCase() : a === b;
37691
+ if (resolvedTo === null) absent.push(command);
37692
+ else if (!samePath(resolvedTo, shimPath)) shadowed.push(command);
37693
+ }
37694
+ return { absent, shadowed };
37695
+ }
37696
+
37697
+ // src/template.ts
37698
+ var import_child_process4 = require("child_process");
37699
+ var import_fs9 = require("fs");
37700
+ var import_path9 = require("path");
37444
37701
  function shellQuote(value) {
37445
37702
  return `'${value.replace(/'/g, "'\\''")}'`;
37446
37703
  }
@@ -37487,15 +37744,15 @@ function renderTemplate(text, environment) {
37487
37744
  return { output, missing: [...missing] };
37488
37745
  }
37489
37746
  function writePrivateFile(path, content) {
37490
- const absolute = (0, import_path8.resolve)(path);
37491
- (0, import_fs8.mkdirSync)((0, import_path8.dirname)(absolute), { recursive: true, mode: 448 });
37492
- (0, import_fs8.writeFileSync)(absolute, content, { mode: 384 });
37493
- (0, import_fs8.chmodSync)(absolute, 384);
37747
+ const absolute = (0, import_path9.resolve)(path);
37748
+ (0, import_fs9.mkdirSync)((0, import_path9.dirname)(absolute), { recursive: true, mode: 448 });
37749
+ (0, import_fs9.writeFileSync)(absolute, content, { mode: 384 });
37750
+ (0, import_fs9.chmodSync)(absolute, 384);
37494
37751
  }
37495
37752
  function gitOutputStatus(path) {
37496
- const absolute = (0, import_path8.resolve)(path);
37497
- const cwd = (0, import_path8.dirname)(absolute);
37498
- const name = (0, import_path8.basename)(absolute);
37753
+ const absolute = (0, import_path9.resolve)(path);
37754
+ const cwd = (0, import_path9.dirname)(absolute);
37755
+ const name = (0, import_path9.basename)(absolute);
37499
37756
  const inside = (0, import_child_process4.spawnSync)("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8" });
37500
37757
  if (inside.status !== 0) return "outside-repo";
37501
37758
  const tracked = (0, import_child_process4.spawnSync)("git", ["ls-files", "--error-unmatch", "--", name], { cwd, encoding: "utf8" });
@@ -37517,7 +37774,7 @@ function warnOutputGitRisk(path) {
37517
37774
  // package.json
37518
37775
  var package_default = {
37519
37776
  name: "morphix-env",
37520
- version: "0.8.0",
37777
+ version: "0.9.0",
37521
37778
  description: "Secret composition CLI \u2014 Infisical, Doppler, macOS Keychain, dotenvx, and local files stay value owners; composes them into one value-safe child environment and ships a built-in Agent Skill (mx-env skill install).",
37522
37779
  author: "MorphixAI <dev@morphixai.com>",
37523
37780
  homepage: "https://github.com/Morphicai/morphix-env#readme",
@@ -37624,7 +37881,10 @@ function parseArgs(argv) {
37624
37881
  target: null,
37625
37882
  force: false,
37626
37883
  dryRun: false,
37627
- serve: false
37884
+ serve: false,
37885
+ write: false,
37886
+ remove: false,
37887
+ unknownFlags: []
37628
37888
  };
37629
37889
  let i = 2;
37630
37890
  const command = argv[i];
@@ -37665,6 +37925,10 @@ function parseArgs(argv) {
37665
37925
  args.force = true;
37666
37926
  } else if (arg === "--dry-run") {
37667
37927
  args.dryRun = true;
37928
+ } else if (arg === "--write") {
37929
+ args.write = true;
37930
+ } else if (arg === "--remove") {
37931
+ args.remove = true;
37668
37932
  } else if (arg === "--serve") {
37669
37933
  args.serve = true;
37670
37934
  } else if (args.command === "run") {
@@ -37672,13 +37936,15 @@ function parseArgs(argv) {
37672
37936
  break;
37673
37937
  } else if (!arg.startsWith("-")) {
37674
37938
  args.positionals.push(arg);
37939
+ } else {
37940
+ args.unknownFlags.push(arg);
37675
37941
  }
37676
37942
  i++;
37677
37943
  }
37678
37944
  return args;
37679
37945
  }
37680
37946
  function configLayers() {
37681
- return loadConfigLayers({ cwd: process.cwd(), homeDir: (0, import_os6.homedir)() });
37947
+ return loadConfigLayers({ cwd: process.cwd(), homeDir: (0, import_os7.homedir)() });
37682
37948
  }
37683
37949
  function composeOptions(args) {
37684
37950
  return {
@@ -37688,14 +37954,29 @@ function composeOptions(args) {
37688
37954
  envName: args.env,
37689
37955
  allowInsecureGlobal: args.allowInsecureGlobal,
37690
37956
  envFiles: args.envFiles,
37691
- homeDir: (0, import_os6.homedir)()
37957
+ homeDir: (0, import_os7.homedir)()
37692
37958
  };
37693
37959
  }
37960
+ var bootInfo = { viaShim: false };
37961
+ var shimWarned = false;
37962
+ function warnInvalidShims(layers, includeGlobal) {
37963
+ if (shimWarned) return;
37964
+ shimWarned = true;
37965
+ const problems = [
37966
+ ...includeGlobal ? validateShimList(layers.global.config.shims, layers.global.path) : [],
37967
+ ...validateShimList(layers.project.config.shims, layers.project.path)
37968
+ ];
37969
+ if (problems.length) {
37970
+ console.warn(`[morphix-env] Ignoring invalid "shims" entries (${problems.length} problem(s)); run \`mx-env shell-init\` for the details.`);
37971
+ }
37972
+ }
37694
37973
  function resolveComposition(args) {
37695
- return composeEnvironment(configLayers(), composeOptions(args));
37974
+ const layers = configLayers();
37975
+ warnInvalidShims(layers, !args.noGlobal);
37976
+ return composeEnvironment(layers, composeOptions(args));
37696
37977
  }
37697
37978
  function generatedOptions(args) {
37698
- const config = loadConfigLayers({ cwd: process.cwd(), homeDir: (0, import_os6.homedir)() }).project.config;
37979
+ const config = loadConfigLayers({ cwd: process.cwd(), homeDir: (0, import_os7.homedir)() }).project.config;
37699
37980
  return {
37700
37981
  outFile: args.outFile || config.generate?.out || "public/__env.js",
37701
37982
  filter: args.filter || config.generate?.filter || null
@@ -37704,12 +37985,15 @@ function generatedOptions(args) {
37704
37985
  function generateClientEnv(outFile, filter, environment) {
37705
37986
  let vars = extractPublicVars(environment);
37706
37987
  if (filter) vars = Object.fromEntries(Object.entries(vars).filter(([key]) => key.startsWith(filter)));
37707
- (0, import_fs9.mkdirSync)((0, import_path9.dirname)(outFile), { recursive: true });
37708
- (0, import_fs9.writeFileSync)(outFile, `window.__ENV=${JSON.stringify(vars)};`);
37988
+ (0, import_fs10.mkdirSync)((0, import_path10.dirname)(outFile), { recursive: true });
37989
+ (0, import_fs10.writeFileSync)(outFile, `window.__ENV=${JSON.stringify(vars)};`);
37709
37990
  console.log(`[morphix-env] Generated ${outFile} (${Object.keys(vars).length} client vars)`);
37710
37991
  }
37711
37992
  async function runChild(command, commandArgs, composition) {
37712
37993
  return new Promise((resolve7) => {
37994
+ if (composition.environment.PATH) {
37995
+ composition.environment.PATH = stripShimsFromPath(composition.environment.PATH) ?? composition.environment.PATH;
37996
+ }
37713
37997
  const child = (0, import_cross_spawn.default)(command, commandArgs, { env: composition.environment, stdio: ["inherit", "pipe", "pipe"] });
37714
37998
  const stdout = new StreamingRedactor(composition.redactionValues);
37715
37999
  const stderr = new StreamingRedactor(composition.redactionValues);
@@ -37736,7 +38020,8 @@ function writeRunAudit(composition, startedAt, outcome, exitCode) {
37736
38020
  keys: Object.keys(composition.provenance).sort(),
37737
38021
  outcome,
37738
38022
  exitCode,
37739
- durationMs: Date.now() - startedAt
38023
+ durationMs: Date.now() - startedAt,
38024
+ via: bootInfo.viaShim ? "shim" : "cli"
37740
38025
  });
37741
38026
  }
37742
38027
  async function startServeFor(composition, verbose) {
@@ -37802,7 +38087,7 @@ async function cmdRun(args) {
37802
38087
  await resolveEnvironmentReferences(composition, args);
37803
38088
  if (args.outFile || generatedOptions(args).outFile) {
37804
38089
  const { outFile, filter } = generatedOptions(args);
37805
- const configured = Boolean(args.outFile || loadConfigLayers({ cwd: process.cwd(), homeDir: (0, import_os6.homedir)() }).project.config.generate);
38090
+ const configured = Boolean(args.outFile || loadConfigLayers({ cwd: process.cwd(), homeDir: (0, import_os7.homedir)() }).project.config.generate);
37806
38091
  if (configured) generateClientEnv(outFile, filter, composition.environment);
37807
38092
  }
37808
38093
  const [command, ...commandArgs] = args.subArgs;
@@ -37907,11 +38192,11 @@ async function cmdRender(args) {
37907
38192
  console.error("[morphix-env] Usage: mx-env render <template-file> <output-file> [options]");
37908
38193
  return 1;
37909
38194
  }
37910
- if (!(0, import_fs9.existsSync)(templatePath)) {
38195
+ if (!(0, import_fs10.existsSync)(templatePath)) {
37911
38196
  console.error(`[morphix-env] Template not found: ${templatePath}`);
37912
38197
  return 1;
37913
38198
  }
37914
- const template = (0, import_fs9.readFileSync)(templatePath, "utf8");
38199
+ const template = (0, import_fs10.readFileSync)(templatePath, "utf8");
37915
38200
  const composition = await resolveComposition(args);
37916
38201
  const names = findTemplateVariables(template);
37917
38202
  const missing = names.filter((name) => composition.environment[name] === void 0 || composition.environment[name] === null);
@@ -37936,12 +38221,14 @@ async function cmdInject(args) {
37936
38221
  console.error("[morphix-env] Usage: mx-env inject <template-file> <output-file> [options]");
37937
38222
  return 1;
37938
38223
  }
37939
- if (!(0, import_fs9.existsSync)(templatePath)) {
38224
+ if (!(0, import_fs10.existsSync)(templatePath)) {
37940
38225
  console.error(`[morphix-env] Template not found: ${templatePath}`);
37941
38226
  return 1;
37942
38227
  }
37943
- const template = (0, import_fs9.readFileSync)(templatePath, "utf8");
37944
- const resolver = new ReferenceResolver(configLayers(), composeOptions(args));
38228
+ const template = (0, import_fs10.readFileSync)(templatePath, "utf8");
38229
+ const layers = configLayers();
38230
+ warnInvalidShims(layers, !args.noGlobal);
38231
+ const resolver = new ReferenceResolver(layers, composeOptions(args));
37945
38232
  const references = findMxEnvReferences(template);
37946
38233
  if (args.dryRun) {
37947
38234
  for (const reference of references) {
@@ -37974,7 +38261,7 @@ async function cmdEdit(args) {
37974
38261
  }
37975
38262
  const composition = await resolveComposition(args);
37976
38263
  if (args.source && !composition.sourceEntries.some((entry) => entry.name === args.source)) {
37977
- const layers = loadConfigLayers({ cwd: process.cwd(), homeDir: (0, import_os6.homedir)() });
38264
+ const layers = loadConfigLayers({ cwd: process.cwd(), homeDir: (0, import_os7.homedir)() });
37978
38265
  const declared = layers.project.config.sources?.[args.source] ? { source: layers.project.config.sources[args.source], baseDir: layers.project.baseDir } : !args.noGlobal && layers.global.config.sources?.[args.source] ? { source: layers.global.config.sources[args.source], baseDir: layers.global.baseDir } : null;
37979
38266
  if (!declared) {
37980
38267
  console.error(`[morphix-env] Unknown source "${args.source}".`);
@@ -37992,7 +38279,7 @@ async function cmdEdit(args) {
37992
38279
  }
37993
38280
  async function cmdDoc(args) {
37994
38281
  const composition = await resolveComposition(args);
37995
- const outFile = args.outFile || (0, import_path9.join)(process.cwd(), ".agents", "skills", "morphix-env", "SKILL.md");
38282
+ const outFile = args.outFile || (0, import_path10.join)(process.cwd(), ".agents", "skills", "morphix-env", "SKILL.md");
37996
38283
  const keyRows = Object.entries(composition.provenance).sort(([a], [b]) => a.localeCompare(b)).map(([key, provenance]) => `| \`${key}\` | ${provenance.source} (${provenance.provider}) |`).join("\n") || "| _No resolved keys_ | \u2014 |";
37997
38284
  const skill = `---
37998
38285
  name: morphix-env
@@ -38026,18 +38313,90 @@ ${keyRows}
38026
38313
 
38027
38314
  Secret values and value prefixes do not belong in this Skill, command output, audit records, or conversation context.
38028
38315
  `;
38029
- (0, import_fs9.mkdirSync)((0, import_path9.dirname)(outFile), { recursive: true });
38030
- (0, import_fs9.writeFileSync)(outFile, skill, { mode: 384 });
38316
+ (0, import_fs10.mkdirSync)((0, import_path10.dirname)(outFile), { recursive: true });
38317
+ (0, import_fs10.writeFileSync)(outFile, skill, { mode: 384 });
38031
38318
  console.log(`[morphix-env] Generated value-free workflow Skill: ${outFile}`);
38032
38319
  return 0;
38033
38320
  }
38321
+ function cmdShellInit(args) {
38322
+ if (process.platform === "win32") {
38323
+ console.error("[morphix-env] shell-init is supported on macOS/Linux only; on Windows keep using explicit `mx-env run -- <command>`.");
38324
+ return 1;
38325
+ }
38326
+ if (args.unknownFlags.length) {
38327
+ console.error(`[morphix-env] shell-init: unrecognized flag(s): ${args.unknownFlags.join(", ")}`);
38328
+ return 1;
38329
+ }
38330
+ if (args.write && args.remove) {
38331
+ console.error("[morphix-env] shell-init: --write and --remove are mutually exclusive.");
38332
+ return 1;
38333
+ }
38334
+ try {
38335
+ return runShellInit(args);
38336
+ } catch (error) {
38337
+ const code = error?.code;
38338
+ console.error(`[morphix-env] shell-init failed${code ? ` (${code})` : ""}: check that your home directory and rc file are writable, then retry.`);
38339
+ return 1;
38340
+ }
38341
+ }
38342
+ function runShellInit(args) {
38343
+ const layers = configLayers();
38344
+ const home = (0, import_os7.homedir)();
38345
+ const rcPath = defaultRcPath(process.env.SHELL, home);
38346
+ if (args.remove) {
38347
+ const removed2 = removeAllShims(home);
38348
+ const rcOutcome = rcPath ? removeRcBlock(rcPath) : "absent";
38349
+ console.log(`[morphix-env] Removed ${removed2.length} shim(s)${removed2.length ? ` (${removed2.join(", ")})` : ""}; rc block ${rcOutcome}${rcPath ? ` in ${rcPath}` : ""}.`);
38350
+ return 0;
38351
+ }
38352
+ const problems = [
38353
+ ...validateShimList(layers.global.config.shims, layers.global.path),
38354
+ ...validateShimList(layers.project.config.shims, layers.project.path)
38355
+ ];
38356
+ if (problems.length) {
38357
+ for (const problem of problems) console.error(`[morphix-env] ${problem}`);
38358
+ return 1;
38359
+ }
38360
+ const commands = resolveShimCommands(layers, !args.noGlobal);
38361
+ const { written, removed, skipped } = writeShims(commands, home);
38362
+ console.log(`[morphix-env] Shims ready in ${shimsDir(home)} (${commands.length} command(s): ${commands.join(", ") || "none"}; ${written.length} written, ${removed.length} removed).`);
38363
+ for (const command of skipped) {
38364
+ console.warn(`[morphix-env] Warning: "${command}" exists in the shims directory as a handmade file (no mx-env marker); refusing to overwrite. Remove or rename it by hand, then re-run shell-init.`);
38365
+ }
38366
+ for (const file of detectHandmadeReservedShims(home)) {
38367
+ console.warn(`[morphix-env] Warning: "${file}" in the shims directory was not created by mx-env and shadows a reserved command; delete it by hand. Do not place files in ${shimsDir(home)} manually.`);
38368
+ }
38369
+ const { absent, shadowed } = findShadowedCommands(commands, bootInfo.originalPath, home);
38370
+ if (absent.length) {
38371
+ console.warn(`[morphix-env] Note: ${absent.join(", ")} cannot be resolved on the current PATH yet \u2014 add the shims directory (below) and open a new shell.`);
38372
+ }
38373
+ if (shadowed.length) {
38374
+ console.warn(`[morphix-env] Warning: these commands resolve to a different executable ahead of their shim: ${shadowed.join(", ")} \u2014 move ${shimsDir(home)} ahead of the entries that provide them.`);
38375
+ }
38376
+ if (args.write) {
38377
+ if (!rcPath) {
38378
+ console.error("[morphix-env] shell-init: cannot infer an rc file from $SHELL (zsh/bash supported); add the PATH line manually.");
38379
+ return 1;
38380
+ }
38381
+ const outcome = upsertRcBlock(rcPath, home);
38382
+ console.log(`[morphix-env] rc block ${outcome} in ${rcPath}. Open a new shell to activate.`);
38383
+ return 0;
38384
+ }
38385
+ console.log(`
38386
+ Add the shims directory to PATH${rcPath ? ` (e.g. in ${rcPath})` : ""}:
38387
+
38388
+ export PATH="${shimsDir(home)}:$PATH"
38389
+
38390
+ Or re-run with --write to let mx-env maintain a marked block in your rc file.`);
38391
+ return 0;
38392
+ }
38034
38393
  function cmdAudit(args) {
38035
- const file = (0, import_path9.join)((0, import_os6.homedir)(), ".mx-env", "audit", `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.jsonl`);
38036
- if (!(0, import_fs9.existsSync)(file)) {
38394
+ const file = (0, import_path10.join)((0, import_os7.homedir)(), ".mx-env", "audit", `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.jsonl`);
38395
+ if (!(0, import_fs10.existsSync)(file)) {
38037
38396
  console.log("[morphix-env] No audit events for today.");
38038
38397
  return 0;
38039
38398
  }
38040
- const lines = (0, import_fs9.readFileSync)(file, "utf8").trim().split("\n").filter(Boolean);
38399
+ const lines = (0, import_fs10.readFileSync)(file, "utf8").trim().split("\n").filter(Boolean);
38041
38400
  const selected = args.subArgs[0] === "tail" ? lines.slice(-20) : lines;
38042
38401
  for (const line of selected) console.log(line);
38043
38402
  return 0;
@@ -38059,6 +38418,7 @@ Usage:
38059
38418
  mx-env edit --profile <name> Open the one-shot visual editor
38060
38419
  mx-env skill install [--target <t>] Install the Agent Skill (global|project|trellis)
38061
38420
  mx-env skill show Show skill install status per target
38421
+ mx-env shell-init [--write|--remove] Generate PATH shims for declared commands
38062
38422
 
38063
38423
  Both mx-env and morphix-env are supported.
38064
38424
 
@@ -38082,6 +38442,7 @@ Options:
38082
38442
  `);
38083
38443
  }
38084
38444
  async function main() {
38445
+ Object.assign(bootInfo, sanitizeBootEnv());
38085
38446
  const args = parseArgs(process.argv);
38086
38447
  switch (args.command) {
38087
38448
  case "run":
@@ -38105,6 +38466,8 @@ async function main() {
38105
38466
  return cmdEdit(args);
38106
38467
  case "doc":
38107
38468
  return cmdDoc(args);
38469
+ case "shell-init":
38470
+ return cmdShellInit(args);
38108
38471
  case "skill": {
38109
38472
  const positional = process.argv.slice(3).find((arg) => !arg.startsWith("-")) ?? "install";
38110
38473
  return cmdSkill(positional, { target: args.target, force: args.force });
package/dist/index.d.ts CHANGED
@@ -69,6 +69,14 @@ interface MxEnvConfig {
69
69
  profiles?: Record<string, ProfileInput>;
70
70
  /** Profile selected when a caller does not pass --profile. */
71
71
  defaultProfile?: string;
72
+ /**
73
+ * Commands that get a PATH shim via `mx-env shell-init` (global and project
74
+ * lists are unioned). Wiring only — never values. Deliberately excluded from
75
+ * validateConfig's hard errors: a convenience layer must not break the core
76
+ * injection path, so `shell-init` validates and hard-fails while composition
77
+ * paths only warn (see shims.ts validateShimList).
78
+ */
79
+ shims?: string[];
72
80
  }
73
81
  interface LoadedConfig {
74
82
  config: MxEnvConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morphix-env",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Secret composition CLI — Infisical, Doppler, macOS Keychain, dotenvx, and local files stay value owners; composes them into one value-safe child environment and ships a built-in Agent Skill (mx-env skill install).",
5
5
  "author": "MorphixAI <dev@morphixai.com>",
6
6
  "homepage": "https://github.com/Morphicai/morphix-env#readme",