@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bahulam/code",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Bahulam Code — abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ }