@gaia-ai/conductor 0.6.0 → 0.6.2

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 (38) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli/config-schema.d.ts +42 -7
  3. package/dist/src/cli/config-schema.js +64 -9
  4. package/dist/src/cli/init.js +16 -10
  5. package/dist/src/cli/migrate-addon-names.d.ts +82 -0
  6. package/dist/src/cli/migrate-addon-names.js +351 -0
  7. package/dist/src/cli/upgrade.d.ts +15 -0
  8. package/dist/src/cli/upgrade.js +87 -8
  9. package/dist/src/commands/conductor.d.ts +27 -3
  10. package/dist/src/commands/conductor.js +39 -52
  11. package/dist/src/config.d.ts +15 -1
  12. package/dist/src/config.js +119 -4
  13. package/dist/src/contract.d.ts +8 -0
  14. package/dist/src/contract.js +16 -0
  15. package/dist/src/core/conductor.d.ts +16 -1
  16. package/dist/src/core/conductor.js +23 -1
  17. package/dist/src/index.d.ts +6 -4
  18. package/dist/src/index.js +20 -3
  19. package/dist/src/plugins/agent.d.ts +61 -0
  20. package/dist/src/plugins/agent.js +11 -0
  21. package/dist/src/plugins/executor.d.ts +104 -0
  22. package/dist/src/plugins/executor.js +1 -0
  23. package/dist/src/plugins/plugins.d.ts +60 -0
  24. package/dist/src/plugins/plugins.js +42 -0
  25. package/dist/src/plugins/preset.d.ts +48 -0
  26. package/dist/src/plugins/preset.js +23 -0
  27. package/dist/src/plugins/remote.d.ts +203 -0
  28. package/dist/src/plugins/remote.js +1 -0
  29. package/dist/src/plugins/workspace.d.ts +35 -0
  30. package/dist/src/plugins/workspace.js +1 -0
  31. package/dist/src/preset.d.ts +2 -2
  32. package/dist/src/types.d.ts +65 -0
  33. package/dist/src/types.js +1 -0
  34. package/package.json +4 -3
  35. package/dist/src/cli/conductor-registry.d.ts +0 -7
  36. package/dist/src/cli/conductor-registry.js +0 -6
  37. package/dist/src/cli/deployment.d.ts +0 -34
  38. package/dist/src/cli/deployment.js +0 -63
