@aiwg/cli 2026.8.17 → 2026.8.18
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/bin/aiwg.mjs +24 -2
- package/dist/src/a2a/agent-card.js +4 -1
- package/dist/src/a2a/client.js +148 -68
- package/dist/src/a2a/codecs.js +480 -0
- package/dist/src/a2a/events.js +226 -0
- package/dist/src/a2a/hitl-driver.js +8 -6
- package/dist/src/a2a/hitl.js +2 -1
- package/dist/src/a2a/http.js +85 -5
- package/dist/src/a2a/protocol.js +136 -0
- package/dist/src/a2a/types.js +4 -14
- package/dist/src/a2a/webhook.js +101 -4
- package/dist/src/audit/operator-decision.js +15 -1
- package/dist/src/channel/manager.mjs +89 -17
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/installation.js +79 -0
- package/dist/src/cli/handlers/refresh.js +4 -2
- package/dist/src/cli/handlers/runtime-info.js +9 -1
- package/dist/src/cli/handlers/serve.js +107 -4
- package/dist/src/cli/handlers/session.js +12 -26
- package/dist/src/cli/handlers/utilities.js +4 -0
- package/dist/src/cli/handlers/version.js +4 -0
- package/dist/src/config/user-config-dir.mjs +29 -0
- package/dist/src/config/user-config.js +4 -22
- package/dist/src/extensions/commands/definitions.js +20 -1
- package/dist/src/features/catalog.js +2 -1
- package/dist/src/flow/graph-metadata.js +56 -0
- package/dist/src/installation/manager.mjs +243 -0
- package/dist/src/serve/a2a-terminal-observer.js +28 -5
- package/dist/src/serve/dispatch-router.js +32 -4
- package/dist/src/serve/executor-registry.js +29 -0
- package/dist/src/serve/mission-conductor.js +15 -1
- package/dist/src/serve/stack-adapters.js +2 -1
- package/dist/src/serve/telemetry.js +5 -1
- package/dist/src/update/checker.mjs +16 -15
- package/dist/src/update/notifier.mjs +8 -3
- package/dist/src/update/service.mjs +49 -5
- package/package.json +1 -1
package/dist/src/a2a/webhook.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// @issue #1256
|
|
17
17
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
18
|
+
import { decodeStreamResponse, A2AEventReconciler } from './events.js';
|
|
18
19
|
/** Header name (case-insensitive). */
|
|
19
20
|
export const SIGNATURE_HEADER = 'x-aiwg-signature';
|
|
20
21
|
/** Five minutes — RFC 8941 timestamp tolerance. */
|
|
@@ -111,6 +112,7 @@ function constantTimeHexEqual(a, b) {
|
|
|
111
112
|
export class IdempotencyCache {
|
|
112
113
|
capacity;
|
|
113
114
|
seen = new Set();
|
|
115
|
+
pending = new Set();
|
|
114
116
|
order = [];
|
|
115
117
|
constructor(capacity = DEFAULT_IDEMPOTENCY_CAPACITY) {
|
|
116
118
|
this.capacity = Math.max(16, capacity);
|
|
@@ -119,6 +121,23 @@ export class IdempotencyCache {
|
|
|
119
121
|
markFresh(id) {
|
|
120
122
|
if (this.seen.has(id))
|
|
121
123
|
return false;
|
|
124
|
+
this.commit(id);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
/** Reserve an event before parsing/routing so concurrent deliveries cannot race. */
|
|
128
|
+
begin(id) {
|
|
129
|
+
if (this.seen.has(id))
|
|
130
|
+
return 'duplicate';
|
|
131
|
+
if (this.pending.has(id))
|
|
132
|
+
return 'pending';
|
|
133
|
+
this.pending.add(id);
|
|
134
|
+
return 'fresh';
|
|
135
|
+
}
|
|
136
|
+
/** Mark a successfully routed reservation as completed. */
|
|
137
|
+
commit(id) {
|
|
138
|
+
this.pending.delete(id);
|
|
139
|
+
if (this.seen.has(id))
|
|
140
|
+
return;
|
|
122
141
|
this.seen.add(id);
|
|
123
142
|
this.order.push(id);
|
|
124
143
|
while (this.order.length > this.capacity) {
|
|
@@ -126,7 +145,10 @@ export class IdempotencyCache {
|
|
|
126
145
|
if (evicted !== undefined)
|
|
127
146
|
this.seen.delete(evicted);
|
|
128
147
|
}
|
|
129
|
-
|
|
148
|
+
}
|
|
149
|
+
/** Release a failed reservation so a later retry can be processed. */
|
|
150
|
+
release(id) {
|
|
151
|
+
this.pending.delete(id);
|
|
130
152
|
}
|
|
131
153
|
size() {
|
|
132
154
|
return this.seen.size;
|
|
@@ -134,15 +156,35 @@ export class IdempotencyCache {
|
|
|
134
156
|
}
|
|
135
157
|
export class PushSecretRegistry {
|
|
136
158
|
entries = new Map();
|
|
159
|
+
reconcilers = new Map();
|
|
137
160
|
register(entry) {
|
|
161
|
+
if (entry.protocolVersion === '1.0' && !entry.taskId) {
|
|
162
|
+
throw new Error('A2A 1.0 push config registration requires taskId ownership scope');
|
|
163
|
+
}
|
|
138
164
|
this.entries.set(entry.configId, entry);
|
|
165
|
+
if (entry.taskId) {
|
|
166
|
+
this.reconcilers.set(entry.configId, new A2AEventReconciler({
|
|
167
|
+
taskId: entry.taskId,
|
|
168
|
+
...(entry.contextId ? { contextId: entry.contextId } : {}),
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
139
171
|
}
|
|
140
172
|
lookup(configId) {
|
|
141
173
|
return this.entries.get(configId) ?? null;
|
|
142
174
|
}
|
|
143
175
|
unregister(configId) {
|
|
176
|
+
this.reconcilers.delete(configId);
|
|
144
177
|
return this.entries.delete(configId);
|
|
145
178
|
}
|
|
179
|
+
reconcile(configId, event) {
|
|
180
|
+
const entry = this.entries.get(configId);
|
|
181
|
+
const eventOwner = ownerOf(event);
|
|
182
|
+
if (entry?.taskOwner && eventOwner && entry.taskOwner !== eventOwner) {
|
|
183
|
+
throw new Error(`A2A event belongs to owner ${eventOwner}, expected ${entry.taskOwner}`);
|
|
184
|
+
}
|
|
185
|
+
const reconciler = this.reconcilers.get(configId);
|
|
186
|
+
return reconciler ? reconciler.accept(event) : event;
|
|
187
|
+
}
|
|
146
188
|
/** Test/debug helper. */
|
|
147
189
|
size() {
|
|
148
190
|
return this.entries.size;
|
|
@@ -193,40 +235,83 @@ export async function handleWebhook(configId, body, signature, eventId, opts) {
|
|
|
193
235
|
// Idempotency check — duplicate event-ids are accepted with 200 but
|
|
194
236
|
// not re-routed. The executor's retry logic depends on a 2xx response
|
|
195
237
|
// to mark delivery complete; failing here would cause infinite retry.
|
|
196
|
-
const
|
|
197
|
-
|
|
238
|
+
const entryForScope = opts.registry.lookup(configId);
|
|
239
|
+
const protocolVersion = entryForScope?.protocolVersion ?? '0.3';
|
|
240
|
+
const scopedEventId = [
|
|
241
|
+
configId,
|
|
242
|
+
protocolVersion,
|
|
243
|
+
entryForScope?.taskOwner ?? '',
|
|
244
|
+
entryForScope?.taskId ?? '',
|
|
245
|
+
eventId,
|
|
246
|
+
].join('|');
|
|
247
|
+
const reservation = opts.idempotency.begin(scopedEventId);
|
|
248
|
+
if (reservation === 'duplicate') {
|
|
198
249
|
return { status: 200, body: { ok: true, deduped: true } };
|
|
199
250
|
}
|
|
251
|
+
if (reservation === 'pending') {
|
|
252
|
+
return {
|
|
253
|
+
status: 409,
|
|
254
|
+
body: errorBody('aiwg.webhook_event_in_progress', 'a concurrent delivery is still being processed'),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
200
257
|
// Route the verified payload. Errors thrown here become 500 so the
|
|
201
258
|
// executor will retry — pick the abstraction carefully on the
|
|
202
259
|
// mission-state side.
|
|
203
260
|
const entry = opts.registry.lookup(configId);
|
|
204
261
|
if (!entry) {
|
|
205
262
|
// Edge case: secret was unregistered between verify and route.
|
|
263
|
+
opts.idempotency.release(scopedEventId);
|
|
206
264
|
return {
|
|
207
265
|
status: 404,
|
|
208
266
|
body: errorBody('aiwg.webhook_secret_unknown', `configId='${configId}' no longer registered`),
|
|
209
267
|
};
|
|
210
268
|
}
|
|
269
|
+
if (protocolVersion === '1.0' && opts.contentType?.split(';')[0]?.trim().toLowerCase() !== 'application/a2a+json') {
|
|
270
|
+
opts.idempotency.release(scopedEventId);
|
|
271
|
+
return {
|
|
272
|
+
status: 415,
|
|
273
|
+
body: errorBody('aiwg.webhook_content_type_invalid', 'A2A 1.0 push requires application/a2a+json'),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
211
276
|
let parsed;
|
|
212
277
|
try {
|
|
213
278
|
parsed = JSON.parse(body.toString('utf8'));
|
|
214
279
|
}
|
|
215
280
|
catch (e) {
|
|
281
|
+
opts.idempotency.release(scopedEventId);
|
|
216
282
|
return {
|
|
217
283
|
status: 400,
|
|
218
284
|
body: errorBody('aiwg.webhook_body_not_json', e.message),
|
|
219
285
|
};
|
|
220
286
|
}
|
|
287
|
+
let event;
|
|
221
288
|
try {
|
|
222
|
-
|
|
289
|
+
event = decodeStreamResponse(protocolVersion, parsed, { eventId });
|
|
290
|
+
const accepted = opts.registry.reconcile(configId, event);
|
|
291
|
+
if (!accepted) {
|
|
292
|
+
opts.idempotency.commit(scopedEventId);
|
|
293
|
+
return { status: 200, body: { ok: true, deduped: true } };
|
|
294
|
+
}
|
|
295
|
+
event = accepted;
|
|
223
296
|
}
|
|
224
297
|
catch (e) {
|
|
298
|
+
opts.idempotency.release(scopedEventId);
|
|
299
|
+
return {
|
|
300
|
+
status: 400,
|
|
301
|
+
body: errorBody('aiwg.webhook_event_invalid', e.message),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await opts.route(entry, event);
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
opts.idempotency.release(scopedEventId);
|
|
225
309
|
return {
|
|
226
310
|
status: 500,
|
|
227
311
|
body: errorBody('aiwg.webhook_route_failed', e.message),
|
|
228
312
|
};
|
|
229
313
|
}
|
|
314
|
+
opts.idempotency.commit(scopedEventId);
|
|
230
315
|
return { status: 200, body: { ok: true } };
|
|
231
316
|
}
|
|
232
317
|
function errorBody(code, detail) {
|
|
@@ -237,4 +322,16 @@ function errorBody(code, detail) {
|
|
|
237
322
|
detail,
|
|
238
323
|
};
|
|
239
324
|
}
|
|
325
|
+
function ownerOf(event) {
|
|
326
|
+
const metadata = event.type === 'task'
|
|
327
|
+
? event.task.metadata
|
|
328
|
+
: event.type === 'message'
|
|
329
|
+
? event.message.metadata
|
|
330
|
+
: event.metadata;
|
|
331
|
+
const owner = metadata?.['task_owner']
|
|
332
|
+
?? metadata?.['taskOwner']
|
|
333
|
+
?? metadata?.['tenant_id']
|
|
334
|
+
?? metadata?.['tenantId'];
|
|
335
|
+
return typeof owner === 'string' && owner ? owner : undefined;
|
|
336
|
+
}
|
|
240
337
|
//# sourceMappingURL=webhook.js.map
|
|
@@ -22,8 +22,9 @@ export function createDecisionRecord(input, previousHash) {
|
|
|
22
22
|
const actor = redact(input.actor);
|
|
23
23
|
const correlation = redact(input.correlation);
|
|
24
24
|
const runtime = input.runtime ? redact(input.runtime) : undefined;
|
|
25
|
+
const graph = input.graph ? redact(input.graph) : undefined;
|
|
25
26
|
const reason = redact(input.reason);
|
|
26
|
-
const detected = [...actor.paths, ...correlation.paths, ...(runtime?.paths ?? []), ...reason.paths];
|
|
27
|
+
const detected = [...actor.paths, ...correlation.paths, ...(runtime?.paths ?? []), ...(graph?.paths ?? []), ...reason.paths];
|
|
27
28
|
const unsigned = {
|
|
28
29
|
schema_version: OPERATOR_DECISION_SCHEMA,
|
|
29
30
|
event_id: input.event_id ?? randomUUID(),
|
|
@@ -36,6 +37,7 @@ export function createDecisionRecord(input, previousHash) {
|
|
|
36
37
|
classification: input.classification,
|
|
37
38
|
correlation: correlation.value,
|
|
38
39
|
...(runtime ? { runtime: runtime.value } : {}),
|
|
40
|
+
...(graph ? { graph: graph.value } : {}),
|
|
39
41
|
...(input.policy_ref ? { policy_ref: input.policy_ref } : {}),
|
|
40
42
|
redacted_fields: [...new Set([...(input.redacted_fields ?? []), ...detected])].sort(),
|
|
41
43
|
previous_hash: previousHash,
|
|
@@ -74,6 +76,15 @@ export function toOpenTelemetryLog(record) {
|
|
|
74
76
|
'aiwg.provider.id': record.correlation.provider_id,
|
|
75
77
|
'aiwg.sandbox.task_id': record.correlation.sandbox_task_id,
|
|
76
78
|
'aiwg.prompt.id': record.correlation.prompt_id,
|
|
79
|
+
'aiwg.graph.id': record.graph?.graph_id,
|
|
80
|
+
'aiwg.graph.version': record.graph?.graph_version,
|
|
81
|
+
'aiwg.graph.run_id': record.graph?.run_id,
|
|
82
|
+
'aiwg.graph.node_id': record.graph?.node_id,
|
|
83
|
+
'aiwg.graph.node_run_id': record.graph?.node_run_id,
|
|
84
|
+
'aiwg.graph.edge_id': record.graph?.edge_id,
|
|
85
|
+
'aiwg.graph.route_name': record.graph?.route_name,
|
|
86
|
+
'aiwg.graph.checkpoint_id': record.graph?.checkpoint_id,
|
|
87
|
+
'aiwg.graph.replay_parent_run_id': record.graph?.replay_parent_run_id,
|
|
77
88
|
}).filter(([, value]) => value !== undefined).map(([key, value]) => ({
|
|
78
89
|
key,
|
|
79
90
|
value: { stringValue: String(value) },
|
|
@@ -135,6 +146,9 @@ function validateInput(input) {
|
|
|
135
146
|
throw new Error('a non-empty operator reason is required');
|
|
136
147
|
if (!Object.values(input.correlation).some(Boolean))
|
|
137
148
|
throw new Error('at least one correlation identifier is required');
|
|
149
|
+
if (input.graph && ![input.graph.graph_id, input.graph.graph_version, input.graph.run_id, input.graph.node_id, input.graph.node_run_id].every(value => typeof value === 'string' && value.length > 0)) {
|
|
150
|
+
throw new Error('graph decision context requires graph, run, node, and node-run identity');
|
|
151
|
+
}
|
|
138
152
|
if (input.timestamp && !Number.isFinite(Date.parse(input.timestamp)))
|
|
139
153
|
throw new Error('timestamp must be valid ISO time');
|
|
140
154
|
}
|
|
@@ -13,8 +13,17 @@ import fs from 'fs/promises';
|
|
|
13
13
|
import { readFileSync, existsSync } from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
|
-
import { execSync, spawn } from 'child_process';
|
|
16
|
+
import { execFileSync, execSync, spawn } from 'child_process';
|
|
17
17
|
import os from 'os';
|
|
18
|
+
import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
|
|
19
|
+
import {
|
|
20
|
+
assertCanonicalInstallation,
|
|
21
|
+
createInstallationIdentity,
|
|
22
|
+
inferInstallationMethod,
|
|
23
|
+
inspectInstallation,
|
|
24
|
+
loadInstallationIdentity,
|
|
25
|
+
saveInstallationIdentity,
|
|
26
|
+
} from '../installation/manager.mjs';
|
|
18
27
|
|
|
19
28
|
/**
|
|
20
29
|
* Run a command with inherited stdio, applying a wall-clock timeout so a
|
|
@@ -80,8 +89,6 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
80
89
|
const __dirname = path.dirname(__filename);
|
|
81
90
|
|
|
82
91
|
// Configuration paths
|
|
83
|
-
const CONFIG_DIR = path.join(os.homedir(), '.aiwg');
|
|
84
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'channel.json');
|
|
85
92
|
const EDGE_INSTALL_PATH = path.join(os.homedir(), '.local', 'share', 'ai-writing-guide');
|
|
86
93
|
const REPO_URL = 'https://github.com/jmagly/aiwg.git';
|
|
87
94
|
|
|
@@ -132,22 +139,65 @@ export function getPackageRoot() {
|
|
|
132
139
|
* Load channel configuration
|
|
133
140
|
* @returns {Promise<object>} Channel configuration
|
|
134
141
|
*/
|
|
135
|
-
export async function loadConfig() {
|
|
142
|
+
export async function loadConfig(options = {}) {
|
|
143
|
+
const configDir = resolveUserConfigDir(options);
|
|
144
|
+
const configFile = path.join(configDir, 'channel.json');
|
|
145
|
+
let legacy = {};
|
|
136
146
|
try {
|
|
137
|
-
const data = await fs.readFile(
|
|
138
|
-
|
|
147
|
+
const data = await fs.readFile(configFile, 'utf8');
|
|
148
|
+
legacy = JSON.parse(data);
|
|
139
149
|
} catch {
|
|
140
|
-
|
|
150
|
+
// Legacy state is optional. The canonical identity below is authoritative.
|
|
141
151
|
}
|
|
152
|
+
const actualRoot = options.actualRoot ?? getPackageRoot();
|
|
153
|
+
const identity = loadInstallationIdentity({ ...options, actualRoot, legacyConfig: legacy });
|
|
154
|
+
if (!identity) return { ...DEFAULT_CONFIG, ...legacy };
|
|
155
|
+
return {
|
|
156
|
+
...DEFAULT_CONFIG,
|
|
157
|
+
...legacy,
|
|
158
|
+
channel: identity.channel,
|
|
159
|
+
edgePath: identity.edgePath ?? legacy.edgePath ?? EDGE_INSTALL_PATH,
|
|
160
|
+
devMode: identity.runMode === 'development',
|
|
161
|
+
lastUpdateCheck: identity.lastUpdateCheck,
|
|
162
|
+
updateCheckInterval: identity.updateCheckInterval,
|
|
163
|
+
checkOnStartup: identity.checkOnStartup,
|
|
164
|
+
installation: identity,
|
|
165
|
+
};
|
|
142
166
|
}
|
|
143
167
|
|
|
144
168
|
/**
|
|
145
169
|
* Save channel configuration
|
|
146
170
|
* @param {object} config - Configuration to save
|
|
147
171
|
*/
|
|
148
|
-
export async function saveConfig(config) {
|
|
149
|
-
|
|
150
|
-
|
|
172
|
+
export async function saveConfig(config, options = {}) {
|
|
173
|
+
const configDir = resolveUserConfigDir(options);
|
|
174
|
+
const configFile = path.join(configDir, 'channel.json');
|
|
175
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
176
|
+
const { installation: _installation, ...legacyConfig } = config;
|
|
177
|
+
await fs.writeFile(configFile, `${JSON.stringify(legacyConfig, null, 2)}\n`);
|
|
178
|
+
|
|
179
|
+
const actualRoot = options.actualRoot ?? getPackageRoot();
|
|
180
|
+
const previous = loadInstallationIdentity({ ...options, actualRoot, legacyConfig: config });
|
|
181
|
+
if (previous) {
|
|
182
|
+
const development = config.devMode === true;
|
|
183
|
+
const root = development && config.edgePath ? config.edgePath : previous.root;
|
|
184
|
+
const method = development ? 'source' : previous.method;
|
|
185
|
+
saveInstallationIdentity(createInstallationIdentity({
|
|
186
|
+
...options,
|
|
187
|
+
actualRoot,
|
|
188
|
+
root,
|
|
189
|
+
method,
|
|
190
|
+
runMode: development ? 'development' : 'normal',
|
|
191
|
+
channel: config.channel ?? previous.channel,
|
|
192
|
+
edgePath: config.edgePath ?? previous.edgePath,
|
|
193
|
+
managerExecutable: development ? undefined : previous.managerExecutable,
|
|
194
|
+
updateStrategy: development ? 'source-git' : previous.updateStrategy,
|
|
195
|
+
lastUpdateCheck: config.lastUpdateCheck ?? previous.lastUpdateCheck,
|
|
196
|
+
updateCheckInterval: config.updateCheckInterval ?? previous.updateCheckInterval,
|
|
197
|
+
checkOnStartup: config.checkOnStartup ?? previous.checkOnStartup,
|
|
198
|
+
recordedAt: previous.recordedAt,
|
|
199
|
+
}), options);
|
|
200
|
+
}
|
|
151
201
|
}
|
|
152
202
|
|
|
153
203
|
/**
|
|
@@ -178,7 +228,7 @@ export async function getFrameworkRoot() {
|
|
|
178
228
|
}
|
|
179
229
|
}
|
|
180
230
|
|
|
181
|
-
return getPackageRoot();
|
|
231
|
+
return config.installation?.root ?? getPackageRoot();
|
|
182
232
|
}
|
|
183
233
|
|
|
184
234
|
/**
|
|
@@ -316,12 +366,16 @@ export async function switchToDev(devPath) {
|
|
|
316
366
|
*/
|
|
317
367
|
export async function switchToNext() {
|
|
318
368
|
const config = await loadConfig();
|
|
369
|
+
const status = assertCanonicalInstallation({ actualRoot: getPackageRoot() });
|
|
370
|
+
if (status.identity.method !== 'npm' || !status.identity.managerExecutable) {
|
|
371
|
+
throw new Error('The next channel requires a canonical npm installation with a recorded package-manager executable.');
|
|
372
|
+
}
|
|
319
373
|
|
|
320
374
|
console.log('Switching to next channel (alpha/beta/RC — latest pre-release)...');
|
|
321
375
|
console.log('');
|
|
322
376
|
|
|
323
377
|
try {
|
|
324
|
-
|
|
378
|
+
execFileSync(status.identity.managerExecutable, ['install', '--global', 'aiwg@next'], { stdio: 'inherit' });
|
|
325
379
|
} catch (error) {
|
|
326
380
|
console.error('Failed to install aiwg@next:', error.message);
|
|
327
381
|
console.error('Check that npm is available and you have write access to the global prefix.');
|
|
@@ -346,12 +400,16 @@ export async function switchToNext() {
|
|
|
346
400
|
*/
|
|
347
401
|
export async function switchToNightly() {
|
|
348
402
|
const config = await loadConfig();
|
|
403
|
+
const status = assertCanonicalInstallation({ actualRoot: getPackageRoot() });
|
|
404
|
+
if (status.identity.method !== 'npm' || !status.identity.managerExecutable) {
|
|
405
|
+
throw new Error('The nightly channel requires a canonical npm installation with a recorded package-manager executable.');
|
|
406
|
+
}
|
|
349
407
|
|
|
350
408
|
console.log('Switching to nightly channel (latest automated snapshot)...');
|
|
351
409
|
console.log('');
|
|
352
410
|
|
|
353
411
|
try {
|
|
354
|
-
|
|
412
|
+
execFileSync(status.identity.managerExecutable, ['install', '--global', 'aiwg@nightly'], { stdio: 'inherit' });
|
|
355
413
|
} catch (error) {
|
|
356
414
|
console.error('Failed to install aiwg@nightly:', error.message);
|
|
357
415
|
console.error('Check that npm is available and you have write access to the global prefix.');
|
|
@@ -376,17 +434,29 @@ export async function switchToNightly() {
|
|
|
376
434
|
export async function switchToStable() {
|
|
377
435
|
const config = await loadConfig();
|
|
378
436
|
|
|
379
|
-
console.log('Switching to stable channel
|
|
437
|
+
console.log('Switching to the stable channel...');
|
|
380
438
|
console.log('');
|
|
381
439
|
|
|
382
440
|
config.channel = 'stable';
|
|
383
441
|
config.devMode = false;
|
|
384
442
|
await saveConfig(config);
|
|
443
|
+
const actualRoot = getPackageRoot();
|
|
444
|
+
saveInstallationIdentity(createInstallationIdentity({
|
|
445
|
+
actualRoot,
|
|
446
|
+
root: actualRoot,
|
|
447
|
+
method: inferInstallationMethod(actualRoot),
|
|
448
|
+
runMode: 'normal',
|
|
449
|
+
channel: 'stable',
|
|
450
|
+
edgePath: config.edgePath,
|
|
451
|
+
lastUpdateCheck: config.lastUpdateCheck,
|
|
452
|
+
updateCheckInterval: config.updateCheckInterval,
|
|
453
|
+
checkOnStartup: config.checkOnStartup,
|
|
454
|
+
}));
|
|
385
455
|
|
|
386
456
|
console.log('Switched to stable channel.');
|
|
387
|
-
console.log('You are now using the
|
|
457
|
+
console.log('You are now using the canonical installed package.');
|
|
388
458
|
console.log('');
|
|
389
|
-
console.log('To update:
|
|
459
|
+
console.log('To update: aiwg refresh --channel latest');
|
|
390
460
|
console.log('To switch to edge: aiwg --use-main');
|
|
391
461
|
}
|
|
392
462
|
|
|
@@ -415,6 +485,7 @@ function normalizeRepoUrl(repository) {
|
|
|
415
485
|
export async function getVersionInfo() {
|
|
416
486
|
const config = await loadConfig();
|
|
417
487
|
const packageRoot = getPackageRoot();
|
|
488
|
+
const installation = inspectInstallation({ actualRoot: packageRoot, identity: config.installation });
|
|
418
489
|
|
|
419
490
|
// Read package.json version
|
|
420
491
|
const packageJsonPath = path.join(packageRoot, 'package.json');
|
|
@@ -441,6 +512,7 @@ export async function getVersionInfo() {
|
|
|
441
512
|
version,
|
|
442
513
|
channel,
|
|
443
514
|
packageRoot,
|
|
515
|
+
installation,
|
|
444
516
|
devMode: config.devMode || false,
|
|
445
517
|
// Public-facing URLs — single source of truth is package.json, so user-visible
|
|
446
518
|
// stamps/links never hardcode the internal build origin. The published package
|
|
@@ -480,7 +552,7 @@ export async function updateEdge() {
|
|
|
480
552
|
const config = await loadConfig();
|
|
481
553
|
|
|
482
554
|
if (config.channel !== 'edge') {
|
|
483
|
-
console.log('Not in edge channel. Use
|
|
555
|
+
console.log('Not in edge channel. Use `aiwg update` for the canonical installed channel.');
|
|
484
556
|
return;
|
|
485
557
|
}
|
|
486
558
|
|
|
@@ -63,12 +63,13 @@ import { costReportHandler } from './cost-report.js';
|
|
|
63
63
|
import { evidenceHandler } from './evidence.js';
|
|
64
64
|
import { artifactVerifyHandler } from './artifact-verify.js';
|
|
65
65
|
import { outputModeHandler } from './output-mode.js';
|
|
66
|
+
import { installationHandler } from './installation.js';
|
|
66
67
|
// Re-export individual handlers
|
|
67
68
|
export {
|
|
68
69
|
// Maintenance
|
|
69
70
|
helpHandler, versionHandler, authHandler, doctorHandler, contextFirewallHandler, updateHandler, refreshHandler, regenerateHandler, workspaceContextHandler,
|
|
70
71
|
// Framework management
|
|
71
|
-
useHandler, listHandler, removeHandler, promoteHandler, installHandler, packagesHandler, marketplaceHandler, initHandler, setupHandler, setupGenerateHandler, setupRunHandler, setupValidateHandler, issueHandler, issueAuditHandler, runHandler, jobHandler, costReportHandler, evidenceHandler, artifactVerifyHandler, outputModeHandler,
|
|
72
|
+
useHandler, listHandler, removeHandler, promoteHandler, installHandler, packagesHandler, marketplaceHandler, initHandler, setupHandler, setupGenerateHandler, setupRunHandler, setupValidateHandler, issueHandler, issueAuditHandler, runHandler, jobHandler, costReportHandler, evidenceHandler, artifactVerifyHandler, outputModeHandler, installationHandler,
|
|
72
73
|
// Project
|
|
73
74
|
newBundleHandler, quickrefHandler, newProjectHandler, sessionHandler, sessionsHandler,
|
|
74
75
|
// Workspace
|
|
@@ -125,6 +126,7 @@ export const allHandlers = [
|
|
|
125
126
|
contextFirewallHandler,
|
|
126
127
|
updateHandler,
|
|
127
128
|
refreshHandler,
|
|
129
|
+
installationHandler,
|
|
128
130
|
regenerateHandler,
|
|
129
131
|
workspaceContextHandler,
|
|
130
132
|
// Framework management
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getPackageRoot } from '../../channel/manager.mjs';
|
|
2
|
+
import { adoptInstallation, inspectInstallation, loadInstallationIdentity, switchInstallation, } from '../../installation/manager.mjs';
|
|
3
|
+
function valueAfter(args, flag) {
|
|
4
|
+
const index = args.indexOf(flag);
|
|
5
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
6
|
+
}
|
|
7
|
+
function display(status, json) {
|
|
8
|
+
if (json) {
|
|
9
|
+
console.log(JSON.stringify(status, null, 2));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
console.log('\nCanonical AIWG Installation');
|
|
13
|
+
console.log('===========================');
|
|
14
|
+
console.log(`State: ${status.state}`);
|
|
15
|
+
console.log(`Canonical method: ${status.identity?.method ?? '(unrecorded)'}`);
|
|
16
|
+
console.log(`Canonical root: ${status.identity?.root ?? '(unrecorded)'}`);
|
|
17
|
+
console.log(`Manager: ${status.identity?.managerExecutable ?? '(internal)'}`);
|
|
18
|
+
console.log(`Update strategy: ${status.identity?.updateStrategy ?? '(unrecorded)'}`);
|
|
19
|
+
console.log(`Run mode: ${status.identity?.runMode ?? '(unrecorded)'}`);
|
|
20
|
+
console.log(`Release channel: ${status.identity?.channel ?? '(unrecorded)'}`);
|
|
21
|
+
console.log(`Actual method: ${status.actualMethod}`);
|
|
22
|
+
console.log(`Actual root: ${status.actualRoot}`);
|
|
23
|
+
if (status.drift.length > 0) {
|
|
24
|
+
console.log('Drift:');
|
|
25
|
+
for (const item of status.drift)
|
|
26
|
+
console.log(` - ${item}`);
|
|
27
|
+
}
|
|
28
|
+
console.log('');
|
|
29
|
+
}
|
|
30
|
+
export const installationHandler = {
|
|
31
|
+
id: 'installation',
|
|
32
|
+
name: 'Installation',
|
|
33
|
+
description: 'Inspect, adopt, or deliberately switch the canonical global installation',
|
|
34
|
+
category: 'maintenance',
|
|
35
|
+
aliases: [],
|
|
36
|
+
async execute(ctx) {
|
|
37
|
+
const [action = 'show'] = ctx.args;
|
|
38
|
+
const json = ctx.args.includes('--json');
|
|
39
|
+
const actualRoot = getPackageRoot();
|
|
40
|
+
const common = {
|
|
41
|
+
actualRoot,
|
|
42
|
+
configDir: valueAfter(ctx.args, '--config-dir'),
|
|
43
|
+
managerExecutable: valueAfter(ctx.args, '--manager'),
|
|
44
|
+
channel: valueAfter(ctx.args, '--channel'),
|
|
45
|
+
};
|
|
46
|
+
if (action === 'show') {
|
|
47
|
+
const identity = loadInstallationIdentity({ ...common, createIfMissing: true });
|
|
48
|
+
display(inspectInstallation({ ...common, identity }), json);
|
|
49
|
+
return { exitCode: 0 };
|
|
50
|
+
}
|
|
51
|
+
if (action === 'adopt') {
|
|
52
|
+
const method = valueAfter(ctx.args, '--method');
|
|
53
|
+
const status = adoptInstallation({
|
|
54
|
+
...common,
|
|
55
|
+
method,
|
|
56
|
+
runMode: valueAfter(ctx.args, '--run-mode'),
|
|
57
|
+
});
|
|
58
|
+
display(status, json);
|
|
59
|
+
return { exitCode: 0 };
|
|
60
|
+
}
|
|
61
|
+
if (action === 'switch') {
|
|
62
|
+
const root = valueAfter(ctx.args, '--root');
|
|
63
|
+
const method = valueAfter(ctx.args, '--method');
|
|
64
|
+
if (!root || !method) {
|
|
65
|
+
return { exitCode: 2, message: 'Usage: aiwg installation switch --root <path> --method <npm|web|source> [--manager <absolute-path>]' };
|
|
66
|
+
}
|
|
67
|
+
const status = switchInstallation({
|
|
68
|
+
...common,
|
|
69
|
+
root,
|
|
70
|
+
method,
|
|
71
|
+
runMode: valueAfter(ctx.args, '--run-mode'),
|
|
72
|
+
});
|
|
73
|
+
display(status, json);
|
|
74
|
+
return { exitCode: 0 };
|
|
75
|
+
}
|
|
76
|
+
return { exitCode: 2, message: 'Usage: aiwg installation <show|adopt|switch> [options]' };
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=installation.js.map
|
|
@@ -242,8 +242,10 @@ export const refreshHandler = {
|
|
|
242
242
|
ui.success('Package up to date');
|
|
243
243
|
}
|
|
244
244
|
else {
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
return {
|
|
246
|
+
exitCode: updateResult.exitCode,
|
|
247
|
+
message: 'Installation update failed; refresh stopped before re-deployment. Run `aiwg installation show` for canonical-install diagnostics.',
|
|
248
|
+
};
|
|
247
249
|
}
|
|
248
250
|
}
|
|
249
251
|
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import { AiwgError, EXIT_CODES, handlerResultFromError } from '../errors.js';
|
|
13
|
+
import { getPackageRoot } from '../../channel/manager.mjs';
|
|
14
|
+
import { inspectInstallation } from '../../installation/manager.mjs';
|
|
13
15
|
function isMissingRuntimeCatalogError(error) {
|
|
14
16
|
return error instanceof Error && error.message.includes('No catalog found');
|
|
15
17
|
}
|
|
@@ -237,8 +239,9 @@ async function handleRuntimeInfo(args, cwd = process.cwd()) {
|
|
|
237
239
|
await discovery.discover();
|
|
238
240
|
summary = await discovery.getSummary();
|
|
239
241
|
}
|
|
242
|
+
const installation = inspectInstallation({ actualRoot: getPackageRoot() });
|
|
240
243
|
if (hasJson) {
|
|
241
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
244
|
+
console.log(JSON.stringify({ ...summary, installation }, null, 2));
|
|
242
245
|
}
|
|
243
246
|
else {
|
|
244
247
|
console.log(`\nRuntime Environment Summary`);
|
|
@@ -256,6 +259,11 @@ async function handleRuntimeInfo(args, cwd = process.cwd()) {
|
|
|
256
259
|
console.log(`\nTotal: ${summary.totalTools} verified tools`);
|
|
257
260
|
console.log(`\nLast Discovery: ${summary.lastDiscovery}`);
|
|
258
261
|
console.log(`Catalog: ${summary.catalogPath}`);
|
|
262
|
+
console.log(`\nAIWG Installation:`);
|
|
263
|
+
console.log(` Canonical: ${installation.identity?.method ?? 'unrecorded'} at ${installation.identity?.root ?? '(unrecorded)'}`);
|
|
264
|
+
console.log(` Actual: ${installation.actualMethod} at ${installation.actualRoot}`);
|
|
265
|
+
console.log(` Run mode: ${installation.identity?.runMode ?? '(unrecorded)'}`);
|
|
266
|
+
console.log(` State: ${installation.state}`);
|
|
259
267
|
// Scheduler backend detection
|
|
260
268
|
const { execSync } = await import('child_process');
|
|
261
269
|
let schedulerBackend = 'external trigger required (system cron/systemd/CI)';
|