@guidobuilds/forge-ai 0.7.0 → 0.9.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/CHANGELOG.md +84 -0
- package/README.md +44 -57
- package/artifacts/forge/forge.md +7 -4
- package/artifacts/forge-adversary/forge-adversary.md +3 -0
- package/artifacts/forge-worker/forge-worker.md +4 -5
- package/artifacts/forge-worker-leaf/forge-worker-leaf.md +3 -0
- package/artifacts/using-forge/using-forge.md +2 -2
- package/bin/forge-ai.mjs +1 -1
- package/dist/src/adapters/claude-known.js +12 -2
- package/dist/src/adapters/codex-known.js +24 -0
- package/dist/src/adapters/codex.js +6 -1
- package/dist/src/adapters/grok-known.js +4 -0
- package/dist/src/adapters/grok.js +3 -0
- package/dist/src/adapters/opencode-known.js +15 -0
- package/dist/src/adapters/opencode.js +20 -4
- package/dist/src/adapters/shared.js +15 -1
- package/dist/src/cli.js +555 -49
- package/dist/src/compose.js +12 -0
- package/dist/src/dispatch-snippets.js +56 -0
- package/dist/src/executable-resolution.js +49 -0
- package/dist/src/index.js +0 -1
- package/dist/src/manifest.js +172 -14
- package/dist/src/model-discovery.js +200 -0
- package/dist/src/model-preferences.js +41 -0
- package/dist/src/opencode-discovery.js +5 -0
- package/dist/src/paths.js +55 -3
- package/dist/src/platform-capabilities.js +17 -0
- package/dist/src/processor.js +66 -16
- package/dist/src/self-update.js +50 -4
- package/dist/src/writer.js +10 -12
- package/package.json +2 -2
- package/dist/src/adapters/claude-plugin.js +0 -43
- package/dist/src/adapters/codex-plugin.js +0 -98
- package/dist/src/build-plugin.js +0 -69
package/dist/src/cli.js
CHANGED
|
@@ -5,15 +5,22 @@ import { readFileSync } from 'node:fs';
|
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { rm } from 'node:fs/promises';
|
|
9
|
+
import { knownClaudeModels } from './adapters/claude-known.js';
|
|
10
|
+
import { knownCodexModels } from './adapters/codex-known.js';
|
|
11
|
+
import { knownGrokModels } from './adapters/grok-known.js';
|
|
8
12
|
import { formatDiagnostic, hasErrors } from './diagnostics.js';
|
|
9
|
-
import { buildManifest, classifyPruneEntries, loadManifest, pruneEntries, resolveBackupPath, resolveBackupRoot, resolveManifestLocation, saveManifest, staleEntries } from './manifest.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
+
import { buildManifest, classifyPruneEntries, detectLegacyStateDrift, listInstalls, loadManifest, pruneEntries, resolveBackupPath, resolveBackupRoot, resolveManifestLocation, saveManifest, stateRoot, staleEntries } from './manifest.js';
|
|
14
|
+
import { getModelPreference, loadModelPreferences, modelPreferencesPath, saveModelPreferences, setModelPreference } from './model-preferences.js';
|
|
15
|
+
import { discoverModels, mergeLiveWithCurated } from './model-discovery.js';
|
|
16
|
+
import { allowedInstallRoots } from './paths.js';
|
|
17
|
+
import { supportsModel } from './platform-capabilities.js';
|
|
18
|
+
import { buildWritePlan, discoverArtifacts, parsePlatform, parseScope, resolvePlatforms } from './processor.js';
|
|
19
|
+
import { isValidVersionSpec, normalizeVersionSpec, runSelfUpdate } from './self-update.js';
|
|
13
20
|
import { checkLatestVersion, formatVersionNotice } from './version-check.js';
|
|
14
21
|
import { writeOutputs } from './writer.js';
|
|
15
22
|
import { hasPendingDecisions } from './model.js';
|
|
16
|
-
const emptyPrunePlan = { deletable: [], modifiedWithConsent: [], skippedMissing: [] };
|
|
23
|
+
const emptyPrunePlan = { deletable: [], modifiedWithConsent: [], skippedMissing: [], skippedUnsafe: [] };
|
|
17
24
|
export async function main(argv = process.argv.slice(2), promptIO = {}) {
|
|
18
25
|
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
19
26
|
showUsage();
|
|
@@ -43,19 +50,19 @@ export async function main(argv = process.argv.slice(2), promptIO = {}) {
|
|
|
43
50
|
skipSpecUpdate: options.skipSpecUpdate,
|
|
44
51
|
});
|
|
45
52
|
}
|
|
53
|
+
if (command === 'uninstall') {
|
|
54
|
+
return runUninstall(options, promptIO);
|
|
55
|
+
}
|
|
56
|
+
if (command === 'list') {
|
|
57
|
+
return runList(promptIO);
|
|
58
|
+
}
|
|
59
|
+
if (command === 'configure') {
|
|
60
|
+
return runConfigure(options, promptIO);
|
|
61
|
+
}
|
|
46
62
|
const versionCheckPromise = shouldCheckForUpdates(options, promptIO)
|
|
47
|
-
? checkLatestVersion({ current: readPackageVersion(), cachePath: path.join(resolveHome(promptIO), '
|
|
63
|
+
? checkLatestVersion({ current: readPackageVersion(), cachePath: path.join(stateRoot(resolveHome(promptIO)), 'version-check.json') }).catch(() => undefined)
|
|
48
64
|
: Promise.resolve(undefined);
|
|
49
65
|
try {
|
|
50
|
-
if (command === 'build-plugin') {
|
|
51
|
-
return await runBuildPlugin({
|
|
52
|
-
source: options.sourceExplicit ? options.source : bundledSourceRoot(),
|
|
53
|
-
outDir: options.out,
|
|
54
|
-
target: options.target,
|
|
55
|
-
dryRun: options.dryRun,
|
|
56
|
-
force: options.force,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
66
|
const install = command === 'install' || command === 'update';
|
|
60
67
|
if (install && !options.sourceExplicit)
|
|
61
68
|
options.source = bundledSourceRoot();
|
|
@@ -64,24 +71,44 @@ export async function main(argv = process.argv.slice(2), promptIO = {}) {
|
|
|
64
71
|
const interactive = install && isInteractivePrompt(promptIO);
|
|
65
72
|
if (interactive)
|
|
66
73
|
p.intro(`${pc.bold('Forge AI')} ${pc.dim(command === 'update' ? 'updater' : 'installer')}`, clackIO(promptIO));
|
|
74
|
+
const cwd = process.cwd();
|
|
75
|
+
const home = resolveHome(promptIO);
|
|
76
|
+
const now = new Date();
|
|
67
77
|
if (install) {
|
|
68
|
-
const prompted = await promptForMissingInstallOptions(options, promptIO);
|
|
78
|
+
const prompted = await promptForMissingInstallOptions(options, promptIO, cwd, home);
|
|
69
79
|
if (!prompted) {
|
|
70
80
|
if (interactive)
|
|
71
81
|
p.cancel('Cancelled', clackIO(promptIO));
|
|
72
82
|
return 1;
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
|
-
const cwd = process.cwd();
|
|
76
|
-
const home = resolveHome(promptIO);
|
|
77
|
-
const now = new Date();
|
|
78
85
|
let manifestLocation;
|
|
79
86
|
let oldManifest;
|
|
80
87
|
let backupRoot;
|
|
88
|
+
let modelPreferences;
|
|
81
89
|
if (install) {
|
|
82
90
|
manifestLocation = await resolveManifestLocation(options.scope, cwd, home);
|
|
83
91
|
oldManifest = await loadManifest(manifestLocation.manifestPath);
|
|
84
92
|
backupRoot = resolveBackupRoot(manifestLocation, now);
|
|
93
|
+
const drift = await detectLegacyStateDrift(manifestLocation, home);
|
|
94
|
+
if (drift)
|
|
95
|
+
console.error(`Warning: ${drift}`);
|
|
96
|
+
const prefsPath = modelPreferencesPath(manifestLocation);
|
|
97
|
+
modelPreferences = await loadModelPreferences(prefsPath);
|
|
98
|
+
if (options.modelExplicit || options.modelMapExplicit) {
|
|
99
|
+
const { artifacts } = await discoverArtifacts(options.source);
|
|
100
|
+
const applied = applyModelFlags(modelPreferences, options, artifacts);
|
|
101
|
+
if ('error' in applied) {
|
|
102
|
+
console.error(applied.error);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
modelPreferences = applied.preferences;
|
|
106
|
+
await saveModelPreferences(prefsPath, modelPreferences);
|
|
107
|
+
}
|
|
108
|
+
else if (options.pendingModelPreferences) {
|
|
109
|
+
modelPreferences = options.pendingModelPreferences;
|
|
110
|
+
await saveModelPreferences(prefsPath, modelPreferences);
|
|
111
|
+
}
|
|
85
112
|
}
|
|
86
113
|
const plan = await buildWritePlan({
|
|
87
114
|
source: options.source,
|
|
@@ -92,10 +119,13 @@ export async function main(argv = process.argv.slice(2), promptIO = {}) {
|
|
|
92
119
|
manifest: oldManifest,
|
|
93
120
|
backupRoot,
|
|
94
121
|
checkCollisions: install,
|
|
122
|
+
modelPreferences,
|
|
95
123
|
});
|
|
96
124
|
let prunePlan = emptyPrunePlan;
|
|
125
|
+
let pruneRoots = [];
|
|
97
126
|
if (install && command === 'update' && options.prune) {
|
|
98
|
-
|
|
127
|
+
pruneRoots = allowedInstallRoots(options.scope, home, cwd, oldManifest?.projectPath);
|
|
128
|
+
prunePlan = await classifyPrune(oldManifest, plan.files, backupRoot, options.scope, cwd, home, pruneRoots);
|
|
99
129
|
}
|
|
100
130
|
const needsConfirm = install && !options.force && (hasPendingDecisions(plan.pending) || prunePlan.modifiedWithConsent.length > 0);
|
|
101
131
|
if (install && !options.dryRun && needsConfirm) {
|
|
@@ -127,8 +157,8 @@ export async function main(argv = process.argv.slice(2), promptIO = {}) {
|
|
|
127
157
|
try {
|
|
128
158
|
await writeOutputs(plan.files);
|
|
129
159
|
if (command === 'update' && options.prune)
|
|
130
|
-
await pruneEntries([...prunePlan.deletable, ...prunePlan.modifiedWithConsent]);
|
|
131
|
-
await saveManifest(manifestLocation.manifestPath, await buildManifest(manifestLocation, plan.files));
|
|
160
|
+
await pruneEntries([...prunePlan.deletable, ...prunePlan.modifiedWithConsent], pruneRoots);
|
|
161
|
+
await saveManifest(manifestLocation.manifestPath, await buildManifest(manifestLocation, plan.files, readPackageVersion(), now, oldManifest));
|
|
132
162
|
spinner.stop(`Wrote ${plan.files.length} file(s).`);
|
|
133
163
|
}
|
|
134
164
|
catch (error) {
|
|
@@ -139,8 +169,8 @@ export async function main(argv = process.argv.slice(2), promptIO = {}) {
|
|
|
139
169
|
else {
|
|
140
170
|
await writeOutputs(plan.files);
|
|
141
171
|
if (command === 'update' && options.prune)
|
|
142
|
-
await pruneEntries([...prunePlan.deletable, ...prunePlan.modifiedWithConsent]);
|
|
143
|
-
await saveManifest(manifestLocation.manifestPath, await buildManifest(manifestLocation, plan.files));
|
|
172
|
+
await pruneEntries([...prunePlan.deletable, ...prunePlan.modifiedWithConsent], pruneRoots);
|
|
173
|
+
await saveManifest(manifestLocation.manifestPath, await buildManifest(manifestLocation, plan.files, readPackageVersion(), now, oldManifest));
|
|
144
174
|
console.log(`Wrote ${plan.files.length} file(s).`);
|
|
145
175
|
const totalDeleted = prunePlan.deletable.length + prunePlan.modifiedWithConsent.length;
|
|
146
176
|
if (command === 'update' && options.prune && totalDeleted > 0)
|
|
@@ -166,7 +196,7 @@ export async function main(argv = process.argv.slice(2), promptIO = {}) {
|
|
|
166
196
|
}
|
|
167
197
|
}
|
|
168
198
|
function parseArgs(argv) {
|
|
169
|
-
const options = { command: argv[0], platform: 'all', scope: 'user', source: '.',
|
|
199
|
+
const options = { command: argv[0], platform: 'all', scope: 'user', source: '.', dryRun: false, force: false, prune: true, yes: false, noUpdateCheck: false, skipSpecUpdate: false, platformExplicit: false, scopeExplicit: false, sourceExplicit: false, modelExplicit: false, modelMapExplicit: false };
|
|
170
200
|
for (let index = 1; index < argv.length; index += 1) {
|
|
171
201
|
const arg = argv[index];
|
|
172
202
|
if (arg === '--dry-run')
|
|
@@ -185,7 +215,9 @@ function parseArgs(argv) {
|
|
|
185
215
|
const value = argv[++index];
|
|
186
216
|
if (!value)
|
|
187
217
|
return { error: 'Missing --to value' };
|
|
188
|
-
|
|
218
|
+
if (!isValidVersionSpec(value))
|
|
219
|
+
return { error: `Invalid --to ${value}; expected a semver or "latest"` };
|
|
220
|
+
options.targetVersion = normalizeVersionSpec(value);
|
|
189
221
|
}
|
|
190
222
|
else if (arg === '--platform') {
|
|
191
223
|
const value = argv[++index];
|
|
@@ -210,20 +242,22 @@ function parseArgs(argv) {
|
|
|
210
242
|
options.source = value;
|
|
211
243
|
options.sourceExplicit = true;
|
|
212
244
|
}
|
|
213
|
-
else if (arg === '--
|
|
245
|
+
else if (arg === '--model') {
|
|
214
246
|
const value = argv[++index];
|
|
215
247
|
if (!value)
|
|
216
|
-
return { error: 'Missing --
|
|
217
|
-
options.
|
|
218
|
-
options.
|
|
248
|
+
return { error: 'Missing --model value' };
|
|
249
|
+
options.model = value;
|
|
250
|
+
options.modelExplicit = true;
|
|
219
251
|
}
|
|
220
|
-
else if (arg === '--
|
|
252
|
+
else if (arg === '--model-map') {
|
|
221
253
|
const value = argv[++index];
|
|
222
|
-
if (value
|
|
223
|
-
return { error:
|
|
224
|
-
|
|
225
|
-
if (!
|
|
226
|
-
|
|
254
|
+
if (!value)
|
|
255
|
+
return { error: 'Missing --model-map value' };
|
|
256
|
+
const modelMap = parseModelMap(value);
|
|
257
|
+
if (!modelMap)
|
|
258
|
+
return { error: `Invalid --model-map ${value}; expected name=model,name2=model2` };
|
|
259
|
+
options.modelMap = modelMap;
|
|
260
|
+
options.modelMapExplicit = true;
|
|
227
261
|
}
|
|
228
262
|
else {
|
|
229
263
|
return { error: `Unknown argument ${arg}` };
|
|
@@ -236,33 +270,45 @@ function parseArgs(argv) {
|
|
|
236
270
|
return { error: 'validate only accepts --platform and --source' };
|
|
237
271
|
if (command !== 'self-update' && (options.targetVersion !== undefined || options.skipSpecUpdate))
|
|
238
272
|
return { error: '--to and --skip-spec-update are only accepted for self-update' };
|
|
273
|
+
if ((options.modelExplicit || options.modelMapExplicit) && (command !== 'install' && command !== 'update' && command !== 'configure'))
|
|
274
|
+
return { error: '--model and --model-map are only accepted for install, update, and configure' };
|
|
275
|
+
if ((options.modelExplicit || options.modelMapExplicit) && (!options.platformExplicit || options.platform === 'all'))
|
|
276
|
+
return { error: '--model and --model-map require an explicit single --platform (model ids are not portable across platforms)' };
|
|
239
277
|
if (command === 'self-update' && (options.platformExplicit || options.scopeExplicit || options.sourceExplicit || options.force || options.yes))
|
|
240
278
|
return { error: 'self-update only accepts --to, --dry-run, --skip-spec-update' };
|
|
241
|
-
if (command === '
|
|
242
|
-
return { error: '
|
|
279
|
+
if (command === 'uninstall' && options.sourceExplicit)
|
|
280
|
+
return { error: 'uninstall does not accept --source' };
|
|
281
|
+
if (command === 'list' && (options.platformExplicit || options.scopeExplicit || options.sourceExplicit || options.dryRun || options.force || options.yes))
|
|
282
|
+
return { error: 'list does not accept any flags' };
|
|
283
|
+
if (command === 'configure' && (options.force || options.yes))
|
|
284
|
+
return { error: 'configure does not accept --force or --yes; use --model/--model-map for non-interactive use' };
|
|
243
285
|
return { options };
|
|
244
286
|
}
|
|
245
|
-
async function classifyPrune(oldManifest, files, backupRoot, scope, cwd, home) {
|
|
287
|
+
async function classifyPrune(oldManifest, files, backupRoot, scope, cwd, home, roots) {
|
|
246
288
|
const stale = staleEntries(oldManifest, files);
|
|
247
|
-
const classified = await classifyPruneEntries(stale);
|
|
289
|
+
const classified = await classifyPruneEntries(stale, roots);
|
|
248
290
|
const anchor = scope === 'user' ? home : cwd;
|
|
249
291
|
const modifiedWithConsent = [];
|
|
250
292
|
const skippedMissing = [];
|
|
293
|
+
const skippedUnsafe = [];
|
|
251
294
|
for (const item of classified.skipped) {
|
|
252
295
|
if (item.reason === 'checksum-mismatch')
|
|
253
296
|
modifiedWithConsent.push({ ...item, backupPath: resolveBackupPath(backupRoot, item.path, anchor) });
|
|
297
|
+
else if (item.reason === 'unsafe-path')
|
|
298
|
+
skippedUnsafe.push(item);
|
|
254
299
|
else
|
|
255
300
|
skippedMissing.push(item);
|
|
256
301
|
}
|
|
257
|
-
return { deletable: classified.deletable, modifiedWithConsent, skippedMissing };
|
|
302
|
+
return { deletable: classified.deletable, modifiedWithConsent, skippedMissing, skippedUnsafe };
|
|
258
303
|
}
|
|
259
|
-
async function promptForMissingInstallOptions(options, promptIO) {
|
|
304
|
+
async function promptForMissingInstallOptions(options, promptIO, cwd, home) {
|
|
260
305
|
if (options.yes)
|
|
261
306
|
return true;
|
|
262
|
-
if (options.platformExplicit && options.scopeExplicit)
|
|
263
|
-
return true;
|
|
264
307
|
if (!isInteractivePrompt(promptIO))
|
|
265
308
|
return true;
|
|
309
|
+
const modelFlagsGiven = options.modelExplicit || options.modelMapExplicit;
|
|
310
|
+
if (options.platformExplicit && options.scopeExplicit && modelFlagsGiven)
|
|
311
|
+
return true;
|
|
266
312
|
const io = clackIO(promptIO);
|
|
267
313
|
if (!options.platformExplicit) {
|
|
268
314
|
const platform = await p.select({
|
|
@@ -295,8 +341,185 @@ async function promptForMissingInstallOptions(options, promptIO) {
|
|
|
295
341
|
return false;
|
|
296
342
|
options.scope = scope;
|
|
297
343
|
}
|
|
344
|
+
if (!modelFlagsGiven) {
|
|
345
|
+
return promptForModelSelection(options, promptIO, cwd, home);
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
function modelablePairs(artifacts, targetPlatforms) {
|
|
350
|
+
const pairs = [];
|
|
351
|
+
for (const platform of targetPlatforms) {
|
|
352
|
+
for (const artifact of artifacts) {
|
|
353
|
+
const effectiveKind = artifact[platform]?.kind ?? artifact.kind;
|
|
354
|
+
if (supportsModel(platform, effectiveKind))
|
|
355
|
+
pairs.push({ platform, artifact });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return pairs;
|
|
359
|
+
}
|
|
360
|
+
const CONFIRM_PROMPT_COUNT_ABOVE = 6;
|
|
361
|
+
// Returns false only on user cancellation (Ctrl+C mid-prompt) — declining the "choose per agent"
|
|
362
|
+
// path, or there being nothing to choose, both return true (proceed with defaults).
|
|
363
|
+
async function promptForModelSelection(options, promptIO, cwd, home) {
|
|
364
|
+
const io = clackIO(promptIO);
|
|
365
|
+
const { artifacts } = await discoverArtifacts(options.source);
|
|
366
|
+
const targetPlatforms = resolvePlatforms(options.platform);
|
|
367
|
+
const pairs = modelablePairs(artifacts, targetPlatforms);
|
|
368
|
+
if (pairs.length === 0)
|
|
369
|
+
return true;
|
|
370
|
+
const mode = await p.select({
|
|
371
|
+
message: 'Model selection',
|
|
372
|
+
initialValue: 'default',
|
|
373
|
+
options: [
|
|
374
|
+
{ value: 'default', label: "Use each agent's recommended default" },
|
|
375
|
+
{ value: 'per-agent', label: 'Choose per agent' }
|
|
376
|
+
],
|
|
377
|
+
...io
|
|
378
|
+
});
|
|
379
|
+
if (p.isCancel(mode))
|
|
380
|
+
return false;
|
|
381
|
+
if (mode === 'default')
|
|
382
|
+
return true;
|
|
383
|
+
if (pairs.length > CONFIRM_PROMPT_COUNT_ABOVE) {
|
|
384
|
+
const proceed = await p.confirm({
|
|
385
|
+
message: `This will ask ${pairs.length} questions (one per agent per platform). Continue?`,
|
|
386
|
+
active: 'Continue',
|
|
387
|
+
inactive: 'Use defaults instead',
|
|
388
|
+
initialValue: true,
|
|
389
|
+
...io
|
|
390
|
+
});
|
|
391
|
+
if (p.isCancel(proceed))
|
|
392
|
+
return false;
|
|
393
|
+
if (!proceed)
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
const manifestLocation = await resolveManifestLocation(options.scope, cwd, home);
|
|
397
|
+
let preferences = await loadModelPreferences(modelPreferencesPath(manifestLocation));
|
|
398
|
+
const modelablePlatforms = [...new Set(pairs.map((pair) => pair.platform))];
|
|
399
|
+
const discovered = discoverModelChoices(modelablePlatforms, cwd, promptIO);
|
|
400
|
+
for (const { platform, artifact } of pairs) {
|
|
401
|
+
const current = getModelPreference(preferences, platform, artifact.name) ?? artifact[platform]?.model;
|
|
402
|
+
const chosen = await promptForModelValue(platform, artifact.name, current, discovered, promptIO);
|
|
403
|
+
if (chosen === undefined)
|
|
404
|
+
return false;
|
|
405
|
+
preferences = setModelPreference(preferences, platform, artifact.name, chosen);
|
|
406
|
+
}
|
|
407
|
+
options.pendingModelPreferences = preferences;
|
|
298
408
|
return true;
|
|
299
409
|
}
|
|
410
|
+
const CUSTOM_MODEL_VALUE = '__custom__';
|
|
411
|
+
// Pure label helper: suffixes a live-derived value's *label* with ` (live)` so the user can tell live
|
|
412
|
+
// options from curated/merged extras. The select **value** stays the bare model id, so stored
|
|
413
|
+
// preferences and `initialValue` matching are unchanged. Curated extras and the `Custom…` escape are
|
|
414
|
+
// never in the live set, so they are never suffixed.
|
|
415
|
+
export function withLiveLabels(values, liveSet) {
|
|
416
|
+
return values.map((value) => ({ value, label: liveSet.has(value) ? `${value} (live)` : value }));
|
|
417
|
+
}
|
|
418
|
+
// Curated model suggestions for the interactive per-agent model prompt, or undefined when there is
|
|
419
|
+
// no fixed list (the prompt then falls back to free text). Discovery results are live-first, highest
|
|
420
|
+
// priority. Each platform is an explicit branch — a missing branch fails loudly rather than silently
|
|
421
|
+
// producing a "no choices" path (see the codex-model-options regression in .forge/lessons.md):
|
|
422
|
+
// opencode: live, else free-text (undefined) — no curated set.
|
|
423
|
+
// codex: live, NO merge (the live catalog is the real account-filtered set; merging stale curated
|
|
424
|
+
// suggestions would present ids the user may not be able to select); curated on failure.
|
|
425
|
+
// grok: live + curated extras merged (dedupe, stable order) — `grok models` doesn't advertise the
|
|
426
|
+
// Forge-specific aliases, so they must survive a successful live query; curated on failure.
|
|
427
|
+
// claude: curated always — no dynamic source, never empty/undefined.
|
|
428
|
+
export function modelChoicesFor(platform, discovered = {}) {
|
|
429
|
+
const live = discovered[platform];
|
|
430
|
+
if (platform === 'opencode')
|
|
431
|
+
return live && live.length > 0 ? live : undefined;
|
|
432
|
+
if (platform === 'claude')
|
|
433
|
+
return [...knownClaudeModels];
|
|
434
|
+
if (platform === 'codex')
|
|
435
|
+
return live && live.length > 0 ? live : [...knownCodexModels];
|
|
436
|
+
if (platform === 'grok')
|
|
437
|
+
return live && live.length > 0 ? mergeLiveWithCurated(live, knownGrokModels) : [...knownGrokModels];
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
async function promptForModelValue(platform, artifactName, current, discovered, promptIO) {
|
|
441
|
+
const io = clackIO(promptIO);
|
|
442
|
+
const knownChoices = modelChoicesFor(platform, discovered);
|
|
443
|
+
if (knownChoices && knownChoices.length > 0) {
|
|
444
|
+
const initialValue = current && knownChoices.includes(current) ? current : knownChoices[0];
|
|
445
|
+
const liveSet = new Set(discovered[platform] ?? []);
|
|
446
|
+
const choice = await p.select({
|
|
447
|
+
message: `Model for \`${artifactName}\` (${platform})`,
|
|
448
|
+
initialValue,
|
|
449
|
+
options: [...withLiveLabels(knownChoices, liveSet), { value: CUSTOM_MODEL_VALUE, label: 'Custom…' }],
|
|
450
|
+
...io
|
|
451
|
+
});
|
|
452
|
+
if (p.isCancel(choice))
|
|
453
|
+
return undefined;
|
|
454
|
+
if (choice !== CUSTOM_MODEL_VALUE)
|
|
455
|
+
return choice;
|
|
456
|
+
}
|
|
457
|
+
const text = await p.text({
|
|
458
|
+
message: `Model for \`${artifactName}\` (${platform})`,
|
|
459
|
+
initialValue: current,
|
|
460
|
+
validate: (value) => (value && value.trim().length > 0 ? undefined : 'A model id is required'),
|
|
461
|
+
...io
|
|
462
|
+
});
|
|
463
|
+
if (p.isCancel(text))
|
|
464
|
+
return undefined;
|
|
465
|
+
return text;
|
|
466
|
+
}
|
|
467
|
+
// Compute the per-platform discovery map once per CLI run, scoped to the deduped set of platforms that
|
|
468
|
+
// actually have a modelable pair in this run (so e.g. `install --platform all` with no Codex agent
|
|
469
|
+
// artifact never sparks a needless `codex debug models` spawn). Discovery is synchronous (spawnSync).
|
|
470
|
+
// A failed non-claude discovery emits a single info diagnostic. `claude` is skipped because it has no
|
|
471
|
+
// dynamic source by design (not because it failed). The diagnostic is rendered inside the clack frame
|
|
472
|
+
// (p.log.warn) when the run is interactive so it doesn't corrupt the TUI (raw console.error writes to
|
|
473
|
+
// stderr, the same terminal clack draws on, which clack does not redraw/clear — see lessons.md
|
|
474
|
+
// dynamic-model-discovery); console.error is only the non-interactive fallback.
|
|
475
|
+
function discoverModelChoices(platforms, cwd, promptIO) {
|
|
476
|
+
const discovered = {};
|
|
477
|
+
for (const platform of platforms) {
|
|
478
|
+
const models = discoverModels(platform, cwd);
|
|
479
|
+
if (models && models.length > 0) {
|
|
480
|
+
discovered[platform] = models;
|
|
481
|
+
}
|
|
482
|
+
else if (platform !== 'claude') {
|
|
483
|
+
const note = `forge: model discovery for ${platform} failed; using curated fallback.`;
|
|
484
|
+
if (isInteractivePrompt(promptIO))
|
|
485
|
+
p.log.warn(note, clackIO(promptIO));
|
|
486
|
+
else
|
|
487
|
+
console.error(note);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return discovered;
|
|
491
|
+
}
|
|
492
|
+
function parseModelMap(value) {
|
|
493
|
+
const result = {};
|
|
494
|
+
for (const pair of value.split(',')) {
|
|
495
|
+
const [name, model] = pair.split('=').map((part) => part?.trim());
|
|
496
|
+
if (!name || !model)
|
|
497
|
+
return undefined;
|
|
498
|
+
result[name] = model;
|
|
499
|
+
}
|
|
500
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
501
|
+
}
|
|
502
|
+
function applyModelFlags(preferences, options, artifacts) {
|
|
503
|
+
const targetPlatforms = resolvePlatforms(options.platform);
|
|
504
|
+
const platform = targetPlatforms[0];
|
|
505
|
+
let next = preferences;
|
|
506
|
+
if (options.model) {
|
|
507
|
+
for (const artifact of artifacts) {
|
|
508
|
+
const effectiveKind = artifact[platform]?.kind ?? artifact.kind;
|
|
509
|
+
if (supportsModel(platform, effectiveKind))
|
|
510
|
+
next = setModelPreference(next, platform, artifact.name, options.model);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (options.modelMap) {
|
|
514
|
+
const knownNames = new Set(artifacts.map((artifact) => artifact.name));
|
|
515
|
+
for (const [name, model] of Object.entries(options.modelMap)) {
|
|
516
|
+
if (!knownNames.has(name))
|
|
517
|
+
return { error: `--model-map references unknown artifact "${name}"` };
|
|
518
|
+
next = setModelPreference(next, platform, name, model);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { preferences: next };
|
|
522
|
+
}
|
|
300
523
|
async function promptForUpdate(plan, prunePlan, backupRoot, promptIO) {
|
|
301
524
|
if (!isInteractivePrompt(promptIO))
|
|
302
525
|
return false;
|
|
@@ -323,6 +546,280 @@ async function promptForUpdate(plan, prunePlan, backupRoot, promptIO) {
|
|
|
323
546
|
return undefined;
|
|
324
547
|
return accepted;
|
|
325
548
|
}
|
|
549
|
+
async function runUninstall(options, promptIO) {
|
|
550
|
+
if (options.yes)
|
|
551
|
+
options.force = true;
|
|
552
|
+
const interactive = isInteractivePrompt(promptIO);
|
|
553
|
+
if (interactive)
|
|
554
|
+
p.intro(`${pc.bold('Forge AI')} ${pc.dim('uninstaller')}`, clackIO(promptIO));
|
|
555
|
+
const prompted = await promptForMissingUninstallOptions(options, promptIO);
|
|
556
|
+
if (!prompted) {
|
|
557
|
+
if (interactive)
|
|
558
|
+
p.cancel('Cancelled', clackIO(promptIO));
|
|
559
|
+
return 1;
|
|
560
|
+
}
|
|
561
|
+
const cwd = process.cwd();
|
|
562
|
+
const home = resolveHome(promptIO);
|
|
563
|
+
const now = new Date();
|
|
564
|
+
const manifestLocation = await resolveManifestLocation(options.scope, cwd, home);
|
|
565
|
+
const manifest = await loadManifest(manifestLocation.manifestPath);
|
|
566
|
+
const drift = await detectLegacyStateDrift(manifestLocation, home);
|
|
567
|
+
if (drift)
|
|
568
|
+
console.error(`Warning: ${drift}`);
|
|
569
|
+
if (!manifest || manifest.entries.length === 0) {
|
|
570
|
+
console.log(`Nothing to uninstall for ${options.scope} scope.`);
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
const targeted = options.platform === 'all' ? manifest.entries : manifest.entries.filter((entry) => entry.platform === options.platform);
|
|
574
|
+
if (targeted.length === 0) {
|
|
575
|
+
console.log(`No ${options.platform} files recorded for this ${options.scope} install.`);
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
const backupRoot = resolveBackupRoot(manifestLocation, now);
|
|
579
|
+
const anchor = options.scope === 'user' ? home : cwd;
|
|
580
|
+
const roots = allowedInstallRoots(options.scope, home, cwd, manifest.projectPath);
|
|
581
|
+
const classified = await classifyPruneEntries(targeted, roots);
|
|
582
|
+
const modifiedWithConsent = classified.skipped
|
|
583
|
+
.filter((item) => item.reason === 'checksum-mismatch')
|
|
584
|
+
.map((item) => ({ ...item, backupPath: resolveBackupPath(backupRoot, item.path, anchor) }));
|
|
585
|
+
const skippedMissing = classified.skipped.filter((item) => item.reason === 'missing');
|
|
586
|
+
const skippedUnsafe = classified.skipped.filter((item) => item.reason === 'unsafe-path');
|
|
587
|
+
const needsConfirm = !options.force && modifiedWithConsent.length > 0;
|
|
588
|
+
if (!options.dryRun && needsConfirm) {
|
|
589
|
+
if (!interactive) {
|
|
590
|
+
printUninstallPlan(classified.deletable, modifiedWithConsent, skippedMissing, skippedUnsafe);
|
|
591
|
+
console.error('Forge needs your decision on edited files; re-run with --yes or --force to accept deletion + backup.');
|
|
592
|
+
return 1;
|
|
593
|
+
}
|
|
594
|
+
const accepted = await promptForUninstall(classified.deletable, modifiedWithConsent, backupRoot, promptIO);
|
|
595
|
+
if (accepted === undefined) {
|
|
596
|
+
p.cancel('Cancelled', clackIO(promptIO));
|
|
597
|
+
return 1;
|
|
598
|
+
}
|
|
599
|
+
if (!accepted) {
|
|
600
|
+
p.outro(pc.yellow('Forge was not uninstalled.'), clackIO(promptIO));
|
|
601
|
+
return 1;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
printUninstallPlan(classified.deletable, modifiedWithConsent, skippedMissing, skippedUnsafe);
|
|
605
|
+
if (options.dryRun) {
|
|
606
|
+
if (interactive)
|
|
607
|
+
p.outro(pc.cyan('Dry run complete.'), clackIO(promptIO));
|
|
608
|
+
return 0;
|
|
609
|
+
}
|
|
610
|
+
const toRemove = [...classified.deletable, ...modifiedWithConsent];
|
|
611
|
+
await pruneEntries(toRemove, roots);
|
|
612
|
+
const targetedPaths = new Set(targeted.map((entry) => entry.path));
|
|
613
|
+
const remaining = manifest.entries.filter((entry) => !targetedPaths.has(entry.path));
|
|
614
|
+
if (remaining.length === 0) {
|
|
615
|
+
await rm(manifestLocation.manifestPath, { force: true });
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
await saveManifest(manifestLocation.manifestPath, { ...manifest, entries: remaining, updatedAt: now.toISOString() });
|
|
619
|
+
}
|
|
620
|
+
console.log(`Removed ${toRemove.length} file(s).`);
|
|
621
|
+
if (skippedMissing.length > 0)
|
|
622
|
+
console.log(`${skippedMissing.length} file(s) were already missing.`);
|
|
623
|
+
if (skippedUnsafe.length > 0)
|
|
624
|
+
console.error(`Warning: skipped ${skippedUnsafe.length} file(s) outside Forge's install roots (unsafe path); they were NOT removed.`);
|
|
625
|
+
if (interactive)
|
|
626
|
+
p.outro(pc.green('Forge was uninstalled.'), clackIO(promptIO));
|
|
627
|
+
return 0;
|
|
628
|
+
}
|
|
629
|
+
async function promptForMissingUninstallOptions(options, promptIO) {
|
|
630
|
+
if (options.yes)
|
|
631
|
+
return true;
|
|
632
|
+
if (options.platformExplicit && options.scopeExplicit)
|
|
633
|
+
return true;
|
|
634
|
+
if (!isInteractivePrompt(promptIO))
|
|
635
|
+
return true;
|
|
636
|
+
const io = clackIO(promptIO);
|
|
637
|
+
if (!options.platformExplicit) {
|
|
638
|
+
const platform = await p.select({
|
|
639
|
+
message: 'Uninstall Forge for which coding agent?',
|
|
640
|
+
initialValue: 'all',
|
|
641
|
+
options: [
|
|
642
|
+
{ value: 'all', label: 'All supported agents', hint: 'OpenCode, Codex, Claude Code, and Grok Build' },
|
|
643
|
+
{ value: 'opencode', label: 'OpenCode' },
|
|
644
|
+
{ value: 'codex', label: 'Codex' },
|
|
645
|
+
{ value: 'claude', label: 'Claude Code' },
|
|
646
|
+
{ value: 'grok', label: 'Grok Build' }
|
|
647
|
+
],
|
|
648
|
+
...io
|
|
649
|
+
});
|
|
650
|
+
if (p.isCancel(platform))
|
|
651
|
+
return false;
|
|
652
|
+
options.platform = platform;
|
|
653
|
+
}
|
|
654
|
+
if (!options.scopeExplicit) {
|
|
655
|
+
const scope = await p.select({
|
|
656
|
+
message: 'Uninstall Forge from which scope?',
|
|
657
|
+
initialValue: 'user',
|
|
658
|
+
options: [
|
|
659
|
+
{ value: 'user', label: 'User', hint: 'Installed for every project' },
|
|
660
|
+
{ value: 'project', label: 'Project', hint: 'Only this repository' }
|
|
661
|
+
],
|
|
662
|
+
...io
|
|
663
|
+
});
|
|
664
|
+
if (p.isCancel(scope))
|
|
665
|
+
return false;
|
|
666
|
+
options.scope = scope;
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
async function promptForUninstall(deletable, modifiedWithConsent, backupRoot, promptIO) {
|
|
671
|
+
if (!isInteractivePrompt(promptIO))
|
|
672
|
+
return false;
|
|
673
|
+
const io = clackIO(promptIO);
|
|
674
|
+
p.log.warn(`${pc.yellow('Edited by you, will be deleted (backup):')}\n${modifiedWithConsent.map((entry) => ` - ${entry.path}`).join('\n')}\n\nBackups → ${backupRoot}`, io);
|
|
675
|
+
const accepted = await p.confirm({
|
|
676
|
+
message: 'Continue with deletion + backup?',
|
|
677
|
+
active: 'Continue',
|
|
678
|
+
inactive: 'Cancel',
|
|
679
|
+
initialValue: false,
|
|
680
|
+
...io
|
|
681
|
+
});
|
|
682
|
+
if (p.isCancel(accepted))
|
|
683
|
+
return undefined;
|
|
684
|
+
return accepted;
|
|
685
|
+
}
|
|
686
|
+
function printUninstallPlan(deletable, modifiedWithConsent, skippedMissing, skippedUnsafe) {
|
|
687
|
+
console.log(`uninstall: ${deletable.length + modifiedWithConsent.length} file(s) to remove`);
|
|
688
|
+
for (const entry of deletable)
|
|
689
|
+
console.log(`- delete ${entry.platform} ${entry.kind} ${entry.name} -> ${entry.path}`);
|
|
690
|
+
for (const entry of modifiedWithConsent)
|
|
691
|
+
console.log(`- delete ${entry.platform} ${entry.kind} ${entry.name} -> ${entry.path} [backup -> ${entry.backupPath}]`);
|
|
692
|
+
for (const entry of skippedMissing)
|
|
693
|
+
console.log(`- skip missing ${entry.platform} ${entry.kind} ${entry.name} -> ${entry.path}`);
|
|
694
|
+
for (const entry of skippedUnsafe)
|
|
695
|
+
console.log(`- skip unsafe ${entry.platform} ${entry.kind} ${entry.name} -> ${entry.path}`);
|
|
696
|
+
}
|
|
697
|
+
async function runList(promptIO) {
|
|
698
|
+
const home = resolveHome(promptIO);
|
|
699
|
+
const summaries = await listInstalls(home);
|
|
700
|
+
if (summaries.length === 0) {
|
|
701
|
+
console.log('No Forge installs recorded.');
|
|
702
|
+
return 0;
|
|
703
|
+
}
|
|
704
|
+
for (const summary of summaries) {
|
|
705
|
+
console.log(formatInstallSummary(summary));
|
|
706
|
+
if (summary.driftWarning)
|
|
707
|
+
console.error(`Warning: ${summary.driftWarning}`);
|
|
708
|
+
}
|
|
709
|
+
return 0;
|
|
710
|
+
}
|
|
711
|
+
function formatInstallSummary(summary) {
|
|
712
|
+
const target = summary.scope === 'user' ? 'user' : `project ${summary.projectPath ?? '(unknown path)'}`;
|
|
713
|
+
const platforms = summary.platforms.length > 0 ? summary.platforms.join(', ') : 'none';
|
|
714
|
+
return `${target}: forge ${summary.forgeVersion}, ${summary.fileCount} file(s), platforms: ${platforms}, updated ${summary.updatedAt}`;
|
|
715
|
+
}
|
|
716
|
+
async function runConfigure(options, promptIO) {
|
|
717
|
+
const interactive = isInteractivePrompt(promptIO);
|
|
718
|
+
if (interactive)
|
|
719
|
+
p.intro(`${pc.bold('Forge AI')} ${pc.dim('configure')}`, clackIO(promptIO));
|
|
720
|
+
if (!options.scopeExplicit) {
|
|
721
|
+
if (!interactive) {
|
|
722
|
+
console.error('forge-ai configure needs --scope when not run interactively.');
|
|
723
|
+
return 1;
|
|
724
|
+
}
|
|
725
|
+
const scope = await p.select({
|
|
726
|
+
message: 'Configure which scope?',
|
|
727
|
+
initialValue: 'user',
|
|
728
|
+
options: [
|
|
729
|
+
{ value: 'user', label: 'User', hint: 'Available in every project' },
|
|
730
|
+
{ value: 'project', label: 'Project', hint: 'Only this repository' }
|
|
731
|
+
],
|
|
732
|
+
...clackIO(promptIO)
|
|
733
|
+
});
|
|
734
|
+
if (p.isCancel(scope)) {
|
|
735
|
+
p.cancel('Cancelled', clackIO(promptIO));
|
|
736
|
+
return 1;
|
|
737
|
+
}
|
|
738
|
+
options.scope = scope;
|
|
739
|
+
}
|
|
740
|
+
const cwd = process.cwd();
|
|
741
|
+
const home = resolveHome(promptIO);
|
|
742
|
+
const now = new Date();
|
|
743
|
+
const manifestLocation = await resolveManifestLocation(options.scope, cwd, home);
|
|
744
|
+
const manifest = await loadManifest(manifestLocation.manifestPath);
|
|
745
|
+
const drift = await detectLegacyStateDrift(manifestLocation, home);
|
|
746
|
+
if (drift)
|
|
747
|
+
console.error(`Warning: ${drift}`);
|
|
748
|
+
if (!manifest || manifest.entries.length === 0) {
|
|
749
|
+
console.log(`Nothing installed for ${options.scope} scope — run \`forge-ai install\` first.`);
|
|
750
|
+
return 0;
|
|
751
|
+
}
|
|
752
|
+
const installedPlatforms = [...new Set(manifest.entries.map((entry) => entry.platform))];
|
|
753
|
+
const targetPlatforms = options.platformExplicit ? installedPlatforms.filter((platform) => platform === options.platform) : installedPlatforms;
|
|
754
|
+
if (targetPlatforms.length === 0) {
|
|
755
|
+
console.log(`${options.platform} is not installed for ${options.scope} scope.`);
|
|
756
|
+
return 0;
|
|
757
|
+
}
|
|
758
|
+
const source = options.sourceExplicit ? options.source : bundledSourceRoot();
|
|
759
|
+
const { artifacts } = await discoverArtifacts(source);
|
|
760
|
+
const prefsPath = modelPreferencesPath(manifestLocation);
|
|
761
|
+
let preferences = await loadModelPreferences(prefsPath);
|
|
762
|
+
if (options.modelExplicit || options.modelMapExplicit) {
|
|
763
|
+
const applied = applyModelFlags(preferences, options, artifacts);
|
|
764
|
+
if ('error' in applied) {
|
|
765
|
+
console.error(applied.error);
|
|
766
|
+
return 1;
|
|
767
|
+
}
|
|
768
|
+
preferences = applied.preferences;
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
if (!interactive) {
|
|
772
|
+
console.error('forge-ai configure needs --model or --model-map when not run interactively.');
|
|
773
|
+
return 1;
|
|
774
|
+
}
|
|
775
|
+
const pairs = modelablePairs(artifacts, targetPlatforms);
|
|
776
|
+
if (pairs.length === 0) {
|
|
777
|
+
console.log('No installed agent on the selected scope supports a configurable model.');
|
|
778
|
+
return 0;
|
|
779
|
+
}
|
|
780
|
+
const modelablePlatforms = [...new Set(pairs.map((pair) => pair.platform))];
|
|
781
|
+
const discovered = discoverModelChoices(modelablePlatforms, cwd, promptIO);
|
|
782
|
+
for (const { platform, artifact } of pairs) {
|
|
783
|
+
const current = getModelPreference(preferences, platform, artifact.name) ?? artifact[platform]?.model;
|
|
784
|
+
const chosen = await promptForModelValue(platform, artifact.name, current, discovered, promptIO);
|
|
785
|
+
if (chosen === undefined) {
|
|
786
|
+
p.cancel('Cancelled', clackIO(promptIO));
|
|
787
|
+
return 1;
|
|
788
|
+
}
|
|
789
|
+
preferences = setModelPreference(preferences, platform, artifact.name, chosen);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
await saveModelPreferences(prefsPath, preferences);
|
|
793
|
+
const backupRoot = resolveBackupRoot(manifestLocation, now);
|
|
794
|
+
let files = [];
|
|
795
|
+
let diagnostics = [];
|
|
796
|
+
for (const platform of targetPlatforms) {
|
|
797
|
+
const result = await buildWritePlan({ source, platform, scope: options.scope, cwd, home, manifest, backupRoot, checkCollisions: true, modelPreferences: preferences });
|
|
798
|
+
files = files.concat(result.files);
|
|
799
|
+
diagnostics = diagnostics.concat(result.diagnostics);
|
|
800
|
+
}
|
|
801
|
+
console.log(`configure: ${files.length} file(s) to update`);
|
|
802
|
+
for (const file of files)
|
|
803
|
+
console.log(`- ${file.platform} ${file.kind} ${file.name} -> ${file.path}${statusSuffix(file)}`);
|
|
804
|
+
for (const item of diagnostics)
|
|
805
|
+
console.log(formatDiagnostic(item));
|
|
806
|
+
if (hasErrors(diagnostics)) {
|
|
807
|
+
if (interactive)
|
|
808
|
+
p.outro(pc.red('Forge was not reconfigured.'), clackIO(promptIO));
|
|
809
|
+
return 1;
|
|
810
|
+
}
|
|
811
|
+
if (options.dryRun) {
|
|
812
|
+
if (interactive)
|
|
813
|
+
p.outro(pc.cyan('Dry run complete. Model preferences were saved; files were not written.'), clackIO(promptIO));
|
|
814
|
+
return 0;
|
|
815
|
+
}
|
|
816
|
+
await writeOutputs(files);
|
|
817
|
+
await saveManifest(manifestLocation.manifestPath, await buildManifest(manifestLocation, files, readPackageVersion(), now, manifest));
|
|
818
|
+
console.log(`Updated ${files.length} file(s) with new model preferences.`);
|
|
819
|
+
if (interactive)
|
|
820
|
+
p.outro(pc.green('Forge is reconfigured.'), clackIO(promptIO));
|
|
821
|
+
return 0;
|
|
822
|
+
}
|
|
326
823
|
function normalizeCommand(command) {
|
|
327
824
|
if (command === 'install' || command === 'i')
|
|
328
825
|
return 'install';
|
|
@@ -330,10 +827,14 @@ function normalizeCommand(command) {
|
|
|
330
827
|
return 'update';
|
|
331
828
|
if (command === 'validate')
|
|
332
829
|
return 'validate';
|
|
830
|
+
if (command === 'uninstall')
|
|
831
|
+
return 'uninstall';
|
|
832
|
+
if (command === 'list' || command === 'ls')
|
|
833
|
+
return 'list';
|
|
834
|
+
if (command === 'configure')
|
|
835
|
+
return 'configure';
|
|
333
836
|
if (command === 'self-update')
|
|
334
837
|
return 'self-update';
|
|
335
|
-
if (command === 'build-plugin')
|
|
336
|
-
return 'build-plugin';
|
|
337
838
|
return undefined;
|
|
338
839
|
}
|
|
339
840
|
function shouldCheckForUpdates(options, promptIO) {
|
|
@@ -373,12 +874,15 @@ function readPackageVersion() {
|
|
|
373
874
|
}
|
|
374
875
|
}
|
|
375
876
|
function showUsage() {
|
|
376
|
-
console.log('Usage: forge-ai install [--platform opencode|claude|codex|grok|all] [--scope user|project] [--source <dir>] [--dry-run] [--force] [--yes]');
|
|
377
|
-
console.log(' forge-ai update [--platform opencode|claude|codex|grok|all] [--scope user|project] [--source <dir>] [--dry-run] [--no-prune] [--yes]');
|
|
877
|
+
console.log('Usage: forge-ai install [--platform opencode|claude|codex|grok|all] [--scope user|project] [--source <dir>] [--dry-run] [--force] [--yes] [--model <id>] [--model-map name=model,...]');
|
|
878
|
+
console.log(' forge-ai update [--platform opencode|claude|codex|grok|all] [--scope user|project] [--source <dir>] [--dry-run] [--no-prune] [--yes] [--model <id>] [--model-map name=model,...]');
|
|
879
|
+
console.log(' forge-ai uninstall [--platform opencode|claude|codex|grok|all] [--scope user|project] [--dry-run] [--force] [--yes]');
|
|
880
|
+
console.log(' forge-ai list');
|
|
881
|
+
console.log(' forge-ai configure [--platform opencode|claude|codex|grok] [--scope user|project] [--source <dir>] [--dry-run] [--model <id>] [--model-map name=model,...]');
|
|
378
882
|
console.log(' forge-ai validate [--platform opencode|claude|codex|grok|all] [--source <dir>]');
|
|
379
883
|
console.log(' forge-ai self-update [--to <version>] [--dry-run] [--skip-spec-update]');
|
|
380
|
-
console.log(' forge-ai build-plugin [--target claude|codex] [--out <dir>] [--source <dir>] [--dry-run] [--force]');
|
|
381
884
|
console.log('');
|
|
885
|
+
console.log('--model and --model-map require an explicit single --platform (model ids are not portable across platforms).');
|
|
382
886
|
console.log('Global flags: --no-update-check (also FORGE_NO_UPDATE_CHECK=1 or CI=true)');
|
|
383
887
|
}
|
|
384
888
|
function printPlan(command, sourceCount, files, diagnostics, prunePlan) {
|
|
@@ -391,6 +895,8 @@ function printPlan(command, sourceCount, files, diagnostics, prunePlan) {
|
|
|
391
895
|
console.log(`- delete stale ${item.platform} ${item.kind} ${item.name} -> ${item.path} [backup -> ${item.backupPath}]`);
|
|
392
896
|
for (const item of prunePlan.skippedMissing)
|
|
393
897
|
console.log(`- skip missing ${item.platform} ${item.kind} ${item.name} -> ${item.path}`);
|
|
898
|
+
for (const item of prunePlan.skippedUnsafe)
|
|
899
|
+
console.log(`- skip unsafe ${item.platform} ${item.kind} ${item.name} -> ${item.path}`);
|
|
394
900
|
for (const item of diagnostics)
|
|
395
901
|
console.log(formatDiagnostic(item));
|
|
396
902
|
}
|