@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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli/config-schema.d.ts +89 -0
  3. package/dist/src/cli/config-schema.js +146 -0
  4. package/dist/src/cli/init.d.ts +31 -33
  5. package/dist/src/cli/init.js +106 -84
  6. package/dist/src/cli/migrate-addon-names.d.ts +72 -0
  7. package/dist/src/cli/migrate-addon-names.js +318 -0
  8. package/dist/src/cli/upgrade.d.ts +53 -0
  9. package/dist/src/cli/upgrade.js +222 -0
  10. package/dist/src/cli/version-check.js +5 -1
  11. package/dist/src/commands/conductor.d.ts +57 -0
  12. package/dist/src/{cli/gaia.js → commands/conductor.js} +107 -215
  13. package/dist/src/config.d.ts +65 -24
  14. package/dist/src/config.js +405 -154
  15. package/dist/src/contract.d.ts +8 -0
  16. package/dist/src/contract.js +16 -0
  17. package/dist/src/core/conductor.d.ts +16 -1
  18. package/dist/src/core/conductor.js +28 -9
  19. package/dist/src/index.d.ts +7 -5
  20. package/dist/src/index.js +26 -3
  21. package/dist/src/plugins/agent.d.ts +61 -0
  22. package/dist/src/plugins/agent.js +11 -0
  23. package/dist/src/plugins/executor.d.ts +104 -0
  24. package/dist/src/plugins/executor.js +1 -0
  25. package/dist/src/plugins/plugins.d.ts +60 -0
  26. package/dist/src/plugins/plugins.js +42 -0
  27. package/dist/src/plugins/preset.d.ts +48 -0
  28. package/dist/src/plugins/preset.js +23 -0
  29. package/dist/src/plugins/remote.d.ts +203 -0
  30. package/dist/src/plugins/remote.js +1 -0
  31. package/dist/src/plugins/workspace.d.ts +35 -0
  32. package/dist/src/plugins/workspace.js +1 -0
  33. package/dist/src/preset.d.ts +2 -0
  34. package/dist/src/preset.js +8 -0
  35. package/dist/src/types.d.ts +65 -0
  36. package/dist/src/types.js +1 -0
  37. package/package.json +8 -5
  38. package/dist/src/cli/gaia.d.ts +0 -23
  39. package/dist/src/cli/local-registry.d.ts +0 -14
  40. 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, buildProgram as buildDropshProgram } from 'dropsh';
8
- import { loadConductorConfig, resolveConfigPath } from '../config.js';
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 { machineContextPath, readMachineContext, scaffold, } from './init.js';
11
- import * as registry from './local-registry.js';
12
- import { fetchUpdateNotice, printVersionLine, resolveCliVersion, runUpdate, } from './version-check.js';
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
- * Default config path. `--config` / `$GAIA_CONDUCTOR_CONFIG` win; otherwise walk
15
- * up from cwd to the nearest `.gaia/conductor.config.js` (git/eslint style), so a
16
- * `gaia` command works from any subdirectory of a project/worktree. Throws an
17
- * actionable error when nothing is found up the tree (see resolveConfigPath).
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
- return loadConductorConfig(defaultConfigPath(configPathOverride));
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 (loadConductorConfig) throws without it — so lifecycle
37
- * commands read it here, never re-derive. A config not built by the loader that
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 (untested: shells out to herdr) ---------------------------
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 registry.list();
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. Must run
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 registry.get(id);
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 registry.register({
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 spawnViaHerdr(handle, checkoutRoot, fgCmd);
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 registry.get(id);
296
+ const entry = await getRegisteredConductor(id);
302
297
  if (entry && entry.host === 'herdr') {
303
- await killViaHerdr(entry.handle);
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
- // Graceful stop: there is no drain phase — the conductor goes offline at once,
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 killViaHerdr(entry.handle);
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 registry.remove(id);
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 loadConductorConfig(defaultConfigPath());
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
- // --- program ----------------------------------------------------------------
394
- export function buildProgram(deps) {
395
- const program = new Command();
396
- program
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 committed .gaia/conductor.config.js for this repo plus the user-global conductor.config.machine.js context (identity + connection incl. secret)')
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 ~/.config/conductor/conductor.config.machine.js)')
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
- // Mode banner.
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
- `run with --reonboard to fill the machine context`);
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
- ? `re-onboarding this machine (--reonboard)`
611
- : `no machine context → onboarding this machine`);
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(`\nNext steps:\n` +
627
- ` gaia dropsh auth login --provider session\n` +
628
- ` gaia dropsh auth login --provider pm\n` +
629
- ` gaia dropsh auth status # both profiles present`);
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
- * Attach the inherited dropsh CLI as a `gaia dropsh ...` subcommand.
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
- /** Actionable hint shown when a `gaia dropsh` command runs with no loadable config. */
648
- function dropshConfigHint(resolvedPath) {
649
- return (`gaia dropsh: no conductor config could be loaded (looked at ${resolvedPath}).\n` +
650
- `Point at one with --config <path>, set $DROPSH_CONFIG, or run from a directory ` +
651
- `containing .gaia/conductor.config.js.`);
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
- export async function main(argv) {
713
- // Drop node + script path; commander parses the rest as user args.
714
- await runGaiaCli(argv.slice(2));
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
  }