@@ -0,0 +1,351 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { listConductorConfigFiles } from '../config.js';
4
+ // GAIA-224 (Finding 8b, spec v3 decision 17): the `gaia upgrade` RENAME pass.
5
+ //
6
+ // The addon packages were renamed `@gaia-ai/plugin-<name>` → `@gaia-ai/addon-<name>`
7
+ // (decision 14) and the core barrels `@gaia-ai/core/{builtins,plugins}` were
8
+ // deleted (Finding 6). Both are npm-breaking for an existing install, whose
9
+ // `.gaia/` configs still name the old packages — so `gaia upgrade` rewrites them
10
+ // in the user's PROJECT `./.gaia/` and HOME `~/.gaia/` configs.
11
+ //
12
+ // All THREE deleted/renamed specifiers are covered, and — GAIA-230 — so is the
13
+ // FOURTH: the `@gaia-ai/gaia/plugins` host barrel.
14
+ //
15
+ // GAIA-224 excluded that barrel on the premise that it "still exists as a
16
+ // back-compat aggregator", so a config naming it keeps resolving. That premise is
17
+ // INSTALL-DEPENDENT and false for a dev-clone install: `@gaia-ai/gaia` is the
18
+ // workspace's leaf SINK, so nothing depends on it and it is absent from
19
+ // `gaia-cli/node_modules/@gaia-ai/`; it is also the only package exporting
20
+ // `./plugins` (`@gaia-ai/core`'s export map has no such subpath). The `gaia` shim
21
+ // prefers a checkout's own build over the global install by design, so exactly
22
+ // there the barrel is unresolvable and that one config line dies while its
23
+ // sibling `addon-*` slots resolve fine.
24
+ //
25
+ // The barrel itself keeps existing — this is not repairing a dead reference, it is
26
+ // making configs stop NAMING a barrel the project docs already tell them not to
27
+ // name. Both barrels are therefore migrated the same closed-set way, over the same
28
+ // `CORE_PLUGINS_BARREL_EXPORTS` table. Two deliberate consequences:
29
+ // - the table is a SUPERSET of what `@gaia-ai/gaia/plugins` actually re-exports
30
+ // (`host/src/plugins-barrel.ts` omits the fakes on purpose — they are
31
+ // workspace-only and must not be a published package's dependency). Mapping
32
+ // them anyway is right: such a descriptor is broken today, and retargeting it
33
+ // at `@gaia-ai/addon-fake` fixes it.
34
+ // - `loadInstructions` IS a barrel export but is not a plugin factory and is
35
+ // absent from the table, so it stays untouched — as does any other unmapped
36
+ // `export:`.
37
+ //
38
+ // Covered per config dir:
39
+ // - the connection config `gaia.config.js` (its `plugins[]` / `addons[]`);
40
+ // - EVERY engine config the loader itself recognises — the default
41
+ // `conductor.config.js` AND each `<variant>.conductor.config.js`
42
+ // (GAIA-137 naming convention, enumerated via `listConductorConfigFiles` so
43
+ // the pass and the loader can never disagree). A legacy `conductor.config.js`
44
+ // still carrying `site`/`plugins` (the back-compat connection source) is
45
+ // therefore covered too, and its `plugins[]` entries are mapped on the
46
+ // CONNECTION surface — see `builtinsTargetFor`.
47
+ //
48
+ // Safety (decision 18 + the v3 risk register):
49
+ // - Replacement is anchored on a CLOSED set of 14 known package names inside
50
+ // a matched quote pair — never a broad `@gaia-ai/plugin-.*` sweep. A
51
+ // hand-written config's comments, formatting and unrelated code survive.
52
+ // The `@gaia-ai/core/plugins` rewrite is likewise closed-set: it is driven by
53
+ // the barrel's OWN export list, and an export name outside that list is left
54
+ // untouched rather than retargeted at a guess.
55
+ // - `*Plugin` interface identifiers (`RemotePlugin`, `herdrExecutorPlugin`, …)
56
+ // and the `plugins:`/`addons:` config KEYS are excluded BY CONSTRUCTION: a
57
+ // replacement only ever fires on a full quoted specifier, and none of those
58
+ // is one.
59
+ // - Idempotent: a config already on `addon-*` produces zero matches and is
60
+ // left BYTE-unchanged (no rewrite, no backup, no report line).
61
+ // - `--dry-run` prints the line diff and writes nothing.
62
+ // - A real rewrite backs the original up next to the file, like the
63
+ // schema-migration path does.
64
+ /** Suffix of the pre-rewrite backup a real (non-dry) rewrite leaves behind. */
65
+ export const ADDON_RENAME_BAK_SUFFIX = '.pre-addon-rename.bak';
66
+ const AUTH_BASIC = '@gaia-ai/addon-auth-basic';
67
+ const REMOTE_DRUPAL = '@gaia-ai/addon-remote-drupal';
68
+ const WORKSPACE_GIT = '@gaia-ai/addon-workspace-git';
69
+ const FAKE = '@gaia-ai/addon-fake';
70
+ /**
71
+ * The 14 addon packages renamed by GAIA-224 (spec v3 decision 14), including
72
+ * the GAIA-220 `pi` agent addon merged in from develop. A CLOSED
73
+ * set — the pass rewrites only these exact specifiers, so an unknown
74
+ * `@gaia-ai/plugin-something` is left alone rather than silently retargeted at a
75
+ * package that does not exist.
76
+ */
77
+ export const RENAMED_ADDONS = [
78
+ 'auth-basic',
79
+ 'claude',
80
+ 'codex',
81
+ 'deployment',
82
+ 'dropsh',
83
+ 'essentials',
84
+ 'fake',
85
+ 'gaia-ui',
86
+ 'herdr',
87
+ 'kimi',
88
+ 'opencode',
89
+ 'pi',
90
+ 'remote-drupal',
91
+ 'workspace-git',
92
+ ];
93
+ /**
94
+ * `'@gaia-ai/plugin-<known>'` (or `"…"` / `` `…` ``), with an optional subpath
95
+ * (`/preset`), inside a MATCHED quote pair. The quote pair + the closing anchor
96
+ * are what make this an exact-specifier match: `@gaia-ai/plugin-herdr-x` cannot
97
+ * match `herdr`, and a bare `RemotePlugin` identifier cannot match at all.
98
+ * Longest name first so a longer name always wins the alternation.
99
+ */
100
+ const QUOTED_LEGACY_ADDON = new RegExp(`(['"\`])@gaia-ai/plugin-(${[...RENAMED_ADDONS]
101
+ .sort((a, b) => b.length - a.length)
102
+ .join('|')})((?:/[^'"\`]*)?)\\1`, 'g');
103
+ /** `'@gaia-ai/core/builtins'` inside a matched quote pair (the deleted preset). */
104
+ const QUOTED_LEGACY_BUILTINS = /(['"`])@gaia-ai\/core\/builtins\1/g;
105
+ /**
106
+ * Where each export of the host barrels lives now — the DELETED
107
+ * `@gaia-ai/core/plugins` and (GAIA-230) the meta package's
108
+ * `@gaia-ai/gaia/plugins`, which is unresolvable from a dev-clone install.
109
+ *
110
+ * The key set is the barrel's own export list — `core/src/plugins/registry-exports.ts`
111
+ * as of the commit that deleted it — re-pointed at the addon that owns each
112
+ * factory today. `defaultExport` records whether the target package
113
+ * DEFAULT-exports exactly that factory; when it does, the descriptor's `export:`
114
+ * becomes redundant and is dropped.
115
+ *
116
+ * Deliberately absent, so they are left untouched:
117
+ * - `selectRemote`/`selectWorkspace`/`selectExecutor`/`selectAgent(s)` — moved
118
+ * to `@gaia-ai/conductor/contract`, but they are config-shape SELECTORS, not
119
+ * plugin factories, so no descriptor can legitimately name them;
120
+ * - any other/unknown export name — a config naming one is reported by the
121
+ * loader rather than silently retargeted at a package that may not have it.
122
+ */
123
+ export const CORE_PLUGINS_BARREL_EXPORTS = {
124
+ DrupalGaiaRemote: { target: REMOTE_DRUPAL, defaultExport: false },
125
+ FakeGaiaRemote: { target: FAKE, defaultExport: false },
126
+ FakeWorkspace: { target: FAKE, defaultExport: false },
127
+ GitWorkspace: { target: WORKSPACE_GIT, defaultExport: false },
128
+ basicAuthProvider: { target: AUTH_BASIC, defaultExport: true },
129
+ drupalRemote: { target: REMOTE_DRUPAL, defaultExport: true },
130
+ // @gaia-ai/addon-fake ships the whole deterministic triple as NAMED exports
131
+ // (no default), so these keep their `export:`.
132
+ fakeRemote: { target: FAKE, defaultExport: false },
133
+ fakeWorkspace: { target: FAKE, defaultExport: false },
134
+ gitWorkspace: { target: WORKSPACE_GIT, defaultExport: true },
135
+ };
136
+ /**
137
+ * The host-barrel specifiers a descriptor may name. Both map through
138
+ * `CORE_PLUGINS_BARREL_EXPORTS`: `@gaia-ai/core/plugins` is DELETED, and
139
+ * `@gaia-ai/gaia/plugins` (GAIA-230) is unresolvable from a dev-clone install
140
+ * because the meta package that exports it is the workspace's leaf sink.
141
+ */
142
+ export const HOST_BARREL_PATHS = [
143
+ '@gaia-ai/core/plugins',
144
+ '@gaia-ai/gaia/plugins',
145
+ ];
146
+ /** The two barrel specifiers as one non-capturing regex alternation, so the
147
+ * backreference numbers in the descriptor patterns below are unaffected. */
148
+ const BARREL_PATH = `(?:${HOST_BARREL_PATHS.join('|')})`;
149
+ /** `plugin:` / `use:` — the two descriptor keys that name a package. */
150
+ const PKG_KEY = '(?:plugin|use)';
151
+ /** A JS identifier, i.e. an `export:` value the barrel could have carried. */
152
+ const IDENT = String.raw `[A-Za-z_$][\w$]*`;
153
+ /**
154
+ * `plugin: '<barrel>', export: 'drupalRemote'` — the barrel
155
+ * descriptor in its written order, single/double/back quoted, one line or many
156
+ * (`\s` spans newlines). Requiring the `export:` to be ADJACENT is what keeps
157
+ * this from ever pairing a barrel mention with an unrelated slot's `export:`.
158
+ */
159
+ const BARREL_THEN_EXPORT = new RegExp(`(${PKG_KEY}\\s*:\\s*)(['"\`])${BARREL_PATH}\\2(\\s*,\\s*export\\s*:\\s*)(['"\`])(${IDENT})\\4`, 'g');
160
+ /** The same descriptor with the keys written the other way round. */
161
+ const EXPORT_THEN_BARREL = new RegExp(`export\\s*:\\s*(['"\`])(${IDENT})\\1(\\s*,\\s*)(${PKG_KEY}\\s*:\\s*)(['"\`])${BARREL_PATH}\\5`, 'g');
162
+ /**
163
+ * Rewrite the host-barrel descriptors (both specifiers) in one config's source text.
164
+ *
165
+ * A mapped descriptor becomes `<key>: '<addon>'` — with the `export:` DROPPED
166
+ * when the addon default-exports that factory *and* the whole descriptor sits on
167
+ * one line, so the rewrite stays line-local (the index-aligned report diff and
168
+ * `migrateAddonNames`' line-count invariant depend on that). A multi-line
169
+ * descriptor keeps its `export:`: every mapped name is also a NAMED export of its
170
+ * target addon, so the result resolves either way — only the redundancy survives.
171
+ *
172
+ * Left byte-untouched: an unmapped/unknown `export:` name, and a barrel
173
+ * occurrence with no adjacent `export:` at all (a bare `addons: ['…/plugins']` or
174
+ * a prose mention) — neither carries enough information to pick a target.
175
+ */
176
+ function migrateBarrelDescriptors(text) {
177
+ let count = 0;
178
+ const rewrite = (whole, name, keyPrefix, quote, keepExport) => {
179
+ const hit = CORE_PLUGINS_BARREL_EXPORTS[name];
180
+ if (hit === undefined)
181
+ return whole;
182
+ count++;
183
+ const spec = `${keyPrefix}${quote}${hit.target}${quote}`;
184
+ if (hit.defaultExport && !whole.includes('\n'))
185
+ return spec;
186
+ return `${spec}${keepExport()}`;
187
+ };
188
+ const out = text
189
+ .replace(BARREL_THEN_EXPORT, (whole, keyPrefix, pkgQuote, exportSep, nameQuote, name) => rewrite(whole, name, keyPrefix, pkgQuote, () => `${exportSep}${nameQuote}${name}${nameQuote}`))
190
+ .replace(EXPORT_THEN_BARREL, (whole, nameQuote, name, sep, keyPrefix, pkgQuote) => rewrite(whole, name, keyPrefix, pkgQuote, () => `${sep}export: ${nameQuote}${name}${nameQuote}`));
191
+ return { text: out, count };
192
+ }
193
+ /**
194
+ * The object-literal entry that encloses `at`, e.g.
195
+ * `{ use: '@gaia-ai/core/builtins', with: { basic: token } }`. Empty string when
196
+ * the occurrence is a BARE string entry (`addons: ['@gaia-ai/core/builtins']`) —
197
+ * the backward scan then hits the array/argument boundary first.
198
+ */
199
+ function enclosingEntry(text, at) {
200
+ let depth = 0;
201
+ let start = -1;
202
+ for (let i = at; i >= 0; i--) {
203
+ const c = text[i];
204
+ if (c === '}')
205
+ depth++;
206
+ else if (c === '{') {
207
+ if (depth === 0) {
208
+ start = i;
209
+ break;
210
+ }
211
+ depth--;
212
+ }
213
+ else if (depth === 0 && (c === '[' || c === '(' || c === ';')) {
214
+ break;
215
+ }
216
+ }
217
+ if (start < 0)
218
+ return '';
219
+ let d = 0;
220
+ for (let i = start; i < text.length; i++) {
221
+ const c = text[i];
222
+ if (c === '{')
223
+ d++;
224
+ else if (c === '}') {
225
+ d--;
226
+ if (d === 0)
227
+ return text.slice(start, i + 1);
228
+ }
229
+ }
230
+ return text.slice(start);
231
+ }
232
+ /** The nearest `plugins:` / `addons:` array key BEFORE `at`, if any. */
233
+ function nearestArrayKey(text, at) {
234
+ const re = /\b(plugins|addons)\s*:/g;
235
+ let last;
236
+ for (let m = re.exec(text); m !== null && m.index < at; m = re.exec(text)) {
237
+ last = m[1];
238
+ }
239
+ return last;
240
+ }
241
+ /**
242
+ * `@gaia-ai/core/builtins` was ONE preset contributing to BOTH surfaces, so its
243
+ * replacement is per-occurrence (spec v3 decision 17, the Finding 6/8 interplay):
244
+ *
245
+ * | signal (first that applies) | target |
246
+ * | the entry carries a `basic` token | `@gaia-ai/addon-auth-basic` |
247
+ * | the nearest array key is `plugins:` | `@gaia-ai/addon-auth-basic` |
248
+ * | the file is a connection config | `@gaia-ai/addon-auth-basic` |
249
+ * | otherwise (engine `addons:` / a slot) | `@gaia-ai/addon-remote-drupal` |
250
+ *
251
+ * The `plugins:` rule is what makes a LEGACY `conductor.config.js` (engine file
252
+ * name, but still carrying a connection `site`/`plugins`) come out right.
253
+ */
254
+ function builtinsTargetFor(text, at, surface) {
255
+ if (/basic/i.test(enclosingEntry(text, at)))
256
+ return AUTH_BASIC;
257
+ if (nearestArrayKey(text, at) === 'plugins')
258
+ return AUTH_BASIC;
259
+ return surface === 'connection' ? AUTH_BASIC : REMOTE_DRUPAL;
260
+ }
261
+ /**
262
+ * Rewrite the legacy specifiers in one config's SOURCE TEXT — all of them: the
263
+ * renamed `@gaia-ai/plugin-*` packages, the deleted `@gaia-ai/core/builtins`
264
+ * preset, and the host-barrel descriptors naming either `@gaia-ai/core/plugins`
265
+ * (deleted) or `@gaia-ai/gaia/plugins` (GAIA-230). Pure; line
266
+ * count is preserved (every replacement is line-local), which is what lets the
267
+ * reported diff be a plain index-aligned line diff.
268
+ */
269
+ export function migrateAddonNames(text, surface) {
270
+ let count = 0;
271
+ let out = text.replace(QUOTED_LEGACY_ADDON, (_m, quote, name, subpath) => {
272
+ count++;
273
+ return `${quote}@gaia-ai/addon-${name}${subpath}${quote}`;
274
+ });
275
+ out = out.replace(QUOTED_LEGACY_BUILTINS, (_m, quote, offset, whole) => {
276
+ count++;
277
+ return `${quote}${builtinsTargetFor(whole, offset, surface)}${quote}`;
278
+ });
279
+ const barrel = migrateBarrelDescriptors(out);
280
+ return { text: barrel.text, count: count + barrel.count };
281
+ }
282
+ /** An index-aligned `- old` / `+ new` line diff (line count is preserved). */
283
+ function lineDiff(before, after) {
284
+ const a = before.split('\n');
285
+ const b = after.split('\n');
286
+ const out = [];
287
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
288
+ if (a[i] === b[i])
289
+ continue;
290
+ if (a[i] !== undefined)
291
+ out.push(` - ${a[i]}`);
292
+ if (b[i] !== undefined)
293
+ out.push(` + ${b[i]}`);
294
+ }
295
+ return out;
296
+ }
297
+ /**
298
+ * Rename pass over ONE config file. Returns `undefined` when the file is absent
299
+ * or already on the new names (idempotency: no write, no backup, no report
300
+ * line — the file stays byte-identical).
301
+ */
302
+ export function migrateAddonNamesInFile(path, surface, opts = {}) {
303
+ if (!existsSync(path))
304
+ return undefined;
305
+ const before = readFileSync(path, 'utf8');
306
+ const { text: after, count } = migrateAddonNames(before, surface);
307
+ if (count === 0 || after === before)
308
+ return undefined;
309
+ const diff = lineDiff(before, after);
310
+ const what = `${count} legacy addon reference(s) → @gaia-ai/addon-*`;
311
+ if (opts.dryRun ?? false) {
312
+ return { action: `would rewrite ${path} (${what})`, diff, count };
313
+ }
314
+ const bak = `${path}${ADDON_RENAME_BAK_SUFFIX}`;
315
+ writeFileSync(bak, before, 'utf8');
316
+ writeFileSync(path, after, 'utf8');
317
+ return {
318
+ action: `rewrote ${path} (${what}; backup ${bak})`,
319
+ diff,
320
+ count,
321
+ };
322
+ }
323
+ /**
324
+ * Rename pass over every recognised config in the given `.gaia/` dirs (project
325
+ * + home). Emits report lines ONLY for files it actually rewrites, so an install
326
+ * already on `addon-*` adds no noise to the upgrade report.
327
+ */
328
+ export function runAddonRenameMigration(opts = { gaiaDirs: [] }) {
329
+ const actions = [];
330
+ let changed = false;
331
+ const seen = new Set();
332
+ for (const dir of opts.gaiaDirs) {
333
+ if (seen.has(dir) || !existsSync(dir))
334
+ continue;
335
+ seen.add(dir);
336
+ const files = [
337
+ [join(dir, 'gaia.config.js'), 'connection'],
338
+ ...listConductorConfigFiles(dir).map((f) => [join(dir, f), 'engine']),
339
+ ];
340
+ for (const [path, surface] of files) {
341
+ const res = migrateAddonNamesInFile(path, surface, {
342
+ dryRun: opts.dryRun ?? false,
343
+ });
344
+ if (res === undefined)
345
+ continue;
346
+ actions.push(res.action, ...res.diff);
347
+ changed = true;
348
+ }
349
+ }
350
+ return { actions, changed };
351
+ }
@@ -5,6 +5,13 @@ export interface UpgradeReport {
5
5
  /** true when nothing needed doing. */
6
6
  alreadyCurrent: boolean;
7
7
  }
8
+ /**
9
+ * GAIA-218: what `gaia upgrade` does to the PROJECT connection override. The
10
+ * pure routine defaults to `'skip'` (non-interactive / `conductor init`): never
11
+ * create, never migrate, never delete. The host CLI resolves `'create'` /
12
+ * `'remove'` from an opt-in TTY prompt.
13
+ */
14
+ export type ProjectConnectionChoice = 'skip' | 'create' | 'remove';
8
15
  /** The outcome of routing a single connection-config file. */
9
16
  interface ConnectionUpgradeResult {
10
17
  action: string;
@@ -29,10 +36,18 @@ export declare function runConnectionUpgrade(path: string, opts?: {
29
36
  current?: number;
30
37
  chain?: ConnectionConfigMigration[];
31
38
  }): ConnectionUpgradeResult;
39
+ /**
40
+ * The absolute path of the PROJECT connection override (`./.gaia/gaia.config.js`)
41
+ * when one exists for `cwd`, else `undefined`. Used by the host CLI to decide
42
+ * whether to ask "remove this override?" vs "create an override?" (GAIA-218).
43
+ */
44
+ export declare function hasProjectConnection(cwd: string): string | undefined;
32
45
  /** Run the migration. Pure w.r.t. injected `cwd`/`home`; `dryRun` suppresses writes. */
33
46
  export declare function runUpgrade(opts?: {
34
47
  cwd?: string;
35
48
  home?: string;
36
49
  dryRun?: boolean;
50
+ /** GAIA-218: what to do with the PROJECT connection override (default skip). */
51
+ projectConnection?: ProjectConnectionChoice;
37
52
  }): UpgradeReport;
38
53
  export {};
@@ -1,9 +1,11 @@
1
- import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
1
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
+ import { listConductorConfigFiles, stripLegacyConnectionFromConfigSource, } from '../config.js';
5
6
  import { applyMigrations, CONNECTION_MIGRATIONS, GAIA_CONFIG_SCHEMA_VERSION, readConfigSchemaVersion, stampVersion, stripVersionHeader, } from './config-schema.js';
6
7
  import { renderGaiaConfig } from './init.js';
8
+ import { runAddonRenameMigration } from './migrate-addon-names.js';
7
9
  const GAIA_DIR = '.gaia';
8
10
  const DEFAULT_ENGINE = 'conductor.config.js';
9
11
  const VARIANT_SUFFIX = '.conductor.config.js';
@@ -98,22 +100,86 @@ export function runConnectionUpgrade(path, opts = {}) {
98
100
  changed: true,
99
101
  };
100
102
  }
103
+ /**
104
+ * The absolute path of the PROJECT connection override (`./.gaia/gaia.config.js`)
105
+ * when one exists for `cwd`, else `undefined`. Used by the host CLI to decide
106
+ * whether to ask "remove this override?" vs "create an override?" (GAIA-218).
107
+ */
108
+ export function hasProjectConnection(cwd) {
109
+ const gaiaDir = findGaiaDir(cwd);
110
+ if (gaiaDir === undefined)
111
+ return undefined;
112
+ const p = join(gaiaDir, CONNECTION);
113
+ return existsSync(p) ? p : undefined;
114
+ }
101
115
  /** Run the migration. Pure w.r.t. injected `cwd`/`home`; `dryRun` suppresses writes. */
102
116
  export function runUpgrade(opts = {}) {
103
117
  const cwd = opts.cwd ?? process.cwd();
104
118
  const home = opts.home ?? homedir();
105
119
  const dry = opts.dryRun ?? false;
120
+ const choice = opts.projectConnection ?? 'skip';
106
121
  const actions = [];
107
122
  let changed = false;
108
- // 1. Project connection config (schema-versioned seed/migrate).
109
123
  const gaiaDir = findGaiaDir(cwd);
124
+ // 0. GAIA-218 (AC-7): strip legacy connection/auth (`site`/`plugins` + the
125
+ // orphaned connection preamble) from EVERY engine config in the `.gaia/`
126
+ // dir — the supported enumeration only, never an unrelated `*.config.js`.
127
+ if (gaiaDir !== undefined) {
128
+ for (const file of listConductorConfigFiles(gaiaDir)) {
129
+ const path = join(gaiaDir, file);
130
+ if (!existsSync(path))
131
+ continue;
132
+ const original = readFileSync(path, 'utf8');
133
+ const { text, removed } = stripLegacyConnectionFromConfigSource(original);
134
+ if (removed.length === 0)
135
+ continue;
136
+ if (dry) {
137
+ actions.push(`would strip ${removed.join('/')} from ${path}`);
138
+ }
139
+ else {
140
+ writeFileSync(`${path}.legacy.bak`, original, 'utf8');
141
+ writeFileSync(path, text, 'utf8');
142
+ actions.push(`stripped ${removed.join('/')} from ${path} (backup ${path}.legacy.bak)`);
143
+ }
144
+ changed = true;
145
+ }
146
+ }
147
+ // 1. Project connection config — decision-driven (GAIA-218, default skip).
110
148
  if (gaiaDir !== undefined && hasEngineConfig(gaiaDir)) {
111
- const res = runConnectionUpgrade(join(gaiaDir, CONNECTION), {
112
- dryRun: dry,
113
- label: 'project connection config',
114
- });
115
- actions.push(res.action);
116
- changed = changed || res.changed;
149
+ const projectConn = join(gaiaDir, CONNECTION);
150
+ if (choice === 'create') {
151
+ if (existsSync(projectConn)) {
152
+ actions.push(`project connection: unchanged (${projectConn} exists — kept, not migrated)`);
153
+ }
154
+ else {
155
+ const res = runConnectionUpgrade(projectConn, {
156
+ dryRun: dry,
157
+ label: 'project connection config',
158
+ });
159
+ actions.push(res.action);
160
+ changed = changed || res.changed;
161
+ }
162
+ }
163
+ else if (choice === 'remove') {
164
+ if (existsSync(projectConn)) {
165
+ const bak = `${projectConn}.removed.bak`;
166
+ if (dry) {
167
+ actions.push(`would remove ${projectConn} (project connection override)`);
168
+ }
169
+ else {
170
+ copyFileSync(projectConn, bak);
171
+ rmSync(projectConn);
172
+ actions.push(`removed ${projectConn} (project connection override; backup ${bak})`);
173
+ }
174
+ changed = true;
175
+ }
176
+ else {
177
+ actions.push('project connection: unchanged (no project override to remove)');
178
+ }
179
+ }
180
+ else {
181
+ actions.push('project connection: unchanged');
182
+ }
117
183
  }
118
184
  // 2. Machine context move (secret-bearing — never schema-migrated).
119
185
  const legacy = legacyMachinePath(home);
@@ -139,5 +205,18 @@ export function runUpgrade(opts = {}) {
139
205
  });
140
206
  actions.push(homeRes.action);
141
207
  changed = changed || homeRes.changed;
208
+ // 4. GAIA-224 addon rename pass over the project + home `.gaia/` configs
209
+ // (connection `gaia.config.js` + every recognised engine config). Runs LAST
210
+ // so a config seeded/migrated above — already on the current names — is a
211
+ // zero-match no-op. Reports only the files it actually rewrites.
212
+ const rename = runAddonRenameMigration({
213
+ gaiaDirs: [
214
+ ...(gaiaDir === undefined ? [] : [gaiaDir]),
215
+ join(home, GAIA_DIR),
216
+ ],
217
+ dryRun: dry,
218
+ });
219
+ actions.push(...rename.actions);
220
+ changed = changed || rename.changed;
142
221
  return { actions, alreadyCurrent: !changed };
143
222
  }
