@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
|
@@ -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
|
+
}
|
package/dist/src/ui.d.ts
ADDED
|
@@ -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;
|