@gaia-ai/ui 0.7.0 → 0.9.0

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,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
+ }
@@ -1,3 +1,3 @@
1
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
2
  export { cmdUi, default, type RunGaiaUiOptions, type UiDeps } from './ui.js';
3
- export { type HomeConnection, type ProjectRooting, resolveAgentCommand, resolveDefaultProject, resolveProjectRooting, } from './ui-home.js';
3
+ export { type HomeConnection, type ProjectRooting, resolveAgentCommand, resolveProjectRooting, } from './ui-home.js';
package/dist/src/index.js CHANGED
@@ -7,4 +7,4 @@
7
7
  // GAIA-223: the deep-link grammar (the pure CLI seam).
8
8
  export { isTicketIdentifier, isUuid, normalizeRepoUrl, parseTarget, parseTicketIdentifierFromBranch, resolveHere, UI_VIEW_MODES, } from './route.js';
9
9
  export { cmdUi, default } from './ui.js';
10
- export { resolveAgentCommand, resolveDefaultProject, resolveProjectRooting, } from './ui-home.js';
10
+ export { resolveAgentCommand, resolveProjectRooting, } from './ui-home.js';
@@ -1,4 +1,4 @@
1
- import { type ConductorRegistryEntry, type MachineContext } from '@gaia-ai/core';
1
+ import { type MachineContext } from '@gaia-ai/core';
2
2
  /**
3
3
  * The resolved control-plane connection passed to the renderer launcher.
4
4
  * A structural mirror of `@gaia-ai/addon-gaia-ui`'s `HomeConnection` — kept
@@ -20,12 +20,6 @@ export interface HomeConnection {
20
20
  */
21
21
  writeProfile?: string | undefined;
22
22
  }
23
- /**
24
- * Resolve the default project (opaque group) from the conductor registry: the
25
- * `preferred` project when it is registered, else the first entry's project.
26
- * `undefined` when the registry is empty — a valid empty cockpit.
27
- */
28
- export declare function resolveDefaultProject(entries: ConductorRegistryEntry[], preferred?: string): string | undefined;
29
23
  /**
30
24
  * Resolve the agent-launch command builder from the home config. Turns a TUI
31
25
  * prompt into the shell command herdr runs. Defaults to `claude` (an empty
@@ -38,11 +32,18 @@ export declare function resolveAgentCommand(machine: Partial<MachineContext> & {
38
32
  /** Where `gaia ui` roots the agents it launches, and which project they belong to. */
39
33
  export interface ProjectRooting {
40
34
  cwd: string;
41
- project?: string | undefined;
42
- ownConductorMachineId?: string | undefined;
35
+ /**
36
+ * The `.gaia` dir of the project we are standing in, when there is one.
37
+ *
38
+ * The ui hands this over rather than a resolved project name: matching it
39
+ * against a conductor's `workspace_root` needs the control plane, and the
40
+ * client that reaches it first exists in the renderer (GAIA-232). Undefined
41
+ * outside any project.
42
+ */
43
+ gaiaDir?: string | undefined;
43
44
  }
44
45
  /**
45
- * Decide the agents' working directory + project (GAIA-219 AC-9).
46
+ * Decide the agents' working directory (GAIA-219 AC-9).
46
47
  *
47
48
  * The trigger is the **cwd**, deliberately not the winning connection config:
48
49
  * "which connection did we resolve" and "which project am I standing in" are
@@ -50,17 +51,22 @@ export interface ProjectRooting {
50
51
  * silently relocate the agents' working directory.
51
52
  *
52
53
  * Standing **in** a project (core's walk-up finds a `.gaia/` project dir that
53
- * is not the user's own `~/.gaia`), the cwd is that repo's **root** and the
54
- * matching registry entry supplies the project + machine id; a project with no
55
- * registry entry still roots at its repo root. Otherwise the registry default
56
- * applies verbatim, exactly as before this ticket.
54
+ * is not the user's own `~/.gaia`), the cwd is that repo's **root**. Otherwise
55
+ * the agents root at the home dir and the cockpit opens with no default group —
56
+ * an honest empty cockpit rather than a guess.
57
+ *
58
+ * The project name and this machine's conductor id used to come from a local
59
+ * registry file, which GAIA-232 deleted. Both now resolve in the renderer's
60
+ * `run()` — `Entity/conductor/Data/own-conductor.ts`, reached through the
61
+ * launcher's `gaiaDir` — from the conductor record whose `workspace_root`
62
+ * equals this dir: the same data, from the store that also knows whether the
63
+ * conductor is alive, and matched by the same exact normalised path comparison
64
+ * the registry lookup used.
57
65
  *
58
66
  * The marker is core's `findProjectGaiaDir` — "is there a `.gaia/` here" —
59
67
  * NOT the connection walk-up `findGaiaConfig`. Since GAIA-230 the latter skips
60
68
  * an engine-only `conductor.config.js`, which GAIA-218 makes the canonical repo
61
- * shape, so keying on it would leave the most common project rooted in the
62
- * registry default. Which connection won and which project we stand in stay two
63
- * separate questions, as this function's whole point requires.
69
+ * shape, so keying on it would leave the most common project rooted at home.
64
70
  *
65
71
  * `~/.gaia` is excluded for the same reason core labels it `home`: the walk-up
66
72
  * reaches it from every directory under `$HOME`, so counting it would make
@@ -69,4 +75,4 @@ export interface ProjectRooting {
69
75
  * Pure with respect to precedence — the walk-up is core's; this helper owns no
70
76
  * filesystem probe of its own.
71
77
  */