@@ -1,5 +1,10 @@
1
- import { type ConductorFileConfig, type ConductorLogger, type GaiaCommandHost, type GaiaCommandPlugin, type GaiaExecutor, type GaiaRemote, type GaiaWorkspace, type ResolvedAgent } from '@gaia-ai/core';
1
+ import { type ConductorLogger, type GaiaCommandHost, type GaiaCommandPlugin } from '@gaia-ai/core';
2
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';
3
8
  /** Test seam: inject any subset of dependencies. */
4
9
  export interface GaiaCliDeps {
5
10
  remote?: GaiaRemote;
@@ -11,8 +16,26 @@ export interface GaiaCliDeps {
11
16
  fetch?: typeof fetch;
12
17
  /** Host contract (resolveBases); default: this install + cwd. */
13
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>;
14
35
  }
15
36
  export declare function parseHerdrJson(output: string, command: string): unknown;
37
+ /** The production host: the real `herdr` binary. */
38
+ export declare const realHerdrHost: HerdrHost;
16
39
  /**
17
40
  * Start-time auth gate. Returns true if authenticated (session or session-less
18
41
  * provider); otherwise logs a single clear line and returns false.
@@ -20,8 +43,9 @@ export declare function parseHerdrJson(output: string, command: string): unknown
20
43
  export declare function ensureAuthenticated(config: ConductorFileConfig, logger: ConductorLogger): Promise<boolean>;
21
44
  /** Build the `conductor` subcommand tree (lifecycle + registry + init). */
22
45
  export declare function createConductorCommand(deps: GaiaCliDeps): Command;
23
- /** The `conductor` command plugin the host mounts (GAIA-201). It also registers
24
- * the `gaia deployment` batch helper (conductor-scoped: needs remote + project). */
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`. */
25
49
  declare const conductorCommandPlugin: GaiaCommandPlugin;
26
50
  export default conductorCommandPlugin;
27
51
  /**