@bakers-build/conductor 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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/dist/src/bakers-project-context.d.ts +12 -0
  4. package/dist/src/bakers-project-context.js +59 -0
  5. package/dist/src/cli/config-plan.d.ts +35 -0
  6. package/dist/src/cli/config-plan.js +1264 -0
  7. package/dist/src/cli/config-schema.d.ts +137 -0
  8. package/dist/src/cli/config-schema.js +239 -0
  9. package/dist/src/cli/config-source-edit.d.ts +17 -0
  10. package/dist/src/cli/config-source-edit.js +95 -0
  11. package/dist/src/cli/config-templates.d.ts +43 -0
  12. package/dist/src/cli/config-templates.js +130 -0
  13. package/dist/src/cli/config-validation.d.ts +12 -0
  14. package/dist/src/cli/config-validation.js +353 -0
  15. package/dist/src/cli/migrate-addon-names.d.ts +82 -0
  16. package/dist/src/cli/migrate-addon-names.js +334 -0
  17. package/dist/src/cli/project-context.d.ts +21 -0
  18. package/dist/src/cli/project-context.js +105 -0
  19. package/dist/src/commands/conductor.d.ts +91 -0
  20. package/dist/src/commands/conductor.js +843 -0
  21. package/dist/src/config.d.ts +109 -0
  22. package/dist/src/config.js +712 -0
  23. package/dist/src/contract.d.ts +13 -0
  24. package/dist/src/contract.js +19 -0
  25. package/dist/src/core/action-queue.d.ts +31 -0
  26. package/dist/src/core/action-queue.js +69 -0
  27. package/dist/src/core/attempt.d.ts +25 -0
  28. package/dist/src/core/attempt.js +41 -0
  29. package/dist/src/core/conductor.d.ts +236 -0
  30. package/dist/src/core/conductor.js +1293 -0
  31. package/dist/src/core/host-key.d.ts +19 -0
  32. package/dist/src/core/host-key.js +27 -0
  33. package/dist/src/core/inbox-runtime.d.ts +48 -0
  34. package/dist/src/core/inbox-runtime.js +180 -0
  35. package/dist/src/core/mr-changes.d.ts +18 -0
  36. package/dist/src/core/mr-changes.js +47 -0
  37. package/dist/src/index.d.ts +8 -0
  38. package/dist/src/index.js +27 -0
  39. package/dist/src/plugins/agent-args.d.ts +43 -0
  40. package/dist/src/plugins/agent-args.js +221 -0
  41. package/dist/src/plugins/agent.d.ts +121 -0
  42. package/dist/src/plugins/agent.js +12 -0
  43. package/dist/src/plugins/executor.d.ts +182 -0
  44. package/dist/src/plugins/executor.js +1 -0
  45. package/dist/src/plugins/message.d.ts +61 -0
  46. package/dist/src/plugins/message.js +47 -0
  47. package/dist/src/plugins/plugins.d.ts +96 -0
  48. package/dist/src/plugins/plugins.js +120 -0
  49. package/dist/src/plugins/preset.d.ts +61 -0
  50. package/dist/src/plugins/preset.js +23 -0
  51. package/dist/src/plugins/process-host.d.ts +7 -0
  52. package/dist/src/plugins/process-host.js +1 -0
  53. package/dist/src/plugins/remote.d.ts +420 -0
  54. package/dist/src/plugins/remote.js +1 -0
  55. package/dist/src/plugins/workspace.d.ts +69 -0
  56. package/dist/src/plugins/workspace.js +1 -0
  57. package/dist/src/preset.d.ts +2 -0
  58. package/dist/src/preset.js +8 -0
  59. package/dist/src/types.d.ts +132 -0
  60. package/dist/src/types.js +6 -0
  61. package/package.json +38 -4