72
- export declare function resolveProjectRooting(cwd: string, entries: ConductorRegistryEntry[], homeDir: string): ProjectRooting;
78
+ export declare function resolveProjectRooting(cwd: string, homeDir: string): ProjectRooting;
@@ -5,17 +5,6 @@
5
5
  // holds no connection-precedence rule of its own (AC-6).
6
6
  import { dirname, resolve } from 'node:path';
7
7
  import { findProjectGaiaDir, homeGaiaDir, shellQuote, } from '@gaia-ai/core';
8
- /**
9
- * Resolve the default project (opaque group) from the conductor registry: the
10
- * `preferred` project when it is registered, else the first entry's project.
11
- * `undefined` when the registry is empty — a valid empty cockpit.
12
- */
13
- export function resolveDefaultProject(entries, preferred) {
14
- if (preferred && entries.some((e) => e.project === preferred)) {
15
- return preferred;
16
- }
17
- return entries[0]?.project;
18
- }
19
8
  /**
20
9
  * Resolve the agent-launch command builder from the home config. Turns a TUI
21
10
  * prompt into the shell command herdr runs. Defaults to `claude` (an empty
@@ -27,7 +16,7 @@ export function resolveAgentCommand(machine) {
27
16
  return (prompt) => prompt.trim() === '' ? bin : `${bin} ${shellQuote(prompt)}`;
28
17
  }
29
18
  /**
30
- * Decide the agents' working directory + project (GAIA-219 AC-9).
19
+ * Decide the agents' working directory (GAIA-219 AC-9).
31
20
  *
32
21
  * The trigger is the **cwd**, deliberately not the winning connection config:
33
22
  * "which connection did we resolve" and "which project am I standing in" are
@@ -35,17 +24,22 @@ export function resolveAgentCommand(machine) {
35
24
  * silently relocate the agents' working directory.
36
25
  *
37
26
  * Standing **in** a project (core's walk-up finds a `.gaia/` project dir that
38
- * is not the user's own `~/.gaia`), the cwd is that repo's **root** and the
39
- * matching registry entry supplies the project + machine id; a project with no
40
- * registry entry still roots at its repo root. Otherwise the registry default
41
- * applies verbatim, exactly as before this ticket.
27
+ * is not the user's own `~/.gaia`), the cwd is that repo's **root**. Otherwise
28
+ * the agents root at the home dir and the cockpit opens with no default group —
29
+ * an honest empty cockpit rather than a guess.
30
+ *
31
+ * The project name and this machine's conductor id used to come from a local
32
+ * registry file, which GAIA-232 deleted. Both now resolve in the renderer's
33
+ * `run()` — `Entity/conductor/Data/own-conductor.ts`, reached through the
34
+ * launcher's `gaiaDir` — from the conductor record whose `workspace_root`
35
+ * equals this dir: the same data, from the store that also knows whether the
36
+ * conductor is alive, and matched by the same exact normalised path comparison
37
+ * the registry lookup used.
42
38
  *
43
39
  * The marker is core's `findProjectGaiaDir` — "is there a `.gaia/` here" —
44
40
  * NOT the connection walk-up `findGaiaConfig`. Since GAIA-230 the latter skips
45
41
  * an engine-only `conductor.config.js`, which GAIA-218 makes the canonical repo
46
- * shape, so keying on it would leave the most common project rooted in the
47
- * registry default. Which connection won and which project we stand in stay two
48
- * separate questions, as this function's whole point requires.
42
+ * shape, so keying on it would leave the most common project rooted at home.
49
43
  *
50
44
  * `~/.gaia` is excluded for the same reason core labels it `home`: the walk-up
51
45
  * reaches it from every directory under `$HOME`, so counting it would make
@@ -54,25 +48,12 @@ export function resolveAgentCommand(machine) {
54
48
  * Pure with respect to precedence — the walk-up is core's; this helper owns no
55
49
  * filesystem probe of its own.
56
50
  */
