@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,72 @@
|
|
|
1
|
+
/** Suffix of the pre-rewrite backup a real (non-dry) rewrite leaves behind. */
|
|
2
|
+
export declare const ADDON_RENAME_BAK_SUFFIX = ".pre-addon-rename.bak";
|
|
3
|
+
/**
|
|
4
|
+
* The 14 addon packages renamed by GAIA-224 (spec v3 decision 14), including
|
|
5
|
+
* the GAIA-220 `pi` agent addon merged in from develop. A CLOSED
|
|
6
|
+
* set — the pass rewrites only these exact specifiers, so an unknown
|
|
7
|
+
* `@gaia-ai/plugin-something` is left alone rather than silently retargeted at a
|
|
8
|
+
* package that does not exist.
|
|
9
|
+
*/
|
|
10
|
+
export declare const RENAMED_ADDONS: readonly ["auth-basic", "claude", "codex", "deployment", "dropsh", "essentials", "fake", "gaia-ui", "herdr", "kimi", "opencode", "pi", "remote-drupal", "workspace-git"];
|
|
11
|
+
/**
|
|
12
|
+
* Where each export of the DELETED `@gaia-ai/core/plugins` host barrel lives now.
|
|
13
|
+
*
|
|
14
|
+
* The key set is the barrel's own export list — `core/src/plugins/registry-exports.ts`
|
|
15
|
+
* as of the commit that deleted it — re-pointed at the addon that owns each
|
|
16
|
+
* factory today. `defaultExport` records whether the target package
|
|
17
|
+
* DEFAULT-exports exactly that factory; when it does, the descriptor's `export:`
|
|
18
|
+
* becomes redundant and is dropped.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately absent, so they are left untouched:
|
|
21
|
+
* - `selectRemote`/`selectWorkspace`/`selectExecutor`/`selectAgent(s)` — moved
|
|
22
|
+
* to `@gaia-ai/conductor/contract`, but they are config-shape SELECTORS, not
|
|
23
|
+
* plugin factories, so no descriptor can legitimately name them;
|
|
24
|
+
* - any other/unknown export name — a config naming one is reported by the
|
|
25
|
+
* loader rather than silently retargeted at a package that may not have it.
|
|
26
|
+
*/
|
|
27
|
+
export declare const CORE_PLUGINS_BARREL_EXPORTS: Readonly<Record<string, {
|
|
28
|
+
target: string;
|
|
29
|
+
defaultExport: boolean;
|
|
30
|
+
}>>;
|
|
31
|
+
/** Which surface a config file speaks — decides the ambiguous builtins mapping. */
|
|
32
|
+
export type ConfigSurface = 'connection' | 'engine';
|
|
33
|
+
/**
|
|
34
|
+
* Rewrite the legacy specifiers in one config's SOURCE TEXT — all three: the
|
|
35
|
+
* renamed `@gaia-ai/plugin-*` packages, the deleted `@gaia-ai/core/builtins`
|
|
36
|
+
* preset, and the deleted `@gaia-ai/core/plugins` barrel descriptors. Pure; line
|
|
37
|
+
* count is preserved (every replacement is line-local), which is what lets the
|
|
38
|
+
* reported diff be a plain index-aligned line diff.
|
|
39
|
+
*/
|
|
40
|
+
export declare function migrateAddonNames(text: string, surface: ConfigSurface): {
|
|
41
|
+
text: string;
|
|
42
|
+
count: number;
|
|
43
|
+
};
|
|
44
|
+
/** The outcome of the rename pass over a single config file. */
|
|
45
|
+
export interface AddonRenameResult {
|
|
46
|
+
/** Report line, in the shape of the other upgrade steps. */
|
|
47
|
+
action: string;
|
|
48
|
+
/** `- old` / `+ new` lines for the rewritten specifiers. */
|
|
49
|
+
diff: string[];
|
|
50
|
+
/** Number of rewritten specifiers. */
|
|
51
|
+
count: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Rename pass over ONE config file. Returns `undefined` when the file is absent
|
|
55
|
+
* or already on the new names (idempotency: no write, no backup, no report
|
|
56
|
+
* line — the file stays byte-identical).
|
|
57
|
+
*/
|
|
58
|
+
export declare function migrateAddonNamesInFile(path: string, surface: ConfigSurface, opts?: {
|
|
59
|
+
dryRun?: boolean;
|
|
60
|
+
}): AddonRenameResult | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Rename pass over every recognised config in the given `.gaia/` dirs (project
|
|
63
|
+
* + home). Emits report lines ONLY for files it actually rewrites, so an install
|
|
64
|
+
* already on `addon-*` adds no noise to the upgrade report.
|
|
65
|
+
*/
|
|
66
|
+
export declare function runAddonRenameMigration(opts?: {
|
|
67
|
+
gaiaDirs: string[];
|
|
68
|
+
dryRun?: boolean;
|
|
69
|
+
}): {
|
|
70
|
+
actions: string[];
|
|
71
|
+
changed: boolean;
|
|
72
|
+
};
|
|
@@ -0,0 +1,318 @@
|
|
|
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. The `@gaia-ai/gaia/plugins`
|
|
13
|
+
// barrel is deliberately NOT covered: it still exists as a back-compat
|
|
14
|
+
// aggregator. `@gaia-ai/core/plugins` has no such reprieve — it is gone, and the
|
|
15
|
+
// pre-GAIA-224 project docs explicitly told configs to name `@gaia-ai/core/plugins`
|
|
16
|
+
// and NOT `@gaia-ai/gaia/plugins` (core resolved from both the dev monorepo and a
|
|
17
|
+
// global install; the meta package did not), so real installs DO name the deleted
|
|
18
|
+
// one. See `CORE_PLUGINS_BARREL_EXPORTS`.
|
|
19
|
+
//
|
|
20
|
+
// Covered per config dir:
|
|
21
|
+
// - the connection config `gaia.config.js` (its `plugins[]` / `addons[]`);
|
|
22
|
+
// - EVERY engine config the loader itself recognises — the default
|
|
23
|
+
// `conductor.config.js` AND each `<variant>.conductor.config.js`
|
|
24
|
+
// (GAIA-137 naming convention, enumerated via `listConductorConfigFiles` so
|
|
25
|
+
// the pass and the loader can never disagree). A legacy `conductor.config.js`
|
|
26
|
+
// still carrying `site`/`plugins` (the back-compat connection source) is
|
|
27
|
+
// therefore covered too, and its `plugins[]` entries are mapped on the
|
|
28
|
+
// CONNECTION surface — see `builtinsTargetFor`.
|
|
29
|
+
//
|
|
30
|
+
// Safety (decision 18 + the v3 risk register):
|
|
31
|
+
// - Replacement is anchored on a CLOSED set of 14 known package names inside
|
|
32
|
+
// a matched quote pair — never a broad `@gaia-ai/plugin-.*` sweep. A
|
|
33
|
+
// hand-written config's comments, formatting and unrelated code survive.
|
|
34
|
+
// The `@gaia-ai/core/plugins` rewrite is likewise closed-set: it is driven by
|
|
35
|
+
// the barrel's OWN export list, and an export name outside that list is left
|
|
36
|
+
// untouched rather than retargeted at a guess.
|
|
37
|
+
// - `*Plugin` interface identifiers (`RemotePlugin`, `herdrExecutorPlugin`, …),
|
|
38
|
+
// the `plugins:`/`addons:` config KEYS and the `@gaia-ai/gaia/plugins`
|
|
39
|
+
// subpath barrel are excluded BY CONSTRUCTION: they carry neither the
|
|
40
|
+
// `@gaia-ai/plugin-` scope prefix nor the `@gaia-ai/core/` path.
|
|
41
|
+
// - Idempotent: a config already on `addon-*` produces zero matches and is
|
|
42
|
+
// left BYTE-unchanged (no rewrite, no backup, no report line).
|
|
43
|
+
// - `--dry-run` prints the line diff and writes nothing.
|
|
44
|
+
// - A real rewrite backs the original up next to the file, like the
|
|
45
|
+
// schema-migration path does.
|
|
46
|
+
/** Suffix of the pre-rewrite backup a real (non-dry) rewrite leaves behind. */
|
|
47
|
+
export const ADDON_RENAME_BAK_SUFFIX = '.pre-addon-rename.bak';
|
|
48
|
+
const AUTH_BASIC = '@gaia-ai/addon-auth-basic';
|
|
49
|
+
const REMOTE_DRUPAL = '@gaia-ai/addon-remote-drupal';
|
|
50
|
+
const WORKSPACE_GIT = '@gaia-ai/addon-workspace-git';
|
|
51
|
+
const FAKE = '@gaia-ai/addon-fake';
|
|
52
|
+
/**
|
|
53
|
+
* The 14 addon packages renamed by GAIA-224 (spec v3 decision 14), including
|
|
54
|
+
* the GAIA-220 `pi` agent addon merged in from develop. A CLOSED
|
|
55
|
+
* set — the pass rewrites only these exact specifiers, so an unknown
|
|
56
|
+
* `@gaia-ai/plugin-something` is left alone rather than silently retargeted at a
|
|
57
|
+
* package that does not exist.
|
|
58
|
+
*/
|
|
59
|
+
export const RENAMED_ADDONS = [
|
|
60
|
+
'auth-basic',
|
|
61
|
+
'claude',
|
|
62
|
+
'codex',
|
|
63
|
+
'deployment',
|
|
64
|
+
'dropsh',
|
|
65
|
+
'essentials',
|
|
66
|
+
'fake',
|
|
67
|
+
'gaia-ui',
|
|
68
|
+
'herdr',
|
|
69
|
+
'kimi',
|
|
70
|
+
'opencode',
|
|
71
|
+
'pi',
|
|
72
|
+
'remote-drupal',
|
|
73
|
+
'workspace-git',
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* `'@gaia-ai/plugin-<known>'` (or `"…"` / `` `…` ``), with an optional subpath
|
|
77
|
+
* (`/preset`), inside a MATCHED quote pair. The quote pair + the closing anchor
|
|
78
|
+
* are what make this an exact-specifier match: `@gaia-ai/plugin-herdr-x` cannot
|
|
79
|
+
* match `herdr`, and a bare `RemotePlugin` identifier cannot match at all.
|
|
80
|
+
* Longest name first so a longer name always wins the alternation.
|
|
81
|
+
*/
|
|
82
|
+
const QUOTED_LEGACY_ADDON = new RegExp(`(['"\`])@gaia-ai/plugin-(${[...RENAMED_ADDONS]
|
|
83
|
+
.sort((a, b) => b.length - a.length)
|
|
84
|
+
.join('|')})((?:/[^'"\`]*)?)\\1`, 'g');
|
|
85
|
+
/** `'@gaia-ai/core/builtins'` inside a matched quote pair (the deleted preset). */
|
|
86
|
+
const QUOTED_LEGACY_BUILTINS = /(['"`])@gaia-ai\/core\/builtins\1/g;
|
|
87
|
+
/**
|
|
88
|
+
* Where each export of the DELETED `@gaia-ai/core/plugins` host barrel lives now.
|
|
89
|
+
*
|
|
90
|
+
* The key set is the barrel's own export list — `core/src/plugins/registry-exports.ts`
|
|
91
|
+
* as of the commit that deleted it — re-pointed at the addon that owns each
|
|
92
|
+
* factory today. `defaultExport` records whether the target package
|
|
93
|
+
* DEFAULT-exports exactly that factory; when it does, the descriptor's `export:`
|
|
94
|
+
* becomes redundant and is dropped.
|
|
95
|
+
*
|
|
96
|
+
* Deliberately absent, so they are left untouched:
|
|
97
|
+
* - `selectRemote`/`selectWorkspace`/`selectExecutor`/`selectAgent(s)` — moved
|
|
98
|
+
* to `@gaia-ai/conductor/contract`, but they are config-shape SELECTORS, not
|
|
99
|
+
* plugin factories, so no descriptor can legitimately name them;
|
|
100
|
+
* - any other/unknown export name — a config naming one is reported by the
|
|
101
|
+
* loader rather than silently retargeted at a package that may not have it.
|
|
102
|
+
*/
|
|
103
|
+
export const CORE_PLUGINS_BARREL_EXPORTS = {
|
|
104
|
+
DrupalGaiaRemote: { target: REMOTE_DRUPAL, defaultExport: false },
|
|
105
|
+
FakeGaiaRemote: { target: FAKE, defaultExport: false },
|
|
106
|
+
FakeWorkspace: { target: FAKE, defaultExport: false },
|
|
107
|
+
GitWorkspace: { target: WORKSPACE_GIT, defaultExport: false },
|
|
108
|
+
basicAuthProvider: { target: AUTH_BASIC, defaultExport: true },
|
|
109
|
+
drupalRemote: { target: REMOTE_DRUPAL, defaultExport: true },
|
|
110
|
+
// @gaia-ai/addon-fake ships the whole deterministic triple as NAMED exports
|
|
111
|
+
// (no default), so these keep their `export:`.
|
|
112
|
+
fakeRemote: { target: FAKE, defaultExport: false },
|
|
113
|
+
fakeWorkspace: { target: FAKE, defaultExport: false },
|
|
114
|
+
gitWorkspace: { target: WORKSPACE_GIT, defaultExport: true },
|
|
115
|
+
};
|
|
116
|
+
const BARREL_PATH = '@gaia-ai/core/plugins';
|
|
117
|
+
/** `plugin:` / `use:` — the two descriptor keys that name a package. */
|
|
118
|
+
const PKG_KEY = '(?:plugin|use)';
|
|
119
|
+
/** A JS identifier, i.e. an `export:` value the barrel could have carried. */
|
|
120
|
+
const IDENT = String.raw `[A-Za-z_$][\w$]*`;
|
|
121
|
+
/**
|
|
122
|
+
* `plugin: '@gaia-ai/core/plugins', export: 'drupalRemote'` — the barrel
|
|
123
|
+
* descriptor in its written order, single/double/back quoted, one line or many
|
|
124
|
+
* (`\s` spans newlines). Requiring the `export:` to be ADJACENT is what keeps
|
|
125
|
+
* this from ever pairing a barrel mention with an unrelated slot's `export:`.
|
|
126
|
+
*/
|
|
127
|
+
const BARREL_THEN_EXPORT = new RegExp(`(${PKG_KEY}\\s*:\\s*)(['"\`])${BARREL_PATH}\\2(\\s*,\\s*export\\s*:\\s*)(['"\`])(${IDENT})\\4`, 'g');
|
|
128
|
+
/** The same descriptor with the keys written the other way round. */
|
|
129
|
+
const EXPORT_THEN_BARREL = new RegExp(`export\\s*:\\s*(['"\`])(${IDENT})\\1(\\s*,\\s*)(${PKG_KEY}\\s*:\\s*)(['"\`])${BARREL_PATH}\\5`, 'g');
|
|
130
|
+
/**
|
|
131
|
+
* Rewrite the `@gaia-ai/core/plugins` descriptors in one config's source text.
|
|
132
|
+
*
|
|
133
|
+
* A mapped descriptor becomes `<key>: '<addon>'` — with the `export:` DROPPED
|
|
134
|
+
* when the addon default-exports that factory *and* the whole descriptor sits on
|
|
135
|
+
* one line, so the rewrite stays line-local (the index-aligned report diff and
|
|
136
|
+
* `migrateAddonNames`' line-count invariant depend on that). A multi-line
|
|
137
|
+
* descriptor keeps its `export:`: every mapped name is also a NAMED export of its
|
|
138
|
+
* target addon, so the result resolves either way — only the redundancy survives.
|
|
139
|
+
*
|
|
140
|
+
* Left byte-untouched: an unmapped/unknown `export:` name, and a barrel
|
|
141
|
+
* occurrence with no adjacent `export:` at all (a bare `addons: ['…/plugins']` or
|
|
142
|
+
* a prose mention) — neither carries enough information to pick a target.
|
|
143
|
+
*/
|
|
144
|
+
function migrateBarrelDescriptors(text) {
|
|
145
|
+
let count = 0;
|
|
146
|
+
const rewrite = (whole, name, keyPrefix, quote, keepExport) => {
|
|
147
|
+
const hit = CORE_PLUGINS_BARREL_EXPORTS[name];
|
|
148
|
+
if (hit === undefined)
|
|
149
|
+
return whole;
|
|
150
|
+
count++;
|
|
151
|
+
const spec = `${keyPrefix}${quote}${hit.target}${quote}`;
|
|
152
|
+
if (hit.defaultExport && !whole.includes('\n'))
|
|
153
|
+
return spec;
|
|
154
|
+
return `${spec}${keepExport()}`;
|
|
155
|
+
};
|
|
156
|
+
const out = text
|
|
157
|
+
.replace(BARREL_THEN_EXPORT, (whole, keyPrefix, pkgQuote, exportSep, nameQuote, name) => rewrite(whole, name, keyPrefix, pkgQuote, () => `${exportSep}${nameQuote}${name}${nameQuote}`))
|
|
158
|
+
.replace(EXPORT_THEN_BARREL, (whole, nameQuote, name, sep, keyPrefix, pkgQuote) => rewrite(whole, name, keyPrefix, pkgQuote, () => `${sep}export: ${nameQuote}${name}${nameQuote}`));
|
|
159
|
+
return { text: out, count };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The object-literal entry that encloses `at`, e.g.
|
|
163
|
+
* `{ use: '@gaia-ai/core/builtins', with: { basic: token } }`. Empty string when
|
|
164
|
+
* the occurrence is a BARE string entry (`addons: ['@gaia-ai/core/builtins']`) —
|
|
165
|
+
* the backward scan then hits the array/argument boundary first.
|
|
166
|
+
*/
|
|
167
|
+
function enclosingEntry(text, at) {
|
|
168
|
+
let depth = 0;
|
|
169
|
+
let start = -1;
|
|
170
|
+
for (let i = at; i >= 0; i--) {
|
|
171
|
+
const c = text[i];
|
|
172
|
+
if (c === '}')
|
|
173
|
+
depth++;
|
|
174
|
+
else if (c === '{') {
|
|
175
|
+
if (depth === 0) {
|
|
176
|
+
start = i;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
depth--;
|
|
180
|
+
}
|
|
181
|
+
else if (depth === 0 && (c === '[' || c === '(' || c === ';')) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (start < 0)
|
|
186
|
+
return '';
|
|
187
|
+
let d = 0;
|
|
188
|
+
for (let i = start; i < text.length; i++) {
|
|
189
|
+
const c = text[i];
|
|
190
|
+
if (c === '{')
|
|
191
|
+
d++;
|
|
192
|
+
else if (c === '}') {
|
|
193
|
+
d--;
|
|
194
|
+
if (d === 0)
|
|
195
|
+
return text.slice(start, i + 1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return text.slice(start);
|
|
199
|
+
}
|
|
200
|
+
/** The nearest `plugins:` / `addons:` array key BEFORE `at`, if any. */
|
|
201
|
+
function nearestArrayKey(text, at) {
|
|
202
|
+
const re = /\b(plugins|addons)\s*:/g;
|
|
203
|
+
let last;
|
|
204
|
+
for (let m = re.exec(text); m !== null && m.index < at; m = re.exec(text)) {
|
|
205
|
+
last = m[1];
|
|
206
|
+
}
|
|
207
|
+
return last;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* `@gaia-ai/core/builtins` was ONE preset contributing to BOTH surfaces, so its
|
|
211
|
+
* replacement is per-occurrence (spec v3 decision 17, the Finding 6/8 interplay):
|
|
212
|
+
*
|
|
213
|
+
* | signal (first that applies) | target |
|
|
214
|
+
* | the entry carries a `basic` token | `@gaia-ai/addon-auth-basic` |
|
|
215
|
+
* | the nearest array key is `plugins:` | `@gaia-ai/addon-auth-basic` |
|
|
216
|
+
* | the file is a connection config | `@gaia-ai/addon-auth-basic` |
|
|
217
|
+
* | otherwise (engine `addons:` / a slot) | `@gaia-ai/addon-remote-drupal` |
|
|
218
|
+
*
|
|
219
|
+
* The `plugins:` rule is what makes a LEGACY `conductor.config.js` (engine file
|
|
220
|
+
* name, but still carrying a connection `site`/`plugins`) come out right.
|
|
221
|
+
*/
|
|
222
|
+
function builtinsTargetFor(text, at, surface) {
|
|
223
|
+
if (/basic/i.test(enclosingEntry(text, at)))
|
|
224
|
+
return AUTH_BASIC;
|
|
225
|
+
if (nearestArrayKey(text, at) === 'plugins')
|
|
226
|
+
return AUTH_BASIC;
|
|
227
|
+
return surface === 'connection' ? AUTH_BASIC : REMOTE_DRUPAL;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Rewrite the legacy specifiers in one config's SOURCE TEXT — all three: the
|
|
231
|
+
* renamed `@gaia-ai/plugin-*` packages, the deleted `@gaia-ai/core/builtins`
|
|
232
|
+
* preset, and the deleted `@gaia-ai/core/plugins` barrel descriptors. Pure; line
|
|
233
|
+
* count is preserved (every replacement is line-local), which is what lets the
|
|
234
|
+
* reported diff be a plain index-aligned line diff.
|
|
235
|
+
*/
|
|
236
|
+
export function migrateAddonNames(text, surface) {
|
|
237
|
+
let count = 0;
|
|
238
|
+
let out = text.replace(QUOTED_LEGACY_ADDON, (_m, quote, name, subpath) => {
|
|
239
|
+
count++;
|
|
240
|
+
return `${quote}@gaia-ai/addon-${name}${subpath}${quote}`;
|
|
241
|
+
});
|
|
242
|
+
out = out.replace(QUOTED_LEGACY_BUILTINS, (_m, quote, offset, whole) => {
|
|
243
|
+
count++;
|
|
244
|
+
return `${quote}${builtinsTargetFor(whole, offset, surface)}${quote}`;
|
|
245
|
+
});
|
|
246
|
+
const barrel = migrateBarrelDescriptors(out);
|
|
247
|
+
return { text: barrel.text, count: count + barrel.count };
|
|
248
|
+
}
|
|
249
|
+
/** An index-aligned `- old` / `+ new` line diff (line count is preserved). */
|
|
250
|
+
function lineDiff(before, after) {
|
|
251
|
+
const a = before.split('\n');
|
|
252
|
+
const b = after.split('\n');
|
|
253
|
+
const out = [];
|
|
254
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
255
|
+
if (a[i] === b[i])
|
|
256
|
+
continue;
|
|
257
|
+
if (a[i] !== undefined)
|
|
258
|
+
out.push(` - ${a[i]}`);
|
|
259
|
+
if (b[i] !== undefined)
|
|
260
|
+
out.push(` + ${b[i]}`);
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Rename pass over ONE config file. Returns `undefined` when the file is absent
|
|
266
|
+
* or already on the new names (idempotency: no write, no backup, no report
|
|
267
|
+
* line — the file stays byte-identical).
|
|
268
|
+
*/
|
|
269
|
+
export function migrateAddonNamesInFile(path, surface, opts = {}) {
|
|
270
|
+
if (!existsSync(path))
|
|
271
|
+
return undefined;
|
|
272
|
+
const before = readFileSync(path, 'utf8');
|
|
273
|
+
const { text: after, count } = migrateAddonNames(before, surface);
|
|
274
|
+
if (count === 0 || after === before)
|
|
275
|
+
return undefined;
|
|
276
|
+
const diff = lineDiff(before, after);
|
|
277
|
+
const what = `${count} legacy addon reference(s) → @gaia-ai/addon-*`;
|
|
278
|
+
if (opts.dryRun ?? false) {
|
|
279
|
+
return { action: `would rewrite ${path} (${what})`, diff, count };
|
|
280
|
+
}
|
|
281
|
+
const bak = `${path}${ADDON_RENAME_BAK_SUFFIX}`;
|
|
282
|
+
writeFileSync(bak, before, 'utf8');
|
|
283
|
+
writeFileSync(path, after, 'utf8');
|
|
284
|
+
return {
|
|
285
|
+
action: `rewrote ${path} (${what}; backup ${bak})`,
|
|
286
|
+
diff,
|
|
287
|
+
count,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Rename pass over every recognised config in the given `.gaia/` dirs (project
|
|
292
|
+
* + home). Emits report lines ONLY for files it actually rewrites, so an install
|
|
293
|
+
* already on `addon-*` adds no noise to the upgrade report.
|
|
294
|
+
*/
|
|
295
|
+
export function runAddonRenameMigration(opts = { gaiaDirs: [] }) {
|
|
296
|
+
const actions = [];
|
|
297
|
+
let changed = false;
|
|
298
|
+
const seen = new Set();
|
|
299
|
+
for (const dir of opts.gaiaDirs) {
|
|
300
|
+
if (seen.has(dir) || !existsSync(dir))
|
|
301
|
+
continue;
|
|
302
|
+
seen.add(dir);
|
|
303
|
+
const files = [
|
|
304
|
+
[join(dir, 'gaia.config.js'), 'connection'],
|
|
305
|
+
...listConductorConfigFiles(dir).map((f) => [join(dir, f), 'engine']),
|
|
306
|
+
];
|
|
307
|
+
for (const [path, surface] of files) {
|
|
308
|
+
const res = migrateAddonNamesInFile(path, surface, {
|
|
309
|
+
dryRun: opts.dryRun ?? false,
|
|
310
|
+
});
|
|
311
|
+
if (res === undefined)
|
|
312
|
+
continue;
|
|
313
|
+
actions.push(res.action, ...res.diff);
|
|
314
|
+
changed = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { actions, changed };
|
|
318
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ConnectionConfigMigration } from './config-schema.js';
|
|
2
|
+
export interface UpgradeReport {
|
|
3
|
+
/** Human-readable action lines (what changed / was already current). */
|
|
4
|
+
actions: string[];
|
|
5
|
+
/** true when nothing needed doing. */
|
|
6
|
+
alreadyCurrent: boolean;
|
|
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';
|
|
15
|
+
/** The outcome of routing a single connection-config file. */
|
|
16
|
+
interface ConnectionUpgradeResult {
|
|
17
|
+
action: string;
|
|
18
|
+
changed: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Route a single connection-config `path` against the schema version (GAIA-216).
|
|
22
|
+
* `label` names the file for the report; `current`/`chain` are injectable for
|
|
23
|
+
* tests (default to the shipped registry). Never executes the config module —
|
|
24
|
+
* the decision is a regex read of the header marker.
|
|
25
|
+
*
|
|
26
|
+
* | existing marker | action |
|
|
27
|
+
* | absent (no file) | seed: render current template, stamp CURRENT |
|
|
28
|
+
* | `== CURRENT` | kept (byte-identical) |
|
|
29
|
+
* | `1..CURRENT-1` (stale) | back up `.v<old>.bak`, run chain → stamp CURRENT |
|
|
30
|
+
* | `0` (unversioned) | kept (do not clobber hand edits) |
|
|
31
|
+
* | `> CURRENT` (newer) | kept (refuse to downgrade) |
|
|
32
|
+
*/
|
|
33
|
+
export declare function runConnectionUpgrade(path: string, opts?: {
|
|
34
|
+
dryRun?: boolean;
|
|
35
|
+
label?: string;
|
|
36
|
+
current?: number;
|
|
37
|
+
chain?: ConnectionConfigMigration[];
|
|
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;
|
|
45
|
+
/** Run the migration. Pure w.r.t. injected `cwd`/`home`; `dryRun` suppresses writes. */
|
|
46
|
+
export declare function runUpgrade(opts?: {
|
|
47
|
+
cwd?: string;
|
|
48
|
+
home?: string;
|
|
49
|
+
dryRun?: boolean;
|
|
50
|
+
/** GAIA-218: what to do with the PROJECT connection override (default skip). */
|
|
51
|
+
projectConnection?: ProjectConnectionChoice;
|
|
52
|
+
}): UpgradeReport;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { listConductorConfigFiles, stripLegacyConnectionFromConfigSource, } from '../config.js';
|
|
6
|
+
import { applyMigrations, CONNECTION_MIGRATIONS, GAIA_CONFIG_SCHEMA_VERSION, readConfigSchemaVersion, stampVersion, stripVersionHeader, } from './config-schema.js';
|
|
7
|
+
import { renderGaiaConfig } from './init.js';
|
|
8
|
+
import { runAddonRenameMigration } from './migrate-addon-names.js';
|
|
9
|
+
const GAIA_DIR = '.gaia';
|
|
10
|
+
const DEFAULT_ENGINE = 'conductor.config.js';
|
|
11
|
+
const VARIANT_SUFFIX = '.conductor.config.js';
|
|
12
|
+
const CONNECTION = 'gaia.config.js';
|
|
13
|
+
/** Walk from cwd root-ward to the nearest `.gaia/` dir. */
|
|
14
|
+
function findGaiaDir(cwd) {
|
|
15
|
+
let dir = resolve(cwd);
|
|
16
|
+
for (;;) {
|
|
17
|
+
const g = join(dir, GAIA_DIR);
|
|
18
|
+
if (existsSync(g))
|
|
19
|
+
return g;
|
|
20
|
+
const parent = dirname(dir);
|
|
21
|
+
if (parent === dir)
|
|
22
|
+
return undefined;
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function hasEngineConfig(gaiaDir) {
|
|
27
|
+
return readdirSync(gaiaDir).some((f) => f === DEFAULT_ENGINE || f.endsWith(VARIANT_SUFFIX));
|
|
28
|
+
}
|
|
29
|
+
function legacyMachinePath(home) {
|
|
30
|
+
return join(home, '.config', 'conductor', 'conductor.config.machine.js');
|
|
31
|
+
}
|
|
32
|
+
function canonicalMachinePath(home) {
|
|
33
|
+
return join(home, GAIA_DIR, 'machine.config.js');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Route a single connection-config `path` against the schema version (GAIA-216).
|
|
37
|
+
* `label` names the file for the report; `current`/`chain` are injectable for
|
|
38
|
+
* tests (default to the shipped registry). Never executes the config module —
|
|
39
|
+
* the decision is a regex read of the header marker.
|
|
40
|
+
*
|
|
41
|
+
* | existing marker | action |
|
|
42
|
+
* | absent (no file) | seed: render current template, stamp CURRENT |
|
|
43
|
+
* | `== CURRENT` | kept (byte-identical) |
|
|
44
|
+
* | `1..CURRENT-1` (stale) | back up `.v<old>.bak`, run chain → stamp CURRENT |
|
|
45
|
+
* | `0` (unversioned) | kept (do not clobber hand edits) |
|
|
46
|
+
* | `> CURRENT` (newer) | kept (refuse to downgrade) |
|
|
47
|
+
*/
|
|
48
|
+
export function runConnectionUpgrade(path, opts = {}) {
|
|
49
|
+
const dry = opts.dryRun ?? false;
|
|
50
|
+
const label = opts.label ?? 'connection config';
|
|
51
|
+
const current = opts.current ?? GAIA_CONFIG_SCHEMA_VERSION;
|
|
52
|
+
const chain = opts.chain ?? CONNECTION_MIGRATIONS;
|
|
53
|
+
if (!existsSync(path)) {
|
|
54
|
+
if (!dry) {
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
+
writeFileSync(path, renderGaiaConfig(), 'utf8');
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
action: `${dry ? 'would write' : 'wrote'} ${path} (${label}, schema v${current})`,
|
|
60
|
+
changed: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const version = readConfigSchemaVersion(path);
|
|
64
|
+
if (version === current) {
|
|
65
|
+
return {
|
|
66
|
+
action: `kept ${path} (${label}, schema v${current} — up to date)`,
|
|
67
|
+
changed: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (version === 0) {
|
|
71
|
+
return {
|
|
72
|
+
action: `kept ${path} (${label}, unversioned — pass --force to regenerate)`,
|
|
73
|
+
changed: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (version > current) {
|
|
77
|
+
return {
|
|
78
|
+
action: `kept ${path} (${label}, schema v${version} newer than gaia's v${current} — not downgrading)`,
|
|
79
|
+
changed: false,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// 1..current-1: migrate via the per-version chain.
|
|
83
|
+
const steps = chain
|
|
84
|
+
.filter((m) => m.from >= version && m.to <= current)
|
|
85
|
+
.map((m) => m.description)
|
|
86
|
+
.join('; ') || 'regenerate';
|
|
87
|
+
if (dry) {
|
|
88
|
+
return {
|
|
89
|
+
action: `would migrate ${path} schema v${version}→v${current} (steps: ${steps})`,
|
|
90
|
+
changed: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const original = readFileSync(path, 'utf8');
|
|
94
|
+
const bak = `${path}.v${version}.bak`;
|
|
95
|
+
writeFileSync(bak, original, 'utf8');
|
|
96
|
+
const migrated = applyMigrations(stripVersionHeader(original), version, current, chain);
|
|
97
|
+
writeFileSync(path, stampVersion(migrated, current), 'utf8');
|
|
98
|
+
return {
|
|
99
|
+
action: `migrated ${path} schema v${version}→v${current} (steps: ${steps}; backup ${bak})`,
|
|
100
|
+
changed: true,
|
|
101
|
+
};
|
|
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
|
+
}
|
|
115
|
+
/** Run the migration. Pure w.r.t. injected `cwd`/`home`; `dryRun` suppresses writes. */
|
|
116
|
+
export function runUpgrade(opts = {}) {
|
|
117
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
118
|
+
const home = opts.home ?? homedir();
|
|
119
|
+
const dry = opts.dryRun ?? false;
|
|
120
|
+
const choice = opts.projectConnection ?? 'skip';
|
|
121
|
+
const actions = [];
|
|
122
|
+
let changed = false;
|
|
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).
|
|
148
|
+
if (gaiaDir !== undefined && hasEngineConfig(gaiaDir)) {
|
|
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
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// 2. Machine context move (secret-bearing — never schema-migrated).
|
|
185
|
+
const legacy = legacyMachinePath(home);
|
|
186
|
+
const canonical = canonicalMachinePath(home);
|
|
187
|
+
if (existsSync(canonical)) {
|
|
188
|
+
actions.push(`kept ${canonical} (machine context already migrated)`);
|
|
189
|
+
}
|
|
190
|
+
else if (existsSync(legacy)) {
|
|
191
|
+
if (!dry) {
|
|
192
|
+
mkdirSync(dirname(canonical), { recursive: true });
|
|
193
|
+
copyFileSync(legacy, canonical);
|
|
194
|
+
chmodSync(canonical, 0o600);
|
|
195
|
+
// Leave a re-export shim at the legacy path so an un-upgraded config still resolves.
|
|
196
|
+
writeFileSync(legacy, `// Moved to ${canonical} by \`gaia upgrade\` (GAIA-201).\nexport { default } from '${pathToFileURL(canonical).href}';\n`, 'utf8');
|
|
197
|
+
}
|
|
198
|
+
actions.push(`${dry ? 'would move' : 'moved'} ${legacy} → ${canonical} (machine context, 0600 + shim)`);
|
|
199
|
+
changed = true;
|
|
200
|
+
}
|
|
201
|
+
// 3. Home connection config (schema-versioned seed/migrate).
|
|
202
|
+
const homeRes = runConnectionUpgrade(join(home, GAIA_DIR, CONNECTION), {
|
|
203
|
+
dryRun: dry,
|
|
204
|
+
label: 'home connection config',
|
|
205
|
+
});
|
|
206
|
+
actions.push(homeRes.action);
|
|
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;
|
|
221
|
+
return { actions, alreadyCurrent: !changed };
|
|
222
|
+
}
|
|
@@ -81,8 +81,12 @@ export function updateNotice(current, latest) {
|
|
|
81
81
|
return null;
|
|
82
82
|
if (!semver.gt(latest, current))
|
|
83
83
|
return null;
|
|
84
|
+
// Two distinct steps, apt-style: first get the newer CODE, then migrate the
|
|
85
|
+
// CONFIG. There is no `gaia update` command — the CLI is a global npm package,
|
|
86
|
+
// so code is updated via npm; `gaia upgrade` then migrates the config shape.
|
|
84
87
|
return (`A new gaia version is available: ${current} → ${latest}\n` +
|
|
85
|
-
'Run `gaia
|
|
88
|
+
'Run `npm install -g @gaia-ai/gaia@latest` to update the CLI, ' +
|
|
89
|
+
'then `gaia upgrade` to migrate your config.');
|
|
86
90
|
}
|
|
87
91
|
const defaultSpawn = (cmd, args) => new Promise((resolve) => {
|
|
88
92
|
const child = spawn(cmd, args, { stdio: 'inherit', shell: false });
|