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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ import { type CommandDescriptor } from '@bakers-build/core';
2
+ import { Command } from 'commander';
3
+ /** The built-in command registry. `$BB_COMMANDS` (JSON array of descriptors)
4
+ * overrides it wholesale. */
5
+ export declare const DEFAULT_COMMANDS: CommandDescriptor[];
6
+ /** Resolve the registry: `$BB_COMMANDS` override else the built-in set. */
7
+ export declare function resolveCommands(): CommandDescriptor[];
8
+ export { resolveCliVersion } from '@bakers-build/core';
9
+ /** Host bases for ESLint-style sibling resolution: the meta install → cwd. */
10
+ export declare function hostBases(): string[];
11
+ /**
12
+ * Value-aware argv pre-scan: find the first positional token that names a
13
+ * registered command. Skips the host's value-taking global (`--conductor <v>`)
14
+ * and maps `help <name>` → `<name>`. Returns undefined for `--help`, an unknown
15
+ * first positional, or a bare invocation.
16
+ */
17
+ export declare function scanTarget(argv: string[], names: string[]): string | undefined;
18
+ /** Build the top-level host program, mounting only the target command (the rest
19
+ * are describe-only stubs). */
20
+ export declare function buildHostProgram(argv: string[], opts?: {
21
+ forceTarget?: string;
22
+ bases?: string[];
23
+ }): Promise<Command>;
24
+ /** Parse argv against the host program, mapping commander's clean exits. */
25
+ export declare function runHost(argv: string[], opts?: {
26
+ forceTarget?: string;
27
+ bases?: string[];
28
+ }): Promise<void>;
29
+ /** The `bakers` entrypoint: drop node + script path, run the host. */
30
+ export declare function main(argv: string[]): Promise<void>;
@@ -0,0 +1,239 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import { commands as agentsCommands } from '@bakers-build/addon-agents/preset';
3
+ import { commands as deploymentCommands } from '@bakers-build/addon-deployment/preset';
4
+ import { commands as dropshCommands } from '@bakers-build/addon-dropsh/preset';
5
+ import { commands as infoCommands } from '@bakers-build/addon-info/preset';
6
+ import { commands as portsCommands } from '@bakers-build/addon-ports/preset';
7
+ import { commands as trackCommands } from '@bakers-build/addon-track/preset';
8
+ import { commands as conductorCommands } from '@bakers-build/conductor/preset';
9
+ import { resolveCliVersion, resolveConfigPath, resolveModuleEslintStyle, } from '@bakers-build/core';
10
+ import { commands as uiCommands } from '@bakers-build/ui/preset';
11
+ import { Command } from 'commander';
12
+ import { registerConfig } from './config.js';
13
+ import { registerUpdate } from './update.js';
14
+ import { registerValidate } from './validate.js';
15
+ // BB-201: the `bakers` plugin HOST. The meta package `@bakers-build/bakers` owns the
16
+ // CLI entrypoint (AC-1); ui / conductor / dropsh / deployment are declared,
17
+ // ESLint-style resolved, LAZILY mounted command plugins (AC-2). The host
18
+ // discovers + mounts; it hard-wires nothing. Only the invoked subcommand's module
19
+ // is imported — `bakers --help` lists all four via describe-only stubs and imports
20
+ // none; `bakers oven …` imports only the conductor module.
21
+ //
22
+ // BB-215: the registry is DERIVED from the command surface — the command
23
+ // addons' preset `commands` accumulators — rather than a hand-written list.
24
+ // Those presets are LIGHT (they import no runtime, only return descriptors), so
25
+ // composing them at boot keeps `bakers --help` import-light. `$BB_COMMANDS`
26
+ // still overrides the whole registry. BB-224 (Finding 7) added the fourth
27
+ // command surface, `@bakers-build/addon-deployment` (`bakers deployment …`), which the
28
+ // `conductor` command plugin used to register as a side effect.
29
+ /** Compose the built-in command registry from the command addons' presets. The
30
+ * accumulators are synchronous descriptor builders (light presets), threaded in
31
+ * the conductor → ui → dropsh → deployment order. */
32
+ function composeCommandSurface() {
33
+ const accumulators = [
34
+ conductorCommands,
35
+ uiCommands,
36
+ dropshCommands,
37
+ deploymentCommands,
38
+ agentsCommands,
39
+ infoCommands,
40
+ portsCommands,
41
+ trackCommands,
42
+ ];
43
+ let acc = [];
44
+ for (const fn of accumulators) {
45
+ if (typeof fn === 'function')
46
+ acc = fn(acc, undefined);
47
+ }
48
+ return acc;
49
+ }
50
+ /** The built-in command registry. `$BB_COMMANDS` (JSON array of descriptors)
51
+ * overrides it wholesale. */
52
+ export const DEFAULT_COMMANDS = composeCommandSurface();
53
+ /** Resolve the registry: `$BB_COMMANDS` override else the built-in set. */
54
+ export function resolveCommands() {
55
+ const override = process.env.BB_COMMANDS;
56
+ if (override && override.trim() !== '') {
57
+ const parsed = JSON.parse(override);
58
+ if (!Array.isArray(parsed)) {
59
+ throw new Error('$BB_COMMANDS must be a JSON array of command descriptors');
60
+ }
61
+ return parsed;
62
+ }
63
+ return DEFAULT_COMMANDS;
64
+ }
65
+ export { resolveCliVersion } from '@bakers-build/core';
66
+ /** Host bases for ESLint-style sibling resolution: the meta install → cwd. */
67
+ export function hostBases() {
68
+ return [import.meta.url, pathToFileURL(`${process.cwd()}/`).href];
69
+ }
70
+ /** Load a command descriptor's `BbCommandPlugin` (default export, or the named
71
+ * `export`), resolving the module over the host bases. */
72
+ async function loadCommandPlugin(entry, bases) {
73
+ const resolved = resolveModuleEslintStyle(entry.plugin, bases);
74
+ if (resolved === undefined) {
75
+ throw new Error(`bakers: cannot resolve command plugin '${entry.plugin}' (command '${entry.name}')`);
76
+ }
77
+ const mod = (await import(pathToFileURL(resolved).href));
78
+ const factory = entry.export ? mod[entry.export] : mod.default;
79
+ if (factory === undefined || factory === null) {
80
+ throw new Error(`bakers: command plugin '${entry.plugin}' has no ${entry.export ?? 'default'} export`);
81
+ }
82
+ return factory;
83
+ }
84
+ /**
85
+ * Value-aware argv pre-scan: find the first positional token that names a
86
+ * registered command. Skips the host's value-taking global (`--conductor <v>`)
87
+ * and maps `help <name>` → `<name>`. Returns undefined for `--help`, an unknown
88
+ * first positional, or a bare invocation.
89
+ */
90
+ export function scanTarget(argv, names) {
91
+ for (let i = 0; i < argv.length; i++) {
92
+ const a = argv[i];
93
+ if (a === '--conductor') {
94
+ i++; // skip its value
95
+ continue;
96
+ }
97
+ if (a === 'help') {
98
+ const next = argv[i + 1];
99
+ return next !== undefined && names.includes(next) ? next : undefined;
100
+ }
101
+ if (a.startsWith('-'))
102
+ continue; // any other flag
103
+ return names.includes(a) ? a : undefined;
104
+ }
105
+ return undefined;
106
+ }
107
+ /** Add a describe-only stub for a not-loaded command so `--help` stays complete.
108
+ * If it is ever dispatched (a mis-scan), its action self-loads the real plugin
109
+ * and re-runs the host forcing that target. */
110
+ function addStub(program, entry, bases, argv) {
111
+ program
112
+ .command(entry.name)
113
+ .description(pluginDescribe(entry))
114
+ .allowUnknownOption(true)
115
+ .allowExcessArguments(true)
116
+ .helpOption(false)
117
+ .action(async () => {
118
+ await runHost(argv, { forceTarget: entry.name, bases });
119
+ });
120
+ }
121
+ /** Best-effort describe text for a stub without importing the plugin. BB-215:
122
+ * the descriptor now carries its own `describe` (from the addon's preset); this
123
+ * falls back only for a `$BB_COMMANDS` entry that omits it. */
124
+ function pluginDescribe(entry) {
125
+ if (typeof entry.describe === 'string' && entry.describe.trim() !== '') {
126
+ return entry.describe;
127
+ }
128
+ switch (entry.name) {
129
+ case 'oven':
130
+ return 'node-agent lifecycle + local registry';
131
+ case 'ui':
132
+ return 'interactive terminal UI: dashboard, projects, tickets';
133
+ case 'dropsh':
134
+ return 'JSON:API shell (dropsh) bound to the bakers.build connection config';
135
+ case 'deployment':
136
+ return 'release / deployment helpers';
137
+ default:
138
+ return `${entry.name} command`;
139
+ }
140
+ }
141
+ /** Build the top-level host program, mounting only the target command (the rest
142
+ * are describe-only stubs). */
143
+ export async function buildHostProgram(argv, opts = {}) {
144
+ const bases = opts.bases ?? hostBases();
145
+ const commands = resolveCommands();
146
+ const names = commands.map((c) => c.name);
147
+ const target = opts.forceTarget ?? scanTarget(argv, names);
148
+ const projectContext = target === 'ui'
149
+ ? await import('@bakers-build/conductor/project-context')
150
+ : undefined;
151
+ const host = {
152
+ resolveBases: bases,
153
+ workspacePlugin: async (workspaceId) => {
154
+ const { workspacePlugin } = await import('@bakers-build/addon-workspace');
155
+ return workspacePlugin({ workspace_id: workspaceId });
156
+ },
157
+ ...(projectContext
158
+ ? {
159
+ projectContext: {
160
+ resolveConfigPath: (cwd) => resolveConfigPath(undefined, cwd),
161
+ readWorkspace: (configPath) => projectContext.readBbProjectSelection(configPath).workspace,
162
+ resolveContext: async (configPath, connection) => {
163
+ const { createBbConnectionClient } = await import('@bakers-build/core');
164
+ const { resolveBbProjectContext } = await import('@bakers-build/conductor/contract');
165
+ const selection = projectContext.readBbProjectSelection(configPath);
166
+ return resolveBbProjectContext(await createBbConnectionClient(connection), selection);
167
+ },
168
+ writeWorkspace: async (configPath, name) => {
169
+ await projectContext.writeBbWorkspaceSelection(projectContext.bakersProjectOwner(configPath), name, `Set bakers.workspace in ${configPath} to switch manually.`);
170
+ },
171
+ },
172
+ }
173
+ : {}),
174
+ };
175
+ const program = new Command();
176
+ // exitOverride so commander surfaces help/version/unknown-command as thrown
177
+ // CommanderErrors (mapped in runHost) instead of calling process.exit —
178
+ // otherwise an unknown command would kill the host process directly.
179
+ program.exitOverride();
180
+ program
181
+ .name('bakers')
182
+ .description('bakers.build conductor + client CLI')
183
+ .option('--conductor <name>', 'select a conductor by stem (conductor→conductor.config.js, else <name>.conductor.config.js; env $BB_CONDUCTOR)');
184
+ // The global --conductor flag threads into $BB_CONDUCTOR (flag wins over env)
185
+ // so every command selects the named engine config without per-command wiring.
186
+ program.hook('preAction', () => {
187
+ const name = program.opts().conductor;
188
+ if (typeof name === 'string' && name !== '') {
189
+ process.env.BB_CONDUCTOR = name;
190
+ }
191
+ });
192
+ const cliVersion = resolveCliVersion();
193
+ program.version(cliVersion); // -V, --version
194
+ program
195
+ .command('version')
196
+ .description('output the bakers CLI version')
197
+ .action(() => {
198
+ console.log(cliVersion);
199
+ });
200
+ registerConfig(program, { host });
201
+ registerUpdate(program);
202
+ registerValidate(program);
203
+ for (const entry of commands) {
204
+ if (entry.name === target) {
205
+ const plugin = await loadCommandPlugin(entry, bases);
206
+ await plugin.register(program, host);
207
+ }
208
+ else {
209
+ addStub(program, entry, bases, argv);
210
+ }
211
+ }
212
+ return program;
213
+ }
214
+ /** Parse argv against the host program, mapping commander's clean exits. */
215
+ export async function runHost(argv, opts = {}) {
216
+ const program = await buildHostProgram(argv, opts);
217
+ try {
218
+ await program.parseAsync(argv, { from: 'user' });
219
+ }
220
+ catch (err) {
221
+ const e = err;
222
+ if (e.code === 'commander.helpDisplayed' ||
223
+ e.code === 'commander.version') {
224
+ return;
225
+ }
226
+ if (typeof e.code === 'string' && e.code.startsWith('commander.')) {
227
+ if (!process.exitCode)
228
+ process.exitCode = 1;
229
+ return;
230
+ }
231
+ process.stderr.write(`${e.message ?? String(err)}\n`);
232
+ if (!process.exitCode)
233
+ process.exitCode = 1;
234
+ }
235
+ }
236
+ /** The `bakers` entrypoint: drop node + script path, run the host. */
237
+ export async function main(argv) {
238
+ await runHost(argv.slice(2));
239
+ }
@@ -0,0 +1,4 @@
1
+ export { basicAuthProvider } from '@bakers-build/addon-auth-basic';
2
+ export { DrupalBbRemote, drupalRemote, } from '@bakers-build/addon-remote-drupal';
3
+ export { GitWorkspace, gitWorkspace, loadInstructions, } from '@bakers-build/addon-workspace-git';
4
+ export { type AgentCandidate, type ResolvedAgent, selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from '@bakers-build/conductor/contract';
@@ -0,0 +1,16 @@
1
+ // BB-224 (Finding 6, decision 11): the `@bakers-build/bakers/plugins` BACK-COMPAT
2
+ // barrel. It used to be a single `export * from '@bakers-build/core/plugins'`; that
3
+ // core barrel is deleted, because core no longer ships plugin implementations.
4
+ // The barrel itself stays (published configs may still name
5
+ // `{ plugin: '@bakers-build/bakers/plugins', export: 'drupalRemote' }`), but it now
6
+ // aggregates the CONTRIBUTIONS from where they actually live: the impl addons +
7
+ // the conductor-surface selectors. "plugins" is the right word for a
8
+ // contribution barrel (decision 16) — only the *package* prefix became `addon-`.
9
+ //
10
+ // The fakes (`fakeRemote`/`fakeWorkspace`) are deliberately NOT re-exported:
11
+ // they moved into `@bakers-build/addon-fake`, a workspace-only test fixture that is
12
+ // never published, so the published meta package must not depend on it.
13
+ export { basicAuthProvider } from '@bakers-build/addon-auth-basic';
14
+ export { DrupalBbRemote, drupalRemote, } from '@bakers-build/addon-remote-drupal';
15
+ export { GitWorkspace, gitWorkspace, loadInstructions, } from '@bakers-build/addon-workspace-git';
16
+ export { selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from '@bakers-build/conductor/contract';
@@ -0,0 +1,15 @@
1
+ import type { Command } from 'commander';
2
+ export type SpawnUpdate = (cmd: string, args: string[]) => Promise<number>;
3
+ export type SpawnCapture = (cmd: string, args: string[]) => Promise<{
4
+ code: number;
5
+ stdout: string;
6
+ }>;
7
+ export interface UpdateOptions {
8
+ out?: (line: string) => void;
9
+ }
10
+ export declare function runUpdate(run?: SpawnUpdate, query?: SpawnCapture, options?: UpdateOptions): Promise<{
11
+ ok: boolean;
12
+ before: string | null;
13
+ after: string | null;
14
+ }>;
15
+ export declare function registerUpdate(program: Command): void;
@@ -0,0 +1,59 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { BB_PACKAGE } from '@bakers-build/core';
3
+ const spawnUpdate = (cmd, args) => new Promise((resolve) => {
4
+ const child = spawn(cmd, args, { stdio: 'inherit', shell: false });
5
+ child.on('error', () => resolve(1));
6
+ child.on('close', (code) => resolve(code ?? 1));
7
+ });
8
+ const capture = (cmd, args) => new Promise((resolve) => {
9
+ let stdout = '';
10
+ const child = spawn(cmd, args, {
11
+ stdio: ['ignore', 'pipe', 'ignore'],
12
+ shell: false,
13
+ });
14
+ child.stdout?.on('data', (chunk) => {
15
+ stdout += String(chunk);
16
+ });
17
+ child.on('error', () => resolve({ code: 1, stdout }));
18
+ child.on('close', (code) => resolve({ code: code ?? 1, stdout }));
19
+ });
20
+ async function installedVersion(run) {
21
+ try {
22
+ const result = await run('npm', [
23
+ 'ls',
24
+ '-g',
25
+ BB_PACKAGE,
26
+ '--json',
27
+ '--depth',
28
+ '0',
29
+ ]);
30
+ if (result.code !== 0)
31
+ return null;
32
+ const parsed = JSON.parse(result.stdout);
33
+ const version = parsed.dependencies?.[BB_PACKAGE]?.version;
34
+ return typeof version === 'string' ? version : null;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ export async function runUpdate(run = spawnUpdate, query = capture, options = {}) {
41
+ const before = await installedVersion(query);
42
+ if ((await run('npm', ['install', '-g', `${BB_PACKAGE}@latest`])) !== 0)
43
+ return { ok: false, before, after: before };
44
+ const after = await installedVersion(query);
45
+ if (after === null)
46
+ return { ok: false, before, after };
47
+ (options.out ?? console.log)(`${before ?? 'unknown'} → ${after}`);
48
+ return { ok: true, before, after };
49
+ }
50
+ export function registerUpdate(program) {
51
+ program
52
+ .command('update')
53
+ .description('install the latest bakers CLI')
54
+ .action(async () => {
55
+ const result = await runUpdate();
56
+ if (!result.ok)
57
+ process.exitCode = 1;
58
+ });
59
+ }
@@ -0,0 +1,78 @@
1
+ import { type EmptyLoaderShape, type SkillContract, type StepContractErrorCode } from '@bakers-build/core';
2
+ import type { Command } from 'commander';
3
+ /** A `SKILL.md` whose frontmatter is not a readable skill contract. */
4
+ export interface SkillReadFailure {
5
+ path: string;
6
+ reason: string;
7
+ }
8
+ /** What the skill scan found: the name index, plus what it could not read. */
9
+ export interface SkillIndex {
10
+ byName: Map<string, SkillContract>;
11
+ /** Paths that parsed, in scan order — for the green summary's count. */
12
+ indexed: string[];
13
+ /** A later `SKILL.md` re-declaring an already-indexed name (first wins). */
14
+ shadowed: {
15
+ name: string;
16
+ path: string;
17
+ }[];
18
+ failures: SkillReadFailure[];
19
+ }
20
+ /** The verdict: `ok`, or the first contract defect with its message. */
21
+ export interface ValidateReport {
22
+ ok: boolean;
23
+ workflowPath: string;
24
+ roots: string[];
25
+ index: SkillIndex;
26
+ /** Loaded leaf skills — owners plus helpers — when the load got that far. */
27
+ loaded?: {
28
+ owners: string[];
29
+ helpers: string[];
30
+ };
31
+ tripleCount?: number;
32
+ error?: string;
33
+ /**
34
+ * The contract defect's classification, when core tagged one. BB-436: this is
35
+ * the seam `formatReport` branches on — it replaced a regex over core's message
36
+ * text, which matched only one of the two throw sites that need a hint and would
37
+ * have broken silently the next time a message was reworded.
38
+ */
39
+ errorCode?: StepContractErrorCode;
40
+ /** For `unresolved_skill`: the skill name the load could not resolve. */
41
+ unresolvedSkill?: string;
42
+ /**
43
+ * For `empty_loader`: what the `## Loaded skills` section actually holds. Core
44
+ * raises that code for three shapes and its message can only describe one of
45
+ * them, so the shape is measured here and `formatReport` prints the remedy that
46
+ * fits the document rather than the one that fits the common case.
47
+ */
48
+ emptyLoader?: EmptyLoaderShape;
49
+ }
50
+ /**
51
+ * Index every skill reachable from `roots` by its declared `name` — the key a
52
+ * `WORKFLOW.md` bullet resolves on, whatever namespace prefix the bullet wears.
53
+ *
54
+ * A `SKILL.md` that is not a readable bakers.build skill contract is RECORDED rather than
55
+ * thrown on: a project's skill tree legitimately holds third-party skills that
56
+ * declare no bakers.build frontmatter, and failing the whole validation over one of them
57
+ * would make the command useless exactly where it is most needed. The record is
58
+ * printed only when a bullet then fails to resolve, where it is the likely cause.
59
+ */
60
+ export declare function indexSkills(roots: string[]): SkillIndex;
61
+ export interface ValidateOptions {
62
+ /** A project directory, or a loader document (`WORKFLOW.md`) directly. */
63
+ path?: string | undefined;
64
+ /** Project root for default skill-root discovery; defaults from `path`. */
65
+ project?: string | undefined;
66
+ /** Extra skill roots, in precedence order before the defaults. */
67
+ skills?: string[] | undefined;
68
+ }
69
+ /**
70
+ * Validate a project's loader: read `WORKFLOW.md`, index the skills it can reach,
71
+ * expand the load, derive the project's triple set, and enforce the contract.
72
+ *
73
+ * Returns the verdict rather than throwing, so the caller owns the exit code.
74
+ */
75
+ export declare function runValidate(options?: ValidateOptions): ValidateReport;
76
+ /** Render the report for a terminal. Returns the lines, so tests can read them. */
77
+ export declare function formatReport(report: ValidateReport): string[];
78
+ export declare function registerValidate(program: Command): void;