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 +25 -0
- package/dist/SKILL.md +3 -0
- package/dist/cli.js +397 -34
- package/dist/index.d.ts +8 -0
- package/package.json +1 -1
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
|
|
29486
|
-
var
|
|
29487
|
-
var
|
|
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/
|
|
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,
|
|
37491
|
-
(0,
|
|
37492
|
-
(0,
|
|
37493
|
-
(0,
|
|
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,
|
|
37497
|
-
const cwd = (0,
|
|
37498
|
-
const name = (0,
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
37708
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
37944
|
-
const
|
|
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,
|
|
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,
|
|
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,
|
|
38030
|
-
(0,
|
|
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,
|
|
38036
|
-
if (!(0,
|
|
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,
|
|
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.
|
|
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",
|