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

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.
@@ -0,0 +1,271 @@
1
+ // BB-223: the pure CLI seam of `bakers ui` deep links.
2
+ //
3
+ // This module owns the *grammar* — argv in, an unresolved `UiInitialRoute` out —
4
+ // and the `--here` ladder. It performs no I/O of its own: git, the filesystem
5
+ // and the conductor registry all enter through injected seams, so the whole
6
+ // grammar is testable offline and a conflict/parse failure costs no network
7
+ // round-trip (spec Decisions 1, 3, 4, 6, 13).
8
+ //
9
+ // Resolving a route against the control plane is deliberately NOT here: the
10
+ // entry has no JSON:API client (spec Finding 1). That happens in the renderer,
11
+ // in `@bakers-build/addon-bakers-ui`'s `resolveRoute`, still before the TUI takes the
12
+ // terminal (spec Decision 5).
13
+ /** The known `--view` modes, in the order error messages list them. */
14
+ export const UI_VIEW_MODES = [
15
+ 'full',
16
+ 'teaser',
17
+ 'board_card',
18
+ ];
19
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20
+ /** `<project-key>-<number>`, case-insensitive (Decision 4). One dash only, so a
21
+ * UUID can never match it. */
22
+ const IDENTIFIER_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
23
+ /**
24
+ * The ticket identifier carried by a branch name, first match wins. Every
25
+ * convention places the identifier before the title slug and `capBranchName()`
26
+ * never truncates it (Finding 3), so the first match is the right one.
27
+ */
28
+ const BRANCH_IDENTIFIER_RE = /(?:^|[/_-])([A-Za-z][A-Za-z0-9]*-\d+)(?=$|[/_-])/;
29
+ /** An origin issue reference: `#19`, `bakers#19`, or an http(s) URL. Only shape
30
+ * is checked here; the server resolves the alias and the URL. */
31
+ const ORIGIN_RE = /^(?:[A-Za-z0-9._-]*#\d+|https?:\/\/\S+)$/;
32
+ const ACCEPTED_FORMS = "accepted forms: BB-221 (a ticket identifier), '#19' or 'bakers#19' or an " +
33
+ "issue URL (an origin issue), 'ticket:<id|uuid>', 'project:<name|uuid>', " +
34
+ "'run:<uuid>'";
35
+ /** True for an `8-4-4-4-12` hex UUID (case-insensitive). */
36
+ export function isUuid(value) {
37
+ return UUID_RE.test(value);
38
+ }
39
+ /** True for a `KEY-123` ticket identifier (case-insensitive). */
40
+ export function isTicketIdentifier(value) {
41
+ return IDENTIFIER_RE.test(value);
42
+ }
43
+ /**
44
+ * The ticket identifier a branch name carries, uppercased, or `null`.
45
+ * Pure and offline — the `--here` ticket rung never needs the network.
46
+ */
47
+ export function parseTicketIdentifierFromBranch(branch) {
48
+ const match = BRANCH_IDENTIFIER_RE.exec(branch);
49
+ return match?.[1] ? match[1].toUpperCase() : null;
50
+ }
51
+ /** Validate a raw `--view` value against the known modes. */
52
+ function parseViewMode(raw) {
53
+ if (raw === undefined)
54
+ return undefined;
55
+ if (UI_VIEW_MODES.includes(raw)) {
56
+ return raw;
57
+ }
58
+ throw new Error(`bakers ui: unknown --view mode '${raw}'. Known modes: ${UI_VIEW_MODES.join(', ')}.`);
59
+ }
60
+ /** A ticket target from a raw value: a UUID by id, else an identifier by name. */
61
+ function ticketTarget(value) {
62
+ if (isUuid(value))
63
+ return { kind: 'ticket', by: 'uuid', value };
64
+ if (isTicketIdentifier(value)) {
65
+ return { kind: 'ticket', by: 'identifier', value: value.toUpperCase() };
66
+ }
67
+ throw new Error(`bakers ui: '${value}' is not a ticket identifier or UUID — ${ACCEPTED_FORMS}.`);
68
+ }
69
+ /** A project target: a UUID by id, else the label verbatim (labels are
70
+ * case-sensitive Drupal entity labels — Decision 4). */
71
+ function projectTarget(value) {
72
+ return isUuid(value)
73
+ ? { kind: 'project', by: 'uuid', value }
74
+ : { kind: 'project', by: 'name', value };
75
+ }
76
+ /** A run target — UUIDs only; there is no human-readable run handle. */
77
+ function runTarget(value) {
78
+ if (!isUuid(value)) {
79
+ throw new Error(`bakers ui: a run target must be a UUID, got '${value}'.`);
80
+ }
81
+ return { kind: 'run', by: 'uuid', value };
82
+ }
83
+ /** Parse the positional form: an optional `kind:` prefix, else a bare identifier. */
84
+ function parsePositional(raw) {
85
+ // Before the `kind:` split: an issue URL carries a colon of its own.
86
+ if (ORIGIN_RE.test(raw))
87
+ return { kind: 'ticket', by: 'origin', value: raw };
88
+ const colon = raw.indexOf(':');
89
+ if (colon >= 0) {
90
+ const prefix = raw.slice(0, colon);
91
+ const value = raw.slice(colon + 1);
92
+ if (prefix !== 'ticket' && prefix !== 'project' && prefix !== 'run') {
93
+ throw new Error(`bakers ui: unknown target kind '${prefix}'. ` +
94
+ "Known kinds: 'ticket:', 'project:', 'run:'.");
95
+ }
96
+ if (value === '') {
97
+ throw new Error(`bakers ui: '${raw}' has an empty value — ${ACCEPTED_FORMS}.`);
98
+ }
99
+ if (prefix === 'ticket')
100
+ return ticketTarget(value);
101
+ if (prefix === 'project')
102
+ return projectTarget(value);
103
+ return runTarget(value);
104
+ }
105
+ // Bare: strictly a ticket identifier. A UUID is ambiguous by construction —
106
+ // it does not say which kind — and never guessed (Decision 1).
107
+ if (isUuid(raw)) {
108
+ throw new Error(`bakers ui: '${raw}' is ambiguous — a bare UUID does not say which kind. ` +
109
+ "Use 'ticket:<uuid>', 'project:<uuid>' or 'run:<uuid>'.");
110
+ }
111
+ return ticketTarget(raw);
112
+ }
113
+ /**
114
+ * The grammar: at most ONE target source (the positional, `--ticket`,
115
+ * `--project`, `--run`, `--here`) plus the orthogonal `--view` modifier.
116
+ *
117
+ * Returns `undefined` for no target at all (the dashboard, AC-4), `{kind:'here'}`
118
+ * when the ladder still has to be walked, else the finished route. Throws — with
119
+ * a message naming exactly what went wrong — on a conflict or an unparseable
120
+ * form; the caller turns that into a stderr line and exit 1 before any network
121
+ * work happens.
122
+ */
123
+ export function parseTarget(inputs) {
124
+ const supplied = [];
125
+ if (inputs.target !== undefined && inputs.target !== '') {
126
+ supplied.push(`the positional target '${inputs.target}'`);
127
+ }
128
+ if (inputs.ticket !== undefined)
129
+ supplied.push('--ticket');
130
+ if (inputs.project !== undefined)
131
+ supplied.push('--project');
132
+ if (inputs.run !== undefined)
133
+ supplied.push('--run');
134
+ if (inputs.here === true)
135
+ supplied.push('--here');
136
+ if (supplied.length > 1) {
137
+ throw new Error(`bakers ui: conflicting targets — ${supplied.join(', ')} were all supplied. ` +
138
+ 'Give at most one of: a positional target, --ticket, --project, --run, --here.');
139
+ }
140
+ const view = parseViewMode(inputs.view);
141
+ if (supplied.length === 0)
142
+ return undefined;
143
+ if (inputs.here === true) {
144
+ return { kind: 'here', view: view ?? 'full' };
145
+ }
146
+ const target = inputs.target !== undefined && inputs.target !== ''
147
+ ? parsePositional(inputs.target)
148
+ : inputs.ticket !== undefined
149
+ ? ticketTarget(inputs.ticket)
150
+ : inputs.project !== undefined
151
+ ? projectTarget(inputs.project)
152
+ : runTarget(inputs.run);
153
+ // `--view` describes how a TICKET renders. A project screen has no view mode,
154
+ // and `run:` already means "the ticket's Runs tab", so pairing it with a
155
+ // compact block is contradictory rather than resolvable (Decision 13).
156
+ if (view !== undefined && target.kind === 'project') {
157
+ throw new Error('bakers ui: --view applies to ticket targets only, but the target is a project. ' +
158
+ 'Drop --view, or name a ticket.');
159
+ }
160
+ if (view !== undefined && target.kind === 'run') {
161
+ throw new Error("bakers ui: --view cannot be combined with a 'run:' target — 'run:' already " +
162
+ "means the owning ticket's Runs tab. Use 'ticket:<id> --view <mode>' instead.");
163
+ }
164
+ return {
165
+ kind: 'route',
166
+ route: target.kind === 'project' ? target : { ...target, view: view ?? 'full' },
167
+ };
168
+ }
169
+ /**
170
+ * A git remote URL reduced to a comparable `host/path` identity: scheme,
171
+ * userinfo, port and a trailing `.git` (and trailing slashes) dropped, the whole
172
+ * thing lowercased. Returns `undefined` for anything that carries no host+path.
173
+ *
174
+ * This is REQUIRED, not cosmetic (spec Decision 3): git and the control plane
175
+ * disagree on form. Measured in this repo — `git remote get-url origin` yields
176
+ * `git@git.key-tec.de:bakers-build/bakers-build.git` while `bb_project.repository.url` holds
177
+ * `ssh://git@git.key-tec.de/bakers-build/bakers-build.git`. Both must reduce to
178
+ * `git.key-tec.de/bakers-build/bakers-build` or the `--here` project rung never matches.
179
+ *
180
+ * | raw | normalised |
181
+ * |-----------------------------------------|---------------------|
182
+ * | `git@host:bakers-build/bakers-build.git` | `host/bakers-build/bakers-build` |
183
+ * | `ssh://git@host/bakers-build/bakers-build.git` | `host/bakers-build/bakers-build` |
184
+ * | `ssh://git@host:2222/bakers-build/bakers-build.git` | `host/bakers-build/bakers-build` |
185
+ * | `https://host/bakers-build/bakers-build.git` | `host/bakers-build/bakers-build` |
186
+ */
187
+ export function normalizeRepoUrl(raw) {
188
+ const trimmed = (raw ?? '').trim();
189
+ if (trimmed === '')
190
+ return undefined;
191
+ let host;
192
+ let path;
193
+ // `scheme://[user@]host[:port]/path` — checked FIRST, so an `ssh://…:2222/…`
194
+ // port is never mistaken for the scp form's `host:path` colon.
195
+ const withScheme = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/(.+)$/.exec(trimmed);
196
+ if (withScheme?.[1] !== undefined) {
197
+ const rest = withScheme[1];
198
+ const slash = rest.indexOf('/');
199
+ if (slash < 0)
200
+ return undefined;
201
+ host = rest.slice(0, slash);
202
+ path = rest.slice(slash + 1);
203
+ }
204
+ else {
205
+ // The scp-like form `[user@]host:path` — what `git remote get-url` emits
206
+ // for an ssh clone. A bare local path has no colon and falls out here.
207
+ const scp = /^(?:[^/@]+@)?([^/:]+):(.+)$/.exec(trimmed);
208
+ if (scp?.[1] === undefined || scp[2] === undefined)
209
+ return undefined;
210
+ host = scp[1];
211
+ path = scp[2];
212
+ }
213
+ host = host
214
+ .replace(/^[^@]*@/, '')
215
+ .replace(/:\d+$/, '')
216
+ .toLowerCase();
217
+ path = path
218
+ .replace(/^\/+/, '')
219
+ .replace(/\/+$/, '')
220
+ .replace(/\.git$/i, '')
221
+ .replace(/\/+$/, '')
222
+ .toLowerCase();
223
+ if (host === '' || path === '')
224
+ return undefined;
225
+ return `${host}/${path}`;
226
+ }
227
+ /**
228
+ * `--here`: derive the target from the cwd (spec Decision 3).
229
+ *
230
+ * ```
231
+ * branch carries a ticket identifier? → that TICKET (feat/bb-223-… → BB-223)
232
+ * else: the cwd's git remote → the PROJECT(s) whose repository claims it
233
+ * else → a named error
234
+ * ```
235
+ *
236
+ * The BRANCH decides the first rung, not git mechanics: a worktree on a
237
+ * non-ticket branch and the main clone on a ticket branch both come out right.
238
+ *
239
+ * The project rung stops HERE at a normalised repo identity — matching it
240
+ * against `bb_project.repository` needs a JSON:API client, which the entry does
241
+ * not have (Finding 1), so `by: 'repo'` travels to the renderer's `resolveRoute`
242
+ * and the ambiguity ruling (one match / a picker / an error) is made there
243
+ * (Decisions 5 + 14).
244
+ */
245
+ export async function resolveHere(ctx) {
246
+ const branch = await ctx.readBranch(ctx.cwd).then((b) => b.trim(), () => '');
247
+ const identifier = branch === '' ? null : parseTicketIdentifierFromBranch(branch);
248
+ if (identifier !== null) {
249
+ return {
250
+ kind: 'ticket',
251
+ by: 'identifier',
252
+ value: identifier,
253
+ view: ctx.view ?? 'full',
254
+ fromBranch: branch,
255
+ };
256
+ }
257
+ const remote = await ctx.readRemoteUrl(ctx.cwd).then((r) => r.trim(), () => '');
258
+ const repo = normalizeRepoUrl(remote);
259
+ if (repo === undefined) {
260
+ const got = remote === '' ? '' : `, got '${remote}'`;
261
+ throw new Error(`bakers ui --here: could not derive a project from '${ctx.cwd}' — ` +
262
+ `no usable git remote (looked at 'origin'${got}).\n` +
263
+ " Run 'bakers ui' and press Tab for the project list, or 'bakers ui project:<name>' to open one directly.");
264
+ }
265
+ return {
266
+ kind: 'project',
267
+ by: 'repo',
268
+ value: repo,
269
+ ...(branch !== '' ? { fromBranch: branch } : {}),
270
+ };
271
+ }
@@ -0,0 +1,69 @@
1
+ import { type MachineContext } from '@bakers-build/core';
2
+ /**
3
+ * The resolved control-plane connection passed to the renderer launcher.
4
+ * A structural mirror of `@bakers-build/addon-bakers-ui`'s `HomeConnection` — kept
5
+ * local so this package needs NO compile-time dependency on the renderer (it
6
+ * resolves the renderer dynamically, ESLint-style, at runtime).
7
+ */
8
+ export interface HomeConnection {
9
+ baseUrl: string;
10
+ jsonapiPrefix?: string | undefined;
11
+ authPlugins: unknown[];
12
+ configPath?: string | undefined;
13
+ }
14
+ /**
15
+ * Resolve the agent-launch command builder from the home config. Turns a TUI
16
+ * prompt into the shell command herdr runs. Defaults to `claude` (an empty
17
+ * prompt yields the bare launch command). A machine context may name a
18
+ * different `agent_command` binary.
19
+ */
20
+ export declare function resolveAgentCommand(machine: Partial<MachineContext> & {
21
+ agent_command?: string;
22
+ }): (prompt: string) => string;
23
+ /** Where `bakers ui` roots the agents it launches, and which project they belong to. */
24
+ export interface ProjectRooting {
25
+ cwd: string;
26
+ /**
27
+ * The `.bakers` dir of the project we are standing in, when there is one.
28
+ *
29
+ * The ui hands this over rather than a resolved project name: matching it
30
+ * against a conductor's `workspace_root` needs the control plane, and the
31
+ * client that reaches it first exists in the renderer (BB-232). Undefined
32
+ * outside any project.
33
+ */
34
+ bakersDir?: string | undefined;
35
+ }
36
+ /**
37
+ * Decide the agents' working directory (BB-219 AC-9).
38
+ *
39
+ * The trigger is the **cwd**, deliberately not the winning connection config:
40
+ * "which connection did we resolve" and "which project am I standing in" are
41
+ * different questions, and binding them would let an explicit `--config`
42
+ * silently relocate the agents' working directory.
43
+ *
44
+ * Standing **in** a project (core's walk-up finds a `.bakers/` project dir that
45
+ * is not the user's own `~/.bakers`), the cwd is that repo's **root**. Otherwise
46
+ * the agents root at the home dir and the cockpit opens with no default group —
47
+ * an honest empty cockpit rather than a guess.
48
+ *
49
+ * The project name and this machine's conductor id used to come from a local
50
+ * registry file, which BB-232 deleted. Both now resolve in the renderer's
51
+ * `run()` — `Entity/conductor/Data/own-conductor.ts`, reached through the
52
+ * launcher's `bakersDir` — from the conductor record whose `workspace_root`
53
+ * equals this dir: the same data, from the store that also knows whether the
54
+ * conductor is alive, and matched by the same exact normalised path comparison
55
+ * the registry lookup used.
56
+ *
57
+ * The marker is core's `findProjectBbDir` — "is there a `.bakers/` here" —
58
+ * NOT the connection walk-up `findBbConfig`. Since BB-230 the latter skips
59
+ * an engine-only `conductor.config.js`, which BB-218 makes the canonical repo
60
+ * shape, so keying on it would leave the most common project rooted at home.
61
+ *
62
+ * `~/.bakers` is excluded for the same reason core labels it `home`: the walk-up
63
+ * reaches it from every directory under `$HOME`, so counting it would make
64
+ * every such directory look like "a project rooted at `$HOME`".
65
+ *
66
+ * Pure with respect to precedence — the walk-up is core's; this helper owns no
67
+ * filesystem probe of its own.
68
+ */
69
+ export declare function resolveProjectRooting(cwd: string, homeDir: string): ProjectRooting;
@@ -0,0 +1,67 @@
1
+ // BB-201 (was BB-194), BB-219: bootstrap builders for `bakers ui`. Pure
2
+ // functions that turn the machine context (identity), the conductor registry
3
+ // and the cwd into everything `runBbUi` needs. The CONNECTION itself is not
4
+ // built here — `ui.ts` asks core's `loadBbConfig` for it, so this package
5
+ // holds no connection-precedence rule of its own (AC-6).
6
+ import { dirname, resolve } from 'node:path';
7
+ import { findProjectBbDir, homeBbDir, shellQuote, } from '@bakers-build/core';
8
+ // BB-391: the two read/write auth-profile members are gone. A connection used
9
+ // to declare two dropsh auth profiles that differed only by OAuth scope, and the
10
+ // cockpit had to name one for reads and one for writes; BB-359 moved capability
11
+ // onto OG ranks, so both resolved to the same role. The connection now declares
12
+ // ONE default provider and nothing has to choose. AC2's grep over
13
+ // `bakers-cli/*/src` is the guard, so the retired names are not spelled here.
14
+ /**
15
+ * Resolve the agent-launch command builder from the home config. Turns a TUI
16
+ * prompt into the shell command herdr runs. Defaults to `claude` (an empty
17
+ * prompt yields the bare launch command). A machine context may name a
18
+ * different `agent_command` binary.
19
+ */
20
+ export function resolveAgentCommand(machine) {
21
+ const bin = (machine.agent_command ?? '').trim() || 'claude';
22
+ return (prompt) => prompt.trim() === '' ? bin : `${bin} ${shellQuote(prompt)}`;
23
+ }
24
+ /**
25
+ * Decide the agents' working directory (BB-219 AC-9).
26
+ *
27
+ * The trigger is the **cwd**, deliberately not the winning connection config:
28
+ * "which connection did we resolve" and "which project am I standing in" are
29
+ * different questions, and binding them would let an explicit `--config`
30
+ * silently relocate the agents' working directory.
31
+ *
32
+ * Standing **in** a project (core's walk-up finds a `.bakers/` project dir that
33
+ * is not the user's own `~/.bakers`), the cwd is that repo's **root**. Otherwise
34
+ * the agents root at the home dir and the cockpit opens with no default group —
35
+ * an honest empty cockpit rather than a guess.
36
+ *
37
+ * The project name and this machine's conductor id used to come from a local
38
+ * registry file, which BB-232 deleted. Both now resolve in the renderer's
39
+ * `run()` — `Entity/conductor/Data/own-conductor.ts`, reached through the
40
+ * launcher's `bakersDir` — from the conductor record whose `workspace_root`
41
+ * equals this dir: the same data, from the store that also knows whether the
42
+ * conductor is alive, and matched by the same exact normalised path comparison
43
+ * the registry lookup used.
44
+ *
45
+ * The marker is core's `findProjectBbDir` — "is there a `.bakers/` here" —
46
+ * NOT the connection walk-up `findBbConfig`. Since BB-230 the latter skips
47
+ * an engine-only `conductor.config.js`, which BB-218 makes the canonical repo
48
+ * shape, so keying on it would leave the most common project rooted at home.
49
+ *
50
+ * `~/.bakers` is excluded for the same reason core labels it `home`: the walk-up
51
+ * reaches it from every directory under `$HOME`, so counting it would make
52
+ * every such directory look like "a project rooted at `$HOME`".
53
+ *
54
+ * Pure with respect to precedence — the walk-up is core's; this helper owns no
55
+ * filesystem probe of its own.
56
+ */
57
+ export function resolveProjectRooting(cwd, homeDir) {
58
+ const bakersDir = findProjectBbDir(cwd);
59
+ // Core owns where `.bakers` lives; the ui never spells the path itself. No
60
+ // argument here: `homeDir` is the fallback cwd (the OS home), not the
61
+ // `.bakers` root, and an explicit argument would beat BB_HOME by design.
62
+ const homeDotBb = resolve(homeBbDir());
63
+ if (bakersDir !== undefined && resolve(bakersDir) !== homeDotBb) {
64
+ return { cwd: dirname(bakersDir), bakersDir };
65
+ }
66
+ return { cwd: homeDir };
67
+ }
@@ -0,0 +1,155 @@
1
+ import { type AgentLaunchHost } from '@bakers-build/addon-herdr';
2
+ import { type BbCommandHost, type BbCommandPlugin, type BbConnectionConfig, type MachineContext, type WorkspaceRecoveryOptions } from '@bakers-build/core';
3
+ import { type UiInitialRoute } from './route.js';
4
+ import { type HomeConnection } from './ui-home.js';
5
+ /** Prompt-level agent service the renderer consumes (mirror of bakers-ui's `TuiAgentService`). */
6
+ interface TuiAgentService {
7
+ launch(o: {
8
+ group: string;
9
+ cwd: string;
10
+ prompt: string;
11
+ env: Record<string, string | undefined>;
12
+ isNew?: boolean | undefined;
13
+ label?: string | undefined;
14
+ }): Promise<{
15
+ id: string;
16
+ group: string;
17
+ label: string;
18
+ }>;
19
+ list(group: string): Promise<{
20
+ id: string;
21
+ group: string;
22
+ label: string;
23
+ }[]>;
24
+ focus(id: string): Promise<void>;
25
+ hide(group: string): Promise<void>;
26
+ kill(id: string): Promise<void>;
27
+ }
28
+ /** A dropsh program (only the `parseAsync` the launcher drives). */
29
+ interface DropshProgram {
30
+ parseAsync(argv: string[], opts: {
31
+ from: 'user';
32
+ }): Promise<unknown>;
33
+ }
34
+ /** Options for bakers-ui's `runBbUi` (mirror of its `BbUiLauncherOptions`). */
35
+ export interface RunBbUiOptions {
36
+ connection: HomeConnection;
37
+ cwd: string;
38
+ project?: string | undefined;
39
+ agents?: TuiAgentService | undefined;
40
+ currentUser?: string | undefined;
41
+ /**
42
+ * The `.bakers` dir of the project the ui was started in, when there is one.
43
+ *
44
+ * Replaces the `ownConductorMachineId` the local registry used to supply
45
+ * (BB-232): the renderer matches this against each conductor's
46
+ * `workspace_root` to find which record is this machine's own — it holds the
47
+ * JSON:API client, and this entry does not.
48
+ */
49
+ bakersDir?: string | undefined;
50
+ /** BB-472 — the connection's ambient workspace uuid; the renderer resolves
51
+ * its name and shows it in the chrome. Absent ⇒ no workspace suffix. */
52
+ workspaceId?: string | undefined;
53
+ /** BB-472 — the loaded connection config's absolute path, so the
54
+ * workspace-picker screen can find `workspace_id`'s owning file. */
55
+ configPath?: string | undefined;
56
+ /** BB-472 — re-executes this process; the picker calls it after writing a
57
+ * new workspace selection. */
58
+ restartProcess?: (() => void) | undefined;
59
+ /** Persist a product-workspace name through the conductor-owned contract. */
60
+ selectWorkspace?: ((name: string) => Promise<void>) | undefined;
61
+ createPrompt?: string | undefined;
62
+ buildProgram: (opts: {
63
+ plugins: unknown[];
64
+ }) => DropshProgram;
65
+ renderer?: ((opts: unknown) => unknown) | undefined;
66
+ /** BB-223: the deep-link target, still UNRESOLVED (the entry has no JSON:API
67
+ * client — Finding 1). Absent ⇒ the renderer boots its dashboard (AC-4). */
68
+ initialRoute?: UiInitialRoute | undefined;
69
+ /** BB-233: the write-scoped JSON:API client. Absent ⇒ the renderer writes
70
+ * through the read client and the server decides (spec Decision 1). */
71
+ writeClient?: unknown;
72
+ /** BB-373: UI extension entry MODULES core discovered from `addons[]`.
73
+ * Specifiers only — this package imports none of them (spec F2). */
74
+ uiEntries?: readonly string[] | undefined;
75
+ }
76
+ /** What a write-client builder needs — everything a connection already carries. */
77
+ export interface BuildWriteClientOptions {
78
+ baseUrl: string;
79
+ jsonapiPrefix: string;
80
+ stateDir?: string;
81
+ plugins: BbConnectionConfig['plugins'];
82
+ }
83
+ /** Test/composition seams for `bakers ui`. */
84
+ export interface UiDeps {
85
+ /** Local operator identity; default: read the host context from disk. */
86
+ machine?: Partial<MachineContext>;
87
+ workspaceRecovery?: WorkspaceRecoveryOptions;
88
+ /** herdr-backed agent host; default: `herdrAgentHost(exec herdr)`. */
89
+ agentHost?: AgentLaunchHost;
90
+ /** The renderer launcher; default: the dynamically-resolved `runBbUi`. */
91
+ runUi?: (opts: RunBbUiOptions) => Promise<void>;
92
+ /** Host contract (resolveBases) for renderer resolution; default: cwd + this install. */
93
+ host?: BbCommandHost;
94
+ /** Explicit connection config (`--config`); beats project + home (AC-4). */
95
+ explicitConfig?: string;
96
+ /** Named connection selected by `--connection`. */
97
+ connection?: string;
98
+ /**
99
+ * `--workspace <name|uuid>` (BB-472): a ONE-SHOT override of the
100
+ * configured workspace for THIS process only — never written to the
101
+ * connection config. Resolved against the identity's reachable workspaces
102
+ * (`discoverBbWorkspaces`) before the ready connection loads, so an
103
+ * unknown or ambiguous name fails before the renderer starts.
104
+ */
105
+ workspace?: string;
106
+ /** Where the resolution + the rooting start; default: `process.cwd()`. */
107
+ cwd?: string;
108
+ /** Connection loader; default: core's `loadBbConfig` (the ONE rule). */
109
+ loadConnection?: (host: BbCommandHost, opts: {
110
+ cwd?: string;
111
+ explicit?: string | undefined;
112
+ connection?: string | undefined;
113
+ workspaceSelector?: string | undefined;
114
+ }) => Promise<BbConnectionConfig>;
115
+ /** Print the resolved connection and exit without launching the TUI (AC-7). */
116
+ printConfig?: boolean;
117
+ /** Sink for `--print-config`; default: `process.stdout.write`. */
118
+ stdout?: (s: string) => void;
119
+ /** Daily update lookup; injectable for startup ordering tests. */
120
+ updateCheck?: () => Promise<string | null>;
121
+ /** Positional `[target]` (`BB-221`, `#19`, `ticket:…`, `project:…`, `run:…`). */
122
+ target?: string | undefined;
123
+ /** `--ticket <id>`. */
124
+ ticket?: string | undefined;
125
+ /** `--project <id>`. */
126
+ project?: string | undefined;
127
+ /** `--run <uuid>`. */
128
+ run?: string | undefined;
129
+ /** `--here` — derive the target from the cwd (Decision 3). */
130
+ here?: boolean | undefined;
131
+ /** `--view <mode>` — how a ticket target renders (Decision 13). */
132
+ view?: string | undefined;
133
+ /** Current git branch of a dir; default `git branch --show-current`. */
134
+ readBranch?: (cwd: string) => Promise<string>;
135
+ /** The dir's `origin` remote URL; default `git remote get-url origin`. */
136
+ readRemoteUrl?: (cwd: string) => Promise<string>;
137
+ /** Where user-facing grammar errors go; default `process.stderr`. */
138
+ stderr?: (line: string) => void;
139
+ /** Builds the write client with the same configured identity as reads.
140
+ * Injected so the write seam is testable without a live token endpoint. */
141
+ buildWriteClient?: (o: BuildWriteClientOptions) => Promise<unknown>;
142
+ }
143
+ /**
144
+ * `bakers ui` — launch the interactive terminal UI. Resolves the connection
145
+ * through core's shared ready loader (explicit → project → global), roots the agents in the project it was started in, reads the
146
+ * machine context for identity + the agent binary, adapts a herdr-backed agent
147
+ * host to the renderer's prompt-level service, and delegates to
148
+ * `@bakers-build/addon-bakers-ui`'s `runBbUi`. `--print-config` reports the
149
+ * resolution and exits without launching. `$BB_UI_PLUGIN` overrides the
150
+ * renderer module (dev).
151
+ */
152
+ export declare function cmdUi(deps?: UiDeps): Promise<void>;
153
+ /** The `ui` command plugin the host mounts (`bakers ui`). */
154
+ declare const uiCommandPlugin: BbCommandPlugin;
155
+ export default uiCommandPlugin;