@gaia-ai/conductor 0.5.4 → 0.6.0
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/dist/src/cli/conductor-registry.d.ts +7 -0
- package/dist/src/cli/conductor-registry.js +6 -0
- package/dist/src/cli/config-schema.d.ts +54 -0
- package/dist/src/cli/config-schema.js +91 -0
- package/dist/src/cli/deployment.d.ts +34 -0
- package/dist/src/cli/deployment.js +63 -0
- package/dist/src/cli/init.d.ts +31 -33
- package/dist/src/cli/init.js +96 -80
- package/dist/src/cli/upgrade.d.ts +38 -0
- package/dist/src/cli/upgrade.js +143 -0
- package/dist/src/cli/version-check.js +5 -1
- package/dist/src/commands/conductor.d.ts +33 -0
- package/dist/src/{cli/gaia.js → commands/conductor.js} +115 -210
- package/dist/src/config.d.ts +50 -23
- package/dist/src/config.js +127 -153
- package/dist/src/core/conductor.js +14 -11
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.js +11 -2
- package/dist/src/preset.d.ts +2 -0
- package/dist/src/preset.js +8 -0
- package/package.json +7 -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,7 @@
|
|
|
1
|
+
export type { ConductorRegistryEntry } from '@gaia-ai/core';
|
|
2
|
+
import { conductorRegistryPath, getRegisteredConductor, listRegisteredConductors, registerConductor, removeConductor } from '@gaia-ai/core';
|
|
3
|
+
export { conductorRegistryPath };
|
|
4
|
+
export declare const list: typeof listRegisteredConductors;
|
|
5
|
+
export declare const get: typeof getRegisteredConductor;
|
|
6
|
+
export declare const register: typeof registerConductor;
|
|
7
|
+
export declare const remove: typeof removeConductor;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { conductorRegistryPath, getRegisteredConductor, listRegisteredConductors, registerConductor, removeConductor, } from '@gaia-ai/core';
|
|
2
|
+
export { conductorRegistryPath };
|
|
3
|
+
export const list = listRegisteredConductors;
|
|
4
|
+
export const get = getRegisteredConductor;
|
|
5
|
+
export const register = registerConductor;
|
|
6
|
+
export const remove = removeConductor;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single, discrete, testable upgrade method `vN → vN+1`, registered in order —
|
|
3
|
+
* exactly like a Drupal `hook_update_N` chain. `gaia upgrade` resolves a config's
|
|
4
|
+
* current version and applies each method in sequence up to CURRENT (it does not
|
|
5
|
+
* blindly overwrite), so a method can PRESERVE operator-set fields across a bump.
|
|
6
|
+
*/
|
|
7
|
+
export interface ConnectionConfigMigration {
|
|
8
|
+
/** The schema version this method upgrades FROM. */
|
|
9
|
+
from: number;
|
|
10
|
+
/** The schema version this method upgrades TO (=== from + 1). */
|
|
11
|
+
to: number;
|
|
12
|
+
/** What shape change this method performs (human-readable). */
|
|
13
|
+
description: string;
|
|
14
|
+
/**
|
|
15
|
+
* Upgrade the config text from `from` to `to`, returning the new file text
|
|
16
|
+
* WITHOUT the version header — the runner re-stamps `@gaia-schema-version to`.
|
|
17
|
+
*/
|
|
18
|
+
apply(currentText: string): string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The ordered, contiguous migration chain. Ships EMPTY at v1: nothing below v1
|
|
22
|
+
* is a versioned config (v0 = seed / hand-authored / untouched). The FIRST real
|
|
23
|
+
* shape change adds the first `{ from: 1, to: 2, … }` method here, which bumps
|
|
24
|
+
* the derived CURRENT to 2 automatically. The extension point is real and
|
|
25
|
+
* exercised by tests now, so the first shape change is a one-method addition,
|
|
26
|
+
* never a re-plumb.
|
|
27
|
+
*/
|
|
28
|
+
export declare const CONNECTION_MIGRATIONS: ConnectionConfigMigration[];
|
|
29
|
+
/** The current connection-config schema version — single source of truth. */
|
|
30
|
+
export declare const GAIA_CONFIG_SCHEMA_VERSION: number;
|
|
31
|
+
/** Parse the `@gaia-schema-version` marker from config text; `0` when absent. */
|
|
32
|
+
export declare function schemaVersionOf(text: string): number;
|
|
33
|
+
/**
|
|
34
|
+
* Read the schema version from a connection config FILE by regex (sync, never
|
|
35
|
+
* executes the module). `0` when the file is absent, unreadable, or carries no
|
|
36
|
+
* marker (legacy / hand-authored).
|
|
37
|
+
*/
|
|
38
|
+
export declare function readConfigSchemaVersion(path: string): number;
|
|
39
|
+
/** Strip a leading `// @gaia-schema-version <N>` header line, if present. */
|
|
40
|
+
export declare function stripVersionHeader(text: string): string;
|
|
41
|
+
/** Prepend (replacing any existing) a `// @gaia-schema-version <N>` header. */
|
|
42
|
+
export declare function stampVersion(text: string, version: number): string;
|
|
43
|
+
/**
|
|
44
|
+
* Validate a migration chain is contiguous: each `to === from + 1`, the first
|
|
45
|
+
* migrates from v1, no gaps, and the chain ends at `current` (default CURRENT).
|
|
46
|
+
* An empty chain is valid iff `current === 1`. Throws with an actionable message.
|
|
47
|
+
*/
|
|
48
|
+
export declare function assertContiguous(chain: ConnectionConfigMigration[], current?: number): void;
|
|
49
|
+
/**
|
|
50
|
+
* Walk the migration chain from `from` up to `to`, applying each method's
|
|
51
|
+
* `apply` in sequence. Input/output text is WITHOUT the version header — the
|
|
52
|
+
* caller stamps CURRENT after. `chain` is injectable for tests.
|
|
53
|
+
*/
|
|
54
|
+
export declare function applyMigrations(text: string, from: number, to: number, chain?: ConnectionConfigMigration[]): string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
// GAIA-216: schema versioning for the generated CONNECTION config
|
|
3
|
+
// (`gaia.config.js`, project + home). Decoupled from the npm package semver —
|
|
4
|
+
// the schema version bumps only when the config SHAPE changes, not every
|
|
5
|
+
// release. Every generated connection config carries a machine-readable header
|
|
6
|
+
// comment `// @gaia-schema-version <N>` (the source of truth for the upgrade
|
|
7
|
+
// decision, read by regex so `gaia upgrade` NEVER executes a possibly
|
|
8
|
+
// side-effecting config module) and a `schema_version: <N>` field for
|
|
9
|
+
// programmatic reads.
|
|
10
|
+
/** The header marker prepended to every generated connection config. */
|
|
11
|
+
const MARKER_RE = /@gaia-schema-version\s+(\d+)/;
|
|
12
|
+
const HEADER_LINE_RE = /^\/\/ @gaia-schema-version \d+\n/;
|
|
13
|
+
/**
|
|
14
|
+
* The ordered, contiguous migration chain. Ships EMPTY at v1: nothing below v1
|
|
15
|
+
* is a versioned config (v0 = seed / hand-authored / untouched). The FIRST real
|
|
16
|
+
* shape change adds the first `{ from: 1, to: 2, … }` method here, which bumps
|
|
17
|
+
* the derived CURRENT to 2 automatically. The extension point is real and
|
|
18
|
+
* exercised by tests now, so the first shape change is a one-method addition,
|
|
19
|
+
* never a re-plumb.
|
|
20
|
+
*/
|
|
21
|
+
export const CONNECTION_MIGRATIONS = [
|
|
22
|
+
// { from: 1, to: 2, description: 'add site.jsonapi_prefix default', apply: (t) => … },
|
|
23
|
+
];
|
|
24
|
+
/** The current connection-config schema version — single source of truth. */
|
|
25
|
+
export const GAIA_CONFIG_SCHEMA_VERSION = CONNECTION_MIGRATIONS.at(-1)?.to ?? 1;
|
|
26
|
+
/** Parse the `@gaia-schema-version` marker from config text; `0` when absent. */
|
|
27
|
+
export function schemaVersionOf(text) {
|
|
28
|
+
const m = MARKER_RE.exec(text);
|
|
29
|
+
return m ? Number.parseInt(m[1], 10) : 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read the schema version from a connection config FILE by regex (sync, never
|
|
33
|
+
* executes the module). `0` when the file is absent, unreadable, or carries no
|
|
34
|
+
* marker (legacy / hand-authored).
|
|
35
|
+
*/
|
|
36
|
+
export function readConfigSchemaVersion(path) {
|
|
37
|
+
try {
|
|
38
|
+
return schemaVersionOf(readFileSync(path, 'utf8'));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Strip a leading `// @gaia-schema-version <N>` header line, if present. */
|
|
45
|
+
export function stripVersionHeader(text) {
|
|
46
|
+
return text.replace(HEADER_LINE_RE, '');
|
|
47
|
+
}
|
|
48
|
+
/** Prepend (replacing any existing) a `// @gaia-schema-version <N>` header. */
|
|
49
|
+
export function stampVersion(text, version) {
|
|
50
|
+
return `// @gaia-schema-version ${version}\n${stripVersionHeader(text)}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Validate a migration chain is contiguous: each `to === from + 1`, the first
|
|
54
|
+
* migrates from v1, no gaps, and the chain ends at `current` (default CURRENT).
|
|
55
|
+
* An empty chain is valid iff `current === 1`. Throws with an actionable message.
|
|
56
|
+
*/
|
|
57
|
+
export function assertContiguous(chain, current = GAIA_CONFIG_SCHEMA_VERSION) {
|
|
58
|
+
let prev = 1;
|
|
59
|
+
chain.forEach((m, i) => {
|
|
60
|
+
if (m.to !== m.from + 1) {
|
|
61
|
+
throw new Error(`migration[${i}]: to (${m.to}) must equal from+1 (${m.from + 1})`);
|
|
62
|
+
}
|
|
63
|
+
const expectedFrom = i === 0 ? 1 : prev;
|
|
64
|
+
if (m.from !== expectedFrom) {
|
|
65
|
+
throw new Error(`migration[${i}]: from (${m.from}) must equal ${expectedFrom} (no gaps; chain starts at v1)`);
|
|
66
|
+
}
|
|
67
|
+
prev = m.to;
|
|
68
|
+
});
|
|
69
|
+
const end = chain.at(-1)?.to ?? 1;
|
|
70
|
+
if (end !== current) {
|
|
71
|
+
throw new Error(`migration chain ends at v${end}, expected CURRENT v${current}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Walk the migration chain from `from` up to `to`, applying each method's
|
|
76
|
+
* `apply` in sequence. Input/output text is WITHOUT the version header — the
|
|
77
|
+
* caller stamps CURRENT after. `chain` is injectable for tests.
|
|
78
|
+
*/
|
|
79
|
+
export function applyMigrations(text, from, to, chain = CONNECTION_MIGRATIONS) {
|
|
80
|
+
let cur = from;
|
|
81
|
+
let out = text;
|
|
82
|
+
while (cur < to) {
|
|
83
|
+
const step = chain.find((m) => m.from === cur);
|
|
84
|
+
if (step === undefined) {
|
|
85
|
+
throw new Error(`no migration from v${cur} (chain incomplete)`);
|
|
86
|
+
}
|
|
87
|
+
out = step.apply(out);
|
|
88
|
+
cur = step.to;
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type GaiaRemote } from '@gaia-ai/core';
|
|
2
|
+
import type { Command } from 'commander';
|
|
3
|
+
export interface GatherResult {
|
|
4
|
+
range: string;
|
|
5
|
+
resolved: Array<{
|
|
6
|
+
identifier: string;
|
|
7
|
+
uuid: string;
|
|
8
|
+
title: string;
|
|
9
|
+
}>;
|
|
10
|
+
unresolved: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Deduped GAIA-nnn from a git log, in first-seen order. */
|
|
13
|
+
export declare function parseIdentifiers(gitLog: string): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Compute the release batch from a git-log delta: parse GAIA-nnn identifiers and
|
|
16
|
+
* resolve each to a ticket. Unresolved identifiers are reported, never fatal
|
|
17
|
+
* (the resolver returning null is not an error) — GAIA-153 AC-2.
|
|
18
|
+
*/
|
|
19
|
+
export declare function gatherBatch(opts: {
|
|
20
|
+
sinceRef: string;
|
|
21
|
+
toRef: string;
|
|
22
|
+
gitLog: string;
|
|
23
|
+
resolve: (id: string) => Promise<{
|
|
24
|
+
uuid: string;
|
|
25
|
+
title: string;
|
|
26
|
+
} | null>;
|
|
27
|
+
}): Promise<GatherResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Register the `gaia deployment` command group. `tickets` computes the release's
|
|
30
|
+
* tickets (git-log identifier parse + resolve over the given range) and prints
|
|
31
|
+
* them as JSON — pure compute + report, it does not write `referenced_tickets`
|
|
32
|
+
* (the prepare-deployment skill does, via the documented dropsh path).
|
|
33
|
+
*/
|
|
34
|
+
export declare function registerDeployment(program: Command, resolveRemote: () => Promise<GaiaRemote>, resolveProject: () => Promise<string>, cwd: () => string): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { exec } from '@gaia-ai/core';
|
|
2
|
+
/** Deduped GAIA-nnn from a git log, in first-seen order. */
|
|
3
|
+
export function parseIdentifiers(gitLog) {
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
const out = [];
|
|
6
|
+
for (const m of gitLog.matchAll(/\bGAIA-\d+\b/g)) {
|
|
7
|
+
if (!seen.has(m[0])) {
|
|
8
|
+
seen.add(m[0]);
|
|
9
|
+
out.push(m[0]);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Compute the release batch from a git-log delta: parse GAIA-nnn identifiers and
|
|
16
|
+
* resolve each to a ticket. Unresolved identifiers are reported, never fatal
|
|
17
|
+
* (the resolver returning null is not an error) — GAIA-153 AC-2.
|
|
18
|
+
*/
|
|
19
|
+
export async function gatherBatch(opts) {
|
|
20
|
+
const resolved = [];
|
|
21
|
+
const unresolved = [];
|
|
22
|
+
for (const identifier of parseIdentifiers(opts.gitLog)) {
|
|
23
|
+
const hit = await opts.resolve(identifier);
|
|
24
|
+
if (hit) {
|
|
25
|
+
resolved.push({ identifier, ...hit });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
unresolved.push(identifier);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { range: `${opts.sinceRef}..${opts.toRef}`, resolved, unresolved };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Register the `gaia deployment` command group. `tickets` computes the release's
|
|
35
|
+
* tickets (git-log identifier parse + resolve over the given range) and prints
|
|
36
|
+
* them as JSON — pure compute + report, it does not write `referenced_tickets`
|
|
37
|
+
* (the prepare-deployment skill does, via the documented dropsh path).
|
|
38
|
+
*/
|
|
39
|
+
export function registerDeployment(program, resolveRemote, resolveProject, cwd) {
|
|
40
|
+
const dep = program
|
|
41
|
+
.command('deployment')
|
|
42
|
+
.description('release / deployment helpers');
|
|
43
|
+
dep
|
|
44
|
+
.command('tickets')
|
|
45
|
+
.description("the release's tickets (git-log identifier parse + resolve over a range) as JSON")
|
|
46
|
+
.requiredOption('--since <ref>', "the env's currently-deployed ref (git range start)")
|
|
47
|
+
.requiredOption('--to <ref>', 'the post-merge env branch tip (git range end)')
|
|
48
|
+
.action(async (opts) => {
|
|
49
|
+
// Prefix each commit with a record separator (%x1e) so one commit's body
|
|
50
|
+
// cannot run into the next commit's header line; the identifier parse is
|
|
51
|
+
// separator-agnostic, but the boundary keeps the log unambiguous.
|
|
52
|
+
const gitLog = await exec('git', ['log', '--format=%x1e%H %s%n%b', `${opts.since}..${opts.to}`], { cwd: cwd() });
|
|
53
|
+
const remote = await resolveRemote();
|
|
54
|
+
const project = await resolveProject();
|
|
55
|
+
const result = await gatherBatch({
|
|
56
|
+
sinceRef: opts.since,
|
|
57
|
+
toRef: opts.to,
|
|
58
|
+
gitLog,
|
|
59
|
+
resolve: (id) => remote.resolveTicketByIdentifier(project, id),
|
|
60
|
+
});
|
|
61
|
+
console.log(JSON.stringify(result));
|
|
62
|
+
});
|
|
63
|
+
}
|
package/dist/src/cli/init.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type MachineContext, machineContextPath, readMachineContext } from '@gaia-ai/core';
|
|
2
|
+
export type { MachineContext };
|
|
3
|
+
export { machineContextPath, readMachineContext };
|
|
1
4
|
export interface InitInputs {
|
|
2
5
|
baseUrl: string;
|
|
3
6
|
project: string;
|
|
@@ -21,19 +24,6 @@ export interface ScaffoldResult {
|
|
|
21
24
|
wroteCommitted: boolean;
|
|
22
25
|
machine: MachineContextResult;
|
|
23
26
|
}
|
|
24
|
-
/**
|
|
25
|
-
* The user-global machine context: a plain importable module carrying the
|
|
26
|
-
* developer's machine identity and connection (incl. the OAuth client secret).
|
|
27
|
-
* Committed configs import it to compose machine_id and read
|
|
28
|
-
* base_url / client_id / client_secret. Gitignored, user-only (chmod 0600).
|
|
29
|
-
*/
|
|
30
|
-
export interface MachineContext {
|
|
31
|
-
machine_id: string;
|
|
32
|
-
user_id: string;
|
|
33
|
-
base_url: string;
|
|
34
|
-
client_id: string;
|
|
35
|
-
client_secret: string;
|
|
36
|
-
}
|
|
37
27
|
export interface MachineContextOptions {
|
|
38
28
|
path: string;
|
|
39
29
|
userId: string;
|
|
@@ -48,35 +38,43 @@ export interface MachineContextResult {
|
|
|
48
38
|
filledKeys: string[];
|
|
49
39
|
}
|
|
50
40
|
/**
|
|
51
|
-
* The committed, structural conductor config.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
41
|
+
* The committed, structural ENGINE conductor config (GAIA-201). It carries the
|
|
42
|
+
* engine wiring only — remote/executor/agent/workspace + `machine_id`
|
|
43
|
+
* composition — and NO `site`/`plugins`, which now live in the sibling
|
|
44
|
+
* connection config `gaia.config.js`. Connection + identity (incl. the secret)
|
|
45
|
+
* come from the user-global machine context (`~/.gaia/machine.config.js`);
|
|
46
|
+
* `machine_id` is composed as `${user_id}-${machine_id}-${project}`.
|
|
57
47
|
*/
|
|
58
48
|
export declare function renderCommittedConfig(inputs: Pick<InitInputs, 'project'>): string;
|
|
49
|
+
/**
|
|
50
|
+
* The committed CONNECTION config `./.gaia/gaia.config.js` (GAIA-201). A
|
|
51
|
+
* dropsh-shaped `{ site, plugins }` read by `gaia ui`, `gaia dropsh`, and the
|
|
52
|
+
* conductor's auth. Reads base_url + client credentials from the user-global
|
|
53
|
+
* machine context (`~/.gaia/machine.config.js`) and declares the session
|
|
54
|
+
* (default) + pm oauth2 profiles.
|
|
55
|
+
*/
|
|
56
|
+
export declare function renderGaiaConfig(): string;
|
|
59
57
|
/** The user-global machine context module: identity + connection (incl. secret). */
|
|
60
58
|
export declare function renderMachineContext(ctx: MachineContext): string;
|
|
61
|
-
/** The user-global machine context path: ~/.config/conductor/conductor.config.machine.js */
|
|
62
|
-
export declare function machineContextPath(): string;
|
|
63
|
-
/** Import an existing context module's default export, or {} if absent/broken. */
|
|
64
|
-
export declare function readMachineContext(path: string): Promise<Partial<MachineContext>>;
|
|
65
59
|
/**
|
|
66
60
|
* Create-if-missing / fill-only-missing the user-global machine context.
|
|
67
61
|
* Existing values always win; only absent/blank keys are filled. machine_id
|
|
68
|
-
* defaults to hostname().
|
|
69
|
-
*
|
|
62
|
+
* defaults to hostname(). Written user-only (chmod 0600) since it holds the
|
|
63
|
+
* client secret.
|
|
70
64
|
*/
|
|
71
65
|
export declare function scaffoldMachineContext(opts: MachineContextOptions): Promise<MachineContextResult>;
|
|
66
|
+
/** Write the sibling connection config `gaia.config.js` next to the engine
|
|
67
|
+
* config. Created only if missing (unless `force`). Returns its path + whether
|
|
68
|
+
* it was written. */
|
|
69
|
+
export declare function scaffoldGaiaConfig(engineConfigPath: string, force: boolean): {
|
|
70
|
+
path: string;
|
|
71
|
+
wrote: boolean;
|
|
72
|
+
};
|
|
72
73
|
/**
|
|
73
|
-
* Scaffold the
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* `skipMachine` writes the committed config only (project-only setup); its
|
|
79
|
-
* mirror `skipCommitted` writes the machine context only (machine-only
|
|
80
|
-
* onboarding, no repo). Setting both is a no-op.
|
|
74
|
+
* Scaffold the committed ENGINE conductor.config.js (created only if missing —
|
|
75
|
+
* never overwritten unless `force`) and the user-global machine context
|
|
76
|
+
* (create-if-missing / fill-only-missing). The connection config is scaffolded
|
|
77
|
+
* separately by `scaffoldGaiaConfig`. `skipMachine` writes the committed config
|
|
78
|
+
* only; `skipCommitted` writes the machine context only.
|
|
81
79
|
*/
|
|
82
80
|
export declare function scaffold(inputs: InitInputs, opts: ScaffoldOptions): Promise<ScaffoldResult>;
|
package/dist/src/cli/init.js
CHANGED
|
@@ -1,50 +1,51 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { hostname } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { machineContextPath, readMachineContext, } from '@gaia-ai/core';
|
|
5
|
+
import { GAIA_CONFIG_SCHEMA_VERSION } from './config-schema.js';
|
|
6
|
+
export { machineContextPath, readMachineContext };
|
|
5
7
|
/** JS single-quoted string literal for a trusted, simple value. */
|
|
6
8
|
function q(value) {
|
|
7
9
|
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
|
-
* The committed, structural conductor config.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* The committed, structural ENGINE conductor config (GAIA-201). It carries the
|
|
13
|
+
* engine wiring only — remote/executor/agent/workspace + `machine_id`
|
|
14
|
+
* composition — and NO `site`/`plugins`, which now live in the sibling
|
|
15
|
+
* connection config `gaia.config.js`. Connection + identity (incl. the secret)
|
|
16
|
+
* come from the user-global machine context (`~/.gaia/machine.config.js`);
|
|
17
|
+
* `machine_id` is composed as `${user_id}-${machine_id}-${project}`.
|
|
16
18
|
*/
|
|
17
19
|
export function renderCommittedConfig(inputs) {
|
|
18
|
-
return `// Canonical GAIA conductor config — committed. Connection + identity
|
|
19
|
-
// your user-global machine context (~/.
|
|
20
|
+
return `// Canonical GAIA conductor ENGINE config — committed. Connection + identity
|
|
21
|
+
// come from your user-global machine context (~/.gaia/machine.config.js:
|
|
20
22
|
// { machine_id, user_id, base_url, client_id, client_secret }); machine_id is
|
|
21
23
|
// composed here as \`\${user_id}-\${machine_id}-\${project}\`. \`project\` is the only
|
|
22
|
-
// per-repo value and is baked in below.
|
|
23
|
-
//
|
|
24
|
+
// per-repo value and is baked in below. This file is ENGINE-ONLY (GAIA-201):
|
|
25
|
+
// it has NO \`site\` and NO auth \`plugins\` — those live in the sibling
|
|
26
|
+
// ./.gaia/gaia.config.js (the connection config that ui + dropsh + this
|
|
27
|
+
// conductor's auth all read).
|
|
24
28
|
//
|
|
25
|
-
// IMPORT-FREE (GAIA-78): the plugin slots
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// barrel (many exports → \`export: 'drupalRemote'\`) and the merged herdr
|
|
33
|
-
// workspace slot (\`@gaia-ai/plugin-herdr\` default-exports the executor, so the
|
|
34
|
-
// workspace names \`export: 'herdrWorkspace'\` — GAIA-139).
|
|
29
|
+
// IMPORT-FREE (GAIA-78): the plugin slots are \`{ plugin, with }\` descriptors
|
|
30
|
+
// naming the REAL published package, not \`import\`ed constructors.
|
|
31
|
+
// loadConductorConfig resolves each name ESLint-style (config dir → cwd →
|
|
32
|
+
// conductor install). Each plugin package default-exports its factory (no
|
|
33
|
+
// \`export:\`) EXCEPT the multi-export host barrel and the merged herdr workspace
|
|
34
|
+
// (\`@gaia-ai/plugin-herdr\` default-exports the executor, so the workspace names
|
|
35
|
+
// \`export: 'herdrWorkspace'\` — GAIA-139).
|
|
35
36
|
|
|
36
37
|
// The user-global machine context: identity + connection (incl. secret), shared
|
|
37
38
|
// by every project on this machine. Never committed.
|
|
38
39
|
async function loadMachine() {
|
|
39
40
|
try {
|
|
40
|
-
return (await import(\`\${process.env.HOME}/.
|
|
41
|
+
return (await import(\`\${process.env.HOME}/.gaia/machine.config.js\`)).default ?? {};
|
|
41
42
|
} catch {}
|
|
42
43
|
return {};
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
// OPTIONAL per-project override — create conductor.config.local.js beside this
|
|
46
|
-
// file to override any field (machine_id,
|
|
47
|
-
//
|
|
47
|
+
// file to override any field (machine_id, model, …). Loaded only if present and
|
|
48
|
+
// NOT created by \`gaia conductor init\`.
|
|
48
49
|
async function loadLocal() {
|
|
49
50
|
try { return (await import('./conductor.config.local.js')).default ?? {}; } catch {}
|
|
50
51
|
return {};
|
|
@@ -53,47 +54,74 @@ async function loadLocal() {
|
|
|
53
54
|
const machine = await loadMachine();
|
|
54
55
|
const local = await loadLocal();
|
|
55
56
|
const project = local.project ?? ${q(inputs.project)};
|
|
56
|
-
const baseUrl = local.base_url ?? machine.base_url;
|
|
57
|
-
const clientId = local.oauth?.client_id ?? machine.client_id ?? 'gaia-agent';
|
|
58
|
-
const clientSecret = local.oauth?.client_secret ?? machine.client_secret;
|
|
59
57
|
const composedMachineId =
|
|
60
58
|
machine.user_id && machine.machine_id
|
|
61
59
|
? \`\${machine.user_id}-\${machine.machine_id}-\${project}\`
|
|
62
60
|
: undefined;
|
|
63
61
|
|
|
64
62
|
export default {
|
|
65
|
-
site: { base_url: baseUrl, jsonapi_prefix: local.jsonapi_prefix ?? '/jsonapi' },
|
|
66
63
|
project,
|
|
67
64
|
machine_id: local.machine_id ?? composedMachineId,
|
|
68
65
|
states: ['spec', 'diagnose', 'coding', 'review'],
|
|
69
66
|
max_parallel: 5,
|
|
70
|
-
// Lifecycle hooks are executor-owned (GAIA-84): the
|
|
71
|
-
//
|
|
72
|
-
// the config top level — NOT on a plugin descriptor's \`with.hooks\`.
|
|
67
|
+
// Lifecycle hooks are executor-owned (GAIA-84): they live at the config top
|
|
68
|
+
// level — NOT on a plugin descriptor's \`with.hooks\`.
|
|
73
69
|
hooks: { after_create: 'ddev init-worktree', after_done: 'ddev delete -Oy' },
|
|
74
70
|
remote: { plugin: '@gaia-ai/gaia/plugins', export: 'drupalRemote' },
|
|
75
|
-
// No hard-wired diff pane for review: the review diff surface is hunk
|
|
76
|
-
// (GAIA-55) — agent-driven + opt-in in the human's interactive pane, not an
|
|
77
|
-
// executor-forced git-diff pane. Clicking a changed file in that hunk pane
|
|
78
|
-
// opens it editable in a spiceedit overlay (see conductor/README.md).
|
|
79
71
|
executor: { plugin: '@gaia-ai/plugin-herdr' },
|
|
80
72
|
agent: {
|
|
81
73
|
plugin: '@gaia-ai/plugin-claude',
|
|
82
74
|
with: { model: local.model ?? 'claude-opus-4-8' },
|
|
83
75
|
},
|
|
84
|
-
// GAIA-139: the herdr workspace ships in @gaia-ai/plugin-herdr
|
|
85
|
-
//
|
|
86
|
-
// the EXECUTOR, so the workspace slot must name export: 'herdrWorkspace'.
|
|
76
|
+
// GAIA-139: the herdr workspace ships in @gaia-ai/plugin-herdr, which
|
|
77
|
+
// default-exports the EXECUTOR — so the workspace slot names export: 'herdrWorkspace'.
|
|
87
78
|
workspace: {
|
|
88
79
|
plugin: '@gaia-ai/plugin-herdr',
|
|
89
80
|
export: 'herdrWorkspace',
|
|
90
81
|
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
};
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The committed CONNECTION config `./.gaia/gaia.config.js` (GAIA-201). A
|
|
87
|
+
* dropsh-shaped `{ site, plugins }` read by `gaia ui`, `gaia dropsh`, and the
|
|
88
|
+
* conductor's auth. Reads base_url + client credentials from the user-global
|
|
89
|
+
* machine context (`~/.gaia/machine.config.js`) and declares the session
|
|
90
|
+
* (default) + pm oauth2 profiles.
|
|
91
|
+
*/
|
|
92
|
+
export function renderGaiaConfig() {
|
|
93
|
+
return `// @gaia-schema-version ${GAIA_CONFIG_SCHEMA_VERSION}
|
|
94
|
+
// Canonical GAIA CONNECTION config — committed (GAIA-201). A dropsh-shaped
|
|
95
|
+
// { site, plugins } read by 'gaia ui', 'gaia dropsh', and this conductor's auth.
|
|
96
|
+
// Connection + credentials come from your user-global machine context
|
|
97
|
+
// (~/.gaia/machine.config.js). The engine wiring lives in the sibling
|
|
98
|
+
// conductor.config.js (engine-only).
|
|
99
|
+
//
|
|
100
|
+
// plugins[] is consumed by DROPSH, whose resolver has no sole-function
|
|
101
|
+
// auto-pick, so @dropsh/plugin-oauth2 entries MUST name export: 'oauth2Plugin'.
|
|
102
|
+
async function loadMachine() {
|
|
103
|
+
try {
|
|
104
|
+
return (await import(\`\${process.env.HOME}/.gaia/machine.config.js\`)).default ?? {};
|
|
105
|
+
} catch {}
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function loadLocal() {
|
|
110
|
+
try { return (await import('./conductor.config.local.js')).default ?? {}; } catch {}
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const machine = await loadMachine();
|
|
115
|
+
const local = await loadLocal();
|
|
116
|
+
const baseUrl = local.base_url ?? machine.base_url;
|
|
117
|
+
const clientId = local.oauth?.client_id ?? machine.client_id ?? 'gaia-agent';
|
|
118
|
+
const clientSecret = local.oauth?.client_secret ?? machine.client_secret;
|
|
119
|
+
|
|
120
|
+
export default {
|
|
121
|
+
// GAIA-216: the connection-config schema version — kept in sync with the
|
|
122
|
+
// header marker above so \`gaia upgrade\` can migrate a stale-shape config.
|
|
123
|
+
schema_version: ${GAIA_CONFIG_SCHEMA_VERSION},
|
|
124
|
+
site: { base_url: baseUrl, jsonapi_prefix: local.jsonapi_prefix ?? '/jsonapi' },
|
|
97
125
|
plugins: [
|
|
98
126
|
{
|
|
99
127
|
plugin: '@dropsh/plugin-oauth2',
|
|
@@ -126,12 +154,12 @@ export default {
|
|
|
126
154
|
}
|
|
127
155
|
/** The user-global machine context module: identity + connection (incl. secret). */
|
|
128
156
|
export function renderMachineContext(ctx) {
|
|
129
|
-
return `// User-global
|
|
157
|
+
return `// User-global GAIA machine context — gitignored, user-only (chmod 0600), never
|
|
130
158
|
// committed. A plain importable module holding your machine identity +
|
|
131
|
-
// connection, incl. the OAuth client secret.
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
// \`gaia conductor init\`; existing values are never overwritten.
|
|
159
|
+
// connection, incl. the OAuth client secret. The engine conductor.config.js
|
|
160
|
+
// composes machine_id (\`\${user_id}-\${machine_id}-\${project}\`) from it; the
|
|
161
|
+
// connection gaia.config.js reads base_url / client_id / client_secret. Created
|
|
162
|
+
// and gap-filled by \`gaia conductor init\`; existing values are never overwritten.
|
|
135
163
|
export default {
|
|
136
164
|
machine_id: ${q(ctx.machine_id)},
|
|
137
165
|
user_id: ${q(ctx.user_id)},
|
|
@@ -141,31 +169,11 @@ export default {
|
|
|
141
169
|
};
|
|
142
170
|
`;
|
|
143
171
|
}
|
|
144
|
-
/** The user-global machine context path: ~/.config/conductor/conductor.config.machine.js */
|
|
145
|
-
export function machineContextPath() {
|
|
146
|
-
return join(process.env.HOME ?? '', '.config', 'conductor', 'conductor.config.machine.js');
|
|
147
|
-
}
|
|
148
|
-
/** Import an existing context module's default export, or {} if absent/broken. */
|
|
149
|
-
export async function readMachineContext(path) {
|
|
150
|
-
if (!existsSync(path))
|
|
151
|
-
return {};
|
|
152
|
-
try {
|
|
153
|
-
// Cache-bust so a re-render within one process re-reads fresh.
|
|
154
|
-
const mod = await import(`${pathToFileURL(path).href}?t=${Date.now()}`);
|
|
155
|
-
const raw = mod.default;
|
|
156
|
-
return raw && typeof raw === 'object'
|
|
157
|
-
? raw
|
|
158
|
-
: {};
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
return {};
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
172
|
/**
|
|
165
173
|
* Create-if-missing / fill-only-missing the user-global machine context.
|
|
166
174
|
* Existing values always win; only absent/blank keys are filled. machine_id
|
|
167
|
-
* defaults to hostname().
|
|
168
|
-
*
|
|
175
|
+
* defaults to hostname(). Written user-only (chmod 0600) since it holds the
|
|
176
|
+
* client secret.
|
|
169
177
|
*/
|
|
170
178
|
export async function scaffoldMachineContext(opts) {
|
|
171
179
|
const existing = await readMachineContext(opts.path);
|
|
@@ -196,20 +204,28 @@ export async function scaffoldMachineContext(opts) {
|
|
|
196
204
|
mkdirSync(dirname(opts.path), { recursive: true });
|
|
197
205
|
writeFileSync(opts.path, renderMachineContext(merged), 'utf8');
|
|
198
206
|
}
|
|
199
|
-
// Always tighten perms — the file holds a secret.
|
|
200
207
|
if (existsSync(opts.path))
|
|
201
208
|
chmodSync(opts.path, 0o600);
|
|
202
209
|
return { path: opts.path, created, filledKeys };
|
|
203
210
|
}
|
|
211
|
+
/** Write the sibling connection config `gaia.config.js` next to the engine
|
|
212
|
+
* config. Created only if missing (unless `force`). Returns its path + whether
|
|
213
|
+
* it was written. */
|
|
214
|
+
export function scaffoldGaiaConfig(engineConfigPath, force) {
|
|
215
|
+
const path = join(dirname(engineConfigPath), 'gaia.config.js');
|
|
216
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
217
|
+
if (!existsSync(path) || force) {
|
|
218
|
+
writeFileSync(path, renderGaiaConfig(), 'utf8');
|
|
219
|
+
return { path, wrote: true };
|
|
220
|
+
}
|
|
221
|
+
return { path, wrote: false };
|
|
222
|
+
}
|
|
204
223
|
/**
|
|
205
|
-
* Scaffold the
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* `skipMachine` writes the committed config only (project-only setup); its
|
|
211
|
-
* mirror `skipCommitted` writes the machine context only (machine-only
|
|
212
|
-
* onboarding, no repo). Setting both is a no-op.
|
|
224
|
+
* Scaffold the committed ENGINE conductor.config.js (created only if missing —
|
|
225
|
+
* never overwritten unless `force`) and the user-global machine context
|
|
226
|
+
* (create-if-missing / fill-only-missing). The connection config is scaffolded
|
|
227
|
+
* separately by `scaffoldGaiaConfig`. `skipMachine` writes the committed config
|
|
228
|
+
* only; `skipCommitted` writes the machine context only.
|
|
213
229
|
*/
|
|
214
230
|
export async function scaffold(inputs, opts) {
|
|
215
231
|
const committedPath = opts.configPath;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
/** The outcome of routing a single connection-config file. */
|
|
9
|
+
interface ConnectionUpgradeResult {
|
|
10
|
+
action: string;
|
|
11
|
+
changed: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Route a single connection-config `path` against the schema version (GAIA-216).
|
|
15
|
+
* `label` names the file for the report; `current`/`chain` are injectable for
|
|
16
|
+
* tests (default to the shipped registry). Never executes the config module —
|
|
17
|
+
* the decision is a regex read of the header marker.
|
|
18
|
+
*
|
|
19
|
+
* | existing marker | action |
|
|
20
|
+
* | absent (no file) | seed: render current template, stamp CURRENT |
|
|
21
|
+
* | `== CURRENT` | kept (byte-identical) |
|
|
22
|
+
* | `1..CURRENT-1` (stale) | back up `.v<old>.bak`, run chain → stamp CURRENT |
|
|
23
|
+
* | `0` (unversioned) | kept (do not clobber hand edits) |
|
|
24
|
+
* | `> CURRENT` (newer) | kept (refuse to downgrade) |
|
|
25
|
+
*/
|
|
26
|
+
export declare function runConnectionUpgrade(path: string, opts?: {
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
label?: string;
|
|
29
|
+
current?: number;
|
|
30
|
+
chain?: ConnectionConfigMigration[];
|
|
31
|
+
}): ConnectionUpgradeResult;
|
|
32
|
+
/** Run the migration. Pure w.r.t. injected `cwd`/`home`; `dryRun` suppresses writes. */
|
|
33
|
+
export declare function runUpgrade(opts?: {
|
|
34
|
+
cwd?: string;
|
|
35
|
+
home?: string;
|
|
36
|
+
dryRun?: boolean;
|
|
37
|
+
}): UpgradeReport;
|
|
38
|
+
export {};
|