@gaia-ai/ui 0.6.1 → 0.6.3
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/README.md +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +9 -4
- package/dist/src/route.d.ts +115 -0
- package/dist/src/route.js +264 -0
- package/dist/src/ui-home.d.ts +34 -35
- package/dist/src/ui-home.js +54 -58
- package/dist/src/ui.d.ts +44 -9
- package/dist/src/ui.js +236 -50
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# @gaia-ai/ui
|
|
2
2
|
|
|
3
|
-
GAIA
|
|
3
|
+
GAIA project-first cockpit: the `gaia ui` command plugin (renderer resolved dynamically).
|
|
4
4
|
|
|
5
5
|
Part of the GAIA CLI. Install the meta package `@gaia-ai/gaia` to get the `gaia` CLI with all addons. Source: https://git.key-tec.de/keytec/gaia (gaia-cli/).
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,2 +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';
|
|
1
2
|
export { cmdUi, default, type RunGaiaUiOptions, type UiDeps } from './ui.js';
|
|
2
|
-
export {
|
|
3
|
+
export { type HomeConnection, type ProjectRooting, resolveAgentCommand, resolveDefaultProject, resolveProjectRooting, } from './ui-home.js';
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
// GAIA-201: `@gaia-ai/ui` — the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// GAIA-201 / GAIA-219: `@gaia-ai/ui` — the `gaia ui` command plugin. Its
|
|
2
|
+
// connection is resolved by core's `loadGaiaConfig` (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 `GaiaCommandPlugin` the host mounts; `cmdUi` + the
|
|
6
|
+
// bootstrap helpers are exported for tests.
|
|
7
|
+
// GAIA-223: the deep-link grammar (the pure CLI seam).
|
|
8
|
+
export { isTicketIdentifier, isUuid, normalizeRepoUrl, parseTarget, parseTicketIdentifierFromBranch, resolveHere, UI_VIEW_MODES, } from './route.js';
|
|
4
9
|
export { cmdUi, default } from './ui.js';
|
|
5
|
-
export {
|
|
10
|
+
export { resolveAgentCommand, resolveDefaultProject, resolveProjectRooting, } from './ui-home.js';
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** How a ticket target is rendered. Mirrors the Drupal `gaia_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
|
+
* (`RunGaiaUiOptions` → `GaiaUiLauncherOptions` → `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
|
+
* every `gaia_project.repos[].url` (spec Decision 3).
|
|
17
|
+
*/
|
|
18
|
+
by: 'uuid' | 'identifier' | 'name' | 'repo';
|
|
19
|
+
value: string;
|
|
20
|
+
/** Drupal view mode for a ticket target; default 'full'. */
|
|
21
|
+
view?: UiViewMode | undefined;
|
|
22
|
+
/** Set when the target came from `--here`; error messages only. */
|
|
23
|
+
fromBranch?: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
/** The raw argv inputs `parseTarget` reads (commander's parsed options). */
|
|
26
|
+
export interface TargetInputs {
|
|
27
|
+
target?: string | undefined;
|
|
28
|
+
ticket?: string | undefined;
|
|
29
|
+
project?: string | undefined;
|
|
30
|
+
run?: string | undefined;
|
|
31
|
+
here?: boolean | undefined;
|
|
32
|
+
view?: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What the grammar made of the argv: nothing (the dashboard), a `--here`
|
|
36
|
+
* request that still needs the ladder walked, or a finished route.
|
|
37
|
+
*/
|
|
38
|
+
export type ParsedTarget = {
|
|
39
|
+
kind: 'here';
|
|
40
|
+
view?: UiViewMode | undefined;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'route';
|
|
43
|
+
route: UiInitialRoute;
|
|
44
|
+
};
|
|
45
|
+
/** True for an `8-4-4-4-12` hex UUID (case-insensitive). */
|
|
46
|
+
export declare function isUuid(value: string): boolean;
|
|
47
|
+
/** True for a `KEY-123` ticket identifier (case-insensitive). */
|
|
48
|
+
export declare function isTicketIdentifier(value: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The ticket identifier a branch name carries, uppercased, or `null`.
|
|
51
|
+
* Pure and offline — the `--here` ticket rung never needs the network.
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseTicketIdentifierFromBranch(branch: string): string | null;
|
|
54
|
+
/**
|
|
55
|
+
* The grammar: at most ONE target source (the positional, `--ticket`,
|
|
56
|
+
* `--project`, `--run`, `--here`) plus the orthogonal `--view` modifier.
|
|
57
|
+
*
|
|
58
|
+
* Returns `undefined` for no target at all (the dashboard, AC-4), `{kind:'here'}`
|
|
59
|
+
* when the ladder still has to be walked, else the finished route. Throws — with
|
|
60
|
+
* a message naming exactly what went wrong — on a conflict or an unparseable
|
|
61
|
+
* form; the caller turns that into a stderr line and exit 1 before any network
|
|
62
|
+
* work happens.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseTarget(inputs: TargetInputs): ParsedTarget | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* A git remote URL reduced to a comparable `host/path` identity: scheme,
|
|
67
|
+
* userinfo, port and a trailing `.git` (and trailing slashes) dropped, the whole
|
|
68
|
+
* thing lowercased. Returns `undefined` for anything that carries no host+path.
|
|
69
|
+
*
|
|
70
|
+
* This is REQUIRED, not cosmetic (spec Decision 3): git and the control plane
|
|
71
|
+
* disagree on form. Measured in this repo — `git remote get-url origin` yields
|
|
72
|
+
* `git@git.key-tec.de:keytec/gaia.git` while `gaia_project.repos[].url` holds
|
|
73
|
+
* `ssh://git@git.key-tec.de/keytec/gaia.git`. Both must reduce to
|
|
74
|
+
* `git.key-tec.de/keytec/gaia` or the `--here` project rung never matches.
|
|
75
|
+
*
|
|
76
|
+
* | raw | normalised |
|
|
77
|
+
* |-----------------------------------------|---------------------|
|
|
78
|
+
* | `git@host:keytec/gaia.git` | `host/keytec/gaia` |
|
|
79
|
+
* | `ssh://git@host/keytec/gaia.git` | `host/keytec/gaia` |
|
|
80
|
+
* | `ssh://git@host:2222/keytec/gaia.git` | `host/keytec/gaia` |
|
|
81
|
+
* | `https://host/keytec/gaia.git` | `host/keytec/gaia` |
|
|
82
|
+
*/
|
|
83
|
+
export declare function normalizeRepoUrl(raw: string | undefined): string | undefined;
|
|
84
|
+
/** Everything `resolveHere` needs, all injected — two git reads, nothing else. */
|
|
85
|
+
export interface HereContext {
|
|
86
|
+
cwd: string;
|
|
87
|
+
view?: UiViewMode | undefined;
|
|
88
|
+
/** The current branch of `cwd` (git resolves it per worktree). May reject. */
|
|
89
|
+
readBranch: (cwd: string) => Promise<string>;
|
|
90
|
+
/**
|
|
91
|
+
* The cwd's `origin` remote URL, raw. Git resolves it per worktree too, and a
|
|
92
|
+
* worktree shares its main clone's remote — which is exactly the right answer
|
|
93
|
+
* (spec Decision 3). May reject (not a repo, no `origin`).
|
|
94
|
+
*/
|
|
95
|
+
readRemoteUrl: (cwd: string) => Promise<string>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* `--here`: derive the target from the cwd (spec Decision 3).
|
|
99
|
+
*
|
|
100
|
+
* ```
|
|
101
|
+
* branch carries a ticket identifier? → that TICKET (feat/gaia-223-… → GAIA-223)
|
|
102
|
+
* else: the cwd's git remote → the PROJECT(s) whose repos[] claim it
|
|
103
|
+
* else → a named error
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* The BRANCH decides the first rung, not git mechanics: a worktree on a
|
|
107
|
+
* non-ticket branch and the main clone on a ticket branch both come out right.
|
|
108
|
+
*
|
|
109
|
+
* The project rung stops HERE at a normalised repo identity — matching it
|
|
110
|
+
* against `gaia_project.repos[]` needs a JSON:API client, which the entry does
|
|
111
|
+
* not have (Finding 1), so `by: 'repo'` travels to the renderer's `resolveRoute`
|
|
112
|
+
* and the ambiguity ruling (one match / a picker / an error) is made there
|
|
113
|
+
* (Decisions 5 + 14).
|
|
114
|
+
*/
|
|
115
|
+
export declare function resolveHere(ctx: HereContext): Promise<UiInitialRoute>;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// GAIA-223: the pure CLI seam of `gaia ui` deep links.
|
|
2
|
+
//
|
|
3
|
+
// This module owns the *grammar* — argv in, an unresolved `UiInitialRoute` out —
|
|
4
|
+
// and the `--here` ladder. It performs no I/O of its own: git, the filesystem
|
|
5
|
+
// and the conductor registry all enter through injected seams, so the whole
|
|
6
|
+
// grammar is testable offline and a conflict/parse failure costs no network
|
|
7
|
+
// round-trip (spec Decisions 1, 3, 4, 6, 13).
|
|
8
|
+
//
|
|
9
|
+
// Resolving a route against the control plane is deliberately NOT here: the
|
|
10
|
+
// entry has no JSON:API client (spec Finding 1). That happens in the renderer,
|
|
11
|
+
// in `@gaia-ai/addon-gaia-ui`'s `resolveRoute`, still before the TUI takes the
|
|
12
|
+
// terminal (spec Decision 5).
|
|
13
|
+
/** The known `--view` modes, in the order error messages list them. */
|
|
14
|
+
export const UI_VIEW_MODES = [
|
|
15
|
+
'full',
|
|
16
|
+
'teaser',
|
|
17
|
+
'board_card',
|
|
18
|
+
];
|
|
19
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
|
+
/** `<project-key>-<number>`, case-insensitive (Decision 4). One dash only, so a
|
|
21
|
+
* UUID can never match it. */
|
|
22
|
+
const IDENTIFIER_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
|
|
23
|
+
/**
|
|
24
|
+
* The ticket identifier carried by a branch name, first match wins. Every
|
|
25
|
+
* convention places the identifier before the title slug and `capBranchName()`
|
|
26
|
+
* never truncates it (Finding 3), so the first match is the right one.
|
|
27
|
+
*/
|
|
28
|
+
const BRANCH_IDENTIFIER_RE = /(?:^|[/_-])([A-Za-z][A-Za-z0-9]*-\d+)(?=$|[/_-])/;
|
|
29
|
+
const ACCEPTED_FORMS = "accepted forms: GAIA-221 (a ticket identifier), 'ticket:<id|uuid>', " +
|
|
30
|
+
"'project:<name|uuid>', 'run:<uuid>'";
|
|
31
|
+
/** True for an `8-4-4-4-12` hex UUID (case-insensitive). */
|
|
32
|
+
export function isUuid(value) {
|
|
33
|
+
return UUID_RE.test(value);
|
|
34
|
+
}
|
|
35
|
+
/** True for a `KEY-123` ticket identifier (case-insensitive). */
|
|
36
|
+
export function isTicketIdentifier(value) {
|
|
37
|
+
return IDENTIFIER_RE.test(value);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The ticket identifier a branch name carries, uppercased, or `null`.
|
|
41
|
+
* Pure and offline — the `--here` ticket rung never needs the network.
|
|
42
|
+
*/
|
|
43
|
+
export function parseTicketIdentifierFromBranch(branch) {
|
|
44
|
+
const match = BRANCH_IDENTIFIER_RE.exec(branch);
|
|
45
|
+
return match?.[1] ? match[1].toUpperCase() : null;
|
|
46
|
+
}
|
|
47
|
+
/** Validate a raw `--view` value against the known modes. */
|
|
48
|
+
function parseViewMode(raw) {
|
|
49
|
+
if (raw === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (UI_VIEW_MODES.includes(raw)) {
|
|
52
|
+
return raw;
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`gaia ui: unknown --view mode '${raw}'. Known modes: ${UI_VIEW_MODES.join(', ')}.`);
|
|
55
|
+
}
|
|
56
|
+
/** A ticket target from a raw value: a UUID by id, else an identifier by name. */
|
|
57
|
+
function ticketTarget(value) {
|
|
58
|
+
if (isUuid(value))
|
|
59
|
+
return { kind: 'ticket', by: 'uuid', value };
|
|
60
|
+
if (isTicketIdentifier(value)) {
|
|
61
|
+
return { kind: 'ticket', by: 'identifier', value: value.toUpperCase() };
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`gaia ui: '${value}' is not a ticket identifier or UUID — ${ACCEPTED_FORMS}.`);
|
|
64
|
+
}
|
|
65
|
+
/** A project target: a UUID by id, else the label verbatim (labels are
|
|
66
|
+
* case-sensitive Drupal entity labels — Decision 4). */
|
|
67
|
+
function projectTarget(value) {
|
|
68
|
+
return isUuid(value)
|
|
69
|
+
? { kind: 'project', by: 'uuid', value }
|
|
70
|
+
: { kind: 'project', by: 'name', value };
|
|
71
|
+
}
|
|
72
|
+
/** A run target — UUIDs only; there is no human-readable run handle. */
|
|
73
|
+
function runTarget(value) {
|
|
74
|
+
if (!isUuid(value)) {
|
|
75
|
+
throw new Error(`gaia ui: a run target must be a UUID, got '${value}'.`);
|
|
76
|
+
}
|
|
77
|
+
return { kind: 'run', by: 'uuid', value };
|
|
78
|
+
}
|
|
79
|
+
/** Parse the positional form: an optional `kind:` prefix, else a bare identifier. */
|
|
80
|
+
function parsePositional(raw) {
|
|
81
|
+
const colon = raw.indexOf(':');
|
|
82
|
+
if (colon >= 0) {
|
|
83
|
+
const prefix = raw.slice(0, colon);
|
|
84
|
+
const value = raw.slice(colon + 1);
|
|
85
|
+
if (prefix !== 'ticket' && prefix !== 'project' && prefix !== 'run') {
|
|
86
|
+
throw new Error(`gaia ui: unknown target kind '${prefix}'. ` +
|
|
87
|
+
"Known kinds: 'ticket:', 'project:', 'run:'.");
|
|
88
|
+
}
|
|
89
|
+
if (value === '') {
|
|
90
|
+
throw new Error(`gaia ui: '${raw}' has an empty value — ${ACCEPTED_FORMS}.`);
|
|
91
|
+
}
|
|
92
|
+
if (prefix === 'ticket')
|
|
93
|
+
return ticketTarget(value);
|
|
94
|
+
if (prefix === 'project')
|
|
95
|
+
return projectTarget(value);
|
|
96
|
+
return runTarget(value);
|
|
97
|
+
}
|
|
98
|
+
// Bare: strictly a ticket identifier. A UUID is ambiguous by construction —
|
|
99
|
+
// it does not say which kind — and never guessed (Decision 1).
|
|
100
|
+
if (isUuid(raw)) {
|
|
101
|
+
throw new Error(`gaia ui: '${raw}' is ambiguous — a bare UUID does not say which kind. ` +
|
|
102
|
+
"Use 'ticket:<uuid>', 'project:<uuid>' or 'run:<uuid>'.");
|
|
103
|
+
}
|
|
104
|
+
return ticketTarget(raw);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The grammar: at most ONE target source (the positional, `--ticket`,
|
|
108
|
+
* `--project`, `--run`, `--here`) plus the orthogonal `--view` modifier.
|
|
109
|
+
*
|
|
110
|
+
* Returns `undefined` for no target at all (the dashboard, AC-4), `{kind:'here'}`
|
|
111
|
+
* when the ladder still has to be walked, else the finished route. Throws — with
|
|
112
|
+
* a message naming exactly what went wrong — on a conflict or an unparseable
|
|
113
|
+
* form; the caller turns that into a stderr line and exit 1 before any network
|
|
114
|
+
* work happens.
|
|
115
|
+
*/
|
|
116
|
+
export function parseTarget(inputs) {
|
|
117
|
+
const supplied = [];
|
|
118
|
+
if (inputs.target !== undefined && inputs.target !== '') {
|
|
119
|
+
supplied.push(`the positional target '${inputs.target}'`);
|
|
120
|
+
}
|
|
121
|
+
if (inputs.ticket !== undefined)
|
|
122
|
+
supplied.push('--ticket');
|
|
123
|
+
if (inputs.project !== undefined)
|
|
124
|
+
supplied.push('--project');
|
|
125
|
+
if (inputs.run !== undefined)
|
|
126
|
+
supplied.push('--run');
|
|
127
|
+
if (inputs.here === true)
|
|
128
|
+
supplied.push('--here');
|
|
129
|
+
if (supplied.length > 1) {
|
|
130
|
+
throw new Error(`gaia ui: conflicting targets — ${supplied.join(', ')} were all supplied. ` +
|
|
131
|
+
'Give at most one of: a positional target, --ticket, --project, --run, --here.');
|
|
132
|
+
}
|
|
133
|
+
const view = parseViewMode(inputs.view);
|
|
134
|
+
if (supplied.length === 0)
|
|
135
|
+
return undefined;
|
|
136
|
+
if (inputs.here === true) {
|
|
137
|
+
return { kind: 'here', view: view ?? 'full' };
|
|
138
|
+
}
|
|
139
|
+
const target = inputs.target !== undefined && inputs.target !== ''
|
|
140
|
+
? parsePositional(inputs.target)
|
|
141
|
+
: inputs.ticket !== undefined
|
|
142
|
+
? ticketTarget(inputs.ticket)
|
|
143
|
+
: inputs.project !== undefined
|
|
144
|
+
? projectTarget(inputs.project)
|
|
145
|
+
: runTarget(inputs.run);
|
|
146
|
+
// `--view` describes how a TICKET renders. A project screen has no view mode,
|
|
147
|
+
// and `run:` already means "the ticket's Runs tab", so pairing it with a
|
|
148
|
+
// compact block is contradictory rather than resolvable (Decision 13).
|
|
149
|
+
if (view !== undefined && target.kind === 'project') {
|
|
150
|
+
throw new Error('gaia ui: --view applies to ticket targets only, but the target is a project. ' +
|
|
151
|
+
'Drop --view, or name a ticket.');
|
|
152
|
+
}
|
|
153
|
+
if (view !== undefined && target.kind === 'run') {
|
|
154
|
+
throw new Error("gaia ui: --view cannot be combined with a 'run:' target — 'run:' already " +
|
|
155
|
+
"means the owning ticket's Runs tab. Use 'ticket:<id> --view <mode>' instead.");
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
kind: 'route',
|
|
159
|
+
route: target.kind === 'project' ? target : { ...target, view: view ?? 'full' },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A git remote URL reduced to a comparable `host/path` identity: scheme,
|
|
164
|
+
* userinfo, port and a trailing `.git` (and trailing slashes) dropped, the whole
|
|
165
|
+
* thing lowercased. Returns `undefined` for anything that carries no host+path.
|
|
166
|
+
*
|
|
167
|
+
* This is REQUIRED, not cosmetic (spec Decision 3): git and the control plane
|
|
168
|
+
* disagree on form. Measured in this repo — `git remote get-url origin` yields
|
|
169
|
+
* `git@git.key-tec.de:keytec/gaia.git` while `gaia_project.repos[].url` holds
|
|
170
|
+
* `ssh://git@git.key-tec.de/keytec/gaia.git`. Both must reduce to
|
|
171
|
+
* `git.key-tec.de/keytec/gaia` or the `--here` project rung never matches.
|
|
172
|
+
*
|
|
173
|
+
* | raw | normalised |
|
|
174
|
+
* |-----------------------------------------|---------------------|
|
|
175
|
+
* | `git@host:keytec/gaia.git` | `host/keytec/gaia` |
|
|
176
|
+
* | `ssh://git@host/keytec/gaia.git` | `host/keytec/gaia` |
|
|
177
|
+
* | `ssh://git@host:2222/keytec/gaia.git` | `host/keytec/gaia` |
|
|
178
|
+
* | `https://host/keytec/gaia.git` | `host/keytec/gaia` |
|
|
179
|
+
*/
|
|
180
|
+
export function normalizeRepoUrl(raw) {
|
|
181
|
+
const trimmed = (raw ?? '').trim();
|
|
182
|
+
if (trimmed === '')
|
|
183
|
+
return undefined;
|
|
184
|
+
let host;
|
|
185
|
+
let path;
|
|
186
|
+
// `scheme://[user@]host[:port]/path` — checked FIRST, so an `ssh://…:2222/…`
|
|
187
|
+
// port is never mistaken for the scp form's `host:path` colon.
|
|
188
|
+
const withScheme = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/(.+)$/.exec(trimmed);
|
|
189
|
+
if (withScheme?.[1] !== undefined) {
|
|
190
|
+
const rest = withScheme[1];
|
|
191
|
+
const slash = rest.indexOf('/');
|
|
192
|
+
if (slash < 0)
|
|
193
|
+
return undefined;
|
|
194
|
+
host = rest.slice(0, slash);
|
|
195
|
+
path = rest.slice(slash + 1);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// The scp-like form `[user@]host:path` — what `git remote get-url` emits
|
|
199
|
+
// for an ssh clone. A bare local path has no colon and falls out here.
|
|
200
|
+
const scp = /^(?:[^/@]+@)?([^/:]+):(.+)$/.exec(trimmed);
|
|
201
|
+
if (scp?.[1] === undefined || scp[2] === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
host = scp[1];
|
|
204
|
+
path = scp[2];
|
|
205
|
+
}
|
|
206
|
+
host = host
|
|
207
|
+
.replace(/^[^@]*@/, '')
|
|
208
|
+
.replace(/:\d+$/, '')
|
|
209
|
+
.toLowerCase();
|
|
210
|
+
path = path
|
|
211
|
+
.replace(/^\/+/, '')
|
|
212
|
+
.replace(/\/+$/, '')
|
|
213
|
+
.replace(/\.git$/i, '')
|
|
214
|
+
.replace(/\/+$/, '')
|
|
215
|
+
.toLowerCase();
|
|
216
|
+
if (host === '' || path === '')
|
|
217
|
+
return undefined;
|
|
218
|
+
return `${host}/${path}`;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* `--here`: derive the target from the cwd (spec Decision 3).
|
|
222
|
+
*
|
|
223
|
+
* ```
|
|
224
|
+
* branch carries a ticket identifier? → that TICKET (feat/gaia-223-… → GAIA-223)
|
|
225
|
+
* else: the cwd's git remote → the PROJECT(s) whose repos[] claim it
|
|
226
|
+
* else → a named error
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* The BRANCH decides the first rung, not git mechanics: a worktree on a
|
|
230
|
+
* non-ticket branch and the main clone on a ticket branch both come out right.
|
|
231
|
+
*
|
|
232
|
+
* The project rung stops HERE at a normalised repo identity — matching it
|
|
233
|
+
* against `gaia_project.repos[]` needs a JSON:API client, which the entry does
|
|
234
|
+
* not have (Finding 1), so `by: 'repo'` travels to the renderer's `resolveRoute`
|
|
235
|
+
* and the ambiguity ruling (one match / a picker / an error) is made there
|
|
236
|
+
* (Decisions 5 + 14).
|
|
237
|
+
*/
|
|
238
|
+
export async function resolveHere(ctx) {
|
|
239
|
+
const branch = await ctx.readBranch(ctx.cwd).then((b) => b.trim(), () => '');
|
|
240
|
+
const identifier = branch === '' ? null : parseTicketIdentifierFromBranch(branch);
|
|
241
|
+
if (identifier !== null) {
|
|
242
|
+
return {
|
|
243
|
+
kind: 'ticket',
|
|
244
|
+
by: 'identifier',
|
|
245
|
+
value: identifier,
|
|
246
|
+
view: ctx.view ?? 'full',
|
|
247
|
+
fromBranch: branch,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const remote = await ctx.readRemoteUrl(ctx.cwd).then((r) => r.trim(), () => '');
|
|
251
|
+
const repo = normalizeRepoUrl(remote);
|
|
252
|
+
if (repo === undefined) {
|
|
253
|
+
const got = remote === '' ? '' : `, got '${remote}'`;
|
|
254
|
+
throw new Error(`gaia ui --here: could not derive a project from '${ctx.cwd}' — ` +
|
|
255
|
+
`no usable git remote (looked at 'origin'${got}).\n` +
|
|
256
|
+
" Run 'gaia ui' for the dashboard, or 'gaia ui project:<name>' to open one directly.");
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
kind: 'project',
|
|
260
|
+
by: 'repo',
|
|
261
|
+
value: repo,
|
|
262
|
+
...(branch !== '' ? { fromBranch: branch } : {}),
|
|
263
|
+
};
|
|
264
|
+
}
|
package/dist/src/ui-home.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ConductorRegistryEntry, type MachineContext } from '@gaia-ai/core';
|
|
2
2
|
/**
|
|
3
|
-
* The resolved
|
|
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
|
|
5
5
|
* local so this package needs NO compile-time dependency on the renderer (it
|
|
6
6
|
* resolves the renderer dynamically, ESLint-style, at runtime).
|
|
@@ -12,30 +12,6 @@ export interface HomeConnection {
|
|
|
12
12
|
configPath?: string | undefined;
|
|
13
13
|
authProfile?: string | undefined;
|
|
14
14
|
}
|
|
15
|
-
/** The `session` oauth2 client-credentials descriptor, matching the connection
|
|
16
|
-
* config template shape exactly so home reads authenticate identically to a
|
|
17
|
-
* project-rooted `gaia dropsh`. */
|
|
18
|
-
export interface SessionAuthDescriptor {
|
|
19
|
-
plugin: '@dropsh/plugin-oauth2';
|
|
20
|
-
export: 'oauth2Plugin';
|
|
21
|
-
with: {
|
|
22
|
-
id: 'session';
|
|
23
|
-
default: true;
|
|
24
|
-
type: 'oauth2_client_credentials';
|
|
25
|
-
client_id: string;
|
|
26
|
-
client_secret: string;
|
|
27
|
-
token_url: string;
|
|
28
|
-
scope: 'gaia:session';
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Build the home control-plane connection from the machine context. Throws an
|
|
33
|
-
* actionable error when the context is missing `base_url` / `client_id` /
|
|
34
|
-
* `client_secret` (there is no project config to fall back to). The returned
|
|
35
|
-
* `authPlugins` carries a single `session` oauth2 descriptor; `configPath` is
|
|
36
|
-
* filled in by the entry once it has resolved the connection config file.
|
|
37
|
-
*/
|
|
38
|
-
export declare function buildHomeConnection(machine: Partial<MachineContext>): HomeConnection;
|
|
39
15
|
/**
|
|
40
16
|
* Resolve the default project (opaque group) from the conductor registry: the
|
|
41
17
|
* `preferred` project when it is registered, else the first entry's project.
|
|
@@ -51,15 +27,38 @@ export declare function resolveDefaultProject(entries: ConductorRegistryEntry[],
|
|
|
51
27
|
export declare function resolveAgentCommand(machine: Partial<MachineContext> & {
|
|
52
28
|
agent_command?: string;
|
|
53
29
|
}): (prompt: string) => string;
|
|
30
|
+
/** Where `gaia ui` roots the agents it launches, and which project they belong to. */
|
|
31
|
+
export interface ProjectRooting {
|
|
32
|
+
cwd: string;
|
|
33
|
+
project?: string | undefined;
|
|
34
|
+
ownConductorMachineId?: string | undefined;
|
|
35
|
+
}
|
|
54
36
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
37
|
+
* Decide the agents' working directory + project (GAIA-219 AC-9).
|
|
38
|
+
*
|
|
39
|
+
* The trigger is the **cwd**, deliberately not the winning connection config:
|
|
40
|
+
* "which connection did we resolve" and "which project am I standing in" are
|
|
41
|
+
* different questions, and binding them would let an explicit `--config`
|
|
42
|
+
* silently relocate the agents' working directory.
|
|
43
|
+
*
|
|
44
|
+
* Standing **in** a project (core's walk-up finds a `.gaia/` project dir that
|
|
45
|
+
* is not the user's own `~/.gaia`), the cwd is that repo's **root** and the
|
|
46
|
+
* matching registry entry supplies the project + machine id; a project with no
|
|
47
|
+
* registry entry still roots at its repo root. Otherwise the registry default
|
|
48
|
+
* applies verbatim, exactly as before this ticket.
|
|
49
|
+
*
|
|
50
|
+
* The marker is core's `findProjectGaiaDir` — "is there a `.gaia/` here" —
|
|
51
|
+
* NOT the connection walk-up `findGaiaConfig`. Since GAIA-230 the latter skips
|
|
52
|
+
* an engine-only `conductor.config.js`, which GAIA-218 makes the canonical repo
|
|
53
|
+
* shape, so keying on it would leave the most common project rooted in the
|
|
54
|
+
* registry default. Which connection won and which project we stand in stay two
|
|
55
|
+
* separate questions, as this function's whole point requires.
|
|
56
|
+
*
|
|
57
|
+
* `~/.gaia` is excluded for the same reason core labels it `home`: the walk-up
|
|
58
|
+
* reaches it from every directory under `$HOME`, so counting it would make
|
|
59
|
+
* every such directory look like "a project rooted at `$HOME`".
|
|
60
|
+
*
|
|
61
|
+
* Pure with respect to precedence — the walk-up is core's; this helper owns no
|
|
62
|
+
* filesystem probe of its own.
|
|
61
63
|
*/
|
|
62
|
-
export declare function
|
|
63
|
-
path: string;
|
|
64
|
-
fallback: boolean;
|
|
65
|
-
};
|
|
64
|
+
export declare function resolveProjectRooting(cwd: string, entries: ConductorRegistryEntry[], homeDir: string): ProjectRooting;
|
package/dist/src/ui-home.js
CHANGED
|
@@ -1,51 +1,10 @@
|
|
|
1
|
-
// GAIA-201 (was GAIA-194)
|
|
2
|
-
// functions that turn the
|
|
3
|
-
// the
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
* Build the home control-plane connection from the machine context. Throws an
|
|
9
|
-
* actionable error when the context is missing `base_url` / `client_id` /
|
|
10
|
-
* `client_secret` (there is no project config to fall back to). The returned
|
|
11
|
-
* `authPlugins` carries a single `session` oauth2 descriptor; `configPath` is
|
|
12
|
-
* filled in by the entry once it has resolved the connection config file.
|
|
13
|
-
*/
|
|
14
|
-
export function buildHomeConnection(machine) {
|
|
15
|
-
const baseUrl = (machine.base_url ?? '').trim();
|
|
16
|
-
const clientId = (machine.client_id ?? '').trim();
|
|
17
|
-
const clientSecret = (machine.client_secret ?? '').trim();
|
|
18
|
-
const missing = [];
|
|
19
|
-
if (baseUrl === '')
|
|
20
|
-
missing.push('base_url');
|
|
21
|
-
if (clientId === '')
|
|
22
|
-
missing.push('client_id');
|
|
23
|
-
if (clientSecret === '')
|
|
24
|
-
missing.push('client_secret');
|
|
25
|
-
if (missing.length > 0) {
|
|
26
|
-
throw new Error(`gaia ui: the machine context is missing ${missing.join(', ')} — ` +
|
|
27
|
-
`run 'gaia conductor init --base-url <url>' to onboard this machine.`);
|
|
28
|
-
}
|
|
29
|
-
const descriptor = {
|
|
30
|
-
plugin: '@dropsh/plugin-oauth2',
|
|
31
|
-
export: 'oauth2Plugin',
|
|
32
|
-
with: {
|
|
33
|
-
id: 'session',
|
|
34
|
-
default: true,
|
|
35
|
-
type: 'oauth2_client_credentials',
|
|
36
|
-
client_id: clientId,
|
|
37
|
-
client_secret: clientSecret,
|
|
38
|
-
token_url: `${baseUrl}/oauth/token`,
|
|
39
|
-
scope: 'gaia:session',
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
return {
|
|
43
|
-
baseUrl,
|
|
44
|
-
jsonapiPrefix: '/jsonapi',
|
|
45
|
-
authPlugins: [descriptor],
|
|
46
|
-
authProfile: 'session',
|
|
47
|
-
};
|
|
48
|
-
}
|
|
1
|
+
// GAIA-201 (was GAIA-194), GAIA-219: bootstrap builders for `gaia ui`. Pure
|
|
2
|
+
// functions that turn the machine context (identity), the conductor registry
|
|
3
|
+
// and the cwd into everything `runGaiaUi` needs. The CONNECTION itself is not
|
|
4
|
+
// built here — `ui.ts` asks core's `loadGaiaConfig` for it, so this package
|
|
5
|
+
// holds no connection-precedence rule of its own (AC-6).
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import { findProjectGaiaDir, homeGaiaDir, shellQuote, } from '@gaia-ai/core';
|
|
49
8
|
/**
|
|
50
9
|
* Resolve the default project (opaque group) from the conductor registry: the
|
|
51
10
|
* `preferred` project when it is registered, else the first entry's project.
|
|
@@ -68,15 +27,52 @@ export function resolveAgentCommand(machine) {
|
|
|
68
27
|
return (prompt) => prompt.trim() === '' ? bin : `${bin} ${shellQuote(prompt)}`;
|
|
69
28
|
}
|
|
70
29
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
30
|
+
* Decide the agents' working directory + project (GAIA-219 AC-9).
|
|
31
|
+
*
|
|
32
|
+
* The trigger is the **cwd**, deliberately not the winning connection config:
|
|
33
|
+
* "which connection did we resolve" and "which project am I standing in" are
|
|
34
|
+
* different questions, and binding them would let an explicit `--config`
|
|
35
|
+
* silently relocate the agents' working directory.
|
|
36
|
+
*
|
|
37
|
+
* 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.
|
|
42
|
+
*
|
|
43
|
+
* The marker is core's `findProjectGaiaDir` — "is there a `.gaia/` here" —
|
|
44
|
+
* NOT the connection walk-up `findGaiaConfig`. Since GAIA-230 the latter skips
|
|
45
|
+
* 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.
|
|
49
|
+
*
|
|
50
|
+
* `~/.gaia` is excluded for the same reason core labels it `home`: the walk-up
|
|
51
|
+
* reaches it from every directory under `$HOME`, so counting it would make
|
|
52
|
+
* every such directory look like "a project rooted at `$HOME`".
|
|
53
|
+
*
|
|
54
|
+
* Pure with respect to precedence — the walk-up is core's; this helper owns no
|
|
55
|
+
* filesystem probe of its own.
|
|
77
56
|
*/
|
|
78
|
-
export function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
57
|
+
export function resolveProjectRooting(cwd, entries, homeDir) {
|
|
58
|
+
const gaiaDir = findProjectGaiaDir(cwd);
|
|
59
|
+
// Core owns where `.gaia` lives; the ui never spells the path itself.
|
|
60
|
+
const homeDotGaia = resolve(homeGaiaDir(homeDir));
|
|
61
|
+
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
|
+
};
|
|
70
|
+
}
|
|
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
|
+
};
|
|
82
78
|
}
|
package/dist/src/ui.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type ConductorRegistryEntry, type GaiaCommandHost, type GaiaCommandPlugin, type MachineContext } from '@gaia-ai/core';
|
|
2
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';
|
|
3
|
+
import { type UiInitialRoute } from './route.js';
|
|
3
4
|
import { type HomeConnection } from './ui-home.js';
|
|
4
5
|
/** Prompt-level agent service the renderer consumes (mirror of gaia-ui's `TuiAgentService`). */
|
|
5
6
|
interface TuiAgentService {
|
|
@@ -43,6 +44,9 @@ export interface RunGaiaUiOptions {
|
|
|
43
44
|
plugins: unknown[];
|
|
44
45
|
}) => DropshProgram;
|
|
45
46
|
renderer?: ((opts: unknown) => unknown) | undefined;
|
|
47
|
+
/** GAIA-223: the deep-link target, still UNRESOLVED (the entry has no JSON:API
|
|
48
|
+
* client — Finding 1). Absent ⇒ the renderer boots its dashboard (AC-4). */
|
|
49
|
+
initialRoute?: UiInitialRoute | undefined;
|
|
46
50
|
}
|
|
47
51
|
/** Test/composition seams for `gaia ui`. */
|
|
48
52
|
export interface UiDeps {
|
|
@@ -54,18 +58,49 @@ export interface UiDeps {
|
|
|
54
58
|
agentHost?: AgentLaunchHost;
|
|
55
59
|
/** The renderer launcher; default: the dynamically-resolved `runGaiaUi`. */
|
|
56
60
|
runUi?: (opts: RunGaiaUiOptions) => Promise<void>;
|
|
57
|
-
/** Pre-resolved home connection config path; default: resolved from ~/.gaia or the shipped fallback. */
|
|
58
|
-
homeConfigPath?: string;
|
|
59
61
|
/** Host contract (resolveBases) for renderer resolution; default: cwd + this install. */
|
|
60
62
|
host?: GaiaCommandHost;
|
|
63
|
+
/** Explicit connection config (`--config`); beats project + home (AC-4). */
|
|
64
|
+
explicitConfig?: string;
|
|
65
|
+
/** Where the resolution + the rooting start; default: `process.cwd()`. */
|
|
66
|
+
cwd?: string;
|
|
67
|
+
/** Connection loader; default: core's `loadGaiaConfig` (the ONE rule). */
|
|
68
|
+
loadConnection?: (host: GaiaCommandHost, opts: {
|
|
69
|
+
cwd?: string;
|
|
70
|
+
explicit?: string | undefined;
|
|
71
|
+
}) => Promise<GaiaConnectionConfig>;
|
|
72
|
+
/** Print the resolved connection and exit without launching the TUI (AC-7). */
|
|
73
|
+
printConfig?: boolean;
|
|
74
|
+
/** Sink for `--print-config`; default: `process.stdout.write`. */
|
|
75
|
+
stdout?: (s: string) => void;
|
|
76
|
+
/** Positional `[target]` (`GAIA-221`, `ticket:…`, `project:…`, `run:…`). */
|
|
77
|
+
target?: string | undefined;
|
|
78
|
+
/** `--ticket <id>`. */
|
|
79
|
+
ticket?: string | undefined;
|
|
80
|
+
/** `--project <id>`. */
|
|
81
|
+
project?: string | undefined;
|
|
82
|
+
/** `--run <uuid>`. */
|
|
83
|
+
run?: string | undefined;
|
|
84
|
+
/** `--here` — derive the target from the cwd (Decision 3). */
|
|
85
|
+
here?: boolean | undefined;
|
|
86
|
+
/** `--view <mode>` — how a ticket target renders (Decision 13). */
|
|
87
|
+
view?: string | undefined;
|
|
88
|
+
/** Current git branch of a dir; default `git branch --show-current`. */
|
|
89
|
+
readBranch?: (cwd: string) => Promise<string>;
|
|
90
|
+
/** The dir's `origin` remote URL; default `git remote get-url origin`. */
|
|
91
|
+
readRemoteUrl?: (cwd: string) => Promise<string>;
|
|
92
|
+
/** Where user-facing grammar errors go; default `process.stderr`. */
|
|
93
|
+
stderr?: (line: string) => void;
|
|
61
94
|
}
|
|
62
95
|
/**
|
|
63
|
-
* `gaia ui` — launch the
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
96
|
+
* `gaia ui` — launch the interactive terminal UI. Resolves the connection
|
|
97
|
+
* through core's `loadGaiaConfig` (explicit → project walk-up → home → shipped
|
|
98
|
+
* fallback), roots the agents in the project it was started in, reads the
|
|
99
|
+
* machine context for identity + the agent binary, adapts a herdr-backed agent
|
|
100
|
+
* host to the renderer's prompt-level service, and delegates to
|
|
101
|
+
* `@gaia-ai/addon-gaia-ui`'s `runGaiaUi`. `--print-config` reports the
|
|
102
|
+
* resolution and exits without launching. `$GAIA_UI_PLUGIN` overrides the
|
|
103
|
+
* renderer module (dev).
|
|
69
104
|
*/
|
|
70
105
|
export declare function cmdUi(deps?: UiDeps): Promise<void>;
|
|
71
106
|
/** The `ui` command plugin the host mounts (`gaia ui`). */
|
package/dist/src/ui.js
CHANGED
|
@@ -1,11 +1,80 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
3
|
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import { CommandRunner, createLogger, exec, homeFallbackGaiaConfigPath, listRegisteredConductors, machineContextPath, readMachineContext, setDefaultCommandRunner, } from '@gaia-ai/core';
|
|
6
4
|
import { herdrAgentHost } from '@gaia-ai/addon-herdr';
|
|
5
|
+
import { CommandRunner, createLogger, exec, listRegisteredConductors, loadGaiaConfig, machineContextPath, readMachineContext, setDefaultCommandRunner, } from '@gaia-ai/core';
|
|
7
6
|
import { buildProgram as buildDropshProgram } from 'dropsh';
|
|
8
|
-
import {
|
|
7
|
+
import { parseTarget, resolveHere, UI_VIEW_MODES, } from './route.js';
|
|
8
|
+
import { resolveAgentCommand, resolveProjectRooting, } from './ui-home.js';
|
|
9
|
+
/**
|
|
10
|
+
* The current branch of `cwd`. Git resolves this PER WORKTREE, so
|
|
11
|
+
* worktree-awareness for `--here` costs nothing (Decision 3). Never throws:
|
|
12
|
+
* outside a repo, or on a detached HEAD, it yields '' and the ladder falls
|
|
13
|
+
* through to its project rung.
|
|
14
|
+
*/
|
|
15
|
+
async function readCurrentBranch(cwd) {
|
|
16
|
+
try {
|
|
17
|
+
const out = await exec('git', ['branch', '--show-current'], { cwd });
|
|
18
|
+
return typeof out === 'string' ? out.trim() : String(out ?? '').trim();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The cwd's `origin` remote URL, raw. Git resolves it per worktree, and a
|
|
26
|
+
* worktree shares its main clone's remote — exactly the right answer for
|
|
27
|
+
* "which repo am I standing in" (Decision 3). Never throws: outside a repo, or
|
|
28
|
+
* with no `origin`, it yields '' and the ladder produces its named error.
|
|
29
|
+
*
|
|
30
|
+
* Revision 4 replaced the previous rung (an `import()` of the repo's engine
|
|
31
|
+
* `conductor.config.js` plus a conductor-registry longest-prefix fallback) with
|
|
32
|
+
* this one read: the registry cannot answer inside a worktree (its paths point
|
|
33
|
+
* at `.gaia` DIRS, and a worktree has its own unregistered one), the config read
|
|
34
|
+
* needed a dynamic import of a possibly secret-bearing module, and the remote
|
|
35
|
+
* works in a fresh clone that has no `.gaia/` at all.
|
|
36
|
+
*/
|
|
37
|
+
async function readOriginRemoteUrl(cwd) {
|
|
38
|
+
try {
|
|
39
|
+
const out = await exec('git', ['remote', 'get-url', 'origin'], { cwd });
|
|
40
|
+
return typeof out === 'string' ? out.trim() : String(out ?? '').trim();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The explicit connection leg (AC-4, D2): `--config` → `$GAIA_CONFIG` →
|
|
48
|
+
* `$DROPSH_CONFIG`, first non-empty wins. The two env vars are read because
|
|
49
|
+
* core's resolver already names all three as the explicit leg and the renderer's
|
|
50
|
+
* launcher already honours a pre-set `$DROPSH_CONFIG` — so the path `gaia ui`
|
|
51
|
+
* reports and the file dropsh loads can never disagree.
|
|
52
|
+
*/
|
|
53
|
+
function resolveExplicitConfig(flag) {
|
|
54
|
+
for (const candidate of [
|
|
55
|
+
flag,
|
|
56
|
+
process.env.GAIA_CONFIG,
|
|
57
|
+
process.env.DROPSH_CONFIG,
|
|
58
|
+
]) {
|
|
59
|
+
if (candidate !== undefined && candidate.trim() !== '')
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/** The `--print-config` block (AC-7): which file won, and what it points at. */
|
|
65
|
+
function formatConnectionReport(connection, rooting) {
|
|
66
|
+
const yesNo = (b) => (b ? 'yes' : 'no');
|
|
67
|
+
const lines = [
|
|
68
|
+
['connection', connection.config_path],
|
|
69
|
+
['source', connection.source],
|
|
70
|
+
['fallback', yesNo(connection.fallback)],
|
|
71
|
+
['legacy', yesNo(connection.legacy)],
|
|
72
|
+
['base_url', connection.site.base_url],
|
|
73
|
+
['cwd', rooting.cwd],
|
|
74
|
+
['project', rooting.project ?? '(none)'],
|
|
75
|
+
];
|
|
76
|
+
return `${lines.map(([k, v]) => `${`${k}:`.padEnd(12)}${v}`).join('\n')}\n`;
|
|
77
|
+
}
|
|
9
78
|
function loggerFor(checkoutRoot) {
|
|
10
79
|
const logger = createLogger({ checkoutRoot });
|
|
11
80
|
setDefaultCommandRunner(new CommandRunner(logger));
|
|
@@ -36,50 +105,109 @@ async function loadGaiaUiModule(logger, bases) {
|
|
|
36
105
|
return (await import(pathToFileURL(resolved).href));
|
|
37
106
|
}
|
|
38
107
|
/**
|
|
39
|
-
* `gaia ui` — launch the
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
108
|
+
* `gaia ui` — launch the interactive terminal UI. Resolves the connection
|
|
109
|
+
* through core's `loadGaiaConfig` (explicit → project walk-up → home → shipped
|
|
110
|
+
* fallback), roots the agents in the project it was started in, reads the
|
|
111
|
+
* machine context for identity + the agent binary, adapts a herdr-backed agent
|
|
112
|
+
* host to the renderer's prompt-level service, and delegates to
|
|
113
|
+
* `@gaia-ai/addon-gaia-ui`'s `runGaiaUi`. `--print-config` reports the
|
|
114
|
+
* resolution and exits without launching. `$GAIA_UI_PLUGIN` overrides the
|
|
115
|
+
* renderer module (dev).
|
|
45
116
|
*/
|
|
46
117
|
export async function cmdUi(deps = {}) {
|
|
47
118
|
const logger = loggerFor(homedir());
|
|
48
119
|
const host = deps.host ?? {
|
|
49
120
|
resolveBases: [`${process.cwd()}/`, import.meta.url],
|
|
50
121
|
};
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
122
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
123
|
+
// User-facing CLI errors go to stderr, not the log sink: `createLogger` writes
|
|
124
|
+
// to `$HOME/log.txt` whenever stdout is not a TTY, and `gaia ui GAIA-1 | cat`
|
|
125
|
+
// must still SAY why it failed. Spec Decision 5: "a stderr line and exit 1".
|
|
126
|
+
const fail = (message) => {
|
|
127
|
+
(deps.stderr ?? ((line) => process.stderr.write(line)))(`${message}\n`);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
};
|
|
130
|
+
// GAIA-223: build the deep-link route FIRST — argv parsing, the conflict rule
|
|
131
|
+
// and the whole `--here` ladder are offline, so every one of their failures
|
|
132
|
+
// costs no config load and no network round-trip, and never reaches the
|
|
133
|
+
// launcher (AC-1/AC-2).
|
|
134
|
+
let initialRoute;
|
|
135
|
+
try {
|
|
136
|
+
const parsed = parseTarget({
|
|
137
|
+
target: deps.target,
|
|
138
|
+
ticket: deps.ticket,
|
|
139
|
+
project: deps.project,
|
|
140
|
+
run: deps.run,
|
|
141
|
+
here: deps.here,
|
|
142
|
+
view: deps.view,
|
|
143
|
+
});
|
|
144
|
+
if (parsed?.kind === 'route') {
|
|
145
|
+
initialRoute = parsed.route;
|
|
69
146
|
}
|
|
70
|
-
if (
|
|
71
|
-
|
|
147
|
+
else if (parsed?.kind === 'here') {
|
|
148
|
+
initialRoute = await resolveHere({
|
|
149
|
+
cwd,
|
|
150
|
+
...(parsed.view !== undefined ? { view: parsed.view } : {}),
|
|
151
|
+
readBranch: deps.readBranch ?? readCurrentBranch,
|
|
152
|
+
readRemoteUrl: deps.readRemoteUrl ?? readOriginRemoteUrl,
|
|
153
|
+
});
|
|
72
154
|
}
|
|
73
155
|
}
|
|
74
|
-
|
|
75
|
-
|
|
156
|
+
catch (err) {
|
|
157
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
158
|
+
return;
|
|
76
159
|
}
|
|
77
|
-
|
|
160
|
+
// Read only AFTER the route: the whole grammar + ladder is offline, so a
|
|
161
|
+
// parse/conflict/no-remote failure must not even touch the registry file.
|
|
78
162
|
const entries = deps.registryEntries ?? (await listRegisteredConductors());
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
163
|
+
const mcPath = machineContextPath();
|
|
164
|
+
// The machine context still supplies identity (`user_id`) + the agent binary;
|
|
165
|
+
// it is NOT the connection any more — core resolves that.
|
|
166
|
+
const machine = deps.machine ?? (await readMachineContext(mcPath).catch(() => ({})));
|
|
167
|
+
// The ONE precedence rule, core's, the same one `gaia dropsh` uses (AC-1/2/3/
|
|
168
|
+
// 4/5/6). A legacy `conductor.config.js` carrying site/plugins is accepted on
|
|
169
|
+
// exactly core's terms — there is no back-compat branch here.
|
|
170
|
+
const load = deps.loadConnection ?? loadGaiaConfig;
|
|
171
|
+
let resolved;
|
|
172
|
+
try {
|
|
173
|
+
resolved = await load(host, {
|
|
174
|
+
cwd,
|
|
175
|
+
explicit: resolveExplicitConfig(deps.explicitConfig),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
logger.error({}, err instanceof Error ? err.message : String(err));
|
|
180
|
+
process.exitCode = 1;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const connection = {
|
|
184
|
+
baseUrl: resolved.site.base_url,
|
|
185
|
+
jsonapiPrefix: resolved.site.jsonapi_prefix,
|
|
186
|
+
authPlugins: resolved.plugins,
|
|
187
|
+
configPath: resolved.config_path,
|
|
188
|
+
authProfile: 'session',
|
|
189
|
+
};
|
|
190
|
+
const rooting = resolveProjectRooting(cwd, entries, homedir());
|
|
191
|
+
const ownConductorMachineId = rooting.ownConductorMachineId;
|
|
192
|
+
const project = rooting.project;
|
|
193
|
+
// AC-7, always on: the resolution lands in the log even when the TUI takes
|
|
194
|
+
// over the screen a moment later.
|
|
195
|
+
logger.info({
|
|
196
|
+
config_path: resolved.config_path,
|
|
197
|
+
source: resolved.source,
|
|
198
|
+
fallback: resolved.fallback,
|
|
199
|
+
legacy: resolved.legacy,
|
|
200
|
+
base_url: resolved.site.base_url,
|
|
201
|
+
cwd: rooting.cwd,
|
|
202
|
+
project,
|
|
203
|
+
}, 'gaia ui: resolved connection');
|
|
204
|
+
// AC-7, the human surface: print and exit 0 WITHOUT booting the TUI, so a
|
|
205
|
+
// wrong-control-plane situation is diagnosable at a prompt.
|
|
206
|
+
if (deps.printConfig) {
|
|
207
|
+
const write = deps.stdout ?? ((s) => process.stdout.write(s));
|
|
208
|
+
write(formatConnectionReport(resolved, rooting));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
83
211
|
const agentCommand = resolveAgentCommand(machine);
|
|
84
212
|
const agentHost = deps.agentHost ?? herdrAgentHost((args) => exec('herdr', args));
|
|
85
213
|
const agents = {
|
|
@@ -122,18 +250,38 @@ export async function cmdUi(deps = {}) {
|
|
|
122
250
|
}
|
|
123
251
|
run = uiMod.runGaiaUi;
|
|
124
252
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
253
|
+
// The launcher (and, through it, the renderer's route resolution — Decision 5)
|
|
254
|
+
// may reject BEFORE the TUI takes the terminal; `await run(...)` was unguarded,
|
|
255
|
+
// so that surfaced as an unhandled rejection. Catch it into exit 1 + a line.
|
|
256
|
+
try {
|
|
257
|
+
await run({
|
|
258
|
+
connection,
|
|
259
|
+
cwd: rooting.cwd,
|
|
260
|
+
...(project !== undefined ? { project } : {}),
|
|
261
|
+
agents,
|
|
262
|
+
...(machine.user_id ? { currentUser: machine.user_id } : {}),
|
|
263
|
+
...(ownConductorMachineId ? { ownConductorMachineId } : {}),
|
|
264
|
+
// The plugins are real constructed `DropSHPlugin[]` now (core built them),
|
|
265
|
+
// matching dropsh's declared contract — F3's substance, a descriptor handed
|
|
266
|
+
// to an API expecting instances, is gone. The two casts that remain are
|
|
267
|
+
// structural, not unsound: `RunGaiaUiOptions` is a local MIRROR of the
|
|
268
|
+
// dynamically-resolved renderer's type (this package must not compile
|
|
269
|
+
// against it), so its `plugins` is `unknown[]` and its program type is the
|
|
270
|
+
// narrow `DropshProgram` above. Both casts only re-tell TypeScript what the
|
|
271
|
+
// mirror deliberately forgot.
|
|
272
|
+
// dropsh uses the plugins for renderers + `registerCommands` only; auth
|
|
273
|
+
// still comes from its own `$DROPSH_CONFIG` reload of the very file we
|
|
274
|
+
// resolved, so nothing is registered twice.
|
|
275
|
+
buildProgram: (o) => buildDropshProgram({
|
|
276
|
+
plugins: o.plugins,
|
|
277
|
+
}),
|
|
278
|
+
...(renderer ? { renderer } : {}),
|
|
279
|
+
...(initialRoute ? { initialRoute } : {}),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
284
|
+
}
|
|
137
285
|
}
|
|
138
286
|
/** The `ui` command plugin the host mounts (`gaia ui`). */
|
|
139
287
|
const uiCommandPlugin = {
|
|
@@ -144,8 +292,46 @@ const uiCommandPlugin = {
|
|
|
144
292
|
program
|
|
145
293
|
.command('ui')
|
|
146
294
|
.description('interactive terminal UI: project dashboard, ticket list, ticket detail')
|
|
147
|
-
.
|
|
148
|
-
|
|
295
|
+
.argument('[target]', 'deep-link target: GAIA-221 · ticket:<id|uuid> · project:<name|uuid> · run:<uuid>')
|
|
296
|
+
.option('--ticket <id>', 'open a ticket by identifier or uuid')
|
|
297
|
+
.option('--project <id>', 'open a project by name or uuid')
|
|
298
|
+
.option('--run <uuid>', "open a run's owning ticket, on its Runs tab")
|
|
299
|
+
.option('--here', "derive the target from the cwd: the branch's ticket, else this repo's project")
|
|
300
|
+
.option('--view <mode>', `how a ticket renders: ${UI_VIEW_MODES.join(' | ')}`)
|
|
301
|
+
.option('--config <path>', 'connection config path; wins over the project and home configs')
|
|
302
|
+
.option('--print-config', 'print the resolved connection config and exit, without launching the UI')
|
|
303
|
+
.addHelpText('after', `
|
|
304
|
+
Targets (at most ONE of the positional, --ticket, --project, --run, --here):
|
|
305
|
+
gaia ui the project dashboard
|
|
306
|
+
gaia ui GAIA-221 a ticket by identifier (case-insensitive)
|
|
307
|
+
gaia ui ticket:GAIA-221 the same, said explicitly
|
|
308
|
+
gaia ui ticket:<uuid> a ticket by uuid
|
|
309
|
+
gaia ui project:gaia a project by name (case-sensitive label)
|
|
310
|
+
gaia ui run:<uuid> that run's owning ticket, Runs tab active
|
|
311
|
+
gaia ui --ticket GAIA-221 the script-friendly flag form
|
|
312
|
+
gaia ui --here this branch's ticket (feat/gaia-223-… → GAIA-223),
|
|
313
|
+
else this repo's project
|
|
314
|
+
|
|
315
|
+
Modifier (orthogonal — never conflicts with a target):
|
|
316
|
+
gaia ui GAIA-221 --view teaser a compact block instead of the tabbed detail
|
|
317
|
+
gaia ui GAIA-221 --view board_card the board-card field set
|
|
318
|
+
|
|
319
|
+
Two target sources at once exit non-zero naming both. A bare uuid is ambiguous
|
|
320
|
+
(prefix it); a bare number is not an identifier. --view applies to ticket
|
|
321
|
+
targets only. Full reference: gaia-cli/ui/README.md
|
|
322
|
+
`)
|
|
323
|
+
.action(async (target, o) => {
|
|
324
|
+
await cmdUi({
|
|
325
|
+
host,
|
|
326
|
+
...(target !== undefined ? { target } : {}),
|
|
327
|
+
...(o.ticket !== undefined ? { ticket: o.ticket } : {}),
|
|
328
|
+
...(o.project !== undefined ? { project: o.project } : {}),
|
|
329
|
+
...(o.run !== undefined ? { run: o.run } : {}),
|
|
330
|
+
...(o.here !== undefined ? { here: o.here } : {}),
|
|
331
|
+
...(o.view !== undefined ? { view: o.view } : {}),
|
|
332
|
+
...(o.config !== undefined ? { explicitConfig: o.config } : {}),
|
|
333
|
+
...(o.printConfig ? { printConfig: true } : {}),
|
|
334
|
+
});
|
|
149
335
|
});
|
|
150
336
|
},
|
|
151
337
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaia-ai/ui",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "GAIA
|
|
3
|
+
"version": "0.6.3",
|
|
4
|
+
"description": "GAIA project-first cockpit: the `gaia ui` command plugin (renderer resolved dynamically).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"directory": "gaia-cli/ui"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@gaia-ai/core": "^0.6.
|
|
30
|
-
"@gaia-ai/addon-herdr": "^0.6.
|
|
29
|
+
"@gaia-ai/core": "^0.6.3",
|
|
30
|
+
"@gaia-ai/addon-herdr": "^0.6.3",
|
|
31
31
|
"commander": "^12.1.0",
|
|
32
32
|
"dropsh": "^0.5.8"
|
|
33
33
|
}
|