57
- export function resolveProjectRooting(cwd, entries, homeDir) {
51
+ export function resolveProjectRooting(cwd, homeDir) {
58
52
  const gaiaDir = findProjectGaiaDir(cwd);
59
53
  // Core owns where `.gaia` lives; the ui never spells the path itself.
60
54
  const homeDotGaia = resolve(homeGaiaDir(homeDir));
61
55
  if (gaiaDir !== undefined && resolve(gaiaDir) !== homeDotGaia) {
62
- // Registry entries store the `.gaia` dir (the conductor's checkoutRoot),
63
- // so that — not the repo root — is what an entry is matched on.
64
- const entry = entries.find((e) => resolve(e.path) === resolve(gaiaDir));
65
- return {
66
- cwd: dirname(gaiaDir),
67
- project: entry?.project,
68
- ownConductorMachineId: entry?.id,
69
- };
56
+ return { cwd: dirname(gaiaDir), gaiaDir };
70
57
  }
71
- const project = resolveDefaultProject(entries);
72
- const projectEntry = entries.find((e) => e.project === project);
73
- return {
74
- cwd: projectEntry?.path ?? homeDir,
75
- project,
76
- ownConductorMachineId: projectEntry?.id,
77
- };
58
+ return { cwd: homeDir };
78
59
  }
package/dist/src/ui.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type AgentLaunchHost } from '@gaia-ai/addon-herdr';
2
- import { type ConductorRegistryEntry, type GaiaCommandHost, type GaiaCommandPlugin, type GaiaConnectionConfig, type MachineContext } from '@gaia-ai/core';
2
+ import { type GaiaCommandHost, type GaiaCommandPlugin, type GaiaConnectionConfig, type MachineContext } from '@gaia-ai/core';
3
3
  import { type UiInitialRoute } from './route.js';
4
4
  import { type HomeConnection } from './ui-home.js';
5
5
  /** Prompt-level agent service the renderer consumes (mirror of gaia-ui's `TuiAgentService`). */
