@bahulam/code 0.1.12 → 0.1.14
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/package.json +1 -1
- package/src/commands/install.mjs +382 -0
- package/src/commands/plugin-manage.mjs +467 -89
- package/src/config/model-catalog-default.json +0 -4
- package/src/config/settings-loader.mjs +15 -0
- package/src/core/resume-mode.mjs +0 -1
- package/src/core/stream-client.mjs +18 -0
- package/src/core/tool-executor.mjs +47 -0
- package/src/daemon/session-core.mjs +3 -0
- package/src/local-service/agent-relay.mjs +6 -1
- package/src/local-service/file-access.mjs +116 -2
- package/src/local-service/server.mjs +209 -20
- package/src/plugins/manifest.mjs +3 -0
- package/src/plugins/npm-install.mjs +138 -0
- package/src/plugins/pi-compat/loader-hook.mjs +45 -0
- package/src/plugins/pi-compat/probe.mjs +294 -0
- package/src/plugins/pi-compat/requirements.mjs +465 -0
- package/src/plugins/pi-compat/scaffold.mjs +563 -0
- package/src/plugins/pi-compat/shim.mjs +162 -0
- package/src/plugins/pi-compose.mjs +147 -0
- package/src/plugins/preflight.mjs +29 -2
- package/src/plugins/registry.mjs +6 -0
- package/src/terminal/main.mjs +33 -6
- package/src/terminal/paste-input.mjs +23 -0
- package/src/terminal/repl.mjs +104 -30
- package/src/tools/registry.mjs +19 -0
- package/src/ui/input-dock.mjs +53 -12
- package/src/ui/text-layout.mjs +4 -3
package/package.json
CHANGED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level `bahulam pull` and `bahulam install` commands.
|
|
3
|
+
*
|
|
4
|
+
* bahulam pull <src> — install an ingredient (currently pi:<name>).
|
|
5
|
+
* No pack scaffolding; the raw package lands in
|
|
6
|
+
* ~/.bahulam/plugins-pi/ and is only useful when
|
|
7
|
+
* referenced by a pack's spec.composes:.
|
|
8
|
+
*
|
|
9
|
+
* bahulam install <src> — install a full pack.
|
|
10
|
+
* For pi:<name>: pulls the ingredient AND
|
|
11
|
+
* scaffolds a Bahulam pack around it (composes +
|
|
12
|
+
* native state layer + agent + workspace panel),
|
|
13
|
+
* then installs the pack via preflight.
|
|
14
|
+
* For git URL / tarball URL / local path: installs
|
|
15
|
+
* as a hand-authored pack (delegates to the
|
|
16
|
+
* existing plugin-manage install path).
|
|
17
|
+
*
|
|
18
|
+
* The split lets users pull pi ingredients they only need as compose targets
|
|
19
|
+
* without generating a full pack, while giving one-command installs for
|
|
20
|
+
* users who want an agent-ready surface from a pi package.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import {
|
|
26
|
+
classifySource,
|
|
27
|
+
installFromGit,
|
|
28
|
+
installFromTarball,
|
|
29
|
+
installFromLocal,
|
|
30
|
+
installFromPi,
|
|
31
|
+
pluginTargetDir,
|
|
32
|
+
resolveComposeDependencies,
|
|
33
|
+
} from './plugin-manage.mjs';
|
|
34
|
+
import { preflightPlugin, existingInstalledNames } from '../plugins/preflight.mjs';
|
|
35
|
+
|
|
36
|
+
const RESET = '\x1b[0m';
|
|
37
|
+
const BOLD = '\x1b[1m';
|
|
38
|
+
const DIM = '\x1b[2m';
|
|
39
|
+
const CYAN = '\x1b[36m';
|
|
40
|
+
const GREEN = '\x1b[32m';
|
|
41
|
+
const YELLOW = '\x1b[33m';
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const parsed = {
|
|
45
|
+
source: null,
|
|
46
|
+
force: false,
|
|
47
|
+
global: true,
|
|
48
|
+
ref: null,
|
|
49
|
+
json: false,
|
|
50
|
+
help: false,
|
|
51
|
+
slug: null,
|
|
52
|
+
state: true,
|
|
53
|
+
workspace: true,
|
|
54
|
+
};
|
|
55
|
+
const positional = [];
|
|
56
|
+
for (let i = 0; i < argv.length; i++) {
|
|
57
|
+
const arg = argv[i];
|
|
58
|
+
switch (arg) {
|
|
59
|
+
case '--help': case '-h': parsed.help = true; break;
|
|
60
|
+
case '--force': case '-f': parsed.force = true; break;
|
|
61
|
+
case '--project': parsed.global = false; break;
|
|
62
|
+
case '--global': parsed.global = true; break;
|
|
63
|
+
case '--ref': case '--tag': case '--branch': parsed.ref = argv[++i]; break;
|
|
64
|
+
case '--json': parsed.json = true; break;
|
|
65
|
+
case '--slug': case '--name': parsed.slug = argv[++i]; break;
|
|
66
|
+
case '--no-state': parsed.state = false; break;
|
|
67
|
+
case '--no-workspace': parsed.workspace = false; break;
|
|
68
|
+
default:
|
|
69
|
+
if (!arg.startsWith('-')) positional.push(arg);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
parsed.source = positional.shift() || null;
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `bahulam pull <src>` — ingredient only. Currently pi: sources only.
|
|
79
|
+
*/
|
|
80
|
+
export async function handlePullCommand(argv, { cwd = process.cwd() } = {}) {
|
|
81
|
+
const args = parseArgs(argv);
|
|
82
|
+
if (args.help || !args.source) {
|
|
83
|
+
process.stderr.write(`
|
|
84
|
+
${BOLD}bahulam pull <source>${RESET}
|
|
85
|
+
|
|
86
|
+
Pull an ingredient (pi package) into ~/.bahulam/plugins-pi/. The
|
|
87
|
+
ingredient is composable but not directly runnable — reference it from
|
|
88
|
+
a pack's ${CYAN}spec.composes:${RESET} block, or use ${CYAN}bahulam install pi:<name>${RESET}
|
|
89
|
+
to auto-scaffold a full pack around it.
|
|
90
|
+
|
|
91
|
+
Sources:
|
|
92
|
+
pi:<npm-package>[@<version>] Pull a pi package (e.g. pi:pi-web-access@^0.27.0)
|
|
93
|
+
|
|
94
|
+
Flags:
|
|
95
|
+
--force, -f Overwrite an existing ingredient at the same path
|
|
96
|
+
--json Machine-readable output
|
|
97
|
+
|
|
98
|
+
`);
|
|
99
|
+
if (!args.source) process.exit(1);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const classified = classifySource(args.source);
|
|
104
|
+
if (classified.kind !== 'pi') {
|
|
105
|
+
if (classified.kind === 'invalid') {
|
|
106
|
+
throw new Error(`invalid pi source: ${args.source}`);
|
|
107
|
+
}
|
|
108
|
+
throw new Error(
|
|
109
|
+
`pull only accepts pi: sources. For ${classified.kind} sources use \`bahulam install ${args.source}\`.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const dest = await installFromPi({
|
|
115
|
+
packageName: classified.package_name,
|
|
116
|
+
versionRange: classified.version_range,
|
|
117
|
+
force: args.force,
|
|
118
|
+
});
|
|
119
|
+
if (args.json) {
|
|
120
|
+
process.stdout.write(JSON.stringify({
|
|
121
|
+
ok: true,
|
|
122
|
+
kind: 'pi',
|
|
123
|
+
package_name: classified.package_name,
|
|
124
|
+
version_range: classified.version_range,
|
|
125
|
+
directory: dest,
|
|
126
|
+
}, null, 2) + '\n');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
process.stderr.write(`\n${GREEN}✓${RESET} Pulled pi package ${BOLD}${classified.package_name}${RESET}\n`);
|
|
130
|
+
process.stderr.write(` ${DIM}location${RESET} ${dest}\n`);
|
|
131
|
+
process.stderr.write(` ${YELLOW}!${RESET} Pi packages run with your full system permissions. Bahulam does not audit pi packages.\n`);
|
|
132
|
+
process.stderr.write(` ${DIM}Wrap in a pack:${RESET} ${CYAN}bahulam install pi:${classified.package_name}${RESET}\n`);
|
|
133
|
+
process.stderr.write(` ${DIM}Compose in yours:${RESET} ${CYAN}spec.composes: [{source: pi:${classified.package_name}, expose: [...]}]${RESET}\n\n`);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
process.stderr.write(`\x1b[31m✗\x1b[0m ${err.message}\n`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `bahulam install <src>` — full pack install.
|
|
142
|
+
* pi:<name> → pull ingredient + scaffold pack + preflight-install
|
|
143
|
+
* git URL → clone + preflight-install
|
|
144
|
+
* tarball → download + preflight-install
|
|
145
|
+
* local path → copy + preflight-install
|
|
146
|
+
*/
|
|
147
|
+
export async function handleInstallCommand(argv, { cwd = process.cwd() } = {}) {
|
|
148
|
+
const args = parseArgs(argv);
|
|
149
|
+
if (args.help || !args.source) {
|
|
150
|
+
process.stderr.write(`
|
|
151
|
+
${BOLD}bahulam install <source>${RESET}
|
|
152
|
+
|
|
153
|
+
Install a pack. For pi sources, pulls the ingredient and scaffolds a
|
|
154
|
+
full Bahulam pack (composition + state layer + workspace + agent), then
|
|
155
|
+
installs it. For git/tarball/local sources, installs the existing pack.
|
|
156
|
+
|
|
157
|
+
Sources:
|
|
158
|
+
pi:<npm-package>[@<version>] Scaffold + install from a pi package
|
|
159
|
+
<git-url>[.git] Clone a hand-authored pack
|
|
160
|
+
<tarball-url> Download + install a pack tarball
|
|
161
|
+
<local-path> Copy + install a local pack directory
|
|
162
|
+
|
|
163
|
+
Flags:
|
|
164
|
+
--force, -f Overwrite an existing pack at the same target
|
|
165
|
+
--slug <name> Override the scaffolded pack slug (pi sources only)
|
|
166
|
+
--no-state Skip the persistent state layer (pi sources only)
|
|
167
|
+
--no-workspace Skip the reactive workspace panel (pi sources only)
|
|
168
|
+
--project Install into ./.bahulam/plugins/ instead of ~/.bahulam/plugins/
|
|
169
|
+
--ref <ref> Git branch/tag/commit (git sources only)
|
|
170
|
+
--json Machine-readable output
|
|
171
|
+
|
|
172
|
+
`);
|
|
173
|
+
if (!args.source) process.exit(1);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const classified = classifySource(args.source);
|
|
178
|
+
const targetDir = pluginTargetDir({ global: args.global, cwd });
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
if (classified.kind === 'pi') {
|
|
182
|
+
await installPiWithScaffolding({ classified, targetDir, cwd, args });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (classified.kind === 'invalid') {
|
|
186
|
+
throw new Error(`unrecognized source: ${args.source}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Non-pi paths reuse the plugin-manage install machinery.
|
|
190
|
+
let dest;
|
|
191
|
+
if (classified.kind === 'git') {
|
|
192
|
+
dest = await installFromGit({ url: classified.url, targetDir, ref: args.ref, force: args.force });
|
|
193
|
+
} else if (classified.kind === 'tarball') {
|
|
194
|
+
dest = await installFromTarball({ url: classified.url, targetDir, force: args.force });
|
|
195
|
+
} else if (classified.kind === 'local') {
|
|
196
|
+
dest = await installFromLocal({ src: classified.path, targetDir, force: args.force });
|
|
197
|
+
} else if (classified.kind === 'name') {
|
|
198
|
+
throw new Error(`registry lookup for bare names is not yet wired into \`bahulam install\`. Provide a git URL, tarball URL, local path, or pi: source.`);
|
|
199
|
+
} else {
|
|
200
|
+
throw new Error(`could not resolve source: ${args.source}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await preflightAndReport({ dest, args, cwd });
|
|
204
|
+
} catch (err) {
|
|
205
|
+
process.stderr.write(`\x1b[31m✗\x1b[0m ${err.message}\n`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function installPiWithScaffolding({ classified, targetDir, cwd, args }) {
|
|
211
|
+
const { bahulamHome } = await import('../core/paths.mjs');
|
|
212
|
+
const { discoverPiTools } = await import('../plugins/pi-compat/probe.mjs');
|
|
213
|
+
const { scaffoldPiPack } = await import('../plugins/pi-compat/scaffold.mjs');
|
|
214
|
+
|
|
215
|
+
const piBaseDir = path.join(bahulamHome(), 'plugins-pi');
|
|
216
|
+
const safeName = classified.package_name.replace(/[/@]/g, '_');
|
|
217
|
+
const piDir = path.join(piBaseDir, safeName);
|
|
218
|
+
|
|
219
|
+
// Step 1: pull the ingredient if we haven't already.
|
|
220
|
+
if (!fs.existsSync(piDir)) {
|
|
221
|
+
process.stderr.write(`${DIM}pulling ${classified.package_name}…${RESET}\n`);
|
|
222
|
+
await installFromPi({
|
|
223
|
+
packageName: classified.package_name,
|
|
224
|
+
versionRange: classified.version_range,
|
|
225
|
+
force: false,
|
|
226
|
+
});
|
|
227
|
+
} else {
|
|
228
|
+
process.stderr.write(`${DIM}reusing existing ingredient at ${piDir}${RESET}\n`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Step 2: ensure the tools cache is present.
|
|
232
|
+
const discovered = await discoverPiTools(piDir, { pluginName: classified.package_name });
|
|
233
|
+
|
|
234
|
+
// Step 2b: host check for required binaries. Install (unlike pull)
|
|
235
|
+
// implies "use this now", so a missing ffmpeg-class dep will fail at
|
|
236
|
+
// first tool call — better to fail loudly here. `--force` bypasses
|
|
237
|
+
// for offline provisioning / CI where deps land later.
|
|
238
|
+
await enforceHostRequirements({ piDir, packageName: classified.package_name, args });
|
|
239
|
+
|
|
240
|
+
// Step 3: generate the pack directory (composes + state + agent + panel).
|
|
241
|
+
process.stderr.write(`${DIM}scaffolding pack…${RESET}\n`);
|
|
242
|
+
const { dest, slug, namespace, exposeTools, agentSlug } = scaffoldPiPack({
|
|
243
|
+
packageName: classified.package_name,
|
|
244
|
+
versionRange: classified.version_range,
|
|
245
|
+
piDir,
|
|
246
|
+
targetDir,
|
|
247
|
+
discoveredTools: discovered,
|
|
248
|
+
state: args.state,
|
|
249
|
+
workspace: args.workspace,
|
|
250
|
+
slug: args.slug,
|
|
251
|
+
force: args.force,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Step 4: preflight the generated pack. Same rules as any other pack.
|
|
255
|
+
await preflightAndReport({ dest, args, cwd, meta: { slug, namespace, exposeTools, agentSlug, packageName: classified.package_name } });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function preflightAndReport({ dest, args, cwd, meta = null }) {
|
|
259
|
+
const preflight = await preflightPlugin(dest, {
|
|
260
|
+
existingPluginNames: () => existingInstalledNames(cwd),
|
|
261
|
+
});
|
|
262
|
+
if (!preflight.ok) {
|
|
263
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
264
|
+
const detail = preflight.errors.map(e => ` · ${e}`).join('\n');
|
|
265
|
+
throw new Error(`preflight failed — rolled back ${dest}:\n${detail}`);
|
|
266
|
+
}
|
|
267
|
+
if (preflight.warnings.length) {
|
|
268
|
+
for (const w of preflight.warnings) process.stderr.write(`${YELLOW}!${RESET} ${w}\n`);
|
|
269
|
+
}
|
|
270
|
+
const m = preflight.manifest;
|
|
271
|
+
|
|
272
|
+
// A hand-authored pack may compose pi packages we haven't pulled yet.
|
|
273
|
+
// Do it in one command; the scaffolder path already has the pi ingredient.
|
|
274
|
+
await resolveComposeDependencies(m, { targetDir: path.dirname(dest) });
|
|
275
|
+
|
|
276
|
+
// Host check for each composed pi ingredient (same policy as the
|
|
277
|
+
// scaffolder path). Blocks install if a required binary is missing.
|
|
278
|
+
const composes = m.spec?.composes || [];
|
|
279
|
+
if (composes.length && !meta) {
|
|
280
|
+
// meta present == scaffolder path already did this pre-scaffold
|
|
281
|
+
const { bahulamHome } = await import('../core/paths.mjs');
|
|
282
|
+
const piBaseDir = path.join(bahulamHome(), 'plugins-pi');
|
|
283
|
+
for (const compose of composes) {
|
|
284
|
+
if (!compose.package_name) continue;
|
|
285
|
+
const safeName = compose.package_name.replace(/[/@]/g, '_');
|
|
286
|
+
const piDir = path.join(piBaseDir, safeName);
|
|
287
|
+
if (!fs.existsSync(piDir)) continue;
|
|
288
|
+
try {
|
|
289
|
+
await enforceHostRequirements({ piDir, packageName: compose.package_name, args });
|
|
290
|
+
} catch (err) {
|
|
291
|
+
// Roll back the pack install — the composed dep won't work.
|
|
292
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
293
|
+
throw err;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (args.json) {
|
|
299
|
+
process.stdout.write(JSON.stringify({
|
|
300
|
+
ok: true,
|
|
301
|
+
name: m.metadata.name,
|
|
302
|
+
version: m.metadata.version,
|
|
303
|
+
directory: dest,
|
|
304
|
+
scaffolded: Boolean(meta),
|
|
305
|
+
...(meta ? { pi_package: meta.packageName, namespace: meta.namespace, composed_tools: meta.exposeTools, agent: meta.agentSlug } : {}),
|
|
306
|
+
}, null, 2) + '\n');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
process.stderr.write(`\n${GREEN}✓${RESET} Installed ${BOLD}${m.metadata.name}${RESET} v${m.metadata.version}\n`);
|
|
311
|
+
process.stderr.write(` ${DIM}location${RESET} ${dest}\n`);
|
|
312
|
+
const nativeTools = (m.spec.tools || []).map(t => t.name);
|
|
313
|
+
const composedCount = (m.spec.composes || []).reduce((n, c) => n + ((c.expose || []).length || 0), 0);
|
|
314
|
+
process.stderr.write(` ${DIM}tools${RESET} ${nativeTools.length ? nativeTools.join(', ') : '(none)'}${composedCount ? ` ${DIM}+ ${composedCount} composed${RESET}` : ''}\n`);
|
|
315
|
+
process.stderr.write(` ${DIM}agents${RESET} ${(m.spec.agents || []).map(a => a.slug).join(', ') || '(none)'}\n`);
|
|
316
|
+
const views = m.spec.workspace?.views || [];
|
|
317
|
+
process.stderr.write(` ${DIM}views${RESET} ${views.length ? views.map(v => v.name).join(', ') : '(none)'}\n`);
|
|
318
|
+
if (meta) {
|
|
319
|
+
process.stderr.write(` ${DIM}scaffolded${RESET} from ${CYAN}pi:${meta.packageName}${RESET} (namespace ${CYAN}${meta.namespace}${RESET}, ${meta.exposeTools.length} composed tool${meta.exposeTools.length === 1 ? '' : 's'})\n`);
|
|
320
|
+
process.stderr.write(` ${DIM}Edit the pack under ${dest} to customize.${RESET}\n`);
|
|
321
|
+
}
|
|
322
|
+
process.stderr.write(`\n ${DIM}Open with:${RESET} ${CYAN}bahulam plugin ${m.metadata.name}${RESET}\n\n`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Install-time host check: read the ingredient's requirements sidecar,
|
|
327
|
+
* verify each detected binary is on PATH. Throws with an actionable
|
|
328
|
+
* message (per-OS install hints) if anything required is missing.
|
|
329
|
+
* `--force` bypasses (for CI, offline provisioning, dev workflows where
|
|
330
|
+
* deps land later).
|
|
331
|
+
*
|
|
332
|
+
* Env vars / credentials are warn-only — many pi tools have optional
|
|
333
|
+
* features and blocking on a missing PEXELS_API_KEY when the user only
|
|
334
|
+
* wants media_probe is too aggressive.
|
|
335
|
+
*/
|
|
336
|
+
async function enforceHostRequirements({ piDir, packageName, args }) {
|
|
337
|
+
const { checkRequirementsAgainstHost, REQUIREMENTS_FILE, analyzeRequirements } =
|
|
338
|
+
await import('../plugins/pi-compat/requirements.mjs');
|
|
339
|
+
|
|
340
|
+
const sidecar = path.join(piDir, REQUIREMENTS_FILE);
|
|
341
|
+
let reqs = null;
|
|
342
|
+
if (fs.existsSync(sidecar)) {
|
|
343
|
+
try { reqs = JSON.parse(fs.readFileSync(sidecar, 'utf-8')); } catch { /* re-analyze */ }
|
|
344
|
+
}
|
|
345
|
+
if (!reqs) {
|
|
346
|
+
// Sidecar was missing (older ingredient install or analyzer crash) —
|
|
347
|
+
// synthesize on the fly so the check is never silently skipped.
|
|
348
|
+
let discoveredTools = null;
|
|
349
|
+
const toolsCache = path.join(piDir, '.bahulam-tools.json');
|
|
350
|
+
if (fs.existsSync(toolsCache)) {
|
|
351
|
+
try { discoveredTools = JSON.parse(fs.readFileSync(toolsCache, 'utf-8')); } catch { /* ignore */ }
|
|
352
|
+
}
|
|
353
|
+
reqs = analyzeRequirements(piDir, { discoveredTools });
|
|
354
|
+
}
|
|
355
|
+
if (!reqs?.system_binaries?.length) {
|
|
356
|
+
// Nothing to check.
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const host = checkRequirementsAgainstHost(reqs);
|
|
361
|
+
const missing = host.binaries.filter(b => !b.found);
|
|
362
|
+
if (missing.length === 0) return;
|
|
363
|
+
|
|
364
|
+
const platformKey = process.platform === 'darwin' ? 'darwin' : 'linux';
|
|
365
|
+
const lines = [];
|
|
366
|
+
lines.push(`${packageName} needs ${missing.length} system binar${missing.length === 1 ? 'y' : 'ies'} not found on your PATH:`);
|
|
367
|
+
for (const b of missing) {
|
|
368
|
+
const hint = b.install_hints?.[platformKey];
|
|
369
|
+
lines.push(` · ${b.name}${hint ? ` — install: ${CYAN}${hint}${RESET}` : ''}`);
|
|
370
|
+
}
|
|
371
|
+
if (args.force) {
|
|
372
|
+
process.stderr.write(`${YELLOW}!${RESET} ${lines.join('\n')}\n`);
|
|
373
|
+
process.stderr.write(`${YELLOW}!${RESET} ${DIM}--force set — continuing anyway. Composed tools using these binaries will fail at first call.${RESET}\n`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const err = new Error(
|
|
377
|
+
`${lines.join('\n')}\n\n` +
|
|
378
|
+
`Install the missing binaries, then rerun. Or use ${CYAN}--force${RESET} to install without them (composed tools using these will fail at first call).\n` +
|
|
379
|
+
`Verify anytime with: ${CYAN}bahulam plugin doctor pi:${packageName}${RESET}`,
|
|
380
|
+
);
|
|
381
|
+
throw err;
|
|
382
|
+
}
|