@morphixai/mxenv-cli 0.0.0-stage → 0.12.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/LICENSE +21 -0
- package/README.md +19 -2
- package/SECURITY.md +22 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +460 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +291 -0
- package/package.json +56 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Morphicai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mxenv-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
L3 management face for [mxenv](https://github.com/Morphicai/morphix-env) — machine-level extras beyond the project CLI.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g @morphixai/mxenv-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
- `mxenv-cli shell-init` — generate/remove PATH shims and the marked rc block (global + project shim manifests)
|
|
14
|
+
- `gui` / `global` / `host` — reserved for Wave B (not implemented; rejected with guidance)
|
|
15
|
+
|
|
16
|
+
Project commands (`run`/`export`/`edit`/…) come from [`@morphixai/mxenv`](https://www.npmjs.com/package/@morphixai/mxenv) installed as a devDependency — the global face deliberately does not include them.
|
|
17
|
+
|
|
18
|
+
## License
|
|
19
|
+
|
|
20
|
+
MIT © Morphicai — see [LICENSE](./LICENSE). Security policy: [SECURITY.md](./SECURITY.md).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
`mxenv` composes secrets into short-lived processes; reports about secret leakage, unauthorized value disclosure, or bypass of the value-safety guarantees are handled with priority.
|
|
4
|
+
|
|
5
|
+
## Supported Versions
|
|
6
|
+
|
|
7
|
+
| Version | Supported |
|
|
8
|
+
|---|---|
|
|
9
|
+
| 0.12.x | ✅ |
|
|
10
|
+
| 0.11.x | ✅ (security fixes during the 0.12.x migration window) |
|
|
11
|
+
| < 0.11 | ❌ |
|
|
12
|
+
|
|
13
|
+
## Reporting a Vulnerability
|
|
14
|
+
|
|
15
|
+
Report privately via **GitHub Security Advisories → Private Vulnerability Reporting** on this repository ("Report a vulnerability"). Do not open a public issue for anything that involves secret exposure, redaction bypass, audit leaks, or local API/token handling.
|
|
16
|
+
|
|
17
|
+
Include: affected version, reproduction steps (value-free — never attach real secrets), and impact. You will get an acknowledgement within 72 hours and a status update within 7 days. Fixes land in a patch release; disclosure is coordinated with the reporter.
|
|
18
|
+
|
|
19
|
+
## Scope notes
|
|
20
|
+
|
|
21
|
+
- The tool's value-safety model is **leakage minimization, not a cryptographic boundary** — see "Value safety" in the README. Reports that only restate this documented boundary (e.g. same-uid process reads a child's environment) are expected behavior, not vulnerabilities.
|
|
22
|
+
- Issues in upstream providers (Infisical, Doppler, dotenvx, macOS Keychain) belong to those projects.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
var import_os2 = require("os");
|
|
6
|
+
var import_mxenv_core3 = require("@morphixai/mxenv-core");
|
|
7
|
+
|
|
8
|
+
// src/shims.ts
|
|
9
|
+
var import_fs = require("fs");
|
|
10
|
+
var import_os = require("os");
|
|
11
|
+
var import_path = require("path");
|
|
12
|
+
var import_mxenv_core = require("@morphixai/mxenv-core");
|
|
13
|
+
var import_mxenv_core2 = require("@morphixai/mxenv-core");
|
|
14
|
+
var SHIM_MARKER = "# mxenv shim \u2014 regenerated by `mxenv shell-init`; do not edit by hand.";
|
|
15
|
+
var LEGACY_SHIM_MARKER = "# mx-env shim \u2014 regenerated by `mx-env shell-init`; do not edit by hand.";
|
|
16
|
+
function isGeneratedShim(content) {
|
|
17
|
+
return content.includes(SHIM_MARKER) || content.includes(LEGACY_SHIM_MARKER);
|
|
18
|
+
}
|
|
19
|
+
function renderShimScript(command) {
|
|
20
|
+
return `#!/bin/sh
|
|
21
|
+
${SHIM_MARKER}
|
|
22
|
+
command -v mxenv >/dev/null 2>&1 || { echo "[mxenv] mxenv not on PATH; reinstall mxenv or run 'mxenv-cli shell-init --remove' to clean up this shim." >&2; exit 127; }
|
|
23
|
+
MXENV_SHIM=1 exec mxenv run -- ${command} "$@"
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
26
|
+
function resolveShimCommands(layers, includeGlobal = true) {
|
|
27
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28
|
+
const commands = [];
|
|
29
|
+
const push = (list) => {
|
|
30
|
+
if (!Array.isArray(list)) return;
|
|
31
|
+
for (const entry of list) {
|
|
32
|
+
if (typeof entry !== "string") continue;
|
|
33
|
+
const lower = entry.toLowerCase();
|
|
34
|
+
if (seen.has(lower)) continue;
|
|
35
|
+
seen.add(lower);
|
|
36
|
+
commands.push(entry);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
if (includeGlobal) push(layers.global.config.shims);
|
|
40
|
+
push(layers.project.config.shims);
|
|
41
|
+
return commands;
|
|
42
|
+
}
|
|
43
|
+
function readIfExists(path) {
|
|
44
|
+
try {
|
|
45
|
+
return (0, import_fs.readFileSync)(path, "utf8");
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function writeShims(commands, homeDir = (0, import_os.homedir)()) {
|
|
51
|
+
const dir = (0, import_mxenv_core.shimsDir)(homeDir);
|
|
52
|
+
(0, import_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
53
|
+
const wanted = new Set(commands.map((command) => command.toLowerCase()));
|
|
54
|
+
const written = [];
|
|
55
|
+
const removed = [];
|
|
56
|
+
const skipped = [];
|
|
57
|
+
for (const file of (0, import_fs.readdirSync)(dir)) {
|
|
58
|
+
const path = (0, import_path.join)(dir, file);
|
|
59
|
+
const content = readIfExists(path);
|
|
60
|
+
if (content === null || !isGeneratedShim(content)) continue;
|
|
61
|
+
if (!wanted.has(file.toLowerCase())) {
|
|
62
|
+
(0, import_fs.rmSync)(path);
|
|
63
|
+
removed.push(file);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const command of commands) {
|
|
67
|
+
const path = (0, import_path.join)(dir, command);
|
|
68
|
+
const content = renderShimScript(command);
|
|
69
|
+
let isSymlink = false;
|
|
70
|
+
try {
|
|
71
|
+
isSymlink = (0, import_fs.lstatSync)(path).isSymbolicLink();
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
const existing = isSymlink ? null : readIfExists(path);
|
|
75
|
+
if (!isSymlink && existing !== null && !isGeneratedShim(existing)) {
|
|
76
|
+
skipped.push(command);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (isSymlink) (0, import_fs.rmSync)(path);
|
|
80
|
+
else if (existing === content) continue;
|
|
81
|
+
(0, import_fs.writeFileSync)(path, content, { mode: 493 });
|
|
82
|
+
(0, import_fs.chmodSync)(path, 493);
|
|
83
|
+
written.push(command);
|
|
84
|
+
}
|
|
85
|
+
return { written, removed, skipped };
|
|
86
|
+
}
|
|
87
|
+
function removeAllShims(homeDir = (0, import_os.homedir)()) {
|
|
88
|
+
const dir = (0, import_mxenv_core.shimsDir)(homeDir);
|
|
89
|
+
if (!(0, import_fs.existsSync)(dir)) return [];
|
|
90
|
+
const removed = [];
|
|
91
|
+
for (const file of (0, import_fs.readdirSync)(dir)) {
|
|
92
|
+
const path = (0, import_path.join)(dir, file);
|
|
93
|
+
const content = readIfExists(path);
|
|
94
|
+
if (content !== null && isGeneratedShim(content)) {
|
|
95
|
+
(0, import_fs.rmSync)(path);
|
|
96
|
+
removed.push(file);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return removed;
|
|
100
|
+
}
|
|
101
|
+
function detectHandmadeReservedShims(homeDir = (0, import_os.homedir)()) {
|
|
102
|
+
const dir = (0, import_mxenv_core.shimsDir)(homeDir);
|
|
103
|
+
if (!(0, import_fs.existsSync)(dir)) return [];
|
|
104
|
+
const found = [];
|
|
105
|
+
for (const file of (0, import_fs.readdirSync)(dir)) {
|
|
106
|
+
if (!import_mxenv_core.RESERVED_SHIM_NAMES.includes(file.toLowerCase())) continue;
|
|
107
|
+
const content = readIfExists((0, import_path.join)(dir, file));
|
|
108
|
+
if (content === null || !isGeneratedShim(content)) found.push(file);
|
|
109
|
+
}
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
112
|
+
function defaultRcPath(shell, homeDir = (0, import_os.homedir)()) {
|
|
113
|
+
const name = shell ? shell.split("/").pop() : "";
|
|
114
|
+
if (name === "zsh") return (0, import_path.join)(homeDir, ".zshrc");
|
|
115
|
+
if (name === "bash") return (0, import_path.join)(homeDir, ".bashrc");
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function writeFileAtomic(path, content) {
|
|
119
|
+
const tmp = `${path}.mxenv-${process.pid}.tmp`;
|
|
120
|
+
(0, import_fs.writeFileSync)(tmp, content);
|
|
121
|
+
(0, import_fs.renameSync)(tmp, path);
|
|
122
|
+
}
|
|
123
|
+
function upsertRcBlock(rcPath, homeDir = (0, import_os.homedir)()) {
|
|
124
|
+
const existing = readIfExists(rcPath) ?? "";
|
|
125
|
+
const block = (0, import_mxenv_core.rcBlock)(homeDir);
|
|
126
|
+
const pattern = (0, import_mxenv_core.rcBlockPattern)();
|
|
127
|
+
if (pattern.test(existing)) {
|
|
128
|
+
const replaced = existing.replace(pattern, block);
|
|
129
|
+
if (replaced === existing) return "unchanged";
|
|
130
|
+
writeFileAtomic(rcPath, replaced.endsWith("\n") ? replaced : `${replaced}
|
|
131
|
+
`);
|
|
132
|
+
return "updated";
|
|
133
|
+
}
|
|
134
|
+
const separator = existing.length && !existing.endsWith("\n") ? "\n" : "";
|
|
135
|
+
writeFileAtomic(rcPath, `${existing}${separator}${block}
|
|
136
|
+
`);
|
|
137
|
+
return "added";
|
|
138
|
+
}
|
|
139
|
+
function removeRcBlock(rcPath) {
|
|
140
|
+
const existing = readIfExists(rcPath);
|
|
141
|
+
if (existing === null) return "absent";
|
|
142
|
+
const pattern = new RegExp(`\\n?${(0, import_mxenv_core.rcBlockPattern)().source}\\n?`, "g");
|
|
143
|
+
if (!pattern.test(existing)) return "absent";
|
|
144
|
+
writeFileAtomic(rcPath, existing.replace(pattern, "\n"));
|
|
145
|
+
return "removed";
|
|
146
|
+
}
|
|
147
|
+
function findShadowedCommands(commands, originalPath, homeDir = (0, import_os.homedir)()) {
|
|
148
|
+
if (!originalPath) return { absent: [...commands], shadowed: [] };
|
|
149
|
+
const dirs = originalPath.split(import_path.delimiter).filter(Boolean);
|
|
150
|
+
const absent = [];
|
|
151
|
+
const shadowed = [];
|
|
152
|
+
for (const command of commands) {
|
|
153
|
+
const shimPath = (0, import_path.join)((0, import_mxenv_core.shimsDir)(homeDir), command);
|
|
154
|
+
let resolvedTo = null;
|
|
155
|
+
for (const dir of dirs) {
|
|
156
|
+
const candidate = (0, import_path.join)(dir, command);
|
|
157
|
+
try {
|
|
158
|
+
(0, import_fs.accessSync)(candidate, import_fs.constants.X_OK);
|
|
159
|
+
resolvedTo = candidate;
|
|
160
|
+
break;
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const samePath = (a, b) => process.platform === "darwin" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
165
|
+
if (resolvedTo === null) absent.push(command);
|
|
166
|
+
else if (!samePath(resolvedTo, shimPath)) shadowed.push(command);
|
|
167
|
+
}
|
|
168
|
+
return { absent, shadowed };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// package.json
|
|
172
|
+
var package_default = {
|
|
173
|
+
name: "@morphixai/mxenv-cli",
|
|
174
|
+
version: "0.12.0",
|
|
175
|
+
description: "L3 management face for mxenv \u2014 machine-level extras like shell-init.",
|
|
176
|
+
license: "MIT",
|
|
177
|
+
author: "MorphixAI <dev@morphixai.com>",
|
|
178
|
+
homepage: "https://github.com/Morphicai/morphix-env#readme",
|
|
179
|
+
repository: {
|
|
180
|
+
type: "git",
|
|
181
|
+
url: "git+https://github.com/Morphicai/morphix-env.git",
|
|
182
|
+
directory: "packages/mxenv-cli"
|
|
183
|
+
},
|
|
184
|
+
bugs: {
|
|
185
|
+
url: "https://github.com/Morphicai/morphix-env/issues"
|
|
186
|
+
},
|
|
187
|
+
main: "./dist/index.js",
|
|
188
|
+
types: "./dist/index.d.ts",
|
|
189
|
+
exports: {
|
|
190
|
+
".": {
|
|
191
|
+
types: "./dist/index.d.ts",
|
|
192
|
+
default: "./dist/index.js"
|
|
193
|
+
},
|
|
194
|
+
"./package.json": "./package.json"
|
|
195
|
+
},
|
|
196
|
+
files: [
|
|
197
|
+
"dist",
|
|
198
|
+
"LICENSE",
|
|
199
|
+
"README.md",
|
|
200
|
+
"SECURITY.md"
|
|
201
|
+
],
|
|
202
|
+
engines: {
|
|
203
|
+
node: ">=18"
|
|
204
|
+
},
|
|
205
|
+
publishConfig: {
|
|
206
|
+
access: "public"
|
|
207
|
+
},
|
|
208
|
+
scripts: {
|
|
209
|
+
build: "tsup",
|
|
210
|
+
test: "pnpm --filter @morphixai/mxenv-core build && pnpm --filter @morphixai/mxenv build && pnpm build && vitest run",
|
|
211
|
+
typecheck: "pnpm --filter @morphixai/mxenv-core build && tsc --noEmit",
|
|
212
|
+
prepublishOnly: "node ../../scripts/prepublish-guard.mjs"
|
|
213
|
+
},
|
|
214
|
+
devDependencies: {
|
|
215
|
+
"@types/node": "^25.5.0",
|
|
216
|
+
tsup: "^8.0.0",
|
|
217
|
+
typescript: "^5.0.0",
|
|
218
|
+
vitest: "^4.1.1"
|
|
219
|
+
},
|
|
220
|
+
bin: {
|
|
221
|
+
mxenv: "./dist/cli.js",
|
|
222
|
+
"mx-env": "./dist/cli.js",
|
|
223
|
+
"morphix-env": "./dist/cli.js",
|
|
224
|
+
"mxenv-cli": "./dist/cli.js"
|
|
225
|
+
},
|
|
226
|
+
dependencies: {
|
|
227
|
+
"@morphixai/mxenv-core": "^0.12.0"
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/cli.ts
|
|
232
|
+
var VERSION = package_default.version;
|
|
233
|
+
function parseArgs(argv) {
|
|
234
|
+
const args = {
|
|
235
|
+
command: "",
|
|
236
|
+
subArgs: [],
|
|
237
|
+
positionals: [],
|
|
238
|
+
envFiles: [],
|
|
239
|
+
outFile: null,
|
|
240
|
+
verbose: false,
|
|
241
|
+
filter: null,
|
|
242
|
+
noInfisical: false,
|
|
243
|
+
env: null,
|
|
244
|
+
profile: null,
|
|
245
|
+
noGlobal: false,
|
|
246
|
+
allowInsecureGlobal: false,
|
|
247
|
+
printEditorUrl: false,
|
|
248
|
+
source: null,
|
|
249
|
+
target: null,
|
|
250
|
+
to: null,
|
|
251
|
+
service: null,
|
|
252
|
+
force: false,
|
|
253
|
+
dryRun: false,
|
|
254
|
+
home: false,
|
|
255
|
+
serve: false,
|
|
256
|
+
write: false,
|
|
257
|
+
remove: false,
|
|
258
|
+
warnPlaintext: false,
|
|
259
|
+
unknownFlags: []
|
|
260
|
+
};
|
|
261
|
+
let i = 2;
|
|
262
|
+
const command = argv[i];
|
|
263
|
+
if (!command) return args;
|
|
264
|
+
args.command = command;
|
|
265
|
+
i++;
|
|
266
|
+
while (i < argv.length) {
|
|
267
|
+
const arg = argv[i];
|
|
268
|
+
if (arg === "--") {
|
|
269
|
+
args.subArgs = argv.slice(i + 1);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
if (arg === "--env-file" || arg === "-f") {
|
|
273
|
+
if (argv[++i]) args.envFiles.push(argv[i]);
|
|
274
|
+
} else if (arg === "--out" || arg === "-o") {
|
|
275
|
+
if (argv[++i]) args.outFile = argv[i];
|
|
276
|
+
} else if (arg === "--filter") {
|
|
277
|
+
if (argv[++i]) args.filter = argv[i];
|
|
278
|
+
} else if (arg === "--env" || arg === "-e") {
|
|
279
|
+
if (argv[++i]) args.env = argv[i];
|
|
280
|
+
} else if (arg === "--profile" || arg === "-p") {
|
|
281
|
+
if (argv[++i]) args.profile = argv[i];
|
|
282
|
+
} else if (arg === "--source") {
|
|
283
|
+
if (argv[++i]) args.source = argv[i];
|
|
284
|
+
} else if (arg === "--verbose" || arg === "-v") {
|
|
285
|
+
args.verbose = true;
|
|
286
|
+
} else if (arg === "--no-infisical") {
|
|
287
|
+
args.noInfisical = true;
|
|
288
|
+
} else if (arg === "--no-global") {
|
|
289
|
+
args.noGlobal = true;
|
|
290
|
+
} else if (arg === "--allow-insecure-global") {
|
|
291
|
+
args.allowInsecureGlobal = true;
|
|
292
|
+
} else if (arg === "--print-editor-url") {
|
|
293
|
+
args.printEditorUrl = true;
|
|
294
|
+
} else if (arg === "--target") {
|
|
295
|
+
if (argv[++i]) args.target = argv[i];
|
|
296
|
+
} else if (arg === "--to") {
|
|
297
|
+
if (argv[++i]) args.to = argv[i];
|
|
298
|
+
} else if (arg === "--service") {
|
|
299
|
+
if (argv[++i]) args.service = argv[i];
|
|
300
|
+
} else if (arg === "--force") {
|
|
301
|
+
args.force = true;
|
|
302
|
+
} else if (arg === "--dry-run") {
|
|
303
|
+
args.dryRun = true;
|
|
304
|
+
} else if (arg === "--home") {
|
|
305
|
+
args.home = true;
|
|
306
|
+
} else if (arg === "--write") {
|
|
307
|
+
args.write = true;
|
|
308
|
+
} else if (arg === "--remove") {
|
|
309
|
+
args.remove = true;
|
|
310
|
+
} else if (arg === "--serve") {
|
|
311
|
+
args.serve = true;
|
|
312
|
+
} else if (arg === "--warn-plaintext") {
|
|
313
|
+
args.warnPlaintext = true;
|
|
314
|
+
} else if (args.command === "run") {
|
|
315
|
+
args.subArgs = argv.slice(i);
|
|
316
|
+
break;
|
|
317
|
+
} else if (!arg.startsWith("-")) {
|
|
318
|
+
args.positionals.push(arg);
|
|
319
|
+
} else {
|
|
320
|
+
args.unknownFlags.push(arg);
|
|
321
|
+
}
|
|
322
|
+
i++;
|
|
323
|
+
}
|
|
324
|
+
return args;
|
|
325
|
+
}
|
|
326
|
+
function configLayers() {
|
|
327
|
+
return (0, import_mxenv_core3.loadConfigLayers)({ cwd: process.cwd(), homeDir: (0, import_os2.homedir)() });
|
|
328
|
+
}
|
|
329
|
+
var bootInfo = { viaShim: false };
|
|
330
|
+
function cmdShellInit(args) {
|
|
331
|
+
if (process.platform === "win32") {
|
|
332
|
+
console.error("[mxenv] shell-init is supported on macOS/Linux only; on Windows keep using explicit `mxenv run -- <command>`.");
|
|
333
|
+
return 1;
|
|
334
|
+
}
|
|
335
|
+
if (args.unknownFlags.length) {
|
|
336
|
+
console.error(`[mxenv] shell-init: unrecognized flag(s): ${args.unknownFlags.join(", ")}`);
|
|
337
|
+
return 1;
|
|
338
|
+
}
|
|
339
|
+
if (args.home) {
|
|
340
|
+
console.error("[mxenv] shell-init: --home belongs to `migrate` (global directory migration) and is not accepted here.");
|
|
341
|
+
return 1;
|
|
342
|
+
}
|
|
343
|
+
if (args.write && args.remove) {
|
|
344
|
+
console.error("[mxenv] shell-init: --write and --remove are mutually exclusive.");
|
|
345
|
+
return 1;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
return runShellInit(args);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
const code = error?.code;
|
|
351
|
+
console.error(`[mxenv] shell-init failed${code ? ` (${code})` : ""}: check that your home directory and rc file are writable, then retry.`);
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function runShellInit(args) {
|
|
356
|
+
const layers = configLayers();
|
|
357
|
+
const home = (0, import_os2.homedir)();
|
|
358
|
+
const rcPath = defaultRcPath(process.env.SHELL, home);
|
|
359
|
+
if (args.remove) {
|
|
360
|
+
const removed2 = removeAllShims(home);
|
|
361
|
+
const rcOutcome = rcPath ? removeRcBlock(rcPath) : "absent";
|
|
362
|
+
console.log(`[mxenv] Removed ${removed2.length} shim(s)${removed2.length ? ` (${removed2.join(", ")})` : ""}; rc block ${rcOutcome}${rcPath ? ` in ${rcPath}` : ""}.`);
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
const problems = [
|
|
366
|
+
...(0, import_mxenv_core3.validateShimList)(layers.global.config.shims, layers.global.path),
|
|
367
|
+
...(0, import_mxenv_core3.validateShimList)(layers.project.config.shims, layers.project.path)
|
|
368
|
+
];
|
|
369
|
+
if (problems.length) {
|
|
370
|
+
for (const problem of problems) console.error(`[mxenv] ${problem}`);
|
|
371
|
+
return 1;
|
|
372
|
+
}
|
|
373
|
+
const commands = resolveShimCommands(layers, !args.noGlobal);
|
|
374
|
+
const { written, removed, skipped } = writeShims(commands, home);
|
|
375
|
+
console.log(`[mxenv] Shims ready in ${(0, import_mxenv_core3.shimsDir)(home)} (${commands.length} command(s): ${commands.join(", ") || "none"}; ${written.length} written, ${removed.length} removed).`);
|
|
376
|
+
for (const command of skipped) {
|
|
377
|
+
console.warn(`[mxenv] Warning: "${command}" exists in the shims directory as a handmade file (no mxenv marker); refusing to overwrite. Remove or rename it by hand, then re-run shell-init.`);
|
|
378
|
+
}
|
|
379
|
+
for (const file of detectHandmadeReservedShims(home)) {
|
|
380
|
+
console.warn(`[mxenv] Warning: "${file}" in the shims directory was not created by mxenv and shadows a reserved command; delete it by hand. Do not place files in ${(0, import_mxenv_core3.shimsDir)(home)} manually.`);
|
|
381
|
+
}
|
|
382
|
+
const { absent, shadowed } = findShadowedCommands(commands, bootInfo.originalPath, home);
|
|
383
|
+
if (absent.length) {
|
|
384
|
+
console.warn(`[mxenv] Note: ${absent.join(", ")} cannot be resolved on the current PATH yet \u2014 add the shims directory (below) and open a new shell.`);
|
|
385
|
+
}
|
|
386
|
+
if (shadowed.length) {
|
|
387
|
+
console.warn(`[mxenv] Warning: these commands resolve to a different executable ahead of their shim: ${shadowed.join(", ")} \u2014 move ${(0, import_mxenv_core3.shimsDir)(home)} ahead of the entries that provide them.`);
|
|
388
|
+
}
|
|
389
|
+
if (args.write) {
|
|
390
|
+
if (!rcPath) {
|
|
391
|
+
console.error("[mxenv] shell-init: cannot infer an rc file from $SHELL (zsh/bash supported); add the PATH line manually.");
|
|
392
|
+
return 1;
|
|
393
|
+
}
|
|
394
|
+
const outcome = upsertRcBlock(rcPath, home);
|
|
395
|
+
console.log(`[mxenv] rc block ${outcome} in ${rcPath}. Open a new shell to activate.`);
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
console.log(`
|
|
399
|
+
Add the shims directory to PATH${rcPath ? ` (e.g. in ${rcPath})` : ""}:
|
|
400
|
+
|
|
401
|
+
export PATH="${(0, import_mxenv_core3.shimsDir)(home)}:$PATH"
|
|
402
|
+
|
|
403
|
+
Or re-run with --write to let mxenv maintain a marked block in your rc file.`);
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
406
|
+
function showHelp() {
|
|
407
|
+
console.log(`
|
|
408
|
+
mxenv CLI (mxenv-cli) v${VERSION} \u2014 machine-level management face
|
|
409
|
+
|
|
410
|
+
Usage:
|
|
411
|
+
mxenv-cli shell-init [--write|--remove] Generate PATH shims for declared commands
|
|
412
|
+
|
|
413
|
+
Options:
|
|
414
|
+
--write Also maintain a marked PATH block in your rc file
|
|
415
|
+
--remove Remove every generated shim and the rc PATH block
|
|
416
|
+
--no-global Generate project-declared shims only (skip the global config list)
|
|
417
|
+
--help, -h Show this help
|
|
418
|
+
--version Show version
|
|
419
|
+
|
|
420
|
+
Project-level commands (run, export, inspect, edit, migrate, \u2026) come from the
|
|
421
|
+
project-local install (node_modules/.bin); this CLI manages the machine only.
|
|
422
|
+
`);
|
|
423
|
+
}
|
|
424
|
+
function cmdReserved(command) {
|
|
425
|
+
console.error(`[mxenv] "${command}" is reserved for a future release and is not implemented yet.`);
|
|
426
|
+
return 1;
|
|
427
|
+
}
|
|
428
|
+
async function main() {
|
|
429
|
+
(0, import_mxenv_core3.warnDeprecatedBinInvocation)(process.argv[1]);
|
|
430
|
+
Object.assign(bootInfo, (0, import_mxenv_core3.sanitizeBootEnv)());
|
|
431
|
+
const args = parseArgs(process.argv);
|
|
432
|
+
switch (args.command) {
|
|
433
|
+
case "shell-init":
|
|
434
|
+
return cmdShellInit(args);
|
|
435
|
+
// Wave B skeleton (package-split 4.3): reserved, not implemented.
|
|
436
|
+
case "gui":
|
|
437
|
+
case "global":
|
|
438
|
+
case "host":
|
|
439
|
+
return cmdReserved(args.command);
|
|
440
|
+
case "--help":
|
|
441
|
+
case "-h":
|
|
442
|
+
case "help":
|
|
443
|
+
showHelp();
|
|
444
|
+
return 0;
|
|
445
|
+
case "--version":
|
|
446
|
+
console.log(VERSION);
|
|
447
|
+
return 0;
|
|
448
|
+
default:
|
|
449
|
+
if (args.command) console.error(`[mxenv] Unknown command: ${args.command}`);
|
|
450
|
+
showHelp();
|
|
451
|
+
return args.command ? 1 : 0;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
main().then((code) => {
|
|
455
|
+
process.exitCode = code;
|
|
456
|
+
}).catch((error) => {
|
|
457
|
+
const message = error instanceof Error ? error.message : "[mxenv] Command failed.";
|
|
458
|
+
console.error(message);
|
|
459
|
+
process.exitCode = 1;
|
|
460
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ConfigLayers } from '@morphixai/mxenv-core';
|
|
2
|
+
export { LEGACY_RC_BLOCK_BEGIN, LEGACY_RC_BLOCK_END, RC_BLOCK_BEGIN, RC_BLOCK_END, hasRcBlock, rcBlock, rcBlockPattern, replaceRcBlocks } from '@morphixai/mxenv-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* D5 spawn 版本对齐(spec package-boundary「项目态写操作版本对齐」):
|
|
6
|
+
* 全局 CLI(L3)在项目作用域执行写操作(修改项目来源/配置)时,必须 spawn
|
|
7
|
+
* 项目本地 CLI(L2,项目 node_modules 钉定版本)执行,绝不以全局侧自身版本
|
|
8
|
+
* 直接改写项目数据。解析顺序 cwd → `node_modules/.bin` → PATH,与
|
|
9
|
+
* mxenv-core 的 ensureCommand 哲学一致;解析失败硬失败并给安装指引。
|
|
10
|
+
* Wave A 消费方为 Wave B 的 gui/global/host 命令预留;本模块先落地机制与
|
|
11
|
+
* 两向验证(「项目写走本地版本」「无本地 CLI 明确失败」)。
|
|
12
|
+
*/
|
|
13
|
+
/** L2 CLI 的 bin 名(命名全切归 mxenv-naming-cutover,此处不改名)。 */
|
|
14
|
+
/** 新名 `mxenv` 优先;旧名 alias 于 0.12.x 垫片窗口内照认(naming-migration)。 */
|
|
15
|
+
declare const LOCAL_CLI_BIN_NAMES: readonly ["mxenv", "mx-env", "morphix-env"];
|
|
16
|
+
declare class LocalCliMissingError extends Error {
|
|
17
|
+
readonly cwd: string;
|
|
18
|
+
constructor(cwd: string);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the project-local L2 CLI. Order mirrors ensureCommand:
|
|
22
|
+
* `<cwd>/node_modules/.bin/<bin>` first, then PATH lookup. Returns null when
|
|
23
|
+
* nothing resolves — callers turn that into a hard failure, never a fallback
|
|
24
|
+
* to the global engine.
|
|
25
|
+
*/
|
|
26
|
+
declare function resolveLocalCli(cwd?: string, env?: NodeJS.ProcessEnv): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Runs a project-scoped write through the project-local CLI. Throws
|
|
29
|
+
* LocalCliMissingError (actionable install guidance, value-free) when no local
|
|
30
|
+
* CLI resolves — the global CLI must never write project data itself.
|
|
31
|
+
* Returns the child's exit status.
|
|
32
|
+
*/
|
|
33
|
+
declare function spawnLocalCliWrite(args: string[], options?: {
|
|
34
|
+
cwd?: string;
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
}): number;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Shim generation / stripping / two-level manifests for `shell-init` (L3).
|
|
40
|
+
* The shared pure PATH/list logic (shimsDir, RESERVED_SHIM_NAMES,
|
|
41
|
+
* validateShimList, stripShimsFromPath, sanitizeBootEnv) lives in
|
|
42
|
+
* mxenv-core so the L2 CLI shares one implementation (package-split 4.1).
|
|
43
|
+
*/
|
|
44
|
+
/** Marks files generated by `mxenv-cli shell-init`; handmade files lack it and are never touched. */
|
|
45
|
+
declare const SHIM_MARKER = "# mxenv shim \u2014 regenerated by `mxenv shell-init`; do not edit by hand.";
|
|
46
|
+
/** 0.12.x 前生成的 shim 标记:识别/剥离双认(升级后旧块不得泄漏),写侧只产新名。 */
|
|
47
|
+
declare const LEGACY_SHIM_MARKER = "# mx-env shim \u2014 regenerated by `mx-env shell-init`; do not edit by hand.";
|
|
48
|
+
/** 工具生成判定(新旧双认):无标记=手工文件,永不触碰。 */
|
|
49
|
+
declare function isGeneratedShim(content: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Shim body: guard against a missing mxenv (uninstall, nvm switch) with an
|
|
52
|
+
* actionable message instead of a bare "command not found", then hand off to
|
|
53
|
+
* `run`. The marker line identifies tool-generated shims for --remove and
|
|
54
|
+
* convergence; MXENV_SHIM is read by the CLI at boot and stripped before any
|
|
55
|
+
* composition or child spawn.
|
|
56
|
+
*/
|
|
57
|
+
declare function renderShimScript(command: string): string;
|
|
58
|
+
/** Effective shim list: global ∪ project, deduplicated case-insensitively. */
|
|
59
|
+
declare function resolveShimCommands(layers: ConfigLayers, includeGlobal?: boolean): string[];
|
|
60
|
+
/**
|
|
61
|
+
* Writes shims for the declared commands and converges the directory: stale
|
|
62
|
+
* tool-generated shims (marker present) whose command is no longer declared
|
|
63
|
+
* are removed. **Handmade files (no marker) are never touched** — a declared
|
|
64
|
+
* command colliding with one is skipped and reported, never overwritten.
|
|
65
|
+
* Idempotent — rewriting an up-to-date shim is skipped byte-for-byte.
|
|
66
|
+
*/
|
|
67
|
+
declare function writeShims(commands: string[], homeDir?: string): {
|
|
68
|
+
written: string[];
|
|
69
|
+
removed: string[];
|
|
70
|
+
skipped: string[];
|
|
71
|
+
};
|
|
72
|
+
/** Removes every tool-generated shim; handmade files are left alone. */
|
|
73
|
+
declare function removeAllShims(homeDir?: string): string[];
|
|
74
|
+
/**
|
|
75
|
+
* Handmade files that shadow reserved mxenv names would wrap mxenv itself in
|
|
76
|
+
* a shim (weird but terminating) — surface them so the user can delete them.
|
|
77
|
+
*/
|
|
78
|
+
declare function detectHandmadeReservedShims(homeDir?: string): string[];
|
|
79
|
+
/** Infers the rc file from $SHELL; returns null for shells we do not write. */
|
|
80
|
+
declare function defaultRcPath(shell: string | undefined, homeDir?: string): string | null;
|
|
81
|
+
/** Idempotently installs the PATH block; all existing marked blocks are replaced. */
|
|
82
|
+
declare function upsertRcBlock(rcPath: string, homeDir?: string): 'added' | 'updated' | 'unchanged';
|
|
83
|
+
/** Idempotently removes every marked PATH block, leaving other content intact. */
|
|
84
|
+
declare function removeRcBlock(rcPath: string): 'removed' | 'absent';
|
|
85
|
+
/**
|
|
86
|
+
* Per-command PATH parity: a declared command must resolve to its shim file.
|
|
87
|
+
* `absent` — no executable by that name anywhere on PATH (first install: the
|
|
88
|
+
* shims dir is not on PATH yet); `shadowed` — an earlier PATH entry (volta,
|
|
89
|
+
* another tool) wins and the shim silently never fires. Uses the
|
|
90
|
+
* pre-sanitization PATH captured at boot.
|
|
91
|
+
*/
|
|
92
|
+
declare function findShadowedCommands(commands: string[], originalPath: string | undefined, homeDir?: string): {
|
|
93
|
+
absent: string[];
|
|
94
|
+
shadowed: string[];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export { LEGACY_SHIM_MARKER, LOCAL_CLI_BIN_NAMES, LocalCliMissingError, SHIM_MARKER, defaultRcPath, detectHandmadeReservedShims, findShadowedCommands, isGeneratedShim, removeAllShims, removeRcBlock, renderShimScript, resolveLocalCli, resolveShimCommands, spawnLocalCliWrite, upsertRcBlock, writeShims };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
LEGACY_RC_BLOCK_BEGIN: () => import_mxenv_core2.LEGACY_RC_BLOCK_BEGIN,
|
|
24
|
+
LEGACY_RC_BLOCK_END: () => import_mxenv_core2.LEGACY_RC_BLOCK_END,
|
|
25
|
+
LEGACY_SHIM_MARKER: () => LEGACY_SHIM_MARKER,
|
|
26
|
+
LOCAL_CLI_BIN_NAMES: () => LOCAL_CLI_BIN_NAMES,
|
|
27
|
+
LocalCliMissingError: () => LocalCliMissingError,
|
|
28
|
+
RC_BLOCK_BEGIN: () => import_mxenv_core2.RC_BLOCK_BEGIN,
|
|
29
|
+
RC_BLOCK_END: () => import_mxenv_core2.RC_BLOCK_END,
|
|
30
|
+
SHIM_MARKER: () => SHIM_MARKER,
|
|
31
|
+
defaultRcPath: () => defaultRcPath,
|
|
32
|
+
detectHandmadeReservedShims: () => detectHandmadeReservedShims,
|
|
33
|
+
findShadowedCommands: () => findShadowedCommands,
|
|
34
|
+
hasRcBlock: () => import_mxenv_core2.hasRcBlock,
|
|
35
|
+
isGeneratedShim: () => isGeneratedShim,
|
|
36
|
+
rcBlock: () => import_mxenv_core2.rcBlock,
|
|
37
|
+
rcBlockPattern: () => import_mxenv_core2.rcBlockPattern,
|
|
38
|
+
removeAllShims: () => removeAllShims,
|
|
39
|
+
removeRcBlock: () => removeRcBlock,
|
|
40
|
+
renderShimScript: () => renderShimScript,
|
|
41
|
+
replaceRcBlocks: () => import_mxenv_core2.replaceRcBlocks,
|
|
42
|
+
resolveLocalCli: () => resolveLocalCli,
|
|
43
|
+
resolveShimCommands: () => resolveShimCommands,
|
|
44
|
+
spawnLocalCliWrite: () => spawnLocalCliWrite,
|
|
45
|
+
upsertRcBlock: () => upsertRcBlock,
|
|
46
|
+
writeShims: () => writeShims
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(src_exports);
|
|
49
|
+
|
|
50
|
+
// src/local-cli.ts
|
|
51
|
+
var import_child_process = require("child_process");
|
|
52
|
+
var import_fs = require("fs");
|
|
53
|
+
var import_path = require("path");
|
|
54
|
+
var LOCAL_CLI_BIN_NAMES = ["mxenv", "mx-env", "morphix-env"];
|
|
55
|
+
var LocalCliMissingError = class extends Error {
|
|
56
|
+
constructor(cwd) {
|
|
57
|
+
super(
|
|
58
|
+
`[mxenv] No project-local mxenv CLI found for ${cwd} (checked node_modules/.bin and PATH).
|
|
59
|
+
Install it in the project so writes run the project-pinned version: npm i -D @morphixai/mxenv (or: pnpm add -D @morphixai/mxenv).
|
|
60
|
+
Refusing to write project data with the global CLI.`
|
|
61
|
+
);
|
|
62
|
+
this.name = "LocalCliMissingError";
|
|
63
|
+
this.cwd = cwd;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
function resolveLocalCli(cwd = process.cwd(), env = process.env) {
|
|
67
|
+
for (const name of LOCAL_CLI_BIN_NAMES) {
|
|
68
|
+
const localBinary = (0, import_path.resolve)(cwd, "node_modules", ".bin", name);
|
|
69
|
+
if (isExecutable(localBinary)) return localBinary;
|
|
70
|
+
}
|
|
71
|
+
for (const dir of (env.PATH ?? "").split(import_path.delimiter).filter(Boolean)) {
|
|
72
|
+
for (const name of LOCAL_CLI_BIN_NAMES) {
|
|
73
|
+
const candidate = (0, import_path.join)(dir, name);
|
|
74
|
+
if (isExecutable(candidate)) return candidate;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function isExecutable(candidate) {
|
|
80
|
+
try {
|
|
81
|
+
if (!(0, import_fs.existsSync)(candidate)) return false;
|
|
82
|
+
(0, import_fs.accessSync)(candidate, import_fs.constants.X_OK);
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function spawnLocalCliWrite(args, options = {}) {
|
|
89
|
+
const cwd = options.cwd ?? process.cwd();
|
|
90
|
+
const cli = resolveLocalCli(cwd, options.env);
|
|
91
|
+
if (!cli) throw new LocalCliMissingError(cwd);
|
|
92
|
+
const result = (0, import_child_process.spawnSync)(cli, args, {
|
|
93
|
+
cwd,
|
|
94
|
+
env: options.env ?? process.env,
|
|
95
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
96
|
+
});
|
|
97
|
+
if (result.error) {
|
|
98
|
+
throw new Error(`[mxenv] Failed to execute the project-local CLI (${cli}): ${result.error.message}`);
|
|
99
|
+
}
|
|
100
|
+
return result.status ?? 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/shims.ts
|
|
104
|
+
var import_fs2 = require("fs");
|
|
105
|
+
var import_os = require("os");
|
|
106
|
+
var import_path2 = require("path");
|
|
107
|
+
var import_mxenv_core = require("@morphixai/mxenv-core");
|
|
108
|
+
var import_mxenv_core2 = require("@morphixai/mxenv-core");
|
|
109
|
+
var SHIM_MARKER = "# mxenv shim \u2014 regenerated by `mxenv shell-init`; do not edit by hand.";
|
|
110
|
+
var LEGACY_SHIM_MARKER = "# mx-env shim \u2014 regenerated by `mx-env shell-init`; do not edit by hand.";
|
|
111
|
+
function isGeneratedShim(content) {
|
|
112
|
+
return content.includes(SHIM_MARKER) || content.includes(LEGACY_SHIM_MARKER);
|
|
113
|
+
}
|
|
114
|
+
function renderShimScript(command) {
|
|
115
|
+
return `#!/bin/sh
|
|
116
|
+
${SHIM_MARKER}
|
|
117
|
+
command -v mxenv >/dev/null 2>&1 || { echo "[mxenv] mxenv not on PATH; reinstall mxenv or run 'mxenv-cli shell-init --remove' to clean up this shim." >&2; exit 127; }
|
|
118
|
+
MXENV_SHIM=1 exec mxenv run -- ${command} "$@"
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
function resolveShimCommands(layers, includeGlobal = true) {
|
|
122
|
+
const seen = /* @__PURE__ */ new Set();
|
|
123
|
+
const commands = [];
|
|
124
|
+
const push = (list) => {
|
|
125
|
+
if (!Array.isArray(list)) return;
|
|
126
|
+
for (const entry of list) {
|
|
127
|
+
if (typeof entry !== "string") continue;
|
|
128
|
+
const lower = entry.toLowerCase();
|
|
129
|
+
if (seen.has(lower)) continue;
|
|
130
|
+
seen.add(lower);
|
|
131
|
+
commands.push(entry);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
if (includeGlobal) push(layers.global.config.shims);
|
|
135
|
+
push(layers.project.config.shims);
|
|
136
|
+
return commands;
|
|
137
|
+
}
|
|
138
|
+
function readIfExists(path) {
|
|
139
|
+
try {
|
|
140
|
+
return (0, import_fs2.readFileSync)(path, "utf8");
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function writeShims(commands, homeDir = (0, import_os.homedir)()) {
|
|
146
|
+
const dir = (0, import_mxenv_core.shimsDir)(homeDir);
|
|
147
|
+
(0, import_fs2.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
148
|
+
const wanted = new Set(commands.map((command) => command.toLowerCase()));
|
|
149
|
+
const written = [];
|
|
150
|
+
const removed = [];
|
|
151
|
+
const skipped = [];
|
|
152
|
+
for (const file of (0, import_fs2.readdirSync)(dir)) {
|
|
153
|
+
const path = (0, import_path2.join)(dir, file);
|
|
154
|
+
const content = readIfExists(path);
|
|
155
|
+
if (content === null || !isGeneratedShim(content)) continue;
|
|
156
|
+
if (!wanted.has(file.toLowerCase())) {
|
|
157
|
+
(0, import_fs2.rmSync)(path);
|
|
158
|
+
removed.push(file);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const command of commands) {
|
|
162
|
+
const path = (0, import_path2.join)(dir, command);
|
|
163
|
+
const content = renderShimScript(command);
|
|
164
|
+
let isSymlink = false;
|
|
165
|
+
try {
|
|
166
|
+
isSymlink = (0, import_fs2.lstatSync)(path).isSymbolicLink();
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
const existing = isSymlink ? null : readIfExists(path);
|
|
170
|
+
if (!isSymlink && existing !== null && !isGeneratedShim(existing)) {
|
|
171
|
+
skipped.push(command);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (isSymlink) (0, import_fs2.rmSync)(path);
|
|
175
|
+
else if (existing === content) continue;
|
|
176
|
+
(0, import_fs2.writeFileSync)(path, content, { mode: 493 });
|
|
177
|
+
(0, import_fs2.chmodSync)(path, 493);
|
|
178
|
+
written.push(command);
|
|
179
|
+
}
|
|
180
|
+
return { written, removed, skipped };
|
|
181
|
+
}
|
|
182
|
+
function removeAllShims(homeDir = (0, import_os.homedir)()) {
|
|
183
|
+
const dir = (0, import_mxenv_core.shimsDir)(homeDir);
|
|
184
|
+
if (!(0, import_fs2.existsSync)(dir)) return [];
|
|
185
|
+
const removed = [];
|
|
186
|
+
for (const file of (0, import_fs2.readdirSync)(dir)) {
|
|
187
|
+
const path = (0, import_path2.join)(dir, file);
|
|
188
|
+
const content = readIfExists(path);
|
|
189
|
+
if (content !== null && isGeneratedShim(content)) {
|
|
190
|
+
(0, import_fs2.rmSync)(path);
|
|
191
|
+
removed.push(file);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return removed;
|
|
195
|
+
}
|
|
196
|
+
function detectHandmadeReservedShims(homeDir = (0, import_os.homedir)()) {
|
|
197
|
+
const dir = (0, import_mxenv_core.shimsDir)(homeDir);
|
|
198
|
+
if (!(0, import_fs2.existsSync)(dir)) return [];
|
|
199
|
+
const found = [];
|
|
200
|
+
for (const file of (0, import_fs2.readdirSync)(dir)) {
|
|
201
|
+
if (!import_mxenv_core.RESERVED_SHIM_NAMES.includes(file.toLowerCase())) continue;
|
|
202
|
+
const content = readIfExists((0, import_path2.join)(dir, file));
|
|
203
|
+
if (content === null || !isGeneratedShim(content)) found.push(file);
|
|
204
|
+
}
|
|
205
|
+
return found;
|
|
206
|
+
}
|
|
207
|
+
function defaultRcPath(shell, homeDir = (0, import_os.homedir)()) {
|
|
208
|
+
const name = shell ? shell.split("/").pop() : "";
|
|
209
|
+
if (name === "zsh") return (0, import_path2.join)(homeDir, ".zshrc");
|
|
210
|
+
if (name === "bash") return (0, import_path2.join)(homeDir, ".bashrc");
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function writeFileAtomic(path, content) {
|
|
214
|
+
const tmp = `${path}.mxenv-${process.pid}.tmp`;
|
|
215
|
+
(0, import_fs2.writeFileSync)(tmp, content);
|
|
216
|
+
(0, import_fs2.renameSync)(tmp, path);
|
|
217
|
+
}
|
|
218
|
+
function upsertRcBlock(rcPath, homeDir = (0, import_os.homedir)()) {
|
|
219
|
+
const existing = readIfExists(rcPath) ?? "";
|
|
220
|
+
const block = (0, import_mxenv_core.rcBlock)(homeDir);
|
|
221
|
+
const pattern = (0, import_mxenv_core.rcBlockPattern)();
|
|
222
|
+
if (pattern.test(existing)) {
|
|
223
|
+
const replaced = existing.replace(pattern, block);
|
|
224
|
+
if (replaced === existing) return "unchanged";
|
|
225
|
+
writeFileAtomic(rcPath, replaced.endsWith("\n") ? replaced : `${replaced}
|
|
226
|
+
`);
|
|
227
|
+
return "updated";
|
|
228
|
+
}
|
|
229
|
+
const separator = existing.length && !existing.endsWith("\n") ? "\n" : "";
|
|
230
|
+
writeFileAtomic(rcPath, `${existing}${separator}${block}
|
|
231
|
+
`);
|
|
232
|
+
return "added";
|
|
233
|
+
}
|
|
234
|
+
function removeRcBlock(rcPath) {
|
|
235
|
+
const existing = readIfExists(rcPath);
|
|
236
|
+
if (existing === null) return "absent";
|
|
237
|
+
const pattern = new RegExp(`\\n?${(0, import_mxenv_core.rcBlockPattern)().source}\\n?`, "g");
|
|
238
|
+
if (!pattern.test(existing)) return "absent";
|
|
239
|
+
writeFileAtomic(rcPath, existing.replace(pattern, "\n"));
|
|
240
|
+
return "removed";
|
|
241
|
+
}
|
|
242
|
+
function findShadowedCommands(commands, originalPath, homeDir = (0, import_os.homedir)()) {
|
|
243
|
+
if (!originalPath) return { absent: [...commands], shadowed: [] };
|
|
244
|
+
const dirs = originalPath.split(import_path2.delimiter).filter(Boolean);
|
|
245
|
+
const absent = [];
|
|
246
|
+
const shadowed = [];
|
|
247
|
+
for (const command of commands) {
|
|
248
|
+
const shimPath = (0, import_path2.join)((0, import_mxenv_core.shimsDir)(homeDir), command);
|
|
249
|
+
let resolvedTo = null;
|
|
250
|
+
for (const dir of dirs) {
|
|
251
|
+
const candidate = (0, import_path2.join)(dir, command);
|
|
252
|
+
try {
|
|
253
|
+
(0, import_fs2.accessSync)(candidate, import_fs2.constants.X_OK);
|
|
254
|
+
resolvedTo = candidate;
|
|
255
|
+
break;
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const samePath = (a, b) => process.platform === "darwin" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
260
|
+
if (resolvedTo === null) absent.push(command);
|
|
261
|
+
else if (!samePath(resolvedTo, shimPath)) shadowed.push(command);
|
|
262
|
+
}
|
|
263
|
+
return { absent, shadowed };
|
|
264
|
+
}
|
|
265
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
266
|
+
0 && (module.exports = {
|
|
267
|
+
LEGACY_RC_BLOCK_BEGIN,
|
|
268
|
+
LEGACY_RC_BLOCK_END,
|
|
269
|
+
LEGACY_SHIM_MARKER,
|
|
270
|
+
LOCAL_CLI_BIN_NAMES,
|
|
271
|
+
LocalCliMissingError,
|
|
272
|
+
RC_BLOCK_BEGIN,
|
|
273
|
+
RC_BLOCK_END,
|
|
274
|
+
SHIM_MARKER,
|
|
275
|
+
defaultRcPath,
|
|
276
|
+
detectHandmadeReservedShims,
|
|
277
|
+
findShadowedCommands,
|
|
278
|
+
hasRcBlock,
|
|
279
|
+
isGeneratedShim,
|
|
280
|
+
rcBlock,
|
|
281
|
+
rcBlockPattern,
|
|
282
|
+
removeAllShims,
|
|
283
|
+
removeRcBlock,
|
|
284
|
+
renderShimScript,
|
|
285
|
+
replaceRcBlocks,
|
|
286
|
+
resolveLocalCli,
|
|
287
|
+
resolveShimCommands,
|
|
288
|
+
spawnLocalCliWrite,
|
|
289
|
+
upsertRcBlock,
|
|
290
|
+
writeShims
|
|
291
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morphixai/mxenv-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "L3 management face for mxenv \u2014 machine-level extras like shell-init.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "MorphixAI <dev@morphixai.com>",
|
|
7
|
+
"homepage": "https://github.com/Morphicai/morphix-env#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Morphicai/morphix-env.git",
|
|
11
|
+
"directory": "packages/mxenv-cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Morphicai/morphix-env/issues"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"README.md",
|
|
29
|
+
"SECURITY.md"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"test": "pnpm --filter @morphixai/mxenv-core build && pnpm --filter @morphixai/mxenv build && pnpm build && vitest run",
|
|
40
|
+
"typecheck": "pnpm --filter @morphixai/mxenv-core build && tsc --noEmit",
|
|
41
|
+
"prepublishOnly": "node ../../scripts/prepublish-guard.mjs"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^25.5.0",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"typescript": "^5.0.0",
|
|
47
|
+
"vitest": "^4.1.1"
|
|
48
|
+
},
|
|
49
|
+
"bin": {
|
|
50
|
+
"mxenv": "./dist/cli.js",
|
|
51
|
+
"mx-env": "./dist/cli.js",
|
|
52
|
+
"morphix-env": "./dist/cli.js",
|
|
53
|
+
"mxenv-cli": "./dist/cli.js"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@morphixai/mxenv-core": "^0.12.0"
|
|
57
|
+
}
|
|
58
|
+
}
|