@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.
Files changed (37) hide show
  1. package/bin/aiwg.mjs +24 -2
  2. package/dist/src/a2a/agent-card.js +4 -1
  3. package/dist/src/a2a/client.js +148 -68
  4. package/dist/src/a2a/codecs.js +480 -0
  5. package/dist/src/a2a/events.js +226 -0
  6. package/dist/src/a2a/hitl-driver.js +8 -6
  7. package/dist/src/a2a/hitl.js +2 -1
  8. package/dist/src/a2a/http.js +85 -5
  9. package/dist/src/a2a/protocol.js +136 -0
  10. package/dist/src/a2a/types.js +4 -14
  11. package/dist/src/a2a/webhook.js +101 -4
  12. package/dist/src/audit/operator-decision.js +15 -1
  13. package/dist/src/channel/manager.mjs +89 -17
  14. package/dist/src/cli/handlers/index.js +3 -1
  15. package/dist/src/cli/handlers/installation.js +79 -0
  16. package/dist/src/cli/handlers/refresh.js +4 -2
  17. package/dist/src/cli/handlers/runtime-info.js +9 -1
  18. package/dist/src/cli/handlers/serve.js +107 -4
  19. package/dist/src/cli/handlers/session.js +12 -26
  20. package/dist/src/cli/handlers/utilities.js +4 -0
  21. package/dist/src/cli/handlers/version.js +4 -0
  22. package/dist/src/config/user-config-dir.mjs +29 -0
  23. package/dist/src/config/user-config.js +4 -22
  24. package/dist/src/extensions/commands/definitions.js +20 -1
  25. package/dist/src/features/catalog.js +2 -1
  26. package/dist/src/flow/graph-metadata.js +56 -0
  27. package/dist/src/installation/manager.mjs +243 -0
  28. package/dist/src/serve/a2a-terminal-observer.js +28 -5
  29. package/dist/src/serve/dispatch-router.js +32 -4
  30. package/dist/src/serve/executor-registry.js +29 -0
  31. package/dist/src/serve/mission-conductor.js +15 -1
  32. package/dist/src/serve/stack-adapters.js +2 -1
  33. package/dist/src/serve/telemetry.js +5 -1
  34. package/dist/src/update/checker.mjs +16 -15
  35. package/dist/src/update/notifier.mjs +8 -3
  36. package/dist/src/update/service.mjs +49 -5
  37. package/package.json +1 -1
@@ -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
- return true;
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 fresh = opts.idempotency.markFresh(eventId);
197
- if (!fresh) {
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
- await opts.route(entry, parsed);
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(CONFIG_FILE, 'utf8');
138
- return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
147
+ const data = await fs.readFile(configFile, 'utf8');
148
+ legacy = JSON.parse(data);
139
149
  } catch {
140
- return { ...DEFAULT_CONFIG };
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
- await fs.mkdir(CONFIG_DIR, { recursive: true });
150
- await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
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
- execSync('npm install -g aiwg@next', { stdio: 'inherit' });
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
- execSync('npm install -g aiwg@nightly', { stdio: 'inherit' });
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 (npm package)...');
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 npm-installed package.');
457
+ console.log('You are now using the canonical installed package.');
388
458
  console.log('');
389
- console.log('To update: npm install -g aiwg@latest');
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 npm install -g aiwg@latest for stable channel.');
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
- if (!quiet)
246
- ui.warn('Update check returned non-zero (may already be current)');
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)';