@gaia-ai/conductor 0.5.5 → 0.6.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/README.md +1 -1
- package/dist/src/cli/config-schema.d.ts +89 -0
- package/dist/src/cli/config-schema.js +146 -0
- package/dist/src/cli/init.d.ts +31 -33
- package/dist/src/cli/init.js +106 -84
- package/dist/src/cli/migrate-addon-names.d.ts +72 -0
- package/dist/src/cli/migrate-addon-names.js +318 -0
- package/dist/src/cli/upgrade.d.ts +53 -0
- package/dist/src/cli/upgrade.js +222 -0
- package/dist/src/cli/version-check.js +5 -1
- package/dist/src/commands/conductor.d.ts +57 -0
- package/dist/src/{cli/gaia.js → commands/conductor.js} +107 -215
- package/dist/src/config.d.ts +65 -24
- package/dist/src/config.js +405 -154
- package/dist/src/contract.d.ts +8 -0
- package/dist/src/contract.js +16 -0
- package/dist/src/core/conductor.d.ts +16 -1
- package/dist/src/core/conductor.js +28 -9
- package/dist/src/index.d.ts +7 -5
- package/dist/src/index.js +26 -3
- package/dist/src/plugins/agent.d.ts +61 -0
- package/dist/src/plugins/agent.js +11 -0
- package/dist/src/plugins/executor.d.ts +104 -0
- package/dist/src/plugins/executor.js +1 -0
- package/dist/src/plugins/plugins.d.ts +60 -0
- package/dist/src/plugins/plugins.js +42 -0
- package/dist/src/plugins/preset.d.ts +48 -0
- package/dist/src/plugins/preset.js +23 -0
- package/dist/src/plugins/remote.d.ts +203 -0
- package/dist/src/plugins/remote.js +1 -0
- package/dist/src/plugins/workspace.d.ts +35 -0
- package/dist/src/plugins/workspace.js +1 -0
- package/dist/src/preset.d.ts +2 -0
- package/dist/src/preset.js +8 -0
- package/dist/src/types.d.ts +65 -0
- package/dist/src/types.js +1 -0
- package/package.json +8 -5
- package/dist/src/cli/gaia.d.ts +0 -23
- package/dist/src/cli/local-registry.d.ts +0 -14
- package/dist/src/cli/local-registry.js +0 -56
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type ConductorLogger, type GaiaCommandHost, type GaiaCommandPlugin } from '@gaia-ai/core';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import type { GaiaExecutor } from '../plugins/executor.js';
|
|
4
|
+
import { type ResolvedAgent } from '../plugins/plugins.js';
|
|
5
|
+
import type { GaiaRemote } from '../plugins/remote.js';
|
|
6
|
+
import type { GaiaWorkspace } from '../plugins/workspace.js';
|
|
7
|
+
import type { ConductorFileConfig } from '../types.js';
|
|
8
|
+
/** Test seam: inject any subset of dependencies. */
|
|
9
|
+
export interface GaiaCliDeps {
|
|
10
|
+
remote?: GaiaRemote;
|
|
11
|
+
executor?: GaiaExecutor;
|
|
12
|
+
workspace?: GaiaWorkspace;
|
|
13
|
+
agents?: ResolvedAgent[];
|
|
14
|
+
config?: ConductorFileConfig;
|
|
15
|
+
/** Injectable registry fetch for the start-time version check (tests). */
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
/** Host contract (resolveBases); default: this install + cwd. */
|
|
18
|
+
host?: GaiaCommandHost;
|
|
19
|
+
/**
|
|
20
|
+
* GAIA-222: the herdr process host behind `start` / `stop`. Defaults to the
|
|
21
|
+
* real shell-out (`realHerdrHost`) — a test MUST inject a fake, or the suite
|
|
22
|
+
* creates real workspaces + real conductor processes on the developer's
|
|
23
|
+
* machine.
|
|
24
|
+
*/
|
|
25
|
+
herdr?: HerdrHost;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The multiplexer seam of the conductor lifecycle: start a detached foreground
|
|
29
|
+
* loop, and kill it again by its handle. One interface so both directions are
|
|
30
|
+
* injectable — `killViaHerdr` is reached by BOTH `stop` and `stop --now`.
|
|
31
|
+
*/
|
|
32
|
+
export interface HerdrHost {
|
|
33
|
+
spawn(label: string, cwd: string, cmd: string): Promise<void>;
|
|
34
|
+
kill(label: string): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
export declare function parseHerdrJson(output: string, command: string): unknown;
|
|
37
|
+
/** The production host: the real `herdr` binary. */
|
|
38
|
+
export declare const realHerdrHost: HerdrHost;
|
|
39
|
+
/**
|
|
40
|
+
* Start-time auth gate. Returns true if authenticated (session or session-less
|
|
41
|
+
* provider); otherwise logs a single clear line and returns false.
|
|
42
|
+
*/
|
|
43
|
+
export declare function ensureAuthenticated(config: ConductorFileConfig, logger: ConductorLogger): Promise<boolean>;
|
|
44
|
+
/** Build the `conductor` subcommand tree (lifecycle + registry + init). */
|
|
45
|
+
export declare function createConductorCommand(deps: GaiaCliDeps): Command;
|
|
46
|
+
/** The `conductor` command plugin the host mounts (GAIA-201). GAIA-224
|
|
47
|
+
* (Finding 7): the `gaia deployment` batch helper is no longer registered here —
|
|
48
|
+
* it is its own command addon, `@gaia-ai/addon-deployment`. */
|
|
49
|
+
declare const conductorCommandPlugin: GaiaCommandPlugin;
|
|
50
|
+
export default conductorCommandPlugin;
|
|
51
|
+
/**
|
|
52
|
+
* Build a standalone program with the conductor command mounted — the test entry
|
|
53
|
+
* (mirrors what the host does for `gaia conductor …`). `deps` inject fakes.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildConductorProgram(deps?: GaiaCliDeps): Command;
|
|
56
|
+
/** Parse argv against the conductor program (test entry). */
|
|
57
|
+
export declare function runConductorCli(argv: string[], deps?: GaiaCliDeps): Promise<void>;
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
|
-
import { CommandRunner, createLogger, exec, setDefaultCommandRunner, } from '@gaia-ai/core';
|
|
5
|
-
import { selectAgents, selectExecutor, selectRemote, selectWorkspace, } from '@gaia-ai/core/plugins';
|
|
4
|
+
import { CommandRunner, createLogger, exec, getRegisteredConductor, listRegisteredConductors, loadGaiaConfig, machineContextPath, readMachineContext, registerConductor, removeConductor, resolveConfigPath, setDefaultCommandRunner, } from '@gaia-ai/core';
|
|
6
5
|
import { Command } from 'commander';
|
|
7
|
-
import { authStatus
|
|
8
|
-
import {
|
|
6
|
+
import { authStatus } from 'dropsh';
|
|
7
|
+
import { scaffold } from '../cli/init.js';
|
|
8
|
+
import { runUpgrade } from '../cli/upgrade.js';
|
|
9
|
+
import { fetchUpdateNotice, printVersionLine } from '../cli/version-check.js';
|
|
10
|
+
import { composeConductorConfig, loadConductorConfig } from '../config.js';
|
|
9
11
|
import { Conductor } from '../core/conductor.js';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
import { selectAgents, selectExecutor, selectRemote, selectWorkspace, } from '../plugins/plugins.js';
|
|
13
|
+
/** Default host bases for connection/plugin resolution when none is injected. */
|
|
14
|
+
function defaultHost() {
|
|
15
|
+
return { resolveBases: [import.meta.url, `${process.cwd()}/`] };
|
|
16
|
+
}
|
|
13
17
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `gaia`
|
|
17
|
-
*
|
|
18
|
+
* Resolve + compose the full conductor config: the engine half
|
|
19
|
+
* (`conductor.config.js` via `loadConductorConfig`) plus the connection half
|
|
20
|
+
* (`gaia.config.js` via `loadGaiaConfig`, walk-up → home → shipped fallback).
|
|
21
|
+
* `--config` / `$GAIA_CONDUCTOR_CONFIG` selects the engine file; the connection
|
|
22
|
+
* is resolved from cwd.
|
|
18
23
|
*/
|
|
19
|
-
function defaultConfigPath(override) {
|
|
20
|
-
return resolveConfigPath(override);
|
|
21
|
-
}
|
|
22
24
|
async function resolveConfig(deps, configPathOverride) {
|
|
23
25
|
if (deps.config) {
|
|
24
26
|
return deps.config;
|
|
25
27
|
}
|
|
26
|
-
|
|
28
|
+
const host = deps.host ?? defaultHost();
|
|
29
|
+
const engine = await loadConductorConfig(resolveConfigPath(configPathOverride));
|
|
30
|
+
const connection = await loadGaiaConfig(host, {});
|
|
31
|
+
return composeConductorConfig(engine, connection);
|
|
27
32
|
}
|
|
28
33
|
async function resolveRemote(deps, config) {
|
|
29
34
|
return deps.remote ?? (await selectRemote(config));
|
|
@@ -33,9 +38,8 @@ function checkoutRootOf(config) {
|
|
|
33
38
|
}
|
|
34
39
|
/**
|
|
35
40
|
* The conductor's stable identity (gaia_conductor.machine_id). machine_id is
|
|
36
|
-
* required — the loader
|
|
37
|
-
*
|
|
38
|
-
* somehow lacks it fails loudly rather than silently minting a divergent id.
|
|
41
|
+
* required — the loader throws without it — so lifecycle commands read it here,
|
|
42
|
+
* never re-derive.
|
|
39
43
|
*/
|
|
40
44
|
function conductorIdOf(config) {
|
|
41
45
|
const id = config.machine_id;
|
|
@@ -58,7 +62,7 @@ function loggerFor(checkoutRoot, log = {}) {
|
|
|
58
62
|
setDefaultCommandRunner(new CommandRunner(logger));
|
|
59
63
|
return logger;
|
|
60
64
|
}
|
|
61
|
-
// --- herdr host (
|
|
65
|
+
// --- herdr host (shells out to herdr; injectable via deps.herdr) -----------
|
|
62
66
|
export function parseHerdrJson(output, command) {
|
|
63
67
|
try {
|
|
64
68
|
return JSON.parse(output);
|
|
@@ -94,6 +98,14 @@ async function killViaHerdr(label) {
|
|
|
94
98
|
await exec('herdr', ['workspace', 'close', ws.workspace_id]);
|
|
95
99
|
}
|
|
96
100
|
}
|
|
101
|
+
/** The production host: the real `herdr` binary. */
|
|
102
|
+
export const realHerdrHost = {
|
|
103
|
+
spawn: spawnViaHerdr,
|
|
104
|
+
kill: killViaHerdr,
|
|
105
|
+
};
|
|
106
|
+
function herdrHostOf(deps) {
|
|
107
|
+
return deps.herdr ?? realHerdrHost;
|
|
108
|
+
}
|
|
97
109
|
// --- ls/status freshness ----------------------------------------------------
|
|
98
110
|
const DEFAULT_FRESH_S = 120;
|
|
99
111
|
function freshnessThresholdS(config) {
|
|
@@ -102,8 +114,6 @@ function freshnessThresholdS(config) {
|
|
|
102
114
|
: DEFAULT_FRESH_S;
|
|
103
115
|
}
|
|
104
116
|
function classify(hub, freshS) {
|
|
105
|
-
// No host probe here (host calls are untested) → can't tell host-missing
|
|
106
|
-
// from wedged. registry+no-hub = registry-only; stale-hub = wedged.
|
|
107
117
|
if (!hub) {
|
|
108
118
|
return 'registry-only';
|
|
109
119
|
}
|
|
@@ -115,7 +125,7 @@ function classify(hub, freshS) {
|
|
|
115
125
|
return fresh ? 'running' : 'wedged';
|
|
116
126
|
}
|
|
117
127
|
async function buildLsRows(remote, config, onlyId) {
|
|
118
|
-
const entries = await
|
|
128
|
+
const entries = await listRegisteredConductors();
|
|
119
129
|
let hub = [];
|
|
120
130
|
try {
|
|
121
131
|
hub = await remote.listConductors('me');
|
|
@@ -161,9 +171,7 @@ function printRows(rows) {
|
|
|
161
171
|
// --- command handlers -------------------------------------------------------
|
|
162
172
|
/**
|
|
163
173
|
* Start-time auth gate. Returns true if authenticated (session or session-less
|
|
164
|
-
* provider); otherwise logs a single clear line and returns false.
|
|
165
|
-
* before remote resolution (resolveRemote), which calls resolveAuth and throws
|
|
166
|
-
* when unauthenticated.
|
|
174
|
+
* provider); otherwise logs a single clear line and returns false.
|
|
167
175
|
*/
|
|
168
176
|
export async function ensureAuthenticated(config, logger) {
|
|
169
177
|
const st = await authStatus({
|
|
@@ -201,10 +209,6 @@ async function cmdReap(deps, log = {}) {
|
|
|
201
209
|
const workspace = deps.workspace ?? (await selectWorkspace(config));
|
|
202
210
|
const agents = deps.agents ?? (await selectAgents(config));
|
|
203
211
|
const conductor = new Conductor(config, remote, executor, workspace, agents, logger, checkoutRoot);
|
|
204
|
-
// The reaper reconciles finished-but-uncleaned tickets (the cleaned_up flag)
|
|
205
|
-
// against their worktrees; it needs no registration/heartbeat (it is not a
|
|
206
|
-
// poll), just the executor + remote, so it runs standalone after a
|
|
207
|
-
// crash/restart.
|
|
208
212
|
await conductor.reap();
|
|
209
213
|
}
|
|
210
214
|
async function cmdStartForeground(deps, log = {}) {
|
|
@@ -232,12 +236,6 @@ async function cmdStartForeground(deps, log = {}) {
|
|
|
232
236
|
}
|
|
233
237
|
}
|
|
234
238
|
async function cmdStart(deps, log = {}) {
|
|
235
|
-
// Print the installed version (AC-2) immediately, then kick the newer-version
|
|
236
|
-
// registry check off CONCURRENTLY with config resolution so a slow/offline
|
|
237
|
-
// registry never stalls start (finding #1 — the check no longer runs serially
|
|
238
|
-
// before resolveConfig). Fail-silent + bounded (AC-3/AC-4/AC-5); the notice is
|
|
239
|
-
// printed once it resolves, after the start work below. Not in
|
|
240
|
-
// cmdStartForeground — that path is herdr-hosted, no TTY.
|
|
241
239
|
const current = printVersionLine();
|
|
242
240
|
const noticePromise = fetchUpdateNotice(current, deps.fetch ?? globalThis.fetch).catch(() => null);
|
|
243
241
|
try {
|
|
@@ -257,7 +255,7 @@ async function cmdStartBody(deps, log) {
|
|
|
257
255
|
return;
|
|
258
256
|
const remote = await resolveRemote(deps, config);
|
|
259
257
|
const id = conductorIdOf(config);
|
|
260
|
-
const existing = await
|
|
258
|
+
const existing = await getRegisteredConductor(id);
|
|
261
259
|
if (existing) {
|
|
262
260
|
const hubStatus = await remote.getConductorStatus(id);
|
|
263
261
|
if (hubStatus !== null && hubStatus !== 'offline') {
|
|
@@ -266,7 +264,7 @@ async function cmdStartBody(deps, log) {
|
|
|
266
264
|
}
|
|
267
265
|
}
|
|
268
266
|
const handle = `gaia-conductor:${id}`;
|
|
269
|
-
await
|
|
267
|
+
await registerConductor({
|
|
270
268
|
id,
|
|
271
269
|
path: checkoutRoot,
|
|
272
270
|
project: config.project,
|
|
@@ -274,9 +272,6 @@ async function cmdStartBody(deps, log) {
|
|
|
274
272
|
host: 'herdr',
|
|
275
273
|
handle,
|
|
276
274
|
});
|
|
277
|
-
// The detached foreground process is a fresh CLI invocation — forward the
|
|
278
|
-
// log flags so the herdr-hosted loop logs at the requested level. Sink stays
|
|
279
|
-
// forced to file (herdr-hosted = no TTY) unless the caller overrode it.
|
|
280
275
|
const fgFlags = [
|
|
281
276
|
log.level ? `--log-level ${log.level}` : '',
|
|
282
277
|
log.sink ? `--log-sink ${log.sink}` : '',
|
|
@@ -285,7 +280,7 @@ async function cmdStartBody(deps, log) {
|
|
|
285
280
|
.join(' ');
|
|
286
281
|
const fgCmd = `GAIA_CONDUCTOR_LOG=file gaia conductor ${fgFlags} start --foreground`.replace(/\s+/g, ' ');
|
|
287
282
|
try {
|
|
288
|
-
await
|
|
283
|
+
await herdrHostOf(deps).spawn(handle, checkoutRoot, fgCmd);
|
|
289
284
|
logger.info({ id, handle }, 'started conductor via herdr');
|
|
290
285
|
}
|
|
291
286
|
catch (err) {
|
|
@@ -298,26 +293,19 @@ async function cmdStop(deps, now) {
|
|
|
298
293
|
const remote = await resolveRemote(deps, config);
|
|
299
294
|
const id = conductorIdOf(config);
|
|
300
295
|
if (now) {
|
|
301
|
-
const entry = await
|
|
296
|
+
const entry = await getRegisteredConductor(id);
|
|
302
297
|
if (entry && entry.host === 'herdr') {
|
|
303
|
-
await
|
|
298
|
+
await herdrHostOf(deps).kill(entry.handle);
|
|
304
299
|
console.log(`hard-stopped conductor ${id} (${entry.handle})`);
|
|
305
300
|
}
|
|
306
301
|
else {
|
|
307
302
|
console.log(`no herdr-hosted conductor to hard-stop for ${id}`);
|
|
308
303
|
}
|
|
309
|
-
// A hard kill just stops the heartbeat; it never deletes the entity. The
|
|
310
|
-
// Drupal cron reaper flips the now-stale registration to offline (single
|
|
311
|
-
// authority for the offline transition — see gaia_core cron).
|
|
312
304
|
return;
|
|
313
305
|
}
|
|
314
|
-
|
|
315
|
-
// just like a hard kill, but writes offline itself instead of waiting for the
|
|
316
|
-
// cron reaper. Stop the process first (else its next heartbeat would flip it
|
|
317
|
-
// back online), then mark it offline. In-flight runs are not awaited.
|
|
318
|
-
const entry = await registry.get(id);
|
|
306
|
+
const entry = await getRegisteredConductor(id);
|
|
319
307
|
if (entry && entry.host === 'herdr') {
|
|
320
|
-
await
|
|
308
|
+
await herdrHostOf(deps).kill(entry.handle);
|
|
321
309
|
}
|
|
322
310
|
await remote.setConductorStatus(id, 'offline');
|
|
323
311
|
console.log(`stopped conductor ${id} (offline)`);
|
|
@@ -338,7 +326,7 @@ async function cmdStatus(deps) {
|
|
|
338
326
|
async function cmdRm(deps) {
|
|
339
327
|
const config = await resolveConfig(deps);
|
|
340
328
|
const id = conductorIdOf(config);
|
|
341
|
-
await
|
|
329
|
+
await removeConductor(id);
|
|
342
330
|
console.log(`removed conductor ${id} from registry`);
|
|
343
331
|
}
|
|
344
332
|
/** ls may run without a config file; best-effort load. */
|
|
@@ -347,7 +335,7 @@ async function tryConfig(deps) {
|
|
|
347
335
|
return deps.config;
|
|
348
336
|
}
|
|
349
337
|
try {
|
|
350
|
-
return await
|
|
338
|
+
return await resolveConfig(deps);
|
|
351
339
|
}
|
|
352
340
|
catch {
|
|
353
341
|
return undefined;
|
|
@@ -371,7 +359,6 @@ async function promptSecret() {
|
|
|
371
359
|
output: process.stdout,
|
|
372
360
|
terminal: true,
|
|
373
361
|
});
|
|
374
|
-
// Mute character echo while the secret is typed.
|
|
375
362
|
rl._writeToOutput = (s) => {
|
|
376
363
|
if (!rl.muted || s.includes('\n'))
|
|
377
364
|
process.stdout.write(s);
|
|
@@ -390,60 +377,10 @@ async function promptSecret() {
|
|
|
390
377
|
rl.close();
|
|
391
378
|
}
|
|
392
379
|
}
|
|
393
|
-
// ---
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
.name('gaia')
|
|
398
|
-
.description('GAIA conductor + client CLI')
|
|
399
|
-
.option('--conductor <name>', 'select a conductor by stem (conductor→conductor.config.js, else <name>.conductor.config.js; default: conductor.config.js, else the sole config; env $GAIA_CONDUCTOR)');
|
|
400
|
-
// GAIA-126/137: a repo may hold several .gaia/ conductor configs — the default
|
|
401
|
-
// conductor.config.js plus <stem>.conductor.config.js variants.
|
|
402
|
-
// resolveConfigPath already honours $GAIA_CONDUCTOR; thread the global
|
|
403
|
-
// --conductor flag into it (flag wins over env) so every command selects the
|
|
404
|
-
// named config without per-command wiring. A per-invocation flag beats any
|
|
405
|
-
// ambient env for this run.
|
|
406
|
-
program.hook('preAction', () => {
|
|
407
|
-
const name = program.opts().conductor;
|
|
408
|
-
if (typeof name === 'string' && name !== '') {
|
|
409
|
-
process.env.GAIA_CONDUCTOR = name;
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
const cliVersion = resolveCliVersion();
|
|
413
|
-
program.version(cliVersion); // -V, --version
|
|
414
|
-
program
|
|
415
|
-
.command('version')
|
|
416
|
-
.description('output the gaia CLI version')
|
|
417
|
-
.action(() => {
|
|
418
|
-
console.log(cliVersion);
|
|
419
|
-
});
|
|
420
|
-
program
|
|
421
|
-
.command('update')
|
|
422
|
-
.description('upgrade the globally installed gaia CLI to the latest npm release')
|
|
423
|
-
.action(async () => {
|
|
424
|
-
const { ok, before, after } = await runUpdate();
|
|
425
|
-
if (!ok) {
|
|
426
|
-
console.error('gaia update failed — see npm output above');
|
|
427
|
-
process.exitCode = 1;
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
// Report the real on-disk change (finding #2): never a fabricated
|
|
431
|
-
// "latest", never a false "upgraded" when npm changed nothing.
|
|
432
|
-
if (after === null) {
|
|
433
|
-
console.log('gaia update completed (installed version undetermined)');
|
|
434
|
-
}
|
|
435
|
-
else if (before === null) {
|
|
436
|
-
console.log(`gaia installed: ${after}`);
|
|
437
|
-
}
|
|
438
|
-
else if (before === after) {
|
|
439
|
-
console.log(`gaia is already up to date (${after})`);
|
|
440
|
-
}
|
|
441
|
-
else {
|
|
442
|
-
console.log(`gaia updated: ${before} → ${after}`);
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
const conductor = program
|
|
446
|
-
.command('conductor')
|
|
380
|
+
// --- the conductor command tree --------------------------------------------
|
|
381
|
+
/** Build the `conductor` subcommand tree (lifecycle + registry + init). */
|
|
382
|
+
export function createConductorCommand(deps) {
|
|
383
|
+
const conductor = new Command('conductor')
|
|
447
384
|
.description('node-agent lifecycle + local registry')
|
|
448
385
|
.option('--log-level <level>', 'log verbosity: debug | info | warn | error (overrides GAIA_LOG_LEVEL)')
|
|
449
386
|
.option('--log-sink <sink>', 'log sink: stdout | file (overrides GAIA_CONDUCTOR_LOG)')
|
|
@@ -509,36 +446,32 @@ Examples:
|
|
|
509
446
|
.action(async () => {
|
|
510
447
|
await cmdRm(deps);
|
|
511
448
|
});
|
|
449
|
+
registerInit(conductor);
|
|
450
|
+
return conductor;
|
|
451
|
+
}
|
|
452
|
+
/** `gaia conductor init` — scaffold the home machine context + the project's
|
|
453
|
+
* split config (connection `gaia.config.js` + engine `conductor.config.js`). */
|
|
454
|
+
function registerInit(conductor) {
|
|
512
455
|
conductor
|
|
513
456
|
.command('init')
|
|
514
|
-
.description('scaffold the
|
|
457
|
+
.description('scaffold the project split config (.gaia/gaia.config.js connection + .gaia/conductor.config.js engine) plus the user-global ~/.gaia/machine.config.js context (identity + connection incl. secret)')
|
|
515
458
|
.option('--base-url <url>', 'control-plane base URL (site.base_url) — required only when onboarding this machine')
|
|
516
459
|
.option('--project <name>', 'GAIA project name — required only to scaffold the committed repo config; omit for machine-only onboarding')
|
|
517
460
|
.option('--secret-env <VAR>', 'env var name to read the oauth client secret from (else TTY prompt)')
|
|
518
461
|
.option('--client-id <id>', 'oauth consumer id', 'gaia-agent')
|
|
519
462
|
.option('--machine-id <id>', 'machine host token for the context (defaults to hostname())')
|
|
520
463
|
.option('--user-id <kuerzel>', 'developer Kürzel for the user-global context')
|
|
521
|
-
.option('--machine-path <path>', 'user-global machine context path (defaults to ~/.
|
|
522
|
-
.option('--config <path>', 'target committed config path', './.gaia/conductor.config.js')
|
|
464
|
+
.option('--machine-path <path>', 'user-global machine context path (defaults to ~/.gaia/machine.config.js)')
|
|
465
|
+
.option('--config <path>', 'target committed engine config path', './.gaia/conductor.config.js')
|
|
523
466
|
.option('--force', 'overwrite an existing committed config', false)
|
|
524
467
|
.option('--reonboard', 'force machine-context onboarding even if a context file exists', false)
|
|
525
468
|
.action(async (opts) => {
|
|
526
|
-
// Two independent axes decide what init writes:
|
|
527
|
-
// - machine axis: an existing context means project-only; --reonboard
|
|
528
|
-
// (or an absent context) forces machine-context (re)scaffolding.
|
|
529
|
-
// - repo axis: --project scaffolds the committed repo config; omitting
|
|
530
|
-
// it means machine-only (no repo). The 4 quadrants:
|
|
531
|
-
// context absent + project → both files
|
|
532
|
-
// context absent + no proj → machine-only onboarding
|
|
533
|
-
// context present + project → project-only
|
|
534
|
-
// context present + no proj → no-op (machine already onboarded)
|
|
535
469
|
const machinePath = opts.machinePath ?? machineContextPath();
|
|
536
470
|
const existing = await readMachineContext(machinePath);
|
|
537
471
|
const contextPresent = existsSync(machinePath);
|
|
538
472
|
const onboarding = !contextPresent || opts.reonboard;
|
|
539
473
|
const project = opts.project ?? '';
|
|
540
474
|
const hasProject = project.trim() !== '';
|
|
541
|
-
// Context present and nothing repo-scoped to do → no-op with guidance.
|
|
542
475
|
if (!onboarding && !hasProject) {
|
|
543
476
|
console.log(`machine already onboarded (${machinePath}) — pass --project to set up a repo`);
|
|
544
477
|
return;
|
|
@@ -547,7 +480,6 @@ Examples:
|
|
|
547
480
|
let secret = '';
|
|
548
481
|
const baseUrl = opts.baseUrl ?? '';
|
|
549
482
|
if (onboarding) {
|
|
550
|
-
// Full onboarding: base-url + user-id + secret are required here.
|
|
551
483
|
if (baseUrl.trim() === '') {
|
|
552
484
|
throw new Error('--base-url is required when onboarding this machine (no machine context yet, or --reonboard)');
|
|
553
485
|
}
|
|
@@ -558,9 +490,6 @@ Examples:
|
|
|
558
490
|
throw new Error('--user-id is required (or run in a TTY to be prompted)');
|
|
559
491
|
}
|
|
560
492
|
}
|
|
561
|
-
// The secret lives in the machine context; resolve it only when that
|
|
562
|
-
// context does not already carry one (existing values are never
|
|
563
|
-
// overwritten). Source: --secret-env value, else a TTY prompt.
|
|
564
493
|
if (typeof existing.client_secret !== 'string' ||
|
|
565
494
|
existing.client_secret.trim() === '') {
|
|
566
495
|
secret =
|
|
@@ -593,13 +522,17 @@ Examples:
|
|
|
593
522
|
skipMachine: !onboarding,
|
|
594
523
|
skipCommitted: !hasProject,
|
|
595
524
|
});
|
|
596
|
-
//
|
|
525
|
+
// GAIA-218: `conductor init` writes the ENGINE config only. It no longer
|
|
526
|
+
// scaffolds a project-local `gaia.config.js` — the global
|
|
527
|
+
// `~/.gaia/gaia.config.js` (seeded by the reachability step below) is the
|
|
528
|
+
// default connection every repo inherits; a project override is opt-in
|
|
529
|
+
// via `gaia upgrade`.
|
|
597
530
|
if (!onboarding) {
|
|
598
531
|
console.log(`machine context found (${machinePath}) → setting up project only`);
|
|
599
532
|
if (typeof existing.base_url !== 'string' ||
|
|
600
533
|
existing.base_url.trim() === '') {
|
|
601
534
|
console.log(`warning: ${machinePath} exists but looks incomplete (no base_url) — ` +
|
|
602
|
-
|
|
535
|
+
'run with --reonboard to fill the machine context');
|
|
603
536
|
}
|
|
604
537
|
}
|
|
605
538
|
else if (!hasProject) {
|
|
@@ -607,13 +540,29 @@ Examples:
|
|
|
607
540
|
}
|
|
608
541
|
else {
|
|
609
542
|
console.log(opts.reonboard
|
|
610
|
-
?
|
|
611
|
-
:
|
|
543
|
+
? 're-onboarding this machine (--reonboard)'
|
|
544
|
+
: 'no machine context → onboarding this machine');
|
|
612
545
|
}
|
|
613
546
|
if (hasProject) {
|
|
614
547
|
console.log(res.wroteCommitted
|
|
615
|
-
? `wrote ${res.committedPath}`
|
|
548
|
+
? `wrote ${res.committedPath} (engine)`
|
|
616
549
|
: `kept ${res.committedPath} (exists — pass --force to replace)`);
|
|
550
|
+
// GAIA-216/218: make the split-config seed reachable from `init` itself
|
|
551
|
+
// — migrate a legacy machine context and seed the HOME connection config
|
|
552
|
+
// so an install can never be left with an engine config and no
|
|
553
|
+
// connection config. `projectConnection: 'skip'` keeps `init` from
|
|
554
|
+
// writing a project override (the global connection is the default;
|
|
555
|
+
// overrides are opt-in via `gaia upgrade`). Idempotent: a fresh init
|
|
556
|
+
// reports these as kept. `home` is derived from the machine-context path
|
|
557
|
+
// (canonical `<home>/.gaia/machine.config.js`) so the home-side steps
|
|
558
|
+
// target the same home the context lives in.
|
|
559
|
+
const reach = runUpgrade({
|
|
560
|
+
cwd: dirname(dirname(opts.config)),
|
|
561
|
+
home: dirname(dirname(machinePath)),
|
|
562
|
+
projectConnection: 'skip',
|
|
563
|
+
});
|
|
564
|
+
for (const line of reach.actions)
|
|
565
|
+
console.log(line);
|
|
617
566
|
}
|
|
618
567
|
const m = res.machine;
|
|
619
568
|
if (onboarding) {
|
|
@@ -623,93 +572,36 @@ Examples:
|
|
|
623
572
|
? `updated ${m.path} (filled: ${m.filledKeys.join(', ')})`
|
|
624
573
|
: `kept ${m.path} (already complete)`);
|
|
625
574
|
}
|
|
626
|
-
console.log(
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
575
|
+
console.log('\nNext steps:\n' +
|
|
576
|
+
' gaia dropsh auth login --provider session\n' +
|
|
577
|
+
' gaia dropsh auth login --provider pm\n' +
|
|
578
|
+
' gaia dropsh auth status # both profiles present');
|
|
630
579
|
});
|
|
631
|
-
return program;
|
|
632
580
|
}
|
|
581
|
+
/** The `conductor` command plugin the host mounts (GAIA-201). GAIA-224
|
|
582
|
+
* (Finding 7): the `gaia deployment` batch helper is no longer registered here —
|
|
583
|
+
* it is its own command addon, `@gaia-ai/addon-deployment`. */
|
|
584
|
+
const conductorCommandPlugin = {
|
|
585
|
+
kind: 'command',
|
|
586
|
+
name: 'conductor',
|
|
587
|
+
describe: 'node-agent lifecycle + local registry',
|
|
588
|
+
register(program, host) {
|
|
589
|
+
program.addCommand(createConductorCommand({ host }));
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
export default conductorCommandPlugin;
|
|
633
593
|
/**
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
* When a config is loadable, mounts the dropsh program built with the
|
|
637
|
-
* conductor's plugins (auth providers, etc.). Best-effort: if no config is
|
|
638
|
-
* loadable, the passthrough is simply not mounted.
|
|
639
|
-
*
|
|
640
|
-
* dropsh's own commands (e.g. `auth login`) reload their config from
|
|
641
|
-
* `--config` / `$DROPSH_CONFIG` / the `dropsh.config.js` default — they do not
|
|
642
|
-
* see the conductor config we already loaded. Default `$DROPSH_CONFIG` to the
|
|
643
|
-
* conductor's own config path so `gaia dropsh auth login` uses the same site +
|
|
644
|
-
* plugins without an explicit `--config` flag. An existing `$DROPSH_CONFIG`
|
|
645
|
-
* (or a `--config` flag) still wins.
|
|
594
|
+
* Build a standalone program with the conductor command mounted — the test entry
|
|
595
|
+
* (mirrors what the host does for `gaia conductor …`). `deps` inject fakes.
|
|
646
596
|
*/
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
async function attachDropsh(program, deps) {
|
|
654
|
-
const config = deps.config ?? (await tryConfig(deps));
|
|
655
|
-
if (config && !process.env.DROPSH_CONFIG) {
|
|
656
|
-
process.env.DROPSH_CONFIG = config.config_path;
|
|
657
|
-
}
|
|
658
|
-
const dropsh = buildDropshProgram({
|
|
659
|
-
plugins: config?.plugins ?? [],
|
|
660
|
-
});
|
|
661
|
-
if (!config) {
|
|
662
|
-
// Registration is unconditional; a config problem must surface WHEN a dropsh
|
|
663
|
-
// command runs, not make the command vanish. The hook fires only on a real
|
|
664
|
-
// subcommand dispatch — never for `--help` — so discoverability holds.
|
|
665
|
-
dropsh.hook('preSubcommand', async (thisCommand) => {
|
|
666
|
-
const override = thisCommand.opts().config ??
|
|
667
|
-
process.env.DROPSH_CONFIG;
|
|
668
|
-
const resolved = defaultConfigPath(override);
|
|
669
|
-
let ok = false;
|
|
670
|
-
try {
|
|
671
|
-
await loadConductorConfig(resolved);
|
|
672
|
-
ok = true;
|
|
673
|
-
}
|
|
674
|
-
catch {
|
|
675
|
-
ok = false;
|
|
676
|
-
}
|
|
677
|
-
if (!ok) {
|
|
678
|
-
throw new Error(dropshConfigHint(resolved));
|
|
679
|
-
}
|
|
680
|
-
if (!process.env.DROPSH_CONFIG) {
|
|
681
|
-
process.env.DROPSH_CONFIG = resolved;
|
|
682
|
-
}
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
program.addCommand(dropsh);
|
|
686
|
-
}
|
|
687
|
-
export async function runGaiaCli(argv, deps = {}) {
|
|
688
|
-
const program = buildProgram(deps);
|
|
689
|
-
await attachDropsh(program, deps);
|
|
690
|
-
try {
|
|
691
|
-
await program.parseAsync(argv, { from: 'user' });
|
|
692
|
-
}
|
|
693
|
-
catch (err) {
|
|
694
|
-
// The mounted dropsh program calls exitOverride(), so commander surfaces
|
|
695
|
-
// help/errors as a thrown CommanderError instead of exiting the process.
|
|
696
|
-
// `--help`/`--version` are clean exits; commander-generated errors have
|
|
697
|
-
// already written their message to stderr, so only report our own thrown
|
|
698
|
-
// errors (e.g. the dropsh missing-config hint) here.
|
|
699
|
-
const e = err;
|
|
700
|
-
if (e.code === 'commander.helpDisplayed' ||
|
|
701
|
-
e.code === 'commander.version') {
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
if (typeof e.code === 'string' && e.code.startsWith('commander.')) {
|
|
705
|
-
process.exitCode = process.exitCode ?? 1;
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
process.stderr.write(`${e.message ?? String(err)}\n`);
|
|
709
|
-
process.exitCode = process.exitCode ?? 1;
|
|
710
|
-
}
|
|
597
|
+
export function buildConductorProgram(deps = {}) {
|
|
598
|
+
const program = new Command();
|
|
599
|
+
program.name('gaia').description('GAIA conductor + client CLI');
|
|
600
|
+
program.addCommand(createConductorCommand(deps));
|
|
601
|
+
return program;
|
|
711
602
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
603
|
+
/** Parse argv against the conductor program (test entry). */
|
|
604
|
+
export async function runConductorCli(argv, deps = {}) {
|
|
605
|
+
const program = buildConductorProgram(deps);
|
|
606
|
+
await program.parseAsync(argv, { from: 'user' });
|
|
715
607
|
}
|