@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 keytec GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Temporary Holding Version
1
+ # @bakers-build/ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ bakers.build project-first cockpit: the `bakers ui` command plugin (renderer resolved dynamically).
4
+
5
+ Part of the bakers.build CLI. Install the meta package `@bakers-build/bakers` to get the `bakers` CLI with all addons. Source: https://git.key-tec.de/bakers-build/bakers-build (bakers-cli/).
@@ -0,0 +1,84 @@
1
+ /**
2
+ * BB-326 — `@opentui/core` reaches its Zig core through FFI, which Node does
3
+ * not expose before 26.1 (and then only behind `--experimental-ffi`). So the
4
+ * cockpit runs under the `bun` that `@bakers-build/addon-bakers-ui` installs.
5
+ *
6
+ * `process.versions.bun` is the loop guard: it is defined under Bun and nowhere
7
+ * else, so a re-exec can never recurse and no sentinel variable is introduced.
8
+ * `--print-config` deliberately stays on Node — it is what a human runs when the
9
+ * cockpit will not start, and a diagnostic that needs the thing it diagnoses is
10
+ * worth nothing.
11
+ *
12
+ * `injectedRenderer` is the same argument one step further: the runtime is
13
+ * needed by the DYNAMICALLY-resolved `@bakers-build/addon-bakers-ui`, so a caller that
14
+ * supplies its own `runUi` never reaches OpenTUI and must not be re-execed.
15
+ */
16
+ export declare function shouldReExecUnderBun(input: {
17
+ bunVersion?: string | undefined;
18
+ noBun?: string | undefined;
19
+ printConfig: boolean;
20
+ injectedRenderer?: boolean | undefined;
21
+ }): boolean;
22
+ /**
23
+ * Resolve the `bun` executable from the renderer addon's own base — the same
24
+ * dynamic seam `loadBbUiModule` resolves the renderer through, so the one
25
+ * dynamic edge in this package stays one edge.
26
+ *
27
+ * The manifest is read with a plain `readFileSync` rather than through the
28
+ * resolver: `createRequire(...).resolve()` gives an absolute path, and reading a
29
+ * known path needs no module system. `bun`'s manifest is resolved by name
30
+ * (`bun/package.json`) because the launcher's location is the thing we are
31
+ * looking up; its `bin` entry is a relative path from the manifest's directory.
32
+ */
33
+ export declare function resolveBunBinary(bases: string[]): string | undefined;
34
+ export type LibcFamily = 'glibc' | 'musl' | 'unknown';
35
+ /**
36
+ * BB-326 (A1) — which C library is this host running?
37
+ *
38
+ * Two evidence sources, cheapest first, both injected so the answer never
39
+ * depends on the machine the test runs on:
40
+ *
41
+ * - `/proc/self/maps` (~0.1 ms) names the interpreter the kernel ACTUALLY
42
+ * mapped into this process. That is the strongest signal available: a glibc
43
+ * box with a musl-compat package installed has `/lib/ld-musl-x86_64.so.1` on
44
+ * disk but never maps it, so the `existsSync` shortcut would misread it.
45
+ * - the node report's `header.glibcVersionRuntime` (~4.6 ms) — present on
46
+ * glibc, absent on musl. It needs no filesystem, which is why it is the
47
+ * fallback for a container that hides `/proc`. Verified on all four
48
+ * combinations: node+glibc `"2.43"`, bun+glibc `"2.43"`, node+musl absent,
49
+ * bun+musl absent (node:22-alpine).
50
+ *
51
+ * Non-Linux returns `unknown` and NOT `musl`: the report header omits
52
+ * `glibcVersionRuntime` on macOS and Windows too, so reading its absence as
53
+ * musl would mislabel every mac. musl is a Linux question.
54
+ *
55
+ * Inconclusive is `unknown`, and `unknown` changes nothing — glibc is upstream's
56
+ * default and the overwhelmingly common case, so a failed probe must degrade to
57
+ * today's behaviour rather than to a guess.
58
+ */
59
+ export declare function detectLibcFamily(probe: {
60
+ platform: string;
61
+ readProcSelfMaps: () => string | undefined;
62
+ readReportLibc: () => LibcFamily;
63
+ }): LibcFamily;
64
+ /** The real probe. Every leg is wrapped: an inconclusive read must not throw. */
65
+ export declare function nodeLibcFamily(): LibcFamily;
66
+ /**
67
+ * BB-326 (A1) — make the cockpit start on musl with no manual step.
68
+ *
69
+ * `@opentui/core@0.5.1` selects its native platform package from
70
+ * `process.env.OPENTUI_LIBC` and DEFAULTS TO GLIBC — it runs no probe of its
71
+ * own. Both prebuilts are always on disk (npm and pnpm filter optional deps by
72
+ * `os`/`cpu`, never by libc), so on Alpine the wrong one loads and the first
73
+ * frame dies with `Error loading shared library ld-linux-x86-64.so.2 (needed by
74
+ * libopentui.so)`. Requiring the user to export the variable IS a manual step,
75
+ * so A1 is only met once we set it.
76
+ *
77
+ * An explicit value always wins: an operator who exports `OPENTUI_LIBC=glibc`
78
+ * on a musl box is doing something deliberate, and a probe must not overrule
79
+ * them. Because a set value short-circuits BEFORE `detect` is called, the Bun
80
+ * child — which inherits this env — never pays for the probe a second time.
81
+ */
82
+ export declare function applyOpentuiLibc(env: NodeJS.ProcessEnv, detect?: () => LibcFamily): void;
83
+ /** Replace this process with the same command under Bun. Never returns. */
84
+ export declare function reExecUnderBun(bin: string, argv: string[]): never;
@@ -0,0 +1,149 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join } from 'node:path';
5
+ /**
6
+ * BB-326 — `@opentui/core` reaches its Zig core through FFI, which Node does
7
+ * not expose before 26.1 (and then only behind `--experimental-ffi`). So the
8
+ * cockpit runs under the `bun` that `@bakers-build/addon-bakers-ui` installs.
9
+ *
10
+ * `process.versions.bun` is the loop guard: it is defined under Bun and nowhere
11
+ * else, so a re-exec can never recurse and no sentinel variable is introduced.
12
+ * `--print-config` deliberately stays on Node — it is what a human runs when the
13
+ * cockpit will not start, and a diagnostic that needs the thing it diagnoses is
14
+ * worth nothing.
15
+ *
16
+ * `injectedRenderer` is the same argument one step further: the runtime is
17
+ * needed by the DYNAMICALLY-resolved `@bakers-build/addon-bakers-ui`, so a caller that
18
+ * supplies its own `runUi` never reaches OpenTUI and must not be re-execed.
19
+ */
20
+ export function shouldReExecUnderBun(input) {
21
+ if (input.printConfig)
22
+ return false;
23
+ if (input.injectedRenderer)
24
+ return false;
25
+ if (input.bunVersion !== undefined)
26
+ return false;
27
+ if (input.noBun !== undefined && input.noBun !== '')
28
+ return false;
29
+ return true;
30
+ }
31
+ /**
32
+ * Resolve the `bun` executable from the renderer addon's own base — the same
33
+ * dynamic seam `loadBbUiModule` resolves the renderer through, so the one
34
+ * dynamic edge in this package stays one edge.
35
+ *
36
+ * The manifest is read with a plain `readFileSync` rather than through the
37
+ * resolver: `createRequire(...).resolve()` gives an absolute path, and reading a
38
+ * known path needs no module system. `bun`'s manifest is resolved by name
39
+ * (`bun/package.json`) because the launcher's location is the thing we are
40
+ * looking up; its `bin` entry is a relative path from the manifest's directory.
41
+ */
42
+ export function resolveBunBinary(bases) {
43
+ for (const base of bases) {
44
+ try {
45
+ const addon = createRequire(base).resolve('@bakers-build/addon-bakers-ui');
46
+ const manifest = createRequire(addon).resolve('bun/package.json');
47
+ const { bin } = JSON.parse(readFileSync(manifest, 'utf8'));
48
+ const relative = typeof bin === 'string' ? bin : bin?.bun;
49
+ if (relative)
50
+ return join(dirname(manifest), relative);
51
+ }
52
+ catch {
53
+ // try the next base
54
+ }
55
+ }
56
+ return undefined;
57
+ }
58
+ /**
59
+ * The dynamic linker musl installs. Matched as a path segment ending in `.so`
60
+ * rather than a bare `includes('ld-musl')`, so an unrelated mapped file whose
61
+ * NAME happens to carry the string cannot be mistaken for the interpreter.
62
+ */
63
+ const MUSL_INTERPRETER = /\/ld-musl-[^/\s]*\.so/;
64
+ /**
65
+ * BB-326 (A1) — which C library is this host running?
66
+ *
67
+ * Two evidence sources, cheapest first, both injected so the answer never
68
+ * depends on the machine the test runs on:
69
+ *
70
+ * - `/proc/self/maps` (~0.1 ms) names the interpreter the kernel ACTUALLY
71
+ * mapped into this process. That is the strongest signal available: a glibc
72
+ * box with a musl-compat package installed has `/lib/ld-musl-x86_64.so.1` on
73
+ * disk but never maps it, so the `existsSync` shortcut would misread it.
74
+ * - the node report's `header.glibcVersionRuntime` (~4.6 ms) — present on
75
+ * glibc, absent on musl. It needs no filesystem, which is why it is the
76
+ * fallback for a container that hides `/proc`. Verified on all four
77
+ * combinations: node+glibc `"2.43"`, bun+glibc `"2.43"`, node+musl absent,
78
+ * bun+musl absent (node:22-alpine).
79
+ *
80
+ * Non-Linux returns `unknown` and NOT `musl`: the report header omits
81
+ * `glibcVersionRuntime` on macOS and Windows too, so reading its absence as
82
+ * musl would mislabel every mac. musl is a Linux question.
83
+ *
84
+ * Inconclusive is `unknown`, and `unknown` changes nothing — glibc is upstream's
85
+ * default and the overwhelmingly common case, so a failed probe must degrade to
86
+ * today's behaviour rather than to a guess.
87
+ */
88
+ export function detectLibcFamily(probe) {
89
+ if (probe.platform !== 'linux')
90
+ return 'unknown';
91
+ const maps = probe.readProcSelfMaps();
92
+ if (maps !== undefined)
93
+ return MUSL_INTERPRETER.test(maps) ? 'musl' : 'glibc';
94
+ return probe.readReportLibc();
95
+ }
96
+ /** The real probe. Every leg is wrapped: an inconclusive read must not throw. */
97
+ export function nodeLibcFamily() {
98
+ return detectLibcFamily({
99
+ platform: process.platform,
100
+ readProcSelfMaps: () => {
101
+ try {
102
+ return readFileSync('/proc/self/maps', 'utf8');
103
+ }
104
+ catch {
105
+ return undefined;
106
+ }
107
+ },
108
+ readReportLibc: () => {
109
+ try {
110
+ const report = process.report.getReport();
111
+ return report.header?.glibcVersionRuntime ? 'glibc' : 'musl';
112
+ }
113
+ catch {
114
+ return 'unknown';
115
+ }
116
+ },
117
+ });
118
+ }
119
+ /**
120
+ * BB-326 (A1) — make the cockpit start on musl with no manual step.
121
+ *
122
+ * `@opentui/core@0.5.1` selects its native platform package from
123
+ * `process.env.OPENTUI_LIBC` and DEFAULTS TO GLIBC — it runs no probe of its
124
+ * own. Both prebuilts are always on disk (npm and pnpm filter optional deps by
125
+ * `os`/`cpu`, never by libc), so on Alpine the wrong one loads and the first
126
+ * frame dies with `Error loading shared library ld-linux-x86-64.so.2 (needed by
127
+ * libopentui.so)`. Requiring the user to export the variable IS a manual step,
128
+ * so A1 is only met once we set it.
129
+ *
130
+ * An explicit value always wins: an operator who exports `OPENTUI_LIBC=glibc`
131
+ * on a musl box is doing something deliberate, and a probe must not overrule
132
+ * them. Because a set value short-circuits BEFORE `detect` is called, the Bun
133
+ * child — which inherits this env — never pays for the probe a second time.
134
+ */
135
+ export function applyOpentuiLibc(env, detect = nodeLibcFamily) {
136
+ const current = env.OPENTUI_LIBC;
137
+ if (current !== undefined && current !== '')
138
+ return;
139
+ if (detect() === 'musl')
140
+ env.OPENTUI_LIBC = 'musl';
141
+ }
142
+ /** Replace this process with the same command under Bun. Never returns. */
143
+ export function reExecUnderBun(bin, argv) {
144
+ const result = spawnSync(bin, argv.slice(1), {
145
+ stdio: 'inherit',
146
+ env: process.env,
147
+ });
148
+ process.exit(result.status ?? 1);
149
+ }
@@ -0,0 +1,3 @@
1
+ export { type HereContext, isTicketIdentifier, isUuid, normalizeRepoUrl, type ParsedTarget, parseTarget, parseTicketIdentifierFromBranch, resolveHere, type TargetInputs, UI_VIEW_MODES, type UiInitialRoute, type UiViewMode, } from './route.js';
2
+ export { cmdUi, default, type RunBbUiOptions, type UiDeps } from './ui.js';
3
+ export { type HomeConnection, type ProjectRooting, resolveAgentCommand, resolveProjectRooting, } from './ui-home.js';
@@ -0,0 +1,10 @@
1
+ // BB-201 / BB-219: `@bakers-build/ui` — the `bakers ui` command plugin. Its
2
+ // connection is resolved by core's `loadBbConfig` (explicit → project
3
+ // walk-up → home → shipped fallback), so running it inside a project uses that
4
+ // project's control plane; this package owns no precedence rule of its own.
5
+ // The default export is the `BbCommandPlugin` the host mounts; `cmdUi` + the
6
+ // bootstrap helpers are exported for tests.
7
+ // BB-223: the deep-link grammar (the pure CLI seam).
8
+ export { isTicketIdentifier, isUuid, normalizeRepoUrl, parseTarget, parseTicketIdentifierFromBranch, resolveHere, UI_VIEW_MODES, } from './route.js';
9
+ export { cmdUi, default } from './ui.js';
10
+ export { resolveAgentCommand, resolveProjectRooting, } from './ui-home.js';
@@ -0,0 +1,2 @@
1
+ import type { BbPreset } from '@bakers-build/core';
2
+ export declare const commands: BbPreset['commands'];
@@ -0,0 +1,8 @@
1
+ export const commands = (acc) => [
2
+ ...acc,
3
+ {
4
+ name: 'ui',
5
+ plugin: '@bakers-build/ui',
6
+ describe: 'interactive terminal UI: dashboard, projects, tickets',
7
+ },
8
+ ];
@@ -0,0 +1,119 @@
1
+ /** How a ticket target is rendered. Mirrors the Drupal `bb_ticket` view modes. */
2
+ export type UiViewMode = 'full' | 'teaser' | 'board_card';
3
+ /** The known `--view` modes, in the order error messages list them. */
4
+ export declare const UI_VIEW_MODES: readonly UiViewMode[];
5
+ /**
6
+ * A deep-link target as the CLI understands it — *unresolved*: it says what to
7
+ * look up and how, never what screen results. Crosses both package boundaries
8
+ * (`RunBbUiOptions` → `BbUiLauncherOptions` → `TuiOptions`) and is mirrored
9
+ * structurally in the renderer, per the `HomeConnection` precedent (Finding 6).
10
+ */
11
+ export interface UiInitialRoute {
12
+ kind: 'ticket' | 'project' | 'run';
13
+ /**
14
+ * How `value` must be looked up. `'repo'` is the `--here` project rung: the
15
+ * value is a NORMALISED remote URL (`host/path`), matched client-side against
16
+ * the `bb_project.repository.url` of every project (spec Decision 3).
17
+ */
18
+ /**
19
+ * `'origin'` (BB-544) is a ticket looked up by its origin issue — `#19`,
20
+ * `bakers#19` or the issue URL — which the server's `filter[search]` resolves.
21
+ */
22
+ by: 'uuid' | 'identifier' | 'name' | 'repo' | 'origin';
23
+ value: string;
24
+ /** Drupal view mode for a ticket target; default 'full'. */
25
+ view?: UiViewMode | undefined;
26
+ /** Set when the target came from `--here`; error messages only. */
27
+ fromBranch?: string | undefined;
28
+ }
29
+ /** The raw argv inputs `parseTarget` reads (commander's parsed options). */
30
+ export interface TargetInputs {
31
+ target?: string | undefined;
32
+ ticket?: string | undefined;
33
+ project?: string | undefined;
34
+ run?: string | undefined;
35
+ here?: boolean | undefined;
36
+ view?: string | undefined;
37
+ }
38
+ /**
39
+ * What the grammar made of the argv: nothing (the dashboard), a `--here`
40
+ * request that still needs the ladder walked, or a finished route.
41
+ */
42
+ export type ParsedTarget = {
43
+ kind: 'here';
44
+ view?: UiViewMode | undefined;
45
+ } | {
46
+ kind: 'route';
47
+ route: UiInitialRoute;
48
+ };
49
+ /** True for an `8-4-4-4-12` hex UUID (case-insensitive). */
50
+ export declare function isUuid(value: string): boolean;
51
+ /** True for a `KEY-123` ticket identifier (case-insensitive). */
52
+ export declare function isTicketIdentifier(value: string): boolean;
53
+ /**
54
+ * The ticket identifier a branch name carries, uppercased, or `null`.
55
+ * Pure and offline — the `--here` ticket rung never needs the network.
56
+ */
57
+ export declare function parseTicketIdentifierFromBranch(branch: string): string | null;
58
+ /**
59
+ * The grammar: at most ONE target source (the positional, `--ticket`,
60
+ * `--project`, `--run`, `--here`) plus the orthogonal `--view` modifier.
61
+ *
62
+ * Returns `undefined` for no target at all (the dashboard, AC-4), `{kind:'here'}`
63
+ * when the ladder still has to be walked, else the finished route. Throws — with
64
+ * a message naming exactly what went wrong — on a conflict or an unparseable
65
+ * form; the caller turns that into a stderr line and exit 1 before any network
66
+ * work happens.
67
+ */
68
+ export declare function parseTarget(inputs: TargetInputs): ParsedTarget | undefined;
69
+ /**
70
+ * A git remote URL reduced to a comparable `host/path` identity: scheme,
71
+ * userinfo, port and a trailing `.git` (and trailing slashes) dropped, the whole
72
+ * thing lowercased. Returns `undefined` for anything that carries no host+path.
73
+ *
74
+ * This is REQUIRED, not cosmetic (spec Decision 3): git and the control plane
75
+ * disagree on form. Measured in this repo — `git remote get-url origin` yields
76
+ * `git@git.key-tec.de:bakers-build/bakers-build.git` while `bb_project.repository.url` holds
77
+ * `ssh://git@git.key-tec.de/bakers-build/bakers-build.git`. Both must reduce to
78
+ * `git.key-tec.de/bakers-build/bakers-build` or the `--here` project rung never matches.
79
+ *
80
+ * | raw | normalised |
81
+ * |-----------------------------------------|---------------------|
82
+ * | `git@host:bakers-build/bakers-build.git` | `host/bakers-build/bakers-build` |
83
+ * | `ssh://git@host/bakers-build/bakers-build.git` | `host/bakers-build/bakers-build` |
84
+ * | `ssh://git@host:2222/bakers-build/bakers-build.git` | `host/bakers-build/bakers-build` |
85
+ * | `https://host/bakers-build/bakers-build.git` | `host/bakers-build/bakers-build` |
86
+ */
87
+ export declare function normalizeRepoUrl(raw: string | undefined): string | undefined;
88
+ /** Everything `resolveHere` needs, all injected — two git reads, nothing else. */
89
+ export interface HereContext {
90
+ cwd: string;
91
+ view?: UiViewMode | undefined;
92
+ /** The current branch of `cwd` (git resolves it per worktree). May reject. */
93
+ readBranch: (cwd: string) => Promise<string>;
94
+ /**
95
+ * The cwd's `origin` remote URL, raw. Git resolves it per worktree too, and a
96
+ * worktree shares its main clone's remote — which is exactly the right answer
97
+ * (spec Decision 3). May reject (not a repo, no `origin`).
98
+ */
99
+ readRemoteUrl: (cwd: string) => Promise<string>;
100
+ }
101
+ /**
102
+ * `--here`: derive the target from the cwd (spec Decision 3).
103
+ *
104
+ * ```
105
+ * branch carries a ticket identifier? → that TICKET (feat/bb-223-… → BB-223)
106
+ * else: the cwd's git remote → the PROJECT(s) whose repository claims it
107
+ * else → a named error
108
+ * ```
109
+ *
110
+ * The BRANCH decides the first rung, not git mechanics: a worktree on a
111
+ * non-ticket branch and the main clone on a ticket branch both come out right.
112
+ *
113
+ * The project rung stops HERE at a normalised repo identity — matching it
114
+ * against `bb_project.repository` needs a JSON:API client, which the entry does
115
+ * not have (Finding 1), so `by: 'repo'` travels to the renderer's `resolveRoute`
116
+ * and the ambiguity ruling (one match / a picker / an error) is made there
117
+ * (Decisions 5 + 14).
118
+ */
119
+ export declare function resolveHere(ctx: HereContext): Promise<UiInitialRoute>;