@@ -0,0 +1,334 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { inspectConfigSource } from '@bakers-build/core';
4
+ import { listConductorConfigFiles } from '../config.js';
5
+ import { ConfigSourceEditor } from './config-source-edit.js';
6
+ // BB-224 (Finding 8b, spec v3 decision 17): the `bakers config upgrade` RENAME pass.
7
+ //
8
+ // The addon packages were renamed `@bakers-build/plugin-<name>` → `@bakers-build/addon-<name>`
9
+ // (decision 14) and the core barrels `@bakers-build/core/{builtins,plugins}` were
10
+ // deleted (Finding 6). Both are npm-breaking for an existing install, whose
11
+ // `.bakers/` configs still name the old packages — so `bakers config upgrade` rewrites them
12
+ // in the user's PROJECT `./.bakers/` and HOME `~/.bakers/` configs.
13
+ //
14
+ // All THREE deleted/renamed specifiers are covered, and — BB-230 — so is the
15
+ // FOURTH: the `@bakers-build/bakers/plugins` host barrel.
16
+ //
17
+ // BB-224 excluded that barrel on the premise that it "still exists as a
18
+ // back-compat aggregator", so a config naming it keeps resolving. That premise is
19
+ // INSTALL-DEPENDENT and false for a dev-clone install: `@bakers-build/bakers` is the
20
+ // workspace's leaf SINK, so nothing depends on it and it is absent from
21
+ // `bakers-cli/node_modules/@bakers-build/`; it is also the only package exporting
22
+ // `./plugins` (`@bakers-build/core`'s export map has no such subpath). The `bakers` shim
23
+ // prefers a checkout's own build over the global install by design, so exactly
24
+ // there the barrel is unresolvable and that one config line dies while its
25
+ // sibling `addon-*` slots resolve fine.
26
+ //
27
+ // The barrel itself keeps existing — this is not repairing a dead reference, it is
28
+ // making configs stop NAMING a barrel the project docs already tell them not to
29
+ // name. Both barrels are therefore migrated the same closed-set way, over the same
30
+ // `CORE_PLUGINS_BARREL_EXPORTS` table. Two deliberate consequences:
31
+ // - the table is a SUPERSET of what `@bakers-build/bakers/plugins` actually re-exports
32
+ // (`host/src/plugins-barrel.ts` omits the fakes on purpose — they are
33
+ // workspace-only and must not be a published package's dependency). Mapping
34
+ // them anyway is right: such a descriptor is broken today, and retargeting it
35
+ // at `@bakers-build/addon-fake` fixes it.
36
+ // - `loadInstructions` IS a barrel export but is not a plugin factory and is
37
+ // absent from the table, so it stays untouched — as does any other unmapped
38
+ // `export:`.
39
+ //
40
+ // Covered per config dir:
41
+ // - the connection config `bakers.config.js` (its `plugins[]` / `addons[]`);
42
+ // - EVERY engine config the loader itself recognises — the default
43
+ // `conductor.config.js` AND each `<variant>.conductor.config.js`
44
+ // (BB-137 naming convention, enumerated via `listConductorConfigFiles` so
45
+ // the pass and the loader can never disagree). A legacy `conductor.config.js`
46
+ // still carrying `site`/`plugins` (the back-compat connection source) is
47
+ // therefore covered too, and its `plugins[]` entries are mapped on the
48
+ // CONNECTION surface — see `builtinsTargetFor`.
49
+ //
50
+ // Safety (decision 18 + the v3 risk register):
51
+ // - Replacement is anchored on a CLOSED set of 14 known package names inside
52
+ // a matched quote pair — never a broad `@bakers-build/plugin-.*` sweep. A
53
+ // hand-written config's comments, formatting and unrelated code survive.
54
+ // The `@bakers-build/core/plugins` rewrite is likewise closed-set: it is driven by
55
+ // the barrel's OWN export list, and an export name outside that list is left
56
+ // untouched rather than retargeted at a guess.
57
+ // - `*Plugin` interface identifiers (`RemotePlugin`, `herdrExecutorPlugin`, …)
58
+ // and the `plugins:`/`addons:` config KEYS are excluded BY CONSTRUCTION: a
59
+ // replacement only ever fires on a full quoted specifier, and none of those
60
+ // is one.
61
+ // - Idempotent: a config already on `addon-*` produces zero matches and is
62
+ // left BYTE-unchanged (no rewrite, no backup, no report line).
63
+ // - `--dry-run` prints the line diff and writes nothing.
64
+ // - A real rewrite backs the original up next to the file, like the
65
+ // schema-migration path does.
66
+ /** Suffix of the pre-rewrite backup a real (non-dry) rewrite leaves behind. */
67
+ export const ADDON_RENAME_BAK_SUFFIX = '.pre-addon-rename.bak';
68
+ const AUTH_BASIC = '@bakers-build/addon-auth-basic';
69
+ const REMOTE_DRUPAL = '@bakers-build/addon-remote-drupal';
70
+ const WORKSPACE_GIT = '@bakers-build/addon-workspace-git';
71
+ const FAKE = '@bakers-build/addon-fake';
72
+ /**
73
+ * The 14 addon packages renamed by BB-224 (spec v3 decision 14), including
74
+ * the BB-220 `pi` agent addon merged in from develop. A CLOSED
75
+ * set — the pass rewrites only these exact specifiers, so an unknown
76
+ * `@bakers-build/plugin-something` is left alone rather than silently retargeted at a
77
+ * package that does not exist.
78
+ */
79
+ export const RENAMED_ADDONS = [
80
+ 'auth-basic',
81
+ 'claude',
82
+ 'codex',
83
+ 'deployment',
84
+ 'dropsh',
85
+ 'essentials',
86
+ 'fake',
87
+ 'bakers-ui',
88
+ 'herdr',
89
+ 'kimi',
90
+ 'opencode',
91
+ 'pi',
92
+ 'remote-drupal',
93
+ 'workspace-git',
94
+ ];
95
+ /**
96
+ * `'@bakers-build/plugin-<known>'` (or `"…"` / `` `…` ``), with an optional subpath
97
+ * (`/preset`), inside a MATCHED quote pair. The quote pair + the closing anchor
98
+ * are what make this an exact-specifier match: `@bakers-build/plugin-herdr-x` cannot
99
+ * match `herdr`, and a bare `RemotePlugin` identifier cannot match at all.
100
+ * Longest name first so a longer name always wins the alternation.
101
+ */
102
+ const QUOTED_LEGACY_ADDON = new RegExp(`(['"\`])@bakers-build/plugin-(${[...RENAMED_ADDONS]
103
+ .sort((a, b) => b.length - a.length)
104
+ .join('|')})((?:/[^'"\`]*)?)\\1`, 'g');
105
+ /** `'@bakers-build/core/builtins'` inside a matched quote pair (the deleted preset). */
106
+ const QUOTED_LEGACY_BUILTINS = /(['"`])@bakers-build\/core\/builtins\1/g;
107
+ /**
108
+ * Where each export of the host barrels lives now — the DELETED
109
+ * `@bakers-build/core/plugins` and (BB-230) the meta package's
110
+ * `@bakers-build/bakers/plugins`, which is unresolvable from a dev-clone install.
111
+ *
112
+ * The key set is the barrel's own export list — `core/src/plugins/registry-exports.ts`
113
+ * as of the commit that deleted it — re-pointed at the addon that owns each
114
+ * factory today. `defaultExport` records whether the target package
115
+ * DEFAULT-exports exactly that factory; when it does, the descriptor's `export:`
116
+ * becomes redundant and is dropped.
117
+ *
118
+ * Deliberately absent, so they are left untouched:
119
+ * - `selectRemote`/`selectWorkspace`/`selectExecutor`/`selectAgent(s)` — moved
120
+ * to `@bakers-build/conductor/contract`, but they are config-shape SELECTORS, not
121
+ * plugin factories, so no descriptor can legitimately name them;
122
+ * - any other/unknown export name — a config naming one is reported by the
123
+ * loader rather than silently retargeted at a package that may not have it.
124
+ */
125
+ export const CORE_PLUGINS_BARREL_EXPORTS = {
126
+ DrupalBbRemote: { target: REMOTE_DRUPAL, defaultExport: false },
127
+ FakeBbRemote: { target: FAKE, defaultExport: false },
128
+ FakeWorkspace: { target: FAKE, defaultExport: false },
129
+ GitWorkspace: { target: WORKSPACE_GIT, defaultExport: false },
130
+ basicAuthProvider: { target: AUTH_BASIC, defaultExport: true },
131
+ drupalRemote: { target: REMOTE_DRUPAL, defaultExport: true },
132
+ // @bakers-build/addon-fake ships the whole deterministic triple as NAMED exports
133
+ // (no default), so these keep their `export:`.
134
+ fakeRemote: { target: FAKE, defaultExport: false },
135
+ fakeWorkspace: { target: FAKE, defaultExport: false },
136
+ gitWorkspace: { target: WORKSPACE_GIT, defaultExport: true },
137
+ };
138
+ /**
139
+ * The host-barrel specifiers a descriptor may name. Both map through
140
+ * `CORE_PLUGINS_BARREL_EXPORTS`: `@bakers-build/core/plugins` is DELETED, and
141
+ * `@bakers-build/bakers/plugins` (BB-230) is unresolvable from a dev-clone install
142
+ * because the meta package that exports it is the workspace's leaf sink.
143
+ */
144
+ export const HOST_BARREL_PATHS = [
145
+ '@bakers-build/core/plugins',
146
+ '@bakers-build/bakers/plugins',
147
+ ];
148
+ /**
149
+ * The object-literal entry that encloses `at`, e.g.
150
+ * `{ use: '@bakers-build/core/builtins', with: { basic: token } }`. Empty string when
151
+ * the occurrence is a BARE string entry (`addons: ['@bakers-build/core/builtins']`) —
152
+ * the backward scan then hits the array/argument boundary first.
153
+ */
154
+ function enclosingEntry(text, at) {
155
+ let depth = 0;
156
+ let start = -1;
157
+ for (let i = at; i >= 0; i--) {
158
+ const c = text[i];
159
+ if (c === '}')
160
+ depth++;
161
+ else if (c === '{') {
162
+ if (depth === 0) {
163
+ start = i;
164
+ break;
165
+ }
166
+ depth--;
167
+ }
168
+ else if (depth === 0 && (c === '[' || c === '(' || c === ';')) {
169
+ break;
170
+ }
171
+ }
172
+ if (start < 0)
173
+ return '';
174
+ let d = 0;
175
+ for (let i = start; i < text.length; i++) {
176
+ const c = text[i];
177
+ if (c === '{')
178
+ d++;
179
+ else if (c === '}') {
180
+ d--;
181
+ if (d === 0)
182
+ return text.slice(start, i + 1);
183
+ }
184
+ }
185
+ return text.slice(start);
186
+ }
187
+ /** The nearest `plugins:` / `addons:` array key BEFORE `at`, if any. */
188
+ function nearestArrayKey(text, at) {
189
+ const re = /\b(plugins|addons)\s*:/g;
190
+ let last;
191
+ for (let m = re.exec(text); m !== null && m.index < at; m = re.exec(text)) {
192
+ last = m[1];
193
+ }
194
+ return last;
195
+ }
196
+ /**
197
+ * `@bakers-build/core/builtins` was ONE preset contributing to BOTH surfaces, so its
198
+ * replacement is per-occurrence (spec v3 decision 17, the Finding 6/8 interplay):
199
+ *
200
+ * | signal (first that applies) | target |
201
+ * | the entry carries a `basic` token | `@bakers-build/addon-auth-basic` |
202
+ * | the nearest array key is `plugins:` | `@bakers-build/addon-auth-basic` |
203
+ * | the file is a connection config | `@bakers-build/addon-auth-basic` |
204
+ * | otherwise (engine `addons:` / a slot) | `@bakers-build/addon-remote-drupal` |
205
+ *
206
+ * The `plugins:` rule is what makes a LEGACY `conductor.config.js` (engine file
207
+ * name, but still carrying a connection `site`/`plugins`) come out right.
208
+ */
209
+ function builtinsTargetFor(text, at, surface) {
210
+ if (/basic/i.test(enclosingEntry(text, at)))
211
+ return AUTH_BASIC;
212
+ if (nearestArrayKey(text, at) === 'plugins')
213
+ return AUTH_BASIC;
214
+ return surface === 'connection' ? AUTH_BASIC : REMOTE_DRUPAL;
215
+ }
216
+ /**
217
+ * Rewrite the legacy specifiers in one config's SOURCE TEXT — all of them: the
218
+ * renamed `@bakers-build/plugin-*` packages, the deleted `@bakers-build/core/builtins`
219
+ * preset, and the host-barrel descriptors naming either `@bakers-build/core/plugins`
220
+ * (deleted) or `@bakers-build/bakers/plugins` (BB-230). Pure; line
221
+ * count is preserved (every replacement is line-local), which is what lets the
222
+ * reported diff be a plain index-aligned line diff.
223
+ */
224
+ export function migrateAddonNames(text, surface) {
225
+ const source = inspectConfigSource(text);
226
+ if (source.syntax !== 'valid')
227
+ return { text, count: 0 };
228
+ const e = new ConfigSourceEditor(text);
229
+ let count = 0;
230
+ const keys = [...source.properties.keys()].filter((key) => /(?:\.plugin|\.use|^plugins\[\d+\]$|^addons\[\d+\]$)$/.test(key));
231
+ for (const key of keys) {
232
+ const p = e.property(key);
233
+ if (!p?.staticallyDetermined || typeof p.value !== 'string')
234
+ continue;
235
+ let value = e.expression(key);
236
+ value = value.replace(QUOTED_LEGACY_ADDON, (_match, quote, name, subpath) => `${quote}@bakers-build/addon-${name}${subpath}${quote}`);
237
+ value = value.replace(QUOTED_LEGACY_BUILTINS, (_match, quote) => `${quote}${builtinsTargetFor(e.text, p.valueStart, surface)}${quote}`);
238
+ if (HOST_BARREL_PATHS.includes(p.value)) {
239
+ const parent = key.replace(/\.(plugin|use)$/, '');
240
+ const name = e.property(`${parent}.export`);
241
+ const target = typeof name?.value === 'string'
242
+ ? CORE_PLUGINS_BARREL_EXPORTS[name.value]
243
+ : undefined;
244
+ if (target) {
245
+ value = `${value[0]}${target.target}${value[0]}`;
246
+ if (target.defaultExport && !e.expression(parent)?.includes('\n'))
247
+ e.remove(`${parent}.export`);
248
+ }
249
+ }
250
+ if (value !== e.expression(key)) {
251
+ e.set(key, value);
252
+ count++;
253
+ }
254
+ }
255
+ for (const span of [...e.source.moduleSpecifiers].reverse()) {
256
+ const before = e.text.slice(span.start, span.end);
257
+ const after = before.replace(QUOTED_LEGACY_ADDON, (_match, quote, name, subpath) => `${quote}@bakers-build/addon-${name}${subpath}${quote}`);
258
+ if (before !== after) {
259
+ e.replace(span.start, span.end, after);
260
+ count++;
261
+ }
262
+ }
263
+ return { text: e.text, count };
264
+ }
265
+ /** An index-aligned `- old` / `+ new` line diff (line count is preserved). */
266
+ function lineDiff(before, after) {
267
+ const a = before.split('\n');
268
+ const b = after.split('\n');
269
+ const out = [];
270
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
271
+ if (a[i] === b[i])
272
+ continue;
273
+ if (a[i] !== undefined)
274
+ out.push(` - ${a[i]}`);
275
+ if (b[i] !== undefined)
276
+ out.push(` + ${b[i]}`);
277
+ }
278
+ return out;
279
+ }
280
+ /**
281
+ * Rename pass over ONE config file. Returns `undefined` when the file is absent
282
+ * or already on the new names (idempotency: no write, no backup, no report
283
+ * line — the file stays byte-identical).
284
+ */
285
+ export function migrateAddonNamesInFile(path, surface, opts = {}) {
286
+ if (!existsSync(path))
287
+ return undefined;
288
+ const before = readFileSync(path, 'utf8');
289
+ const { text: after, count } = migrateAddonNames(before, surface);
290
+ if (count === 0 || after === before)
291
+ return undefined;
292
+ const diff = lineDiff(before, after);
293
+ const what = `${count} legacy addon reference(s) → @bakers-build/addon-*`;
294
+ if (opts.dryRun ?? false) {
295
+ return { action: `would rewrite ${path} (${what})`, diff, count };
296
+ }
297
+ const bak = `${path}${ADDON_RENAME_BAK_SUFFIX}`;
298
+ writeFileSync(bak, before, 'utf8');
299
+ writeFileSync(path, after, 'utf8');
300
+ return {
301
+ action: `rewrote ${path} (${what}; backup ${bak})`,
302
+ diff,
303
+ count,
304
+ };
305
+ }
306
+ /**
307
+ * Rename pass over every recognised config in the given `.bakers/` dirs (project
308
+ * + home). Emits report lines ONLY for files it actually rewrites, so an install
309
+ * already on `addon-*` adds no noise to the upgrade report.
310
+ */
311
+ export function runAddonRenameMigration(opts = { bakersDirs: [] }) {
312
+ const actions = [];
313
+ let changed = false;
314
+ const seen = new Set();
315
+ for (const dir of opts.bakersDirs) {
316
+ if (seen.has(dir) || !existsSync(dir))
317
+ continue;
318
+ seen.add(dir);
319
+ const files = [
320
+ [join(dir, 'bakers.config.js'), 'connection'],
321
+ ...listConductorConfigFiles(dir).map((f) => [join(dir, f), 'engine']),
322
+ ];
323
+ for (const [path, surface] of files) {
324
+ const res = migrateAddonNamesInFile(path, surface, {
325
+ dryRun: opts.dryRun ?? false,
326
+ });
327
+ if (res === undefined)
328
+ continue;
329
+ actions.push(res.action, ...res.diff);
330
+ changed = true;
331
+ }
332
+ }
333
+ return { actions, changed };
334
+ }
@@ -0,0 +1,21 @@
1
+ export interface BbWorkspaceOwner {
2
+ path: string;
3
+ before: string;
4
+ }
5
+ export interface BbProjectSelection {
6
+ project: string;
7
+ workspace?: string;
8
+ }
9
+ export type BbProjectOwner = BbWorkspaceOwner;
10
+ /** Read committed bakers.build identity without executing conductor source. */
11
+ export declare function readBbProjectSelection(path: string): BbProjectSelection;
12
+ /** Establish the exact file that can receive a nested workspace choice. */
13
+ export declare function bakersProjectOwner(path: string): BbProjectOwner;
14
+ /** Read the committed project name without executing engine config. */
15
+ export declare function readProjectSelection(path: string): string | undefined;
16
+ /** Read the committed product-workspace name without executing engine config. */
17
+ export declare function readBbWorkspaceSelection(path: string): string | undefined;
18
+ /** Establish the conductor-owned source that the cockpit may update. */
19
+ export declare function bakersWorkspaceOwner(path: string): BbWorkspaceOwner;
20
+ /** Atomically update only the product-workspace field, preserving plugin slots. */
21
+ export declare function writeBbWorkspaceSelection(owner: BbWorkspaceOwner, selected: string, hint: string): Promise<void>;
@@ -0,0 +1,105 @@
1
+ import { existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ import { ConfigSourceEditor } from './config-source-edit.js';
4
+ /** Read committed bakers.build identity without executing conductor source. */
5
+ export function readBbProjectSelection(path) {
6
+ const editor = new ConfigSourceEditor(readFileSync(path, 'utf8'));
7
+ const invalid = () => new Error(`Cannot read committed bakers.project in ${path}; run bakers config upgrade --scope project.`);
8
+ const bakers = editor.property('bakers');
9
+ const bakersExpression = editor.expression('bakers');
10
+ const bakersHasSpread = bakersExpression
11
+ ? new ConfigSourceEditor(`export default ${bakersExpression};`).source
12
+ .unknownKeys
13
+ : false;
14
+ const project = editor.property('bakers.project');
15
+ const workspace = editor.property('bakers.workspace');
16
+ if (editor.source.shape !== 'object' ||
17
+ bakersHasSpread ||
18
+ editor.property('project') ||
19
+ editor.property('bb_workspace') ||
20
+ bakers?.nodeType !== 'ObjectExpression' ||
21
+ !project?.staticallyDetermined ||
22
+ typeof project.value !== 'string' ||
23
+ !project.value.trim() ||
24
+ (workspace !== undefined &&
25
+ (!workspace.staticallyDetermined ||
26
+ typeof workspace.value !== 'string' ||
27
+ !workspace.value.trim())))
28
+ throw invalid();
29
+ return {
30
+ project: project.value.trim(),
31
+ ...(workspace ? { workspace: workspace.value.trim() } : {}),
32
+ };
33
+ }
34
+ /** Establish the exact file that can receive a nested workspace choice. */
35
+ export function bakersProjectOwner(path) {
36
+ const file = basename(path);
37
+ if (file !== 'conductor.config.js' && !file.endsWith('.conductor.config.js'))
38
+ throw new Error(`Cannot write bakers.build workspace selection to ${path}.`);
39
+ readBbProjectSelection(path);
40
+ return { path, before: readFileSync(path, 'utf8') };
41
+ }
42
+ /** Read the committed project name without executing engine config. */
43
+ export function readProjectSelection(path) {
44
+ try {
45
+ const property = new ConfigSourceEditor(readFileSync(path, 'utf8')).property('project');
46
+ return property?.staticallyDetermined &&
47
+ typeof property.value === 'string' &&
48
+ property.value.trim() !== ''
49
+ ? property.value.trim()
50
+ : undefined;
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ /** Read the committed product-workspace name without executing engine config. */
57
+ export function readBbWorkspaceSelection(path) {
58
+ try {
59
+ const property = new ConfigSourceEditor(readFileSync(path, 'utf8')).property('bb_workspace');
60
+ return property?.staticallyDetermined &&
61
+ typeof property.value === 'string' &&
62
+ property.value.trim() !== ''
63
+ ? property.value.trim()
64
+ : undefined;
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
70
+ /** Establish the conductor-owned source that the cockpit may update. */
71
+ export function bakersWorkspaceOwner(path) {
72
+ const file = basename(path);
73
+ if (file !== 'conductor.config.js' && !file.endsWith('.conductor.config.js'))
74
+ throw new Error(`Cannot write bakers.build workspace selection to ${path}; expected conductor.config.js or a named conductor variant. No configuration was written.`);
75
+ const before = readFileSync(path, 'utf8');
76
+ const editor = new ConfigSourceEditor(before);
77
+ const property = editor.property('bb_workspace');
78
+ if (editor.source.shape === 'object' &&
79
+ !editor.source.unknownKeys &&
80
+ (!property ||
81
+ (property.staticallyDetermined && typeof property.value === 'string')))
82
+ return { path, before };
83
+ throw new Error(`Cannot establish a literal writable bb_workspace in ${path}. No configuration was written.`);
84
+ }
85
+ /** Atomically update only the product-workspace field, preserving plugin slots. */
86
+ export async function writeBbWorkspaceSelection(owner, selected, hint) {
87
+ const editor = new ConfigSourceEditor(owner.before);
88
+ editor.set(editor.property('bakers.project') ? 'bakers.workspace' : 'bb_workspace', JSON.stringify(selected));
89
+ if ((existsSync(owner.path) ? readFileSync(owner.path, 'utf8') : null) !==
90
+ owner.before)
91
+ throw new Error(`Configuration changed while choosing; recheck ${owner.path} before retrying. No configuration was written.`);
92
+ const temporary = `${owner.path}.${process.pid}.${Date.now()}.tmp`;
93
+ try {
94
+ writeFileSync(temporary, editor.text, {
95
+ mode: statSync(owner.path).mode & 0o777,
96
+ flag: 'wx',
97
+ });
98
+ renameSync(temporary, owner.path);
99
+ }
100
+ catch {
101
+ if (existsSync(temporary))
102
+ unlinkSync(temporary);
103
+ throw new Error(`Could not write bakers.build workspace to ${owner.path}. ${hint}`);
104
+ }
105
+ }
@@ -0,0 +1,91 @@
1
+ import { type BbCommandHost, type BbCommandPlugin, type ConductorLogger, type ProcProbe, type UpdateCheckOptions } from '@bakers-build/core';
2
+ import { Command } from 'commander';
3
+ import type { BbExecutor } from '../plugins/executor.js';
4
+ import { type ResolvedAgent } from '../plugins/plugins.js';
5
+ import type { HerdrHost } from '../plugins/process-host.js';
6
+ import type { BbRemote, ConductorStatus } from '../plugins/remote.js';
7
+ import type { BbWorkspace } from '../plugins/workspace.js';
8
+ import type { ConductorFileConfig } from '../types.js';
9
+ export type { HerdrHost };
10
+ /** Test seam: inject any subset of dependencies. */
11
+ export interface BbCliDeps {
12
+ remote?: BbRemote;
13
+ executor?: BbExecutor;
14
+ workspace?: BbWorkspace;
15
+ agents?: ResolvedAgent[];
16
+ config?: ConductorFileConfig;
17
+ /** Injectable registry fetch for the start-time version check (tests). */
18
+ fetch?: typeof fetch;
19
+ /** Marker immediately before start hands control to conductor work. */
20
+ onStartLoop?: () => void;
21
+ /** Host contract (resolveBases); default: this install + cwd. */
22
+ host?: BbCommandHost;
23
+ /**
24
+ * The process host behind `start`. Defaults to the host contributed by a
25
+ * conductor addon (`processHosts`, last wins). A test MUST inject a fake,
26
+ * or the suite creates real workspaces + real conductor processes.
27
+ */
28
+ herdr?: HerdrHost;
29
+ /**
30
+ * BB-232: the pid probe behind `stop`. Defaults to the real /proc reader —
31
+ * a test MUST inject a fake, or the suite signals real processes.
32
+ */
33
+ procProbe?: ProcProbe;
34
+ /**
35
+ * BB-450 D9: how long `start` waits between polls for a fresh heartbeat,
36
+ * and how many it takes before giving up and reporting **failed**. Defaults
37
+ * are a real wait budget (1s × 20 ≈ 20s); a test MUST inject both (with a
38
+ * no-op `sleep`) or it waits for real time.
39
+ */
40
+ startPollIntervalMs?: number;
41
+ startPollAttempts?: number;
42
+ /**
43
+ * BB-450 D10: `start --force`'s grace period — how long it waits between
44
+ * polls for the stopped conductor to actually exit before escalating to
45
+ * `SIGKILL`, and how many it takes before escalating. Defaults are a real
46
+ * wait budget (500ms × 10 ≈ 5s).
47
+ */
48
+ forceGraceIntervalMs?: number;
49
+ forceGraceAttempts?: number;
50
+ /** Test seam: replaces the real timer `start` waits on between polls. */
51
+ sleep?: (ms: number) => Promise<void>;
52
+ /** Cache path, clock and fetch overrides for the start-time update check. */
53
+ updateCheckOptions?: UpdateCheckOptions;
54
+ /** stdin TTY override for the install prompt. */
55
+ stdinIsTTY?: boolean;
56
+ /** stdout TTY override for the install prompt. */
57
+ stdoutIsTTY?: boolean;
58
+ /** Environment for CI / selector detection. Defaults to process.env. */
59
+ env?: NodeJS.ProcessEnv;
60
+ /** One prompt line; empty string is default yes. Reject to cancel. */
61
+ askUpdate?: (question: string) => Promise<string>;
62
+ /** Invoke `bakers` with an argv array (no binary name). */
63
+ runBb?: (args: string[]) => Promise<void>;
64
+ }
65
+ /**
66
+ * Start-time auth gate. Returns true if authenticated (session or session-less
67
+ * provider); otherwise logs a single clear line and returns false.
68
+ */
69
+ export declare function ensureAuthenticated(config: ConductorFileConfig, logger: ConductorLogger): Promise<boolean>;
70
+ /**
71
+ * Other conductor records that name the SAME checkout as `own` (BB-450 D6,
72
+ * AC-19) — the exact ambiguity BB-281 recorded in the field: one checkout,
73
+ * two `bb_conductor` rows under different ids. Reported so the operator can
74
+ * see the stray; never signalled and never cleaned up here.
75
+ */
76
+ export declare function strayConductorsOf(rows: ConductorStatus[], own: ConductorStatus): ConductorStatus[];
77
+ /** Prompt for the developer Kürzel / user id on an interactive terminal. */
78
+ /** Build the `oven` subcommand tree (lifecycle + registry). */
79
+ export declare function createConductorCommand(deps: BbCliDeps): Command;
80
+ /** The `oven` command plugin the host mounts (BB-201). BB-224
81
+ * (Finding 7): the `bakers deployment` batch helper is no longer registered here —
82
+ * it is its own command addon, `@bakers-build/addon-deployment`. */
83
+ declare const conductorCommandPlugin: BbCommandPlugin;
84
+ export default conductorCommandPlugin;
85
+ /**
86
+ * Build a standalone program with the conductor command mounted — the test entry
87
+ * (mirrors what the host does for `bakers oven …`). `deps` inject fakes.
88
+ */
89
+ export declare function buildConductorProgram(deps?: BbCliDeps): Command;
90
+ /** Parse argv against the conductor program (test entry). */
91
+ export declare function runConductorCli(argv: string[], deps?: BbCliDeps): Promise<void>;