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