@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,137 @@
1
+ /**
2
+ * A single, discrete, testable upgrade method `vN → vN+1`, registered in order —
3
+ * exactly like a Drupal `hook_update_N` chain. `bakers config upgrade` resolves a config's
4
+ * current version and applies each method in sequence up to CURRENT (it does not
5
+ * blindly overwrite), so a method can PRESERVE operator-set fields across a bump.
6
+ */
7
+ export interface ConnectionConfigMigration {
8
+ /** The schema version this method upgrades FROM. */
9
+ from: number;
10
+ /** The schema version this method upgrades TO (=== from + 1). */
11
+ to: number;
12
+ /** What shape change this method performs (human-readable). */
13
+ description: string;
14
+ /**
15
+ * Upgrade the config text from `from` to `to`, returning the new file text
16
+ * WITHOUT the version header — the runner re-stamps `@bakers-schema-version to`.
17
+ */
18
+ apply(currentText: string): string;
19
+ }
20
+ /**
21
+ * BB-226: the package name whose `plugins[]` entry contributes the `md`
22
+ * renderer (plus the authoritative JSON:API schema) to a connection config. The
23
+ * AGGREGATOR is named, not `@dropsh/plugin-markdown` directly: a direct
24
+ * `@dropsh/*` descriptor is unresolvable from `loadBbConfig`'s bases
25
+ * (config dir / host module / cwd), whereas `@bakers-build/addon-essentials` is a
26
+ * real dependency of both the host and the engine and therefore resolves in the
27
+ * monorepo AND from a global `@bakers-build/bakers` install. No `export:` is needed —
28
+ * the package default-exports its factory.
29
+ */
30
+ export declare const RENDERER_PLUGIN_PACKAGE = "@bakers-build/addon-essentials";
31
+ /** The exact `plugins[]` element text the v1→v2 migration and the scaffold emit. */
32
+ export declare const RENDERER_PLUGIN_ENTRY = "{ plugin: '@bakers-build/addon-essentials' },";
33
+ /**
34
+ * BB-226 (v1 → v2): insert the renderer aggregator as the FIRST `plugins[]`
35
+ * element, so `bakers dropsh --format md` works instead of failing with
36
+ * `E_CONFIG: Unknown format 'md'. Available: json`.
37
+ *
38
+ * A pure TEXT transform — the upgrade runner must never execute a possibly
39
+ * side-effecting config module. Idempotent: text that already names the package
40
+ * in CODE is returned unchanged (whole-line `//` comments are stripped for that
41
+ * probe, so the template's own explanatory header does not read as a hit), and a
42
+ * config with no `plugins: [` array literal (an `addons: []`-shaped or
43
+ * hand-reshaped file) is left alone rather than guessed at.
44
+ */
45
+ export declare function addRendererPluginEntry(text: string): string;
46
+ /** The dropsh plugin package whose entries carry a connection's OAuth identity. */
47
+ export declare const OAUTH2_PLUGIN_PACKAGE = "@dropsh/plugin-oauth2";
48
+ /**
49
+ * The id of the ONE oauth2 provider a bakers.build connection declares (BB-391 D1).
50
+ *
51
+ * `bakers`, not `session`: the entry names the CONNECTION, not a capability. Until
52
+ * v3 a config declared two providers that differed only by scope — `session`
53
+ * (`bb:session`) and `pm` (`bakers:project_manager`) — and every write had to
54
+ * pick one. What that choice cost is what BB-391 removes; the id therefore
55
+ * names the connection, and the surviving scope carries the capability.
56
+ */
57
+ export declare const OAUTH2_PROVIDER_ID = "bakers";
58
+ /**
59
+ * The ONE OAuth2 scope a bakers.build connection requests (BB-391 D1, as revised).
60
+ *
61
+ * There is exactly one, and there is not zero. `bb:session`'s role carries
62
+ * the ceiling of what a machine identity may do.
63
+ *
64
+ * Zero was the original design, and a live control plane refuted it:
65
+ * `simple_oauth`'s Oauth2AccessPolicy rewrites a token account's permissions
66
+ * with `overwrite: TRUE`, and for a `client_credentials` token — every bakers.build
67
+ * machine identity — the set becomes EXACTLY the scope's permissions, with
68
+ * neither user_role nor OG-rank permissions surviving. A request naming no scope
69
+ * against a consumer holding no default scopes does not even mint a token.
70
+ */
71
+ export declare const OAUTH2_SCOPE = "bb:session";
72
+ /**
73
+ * BB-391 (v2 → v3): collapse a connection's oauth2 entries into ONE `bakers`
74
+ * provider on ONE scope — the whole of the one-identity change, as a pure text
75
+ * transform.
76
+ *
77
+ * A MIGRATION rather than a cutover (spec D3): operators have hand-edited these
78
+ * files — a pinned literal `baseUrl`, a per-install `workspace_id`, an extra
79
+ * `addons[]` entry — so every value outside the two elements being merged is
80
+ * carried over verbatim, and the runner never EXECUTES a possibly
81
+ * side-effecting config module.
82
+ *
83
+ * The FIRST oauth2 element survives (it is the one v2 marked `default: true`);
84
+ * the rest are removed line-and-all. A config with no oauth2 element is left
85
+ * exactly as it is — that is the one silent no-op, and it is silent because
86
+ * there is nothing to collapse. An element that IS there but cannot be given the
87
+ * `bakers` identity throws (see {@link ensureProviderId}) rather than passing
88
+ * through unchanged.
89
+ */
90
+ export declare function collapseOauth2Providers(text: string): string;
91
+ /**
92
+ * The ordered, contiguous migration chain. v1 was the pre-BB-226 shape (auth
93
+ * plugins only); v2 adds the markdown renderer entry; v3 collapses the two
94
+ * differently-scoped oauth2 providers into ONE identity on ONE scope (BB-391)
95
+ * — one, not zero: see {@link OAUTH2_SCOPE} for why the scope survives.
96
+ * Nothing below v1 is a versioned config (v0 = seed / hand-authored / untouched).
97
+ * `BB_CONFIG_SCHEMA_VERSION` derives from the chain's last `to` — never
98
+ * hardcode it.
99
+ */
100
+ export declare const CONNECTION_MIGRATIONS: ConnectionConfigMigration[];
101
+ /** The current connection-config schema version — single source of truth. */
102
+ export declare const BB_CONFIG_SCHEMA_VERSION: number;
103
+ /** Parse the `@bakers-schema-version` marker from config text; `0` when absent. */
104
+ export declare function schemaVersionOf(text: string): number;
105
+ /**
106
+ * Read the schema version from a connection config FILE by regex (sync, never
107
+ * executes the module). `0` when the file is absent, unreadable, or carries no
108
+ * marker (legacy / hand-authored).
109
+ */
110
+ export declare function readConfigSchemaVersion(path: string): number;
111
+ /** Strip a leading `// @bakers-schema-version <N>` header line, if present. */
112
+ export declare function stripVersionHeader(text: string): string;
113
+ /** Prepend (replacing any existing) a `// @bakers-schema-version <N>` header. */
114
+ export declare function stampVersion(text: string, version: number): string;
115
+ /**
116
+ * Re-stamp the in-body `schema_version: <N>` field, which the generated config
117
+ * carries alongside the header marker for programmatic reads. The header is the
118
+ * upgrade decision's source of truth and is stamped by the runner; this keeps the
119
+ * FIELD from going stale when a migration bumps the version (BB-226 — the first
120
+ * migration is where the two could first disagree). No-op when the field is
121
+ * absent (a hand-authored config need not carry it).
122
+ */
123
+ export declare function stampSchemaVersionField(text: string, version: number): string;
124
+ /**
125
+ * Validate a migration chain is contiguous: each `to === from + 1`, the first
126
+ * migrates from v1, no gaps, and the chain ends at `current` (default CURRENT).
127
+ * An empty chain is valid iff `current === 1`. Throws with an actionable message.
128
+ */
129
+ export declare function assertContiguous(chain: ConnectionConfigMigration[], current?: number): void;
130
+ /**
131
+ * Walk the migration chain from `from` up to `to`, applying each method's
132
+ * `apply` in sequence. Input/output text is WITHOUT the version header — the
133
+ * caller stamps CURRENT after. After each step the in-body `schema_version:`
134
+ * field is re-stamped to that step's `to`, so no individual method has to know
135
+ * (or hardcode) its own number twice. `chain` is injectable for tests.
136
+ */
137
+ export declare function applyMigrations(text: string, from: number, to: number, chain?: ConnectionConfigMigration[]): string;
@@ -0,0 +1,239 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { ConfigSourceEditor } from './config-source-edit.js';
3
+ // BB-216: schema versioning for the generated CONNECTION config
4
+ // (`bakers.config.js`, project + home). Decoupled from the npm package semver —
5
+ // the schema version bumps only when the config SHAPE changes, not every
6
+ // release. Every generated connection config carries a machine-readable header
7
+ // comment `// @bakers-schema-version <N>` (the source of truth for the upgrade
8
+ // decision, read by regex so `bakers config upgrade` NEVER executes a possibly
9
+ // side-effecting config module) and a `schema_version: <N>` field for
10
+ // programmatic reads.
11
+ /** The header marker prepended to every generated connection config. */
12
+ const MARKER_RE = /@bakers-schema-version\s+(\d+)/;
13
+ const HEADER_LINE_RE = /^\/\/ @bakers-schema-version \d+\n/;
14
+ /**
15
+ * BB-226: the package name whose `plugins[]` entry contributes the `md`
16
+ * renderer (plus the authoritative JSON:API schema) to a connection config. The
17
+ * AGGREGATOR is named, not `@dropsh/plugin-markdown` directly: a direct
18
+ * `@dropsh/*` descriptor is unresolvable from `loadBbConfig`'s bases
19
+ * (config dir / host module / cwd), whereas `@bakers-build/addon-essentials` is a
20
+ * real dependency of both the host and the engine and therefore resolves in the
21
+ * monorepo AND from a global `@bakers-build/bakers` install. No `export:` is needed —
22
+ * the package default-exports its factory.
23
+ */
24
+ export const RENDERER_PLUGIN_PACKAGE = '@bakers-build/addon-essentials';
25
+ /** The exact `plugins[]` element text the v1→v2 migration and the scaffold emit. */
26
+ export const RENDERER_PLUGIN_ENTRY = `{ plugin: '${RENDERER_PLUGIN_PACKAGE}' },`;
27
+ /**
28
+ * BB-226 (v1 → v2): insert the renderer aggregator as the FIRST `plugins[]`
29
+ * element, so `bakers dropsh --format md` works instead of failing with
30
+ * `E_CONFIG: Unknown format 'md'. Available: json`.
31
+ *
32
+ * A pure TEXT transform — the upgrade runner must never execute a possibly
33
+ * side-effecting config module. Idempotent: text that already names the package
34
+ * in CODE is returned unchanged (whole-line `//` comments are stripped for that
35
+ * probe, so the template's own explanatory header does not read as a hit), and a
36
+ * config with no `plugins: [` array literal (an `addons: []`-shaped or
37
+ * hand-reshaped file) is left alone rather than guessed at.
38
+ */
39
+ export function addRendererPluginEntry(text) {
40
+ const e = new ConfigSourceEditor(text);
41
+ const array = e.property('plugins');
42
+ if (array?.nodeType !== 'ArrayExpression')
43
+ return text;
44
+ if ([...e.source.properties].some(([key, p]) => /^plugins\[\d+\]\.plugin$/.test(key) &&
45
+ p.value === RENDERER_PLUGIN_PACKAGE))
46
+ return text;
47
+ e.replace(array.valueStart + 1, array.valueStart + 1, `\n ${RENDERER_PLUGIN_ENTRY}`);
48
+ return e.text;
49
+ }
50
+ /** The dropsh plugin package whose entries carry a connection's OAuth identity. */
51
+ export const OAUTH2_PLUGIN_PACKAGE = '@dropsh/plugin-oauth2';
52
+ /**
53
+ * The id of the ONE oauth2 provider a bakers.build connection declares (BB-391 D1).
54
+ *
55
+ * `bakers`, not `session`: the entry names the CONNECTION, not a capability. Until
56
+ * v3 a config declared two providers that differed only by scope — `session`
57
+ * (`bb:session`) and `pm` (`bakers:project_manager`) — and every write had to
58
+ * pick one. What that choice cost is what BB-391 removes; the id therefore
59
+ * names the connection, and the surviving scope carries the capability.
60
+ */
61
+ export const OAUTH2_PROVIDER_ID = 'bakers';
62
+ /**
63
+ * The ONE OAuth2 scope a bakers.build connection requests (BB-391 D1, as revised).
64
+ *
65
+ * There is exactly one, and there is not zero. `bb:session`'s role carries
66
+ * the ceiling of what a machine identity may do.
67
+ *
68
+ * Zero was the original design, and a live control plane refuted it:
69
+ * `simple_oauth`'s Oauth2AccessPolicy rewrites a token account's permissions
70
+ * with `overwrite: TRUE`, and for a `client_credentials` token — every bakers.build
71
+ * machine identity — the set becomes EXACTLY the scope's permissions, with
72
+ * neither user_role nor OG-rank permissions surviving. A request naming no scope
73
+ * against a consumer holding no default scopes does not even mint a token.
74
+ */
75
+ export const OAUTH2_SCOPE = 'bb:session';
76
+ /**
77
+ * BB-391 (v2 → v3): collapse a connection's oauth2 entries into ONE `bakers`
78
+ * provider on ONE scope — the whole of the one-identity change, as a pure text
79
+ * transform.
80
+ *
81
+ * A MIGRATION rather than a cutover (spec D3): operators have hand-edited these
82
+ * files — a pinned literal `baseUrl`, a per-install `workspace_id`, an extra
83
+ * `addons[]` entry — so every value outside the two elements being merged is
84
+ * carried over verbatim, and the runner never EXECUTES a possibly
85
+ * side-effecting config module.
86
+ *
87
+ * The FIRST oauth2 element survives (it is the one v2 marked `default: true`);
88
+ * the rest are removed line-and-all. A config with no oauth2 element is left
89
+ * exactly as it is — that is the one silent no-op, and it is silent because
90
+ * there is nothing to collapse. An element that IS there but cannot be given the
91
+ * `bakers` identity throws (see {@link ensureProviderId}) rather than passing
92
+ * through unchanged.
93
+ */
94
+ export function collapseOauth2Providers(text) {
95
+ const e = new ConfigSourceEditor(text);
96
+ const entries = [...e.source.properties]
97
+ .filter(([key, p]) => /^plugins\[\d+\]\.plugin$/.test(key) &&
98
+ p.value === OAUTH2_PLUGIN_PACKAGE)
99
+ .map(([key]) => key.replace(/\.plugin$/, ''));
100
+ const first = entries[0];
101
+ if (!first)
102
+ return text;
103
+ if (e.property(`${first}.with`)?.nodeType !== 'ObjectExpression')
104
+ throw new Error('Cannot migrate oauth2 without a with object.');
105
+ for (const duplicate of entries.slice(1).reverse()) {
106
+ for (const key of ['client_id', 'client_secret', 'token_url', 'type']) {
107
+ if (e.expression(`${first}.with.${key}`) !==
108
+ e.expression(`${duplicate}.with.${key}`))
109
+ throw new Error('OAuth providers have conflicting connection inputs.');
110
+ }
111
+ e.remove(duplicate);
112
+ }
113
+ for (const [key, value] of [
114
+ ['id', "'bakers'"],
115
+ ['scope', "'bb:session'"],
116
+ ['default', 'true'],
117
+ ]) {
118
+ if (e.expression(`${first}.with.${key}`) !== value)
119
+ e.set(`${first}.with.${key}`, value);
120
+ }
121
+ return e.text;
122
+ }
123
+ /**
124
+ * The ordered, contiguous migration chain. v1 was the pre-BB-226 shape (auth
125
+ * plugins only); v2 adds the markdown renderer entry; v3 collapses the two
126
+ * differently-scoped oauth2 providers into ONE identity on ONE scope (BB-391)
127
+ * — one, not zero: see {@link OAUTH2_SCOPE} for why the scope survives.
128
+ * Nothing below v1 is a versioned config (v0 = seed / hand-authored / untouched).
129
+ * `BB_CONFIG_SCHEMA_VERSION` derives from the chain's last `to` — never
130
+ * hardcode it.
131
+ */
132
+ export const CONNECTION_MIGRATIONS = [
133
+ {
134
+ from: 1,
135
+ to: 2,
136
+ description: `add the ${RENDERER_PLUGIN_PACKAGE} renderer entry to plugins[]`,
137
+ apply: addRendererPluginEntry,
138
+ },
139
+ {
140
+ from: 2,
141
+ to: 3,
142
+ description: `collapse the two ${OAUTH2_PLUGIN_PACKAGE} entries into one ` +
143
+ `'${OAUTH2_PROVIDER_ID}' provider on scope '${OAUTH2_SCOPE}'`,
144
+ apply: collapseOauth2Providers,
145
+ },
146
+ {
147
+ from: 3,
148
+ to: 4,
149
+ description: 'connection owns credentials and workspace independently of machine context',
150
+ apply: (text) => {
151
+ const source = new ConfigSourceEditor(text);
152
+ if (/const (baseUrl|clientId|clientSecret|workspaceId) = [^;]*machine\.(base_url|client_id|client_secret|workspace_id)/.test(text) ||
153
+ [...source.source.properties.keys()].some((key) => /\bmachine\.(base_url|client_id|client_secret|workspace_id)\b/.test(source.expression(key) ?? '')))
154
+ throw new Error('Connection ownership requires scoped cross-file planning.');
155
+ return text;
156
+ },
157
+ },
158
+ ];
159
+ /** The current connection-config schema version — single source of truth. */
160
+ export const BB_CONFIG_SCHEMA_VERSION = CONNECTION_MIGRATIONS.at(-1)?.to ?? 1;
161
+ /** Parse the `@bakers-schema-version` marker from config text; `0` when absent. */
162
+ export function schemaVersionOf(text) {
163
+ const m = MARKER_RE.exec(text);
164
+ return m ? Number.parseInt(m[1], 10) : 0;
165
+ }
166
+ /**
167
+ * Read the schema version from a connection config FILE by regex (sync, never
168
+ * executes the module). `0` when the file is absent, unreadable, or carries no
169
+ * marker (legacy / hand-authored).
170
+ */
171
+ export function readConfigSchemaVersion(path) {
172
+ try {
173
+ return schemaVersionOf(readFileSync(path, 'utf8'));
174
+ }
175
+ catch {
176
+ return 0;
177
+ }
178
+ }
179
+ /** Strip a leading `// @bakers-schema-version <N>` header line, if present. */
180
+ export function stripVersionHeader(text) {
181
+ return text.replace(HEADER_LINE_RE, '');
182
+ }
183
+ /** Prepend (replacing any existing) a `// @bakers-schema-version <N>` header. */
184
+ export function stampVersion(text, version) {
185
+ return `// @bakers-schema-version ${version}\n${stripVersionHeader(text)}`;
186
+ }
187
+ /**
188
+ * Re-stamp the in-body `schema_version: <N>` field, which the generated config
189
+ * carries alongside the header marker for programmatic reads. The header is the
190
+ * upgrade decision's source of truth and is stamped by the runner; this keeps the
191
+ * FIELD from going stale when a migration bumps the version (BB-226 — the first
192
+ * migration is where the two could first disagree). No-op when the field is
193
+ * absent (a hand-authored config need not carry it).
194
+ */
195
+ export function stampSchemaVersionField(text, version) {
196
+ return text.replace(/^([ \t]*)schema_version:\s*\d+\s*,/m, `$1schema_version: ${version},`);
197
+ }
198
+ /**
199
+ * Validate a migration chain is contiguous: each `to === from + 1`, the first
200
+ * migrates from v1, no gaps, and the chain ends at `current` (default CURRENT).
201
+ * An empty chain is valid iff `current === 1`. Throws with an actionable message.
202
+ */
203
+ export function assertContiguous(chain, current = BB_CONFIG_SCHEMA_VERSION) {
204
+ let prev = 1;
205
+ chain.forEach((m, i) => {
206
+ if (m.to !== m.from + 1) {
207
+ throw new Error(`migration[${i}]: to (${m.to}) must equal from+1 (${m.from + 1})`);
208
+ }
209
+ const expectedFrom = i === 0 ? 1 : prev;
210
+ if (m.from !== expectedFrom) {
211
+ throw new Error(`migration[${i}]: from (${m.from}) must equal ${expectedFrom} (no gaps; chain starts at v1)`);
212
+ }
213
+ prev = m.to;
214
+ });
215
+ const end = chain.at(-1)?.to ?? 1;
216
+ if (end !== current) {
217
+ throw new Error(`migration chain ends at v${end}, expected CURRENT v${current}`);
218
+ }
219
+ }
220
+ /**
221
+ * Walk the migration chain from `from` up to `to`, applying each method's
222
+ * `apply` in sequence. Input/output text is WITHOUT the version header — the
223
+ * caller stamps CURRENT after. After each step the in-body `schema_version:`
224
+ * field is re-stamped to that step's `to`, so no individual method has to know
225
+ * (or hardcode) its own number twice. `chain` is injectable for tests.
226
+ */
227
+ export function applyMigrations(text, from, to, chain = CONNECTION_MIGRATIONS) {
228
+ let cur = from;
229
+ let out = text;
230
+ while (cur < to) {
231
+ const step = chain.find((m) => m.from === cur);
232
+ if (step === undefined) {
233
+ throw new Error(`no migration from v${cur} (chain incomplete)`);
234
+ }
235
+ out = stampSchemaVersionField(step.apply(out), step.to);
236
+ cur = step.to;
237
+ }
238
+ return out;
239
+ }
@@ -0,0 +1,17 @@
1
+ import { type ConfigSourceProperty } from '@bakers-build/core';
2
+ /** Small span editor. Every operation re-parses so offsets never become stale. */
3
+ export declare class ConfigSourceEditor {
4
+ text: string;
5
+ constructor(text: string);
6
+ get source(): import("@bakers-build/core").ConfigSourceInspection;
7
+ property(key: string): ConfigSourceProperty | undefined;
8
+ expression(key: string): string | undefined;
9
+ replace(start: number, end: number, value: string): void;
10
+ set(key: string, value: string): void;
11
+ remove(key: string): void;
12
+ rename(key: string, next: string): void;
13
+ stamp(version: number): void;
14
+ }
15
+ export declare function sameValue(a: ConfigSourceProperty, b: ConfigSourceProperty): boolean;
16
+ /** Hide all computed source and every credential-bearing statement in previews. */
17
+ export declare function redactConfigSource(text: string): string;
@@ -0,0 +1,95 @@
1
+ import { inspectConfigSource, } from '@bakers-build/core';
2
+ /** Small span editor. Every operation re-parses so offsets never become stale. */
3
+ export class ConfigSourceEditor {
4
+ text;
5
+ constructor(text) {
6
+ this.text = text;
7
+ }
8
+ get source() {
9
+ return inspectConfigSource(this.text);
10
+ }
11
+ property(key) {
12
+ return this.source.properties.get(key);
13
+ }
14
+ expression(key) {
15
+ const p = this.property(key);
16
+ return p ? this.text.slice(p.valueStart, p.valueEnd) : undefined;
17
+ }
18
+ replace(start, end, value) {
19
+ this.text = this.text.slice(0, start) + value + this.text.slice(end);
20
+ }
21
+ set(key, value) {
22
+ const existing = this.property(key);
23
+ if (existing) {
24
+ this.replace(existing.valueStart, existing.valueEnd, value);
25
+ return;
26
+ }
27
+ const dot = key.lastIndexOf('.');
28
+ const parentKey = dot < 0 ? null : key.slice(0, dot);
29
+ const name = dot < 0 ? key : key.slice(dot + 1);
30
+ const source = this.source;
31
+ const parent = parentKey ? source.properties.get(parentKey) : undefined;
32
+ if (parentKey && !parent) {
33
+ this.set(parentKey, `{ ${/^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name)}: ${value} }`);
34
+ return;
35
+ }
36
+ const start = parent ? parent.valueStart : source.object?.start;
37
+ if (start === undefined ||
38
+ (parent && parent.nodeType !== 'ObjectExpression'))
39
+ throw new Error('Unsupported insertion');
40
+ this.replace(start + 1, start + 1, `\n ${/^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name)}: ${value},`);
41
+ }
42
+ remove(key) {
43
+ const p = this.property(key);
44
+ if (!p)
45
+ return;
46
+ let end = p.end;
47
+ while (/\s/.test(this.text[end] ?? '') && end < this.text.length)
48
+ end++;
49
+ if (this.text[end] === ',')
50
+ this.replace(p.start, end + 1, '');
51
+ else {
52
+ let start = p.start - 1;
53
+ while (start >= 0 && /\s/.test(this.text[start] ?? ''))
54
+ start--;
55
+ this.replace(this.text[start] === ',' ? start : p.start, p.end, '');
56
+ }
57
+ }
58
+ rename(key, next) {
59
+ const p = this.property(key);
60
+ if (!p)
61
+ return;
62
+ const colon = this.text.indexOf(':', p.start);
63
+ if (colon < 0 || colon >= p.valueStart)
64
+ throw new Error('Unsupported key');
65
+ this.replace(p.start, colon, next);
66
+ }
67
+ stamp(version) {
68
+ this.set('schema_version', String(version));
69
+ this.text = this.text
70
+ .replace(/^\s*\/\/ @bakers-schema-version \d+\s*$/gm, '')
71
+ .replace(/^\n/, '');
72
+ this.text = `// @bakers-schema-version ${version}\n${this.text}`;
73
+ }
74
+ }
75
+ export function sameValue(a, b) {
76
+ return (a.staticallyDetermined &&
77
+ b.staticallyDetermined &&
78
+ a.value !== undefined &&
79
+ a.value === b.value);
80
+ }
81
+ /** Hide all computed source and every credential-bearing statement in previews. */
82
+ export function redactConfigSource(text) {
83
+ const editor = new ConfigSourceEditor(text);
84
+ const source = editor.source;
85
+ if (source.syntax !== 'valid' || !source.object)
86
+ return '[SOURCE REDACTED]';
87
+ const spans = source.propertyOccurrences.filter((p) => /secret|token|password|credential|client_id|authorization|url/i.test(p.key) ||
88
+ (!p.staticallyDetermined &&
89
+ !['ObjectExpression', 'ArrayExpression'].includes(p.nodeType)));
90
+ const outer = spans.filter((p) => !spans.some((q) => q !== p && q.valueStart <= p.valueStart && q.valueEnd >= p.valueEnd));
91
+ for (const p of outer.sort((a, b) => b.valueStart - a.valueStart))
92
+ editor.replace(p.valueStart, p.valueEnd, "'[REDACTED]'");
93
+ const clean = editor.source;
94
+ return `[Module preamble redacted]\nexport default ${editor.text.slice(clean.object?.start, clean.object?.end)};`;
95
+ }
@@ -0,0 +1,43 @@
1
+ export declare const DEFAULT_CONDUCTOR_AGENT_ADDONS = "[{ use: '@bakers-build/addon-herdr' }, { use: '@bakers-build/addon-claude', alias: 'opus' }]";
2
+ /**
3
+ * A dynamic-import expression for a user-global config file, as source text.
4
+ *
5
+ * The result goes into a COMMITTED file, so it must name no path from the
6
+ * machine that generated it: an absolute path resolves nowhere on the next
7
+ * checkout, and loadMachine() swallows that failure, leaving conductor_id
8
+ * undefined with nothing erroring. BB_HOME is the relocation knob; HOME
9
+ * stays the fallback so the generated text works on a machine that sets
10
+ * neither.
11
+ */
12
+ export declare function configImportExpression(name: string): string;
13
+ export type ConnectionAuthType = 'oauth2_client_credentials' | 'oauth2_device_code';
14
+ /** Current connection template inputs never come from machine context. */
15
+ export interface ConnectionInputs {
16
+ baseUrl: string;
17
+ clientId: string;
18
+ /** Absent for `oauth2_device_code`: a public client holds no secret. */
19
+ secret?: string;
20
+ workspaceId?: string;
21
+ authType: ConnectionAuthType;
22
+ }
23
+ export declare function renderGlobalConnection(inputs: ConnectionInputs): string;
24
+ export declare function renderHostMachine(inputs: {
25
+ machineId: string;
26
+ userId: string;
27
+ maxParallel: number;
28
+ }): string;
29
+ export declare function renderCommittedConfig(inputs: {
30
+ project: string;
31
+ bakersWorkspace?: string;
32
+ /**
33
+ * The operator's own answer for each lifecycle hook — never invented here.
34
+ * `bakers-cli` names no provisioning tool: an absent or empty command omits
35
+ * that hook's key entirely, and an all-absent `hooks` omits the whole block
36
+ * rather than writing an empty one, because an empty `hooks: {}` would
37
+ * state a decision no one made.
38
+ */
39
+ hooks?: {
40
+ after_create?: string;
41
+ after_done?: string;
42
+ };
43
+ }): string;
@@ -0,0 +1,130 @@
1
+ export const DEFAULT_CONDUCTOR_AGENT_ADDONS = "[{ use: '@bakers-build/addon-herdr' }, { use: '@bakers-build/addon-claude', alias: 'opus' }]";
2
+ /**
3
+ * A dynamic-import expression for a user-global config file, as source text.
4
+ *
5
+ * The result goes into a COMMITTED file, so it must name no path from the
6
+ * machine that generated it: an absolute path resolves nowhere on the next
7
+ * checkout, and loadMachine() swallows that failure, leaving conductor_id
8
+ * undefined with nothing erroring. BB_HOME is the relocation knob; HOME
9
+ * stays the fallback so the generated text works on a machine that sets
10
+ * neither.
11
+ */
12
+ export function configImportExpression(name) {
13
+ return `\`\${process.env.BB_HOME ?? \`\${process.env.HOME}/.bakers\`}/${name}\``;
14
+ }
15
+ export function renderGlobalConnection(inputs) {
16
+ // The device-code grant has no client_secret in @dropsh/plugin-oauth2's
17
+ // config union, so the key is omitted rather than rendered undefined
18
+ // (BB-496), and it carries device_authorization_url instead.
19
+ const baseUrl = inputs.baseUrl.replace(/\/$/, '');
20
+ // The client-credentials branch renders byte-identical to the pre-BB-496
21
+ // template (a single-quoted `type:` literal): only the device-code branch,
22
+ // which is new, uses JSON.stringify for its type value.
23
+ const type = inputs.authType === 'oauth2_device_code'
24
+ ? JSON.stringify(inputs.authType)
25
+ : "'oauth2_client_credentials'";
26
+ const credential = inputs.authType === 'oauth2_device_code'
27
+ ? `client_id: ${JSON.stringify(inputs.clientId)},
28
+ device_authorization_url: ${JSON.stringify(`${baseUrl}/oauth/device_authorization`)},`
29
+ : `client_id: ${JSON.stringify(inputs.clientId)}, client_secret: ${JSON.stringify(inputs.secret ?? '')},`;
30
+ return `// @bakers-schema-version 4
31
+ // Global bakers.build connection: credentials and optional legacy workspace. Keep this file private.
32
+ export default {
33
+ schema_version: 4,
34
+ site: { base_url: ${JSON.stringify(inputs.baseUrl)}, jsonapi_prefix: '/jsonapi' },
35
+ ${inputs.workspaceId ? ` workspace_id: ${JSON.stringify(inputs.workspaceId)},\n` : ''}
36
+ plugins: [
37
+ { plugin: '@bakers-build/addon-essentials' },
38
+ { plugin: '@dropsh/plugin-oauth2', export: 'oauth2Plugin', with: {
39
+ id: 'bakers', default: true, type: ${type}, scope: 'bb:session',
40
+ ${credential}
41
+ token_url: ${JSON.stringify(`${baseUrl}/oauth/token`)},
42
+ } },
43
+ ],
44
+ };
45
+ `;
46
+ }
47
+ export function renderHostMachine(inputs) {
48
+ return `// @bakers-schema-version 2
49
+ // Local host identity and capacity. Server credentials belong to bakers.config.js.
50
+ export default {
51
+ schema_version: 2,
52
+ machine_id: ${JSON.stringify(inputs.machineId)},
53
+ user_id: ${JSON.stringify(inputs.userId)},
54
+ max_parallel: ${inputs.maxParallel},
55
+ };
56
+ `;
57
+ }
58
+ export function renderCommittedConfig(inputs) {
59
+ return `// Canonical bakers.build conductor engine configuration — committed.
60
+ // Host identity comes from machine.config.js; the optional local override
61
+ // composes this project's conductor identity. Connections belong to bakers.config.js.
62
+ //
63
+ // IMPORT-FREE (BB-78): the plugin slots are \`{ plugin, with }\` descriptors
64
+ // naming the REAL published package, not \`import\`ed constructors.
65
+ // loadConductorConfig resolves each name ESLint-style (config dir → cwd →
66
+ // conductor install). Every addon package default-exports its single factory, so
67
+ // no slot needs an \`export:\` — EXCEPT the merged herdr workspace
68
+ // (\`@bakers-build/addon-herdr\` default-exports the executor, so the workspace names
69
+ // \`export: 'herdrWorkspace'\` — BB-139). BB-224 replaced the old
70
+ // \`{ plugin: '@bakers-build/bakers/plugins', export: 'drupalRemote' }\` host-barrel
71
+ // descriptor with the real addon package \`@bakers-build/addon-remote-drupal\`.
72
+
73
+ // The user-global machine context: host identity and capacity, shared
74
+ // by every project on this machine. Never committed.
75
+ async function loadMachine() {
76
+ try {
77
+ return (await import(${configImportExpression('machine.config.js')})).default ?? {};
78
+ } catch {}
79
+ return {};
80
+ }
81
+
82
+ // OPTIONAL per-project override — create conductor.config.local.js beside this
83
+ // file to override machine-local values (conductor_id, model, …).
84
+ async function loadLocal() {
85
+ try { return (await import('./conductor.config.local.js')).default ?? {}; } catch {}
86
+ return {};
87
+ }
88
+
89
+ const machine = await loadMachine();
90
+ const local = await loadLocal();
91
+ const project = ${JSON.stringify(inputs.project)};
92
+ const composedConductorId =
93
+ machine.user_id && machine.machine_id
94
+ ? \`\${machine.user_id}-\${machine.machine_id}-\${project}\`
95
+ : undefined;
96
+
97
+ export default {
98
+ bakers: { project: ${JSON.stringify(inputs.project)}${inputs.bakersWorkspace ? `, workspace: ${JSON.stringify(inputs.bakersWorkspace)}` : ''} },
99
+ // Registration key (BB-353 renamed from machine_id). Host capacity lives on
100
+ // machine.config.js as max_parallel — do not set max_parallel here.
101
+ conductor_id: local.conductor_id ?? composedConductorId,
102
+ // Set to false to keep this conductor's transcripts out of the log store
103
+ // (e.g. large, uninteresting, or subject to a local policy). Default true;
104
+ // metrics are unaffected either way.
105
+ capture_logs: true,
106
+ ${inputs.hooks?.after_create || inputs.hooks?.after_done
107
+ ? ` // Lifecycle hooks are executor-owned (BB-84): they live at the config top
108
+ // level — NOT on a plugin descriptor's \`with.hooks\`.
109
+ hooks: { ${[
110
+ inputs.hooks.after_create
111
+ ? `after_create: ${JSON.stringify(inputs.hooks.after_create)}`
112
+ : '',
113
+ inputs.hooks.after_done
114
+ ? `after_done: ${JSON.stringify(inputs.hooks.after_done)}`
115
+ : '',
116
+ ]
117
+ .filter(Boolean)
118
+ .join(', ')} },\n`
119
+ : ''} remote: { plugin: '@bakers-build/addon-remote-drupal' },
120
+ executor: { plugin: '@bakers-build/addon-herdr' },
121
+ addons: ${DEFAULT_CONDUCTOR_AGENT_ADDONS},
122
+ // BB-139: the herdr workspace ships in @bakers-build/addon-herdr, which
123
+ // default-exports the EXECUTOR — so the workspace slot names export: 'herdrWorkspace'.
124
+ workspace: {
125
+ plugin: '@bakers-build/addon-herdr',
126
+ export: 'herdrWorkspace',
127
+ },
128
+ };
129
+ `;
130
+ }