@bakers-build/ui 0.0.0-stage → 0.1.0-next.42

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/src/ui.js ADDED
@@ -0,0 +1,550 @@
1
+ import { createRequire } from 'node:module';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { herdrAgentHost, } from '@bakers-build/addon-herdr';
6
+ import { workspacePlugin } from '@bakers-build/addon-workspace';
7
+ import { CommandRunner, createBbConnectionClient, createBbConnectionContext, createLogger, exec, fetchUpdateNotice, loadReadyBbConnection, machineContextPath, readMachineContext, resolveCliVersion, setDefaultCommandRunner, } from '@bakers-build/core';
8
+ import { buildProgram as buildDropshProgram } from 'dropsh';
9
+ import { applyOpentuiLibc, reExecUnderBun, resolveBunBinary, shouldReExecUnderBun, } from './bun-runtime.js';
10
+ import { parseTarget, resolveHere, UI_VIEW_MODES, } from './route.js';
11
+ import { resolveAgentCommand, resolveProjectRooting, } from './ui-home.js';
12
+ /**
13
+ * Build the write-side JSON:API client from the plugins core already
14
+ * constructed, resolving the connection's DEFAULT auth provider.
15
+ *
16
+ * BB-391: no profile is named any more. This used to resolve a second,
17
+ * differently-SCOPED provider (`pm`) beside the reads' `session`, because at
18
+ * granularity `role` the requested scope alone decided which role a token
19
+ * carried. BB-359 moved capability onto OG ranks, so both scopes resolved to
20
+ * the same role and the split was ceremony.
21
+ *
22
+ * It is still a second CLIENT, and that is not an oversight (spec D6): the READ
23
+ * client lives inside dropsh — the TUI renders within the dropsh program, which
24
+ * resolves auth itself — and is unreachable from here. Two client objects, ONE
25
+ * identity.
26
+ *
27
+ * Throws when the connection declares no usable provider — the caller catches
28
+ * it and runs the cockpit read-only rather than aborting, because a connection
29
+ * with no resolvable provider still reads.
30
+ */
31
+ async function buildWriteClientDefault(o) {
32
+ return createBbConnectionClient({
33
+ site: { base_url: o.baseUrl, jsonapi_prefix: o.jsonapiPrefix },
34
+ plugins: o.plugins,
35
+ }, o.stateDir ? { stateDir: o.stateDir } : {});
36
+ }
37
+ /**
38
+ * The current branch of `cwd`. Git resolves this PER WORKTREE, so
39
+ * worktree-awareness for `--here` costs nothing (Decision 3). Never throws:
40
+ * outside a repo, or on a detached HEAD, it yields '' and the ladder falls
41
+ * through to its project rung.
42
+ */
43
+ async function readCurrentBranch(cwd) {
44
+ try {
45
+ const out = await exec('git', ['branch', '--show-current'], { cwd });
46
+ return typeof out === 'string' ? out.trim() : String(out ?? '').trim();
47
+ }
48
+ catch {
49
+ return '';
50
+ }
51
+ }
52
+ /**
53
+ * The cwd's `origin` remote URL, raw. Git resolves it per worktree, and a
54
+ * worktree shares its main clone's remote — exactly the right answer for
55
+ * "which repo am I standing in" (Decision 3). Never throws: outside a repo, or
56
+ * with no `origin`, it yields '' and the ladder produces its named error.
57
+ *
58
+ * Revision 4 replaced the previous rung (an `import()` of the repo's engine
59
+ * `conductor.config.js` plus a longest-prefix fallback over the then-still-alive
60
+ * local conductor registry) with this one read: neither could answer inside a
61
+ * worktree (both keyed on `.bakers` DIRS, and a worktree has its own), the config
62
+ * read needed a dynamic import of a possibly secret-bearing module, and the
63
+ * remote works in a fresh clone that has no `.bakers/` at all. Revision 5 then
64
+ * deleted that registry outright, which only removes an alternative this rung
65
+ * had already rejected.
66
+ */
67
+ async function readOriginRemoteUrl(cwd) {
68
+ try {
69
+ const out = await exec('git', ['remote', 'get-url', 'origin'], { cwd });
70
+ return typeof out === 'string' ? out.trim() : String(out ?? '').trim();
71
+ }
72
+ catch {
73
+ return '';
74
+ }
75
+ }
76
+ /**
77
+ * The explicit connection leg (AC-4, D2): `--config` → `$BB_CONFIG` →
78
+ * `$DROPSH_CONFIG`, first non-empty wins. The two env vars are read because
79
+ * core's resolver already names all three as the explicit leg and the renderer's
80
+ * launcher already honours a pre-set `$DROPSH_CONFIG` — so the path `bakers ui`
81
+ * reports and the file dropsh loads can never disagree.
82
+ */
83
+ function resolveExplicitConfig(flag) {
84
+ for (const candidate of [
85
+ flag,
86
+ process.env.BB_CONFIG,
87
+ process.env.DROPSH_CONFIG,
88
+ ]) {
89
+ if (candidate !== undefined && candidate.trim() !== '')
90
+ return candidate;
91
+ }
92
+ return undefined;
93
+ }
94
+ /**
95
+ * The `--print-config` block (AC-7): which file won, and what it points at.
96
+ *
97
+ * `project` reports the `--project` flag, and `bb_dir` the project dir the
98
+ * rooting resolved. BB-232 split the two on purpose: the DEFAULT project is no
99
+ * longer knowable here. It used to be read out of `~/.bakers/conductors.json`,
100
+ * which this ticket deleted, and its replacement is the conductor record whose
101
+ * `workspace_root` equals `bb_dir` — a control-plane read, and this command
102
+ * holds no JSON:API client (BB-223 spec Finding 1). Printing `bb_dir` is
103
+ * therefore the honest form: it names the exact input the renderer resolves the
104
+ * project from, instead of a `project:` line that could only ever say `(none)`.
105
+ */
106
+ function formatConnectionReport(connection, rooting, project) {
107
+ const yesNo = (b) => (b ? 'yes' : 'no');
108
+ const lines = [
109
+ ['connection', connection.config_path],
110
+ ['connection_id', connection.connection_id ?? 'default'],
111
+ ['selection', connection.selection_source],
112
+ ['source', connection.source],
113
+ ['fallback', yesNo(connection.fallback)],
114
+ ['legacy', yesNo(connection.legacy)],
115
+ ['base_url', connection.site.base_url],
116
+ ['cwd', rooting.cwd],
117
+ ['bb_dir', rooting.bakersDir ?? '(none)'],
118
+ [
119
+ 'project',
120
+ project ?? (rooting.bakersDir ? '(resolved from bb_dir)' : '(none)'),
121
+ ],
122
+ ];
123
+ return `${lines.map(([k, v]) => `${`${k}:`.padEnd(12)}${v}`).join('\n')}\n`;
124
+ }
125
+ function loggerFor(checkoutRoot) {
126
+ const logger = createLogger({ checkoutRoot });
127
+ setDefaultCommandRunner(new CommandRunner(logger));
128
+ return logger;
129
+ }
130
+ /**
131
+ * Resolve + dynamically import `@bakers-build/addon-bakers-ui` (the renderer launcher),
132
+ * ESLint-style over the host bases. This package never statically imports the
133
+ * renderer — it is a swappable plugin. Returns undefined (and logs) when the
134
+ * package is not installed.
135
+ */
136
+ async function loadBbUiModule(logger, bases) {
137
+ let resolved;
138
+ for (const base of bases) {
139
+ try {
140
+ resolved = createRequire(base).resolve('@bakers-build/addon-bakers-ui');
141
+ break;
142
+ }
143
+ catch {
144
+ // try the next base
145
+ }
146
+ }
147
+ if (resolved === undefined) {
148
+ logger.error({}, 'bakers ui: could not resolve @bakers-build/addon-bakers-ui. ' +
149
+ 'Install it, or set $BB_UI_PLUGIN to a renderer plugin path.');
150
+ return undefined;
151
+ }
152
+ return (await import(pathToFileURL(resolved).href));
153
+ }
154
+ /**
155
+ * `bakers ui` — launch the interactive terminal UI. Resolves the connection
156
+ * through core's shared ready loader (explicit → project → global), roots the agents in the project it was started in, reads the
157
+ * machine context for identity + the agent binary, adapts a herdr-backed agent
158
+ * host to the renderer's prompt-level service, and delegates to
159
+ * `@bakers-build/addon-bakers-ui`'s `runBbUi`. `--print-config` reports the
160
+ * resolution and exits without launching. `$BB_UI_PLUGIN` overrides the
161
+ * renderer module (dev).
162
+ */
163
+ export async function cmdUi(deps = {}) {
164
+ const logger = loggerFor(homedir());
165
+ const host = deps.host ?? {
166
+ resolveBases: [`${process.cwd()}/`, import.meta.url],
167
+ };
168
+ const cwd = deps.cwd ?? process.cwd();
169
+ // User-facing CLI errors go to stderr, not the log sink: `createLogger` writes
170
+ // to `$HOME/log.txt` whenever stdout is not a TTY, and `bakers ui BB-1 | cat`
171
+ // must still SAY why it failed. Spec Decision 5: "a stderr line and exit 1".
172
+ const fail = (message) => {
173
+ (deps.stderr ?? ((line) => process.stderr.write(line)))(`${message}\n`);
174
+ process.exitCode = 1;
175
+ };
176
+ // BB-326 (A1) — teach OpenTUI which libc this host runs BEFORE anything
177
+ // else. It is set on THIS process rather than only on the re-exec's `env`
178
+ // argument, and that placement is the whole point: `reExecUnderBun` passes
179
+ // `env: process.env`, so the Bun child inherits it for free, while the three
180
+ // paths that never re-exec (already under Bun, `$BB_UI_NO_BUN` on Node 26 +
181
+ // --experimental-ffi, an injected `runUi`) load `@opentui/core` in THIS
182
+ // process and would otherwise get the glibc default on Alpine. `@opentui/core`
183
+ // reads the variable when it resolves its native asset — strictly later than
184
+ // here, since the renderer is a dynamic import — so one assignment covers
185
+ // every path. On glibc it is a no-op.
186
+ applyOpentuiLibc(process.env);
187
+ // BB-326 — the renderer needs Bun's FFI. Re-exec here rather than in a bin
188
+ // shim: there are three shims, the host already owns the value-aware argv
189
+ // pre-scan that says `ui` was invoked, and `--print-config` must keep working
190
+ // without Bun. This sits after `fail` is declared (the only ordering the
191
+ // named-error path allows) and still before every config load and the whole
192
+ // deep-link ladder, so nothing is resolved twice across the two processes.
193
+ if (shouldReExecUnderBun({
194
+ bunVersion: process.versions.bun,
195
+ noBun: process.env.BB_UI_NO_BUN,
196
+ printConfig: Boolean(deps.printConfig),
197
+ injectedRenderer: deps.runUi !== undefined,
198
+ })) {
199
+ const bin = resolveBunBinary(host.resolveBases);
200
+ if (bin === undefined) {
201
+ fail('bakers ui: could not resolve the bun runtime. `@bakers-build/addon-bakers-ui` ' +
202
+ 'depends on it; reinstall, or set $BB_UI_NO_BUN=1 to run on Node ' +
203
+ '26+ with --experimental-ffi.');
204
+ return;
205
+ }
206
+ reExecUnderBun(bin, process.argv);
207
+ }
208
+ // BB-223: build the deep-link route FIRST — argv parsing, the conflict rule
209
+ // and the whole `--here` ladder are offline, so every one of their failures
210
+ // costs no config load and no network round-trip, and never reaches the
211
+ // launcher (AC-1/AC-2).
212
+ let initialRoute;
213
+ try {
214
+ const parsed = parseTarget({
215
+ target: deps.target,
216
+ ticket: deps.ticket,
217
+ project: deps.project,
218
+ run: deps.run,
219
+ here: deps.here,
220
+ view: deps.view,
221
+ });
222
+ if (parsed?.kind === 'route') {
223
+ initialRoute = parsed.route;
224
+ }
225
+ else if (parsed?.kind === 'here') {
226
+ initialRoute = await resolveHere({
227
+ cwd,
228
+ ...(parsed.view !== undefined ? { view: parsed.view } : {}),
229
+ readBranch: deps.readBranch ?? readCurrentBranch,
230
+ readRemoteUrl: deps.readRemoteUrl ?? readOriginRemoteUrl,
231
+ });
232
+ }
233
+ }
234
+ catch (err) {
235
+ fail(err instanceof Error ? err.message : String(err));
236
+ return;
237
+ }
238
+ const mcPath = machineContextPath();
239
+ // The machine context still supplies identity (`user_id`) + the agent binary;
240
+ // it is NOT the connection any more — core resolves that.
241
+ const machine = deps.machine ?? (await readMachineContext(mcPath).catch(() => ({})));
242
+ const rooting = resolveProjectRooting(cwd, homedir());
243
+ const conductorPath = rooting.bakersDir
244
+ ? (host.projectContext?.resolveConfigPath(cwd) ??
245
+ join(rooting.bakersDir, 'conductor.config.js'))
246
+ : undefined;
247
+ const workspaceSelector = deps.workspace ??
248
+ (conductorPath
249
+ ? host.projectContext?.readWorkspace(conductorPath)
250
+ : undefined);
251
+ // Core resolves the selected connection; the host delegates project context
252
+ // to conductor so this command package stays independent of both packages.
253
+ const load = deps.loadConnection ?? loadReadyBbConnection;
254
+ let resolved;
255
+ try {
256
+ const projectContext = host.projectContext;
257
+ const resolveProject = Boolean(conductorPath &&
258
+ projectContext?.resolveContext &&
259
+ !deps.workspace &&
260
+ !deps.printConfig);
261
+ resolved = await load(host, {
262
+ ...deps.workspaceRecovery,
263
+ cwd,
264
+ explicit: resolveExplicitConfig(deps.explicitConfig),
265
+ ...(deps.connection ? { connection: deps.connection } : {}),
266
+ ...(resolveProject
267
+ ? { workspaceIdOverride: '' }
268
+ : workspaceSelector
269
+ ? { workspaceSelector }
270
+ : {}),
271
+ });
272
+ if (resolveProject && conductorPath && projectContext?.resolveContext) {
273
+ const context = await projectContext.resolveContext(conductorPath, resolved);
274
+ resolved = await load(host, {
275
+ ...deps.workspaceRecovery,
276
+ cwd,
277
+ explicit: resolveExplicitConfig(deps.explicitConfig),
278
+ ...(deps.connection ? { connection: deps.connection } : {}),
279
+ workspaceIdOverride: '',
280
+ workspaceSelector: context.workspaceId,
281
+ });
282
+ }
283
+ // BB-495 D1: the dropsh request path no longer carries an ambient
284
+ // workspace — the caller names it on the call. `bakers ui` scopes its own
285
+ // reads because a focused workspace is what makes its lists readable, so
286
+ // it attaches the plugin itself, right here, with the workspace it
287
+ // already resolved.
288
+ if (resolved.workspace_id &&
289
+ !resolved.plugins.some((plugin) => plugin.id === 'bakers-workspace')) {
290
+ resolved = {
291
+ ...resolved,
292
+ plugins: [
293
+ ...resolved.plugins,
294
+ workspacePlugin({ workspace_id: resolved.workspace_id }),
295
+ ],
296
+ };
297
+ }
298
+ }
299
+ catch (err) {
300
+ // `fail`, not `logger.error`: a connection config that will not resolve is
301
+ // the most user-facing error this command has, and the logger writes to
302
+ // `$HOME/log.txt` whenever stdout is not a TTY. Sent to the log sink it
303
+ // came out as `exit 1` and NOT ONE LINE of output — which is exactly how
304
+ // CI reported it, with the actual cause (a missing `site.base_url`)
305
+ // sitting in a file nobody on that runner would think to read. The log
306
+ // keeps its copy; stderr gets the one a human reads.
307
+ const message = err instanceof Error ? err.message : String(err);
308
+ logger.error({}, message);
309
+ fail(`bakers ui: ${message}`);
310
+ return;
311
+ }
312
+ const notice = deps.updateCheck
313
+ ? await deps.updateCheck()
314
+ : deps.runUi
315
+ ? null
316
+ : await fetchUpdateNotice(resolveCliVersion());
317
+ if (notice !== null) {
318
+ (deps.stdout ?? ((line) => process.stdout.write(line)))(`${notice}\n`);
319
+ }
320
+ const connection = {
321
+ baseUrl: resolved.site.base_url,
322
+ jsonapiPrefix: resolved.site.jsonapi_prefix,
323
+ authPlugins: resolved.plugins,
324
+ configPath: resolved.config_path,
325
+ };
326
+ // BB-233: the write client, built here because this package already holds
327
+ // the constructed plugins and imports dropsh — the renderer stays
328
+ // dropsh-decoupled and receives a finished object.
329
+ //
330
+ // BB-391 emptied this fallback of the PROFILE concept but kept the fallback
331
+ // itself, and the difference is the point. It used to point the write profile
332
+ // at the read one, because a connection declaring the reads' provider and not
333
+ // the writes' was an ordinary configuration; that rename is gone along with
334
+ // both fields, and there is no profile left to name.
335
+ //
336
+ // What remains is why it was a `catch` and not a `fail`: a connection with NO
337
+ // resolvable auth provider still READS. `resolveAuth` throws
338
+ // `Multiple auth profiles configured (); none active` for a plugins[] that
339
+ // yields none, and a `@bakers-build/addon-auth-basic` connection reaches this line
340
+ // the same way. Aborting there would take the whole cockpit down over a write
341
+ // seam it may never use. So no write client is injected, the renderer falls
342
+ // back to the read client, and the server's own 403 is the operator's answer —
343
+ // Decision 1's single failure path, minus the profile it used to name.
344
+ let writeClient;
345
+ try {
346
+ writeClient = await (deps.buildWriteClient ?? buildWriteClientDefault)({
347
+ baseUrl: resolved.site.base_url,
348
+ jsonapiPrefix: resolved.site.jsonapi_prefix ?? '/jsonapi',
349
+ plugins: resolved.plugins,
350
+ ...(deps.workspaceRecovery?.stateDir
351
+ ? { stateDir: deps.workspaceRecovery.stateDir }
352
+ : {}),
353
+ });
354
+ }
355
+ catch (err) {
356
+ logger.warn({}, 'bakers ui: no write client could be built ' +
357
+ `(${err instanceof Error ? err.message : String(err)}); ` +
358
+ 'edits will be attempted through the read client.');
359
+ }
360
+ const project = deps.project;
361
+ // AC-7, always on: the resolution lands in the log even when the TUI takes
362
+ // over the screen a moment later.
363
+ logger.info({
364
+ config_path: resolved.config_path,
365
+ source: resolved.source,
366
+ fallback: resolved.fallback,
367
+ legacy: resolved.legacy,
368
+ base_url: resolved.site.base_url,
369
+ cwd: rooting.cwd,
370
+ // The `.bakers` dir, not a project name: since BB-232 the default project
371
+ // is resolved in the renderer from the conductor record whose
372
+ // `workspace_root` is this dir. `project` here is the flag alone.
373
+ bb_dir: rooting.bakersDir,
374
+ project,
375
+ }, 'bakers ui: resolved connection');
376
+ // AC-7, the human surface: print and exit 0 WITHOUT booting the TUI, so a
377
+ // wrong-control-plane situation is diagnosable at a prompt.
378
+ if (deps.printConfig) {
379
+ const write = deps.stdout ?? ((s) => process.stdout.write(s));
380
+ write(formatConnectionReport(resolved, rooting, project));
381
+ return;
382
+ }
383
+ const agentCommand = resolveAgentCommand(machine);
384
+ const agentHost = deps.agentHost ?? herdrAgentHost((args) => exec('herdr', args));
385
+ const agents = {
386
+ launch: (o) => agentHost.launch({
387
+ group: o.group,
388
+ cwd: o.cwd,
389
+ command: agentCommand(o.prompt),
390
+ env: o.env,
391
+ ...(o.label !== undefined ? { label: o.label } : {}),
392
+ }),
393
+ list: (group) => agentHost.list(group),
394
+ focus: (id) => agentHost.focus(id),
395
+ hide: (group) => agentHost.hide(group),
396
+ kill: (id) => agentHost.kill(id),
397
+ };
398
+ let renderer;
399
+ const override = process.env.BB_UI_PLUGIN;
400
+ if (override) {
401
+ try {
402
+ const mod = (await import(pathToFileURL(override).href));
403
+ const factory = mod.default ?? mod.ticketRendererPlugin;
404
+ if (typeof factory !== 'function') {
405
+ throw new Error('module exports no renderer-plugin factory');
406
+ }
407
+ renderer = factory;
408
+ }
409
+ catch (err) {
410
+ logger.error({ override }, `bakers ui: could not load $BB_UI_PLUGIN at ${override} ` +
411
+ `(${err instanceof Error ? err.message : String(err)}).`);
412
+ process.exitCode = 1;
413
+ return;
414
+ }
415
+ }
416
+ let run = deps.runUi;
417
+ if (!run) {
418
+ const uiMod = await loadBbUiModule(logger, host.resolveBases);
419
+ if (uiMod === undefined) {
420
+ process.exitCode = 1;
421
+ return;
422
+ }
423
+ run = uiMod.runBbUi;
424
+ }
425
+ // The launcher (and, through it, the renderer's route resolution — Decision 5)
426
+ // may reject BEFORE the TUI takes the terminal; `await run(...)` was unguarded,
427
+ // so that surfaced as an unhandled rejection. Catch it into exit 1 + a line.
428
+ try {
429
+ await run({
430
+ connection,
431
+ cwd: rooting.cwd,
432
+ ...(project !== undefined ? { project } : {}),
433
+ agents,
434
+ ...(machine.user_id ? { currentUser: machine.user_id } : {}),
435
+ ...(rooting.bakersDir ? { bakersDir: rooting.bakersDir } : {}),
436
+ ...(resolved.workspace_id ? { workspaceId: resolved.workspace_id } : {}),
437
+ configPath: resolved.config_path,
438
+ // BB-472: the workspace-picker screen's restart seam — the same
439
+ // reExecUnderBun step this process took to reach OpenTUI at all
440
+ // (`shouldReExecUnderBun`, above). Absent only when the bun binary could
441
+ // not be resolved, which already failed startup before this point.
442
+ restartProcess: () => {
443
+ const bin = resolveBunBinary(host.resolveBases);
444
+ if (bin)
445
+ reExecUnderBun(bin, process.argv);
446
+ },
447
+ ...(conductorPath && host.projectContext
448
+ ? {
449
+ selectWorkspace: async (name) => {
450
+ await host.projectContext?.writeWorkspace(conductorPath, name);
451
+ },
452
+ }
453
+ : {}),
454
+ // The plugins are real constructed `DropSHPlugin[]` now (core built them),
455
+ // matching dropsh's declared contract — F3's substance, a descriptor handed
456
+ // to an API expecting instances, is gone. The two casts that remain are
457
+ // structural, not unsound: `RunBbUiOptions` is a local MIRROR of the
458
+ // dynamically-resolved renderer's type (this package must not compile
459
+ // against it), so its `plugins` is `unknown[]` and its program type is the
460
+ // narrow `DropshProgram` above. Both casts only re-tell TypeScript what the
461
+ // mirror deliberately forgot.
462
+ // Bind command context to the freshly loaded connection, including its
463
+ // request hooks. The renderer cannot reload an earlier environment config.
464
+ buildProgram: (o) => buildDropshProgram({
465
+ plugins: o.plugins,
466
+ contextFactory: () => createBbConnectionContext({
467
+ ...resolved,
468
+ plugins: o.plugins,
469
+ }, { ...deps.workspaceRecovery, cwd }),
470
+ }),
471
+ ...(renderer ? { renderer } : {}),
472
+ ...(initialRoute ? { initialRoute } : {}),
473
+ ...(writeClient !== undefined ? { writeClient } : {}),
474
+ // BB-373 — the UI contributions core discovered from `addons[]`, still
475
+ // as module specifiers. Omitted when there are none, so a config without
476
+ // `addons[]` hands the renderer exactly what it got before.
477
+ //
478
+ // `?? []` because `loadConnection` is an INJECTED seam (`deps`), so the
479
+ // resolved object is only as complete as whoever supplied the loader:
480
+ // core's own always carries this key, a caller's substitute need not.
481
+ ...(resolved.ui_entries?.length
482
+ ? { uiEntries: resolved.ui_entries }
483
+ : {}),
484
+ });
485
+ }
486
+ catch (err) {
487
+ fail(err instanceof Error ? err.message : String(err));
488
+ }
489
+ }
490
+ /** The `ui` command plugin the host mounts (`bakers ui`). */
491
+ const uiCommandPlugin = {
492
+ kind: 'command',
493
+ name: 'ui',
494
+ describe: 'interactive terminal UI: dashboard, projects, tickets',
495
+ register(program, host) {
496
+ program
497
+ .command('ui')
498
+ .description('interactive terminal UI: dashboard, projects, tickets')
499
+ .argument('[target]', 'deep-link target: BB-221 · #19 · bakers#19 · <issue url> · ticket:<id|uuid> · project:<name|uuid> · run:<uuid>')
500
+ .option('--ticket <id>', 'open a ticket by identifier or uuid')
501
+ .option('--project <id>', 'open a project by name or uuid')
502
+ .option('--run <uuid>', "open a run's owning ticket, on its Runs tab")
503
+ .option('--here', "derive the target from the cwd: the branch's ticket, else this repo's project")
504
+ .option('--view <mode>', `how a ticket renders: ${UI_VIEW_MODES.join(' | ')}`)
505
+ .option('--config <path>', 'connection config path; wins over the project and home configs')
506
+ .option('--connection <id>', 'named connection from ~/.bakers/connections')
507
+ .option('--workspace <name|uuid>', 'select a workspace for THIS run only — never written to the connection config')
508
+ .option('--yes', 'never prompt for workspace selection')
509
+ .option('--print-config', 'print the resolved connection config and exit, without launching the UI')
510
+ .addHelpText('after', `
511
+ Targets (at most ONE of the positional, --ticket, --project, --run, --here):
512
+ bakers ui the dashboard
513
+ bakers ui BB-221 a ticket by identifier (case-insensitive)
514
+ bakers ui ticket:BB-221 the same, said explicitly
515
+ bakers ui ticket:<uuid> a ticket by uuid
516
+ bakers ui '#19' a ticket by its origin issue (quote the #);
517
+ bakers ui bakers#19 also <repo>#<n> or the issue URL
518
+ bakers ui project:bakers a project by name (case-sensitive label)
519
+ bakers ui run:<uuid> that run's owning ticket, Runs tab active
520
+ bakers ui --ticket BB-221 the script-friendly flag form
521
+ bakers ui --here this branch's ticket (feat/bb-223-… → BB-223),
522
+ else this repo's project
523
+
524
+ Modifier (orthogonal — never conflicts with a target):
525
+ bakers ui BB-221 --view teaser a compact block instead of the tabbed detail
526
+ bakers ui BB-221 --view board_card the board-card field set
527
+
528
+ Two target sources at once exit non-zero naming both. A bare uuid is ambiguous
529
+ (prefix it); a bare number is not an identifier. --view applies to ticket
530
+ targets only. Full reference: bakers-cli/ui/README.md
531
+ `)
532
+ .action(async (target, o) => {
533
+ await cmdUi({
534
+ host,
535
+ ...(target !== undefined ? { target } : {}),
536
+ ...(o.ticket !== undefined ? { ticket: o.ticket } : {}),
537
+ ...(o.project !== undefined ? { project: o.project } : {}),
538
+ ...(o.run !== undefined ? { run: o.run } : {}),
539
+ ...(o.here !== undefined ? { here: o.here } : {}),
540
+ ...(o.view !== undefined ? { view: o.view } : {}),
541
+ ...(o.config !== undefined ? { explicitConfig: o.config } : {}),
542
+ ...(o.connection !== undefined ? { connection: o.connection } : {}),
543
+ ...(o.workspace !== undefined ? { workspace: o.workspace } : {}),
544
+ ...(o.yes ? { workspaceRecovery: { yes: true } } : {}),
545
+ ...(o.printConfig ? { printConfig: true } : {}),
546
+ });
547
+ });
548
+ },
549
+ };
550
+ export default uiCommandPlugin;
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@bakers-build/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-next.42",
4
+ "description": "bakers.build project-first cockpit: the `bakers ui` command plugin (renderer resolved dynamically).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=20.19"
9
+ },
10
+ "exports": {
11
+ ".": "./dist/src/index.js",
12
+ "./preset": "./dist/src/preset.js",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist/src",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://git.key-tec.de/bakers-build/bakers-build.git",
26
+ "directory": "bakers-cli/ui"
27
+ },
28
+ "dependencies": {
29
+ "@bakers-build/core": "^0.1.0-next.42",
30
+ "@bakers-build/addon-herdr": "^0.1.0-next.42",
31
+ "@bakers-build/addon-workspace": "^0.1.0-next.42",
32
+ "commander": "^12.1.0",
33
+ "dropsh": "^0.7.5"
34
+ }
35
+ }