@aiwg/cli 2026.8.16 → 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/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/cli.js +1 -1
- package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -0
- package/dist/src/artifacts/types.js +73 -1
- package/dist/src/audit/operator-decision.js +15 -1
- package/dist/src/channel/manager.mjs +89 -17
- package/dist/src/cli/agent-spawn.js +4 -2
- package/dist/src/cli/handlers/cockpit.js +41 -0
- package/dist/src/cli/handlers/help.js +1 -1
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/installation.js +79 -0
- package/dist/src/cli/handlers/ralph.js +2 -1
- package/dist/src/cli/handlers/refresh.js +4 -2
- package/dist/src/cli/handlers/runtime-info.js +9 -1
- package/dist/src/cli/handlers/sdlc-accelerate.js +2 -1
- package/dist/src/cli/handlers/serve.js +107 -4
- package/dist/src/cli/handlers/session.js +12 -26
- package/dist/src/cli/handlers/use.js +117 -12
- package/dist/src/cli/handlers/utilities.js +4 -0
- package/dist/src/cli/handlers/version.js +4 -0
- package/dist/src/cli/handlers/workspace.js +24 -2
- package/dist/src/cli/services/deployment-verification.js +9 -2
- package/dist/src/cockpit/doctor.js +257 -0
- package/dist/src/config/aiwg-config.js +36 -3
- 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/providers/transformation-receipt-integration.js +130 -3
- package/dist/src/security/artifact-verifier.js +7 -1
- 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/pty-bridge.js +6 -11
- package/dist/src/serve/stack-adapters.js +2 -1
- package/dist/src/serve/telemetry.js +5 -1
- package/dist/src/skills/run.js +15 -6
- 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 +2 -1
|
@@ -46,6 +46,9 @@ export class MissionConductor {
|
|
|
46
46
|
* crash-resilient resume path; their prior results are carried forward.
|
|
47
47
|
*/
|
|
48
48
|
async conduct(plan, pool, resumeFrom) {
|
|
49
|
+
if (plan.cycles.some((cycle) => cycle.graph) && !plan.graph) {
|
|
50
|
+
throw new Error('Graph-projected worker cycles require MissionPlan.graph identity.');
|
|
51
|
+
}
|
|
49
52
|
const carried = new Map();
|
|
50
53
|
if (resumeFrom) {
|
|
51
54
|
for (const c of resumeFrom.cycles) {
|
|
@@ -62,10 +65,17 @@ export class MissionConductor {
|
|
|
62
65
|
totalCost: 0,
|
|
63
66
|
checkpoint: { completed: [], pending: [], failed: [] },
|
|
64
67
|
runtimesUsed: [],
|
|
68
|
+
...(plan.graph ? { graph: structuredClone(plan.graph) } : {}),
|
|
65
69
|
};
|
|
66
70
|
ledger.activityLog.push(`mission ${plan.missionId} start — goal: ${plan.goal} — ${plan.cycles.length} cycle(s)` +
|
|
67
71
|
(resumeFrom ? ` (resume: ${carried.size} carried)` : ''));
|
|
68
72
|
for (const cycle of plan.cycles) {
|
|
73
|
+
const graph = plan.graph && cycle.graph ? {
|
|
74
|
+
...structuredClone(plan.graph),
|
|
75
|
+
...structuredClone(cycle.graph),
|
|
76
|
+
schemaVersion: 'graph.flow.aiwg.io/v1',
|
|
77
|
+
nodeRunId: cycle.graph.nodeRunId ?? `${plan.graph.runId}:${cycle.graph.nodeId}`,
|
|
78
|
+
} : undefined;
|
|
69
79
|
// Resume: carry a previously-completed cycle forward, identical bookkeeping.
|
|
70
80
|
const prior = carried.get(cycle.id);
|
|
71
81
|
if (prior) {
|
|
@@ -86,6 +96,7 @@ export class MissionConductor {
|
|
|
86
96
|
routed: false,
|
|
87
97
|
reason: `no stack adapter registered for runtime '${cycle.runtime}'`,
|
|
88
98
|
cost: 0,
|
|
99
|
+
...(graph ? { graph } : {}),
|
|
89
100
|
};
|
|
90
101
|
ledger.cycles.push(result);
|
|
91
102
|
ledger.checkpoint.failed.push(cycle.id);
|
|
@@ -104,6 +115,7 @@ export class MissionConductor {
|
|
|
104
115
|
routed: false,
|
|
105
116
|
reason: `no connected executor advertises ${filter.capabilities.join(', ')}`,
|
|
106
117
|
cost: 0,
|
|
118
|
+
...(graph ? { graph } : {}),
|
|
107
119
|
};
|
|
108
120
|
ledger.cycles.push(result);
|
|
109
121
|
ledger.checkpoint.failed.push(cycle.id);
|
|
@@ -111,7 +123,7 @@ export class MissionConductor {
|
|
|
111
123
|
continue;
|
|
112
124
|
}
|
|
113
125
|
const executor = routing.selected.executor;
|
|
114
|
-
const invocation = adapter.invoke(cycle.prompt);
|
|
126
|
+
const invocation = adapter.invoke(cycle.prompt, graph);
|
|
115
127
|
ledger.activityLog.push(`cycle ${cycle.id} → executor ${executor.name} (${executor.executorId}) on ${cycle.runtime}: ${invocation.describe}`);
|
|
116
128
|
let output;
|
|
117
129
|
let cost = 0;
|
|
@@ -129,6 +141,7 @@ export class MissionConductor {
|
|
|
129
141
|
routed: true,
|
|
130
142
|
reason: `worker error: ${err instanceof Error ? err.message : String(err)}`,
|
|
131
143
|
cost: 0,
|
|
144
|
+
...(graph ? { graph } : {}),
|
|
132
145
|
};
|
|
133
146
|
ledger.cycles.push(result);
|
|
134
147
|
ledger.checkpoint.failed.push(cycle.id);
|
|
@@ -144,6 +157,7 @@ export class MissionConductor {
|
|
|
144
157
|
reason: routing.selected.matchReason,
|
|
145
158
|
output,
|
|
146
159
|
cost,
|
|
160
|
+
...(graph ? { graph } : {}),
|
|
147
161
|
};
|
|
148
162
|
ledger.cycles.push(result);
|
|
149
163
|
ledger.checkpoint.completed.push(cycle.id);
|
|
@@ -695,19 +695,14 @@ export async function handlePtyConnection(sessionId, ws, command = 'aiwg', cmdAr
|
|
|
695
695
|
}
|
|
696
696
|
}
|
|
697
697
|
else if (!session.exited) {
|
|
698
|
-
// Reconnect to existing session
|
|
698
|
+
// Reconnect to an existing session by replaying the complete retained
|
|
699
|
+
// buffer. Do not trim at the last full-screen erase: terminal erase
|
|
700
|
+
// sequences repaint the current viewport but xterm still needs the bytes
|
|
701
|
+
// that preceded them to reconstruct scrollback when a user switches away
|
|
702
|
+
// from a session and later returns (#2146).
|
|
699
703
|
registry.addClient(sessionId, clientId, ws);
|
|
700
704
|
if (session.outputBuffer) {
|
|
701
|
-
|
|
702
|
-
// screen-init sequences (cursor moves, status-bar paint) from before the
|
|
703
|
-
// erase don't render as literal garbage in a fresh xterm.js context.
|
|
704
|
-
// Everything before \x1b[2J would be cleared by the erase anyway;
|
|
705
|
-
// everything after is the session content tmux redrew (MOTD, history, etc).
|
|
706
|
-
// If no erase is found, replay the whole buffer unchanged.
|
|
707
|
-
const ERASE = '\x1b[2J';
|
|
708
|
-
const lastErase = session.outputBuffer.lastIndexOf(ERASE);
|
|
709
|
-
const replay = lastErase !== -1 ? session.outputBuffer.slice(lastErase) : session.outputBuffer;
|
|
710
|
-
ws.send(JSON.stringify({ type: 'data', payload: replay }));
|
|
705
|
+
ws.send(JSON.stringify({ type: 'data', payload: session.outputBuffer }));
|
|
711
706
|
}
|
|
712
707
|
}
|
|
713
708
|
else {
|
|
@@ -23,7 +23,7 @@ function makeAdapter(runtime, primitive) {
|
|
|
23
23
|
runtime,
|
|
24
24
|
runtimeCapability,
|
|
25
25
|
primitive,
|
|
26
|
-
invoke(prompt) {
|
|
26
|
+
invoke(prompt, graph) {
|
|
27
27
|
// prompt is carried by the dispatch payload; the descriptor records the
|
|
28
28
|
// mechanism so the conductor's ledger is identical-shape across stacks.
|
|
29
29
|
const trimmed = prompt.length > 60 ? `${prompt.slice(0, 57)}...` : prompt;
|
|
@@ -31,6 +31,7 @@ function makeAdapter(runtime, primitive) {
|
|
|
31
31
|
runtimeCapability,
|
|
32
32
|
primitive,
|
|
33
33
|
describe: `dispatch worker to ${runtime} executor via ${primitive} (${trimmed})`,
|
|
34
|
+
...(graph ? { graph: structuredClone(graph) } : {}),
|
|
34
35
|
};
|
|
35
36
|
},
|
|
36
37
|
};
|
|
@@ -130,7 +130,10 @@ export const telemetryStore = new TelemetryStore();
|
|
|
130
130
|
// Event Factory
|
|
131
131
|
// ============================================================
|
|
132
132
|
let eventCounter = 0;
|
|
133
|
-
export function createEvent(type, sessionId, payload, missionId) {
|
|
133
|
+
export function createEvent(type, sessionId, payload, missionId, graph) {
|
|
134
|
+
if (type.startsWith('graph.') && !graph) {
|
|
135
|
+
throw new Error(`Telemetry event '${type}' requires graph execution metadata.`);
|
|
136
|
+
}
|
|
134
137
|
return {
|
|
135
138
|
id: `evt-${Date.now()}-${++eventCounter}`,
|
|
136
139
|
sessionId,
|
|
@@ -138,6 +141,7 @@ export function createEvent(type, sessionId, payload, missionId) {
|
|
|
138
141
|
timestamp: new Date().toISOString(),
|
|
139
142
|
type,
|
|
140
143
|
payload,
|
|
144
|
+
...(graph ? { graph: structuredClone(graph) } : {}),
|
|
141
145
|
};
|
|
142
146
|
}
|
|
143
147
|
//# sourceMappingURL=telemetry.js.map
|
package/dist/src/skills/run.js
CHANGED
|
@@ -25,6 +25,8 @@ import { spawn } from 'node:child_process';
|
|
|
25
25
|
import { promises as fs } from 'node:fs';
|
|
26
26
|
import * as path from 'node:path';
|
|
27
27
|
import { resolveRuntime, supportedRuntimes } from './runtime.js';
|
|
28
|
+
import { recordTypeForEntry, stableRecordId } from '../artifacts/browser-export.js';
|
|
29
|
+
import { loadFortemiCoreMetadataEntries } from '../artifacts/fortemi-core-query-adapter.js';
|
|
28
30
|
/**
|
|
29
31
|
* Resolve the AIWG installation root. Prefers `$AIWG_ROOT` env, falls
|
|
30
32
|
* back to the channel manager's framework-root resolver.
|
|
@@ -53,22 +55,29 @@ async function findSkillEntry(cwd, name) {
|
|
|
53
55
|
const reader = await import('../artifacts/index-reader.js');
|
|
54
56
|
const entries = [];
|
|
55
57
|
for (const g of ['framework', 'project', 'codebase']) {
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
58
|
-
entries.push(...
|
|
58
|
+
const canonical = loadFortemiCoreMetadataEntries(cwd, g);
|
|
59
|
+
if (canonical.entries.length > 0) {
|
|
60
|
+
entries.push(...canonical.entries);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const idx = reader.loadGraphIndexFile(cwd, 'metadata.json', g);
|
|
64
|
+
if (idx)
|
|
65
|
+
entries.push(...Object.values(idx.entries));
|
|
66
|
+
}
|
|
59
67
|
}
|
|
60
68
|
if (entries.length === 0) {
|
|
61
69
|
const legacy = reader.loadMetadataIndex(cwd);
|
|
62
70
|
if (legacy)
|
|
63
71
|
entries.push(...Object.values(legacy.entries));
|
|
64
72
|
}
|
|
65
|
-
const skills = entries.filter(e => e.type === 'skill');
|
|
73
|
+
const skills = entries.filter(e => e.type === 'skill' || e.type === 'aiwg.skill');
|
|
66
74
|
const needle = name.trim();
|
|
67
75
|
// Basename match — skills are conventionally `<dir>/SKILL.md`
|
|
68
76
|
const matches = skills.filter(e => {
|
|
69
77
|
const dir = path.basename(path.dirname(e.path));
|
|
70
78
|
const stem = path.basename(e.path).replace(/\.[^.]+$/, '');
|
|
71
|
-
|
|
79
|
+
const id = stableRecordId(recordTypeForEntry({ ...e, type: 'skill' }, 'v2'), e.path);
|
|
80
|
+
return id === needle || e.name === needle || dir === needle || stem === needle || e.path === needle;
|
|
72
81
|
});
|
|
73
82
|
if (matches.length === 0)
|
|
74
83
|
return null;
|
|
@@ -248,7 +257,7 @@ export async function main(args, env) {
|
|
|
248
257
|
});
|
|
249
258
|
}
|
|
250
259
|
function printUsage() {
|
|
251
|
-
console.log('Usage: aiwg run skill <name> [--cwd <path>] [-- <args...>]');
|
|
260
|
+
console.log('Usage: aiwg run skill <stable-id-or-name> [--cwd <path>] [-- <args...>]');
|
|
252
261
|
console.log('');
|
|
253
262
|
console.log('Examples:');
|
|
254
263
|
console.log(' aiwg run skill voice-apply -- --voice technical-authority --input draft.md');
|
|
@@ -15,6 +15,7 @@ import https from 'https';
|
|
|
15
15
|
import { execSync } from 'child_process';
|
|
16
16
|
import { createInterface } from 'readline';
|
|
17
17
|
import { loadConfig, saveConfig, getChannel, getPackageRoot } from '../channel/manager.mjs';
|
|
18
|
+
import { updateInstallation } from './service.mjs';
|
|
18
19
|
|
|
19
20
|
const NPM_REGISTRY = 'https://registry.npmjs.org/aiwg';
|
|
20
21
|
|
|
@@ -212,13 +213,13 @@ async function checkStableUpdates(config) {
|
|
|
212
213
|
console.log('');
|
|
213
214
|
console.log('Updating aiwg...');
|
|
214
215
|
try {
|
|
215
|
-
|
|
216
|
+
await updateInstallation({ config, channel: 'stable' });
|
|
216
217
|
console.log('Update complete! Please restart your terminal.');
|
|
217
218
|
} catch (error) {
|
|
218
|
-
console.error('Update failed. Run manually:
|
|
219
|
+
console.error('Update failed. Run manually: aiwg update');
|
|
219
220
|
}
|
|
220
221
|
} else {
|
|
221
|
-
console.log('Update skipped. Run `
|
|
222
|
+
console.log('Update skipped. Run `aiwg update` when ready.');
|
|
222
223
|
}
|
|
223
224
|
console.log('');
|
|
224
225
|
}
|
|
@@ -251,13 +252,13 @@ async function checkNextUpdates(config) {
|
|
|
251
252
|
console.log('');
|
|
252
253
|
console.log('Updating aiwg@next...');
|
|
253
254
|
try {
|
|
254
|
-
|
|
255
|
+
await updateInstallation({ config, channel: 'next' });
|
|
255
256
|
console.log('Update complete! Please restart your terminal.');
|
|
256
257
|
} catch {
|
|
257
|
-
console.error('Update failed. Run manually:
|
|
258
|
+
console.error('Update failed. Run manually: aiwg update');
|
|
258
259
|
}
|
|
259
260
|
} else {
|
|
260
|
-
console.log('Update skipped. Run `
|
|
261
|
+
console.log('Update skipped. Run `aiwg update` when ready.');
|
|
261
262
|
}
|
|
262
263
|
console.log('');
|
|
263
264
|
}
|
|
@@ -290,13 +291,13 @@ async function checkNightlyUpdates(config) {
|
|
|
290
291
|
console.log('');
|
|
291
292
|
console.log('Updating aiwg@nightly...');
|
|
292
293
|
try {
|
|
293
|
-
|
|
294
|
+
await updateInstallation({ config, channel: 'nightly' });
|
|
294
295
|
console.log('Update complete! Please restart your terminal.');
|
|
295
296
|
} catch {
|
|
296
|
-
console.error('Update failed. Run manually:
|
|
297
|
+
console.error('Update failed. Run manually: aiwg update');
|
|
297
298
|
}
|
|
298
299
|
} else {
|
|
299
|
-
console.log('Update skipped. Run `
|
|
300
|
+
console.log('Update skipped. Run `aiwg update` when ready.');
|
|
300
301
|
}
|
|
301
302
|
console.log('');
|
|
302
303
|
}
|
|
@@ -348,13 +349,13 @@ export async function forceUpdateCheck() {
|
|
|
348
349
|
console.log('Checking for updates on next channel...');
|
|
349
350
|
const latestVersion = await fetchNpmDistTag('next');
|
|
350
351
|
if (!latestVersion) {
|
|
351
|
-
console.log('Could not check npm registry. Try:
|
|
352
|
+
console.log('Could not check npm registry. Try: aiwg update');
|
|
352
353
|
return;
|
|
353
354
|
}
|
|
354
355
|
if (currentVersion !== latestVersion) {
|
|
355
356
|
console.log(`Update available: ${currentVersion} → ${latestVersion}`);
|
|
356
357
|
console.log('');
|
|
357
|
-
console.log('Run:
|
|
358
|
+
console.log('Run: aiwg update');
|
|
358
359
|
} else {
|
|
359
360
|
console.log(`You are on the latest next release: ${currentVersion}`);
|
|
360
361
|
}
|
|
@@ -362,13 +363,13 @@ export async function forceUpdateCheck() {
|
|
|
362
363
|
console.log('Checking for updates on nightly channel...');
|
|
363
364
|
const latestVersion = await fetchNpmDistTag('nightly');
|
|
364
365
|
if (!latestVersion) {
|
|
365
|
-
console.log('Could not check npm registry. Try:
|
|
366
|
+
console.log('Could not check npm registry. Try: aiwg update');
|
|
366
367
|
return;
|
|
367
368
|
}
|
|
368
369
|
if (currentVersion !== latestVersion) {
|
|
369
370
|
console.log(`Update available: ${currentVersion} → ${latestVersion}`);
|
|
370
371
|
console.log('');
|
|
371
|
-
console.log('Run:
|
|
372
|
+
console.log('Run: aiwg update');
|
|
372
373
|
} else {
|
|
373
374
|
console.log(`You are on the latest nightly snapshot: ${currentVersion}`);
|
|
374
375
|
}
|
|
@@ -381,14 +382,14 @@ export async function forceUpdateCheck() {
|
|
|
381
382
|
const latestVersion = await fetchLatestNpmVersion();
|
|
382
383
|
|
|
383
384
|
if (!latestVersion) {
|
|
384
|
-
console.log('Could not check npm registry. Try:
|
|
385
|
+
console.log('Could not check npm registry. Try: aiwg update');
|
|
385
386
|
return;
|
|
386
387
|
}
|
|
387
388
|
|
|
388
389
|
if (isNewerVersion(currentVersion, latestVersion)) {
|
|
389
390
|
console.log(`Update available: ${currentVersion} → ${latestVersion}`);
|
|
390
391
|
console.log('');
|
|
391
|
-
console.log('Run:
|
|
392
|
+
console.log('Run: aiwg update');
|
|
392
393
|
} else {
|
|
393
394
|
console.log(`You are on the latest version: ${currentVersion}`);
|
|
394
395
|
}
|
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
|
|
28
28
|
import https from 'https';
|
|
29
29
|
import path from 'path';
|
|
30
|
-
import os from 'os';
|
|
31
30
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
32
31
|
import { spawn } from 'child_process';
|
|
33
32
|
import { fileURLToPath } from 'url';
|
|
33
|
+
import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
|
|
34
|
+
import { loadInstallationIdentity } from '../installation/manager.mjs';
|
|
34
35
|
|
|
35
|
-
const CACHE_DIR =
|
|
36
|
+
const CACHE_DIR = resolveUserConfigDir();
|
|
36
37
|
const CACHE_FILE = path.join(CACHE_DIR, 'update-notifier.json');
|
|
37
38
|
|
|
38
39
|
/**
|
|
@@ -191,11 +192,13 @@ async function runCheck(packageRoot) {
|
|
|
191
192
|
*/
|
|
192
193
|
export function scheduleBackgroundCheck(packageRoot) {
|
|
193
194
|
if (isDisabled()) return;
|
|
195
|
+
const installation = loadInstallationIdentity({ actualRoot: packageRoot, createIfMissing: false });
|
|
196
|
+
if (installation?.checkOnStartup === false) return;
|
|
194
197
|
|
|
195
198
|
const cache = readCache();
|
|
196
199
|
if (cacheMatchesPackage(cache, packageRoot) && cache?.lastCheckAt) {
|
|
197
200
|
const age = Date.now() - new Date(cache.lastCheckAt).getTime();
|
|
198
|
-
if (age < CHECK_INTERVAL_MS) return; // recent enough — skip
|
|
201
|
+
if (age < (installation?.updateCheckInterval ?? CHECK_INTERVAL_MS)) return; // recent enough — skip
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
try {
|
|
@@ -225,6 +228,8 @@ export function scheduleBackgroundCheck(packageRoot) {
|
|
|
225
228
|
*/
|
|
226
229
|
export function maybePrintNotice(packageRoot) {
|
|
227
230
|
if (isDisabled()) return;
|
|
231
|
+
const installation = loadInstallationIdentity({ actualRoot: packageRoot, createIfMissing: false });
|
|
232
|
+
if (installation?.checkOnStartup === false) return;
|
|
228
233
|
const cache = readCache();
|
|
229
234
|
if (!cacheMatchesPackage(cache, packageRoot)) return;
|
|
230
235
|
if (!cache?.hasUpdate || !cache.current || !cache.latest) return;
|
|
@@ -10,6 +10,12 @@ import { execFileSync } from 'node:child_process';
|
|
|
10
10
|
import { existsSync, readFileSync } from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { getPackageRoot, loadConfig } from '../channel/manager.mjs';
|
|
13
|
+
import {
|
|
14
|
+
assertCanonicalInstallation,
|
|
15
|
+
createInstallationIdentity,
|
|
16
|
+
loadInstallationIdentity,
|
|
17
|
+
saveInstallationIdentity,
|
|
18
|
+
} from '../installation/manager.mjs';
|
|
13
19
|
|
|
14
20
|
const VALID_MODES = new Set(['npm', 'web', 'source']);
|
|
15
21
|
|
|
@@ -24,14 +30,42 @@ function readPackageName(packageRoot) {
|
|
|
24
30
|
export async function detectInstallMode(options = {}) {
|
|
25
31
|
const packageRoot = options.packageRoot ?? getPackageRoot();
|
|
26
32
|
const override = options.env?.AIWG_INSTALL_MODE ?? process.env.AIWG_INSTALL_MODE;
|
|
33
|
+
const config = options.config ?? await loadConfig(options);
|
|
34
|
+
let identity = options.identity ?? config.installation ?? null;
|
|
35
|
+
let identityPersistent = Boolean(config.installation) || options.identityPersistent === true;
|
|
36
|
+
if (!identity && options.config) {
|
|
37
|
+
const method = override ?? (readPackageName(packageRoot) === '@aiwg/cli'
|
|
38
|
+
? 'web'
|
|
39
|
+
: (config.devMode || config.channel === 'edge' || existsSync(path.join(packageRoot, '.git'))) ? 'source' : 'npm');
|
|
40
|
+
identity = createInstallationIdentity({
|
|
41
|
+
...options,
|
|
42
|
+
actualRoot: packageRoot,
|
|
43
|
+
method,
|
|
44
|
+
channel: config.channel,
|
|
45
|
+
runMode: config.devMode ? 'development' : undefined,
|
|
46
|
+
});
|
|
47
|
+
} else if (!identity) {
|
|
48
|
+
identity = loadInstallationIdentity({ ...options, actualRoot: packageRoot });
|
|
49
|
+
identityPersistent = Boolean(identity);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (override && !VALID_MODES.has(override)) {
|
|
53
|
+
throw new Error(`AIWG_INSTALL_MODE must be one of: ${[...VALID_MODES].join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
if (override && identity && override !== identity.method) {
|
|
56
|
+
throw new Error(`AIWG_INSTALL_MODE=${override} conflicts with the canonical installation method ${identity.method}. Use \`aiwg installation switch\` instead.`);
|
|
57
|
+
}
|
|
58
|
+
if (identity) {
|
|
59
|
+
const status = assertCanonicalInstallation({ ...options, actualRoot: packageRoot, identity });
|
|
60
|
+
return { mode: identity.method, packageRoot, packageName: readPackageName(packageRoot), identity, identityPersistent, installation: status };
|
|
61
|
+
}
|
|
62
|
+
|
|
27
63
|
if (override) {
|
|
28
64
|
if (!VALID_MODES.has(override)) {
|
|
29
65
|
throw new Error(`AIWG_INSTALL_MODE must be one of: ${[...VALID_MODES].join(', ')}`);
|
|
30
66
|
}
|
|
31
67
|
return { mode: override, packageRoot, packageName: readPackageName(packageRoot) };
|
|
32
68
|
}
|
|
33
|
-
|
|
34
|
-
const config = options.config ?? await loadConfig();
|
|
35
69
|
const packageName = readPackageName(packageRoot);
|
|
36
70
|
if (packageName === '@aiwg/cli') return { mode: 'web', packageRoot, packageName };
|
|
37
71
|
if (config.devMode || config.channel === 'edge' || existsSync(path.join(packageRoot, '.git'))) {
|
|
@@ -56,7 +90,7 @@ function npmTag(channel) {
|
|
|
56
90
|
export async function updateInstallation(options = {}) {
|
|
57
91
|
const config = options.config ?? await loadConfig();
|
|
58
92
|
const detected = await detectInstallMode({ ...options, config });
|
|
59
|
-
const channel = options.channel ?? config.channel ?? 'stable';
|
|
93
|
+
const channel = options.channel ?? detected.identity?.channel ?? config.channel ?? 'stable';
|
|
60
94
|
const dryRun = options.dryRun === true;
|
|
61
95
|
const offline = options.offline === true;
|
|
62
96
|
|
|
@@ -84,6 +118,9 @@ export async function updateInstallation(options = {}) {
|
|
|
84
118
|
return resolveWebRelease({ selector });
|
|
85
119
|
});
|
|
86
120
|
const release = await refreshWebResources(channel);
|
|
121
|
+
if (detected.identity && detected.identityPersistent && options.persistIdentity !== false) {
|
|
122
|
+
saveInstallationIdentity({ ...detected.identity, channel }, options);
|
|
123
|
+
}
|
|
87
124
|
return {
|
|
88
125
|
...detected,
|
|
89
126
|
channel,
|
|
@@ -106,16 +143,23 @@ export async function updateInstallation(options = {}) {
|
|
|
106
143
|
|
|
107
144
|
const tag = npmTag(channel);
|
|
108
145
|
const command = ['install', '--global', `aiwg@${tag}`];
|
|
146
|
+
const managerExecutable = detected.identity?.managerExecutable;
|
|
147
|
+
if (!managerExecutable) {
|
|
148
|
+
throw new Error('Canonical npm installation has no package-manager executable. Run `aiwg installation adopt --manager <absolute-path-to-npm>`.');
|
|
149
|
+
}
|
|
109
150
|
if (!dryRun) {
|
|
110
151
|
const execute = options.execute ?? ((file, args) => execFileSync(file, args, { stdio: 'inherit' }));
|
|
111
|
-
execute(
|
|
152
|
+
execute(managerExecutable, command);
|
|
153
|
+
if (detected.identity && detected.identityPersistent && options.persistIdentity !== false) {
|
|
154
|
+
saveInstallationIdentity({ ...detected.identity, channel }, options);
|
|
155
|
+
}
|
|
112
156
|
}
|
|
113
157
|
return {
|
|
114
158
|
...detected,
|
|
115
159
|
channel,
|
|
116
160
|
status: dryRun ? 'dry-run' : 'updated',
|
|
117
161
|
changed: !dryRun,
|
|
118
|
-
command:
|
|
162
|
+
command: `${managerExecutable} ${command.join(' ')}`,
|
|
119
163
|
message: dryRun
|
|
120
164
|
? `Would update the full AIWG npm distribution on the '${tag}' channel.`
|
|
121
165
|
: `Updated the full AIWG npm distribution on the '${tag}' channel.`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwg/cli",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.18",
|
|
4
4
|
"description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@fortemi/core": "2026.7.15",
|
|
69
69
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
70
|
+
"ajv": "^8.20.0",
|
|
70
71
|
"chalk": "^4.1.2",
|
|
71
72
|
"chokidar": "^4.0.3",
|
|
72
73
|
"commander": "^12.1.0",
|