@bitspark-ai/ba 0.1.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.
Files changed (3) hide show
  1. package/README.md +583 -0
  2. package/dist/main.js +28168 -0
  3. package/package.json +18 -0
package/README.md ADDED
@@ -0,0 +1,583 @@
1
+ # ba — the bitagent family CLI
2
+
3
+ > `ba` is **one** CLI for the whole `bitagent` family of services. Each service
4
+ > contributes a top-level namespace (`ba runtime …`, `ba gateway …`, …) so users
5
+ > learn, install, and update a single tool. The runner is **TypeScript**; the
6
+ > services it drives may be any language — `ba` talks to each over that service's
7
+ > SDK/HTTP API, never in-process.
8
+
9
+ This README is the **contributor guide**: if you own a bitagent service and want it
10
+ to appear under `ba`, this is how you add your subcommand.
11
+
12
+ ---
13
+
14
+ ## Table of contents
15
+
16
+ - [The model in 60 seconds](#the-model-in-60-seconds)
17
+ - [Quickstart: add your subcommand](#quickstart-add-your-subcommand)
18
+ - [The `BaPlugin` contract](#the-baplugin-contract)
19
+ - [Authoring a plugin](#authoring-a-plugin)
20
+ - [Anatomy of a command](#anatomy-of-a-command)
21
+ - [Talking to your service](#talking-to-your-service)
22
+ - [Output: always use `ctx.printer`](#output-always-use-ctxprinter)
23
+ - [The global `--target` context](#the-global---target-context)
24
+ - [Errors](#errors)
25
+ - [Testing your plugin](#testing-your-plugin)
26
+ - [Registering your plugin with `ba`](#registering-your-plugin-with-ba)
27
+ - [Naming & packaging conventions](#naming--packaging-conventions)
28
+ - [One service, multiple plugins — the gateway case](#one-service-multiple-plugins--the-gateway-case)
29
+ - [Adapting an existing commander CLI](#adapting-an-existing-commander-cli)
30
+ - [Adapting a service with its own command model](#adapting-a-service-with-its-own-command-model)
31
+ - [Cross-repo local development](#cross-repo-local-development)
32
+ - [Contributor checklist](#contributor-checklist)
33
+ - [Repo layout & commands](#repo-layout--commands)
34
+ - [Roadmap](#roadmap)
35
+
36
+ ---
37
+
38
+ ## The model in 60 seconds
39
+
40
+ ```
41
+ ba (runner) ── owns the cross-cutting UX:
42
+ │ dispatch · help aggregation · --version ·
43
+ │ global --target context · session & credentials
44
+ ├── auth plugin → @bitspark-ai/accounts-cli (human identity — login/out)
45
+ ├── commons plugin → @bitspark-ai/ba-commons-plugin (coordination — roster/inbox/lease/…)
46
+ ├── demo plugin → @bitspark-ai/ba-plugin-demo (reference plugin)
47
+ ├── echo plugin → @bitspark-ai/demo-cli (demo service — identity slice e2e)
48
+ ├── gateway plugin → @bitspark-ai/gateway-cli (Claude gateway — developer)
49
+ ├── runtime plugin → @bitspark-ai/bitagent-runtime-cli (durable agents)
50
+ ├── gateway-admin plugin → @bitspark-ai/gateway-admin-cli (Claude gateway — operator; planned, P3)
51
+ └── <service> plugin → @bitspark-ai/<service>-cli
52
+ ```
53
+
54
+ > Mounted today: `auth`, `commons`, `demo`, `echo`, `gateway`, `runtime` (see `packages/core/src/plugins.ts`).
55
+ > `gateway-admin` is planned (P3); completion + config are P4. `ba gateway` invokes the agw
56
+ > binary at runtime via `GATEWAY_AGW_BIN` → an optional co-installed `bitspark-agw` → `PATH`.
57
+
58
+ - A **plugin** is a package that exports a single `BaPlugin` value.
59
+ - Each plugin owns exactly **one top-level namespace** (`ba <namespace> …`).
60
+ - `ba` mounts plugins at **compile time** (see [Registering](#registering-your-plugin-with-ba)),
61
+ esbuild-bundles them into one self-contained binary, and hands each plugin a
62
+ shared [`BaContext`](#the-global---target-context) so the runner — not the plugin —
63
+ owns version, help, the global `--target`, and the session/credentials context.
64
+ (Completion, richer help, and config are roadmap P4.)
65
+
66
+ > **Why compile-time?** It gives one bundled, fully type-safe `ba` with zero runtime
67
+ > deps. The `BaPlugin` contract is intentionally independent of *how* plugins are
68
+ > discovered, so if we later add installed-plugin discovery (`ba plugin add …`),
69
+ > **your plugin does not change** — only the runner's mount step does.
70
+
71
+ ---
72
+
73
+ ## Quickstart: add your subcommand
74
+
75
+ There are two halves: **author** a plugin in your service's repo, then **register**
76
+ it here.
77
+
78
+ **1. In your service repo**, add a CLI package (convention: `@bitspark-ai/<service>-cli`)
79
+ that exports a `BaPlugin`:
80
+
81
+ ```ts
82
+ // packages/cli/src/index.ts
83
+ import type { BaPlugin } from "@bitspark-ai/ba-plugin-api";
84
+
85
+ export const plugin: BaPlugin = {
86
+ namespace: "myservice",
87
+ summary: "what my service does (one line, shown in `ba --help`)",
88
+ register(program, ctx) {
89
+ const ns = program.command("myservice").description("what my service does");
90
+
91
+ ns.command("ping")
92
+ .description("check the service is reachable")
93
+ .option("--json", "machine-readable output")
94
+ .action(async (opts: { json?: boolean }) => {
95
+ const ok = await myClient(ctx).ping();
96
+ ctx.printer.out(opts.json ? JSON.stringify({ ok }) : ok ? "ok" : "unreachable");
97
+ });
98
+ },
99
+ };
100
+ ```
101
+
102
+ **2. In this repo (`ba-cli`)**, wire it in — two lines:
103
+
104
+ ```jsonc
105
+ // packages/core/package.json
106
+ "dependencies": {
107
+ "@bitspark-ai/myservice-cli": "workspace:*" // or a published ^version
108
+ }
109
+ ```
110
+ ```ts
111
+ // packages/core/src/plugins.ts
112
+ import { plugin as myservice } from "@bitspark-ai/myservice-cli";
113
+ export const PLUGINS: BaPlugin[] = [demo, myservice];
114
+ ```
115
+
116
+ Done. `ba myservice ping` works and `myservice` shows up in `ba --help`.
117
+
118
+ The fastest way to start is to **copy [`packages/plugin-demo`](packages/plugin-demo/src/index.ts)** —
119
+ it is the reference implementation and exists purely to be copied.
120
+
121
+ ---
122
+
123
+ ## The `BaPlugin` contract
124
+
125
+ The full contract lives in [`@bitspark-ai/ba-plugin-api`](packages/plugin-api/src/index.ts).
126
+ It is small on purpose:
127
+
128
+ ```ts
129
+ export interface BaPlugin {
130
+ /** Top-level subcommand — lowercase/hyphenated, e.g. "runtime" | "gateway-admin" ("help" is reserved). */
131
+ readonly namespace: string;
132
+ /** One-line summary shown in `ba --help`. Non-empty. */
133
+ readonly summary: string;
134
+ /** Attach your command subtree to the root program. MUST be synchronous (build it before returning). */
135
+ register(program: Command, ctx: BaContext): void;
136
+ }
137
+
138
+ export interface BaContext {
139
+ /** The active target/context (e.g. an env) if the user passed `--target`. Lazy. */
140
+ readonly target: string | undefined;
141
+ /** Output sink — use this instead of console.*. */
142
+ readonly printer: Printer;
143
+ /** The `ba` runner version. */
144
+ readonly version: string;
145
+ /** The logged-in session for the active target, if any (convenience for `credentials.get()`). */
146
+ readonly session: Session | undefined;
147
+ /** Read/write the local session store, scoped to the active target. */
148
+ readonly credentials: CredentialStore;
149
+ }
150
+
151
+ export interface Printer {
152
+ out(s: string): void;
153
+ err(s: string): void;
154
+ }
155
+ ```
156
+
157
+ Rules the runner relies on:
158
+
159
+ - **One plugin = one namespace.** Two plugins claiming the same `namespace` is a
160
+ hard error at startup (caught in [`buildProgram`](packages/core/src/program.ts)).
161
+ - **Keep the module side-effect-free.** The file that exports your `BaPlugin`
162
+ should do no top-level work (no network, no reading argv, no `console`), so the
163
+ runner can import it cheaply and esbuild can tree-shake cleanly. Do all work
164
+ inside `.action(...)` handlers.
165
+ - **`register` is synchronous.** Build the command tree synchronously; your
166
+ *actions* can be `async`.
167
+ - The contract is **commander-native** (`program` is a [commander](https://github.com/tj/commander.js)
168
+ `Command`). This is deliberate — see [Adapting an existing commander CLI](#adapting-an-existing-commander-cli).
169
+
170
+ ---
171
+
172
+ ## Authoring a plugin
173
+
174
+ ### Anatomy of a command
175
+
176
+ ```ts
177
+ register(program, ctx) {
178
+ const ns = program.command("myservice").description("…"); // your namespace
179
+
180
+ ns.command("run") // a verb under it
181
+ .description("spawn a thing; returns its id")
182
+ .argument("<task>", "the task prompt") // required positional
183
+ .argument("[name]", "optional name", "default") // optional positional w/ default
184
+ .option("--count <n>", "how many", "1") // option with value
185
+ .option("--json", "machine-readable output") // boolean flag
186
+ .action(async (task: string, name: string, opts: { count: string; json?: boolean }) => {
187
+ // … do the work, print via ctx.printer …
188
+ });
189
+ }
190
+ ```
191
+
192
+ Conventions across the family (please follow them so `ba` feels like one tool):
193
+
194
+ - **Namespace = your service**, lowercase, hyphenated (`gateway`, `gateway-admin`).
195
+ - **Verbs are short and imperative**: `run`, `ps`, `logs`, `stop`, `send`, `get`,
196
+ `list`. Match sibling services where it makes sense.
197
+ - **Every command that prints data supports `--json`** for scripting. Humans get a
198
+ terse default; `--json` gets `JSON.stringify`.
199
+ - Prefer **flags with values** (`--control-url <url>`) over positional soup.
200
+
201
+ ### Talking to your service
202
+
203
+ `ba` is a thin client. Your plugin should call your service over **its SDK / HTTP
204
+ API**, never reach into its process. Build your client inside the action (or a tiny
205
+ factory) so it stays injectable for tests:
206
+
207
+ ```ts
208
+ import { MyServiceClient } from "@bitspark-ai/myservice-client";
209
+
210
+ function makeClient(ctx: BaContext) {
211
+ return new MyServiceClient({
212
+ baseUrl: process.env.MYSERVICE_URL ?? "http://127.0.0.1:4500",
213
+ apiKey: process.env.MYSERVICE_API_KEY,
214
+ // resolve env-specific config from ctx.target if you support named targets
215
+ });
216
+ }
217
+ ```
218
+
219
+ The gateway plugins reuse the gateway's existing SDK packages —
220
+ `@bitspark-ai/gateway-client` (transport) and `@bitspark-ai/gateway-cli-targets`
221
+ (named env/target resolution) — rather than re-implementing transport.
222
+
223
+ ### Output: always use `ctx.printer`
224
+
225
+ Do **not** call `console.log` / `console.error` directly. Use `ctx.printer.out` and
226
+ `ctx.printer.err`. The runner injects the printer, which is what makes your commands
227
+ testable (capture output) and lets the runner control formatting/streams centrally.
228
+
229
+ ### The global `--target` context
230
+
231
+ `--target <name>` is a **runner-global** flag (e.g. select an environment). Read it
232
+ via `ctx.target` — it is lazy, so it reflects the value parsed from argv at action
233
+ time:
234
+
235
+ ```ts
236
+ .action(() => {
237
+ const env = ctx.target ?? "local";
238
+ // …resolve config for `env`, then talk to that env's service…
239
+ });
240
+ ```
241
+
242
+ Don't define your own `--target`/`--env`/`--version` on your namespace; inherit the
243
+ runner's. Service-specific connection flags (`--control-url`) are fine to add.
244
+
245
+ ### Errors
246
+
247
+ Throw — don't `process.exit` inside an action. The runner's top-level catch in
248
+ [`main.ts`](packages/core/src/main.ts) prints the message and sets exit code 1.
249
+ Throw `Error` with a clear, user-facing message:
250
+
251
+ ```ts
252
+ if (!agentId) throw new Error("myservice run: no agent id returned");
253
+ ```
254
+
255
+ ### Testing your plugin
256
+
257
+ Because output and clients are injectable, you can unit-test a command without
258
+ spawning a process. Mirror the runner's own [smoke test](packages/core/test/smoke.test.ts):
259
+
260
+ ```ts
261
+ import { describe, it, expect } from "vitest";
262
+ import { Command } from "commander";
263
+ import { plugin } from "../src/index";
264
+
265
+ function harness() {
266
+ const lines: string[] = [];
267
+ const program = new Command();
268
+ program.exitOverride(); // don't process.exit in tests
269
+ const ctx = {
270
+ target: undefined,
271
+ version: "test",
272
+ printer: { out: (s: string) => lines.push(s), err: () => {} },
273
+ session: undefined, // no logged-in session in this test
274
+ credentials: { get: () => undefined, set: () => {}, clear: () => {} }, // no-op store
275
+ };
276
+ plugin.register(program, ctx);
277
+ return { program, lines };
278
+ }
279
+
280
+ it("pings", async () => {
281
+ const { program, lines } = harness();
282
+ await program.parseAsync(["node", "ba", "myservice", "ping", "--json"]);
283
+ expect(lines).toEqual([JSON.stringify({ ok: true })]);
284
+ });
285
+ ```
286
+
287
+ (For real I/O, inject a fake client via a `deps` param on your plugin factory, the
288
+ way the runtime CLI does with `CliDeps.makeClient`.)
289
+
290
+ ---
291
+
292
+ ## Registering your plugin with `ba`
293
+
294
+ Mounting is **compile-time and explicit** — there is exactly one place to look:
295
+
296
+ 1. **Export** a `BaPlugin` from your CLI package (above).
297
+ 2. **Depend** on it in [`packages/core/package.json`](packages/core/package.json)
298
+ (`workspace:*` for local dev, or a published `^version` once your package ships).
299
+ 3. **Add** it to `PLUGINS` in [`packages/core/src/plugins.ts`](packages/core/src/plugins.ts):
300
+
301
+ ```ts
302
+ import { plugin as myservice } from "@bitspark-ai/myservice-cli";
303
+
304
+ // The mounted set today (packages/core/src/plugins.ts) is [demo, auth, echo, runtime];
305
+ // append yours. (gateway/gateway-admin are commented "planned additions" there.)
306
+ export const PLUGINS: BaPlugin[] = [demo, auth, echo, runtime, myservice];
307
+ ```
308
+
309
+ The runner sorts namespaces for stable help and refuses duplicates. After wiring,
310
+ `pnpm build` bundles your plugin into the one `ba` binary.
311
+
312
+ ---
313
+
314
+ ## Naming & packaging conventions
315
+
316
+ | Thing | Convention | Example |
317
+ |---|---|---|
318
+ | CLI package name | `@bitspark-ai/<service>-cli` | `@bitspark-ai/gateway-cli` |
319
+ | Plugin export | a named/`plugin` export of type `BaPlugin` | `export const plugin: BaPlugin` |
320
+ | Namespace | lowercase, hyphenated, = the service | `gateway`, `gateway-admin` |
321
+ | Client dep | `@bitspark-ai/<service>-client` (the SDK) | `@bitspark-ai/gateway-client` |
322
+
323
+ > The package name need not equal the namespace — the first-party family diverges for
324
+ > product naming: `@bitspark-ai/accounts-cli` ships the `auth` namespace and
325
+ > `@bitspark-ai/demo-cli` ships `echo`. `isBaPlugin` enforces only the namespace grammar
326
+ > (lowercase, hyphenated, leading letter; `help` is reserved), not the package name.
327
+
328
+ Keep the plugin package **thin**: command wiring + your SDK. No business logic that
329
+ belongs in the service.
330
+
331
+ ---
332
+
333
+ ## One service, multiple plugins — the gateway case
334
+
335
+ A single service repo can contribute **more than one** plugin (more than one
336
+ namespace). The Claude gateway (`claude-gateway`, soon `bitagent/gateway`) is the
337
+ canonical example — it contributes **two**:
338
+
339
+ | Namespace | Package | Audience | Contains |
340
+ |---|---|---|---|
341
+ | `gateway` | `@bitspark-ai/gateway-cli` | **developer** (public) | auth/setup, connection, projects — day-to-day developer use |
342
+ | `gateway-admin` | `@bitspark-ai/gateway-admin-cli` | **operator** | deploy, ops, capacity, profiles, routes, retention, audit |
343
+
344
+ Why two plugins instead of one role-aware command:
345
+
346
+ - **Structural boundary.** Operator code lives in a package the developer plugin
347
+ **never depends on**. The public developer surface cannot ship operator/deploy
348
+ logic by construction — it's a dependency-graph fact, not a build-time strip.
349
+ (This replaces the gateway's old single `agw` binary with its `--public` esbuild
350
+ stub + content audit.)
351
+ - **Two distinct UXs.** Developers and operators are different audiences; each gets
352
+ a focused namespace, help, and setup flow. Someone who is both installs a `ba`
353
+ that has both namespaces.
354
+
355
+ Both gateway plugins are built on the same gateway SDK packages
356
+ (`@bitspark-ai/gateway-client`, `@bitspark-ai/gateway-cli-targets`), so the API
357
+ layer is shared, not duplicated — only the command surfaces differ.
358
+
359
+ If your service has a similar split (a public surface and a privileged one), follow
360
+ this pattern: two packages, two namespaces, one shared SDK.
361
+
362
+ ---
363
+
364
+ ## Adapting an existing commander CLI
365
+
366
+ If your service already has a [commander](https://github.com/tj/commander.js) CLI
367
+ (like the runtime CLI, which already does `program.name("ba").command("runtime")…`),
368
+ adapting it is nearly mechanical: move the body that builds your `program.command(ns)`
369
+ subtree into `register`, and take `program` + `ctx` as inputs instead of creating
370
+ your own `Command` and reading `process.env`/`console` directly.
371
+
372
+ ```ts
373
+ // before: buildProgram(): Command { const program = new Command(); const rt = program.command("runtime")…; return program; }
374
+ // after:
375
+ export const plugin: BaPlugin = {
376
+ namespace: "runtime",
377
+ summary: "durable, decoupled, movable agents",
378
+ register(program, ctx) {
379
+ const rt = program.command("runtime").description("durable, decoupled, movable agents");
380
+ rt.command("run") /* …unchanged… */;
381
+ // swap console.log → ctx.printer.out; drop your own --version.
382
+ },
383
+ };
384
+ ```
385
+
386
+ Keep your service's own standalone bin during the transition if you want; it can
387
+ import the same command-building code the plugin uses.
388
+
389
+ ---
390
+
391
+ ## Adapting a service with its own command model
392
+
393
+ If your service describes commands as **data** (e.g. the gateway's declarative
394
+ command registry with role/capability gating), don't fight it — write a small
395
+ **adapter** in `register` that walks your data and calls `program.command(...)`,
396
+ mapping your gating to a pre-action check:
397
+
398
+ ```ts
399
+ register(program, ctx) {
400
+ const ns = program.command("gateway-admin").description("…");
401
+ for (const entry of OPERATOR_COMMANDS) { // your declarative registry
402
+ const cmd = ns.command(entry.name).description(entry.summary);
403
+ for (const opt of entry.options ?? []) cmd.option(opt.flag, opt.desc);
404
+ cmd.action((...args) => {
405
+ assertCapability(entry.group, ctx); // your gating, surfaced as an error
406
+ return entry.dispatch({ ctx, args });
407
+ });
408
+ }
409
+ }
410
+ ```
411
+
412
+ The runner stays oblivious to your internal model; it only sees commander commands.
413
+
414
+ ---
415
+
416
+ ## Cross-repo local development
417
+
418
+ The family is **federated** — `ba-cli`, `runtime`, `gateway`, etc. are separate
419
+ repos in sibling directories under `C:\Development\bitspark\bitagent\`, each its own
420
+ pnpm workspace. Two ways to develop a plugin against a live `ba`:
421
+
422
+ - **Published version (steady state):** your `*-cli` package is published; `ba-cli`
423
+ depends on a `^version`. Bump to upgrade.
424
+ - **Local link (while iterating):** point the dependency at your working copy via a
425
+ pnpm [`overrides`](https://pnpm.io/package_json#pnpmoverrides) entry or
426
+ `pnpm link`, so `ba` bundles your local plugin source. (If the family later
427
+ consolidates into one workspace, this becomes plain `workspace:*`.)
428
+
429
+ Today the mounted sibling plugins are wired as `workspace:*` members in
430
+ [`pnpm-workspace.yaml`](pnpm-workspace.yaml) (`../runtime/*`, `../commons/*`).
431
+
432
+ > **Build prerequisite for dist-first plugins.** Most family packages are
433
+ > **source-first** (`main` → `./src/index.ts`), so esbuild/tsc read their TypeScript
434
+ > directly — no prebuild. A few are **dist-first** (`main` → `./dist`), notably the
435
+ > commons plugin chain (`@bitspark-ai/ba-commons-plugin` → `@bitspark-ai/commons-client`
436
+ > → `@bitspark-ai/commons`). For those, the sibling workspace **must be built before**
437
+ > `ba` is type-checked or bundled here, or `pnpm check`/`pnpm build` can't resolve their
438
+ > `./dist`. In a fresh checkout (or after commons changes):
439
+ >
440
+ > ```sh
441
+ > pnpm -C ../commons install --frozen-lockfile && pnpm -C ../commons build
442
+ > # then, in this repo:
443
+ > pnpm install && pnpm check && pnpm test && pnpm build
444
+ > ```
445
+
446
+ Run the runner without building:
447
+
448
+ ```sh
449
+ pnpm exec tsx packages/core/src/main.ts <args> # e.g. … myservice ping --json
450
+ ```
451
+
452
+ > Gotcha: `pnpm run dev -- --flag` forwards a literal `--` into argv, which commander
453
+ > treats as end-of-options. Use `pnpm exec tsx …` (above) when passing top-level
454
+ > flags like `--help`/`--version`/`--target`.
455
+
456
+ ---
457
+
458
+ ## Contributor checklist
459
+
460
+ Before you open a PR to add your plugin:
461
+
462
+ - [ ] Plugin module is **side-effect-free**; all work is inside `.action`.
463
+ - [ ] Exactly **one namespace**, lowercase/hyphenated, named after your service.
464
+ - [ ] All output goes through **`ctx.printer`** (no `console.*`).
465
+ - [ ] Data commands support **`--json`**.
466
+ - [ ] No custom `--version`/`--target`; inherit the runner's.
467
+ - [ ] Actions **throw** on failure (no `process.exit`).
468
+ - [ ] You talk to your service via its **SDK**, with an injectable client.
469
+ - [ ] At least one **vitest** test that parses argv and asserts on captured output.
470
+ - [ ] Registered in **`packages/core`** (dependency + `PLUGINS` entry).
471
+ - [ ] `pnpm check`, `pnpm test`, and `pnpm build` are green.
472
+
473
+ ---
474
+
475
+ ## Repo layout & commands
476
+
477
+ ```
478
+ packages/
479
+ plugin-api/ @bitspark-ai/ba-plugin-api — the BaPlugin contract (the linchpin)
480
+ core/ @bitspark-ai/ba — the runner + bin `ba` + compile-time mount
481
+ plugin-demo/ @bitspark-ai/ba-plugin-demo — reference plugin / copy-me template
482
+ accounts-client/ @bitspark-ai/accounts-client — isomorphic accounts SDK (login + session keys)
483
+ accounts-cli/ @bitspark-ai/accounts-cli — the `ba auth` plugin (human identity)
484
+ demo-cli/ @bitspark-ai/demo-cli — the `ba echo` plugin (identity slice, end to end)
485
+ ```
486
+
487
+ ```sh
488
+ pnpm install
489
+ pnpm exec tsx packages/core/src/main.ts --help # run the runner via tsx
490
+ pnpm exec tsx packages/core/src/main.ts demo greet Ada
491
+ pnpm check # tsc --noEmit across packages
492
+ pnpm test # vitest
493
+ pnpm build # esbuild → one self-contained `ba`
494
+ ```
495
+
496
+ Toolchain: Node **24+**, Corepack-pinned **pnpm@10.20.0**, TypeScript **6**,
497
+ commander **12**, vitest **4** — matching the rest of the bitagent family.
498
+
499
+ ---
500
+
501
+ ## Distribution (publishing)
502
+
503
+ `ba` ships as a **single self-contained bundle** — `packages/core/dist/main.js` with
504
+ **zero runtime dependencies** (esbuild inlines every plugin + commander at build time).
505
+ End users install one package and need **no** sibling runtime/commons/gateway checkouts.
506
+
507
+ The source `packages/core/package.json` stays `private` and keeps its `workspace:*`
508
+ plugins as **devDependencies** (build-time only). The publishable manifest is *derived*
509
+ by the dry-run packer (no runtime deps, not private, `bin ba → ./dist/main.js`,
510
+ `files: ["dist/main.js"]`, `publishConfig.access: public`).
511
+
512
+ **Dry-run packaging gate** (proves the tarball without publishing):
513
+
514
+ ```sh
515
+ # family prerequisites (fresh checkout): commons is dist-first, so build it first
516
+ pnpm -C ../commons install --frozen-lockfile && pnpm -C ../commons build
517
+ pnpm install && pnpm check && pnpm test && pnpm build
518
+ # pack → install OUTSIDE the monorepo → smoke the bundled CLI:
519
+ pnpm --filter @bitspark-ai/ba publish:dryrun
520
+ ```
521
+
522
+ [`packages/core/scripts/publish-dryrun.mjs`](packages/core/scripts/publish-dryrun.mjs)
523
+ packs the derived manifest and asserts the **packed** tarball (name, non-`0.0.0`
524
+ version, not private, no `workspace:*`, empty runtime deps, `bin`, file allowlist, no
525
+ `gateway-admin`), installs it in a temp dir **outside** the workspace, and smokes
526
+ `ba --help`, `ba gateway|runtime|commons --help`, and the missing-`agw` setup hint.
527
+
528
+ Version: the dry-run stamps `0.0.0-dryrun`; a real release sets `BA_PUBLISH_VERSION`.
529
+ **No real `npm publish` happens in the dry-run.**
530
+
531
+ ### Real publish
532
+
533
+ The real publish reuses the dry-run packer, so what ships is exactly what the gate
534
+ verified. It is **heavily guarded** — [`scripts/publish.mjs`](packages/core/scripts/publish.mjs)
535
+ refuses unless `BA_PUBLISH_VERSION` is a real semver (not `0.0.0`/`-dryrun`), only
536
+ publishes when `BA_PUBLISH_CONFIRM=1` (otherwise it runs the gate and prints a preview),
537
+ and needs an npm auth token. In CI it emits build provenance via OIDC.
538
+
539
+ Preferred path — the manual workflow [`.github/workflows/publish.yml`](.github/workflows/publish.yml)
540
+ (Actions → **publish** → Run workflow): it checks out the sibling family, builds commons
541
+ (dist-first), gates, then runs the guarded publish. It is a **dry preview** unless you
542
+ type `publish` in the confirm box. It uses the **org-wide** secrets `NPM_TOKEN` (publish
543
+ rights to `@bitspark-ai`) and `BITSPARK_CI_TOKEN` (read access to the sibling repos for
544
+ the family checkout — the default `GITHUB_TOKEN` is this-repo only), both already
545
+ provided on the Bitspark org, so there is nothing extra to configure.
546
+
547
+ Local equivalent (from a built family workspace, with an npm token configured):
548
+
549
+ ```sh
550
+ BA_PUBLISH_VERSION=0.1.0 pnpm publish:npm # preview (runs the gate, no publish)
551
+ BA_PUBLISH_VERSION=0.1.0 BA_PUBLISH_CONFIRM=1 pnpm publish:npm # actually publishes
552
+ ```
553
+
554
+ ---
555
+
556
+ ## Roadmap
557
+
558
+ - **P0 — runner foundation** *(done)*: `BaPlugin` contract, compile-time mount,
559
+ reference plugin, smoke tests, esbuild bundle.
560
+ - **P1 — runtime plugin** *(done)*: adapted `@bitspark-ai/bitagent-runtime-cli`
561
+ (a commander `program.command("runtime")…`) to export a `BaPlugin`; mounted as `runtime`.
562
+ - **Identity slice** *(done)*: `ba auth` over `@bitspark-ai/accounts-client`
563
+ (login → session-key delegation), a runner-owned, target-scoped session/credential
564
+ store, and `ba echo` proving login → service-use end to end.
565
+ - **Commons plugin** *(done)*: mounted `@bitspark-ai/ba-commons-plugin` as `commons`
566
+ (the first external service to adopt the frame) — roster/inbox/lease/message/broadcast/decision.
567
+ - **P2 — gateway developer plugin** *(done)*: mounted `@bitspark-ai/gateway-cli` as
568
+ `gateway` (developer command surface: status, target, projects, key, runs, usage, auth).
569
+ Consumer-safe per gateway #2771; the agw binary is resolved at runtime
570
+ (`GATEWAY_AGW_BIN` → optional co-installed `bitspark-agw` → `PATH`).
571
+ - **P3 — gateway operator plugin**: extract the operator surface + deploy/ops into
572
+ `@bitspark-ai/gateway-admin-cli`; mount as `gateway-admin`.
573
+ - **P4 — shared runner UX**: lift completion, richer help, and config from the
574
+ gateway CLI into the runner so every plugin inherits them. (Runner-owned
575
+ session/credentials already landed with the identity slice.)
576
+ - **P5/P6 — distribution** *(packaging done; first publish pending)*: a dry-run gate
577
+ (`pnpm publish:dryrun`) proves a clean, self-contained, zero-runtime-dep tarball that
578
+ installs + runs outside the monorepo, and a **guarded** real-publish path
579
+ (`pnpm publish:npm` / `.github/workflows/publish.yml`, OIDC provenance, npm-token +
580
+ explicit-confirm gated) is in place, wired to the org-wide `NPM_TOKEN` +
581
+ `BITSPARK_CI_TOKEN` secrets. See [Distribution](#distribution-publishing).
582
+ Pending: merge the publish workflow + run the first real publish; then retire the
583
+ standalone per-service bins.