@@ -38,7 +38,15 @@ export interface RunGaiaUiOptions {
38
38
  project?: string | undefined;
39
39
  agents?: TuiAgentService | undefined;
40
40
  currentUser?: string | undefined;
41
- ownConductorMachineId?: string | undefined;
41
+ /**
42
+ * The `.gaia` dir of the project the ui was started in, when there is one.
43
+ *
44
+ * Replaces the `ownConductorMachineId` the local registry used to supply
45
+ * (GAIA-232): the renderer matches this against each conductor's
46
+ * `workspace_root` to find which record is this machine's own — it holds the
47
+ * JSON:API client, and this entry does not.
48
+ */
49
+ gaiaDir?: string | undefined;
42
50
  createPrompt?: string | undefined;
43
51
  buildProgram: (opts: {
44
52
  plugins: unknown[];
@@ -62,8 +70,6 @@ export interface BuildWriteClientOptions {
62
70
  export interface UiDeps {
63
71
  /** Machine context (identity + connection); default: read from disk. */
64
72
  machine?: Partial<MachineContext>;
65
- /** Conductor registry entries; default: `listRegisteredConductors()`. */
66
- registryEntries?: ConductorRegistryEntry[];
67
73
  /** herdr-backed agent host; default: `herdrAgentHost(exec herdr)`. */
68
74
  agentHost?: AgentLaunchHost;
69
75
  /** The renderer launcher; default: the dynamically-resolved `runGaiaUi`. */
@@ -83,6 +89,8 @@ export interface UiDeps {
83
89
  printConfig?: boolean;
84
90
  /** Sink for `--print-config`; default: `process.stdout.write`. */
85
91
  stdout?: (s: string) => void;
92
+ /** Daily update lookup; injectable for startup ordering tests. */
93
+ updateCheck?: () => Promise<string | null>;
86
94
  /** Positional `[target]` (`GAIA-221`, `ticket:…`, `project:…`, `run:…`). */
87
95
  target?: string | undefined;
88
96
  /** `--ticket <id>`. */
package/dist/src/ui.js CHANGED
@@ -2,9 +2,10 @@ import { createRequire } from 'node:module';
2
2
  import { homedir } from 'node:os';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { herdrAgentHost } from '@gaia-ai/addon-herdr';
5
- import { CommandRunner, createLogger, exec, listRegisteredConductors, loadGaiaConfig, machineContextPath, readMachineContext, setDefaultCommandRunner, } from '@gaia-ai/core';
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`. */
@@ -57,11 +58,13 @@ async function readCurrentBranch(cwd) {
57
58
  * with no `origin`, it yields '' and the ladder produces its named error.
58
59
  *
59
60
  * Revision 4 replaced the previous rung (an `import()` of the repo's engine
60
- * `conductor.config.js` plus a conductor-registry longest-prefix fallback) with
61
- * this one read: the registry cannot answer inside a worktree (its paths point
62
- * at `.gaia` DIRS, and a worktree has its own unregistered one), the config read
63
- * needed a dynamic import of a possibly secret-bearing module, and the remote
64
- * works in a fresh clone that has no `.gaia/` at all.
61
+ * `conductor.config.js` plus a longest-prefix fallback over the then-still-alive
62
+ * local conductor registry) with this one read: neither could answer inside a
63
+ * worktree (both keyed on `.gaia` DIRS, and a worktree has its own), the config
64
+ * read needed a dynamic import of a possibly secret-bearing module, and the
65
+ * remote works in a fresh clone that has no `.gaia/` at all. Revision 5 then
66
+ * deleted that registry outright, which only removes an alternative this rung
67
+ * had already rejected.
65
68
  */
66
69
  async function readOriginRemoteUrl(cwd) {
67
70
  try {
@@ -90,8 +93,19 @@ function resolveExplicitConfig(flag) {
90
93
  }
91
94
  return undefined;
92
95
  }
93
- /** The `--print-config` block (AC-7): which file won, and what it points at. */
94
- function formatConnectionReport(connection, rooting) {
96
+ /**
97
+ * The `--print-config` block (AC-7): which file won, and what it points at.
98
+ *
99
+ * `project` reports the `--project` flag, and `gaia_dir` the project dir the
100
+ * rooting resolved. GAIA-232 split the two on purpose: the DEFAULT project is no
101
+ * longer knowable here. It used to be read out of `~/.gaia/conductors.json`,
102
+ * which this ticket deleted, and its replacement is the conductor record whose
103
+ * `workspace_root` equals `gaia_dir` — a control-plane read, and this command
104
+ * holds no JSON:API client (GAIA-223 spec Finding 1). Printing `gaia_dir` is
105
+ * therefore the honest form: it names the exact input the renderer resolves the
106
+ * project from, instead of a `project:` line that could only ever say `(none)`.
107
+ */
108
+ function formatConnectionReport(connection, rooting, project) {
95
109
  const yesNo = (b) => (b ? 'yes' : 'no');
96
110
  const lines = [
97
111
  ['connection', connection.config_path],
@@ -100,7 +114,11 @@ function formatConnectionReport(connection, rooting) {
100
114
  ['legacy', yesNo(connection.legacy)],
101
115
  ['base_url', connection.site.base_url],
102
116
  ['cwd', rooting.cwd],
103
- ['project', rooting.project ?? '(none)'],
117
+ ['gaia_dir', rooting.gaiaDir ?? '(none)'],
118
+ [
119
+ 'project',
120
+ project ?? (rooting.gaiaDir ? '(resolved from gaia_dir)' : '(none)'),
121
+ ],
104
122
  ];
105
123
  return `${lines.map(([k, v]) => `${`${k}:`.padEnd(12)}${v}`).join('\n')}\n`;
106
124
  }
@@ -156,6 +174,38 @@ export async function cmdUi(deps = {}) {
156
174
  (deps.stderr ?? ((line) => process.stderr.write(line)))(`${message}\n`);
157
175
  process.exitCode = 1;
158
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
+ }
159
209
  // GAIA-223: build the deep-link route FIRST — argv parsing, the conflict rule
160
210
  // and the whole `--here` ladder are offline, so every one of their failures
161
211
  // costs no config load and no network round-trip, and never reaches the
@@ -186,9 +236,6 @@ export async function cmdUi(deps = {}) {
186
236
  fail(err instanceof Error ? err.message : String(err));
187
237
  return;
188
238
  }
189
- // Read only AFTER the route: the whole grammar + ladder is offline, so a
190
- // parse/conflict/no-remote failure must not even touch the registry file.
191
- const entries = deps.registryEntries ?? (await listRegisteredConductors());
192
239
  const mcPath = machineContextPath();
193
240
  // The machine context still supplies identity (`user_id`) + the agent binary;
194
241
  // it is NOT the connection any more — core resolves that.
@@ -209,6 +256,14 @@ export async function cmdUi(deps = {}) {
209
256
  process.exitCode = 1;
210
257
  return;
211
258
  }
259
+ const notice = deps.updateCheck
260
+ ? await deps.updateCheck()
261
+ : deps.runUi
262
+ ? null
263
+ : await fetchUpdateNotice(resolveCliVersion());
264
+ if (notice !== null) {
265
+ (deps.stdout ?? ((line) => process.stdout.write(line)))(`${notice}\n`);
266
+ }
212
267
  const connection = {
213
268
  baseUrl: resolved.site.base_url,
214
269
  jsonapiPrefix: resolved.site.jsonapi_prefix,
@@ -242,9 +297,8 @@ export async function cmdUi(deps = {}) {
242
297
  `(${err instanceof Error ? err.message : String(err)}); ` +
243
298
  `edits will be attempted as '${connection.authProfile}'.`);
244
299
  }
245
- const rooting = resolveProjectRooting(cwd, entries, homedir());
246
- const ownConductorMachineId = rooting.ownConductorMachineId;
247
- const project = rooting.project;
300
+ const rooting = resolveProjectRooting(cwd, homedir());
301
+ const project = deps.project;
248
302
  // AC-7, always on: the resolution lands in the log even when the TUI takes
249
303
  // over the screen a moment later.
250
304
  logger.info({
@@ -254,13 +308,17 @@ export async function cmdUi(deps = {}) {
254
308
  legacy: resolved.legacy,
255
309
  base_url: resolved.site.base_url,
256
310
  cwd: rooting.cwd,
311
+ // The `.gaia` dir, not a project name: since GAIA-232 the default project
312
+ // is resolved in the renderer from the conductor record whose
313
+ // `workspace_root` is this dir. `project` here is the flag alone.
314
+ gaia_dir: rooting.gaiaDir,
257
315
  project,
258
316
  }, 'gaia ui: resolved connection');
259
317
  // AC-7, the human surface: print and exit 0 WITHOUT booting the TUI, so a
260
318
  // wrong-control-plane situation is diagnosable at a prompt.
261
319
  if (deps.printConfig) {
262
320
  const write = deps.stdout ?? ((s) => process.stdout.write(s));
263
- write(formatConnectionReport(resolved, rooting));
321
+ write(formatConnectionReport(resolved, rooting, project));
264
322
  return;
265
323
  }
266
324
  const agentCommand = resolveAgentCommand(machine);
@@ -315,7 +373,7 @@ export async function cmdUi(deps = {}) {
315
373
  ...(project !== undefined ? { project } : {}),
316
374
  agents,
317
375
  ...(machine.user_id ? { currentUser: machine.user_id } : {}),
318
- ...(ownConductorMachineId ? { ownConductorMachineId } : {}),
376
+ ...(rooting.gaiaDir ? { gaiaDir: rooting.gaiaDir } : {}),
319
377
  // The plugins are real constructed `DropSHPlugin[]` now (core built them),
320
378
  // matching dropsh's declared contract — F3's substance, a descriptor handed
321
379
  // to an API expecting instances, is gone. The two casts that remain are
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/ui",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
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.7.0",
30
- "@gaia-ai/addon-herdr": "^0.7.0",
29
+ "@gaia-ai/core": "^0.9.0",
30
+ "@gaia-ai/addon-herdr": "^0.9.0",
31
31
  "commander": "^12.1.0",
32
32
  "dropsh": "^0.5.8"
33
33
  }