@bahulam/code 0.1.12 → 0.1.13
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 +295 -0
- package/src/commands/plugin-manage.mjs +280 -88
- package/src/config/settings-loader.mjs +15 -0
- package/src/core/tool-executor.mjs +47 -0
- package/src/plugins/manifest.mjs +3 -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/scaffold.mjs +487 -0
- package/src/plugins/pi-compat/shim.mjs +134 -0
- package/src/plugins/pi-compose.mjs +147 -0
- package/src/plugins/preflight.mjs +27 -2
- package/src/plugins/registry.mjs +6 -0
- package/src/terminal/main.mjs +31 -5
- package/src/terminal/paste-input.mjs +23 -0
- package/src/terminal/repl.mjs +52 -7
- package/src/tools/registry.mjs +19 -0
- package/src/ui/input-dock.mjs +5 -2
package/package.json
CHANGED
|
@@ -0,0 +1,295 @@
|
|
|
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 3: generate the pack directory (composes + state + agent + panel).
|
|
235
|
+
process.stderr.write(`${DIM}scaffolding pack…${RESET}\n`);
|
|
236
|
+
const { dest, slug, namespace, exposeTools, agentSlug } = scaffoldPiPack({
|
|
237
|
+
packageName: classified.package_name,
|
|
238
|
+
versionRange: classified.version_range,
|
|
239
|
+
piDir,
|
|
240
|
+
targetDir,
|
|
241
|
+
discoveredTools: discovered,
|
|
242
|
+
state: args.state,
|
|
243
|
+
workspace: args.workspace,
|
|
244
|
+
slug: args.slug,
|
|
245
|
+
force: args.force,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Step 4: preflight the generated pack. Same rules as any other pack.
|
|
249
|
+
await preflightAndReport({ dest, args, cwd, meta: { slug, namespace, exposeTools, agentSlug, packageName: classified.package_name } });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function preflightAndReport({ dest, args, cwd, meta = null }) {
|
|
253
|
+
const preflight = await preflightPlugin(dest, {
|
|
254
|
+
existingPluginNames: () => existingInstalledNames(cwd),
|
|
255
|
+
});
|
|
256
|
+
if (!preflight.ok) {
|
|
257
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
258
|
+
const detail = preflight.errors.map(e => ` · ${e}`).join('\n');
|
|
259
|
+
throw new Error(`preflight failed — rolled back ${dest}:\n${detail}`);
|
|
260
|
+
}
|
|
261
|
+
if (preflight.warnings.length) {
|
|
262
|
+
for (const w of preflight.warnings) process.stderr.write(`${YELLOW}!${RESET} ${w}\n`);
|
|
263
|
+
}
|
|
264
|
+
const m = preflight.manifest;
|
|
265
|
+
|
|
266
|
+
// A hand-authored pack may compose pi packages we haven't pulled yet.
|
|
267
|
+
// Do it in one command; the scaffolder path already has the pi ingredient.
|
|
268
|
+
await resolveComposeDependencies(m, { targetDir: path.dirname(dest) });
|
|
269
|
+
|
|
270
|
+
if (args.json) {
|
|
271
|
+
process.stdout.write(JSON.stringify({
|
|
272
|
+
ok: true,
|
|
273
|
+
name: m.metadata.name,
|
|
274
|
+
version: m.metadata.version,
|
|
275
|
+
directory: dest,
|
|
276
|
+
scaffolded: Boolean(meta),
|
|
277
|
+
...(meta ? { pi_package: meta.packageName, namespace: meta.namespace, composed_tools: meta.exposeTools, agent: meta.agentSlug } : {}),
|
|
278
|
+
}, null, 2) + '\n');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
process.stderr.write(`\n${GREEN}✓${RESET} Installed ${BOLD}${m.metadata.name}${RESET} v${m.metadata.version}\n`);
|
|
283
|
+
process.stderr.write(` ${DIM}location${RESET} ${dest}\n`);
|
|
284
|
+
const nativeTools = (m.spec.tools || []).map(t => t.name);
|
|
285
|
+
const composedCount = (m.spec.composes || []).reduce((n, c) => n + ((c.expose || []).length || 0), 0);
|
|
286
|
+
process.stderr.write(` ${DIM}tools${RESET} ${nativeTools.length ? nativeTools.join(', ') : '(none)'}${composedCount ? ` ${DIM}+ ${composedCount} composed${RESET}` : ''}\n`);
|
|
287
|
+
process.stderr.write(` ${DIM}agents${RESET} ${(m.spec.agents || []).map(a => a.slug).join(', ') || '(none)'}\n`);
|
|
288
|
+
const views = m.spec.workspace?.views || [];
|
|
289
|
+
process.stderr.write(` ${DIM}views${RESET} ${views.length ? views.map(v => v.name).join(', ') : '(none)'}\n`);
|
|
290
|
+
if (meta) {
|
|
291
|
+
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`);
|
|
292
|
+
process.stderr.write(` ${DIM}Edit the pack under ${dest} to customize.${RESET}\n`);
|
|
293
|
+
}
|
|
294
|
+
process.stderr.write(`\n ${DIM}Open with:${RESET} ${CYAN}bahulam plugin ${m.metadata.name}${RESET}\n\n`);
|
|
295
|
+
}
|