@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/LICENSE +21 -0
- package/README.md +4 -2
- package/dist/src/bun-runtime.d.ts +84 -0
- package/dist/src/bun-runtime.js +149 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +10 -0
- package/dist/src/preset.d.ts +2 -0
- package/dist/src/preset.js +8 -0
- package/dist/src/route.d.ts +119 -0
- package/dist/src/route.js +271 -0
- package/dist/src/ui-home.d.ts +69 -0
- package/dist/src/ui-home.js +67 -0
- package/dist/src/ui.d.ts +155 -0
- package/dist/src/ui.js +550 -0
- package/package.json +33 -4
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|