@gaia-ai/ui 0.8.0 → 0.9.1
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/dist/src/bun-runtime.d.ts +84 -0
- package/dist/src/bun-runtime.js +149 -0
- package/dist/src/ui.js +33 -0
- package/package.json +3 -3
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GAIA-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 `@gaia-ai/addon-gaia-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 `@gaia-ai/addon-gaia-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 `loadGaiaUiModule` 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
|
+
* GAIA-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
|
+
* GAIA-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
|
+
* GAIA-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 `@gaia-ai/addon-gaia-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 `@gaia-ai/addon-gaia-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 `loadGaiaUiModule` 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('@gaia-ai/addon-gaia-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
|
+
* GAIA-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
|
+
* GAIA-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
|
+
}
|
package/dist/src/ui.js
CHANGED
|
@@ -5,6 +5,7 @@ import { herdrAgentHost } from '@gaia-ai/addon-herdr';
|
|
|
5
5
|
import { CommandRunner, createLogger, exec, fetchUpdateNotice, loadGaiaConfig, machineContextPath, readMachineContext, resolveCliVersion, setDefaultCommandRunner, } from '@gaia-ai/core';
|
|
6
6
|
import { buildProgram as buildDropshProgram, resolveAuth } from 'dropsh';
|
|
7
7
|
import { createHttpClient, createJsonApiClient } from 'dropsh/plugin';
|
|
8
|
+
import { applyOpentuiLibc, reExecUnderBun, resolveBunBinary, shouldReExecUnderBun, } from './bun-runtime.js';
|
|
8
9
|
import { parseTarget, resolveHere, UI_VIEW_MODES, } from './route.js';
|
|
9
10
|
import { resolveAgentCommand, resolveProjectRooting, } from './ui-home.js';
|
|
10
11
|
/** The auth profile `gaia ui` writes as. Reads stay on `session`. */
|
|
@@ -173,6 +174,38 @@ export async function cmdUi(deps = {}) {
|
|
|
173
174
|
(deps.stderr ?? ((line) => process.stderr.write(line)))(`${message}\n`);
|
|
174
175
|
process.exitCode = 1;
|
|
175
176
|
};
|
|
177
|
+
// GAIA-326 (A1) — teach OpenTUI which libc this host runs BEFORE anything
|
|
178
|
+
// else. It is set on THIS process rather than only on the re-exec's `env`
|
|
179
|
+
// argument, and that placement is the whole point: `reExecUnderBun` passes
|
|
180
|
+
// `env: process.env`, so the Bun child inherits it for free, while the three
|
|
181
|
+
// paths that never re-exec (already under Bun, `$GAIA_UI_NO_BUN` on Node 26 +
|
|
182
|
+
// --experimental-ffi, an injected `runUi`) load `@opentui/core` in THIS
|
|
183
|
+
// process and would otherwise get the glibc default on Alpine. `@opentui/core`
|
|
184
|
+
// reads the variable when it resolves its native asset — strictly later than
|
|
185
|
+
// here, since the renderer is a dynamic import — so one assignment covers
|
|
186
|
+
// every path. On glibc it is a no-op.
|
|
187
|
+
applyOpentuiLibc(process.env);
|
|
188
|
+
// GAIA-326 — the renderer needs Bun's FFI. Re-exec here rather than in a bin
|
|
189
|
+
// shim: there are three shims, the host already owns the value-aware argv
|
|
190
|
+
// pre-scan that says `ui` was invoked, and `--print-config` must keep working
|
|
191
|
+
// without Bun. This sits after `fail` is declared (the only ordering the
|
|
192
|
+
// named-error path allows) and still before every config load and the whole
|
|
193
|
+
// deep-link ladder, so nothing is resolved twice across the two processes.
|
|
194
|
+
if (shouldReExecUnderBun({
|
|
195
|
+
bunVersion: process.versions.bun,
|
|
196
|
+
noBun: process.env.GAIA_UI_NO_BUN,
|
|
197
|
+
printConfig: Boolean(deps.printConfig),
|
|
198
|
+
injectedRenderer: deps.runUi !== undefined,
|
|
199
|
+
})) {
|
|
200
|
+
const bin = resolveBunBinary(host.resolveBases);
|
|
201
|
+
if (bin === undefined) {
|
|
202
|
+
fail('gaia ui: could not resolve the bun runtime. `@gaia-ai/addon-gaia-ui` ' +
|
|
203
|
+
'depends on it; reinstall, or set $GAIA_UI_NO_BUN=1 to run on Node ' +
|
|
204
|
+
'26+ with --experimental-ffi.');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
reExecUnderBun(bin, process.argv);
|
|
208
|
+
}
|
|
176
209
|
// GAIA-223: build the deep-link route FIRST — argv parsing, the conflict rule
|
|
177
210
|
// and the whole `--here` ladder are offline, so every one of their failures
|
|
178
211
|
// costs no config load and no network round-trip, and never reaches the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaia-ai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "GAIA project-first cockpit: the `gaia ui` command plugin (renderer resolved dynamically).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"directory": "gaia-cli/ui"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@gaia-ai/core": "^0.
|
|
30
|
-
"@gaia-ai/addon-herdr": "^0.
|
|
29
|
+
"@gaia-ai/core": "^0.9.1",
|
|
30
|
+
"@gaia-ai/addon-herdr": "^0.9.1",
|
|
31
31
|
"commander": "^12.1.0",
|
|
32
32
|
"dropsh": "^0.5.8"
|
|
33
33
|
}
|