@codyswann/lisa 2.328.5 → 2.329.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +39 -0
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/agents/openai.yaml +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
- package/plugins/lisa/commands/setup/workstation.md +7 -0
- package/plugins/lisa/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/lisa/skills/lisa-setup-workstation/agents/openai.yaml +4 -0
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
- package/plugins/lisa-agy/commands/lisa/setup/workstation.md +7 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/commands/lisa/setup/workstation.md +7 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/commands/lisa/setup/workstation.md +7 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/commands/setup/workstation.md +7 -0
- package/plugins/src/base/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Command-line entry for the workstation bootstrap.
|
|
4
|
+
*
|
|
5
|
+
* Kept separate from `workstation.mjs` so the planning logic stays importable
|
|
6
|
+
* and testable without a process boundary — a test should be able to ask "what
|
|
7
|
+
* would this do" without any risk of it doing it.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* cli.mjs report present vs missing
|
|
11
|
+
* cli.mjs --install provision what is missing
|
|
12
|
+
* cli.mjs --install --agents=claude,codex only these agents
|
|
13
|
+
* cli.mjs --provider=bitwarden credential manager (asked if a TTY)
|
|
14
|
+
* cli.mjs --json machine-readable plan
|
|
15
|
+
* cli.mjs --print-dockerfile an image that runs this same script
|
|
16
|
+
* @module cli
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { closeSync, existsSync, openSync, readSync, writeSync } from "node:fs";
|
|
20
|
+
import { dirname, resolve } from "node:path";
|
|
21
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
22
|
+
|
|
23
|
+
import { AGENTS, PROVIDERS, TOOLS, pathDirs } from "./catalogue.mjs";
|
|
24
|
+
import { installEntry, planWorkstation, renderPlan } from "./workstation.mjs";
|
|
25
|
+
|
|
26
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
|
|
28
|
+
/** Where pinned binaries land, and what must therefore be on PATH. */
|
|
29
|
+
export const BIN_DIR = `${process.env.HOME}/.local/bin`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Borrow the pinned, checksummed installer the remote-env skill owns.
|
|
33
|
+
*
|
|
34
|
+
* Composed rather than reimplemented: the pins and checksums must be the same
|
|
35
|
+
* ones the remote surfaces use, or the two drift and only one gets fixed.
|
|
36
|
+
* Returns null when the sibling is absent, so a machine can still install the
|
|
37
|
+
* unpinned entries instead of failing wholesale — the report says which ones
|
|
38
|
+
* were skipped rather than passing them off as done.
|
|
39
|
+
* @returns {Promise<Function|null>} An installer, or null.
|
|
40
|
+
*/
|
|
41
|
+
export async function loadPinnedInstaller() {
|
|
42
|
+
const sibling = name =>
|
|
43
|
+
resolve(HERE, "..", "..", "lisa-setup-remote-env", "scripts", name);
|
|
44
|
+
const setup = sibling("setup-remote-env.mjs");
|
|
45
|
+
const toolchain = sibling("toolchain.mjs");
|
|
46
|
+
if (!existsSync(setup) || !existsSync(toolchain)) return null;
|
|
47
|
+
|
|
48
|
+
const { installTool, ensureBinDir } = await import(pathToFileURL(setup).href);
|
|
49
|
+
const { resolvePlatform } = await import(pathToFileURL(toolchain).href);
|
|
50
|
+
if (typeof installTool !== "function") return null;
|
|
51
|
+
|
|
52
|
+
return entry => {
|
|
53
|
+
// resolvePlatform must run first: installTool calls assertPinned, which
|
|
54
|
+
// refuses an entry that still carries a `platforms` map. Skipping it failed
|
|
55
|
+
// bws and gh with "platform-specific entry was not resolved before install"
|
|
56
|
+
// — the guard doing exactly its job.
|
|
57
|
+
const resolved = resolvePlatform(toManifestEntry(entry));
|
|
58
|
+
installTool(resolved, ensureBinDir ? ensureBinDir() : BIN_DIR);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Translate a catalogue entry into the manifest shape the installer expects.
|
|
64
|
+
*
|
|
65
|
+
* The two vocabularies differ — the catalogue describes a machine, the manifest
|
|
66
|
+
* describes a project — so the seam is made explicit here rather than by giving
|
|
67
|
+
* the catalogue a shape it does not otherwise need. `kind` becomes `install`;
|
|
68
|
+
* everything else the manifest reads is already named the same.
|
|
69
|
+
* @param {object} entry Catalogue entry.
|
|
70
|
+
* @returns {object} A `remoteEnv.tools` style entry.
|
|
71
|
+
*/
|
|
72
|
+
export function toManifestEntry(entry) {
|
|
73
|
+
const { kind, label, binDir, note, selfUpdates, ...rest } = entry;
|
|
74
|
+
return { ...rest, install: kind };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Decide which credential manager to provision.
|
|
79
|
+
*
|
|
80
|
+
* Asked, never assumed. Defaulting to Bitwarden because that is what this repo
|
|
81
|
+
* happens to use would install a vendor CLI on a machine whose project resolves
|
|
82
|
+
* secrets some other way — and would make `none`, a supported configuration,
|
|
83
|
+
* reachable only by accident.
|
|
84
|
+
*
|
|
85
|
+
* Headless is the primary mode, so a missing answer without a TTY is `none`
|
|
86
|
+
* rather than a prompt that would hang a container build forever.
|
|
87
|
+
* @param {string|null} requested Value of --provider, if given.
|
|
88
|
+
* @param {object} io Injected ask/tty, for tests.
|
|
89
|
+
* @returns {string} A provider name from the catalogue.
|
|
90
|
+
*/
|
|
91
|
+
export function chooseProvider(requested, io = {}) {
|
|
92
|
+
if (requested) return requested;
|
|
93
|
+
|
|
94
|
+
const ask = io.ask ?? null;
|
|
95
|
+
const interactive = io.interactive ?? Boolean(process.stdin.isTTY);
|
|
96
|
+
if (!ask || !interactive) return "none";
|
|
97
|
+
|
|
98
|
+
const names = PROVIDERS.map(p => p.name);
|
|
99
|
+
const menu = PROVIDERS.map((p, i) => ` ${i + 1}) ${p.label}`).join("\n");
|
|
100
|
+
const answer = String(
|
|
101
|
+
ask(`Which credential manager does this machine use?\n${menu}\nChoice: `) ??
|
|
102
|
+
""
|
|
103
|
+
).trim();
|
|
104
|
+
|
|
105
|
+
// Accept either the menu number or the name, because an operator reading the
|
|
106
|
+
// list will reach for whichever is in front of them.
|
|
107
|
+
const index = Number.parseInt(answer, 10);
|
|
108
|
+
if (Number.isInteger(index) && index >= 1 && index <= names.length) {
|
|
109
|
+
return names[index - 1];
|
|
110
|
+
}
|
|
111
|
+
return names.includes(answer) ? answer : "none";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A Dockerfile that runs this same script, for a throwaway environment.
|
|
116
|
+
*
|
|
117
|
+
* Emitted rather than committed so it cannot drift from the catalogue it
|
|
118
|
+
* provisions. There is no Docker-specific code path anywhere in this skill —
|
|
119
|
+
* the image simply gives the script a `$HOME` that dies with the container.
|
|
120
|
+
*
|
|
121
|
+
* Deliberately bakes in NO repository. Clone inside the container, then run
|
|
122
|
+
* lisa-detect-tooling and lisa-setup-local-env for that project's own tools.
|
|
123
|
+
* @param {string[]} agents Agent names to install.
|
|
124
|
+
* @param {string} [provider] Credential manager to provision.
|
|
125
|
+
* @returns {string} Dockerfile text.
|
|
126
|
+
*/
|
|
127
|
+
export function renderDockerfile(agents, provider = "none") {
|
|
128
|
+
const list = agents.length ? agents.join(",") : "claude,codex";
|
|
129
|
+
const clean = "apt-get clean";
|
|
130
|
+
return [
|
|
131
|
+
"# Ephemeral coding environment. Build, use, discard.",
|
|
132
|
+
"# docker build -t lisa-workstation .",
|
|
133
|
+
"# docker run --rm -it lisa-workstation",
|
|
134
|
+
"FROM ubuntu:24.04",
|
|
135
|
+
"ENV DEBIAN_FRONTEND=noninteractive",
|
|
136
|
+
"RUN apt-get update -qq \\",
|
|
137
|
+
" && apt-get install -y -qq curl git unzip ca-certificates python3 \\",
|
|
138
|
+
" && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \\",
|
|
139
|
+
` && apt-get install -y -qq nodejs && ${clean}`,
|
|
140
|
+
// The whole skills/ tree, not just this skill: the checksummed installs are
|
|
141
|
+
// borrowed from lisa-setup-remote-env, and copying only this directory left
|
|
142
|
+
// bws and gh silently unprovisioned.
|
|
143
|
+
"COPY skills/ /opt/lisa/skills/",
|
|
144
|
+
// Set BEFORE the install, not after. Installers write into ~/.local/bin and
|
|
145
|
+
// the post-install check looks for the binary by bare name, so a PATH set
|
|
146
|
+
// afterwards would fail every verification in the image while passing on a
|
|
147
|
+
// laptop that already exports the directory.
|
|
148
|
+
`ENV PATH="${pathDirs("/root").join(":")}:\${PATH}"`,
|
|
149
|
+
// --provider is passed explicitly: the build has no TTY, so an omitted flag
|
|
150
|
+
// would resolve to `none` and produce an image with no way to reach secrets.
|
|
151
|
+
`RUN node /opt/lisa/skills/lisa-setup-workstation/scripts/cli.mjs \\`,
|
|
152
|
+
` --install --agents=${list} --provider=${provider}`,
|
|
153
|
+
'CMD ["bash"]',
|
|
154
|
+
].join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Run the CLI.
|
|
159
|
+
* @param {string[]} argv Arguments after the script name.
|
|
160
|
+
* @param {object} [io] Injected output sinks, for tests.
|
|
161
|
+
* @returns {number} Process exit code.
|
|
162
|
+
*/
|
|
163
|
+
export async function run(argv, io = {}) {
|
|
164
|
+
const out = io.log ?? console.log;
|
|
165
|
+
const err = io.error ?? console.error;
|
|
166
|
+
const flag = name => argv.includes(`--${name}`);
|
|
167
|
+
const value = name => {
|
|
168
|
+
const hit = argv.find(a => a.startsWith(`--${name}=`));
|
|
169
|
+
return hit ? hit.slice(name.length + 3) : null;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const selected = value("agents");
|
|
173
|
+
const agents = selected
|
|
174
|
+
? selected
|
|
175
|
+
.split(",")
|
|
176
|
+
.map(s => s.trim())
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
: null;
|
|
179
|
+
|
|
180
|
+
const provider = chooseProvider(value("provider"), io);
|
|
181
|
+
|
|
182
|
+
if (flag("print-dockerfile")) {
|
|
183
|
+
out(renderDockerfile(agents ?? [], provider));
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let plan;
|
|
188
|
+
try {
|
|
189
|
+
plan = planWorkstation({ agents, provider, ...(io.probes ?? {}) });
|
|
190
|
+
} catch (error) {
|
|
191
|
+
// A misspelled provider stops here rather than silently provisioning the
|
|
192
|
+
// wrong credential manager.
|
|
193
|
+
err(String(error.message));
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (flag("json")) {
|
|
198
|
+
out(JSON.stringify({ plan }, null, 2));
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
out(renderPlan(plan));
|
|
203
|
+
|
|
204
|
+
const blocked = plan.filter(r => r.action === "blocked");
|
|
205
|
+
if (blocked.length) {
|
|
206
|
+
err(
|
|
207
|
+
`\n${blocked.length} required tool(s) absent. These come from the OS or ` +
|
|
208
|
+
`base image and cannot be installed here.`
|
|
209
|
+
);
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const pending = plan.filter(r => r.action === "install");
|
|
214
|
+
if (!pending.length) {
|
|
215
|
+
// Idempotence made visible. A second run states it rather than going quiet,
|
|
216
|
+
// because silence reads as "did nothing because it broke".
|
|
217
|
+
out("\nNothing to install — this workstation is already prepared.");
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!flag("install")) {
|
|
222
|
+
out(
|
|
223
|
+
`\n${pending.length} missing. Re-run with --install to provision them. ` +
|
|
224
|
+
`Nothing has been installed or written.`
|
|
225
|
+
);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
out("");
|
|
230
|
+
// Provider entries are keyed by vendor name but planned under their BINARY
|
|
231
|
+
// name (`bitwarden` -> `bws`), so they are re-keyed here or the lookup below
|
|
232
|
+
// silently finds nothing and the install is skipped without a word.
|
|
233
|
+
const catalogue = [
|
|
234
|
+
...AGENTS,
|
|
235
|
+
...TOOLS,
|
|
236
|
+
...PROVIDERS.filter(p => p.binary).map(p => ({ ...p, name: p.binary })),
|
|
237
|
+
];
|
|
238
|
+
const probes = io.probes ?? {};
|
|
239
|
+
const installPinned =
|
|
240
|
+
probes.installPinned ?? (await loadPinnedInstaller()) ?? undefined;
|
|
241
|
+
|
|
242
|
+
const results = [];
|
|
243
|
+
for (const row of pending) {
|
|
244
|
+
results.push(
|
|
245
|
+
installEntry(
|
|
246
|
+
catalogue.find(e => e.name === row.name),
|
|
247
|
+
row,
|
|
248
|
+
{ ...probes, installPinned }
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
for (const result of results) {
|
|
253
|
+
out(` ${result.action.padEnd(10)} ${result.label} ${result.reason}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const failed = results.filter(r => r.action === "failed");
|
|
257
|
+
if (failed.length) {
|
|
258
|
+
err(
|
|
259
|
+
`\n${failed.length} install(s) failed: ${failed
|
|
260
|
+
.map(r => r.name)
|
|
261
|
+
.join(", ")}. Nothing above was reported as installed unless the ` +
|
|
262
|
+
`binary was found on PATH afterwards.`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return failed.length ? 1 : 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Prompt on the terminal, synchronously.
|
|
270
|
+
*
|
|
271
|
+
* Reads `/dev/tty` rather than stdin so the question still works when the
|
|
272
|
+
* script is being piped — the ordinary way a bootstrap gets run.
|
|
273
|
+
* @param {string} prompt Text to display.
|
|
274
|
+
* @returns {string|null} The answer, or null if no terminal is attached.
|
|
275
|
+
*/
|
|
276
|
+
export function askTty(prompt) {
|
|
277
|
+
let fd;
|
|
278
|
+
try {
|
|
279
|
+
fd = openSync("/dev/tty", "r+");
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
writeSync(fd, prompt);
|
|
285
|
+
const buffer = Buffer.alloc(256);
|
|
286
|
+
const read = readSync(fd, buffer, 0, buffer.length, null);
|
|
287
|
+
return buffer.toString("utf8", 0, read).trim();
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
} finally {
|
|
291
|
+
closeSync(fd);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Put the install directory on PATH before anything probes it.
|
|
297
|
+
*
|
|
298
|
+
* Installers write into `~/.local/bin`, and the post-install check then looks
|
|
299
|
+
* for the binary by bare name. That directory is on PATH by default on a
|
|
300
|
+
* developer workstation and is NOT on a minimal container, so without this
|
|
301
|
+
* every install in a container would report "exited 0 but still not on PATH"
|
|
302
|
+
* while every laptop run passed — the environment doing the verifying hiding
|
|
303
|
+
* the bug. Prepended, and compared entry-by-entry rather than by substring.
|
|
304
|
+
* @param {string} dir Directory to ensure is present.
|
|
305
|
+
* @param {object} [env] Environment to mutate.
|
|
306
|
+
*/
|
|
307
|
+
export function ensureOnPath(dir, env = process.env) {
|
|
308
|
+
const entries = (env.PATH ?? "").split(":").filter(Boolean);
|
|
309
|
+
if (!entries.includes(dir)) env.PATH = [dir, ...entries].join(":");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (process.argv[1] && process.argv[1].endsWith("cli.mjs")) {
|
|
313
|
+
for (const dir of pathDirs(process.env.HOME ?? "")) ensureOnPath(dir);
|
|
314
|
+
process.exitCode = await run(process.argv.slice(2), { ask: askTty });
|
|
315
|
+
}
|