agentsw 0.9.2 → 0.10.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/README.md +42 -1
- package/README.zh-CN.md +38 -1
- package/dist/app-paths.js +127 -0
- package/dist/apps.js +11 -15
- package/dist/commands.js +375 -205
- package/dist/discover.js +21 -11
- package/dist/gateway.js +5 -4
- package/dist/i18n.js +147 -3
- package/dist/index.js +14 -2
- package/dist/menu.js +141 -121
- package/dist/modelsdev.js +51 -21
- package/dist/progress.js +116 -0
- package/dist/provider-actions.js +29 -9
- package/dist/remove.js +15 -30
- package/dist/rename.js +17 -32
- package/dist/report.js +67 -0
- package/dist/store.js +25 -0
- package/dist/targets/claudecode.js +12 -9
- package/dist/targets/codex.js +22 -13
- package/dist/targets/dsh.js +19 -34
- package/dist/targets/hermes.js +11 -20
- package/dist/targets/omp.js +15 -7
- package/dist/targets/opencode.js +42 -25
- package/dist/targets/pistyle.js +9 -20
- package/dist/targets/workbuddy.js +6 -4
- package/dist/ui.js +157 -0
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -8,7 +8,7 @@ import prompts from "prompts";
|
|
|
8
8
|
import { loadStore, saveStore, getProvider, configFile } from "./store.js";
|
|
9
9
|
import { scanCandidates, normalizeUrl, findMatchingProvider } from "./import.js";
|
|
10
10
|
import { loadCatalog, searchCatalog } from "./modelsdev.js";
|
|
11
|
-
import { classifyUnresolved, enrichProviderModels,
|
|
11
|
+
import { classifyUnresolved, enrichProviderModels, resolveMetadataOptions } from "./metadata.js";
|
|
12
12
|
import { loadGatewayCatalog } from "./gateway.js";
|
|
13
13
|
import { resolveTargets, supportsProtocol, targets } from "./targets/index.js";
|
|
14
14
|
import { discoverProviderModels, probeProtocols } from "./discover.js";
|
|
@@ -17,38 +17,16 @@ import { SafeConfigError, drainPendingWrites, readTextIfExists, setDryRun } from
|
|
|
17
17
|
import { applyModelFilter } from "./filter.js";
|
|
18
18
|
import { availableProviderId, providerIdFromBaseUrl, providerNameFromBaseUrl } from "./slug.js";
|
|
19
19
|
import { t } from "./i18n.js";
|
|
20
|
+
import { cancelInteractive, emitJson, error, isJson, note, out, table } from "./ui.js";
|
|
21
|
+
import { withProgress } from "./progress.js";
|
|
22
|
+
import { appsReport, listReport, modelsQueryReport, providerMetadataReport, providerModelsReport, statusReport, } from "./report.js";
|
|
20
23
|
/** Share successes and failures without fetching until a model actually needs the supplement. */
|
|
21
24
|
function sharedGatewayLoader(refresh = false) {
|
|
22
25
|
let pending;
|
|
23
|
-
return () => pending ??= loadGatewayCatalog({ refresh }).then((catalog) => catalog ?? null);
|
|
26
|
+
return () => pending ??= withProgress(t("progress.gateway"), () => loadGatewayCatalog({ refresh }).then((catalog) => catalog ?? null));
|
|
24
27
|
}
|
|
25
28
|
function fail(message) {
|
|
26
|
-
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
function table(rows, header) {
|
|
30
|
-
const all = header ? [header, ...rows] : rows;
|
|
31
|
-
if (all.length === 0)
|
|
32
|
-
return "";
|
|
33
|
-
const widths = [];
|
|
34
|
-
for (const row of all) {
|
|
35
|
-
row.forEach((cell, i) => {
|
|
36
|
-
// eslint-disable-next-line no-control-regex
|
|
37
|
-
const len = cell.replace(/\u001b\[[0-9;]*m/g, "").length;
|
|
38
|
-
widths[i] = Math.max(widths[i] ?? 0, len);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
const render = (row) => row
|
|
42
|
-
.map((cell, i) => {
|
|
43
|
-
const len = cell.replace(/\u001b\[[0-9;]*m/g, "").length;
|
|
44
|
-
return cell + " ".repeat((widths[i] ?? 0) - len);
|
|
45
|
-
})
|
|
46
|
-
.join(" ")
|
|
47
|
-
.trimEnd();
|
|
48
|
-
const lines = all.map(render);
|
|
49
|
-
if (header)
|
|
50
|
-
lines.splice(1, 0, widths.map((w) => "-".repeat(w)).join(" "));
|
|
51
|
-
return lines.join("\n");
|
|
29
|
+
error(message);
|
|
52
30
|
}
|
|
53
31
|
function fmtTokens(n) {
|
|
54
32
|
if (!n)
|
|
@@ -70,6 +48,12 @@ function modelRows(models) {
|
|
|
70
48
|
]);
|
|
71
49
|
}
|
|
72
50
|
const MODEL_HEADER = ["MODEL", "CTX", "IN", "OUT", "REASONING", "$IN/$OUT per M"];
|
|
51
|
+
/** Numbers right-align; the free-text columns shrink before ids on narrow terminals. */
|
|
52
|
+
const MODEL_TABLE_OPTIONS = { align: ["left", "right", "right", "right", "left", "right"], truncate: [4, 0] };
|
|
53
|
+
const MODEL_QUERY_OPTIONS = {
|
|
54
|
+
align: ["left", "left", "right", "right", "right", "left", "right"],
|
|
55
|
+
truncate: [5, 1, 0],
|
|
56
|
+
};
|
|
73
57
|
/**
|
|
74
58
|
* Explain why some models stayed bare: only ambiguous ids are worth the user's
|
|
75
59
|
* attention, so those are named separately from ids nothing can resolve.
|
|
@@ -78,11 +62,11 @@ function printUncatalogedHint(models, catalog, hint) {
|
|
|
78
62
|
const bare = models.filter((m) => m.contextWindow === undefined && !m.cost);
|
|
79
63
|
if (bare.length === 0)
|
|
80
64
|
return;
|
|
81
|
-
|
|
65
|
+
out(pc.dim(t("meta.gap", { count: bare.length })));
|
|
82
66
|
const { ambiguous, unknown, noCreatorRow } = classifyUnresolved(catalog, bare.map((m) => m.id), hint);
|
|
83
67
|
for (const [key, ids] of [["meta.ambiguous", ambiguous], ["meta.noCreatorRow", noCreatorRow], ["meta.unknown", unknown]]) {
|
|
84
68
|
if (ids.length)
|
|
85
|
-
|
|
69
|
+
out(pc.dim(` ${t(key, { count: ids.length, ids: ids.join(", "), id: ids[0] })}`));
|
|
86
70
|
}
|
|
87
71
|
}
|
|
88
72
|
/** Guess the models.dev provider whose API host matches the configured baseUrl. */
|
|
@@ -122,14 +106,14 @@ function parseFilterOpts(opts, previous) {
|
|
|
122
106
|
function reportDropped(dropped) {
|
|
123
107
|
if (!dropped.length)
|
|
124
108
|
return;
|
|
125
|
-
|
|
109
|
+
out(pc.dim(`filtered out ${dropped.length} model(s):`));
|
|
126
110
|
for (const d of dropped)
|
|
127
|
-
|
|
111
|
+
out(pc.dim(` - ${d.id} (${d.reason})`));
|
|
128
112
|
}
|
|
129
113
|
export async function cmdAdd(opts) {
|
|
130
114
|
const store = loadStore();
|
|
131
115
|
resolveMetadataOptions(opts);
|
|
132
|
-
const interactive = process.stdin.isTTY && !opts.yes;
|
|
116
|
+
const interactive = process.stdin.isTTY === true && !opts.yes && !isJson();
|
|
133
117
|
let answers = {};
|
|
134
118
|
if (interactive) {
|
|
135
119
|
prompts.override({
|
|
@@ -177,7 +161,7 @@ export async function cmdAdd(opts) {
|
|
|
177
161
|
name: "models",
|
|
178
162
|
message: t("add.models"),
|
|
179
163
|
},
|
|
180
|
-
], { onCancel: () =>
|
|
164
|
+
], { onCancel: () => cancelInteractive() });
|
|
181
165
|
}
|
|
182
166
|
else {
|
|
183
167
|
const required = opts.discover
|
|
@@ -211,7 +195,7 @@ export async function cmdAdd(opts) {
|
|
|
211
195
|
const sameAccount = byId ? undefined : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey: answers.apiKey });
|
|
212
196
|
const existing = byId ?? sameAccount;
|
|
213
197
|
if (byId === undefined && explicitId && sameAccount) {
|
|
214
|
-
|
|
198
|
+
out(pc.yellow(t("add.alreadyConfigured", { id: pc.bold(sameAccount.id) })));
|
|
215
199
|
}
|
|
216
200
|
const id = byId?.id ?? sameAccount?.id ?? explicitId ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
217
201
|
const openaiApi = protocol === "openai" ? wire ?? existing?.openaiApi : undefined;
|
|
@@ -222,11 +206,30 @@ export async function cmdAdd(opts) {
|
|
|
222
206
|
const shouldDiscover = opts.discover || manualIds.length === 0;
|
|
223
207
|
if (shouldDiscover) {
|
|
224
208
|
if (!opts.discover) {
|
|
225
|
-
|
|
209
|
+
note(t("add.autoDiscover"));
|
|
210
|
+
}
|
|
211
|
+
const discover = () => withProgress(t("add.discovering", { url: baseUrl }), (progress) => discoverProviderModels({ baseUrl, apiKey: answers.apiKey, protocol }, {
|
|
212
|
+
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: baseUrl, page, count })),
|
|
213
|
+
}));
|
|
214
|
+
let discovered = [];
|
|
215
|
+
for (let attempt = 0;; attempt++) {
|
|
216
|
+
try {
|
|
217
|
+
discovered = await discover();
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
// Unattended runs keep failing fast; a person at the terminal gets a bounded retry, then manual ids.
|
|
222
|
+
if (!interactive)
|
|
223
|
+
throw err;
|
|
224
|
+
if (attempt >= 2)
|
|
225
|
+
break;
|
|
226
|
+
const { retry } = await prompts({ type: "toggle", name: "retry", message: t("add.retryDiscovery", { error: err.message }), initial: true }, { onCancel: () => cancelInteractive() });
|
|
227
|
+
if (!retry)
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
226
230
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
console.log(t("add.providerLists", { count: discovered.length }));
|
|
231
|
+
if (discovered.length)
|
|
232
|
+
out(t("add.providerLists", { count: discovered.length }));
|
|
230
233
|
modelIds = [...new Set([...manualIds, ...discovered])];
|
|
231
234
|
}
|
|
232
235
|
const retained = [opts.defaultModel === undefined ? existing?.defaultModel : undefined, opts.smallModel === undefined ? existing?.smallModel : undefined].filter((id) => !!id);
|
|
@@ -237,9 +240,9 @@ export async function cmdAdd(opts) {
|
|
|
237
240
|
modelIds = outcome.kept;
|
|
238
241
|
if (modelIds.length === 0)
|
|
239
242
|
fail(t("add.atLeastOne"));
|
|
240
|
-
const catalog = await loadCatalog();
|
|
243
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
241
244
|
const hint = opts.modelsDev ?? existing?.modelsDevId ?? guessProviderHint(catalog, answers.baseUrl);
|
|
242
|
-
const models = await enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: existing?.models });
|
|
245
|
+
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: existing?.models }));
|
|
243
246
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
244
247
|
const defaultModel = opts.defaultModel ?? existing?.defaultModel ?? modelIds[0];
|
|
245
248
|
if (!modelIds.includes(defaultModel))
|
|
@@ -268,19 +271,23 @@ export async function cmdAdd(opts) {
|
|
|
268
271
|
store.active = provider.id;
|
|
269
272
|
saveStore(store);
|
|
270
273
|
const savedStatus = t(existed ? "add.updated" : "add.added");
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
+
if (isJson()) {
|
|
275
|
+
emitJson({ ...providerModelsReport(provider, false), created: !existed, matched });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
out(pc.green(t("add.saved", { status: savedStatus, id: pc.bold(provider.id), protocol })));
|
|
279
|
+
out(t("add.metadata", { matched, total: models.length }) + (hint ? t("add.providerHint", { hint }) : ""));
|
|
280
|
+
out(table(modelRows(models), MODEL_HEADER, MODEL_TABLE_OPTIONS));
|
|
274
281
|
printUncatalogedHint(models, catalog, hint);
|
|
275
|
-
|
|
282
|
+
note(`\n${t("add.next", { id: provider.id })}`);
|
|
276
283
|
}
|
|
277
284
|
/** Shared logic to create and save a single provider from discovered models. */
|
|
278
285
|
async function createProvider(opts) {
|
|
279
286
|
const { store, id, name, protocol, openaiApi, baseUrl, apiKey, modelIds, modelFilter } = opts;
|
|
280
287
|
const metadataOptions = resolveMetadataOptions(opts, opts.existing);
|
|
281
|
-
const catalog = await loadCatalog();
|
|
288
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
282
289
|
const hint = opts.modelsDev ?? opts.existing?.modelsDevId ?? guessProviderHint(catalog, baseUrl);
|
|
283
|
-
const models = await enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: opts.existing?.models }, { gatewayLoader: opts.gatewayLoader });
|
|
290
|
+
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: opts.existing?.models }, { gatewayLoader: opts.gatewayLoader }));
|
|
284
291
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
285
292
|
const defaultModel = opts.defaultModel ?? opts.existing?.defaultModel ?? modelIds[0];
|
|
286
293
|
if (!modelIds.includes(defaultModel))
|
|
@@ -309,9 +316,9 @@ async function createProvider(opts) {
|
|
|
309
316
|
if (!store.active)
|
|
310
317
|
store.active = provider.id;
|
|
311
318
|
const savedStatus = t(existed ? "add.updated" : "add.added");
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
319
|
+
out(pc.green(t("add.saved", { status: savedStatus, id: pc.bold(provider.id), protocol })));
|
|
320
|
+
out(t("add.metadata", { matched, total: models.length }) + (hint ? t("add.providerHint", { hint }) : ""));
|
|
321
|
+
out(table(modelRows(models), MODEL_HEADER, MODEL_TABLE_OPTIONS));
|
|
315
322
|
printUncatalogedHint(models, catalog, hint);
|
|
316
323
|
return provider;
|
|
317
324
|
}
|
|
@@ -323,14 +330,14 @@ async function createProvider(opts) {
|
|
|
323
330
|
export async function cmdQuickAdd(opts) {
|
|
324
331
|
const store = loadStore();
|
|
325
332
|
resolveMetadataOptions(opts);
|
|
326
|
-
const interactive = process.stdin.isTTY && !opts.yes;
|
|
333
|
+
const interactive = process.stdin.isTTY === true && !opts.yes && !isJson();
|
|
327
334
|
let baseUrl;
|
|
328
335
|
let apiKey;
|
|
329
336
|
if (interactive) {
|
|
330
337
|
const answers = await prompts([
|
|
331
338
|
{ type: "text", name: "baseUrl", message: t("add.baseUrl"), validate: (v) => (/^https?:\/\//.test(v) ? true : t("add.baseUrlInvalid")) },
|
|
332
339
|
{ type: "password", name: "apiKey", message: t("add.apiKey") },
|
|
333
|
-
], { onCancel: () =>
|
|
340
|
+
], { onCancel: () => cancelInteractive() });
|
|
334
341
|
baseUrl = normalizeUrl(answers.baseUrl);
|
|
335
342
|
apiKey = answers.apiKey;
|
|
336
343
|
}
|
|
@@ -342,8 +349,25 @@ export async function cmdQuickAdd(opts) {
|
|
|
342
349
|
baseUrl = normalizeUrl(opts.baseUrl);
|
|
343
350
|
apiKey = opts.apiKey;
|
|
344
351
|
}
|
|
345
|
-
|
|
346
|
-
|
|
352
|
+
const probe = () => withProgress(t("quick.probing", { url: baseUrl }), (progress) => probeProtocols({ baseUrl, apiKey }, {
|
|
353
|
+
onAttempt: (protocol) => progress.update(t("progress.probe", { url: baseUrl, protocol })),
|
|
354
|
+
}));
|
|
355
|
+
let protocols = await probe();
|
|
356
|
+
// A typo in the endpoint is the common cause of "nothing answered"; let the user correct it instead of exiting.
|
|
357
|
+
for (let attempt = 0; interactive && protocols.length === 0 && attempt < 2; attempt++) {
|
|
358
|
+
const { retry } = await prompts({ type: "toggle", name: "retry", message: t("quick.retryProbe", { url: baseUrl }), initial: true }, { onCancel: () => cancelInteractive() });
|
|
359
|
+
if (!retry)
|
|
360
|
+
break;
|
|
361
|
+
const again = await prompts([
|
|
362
|
+
{ type: "text", name: "baseUrl", message: t("add.baseUrl"), initial: baseUrl, validate: (v) => (/^https?:\/\//.test(v) ? true : t("add.baseUrlInvalid")) },
|
|
363
|
+
{ type: "password", name: "apiKey", message: t("add.apiKey") },
|
|
364
|
+
], { onCancel: () => cancelInteractive() });
|
|
365
|
+
if (!again.baseUrl || !again.apiKey)
|
|
366
|
+
break;
|
|
367
|
+
baseUrl = normalizeUrl(again.baseUrl);
|
|
368
|
+
apiKey = again.apiKey;
|
|
369
|
+
protocols = await probe();
|
|
370
|
+
}
|
|
347
371
|
if (protocols.length === 0) {
|
|
348
372
|
fail(t("quick.noProtocol"));
|
|
349
373
|
}
|
|
@@ -358,14 +382,15 @@ export async function cmdQuickAdd(opts) {
|
|
|
358
382
|
const sameAccount = byId ? undefined : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey });
|
|
359
383
|
const existing = byId ?? sameAccount;
|
|
360
384
|
if (byId === undefined && explicitId && sameAccount) {
|
|
361
|
-
|
|
385
|
+
out(pc.yellow(t("add.alreadyConfigured", { id: pc.bold(sameAccount.id) })));
|
|
362
386
|
}
|
|
363
387
|
const id = byId?.id ?? sameAccount?.id ?? explicitId ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
364
388
|
const name = opts.name ?? existing?.name ?? (opts.id === undefined ? providerNameFromBaseUrl(baseUrl, protocol) : multi ? `${opts.id} (${protocol})` : opts.id);
|
|
365
389
|
const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
390
|
+
const discovered = await withProgress(`${t("add.discovering", { url: baseUrl })} [${protocol}]`, (progress) => discoverProviderModels({ baseUrl, apiKey, protocol }, {
|
|
391
|
+
onPage: ({ page, count }) => progress.update(`${t("progress.discoverPage", { url: baseUrl, page, count })} [${protocol}]`),
|
|
392
|
+
}));
|
|
393
|
+
out(t("add.providerLists", { count: discovered.length }));
|
|
369
394
|
const retained = [opts.defaultModel === undefined ? existing?.defaultModel : undefined, opts.smallModel === undefined ? existing?.smallModel : undefined].filter((id) => !!id);
|
|
370
395
|
let modelIds = [...new Set([...discovered, ...retained])];
|
|
371
396
|
const pinned = [...retained, ...(opts.defaultModel ? [opts.defaultModel] : []), ...(opts.smallModel ? [opts.smallModel] : [])];
|
|
@@ -373,7 +398,7 @@ export async function cmdQuickAdd(opts) {
|
|
|
373
398
|
reportDropped(outcome.dropped);
|
|
374
399
|
modelIds = outcome.kept;
|
|
375
400
|
if (modelIds.length === 0) {
|
|
376
|
-
|
|
401
|
+
out(pc.yellow(t("quick.noModelsAfterFilter", { id })));
|
|
377
402
|
continue;
|
|
378
403
|
}
|
|
379
404
|
const provider = await createProvider({
|
|
@@ -397,18 +422,26 @@ export async function cmdQuickAdd(opts) {
|
|
|
397
422
|
gatewayLoader,
|
|
398
423
|
});
|
|
399
424
|
createdIds.push(provider.id);
|
|
400
|
-
|
|
425
|
+
note(`\n${t("add.next", { id: provider.id })}`);
|
|
401
426
|
}
|
|
402
427
|
saveStore(store);
|
|
428
|
+
if (isJson()) {
|
|
429
|
+
emitJson({ providers: createdIds });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
403
432
|
if (createdIds.length > 0) {
|
|
404
|
-
|
|
433
|
+
out(pc.green(t("quick.summary", { count: createdIds.length, ids: createdIds.join(", ") })));
|
|
405
434
|
}
|
|
406
435
|
}
|
|
407
436
|
export function cmdList() {
|
|
408
437
|
const store = loadStore();
|
|
438
|
+
if (isJson()) {
|
|
439
|
+
emitJson(listReport(store));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
409
442
|
const ids = Object.keys(store.providers);
|
|
410
443
|
if (ids.length === 0) {
|
|
411
|
-
|
|
444
|
+
out(t("list.none", { file: configFile }));
|
|
412
445
|
return;
|
|
413
446
|
}
|
|
414
447
|
const rows = ids.map((id) => {
|
|
@@ -422,23 +455,37 @@ export function cmdList() {
|
|
|
422
455
|
String(p.models.length),
|
|
423
456
|
];
|
|
424
457
|
});
|
|
425
|
-
|
|
458
|
+
out(table(rows, [" ", "ID", t("table.protocol"), "BASE URL", t("table.defaultModel"), t("table.models")], {
|
|
459
|
+
truncate: [3, 1, 4],
|
|
460
|
+
align: ["left", "left", "left", "left", "left", "right"],
|
|
461
|
+
}));
|
|
426
462
|
}
|
|
427
463
|
export async function cmdPrune(id, opts) {
|
|
428
464
|
const store = loadStore();
|
|
429
465
|
const provider = getProvider(store, id);
|
|
430
|
-
|
|
431
|
-
|
|
466
|
+
out(`${t("remove.pruning", { id: pc.bold(id) })}\n`);
|
|
467
|
+
const results = await runTargets("prune", provider, opts.apps);
|
|
468
|
+
if (isJson()) {
|
|
469
|
+
emitJson({
|
|
470
|
+
provider: id,
|
|
471
|
+
op: "prune",
|
|
472
|
+
apps: results.filter((r) => !r.skipped).map((r) => r.app),
|
|
473
|
+
files: results.flatMap((r) => (r.skipped ? [] : r.changed)),
|
|
474
|
+
skipped: results.filter((r) => r.skipped).map((r) => ({ app: r.app, reason: r.skipped })),
|
|
475
|
+
});
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
reportResults(results);
|
|
432
479
|
}
|
|
433
480
|
function reportResults(results) {
|
|
434
481
|
for (const r of results) {
|
|
435
482
|
if (r.skipped) {
|
|
436
|
-
|
|
483
|
+
out(`${pc.yellow(t("common.skip"))} ${r.app.padEnd(9)} ${pc.dim(r.skipped)}`);
|
|
437
484
|
continue;
|
|
438
485
|
}
|
|
439
|
-
|
|
486
|
+
out(`${pc.green("ok ")} ${r.app.padEnd(9)} ${r.changed.join(", ")}`);
|
|
440
487
|
for (const note of r.notes)
|
|
441
|
-
|
|
488
|
+
out(` ${" ".repeat(9)} ${pc.dim(note)}`);
|
|
442
489
|
}
|
|
443
490
|
}
|
|
444
491
|
async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
@@ -464,7 +511,7 @@ async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
|
464
511
|
}
|
|
465
512
|
catch (err) {
|
|
466
513
|
const safe = redactErrors && !(err instanceof SafeConfigError);
|
|
467
|
-
results.push({ app: target.id, changed: [], notes: [], skipped: pc.red(`failed: ${safe ? "
|
|
514
|
+
results.push({ app: target.id, changed: [], notes: [], skipped: pc.red(`failed: ${safe ? t("preview.failedSafely") : err.message}`) });
|
|
468
515
|
process.exitCode = 1;
|
|
469
516
|
}
|
|
470
517
|
}
|
|
@@ -501,10 +548,10 @@ function previewDocument(file, text) {
|
|
|
501
548
|
function printFileDiff(file, next, apiKey) {
|
|
502
549
|
const before = readTextIfExists(file) ?? "";
|
|
503
550
|
if (before === next) {
|
|
504
|
-
|
|
551
|
+
out(`${pc.dim(t("preview.unchanged"))} ${file}`);
|
|
505
552
|
return;
|
|
506
553
|
}
|
|
507
|
-
|
|
554
|
+
out(pc.bold(t("preview.header", { file })));
|
|
508
555
|
let oldText;
|
|
509
556
|
let newText;
|
|
510
557
|
try {
|
|
@@ -554,7 +601,7 @@ function printFileDiff(file, next, apiKey) {
|
|
|
554
601
|
newText = JSON.stringify(newDocument, redact, 2) ?? "";
|
|
555
602
|
}
|
|
556
603
|
catch {
|
|
557
|
-
|
|
604
|
+
out(pc.dim(t("preview.contentWithheld")));
|
|
558
605
|
return;
|
|
559
606
|
}
|
|
560
607
|
const oldLines = oldText.split("\n");
|
|
@@ -570,22 +617,28 @@ function printFileDiff(file, next, apiKey) {
|
|
|
570
617
|
if (count > 0)
|
|
571
618
|
newSeen.set(line, count - 1);
|
|
572
619
|
else
|
|
573
|
-
|
|
620
|
+
out(pc.red(`- ${line}`));
|
|
574
621
|
}
|
|
575
622
|
for (const line of newLines) {
|
|
576
623
|
const count = oldSeen.get(line) ?? 0;
|
|
577
624
|
if (count > 0)
|
|
578
625
|
oldSeen.set(line, count - 1);
|
|
579
626
|
else
|
|
580
|
-
|
|
627
|
+
out(pc.green(`+ ${line}`));
|
|
581
628
|
}
|
|
582
629
|
if (oldText === newText)
|
|
583
|
-
|
|
630
|
+
out(pc.dim(t("preview.onlyRedacted")));
|
|
584
631
|
}
|
|
585
632
|
async function runWithOptionalDryRun(op, provider, apps, dryRun) {
|
|
633
|
+
const summarize = (results, files) => ({
|
|
634
|
+
apps: results.map((r) => r.app),
|
|
635
|
+
files,
|
|
636
|
+
skipped: results.filter((r) => r.skipped).map((r) => ({ app: r.app, reason: r.skipped })),
|
|
637
|
+
});
|
|
586
638
|
if (!dryRun) {
|
|
587
|
-
|
|
588
|
-
|
|
639
|
+
const results = await runTargets(op, provider, apps);
|
|
640
|
+
reportResults(results);
|
|
641
|
+
return summarize(results, results.flatMap((r) => (r.skipped ? [] : r.changed)));
|
|
589
642
|
}
|
|
590
643
|
setDryRun(true);
|
|
591
644
|
try {
|
|
@@ -593,11 +646,13 @@ async function runWithOptionalDryRun(op, provider, apps, dryRun) {
|
|
|
593
646
|
const writes = drainPendingWrites();
|
|
594
647
|
for (const r of results) {
|
|
595
648
|
if (r.skipped)
|
|
596
|
-
|
|
649
|
+
out(`${pc.yellow(t("dryRun.skip"))} ${r.app.padEnd(9)} ${pc.dim(r.skipped)}`);
|
|
597
650
|
}
|
|
598
|
-
|
|
651
|
+
out(pc.bold(`\n${t("dryRun.wouldWrite", { count: writes.length })}\n`));
|
|
599
652
|
for (const w of writes)
|
|
600
653
|
printFileDiff(w.file, w.content, provider.apiKey);
|
|
654
|
+
// Only paths: the staged content is redacted for humans but still configuration, not a machine interface.
|
|
655
|
+
return summarize(results, writes.map((w) => w.file));
|
|
601
656
|
}
|
|
602
657
|
finally {
|
|
603
658
|
setDryRun(false);
|
|
@@ -617,8 +672,11 @@ export async function cmdUse(id, opts) {
|
|
|
617
672
|
store.active = id;
|
|
618
673
|
saveStore(store);
|
|
619
674
|
}
|
|
620
|
-
|
|
621
|
-
await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun);
|
|
675
|
+
out(`${t("use.switching", { id: pc.bold(id), protocol: provider.protocol, model: provider.defaultModel })}\n`);
|
|
676
|
+
const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun);
|
|
677
|
+
if (isJson()) {
|
|
678
|
+
emitJson({ provider: id, model: provider.defaultModel, dryRun: opts.dryRun === true, ...outcome });
|
|
679
|
+
}
|
|
622
680
|
}
|
|
623
681
|
export async function cmdSync(opts) {
|
|
624
682
|
resolveTargets(opts.apps);
|
|
@@ -627,59 +685,85 @@ export async function cmdSync(opts) {
|
|
|
627
685
|
if (!id)
|
|
628
686
|
fail(t("sync.noActive"));
|
|
629
687
|
const provider = getProvider(store, id);
|
|
630
|
-
|
|
631
|
-
await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun);
|
|
688
|
+
out(`${t("sync.syncing", { id: pc.bold(id), model: provider.defaultModel })}\n`);
|
|
689
|
+
const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun);
|
|
690
|
+
if (isJson()) {
|
|
691
|
+
emitJson({ provider: id, model: provider.defaultModel, dryRun: opts.dryRun === true, ...outcome });
|
|
692
|
+
}
|
|
632
693
|
}
|
|
633
694
|
export function cmdStatus() {
|
|
634
695
|
const store = loadStore();
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
target.
|
|
639
|
-
target.
|
|
640
|
-
target.
|
|
641
|
-
|
|
642
|
-
|
|
696
|
+
const apps = targets.map((target) => ({
|
|
697
|
+
id: target.id,
|
|
698
|
+
detected: target.detect(),
|
|
699
|
+
protocols: [...target.protocols],
|
|
700
|
+
current: target.current() ?? null,
|
|
701
|
+
configPath: target.configPaths[0] ?? "",
|
|
702
|
+
}));
|
|
703
|
+
if (isJson()) {
|
|
704
|
+
emitJson(statusReport(configFile, store, apps));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
out(t("status.config", { file: configFile }));
|
|
708
|
+
out(`${t("status.active", { id: store.active ? pc.bold(store.active) : pc.dim(t("status.none")) })}\n`);
|
|
709
|
+
const rows = apps.map((app) => [
|
|
710
|
+
app.id,
|
|
711
|
+
app.detected ? pc.green(t("common.yes")) : pc.dim(t("common.no")),
|
|
712
|
+
app.protocols.join("+"),
|
|
713
|
+
app.current ?? pc.dim("-"),
|
|
714
|
+
pc.dim(app.configPath),
|
|
643
715
|
]);
|
|
644
|
-
|
|
716
|
+
out(table(rows, ["APP", t("table.found"), t("table.protocols"), t("table.current"), t("table.config")], {
|
|
717
|
+
truncate: [4, 3],
|
|
718
|
+
}));
|
|
719
|
+
}
|
|
720
|
+
/** `--limit` is user input: a non-numeric or non-positive value must fail, not silently list everything. */
|
|
721
|
+
function parseLimit(value) {
|
|
722
|
+
const limit = value === undefined ? 30 : Number(value);
|
|
723
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
724
|
+
fail(t("error.limit"));
|
|
725
|
+
return limit;
|
|
645
726
|
}
|
|
646
727
|
export async function cmdModels(query, opts) {
|
|
647
728
|
if (opts.metadata && !opts.provider)
|
|
648
|
-
fail("
|
|
729
|
+
fail(t("models.metadataRequiresProvider"));
|
|
649
730
|
if (opts.provider) {
|
|
650
731
|
const store = loadStore();
|
|
651
732
|
const provider = getProvider(store, opts.provider);
|
|
733
|
+
if (isJson()) {
|
|
734
|
+
emitJson(providerModelsReport(provider, opts.metadata === true));
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
// Kept as a bare document without --json: existing scripts parse it directly.
|
|
652
738
|
if (opts.metadata) {
|
|
653
|
-
|
|
654
|
-
provider: provider.id,
|
|
655
|
-
gatewayMetadata: provider.gatewayMetadata ?? "auto",
|
|
656
|
-
metadataMode: getMetadataMode(provider),
|
|
657
|
-
gatewayModelAliases: provider.gatewayModelAliases ?? {},
|
|
658
|
-
models: provider.models.map(({ id, metadata }) => ({ id, metadata })),
|
|
659
|
-
}, null, 2));
|
|
739
|
+
out(JSON.stringify(providerMetadataReport(provider), null, 2));
|
|
660
740
|
return;
|
|
661
741
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
printUncatalogedHint(provider.models, await loadCatalog(), provider.modelsDevId);
|
|
742
|
+
out(`${pc.bold(provider.id)} (${provider.protocol}) · ${provider.baseUrl}`);
|
|
743
|
+
out(table(modelRows(provider.models), MODEL_HEADER, MODEL_TABLE_OPTIONS));
|
|
744
|
+
printUncatalogedHint(provider.models, await withProgress(t("progress.catalog"), () => loadCatalog()), provider.modelsDevId);
|
|
665
745
|
return;
|
|
666
746
|
}
|
|
667
|
-
const catalog = await loadCatalog({ refresh: opts.refresh });
|
|
747
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog({ refresh: opts.refresh }));
|
|
668
748
|
if (!catalog)
|
|
669
|
-
fail("models.
|
|
749
|
+
fail(t("models.catalogUnavailable"));
|
|
670
750
|
if (!query)
|
|
671
|
-
fail("usage
|
|
672
|
-
const limit =
|
|
751
|
+
fail(t("models.usage"));
|
|
752
|
+
const limit = parseLimit(opts.limit);
|
|
673
753
|
const hits = searchCatalog(catalog, query, limit);
|
|
754
|
+
if (isJson()) {
|
|
755
|
+
emitJson(modelsQueryReport(query, hits));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
674
758
|
if (hits.length === 0) {
|
|
675
|
-
|
|
759
|
+
out(t("models.noMatch", { query }));
|
|
676
760
|
return;
|
|
677
761
|
}
|
|
678
762
|
const rows = hits.map((h) => {
|
|
679
763
|
const row = modelRows([h.spec])[0];
|
|
680
764
|
return [h.provider, ...row];
|
|
681
765
|
});
|
|
682
|
-
|
|
766
|
+
out(table(rows, ["PROVIDER", ...MODEL_HEADER], MODEL_QUERY_OPTIONS));
|
|
683
767
|
}
|
|
684
768
|
export async function cmdRefreshMeta(opts = {}) {
|
|
685
769
|
const store = loadStore();
|
|
@@ -687,22 +771,26 @@ export async function cmdRefreshMeta(opts = {}) {
|
|
|
687
771
|
resolveMetadataOptions(opts);
|
|
688
772
|
for (const provider of providers)
|
|
689
773
|
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
690
|
-
const catalog = await loadCatalog({ refresh: true });
|
|
774
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog({ refresh: true }));
|
|
691
775
|
const gatewayLoader = sharedGatewayLoader(true);
|
|
692
776
|
let updated = 0;
|
|
693
777
|
for (const provider of providers) {
|
|
694
778
|
const before = JSON.stringify(provider.models);
|
|
695
|
-
provider.models = await enrichProviderModels(catalog, provider.models.map((m) => m.id), {
|
|
779
|
+
provider.models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, provider.models.map((m) => m.id), {
|
|
696
780
|
...provider,
|
|
697
781
|
modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl),
|
|
698
|
-
}, { gatewayLoader });
|
|
782
|
+
}, { gatewayLoader }));
|
|
699
783
|
if (JSON.stringify(provider.models) !== before)
|
|
700
784
|
updated++;
|
|
701
785
|
}
|
|
702
786
|
saveStore(store);
|
|
703
|
-
|
|
787
|
+
if (isJson()) {
|
|
788
|
+
emitJson({ checked: providers.length, updated, providers: providers.map((p) => p.id) });
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
out(pc.green(t("refresh.checked", { changed: updated })));
|
|
704
792
|
if (updated > 0)
|
|
705
|
-
|
|
793
|
+
note(t("refresh.next"));
|
|
706
794
|
for (const provider of providers)
|
|
707
795
|
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
708
796
|
}
|
|
@@ -717,133 +805,182 @@ export async function cmdDiscover(id, opts) {
|
|
|
717
805
|
provider.modelFilter = undefined;
|
|
718
806
|
else if (flagFilter)
|
|
719
807
|
provider.modelFilter = flagFilter;
|
|
720
|
-
|
|
721
|
-
|
|
808
|
+
const listed = await withProgress(t("add.discovering", { url: provider.baseUrl }), (progress) => discoverProviderModels(provider, {
|
|
809
|
+
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: provider.baseUrl, page, count })),
|
|
810
|
+
}));
|
|
722
811
|
const pinned = [provider.defaultModel, provider.smallModel].filter((model) => !!model);
|
|
723
812
|
const outcome = applyModelFilter(listed, provider.modelFilter, pinned);
|
|
724
813
|
reportDropped(outcome.dropped);
|
|
725
814
|
const ids = outcome.kept;
|
|
815
|
+
// Pinned ids survive a listing that dropped them, so they are not "removed upstream".
|
|
816
|
+
const retainedIds = [...new Set([...ids, ...pinned])];
|
|
817
|
+
const retained = new Set(retainedIds);
|
|
726
818
|
const known = provider.models.map((m) => m.id);
|
|
727
819
|
const added = ids.filter((m) => !known.includes(m));
|
|
728
|
-
const gone = known.filter((m) => !
|
|
729
|
-
const catalog = await loadCatalog();
|
|
730
|
-
|
|
731
|
-
provider.models = await enrichProviderModels(catalog, retainedIds, { ...provider, modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl) });
|
|
820
|
+
const gone = known.filter((m) => !retained.has(m));
|
|
821
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
822
|
+
provider.models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, retainedIds, { ...provider, modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl) }));
|
|
732
823
|
if (!ids.includes(provider.defaultModel)) {
|
|
733
|
-
|
|
824
|
+
out(pc.yellow(t("discover.defaultMissing", { model: provider.defaultModel })));
|
|
734
825
|
}
|
|
735
826
|
saveStore(store);
|
|
736
|
-
|
|
827
|
+
if (isJson()) {
|
|
828
|
+
emitJson({ provider: id, models: ids.length, added, removed: gone });
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
out(`${pc.bold(id)}: ${ids.length} models (${pc.green(`+${added.length}`)} / ${pc.red(`-${gone.length}`)})` +
|
|
737
832
|
(added.length ? `\n new: ${added.join(", ")}` : "") +
|
|
738
833
|
(gone.length ? `\n removed upstream: ${gone.join(", ")}` : ""));
|
|
739
|
-
|
|
834
|
+
out(table(modelRows(provider.models), MODEL_HEADER, MODEL_TABLE_OPTIONS));
|
|
740
835
|
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
741
836
|
if (opts.sync) {
|
|
742
|
-
|
|
837
|
+
out("");
|
|
743
838
|
await cmdSync({ provider: id, apps: opts.apps });
|
|
744
839
|
}
|
|
745
840
|
else {
|
|
746
|
-
|
|
841
|
+
note(`\n${t("discover.next")}`);
|
|
747
842
|
}
|
|
748
843
|
}
|
|
749
|
-
async function collectAppRows() {
|
|
844
|
+
async function collectAppRows(onProgress) {
|
|
845
|
+
const total = appPackages.length;
|
|
846
|
+
let done = 0;
|
|
750
847
|
return Promise.all(appPackages.map(async (app) => {
|
|
751
|
-
|
|
752
|
-
Promise.
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
848
|
+
try {
|
|
849
|
+
const [installedResult, latestResult] = await Promise.allSettled([
|
|
850
|
+
Promise.resolve().then(() => installedVersion(app)),
|
|
851
|
+
latestVersion(app),
|
|
852
|
+
]);
|
|
853
|
+
const installed = installedResult.status === "fulfilled" ? installedResult.value : "?";
|
|
854
|
+
const latest = latestResult.status === "fulfilled" ? latestResult.value : undefined;
|
|
855
|
+
const knownInstalled = normalizeAppVersion(installed);
|
|
856
|
+
const knownLatest = normalizeAppVersion(latest);
|
|
857
|
+
const checkFailed = installedResult.status === "rejected"
|
|
858
|
+
? t("apps.checkFailedProbe")
|
|
859
|
+
: installed && !knownInstalled
|
|
860
|
+
? t("apps.checkFailedUnknown")
|
|
861
|
+
: installed && !knownLatest
|
|
862
|
+
? t("apps.checkFailedLatest")
|
|
863
|
+
: undefined;
|
|
864
|
+
return {
|
|
865
|
+
id: app.id,
|
|
866
|
+
name: app.name,
|
|
867
|
+
installed,
|
|
868
|
+
latest,
|
|
869
|
+
upgradable: !!knownInstalled && !!knownLatest && isNewer(knownInstalled, knownLatest),
|
|
870
|
+
installable: !installed && !!appCommand(app, "install"),
|
|
871
|
+
checkFailed,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
finally {
|
|
875
|
+
onProgress?.(++done, total);
|
|
876
|
+
}
|
|
775
877
|
}));
|
|
776
878
|
}
|
|
879
|
+
function appStatus(row) {
|
|
880
|
+
if (row.upgradable)
|
|
881
|
+
return "upgradable";
|
|
882
|
+
if (!row.installed)
|
|
883
|
+
return row.installable ? "installable" : "not-installed";
|
|
884
|
+
return row.checkFailed || !normalizeAppVersion(row.latest) ? "unknown" : "up-to-date";
|
|
885
|
+
}
|
|
886
|
+
function appStatusLabel(status) {
|
|
887
|
+
switch (status) {
|
|
888
|
+
case "upgradable": return pc.yellow(t("apps.upgradeAvailable"));
|
|
889
|
+
case "installable": return pc.dim(t("apps.installable"));
|
|
890
|
+
case "not-installed": return pc.dim("-");
|
|
891
|
+
case "unknown": return pc.dim(t("apps.unknown"));
|
|
892
|
+
case "up-to-date": return pc.green(t("apps.upToDate"));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
777
895
|
export async function cmdApps() {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
896
|
+
const rows = await withProgress(t("apps.checking"), (progress) => collectAppRows((done, total) => progress.update(t("progress.apps", { done, total }))));
|
|
897
|
+
if (isJson()) {
|
|
898
|
+
const report = rows.map((row) => ({
|
|
899
|
+
id: row.id,
|
|
900
|
+
name: row.name,
|
|
901
|
+
installed: row.installed,
|
|
902
|
+
latest: row.latest,
|
|
903
|
+
upgradable: row.upgradable,
|
|
904
|
+
installable: row.installable,
|
|
905
|
+
status: appStatus(row),
|
|
906
|
+
}));
|
|
907
|
+
emitJson(appsReport(report));
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
out(table(rows.map((r) => [
|
|
781
911
|
r.id,
|
|
782
|
-
r.installed ?? pc.dim("
|
|
912
|
+
r.installed ?? pc.dim(t("apps.notInstalled")),
|
|
783
913
|
r.latest ?? pc.dim("?"),
|
|
784
|
-
r
|
|
785
|
-
? pc.yellow("upgrade available")
|
|
786
|
-
: !r.installed
|
|
787
|
-
? r.installable
|
|
788
|
-
? pc.dim("installable")
|
|
789
|
-
: pc.dim("-")
|
|
790
|
-
: r.checkFailed || !normalizeAppVersion(r.latest)
|
|
791
|
-
? pc.dim("unknown")
|
|
792
|
-
: pc.green("up to date"),
|
|
914
|
+
appStatusLabel(appStatus(r)),
|
|
793
915
|
]), ["APP", "INSTALLED", "LATEST", "STATUS"]));
|
|
794
916
|
const upgradable = rows.filter((r) => r.upgradable).map((r) => r.id);
|
|
795
917
|
if (upgradable.length)
|
|
796
|
-
|
|
918
|
+
note(`\n${t("apps.upgradeWith", { ids: upgradable.join(" ") })}`);
|
|
797
919
|
}
|
|
798
920
|
export async function cmdInstall(id) {
|
|
799
921
|
const app = appPackages.find((a) => a.id === id);
|
|
800
922
|
if (!app)
|
|
801
|
-
fail(
|
|
923
|
+
fail(t("install.unknownApp", { value: id, apps: appPackages.map((a) => a.id).join(", ") }));
|
|
802
924
|
const installCmd = appCommand(app, "install");
|
|
803
925
|
if (!installCmd)
|
|
804
|
-
fail(
|
|
926
|
+
fail(t("install.notInstallable", { name: app.name, platform: process.platform }));
|
|
805
927
|
const installed = installedVersion(app);
|
|
806
928
|
if (installed) {
|
|
807
|
-
|
|
929
|
+
if (isJson()) {
|
|
930
|
+
emitJson({ app: id, status: "already-installed", version: installed });
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
out(t("install.already", { name: app.name, version: installed, id }));
|
|
808
934
|
return;
|
|
809
935
|
}
|
|
810
|
-
|
|
936
|
+
out(t("install.installing", { name: app.name, command: pc.dim(installCmd) }));
|
|
811
937
|
runShell(installCmd);
|
|
812
938
|
const detected = installedVersion(app);
|
|
813
939
|
if (!detected)
|
|
814
|
-
throw new Error(
|
|
940
|
+
throw new Error(t("install.notDetected", { name: app.name }));
|
|
815
941
|
const version = normalizeAppVersion(detected);
|
|
816
942
|
if (!version) {
|
|
817
|
-
|
|
943
|
+
if (isJson())
|
|
944
|
+
error(t("install.versionUnknown", { name: app.name }));
|
|
945
|
+
out(pc.yellow(t("install.versionUnknown", { name: app.name })));
|
|
818
946
|
process.exitCode = 1;
|
|
819
947
|
return;
|
|
820
948
|
}
|
|
821
|
-
|
|
949
|
+
if (isJson()) {
|
|
950
|
+
emitJson({ app: id, status: "installed", version });
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
out(pc.green(t("install.installed", { name: app.name, version })));
|
|
822
954
|
}
|
|
823
955
|
export async function cmdUpgrade(ids) {
|
|
824
956
|
let selected = appPackages.filter((a) => ids.length === 0 || ids.includes(a.id));
|
|
825
957
|
const unknown = ids.filter((id) => !appPackages.some((a) => a.id === id));
|
|
826
958
|
if (unknown.length)
|
|
827
|
-
fail(
|
|
959
|
+
fail(t("upgrade.unknownApps", { ids: unknown.join(", ") }));
|
|
828
960
|
const expectedVersions = new Map();
|
|
961
|
+
const upgraded = [];
|
|
962
|
+
const skipped = [];
|
|
829
963
|
if (ids.length === 0) {
|
|
830
964
|
// no args: upgrade everything that is installed and outdated
|
|
831
|
-
|
|
832
|
-
const rows = await collectAppRows();
|
|
965
|
+
const rows = await withProgress(t("upgrade.checking"), (progress) => collectAppRows((done, total) => progress.update(t("progress.apps", { done, total }))));
|
|
833
966
|
const managed = rows.filter((row) => !!appCommand(appPackages.find((app) => app.id === row.id), "upgrade"));
|
|
834
967
|
const failed = managed.filter((row) => row.checkFailed);
|
|
835
968
|
for (const row of failed)
|
|
836
|
-
|
|
969
|
+
out(pc.yellow(t("upgrade.statusUnknown", { id: row.id, reason: row.checkFailed })));
|
|
837
970
|
if (failed.length)
|
|
838
971
|
process.exitCode = 1;
|
|
839
972
|
const upgradable = managed.filter((row) => row.upgradable);
|
|
840
973
|
if (upgradable.length === 0) {
|
|
974
|
+
if (isJson()) {
|
|
975
|
+
emitJson({ upgraded, skipped: failed.map((row) => ({ id: row.id, reason: row.checkFailed })) });
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
841
978
|
if (failed.length)
|
|
842
|
-
|
|
979
|
+
out(t("upgrade.cannotDetermine"));
|
|
843
980
|
else if (managed.some((row) => row.installed))
|
|
844
|
-
|
|
981
|
+
out(t("upgrade.allCurrent"));
|
|
845
982
|
else
|
|
846
|
-
|
|
983
|
+
out(t("upgrade.none"));
|
|
847
984
|
return;
|
|
848
985
|
}
|
|
849
986
|
for (const row of upgradable)
|
|
@@ -853,57 +990,68 @@ export async function cmdUpgrade(ids) {
|
|
|
853
990
|
for (const app of selected) {
|
|
854
991
|
const cmd = appCommand(app, "upgrade");
|
|
855
992
|
if (!cmd) {
|
|
856
|
-
|
|
993
|
+
out(`${pc.yellow(t("dryRun.skip"))} ${t("upgrade.notCli", { id: app.id })}`);
|
|
994
|
+
skipped.push({ id: app.id, reason: "not-cli-managed" });
|
|
857
995
|
process.exitCode = 1;
|
|
858
996
|
continue;
|
|
859
997
|
}
|
|
860
998
|
try {
|
|
861
999
|
if (!installedVersion(app)) {
|
|
862
|
-
|
|
1000
|
+
out(`${pc.yellow(t("dryRun.skip"))} ${t("upgrade.notInstalled", { id: app.id })}`);
|
|
1001
|
+
skipped.push({ id: app.id, reason: "not-installed" });
|
|
863
1002
|
process.exitCode = 1;
|
|
864
1003
|
continue;
|
|
865
1004
|
}
|
|
866
|
-
|
|
1005
|
+
out(t("upgrade.upgrading", { name: app.name, command: pc.dim(cmd) }));
|
|
867
1006
|
runShell(cmd);
|
|
868
1007
|
const version = normalizeAppVersion(installedVersion(app));
|
|
869
1008
|
if (!version)
|
|
870
|
-
throw new Error("upgrade
|
|
1009
|
+
throw new Error(t("upgrade.unknownVersion"));
|
|
871
1010
|
const expected = expectedVersions.get(app.id);
|
|
872
1011
|
if (expected && isNewer(version, expected))
|
|
873
|
-
throw new Error(
|
|
874
|
-
|
|
1012
|
+
throw new Error(t("upgrade.olderThanAvailable", { version, expected }));
|
|
1013
|
+
out(pc.green(t("upgrade.done", { id: app.id, version })));
|
|
1014
|
+
upgraded.push({ id: app.id, version });
|
|
875
1015
|
}
|
|
876
1016
|
catch (err) {
|
|
877
|
-
|
|
1017
|
+
out(pc.red(t("upgrade.failed", { id: app.id, message: err.message })));
|
|
1018
|
+
skipped.push({ id: app.id, reason: err.message });
|
|
878
1019
|
process.exitCode = 1;
|
|
879
1020
|
}
|
|
880
1021
|
}
|
|
1022
|
+
if (isJson())
|
|
1023
|
+
emitJson({ upgraded, skipped });
|
|
881
1024
|
}
|
|
882
1025
|
/** Collect custom providers, dedupe by endpoint and credentials, and import what is new. */
|
|
883
1026
|
export async function cmdImport(opts) {
|
|
1027
|
+
const interactive = process.stdin.isTTY === true && !opts.all && !isJson();
|
|
884
1028
|
const metadataOptions = resolveMetadataOptions(opts);
|
|
885
1029
|
const rows = scanCandidates();
|
|
886
1030
|
const fresh = rows.filter((r) => !r.configured);
|
|
887
1031
|
for (const r of rows) {
|
|
888
1032
|
if (r.configured) {
|
|
889
|
-
|
|
1033
|
+
out(`${pc.yellow(t("import.skip"))} ${pc.bold(r.id)} · ${r.protocol} · ${r.baseUrl} — ${t("import.already", { id: pc.bold(r.configured) })}`);
|
|
890
1034
|
}
|
|
891
1035
|
}
|
|
892
1036
|
if (fresh.length === 0) {
|
|
893
|
-
|
|
1037
|
+
if (isJson()) {
|
|
1038
|
+
emitJson({ providers: [], candidates: rows.length });
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
out(rows.length ? t("import.noneNew") : t("import.noneFound"));
|
|
894
1042
|
return;
|
|
895
1043
|
}
|
|
896
|
-
|
|
897
|
-
|
|
1044
|
+
out("");
|
|
1045
|
+
out(table(fresh.map((r) => [
|
|
898
1046
|
r.id,
|
|
899
1047
|
r.protocol,
|
|
900
1048
|
r.baseUrl,
|
|
901
1049
|
String(r.models.length),
|
|
902
1050
|
r.sources.join(","),
|
|
903
1051
|
r.apiKey ? pc.green(t("import.keyYes")) : r.keyEnv ? pc.yellow(t("import.keyEnv", { name: r.keyEnv })) : pc.red(t("import.keyMissing")),
|
|
904
|
-
]), ["ID", t("table.protocol"), "BASE URL", t("table.models"), t("table.from"), t("table.key")]));
|
|
1052
|
+
]), ["ID", t("table.protocol"), "BASE URL", t("table.models"), t("table.from"), t("table.key")], { truncate: [2, 4] }));
|
|
905
1053
|
let chosen;
|
|
906
|
-
if (
|
|
1054
|
+
if (interactive) {
|
|
907
1055
|
const { pick } = await prompts({
|
|
908
1056
|
type: "multiselect",
|
|
909
1057
|
name: "pick",
|
|
@@ -914,9 +1062,9 @@ export async function cmdImport(opts) {
|
|
|
914
1062
|
value: i,
|
|
915
1063
|
selected: true,
|
|
916
1064
|
})),
|
|
917
|
-
}, { onCancel: () =>
|
|
1065
|
+
}, { onCancel: () => cancelInteractive() });
|
|
918
1066
|
if (!Array.isArray(pick) || pick.length === 0) {
|
|
919
|
-
|
|
1067
|
+
out(pc.dim(t("import.nothingSelected")));
|
|
920
1068
|
return;
|
|
921
1069
|
}
|
|
922
1070
|
chosen = pick.map((i) => fresh[i]).filter((r) => r !== undefined);
|
|
@@ -925,14 +1073,14 @@ export async function cmdImport(opts) {
|
|
|
925
1073
|
chosen = fresh;
|
|
926
1074
|
}
|
|
927
1075
|
const store = loadStore();
|
|
928
|
-
const catalog = await loadCatalog();
|
|
1076
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
929
1077
|
const gatewayLoader = sharedGatewayLoader();
|
|
930
1078
|
const imported = [];
|
|
931
1079
|
for (const c of chosen) {
|
|
932
1080
|
let apiKey = c.apiKey;
|
|
933
1081
|
if (!apiKey) {
|
|
934
1082
|
const why = c.keyEnv ? t("import.keyRefMissing", { name: c.keyEnv }) : t("import.keyNotStored");
|
|
935
|
-
if (!
|
|
1083
|
+
if (!interactive) {
|
|
936
1084
|
fail(t("import.missingKey", { id: c.id, why }));
|
|
937
1085
|
}
|
|
938
1086
|
const a = await prompts({
|
|
@@ -940,31 +1088,49 @@ export async function cmdImport(opts) {
|
|
|
940
1088
|
name: "key",
|
|
941
1089
|
message: t("import.keyPrompt", { id: c.id, url: c.baseUrl, why }),
|
|
942
1090
|
validate: (v) => (v.trim() ? true : t("import.required")),
|
|
943
|
-
}, { onCancel: () =>
|
|
1091
|
+
}, { onCancel: () => cancelInteractive() });
|
|
944
1092
|
apiKey = a.key;
|
|
945
1093
|
}
|
|
946
1094
|
// A prompted credential can identify a provider imported earlier in this same batch.
|
|
947
1095
|
const existing = findMatchingProvider(Object.values(store.providers), { protocol: c.protocol, baseUrl: c.baseUrl, apiKey });
|
|
948
1096
|
if (existing) {
|
|
949
|
-
|
|
1097
|
+
out(`${pc.yellow(t("import.skip"))} ${pc.bold(c.id)} · ${c.protocol} · ${c.baseUrl} — ${t("import.already", { id: pc.bold(existing.id) })}`);
|
|
950
1098
|
continue;
|
|
951
1099
|
}
|
|
952
1100
|
let ids = [...c.models];
|
|
953
1101
|
if (ids.length === 0) {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1102
|
+
const discover = () => withProgress(t("import.discovering", { id: c.id }), (progress) => discoverProviderModels({ baseUrl: c.baseUrl, apiKey: apiKey, protocol: c.protocol }, {
|
|
1103
|
+
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: c.baseUrl, page, count })),
|
|
1104
|
+
}));
|
|
1105
|
+
let listed;
|
|
1106
|
+
for (let attempt = 0;; attempt++) {
|
|
1107
|
+
try {
|
|
1108
|
+
listed = await discover();
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
catch (err) {
|
|
1112
|
+
if (!interactive)
|
|
1113
|
+
fail(t("import.discoveryFailed", { id: c.id, error: err.message }));
|
|
1114
|
+
// Skipping one provider keeps the rest of the batch importable.
|
|
1115
|
+
if (attempt >= 2)
|
|
1116
|
+
break;
|
|
1117
|
+
const { retry } = await prompts({ type: "toggle", name: "retry", message: t("import.retryDiscovery", { id: c.id, error: err.message }), initial: true }, { onCancel: () => cancelInteractive() });
|
|
1118
|
+
if (!retry)
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
957
1121
|
}
|
|
958
|
-
|
|
959
|
-
|
|
1122
|
+
if (listed === undefined) {
|
|
1123
|
+
out(pc.yellow(t("import.skippedAfterFailure", { id: c.id })));
|
|
1124
|
+
continue;
|
|
960
1125
|
}
|
|
1126
|
+
ids = listed;
|
|
961
1127
|
ids = applyModelFilter(ids, undefined, []).kept;
|
|
962
1128
|
}
|
|
963
1129
|
if (ids.length === 0)
|
|
964
1130
|
fail(t("import.noModelsImport", { id: c.id }));
|
|
965
1131
|
const id = availableProviderId(c.id, store.providers);
|
|
966
1132
|
const hint = guessProviderHint(catalog, c.baseUrl);
|
|
967
|
-
const models = await enrichProviderModels(catalog, ids, { ...metadataOptions, modelsDevId: hint }, { gatewayLoader });
|
|
1133
|
+
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, ids, { ...metadataOptions, modelsDevId: hint }, { gatewayLoader }));
|
|
968
1134
|
const defaultModel = c.defaultModel && ids.includes(c.defaultModel) ? c.defaultModel : ids[0];
|
|
969
1135
|
const existed = store.providers[id] !== undefined;
|
|
970
1136
|
store.providers[id] = {
|
|
@@ -982,9 +1148,13 @@ export async function cmdImport(opts) {
|
|
|
982
1148
|
if (!store.active)
|
|
983
1149
|
store.active = id;
|
|
984
1150
|
imported.push(id);
|
|
985
|
-
|
|
1151
|
+
out(`${pc.green(t(existed ? "import.updated" : "import.imported"))} ${pc.bold(id)} · ${c.protocol} · ${c.baseUrl} · ${t("import.modelsCount", { count: ids.length })} [${t("import.from")} ${c.sources.join(", ")}]`);
|
|
986
1152
|
}
|
|
987
1153
|
saveStore(store);
|
|
1154
|
+
if (isJson()) {
|
|
1155
|
+
emitJson({ providers: imported });
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
988
1158
|
if (imported.length)
|
|
989
|
-
|
|
1159
|
+
note(`\n${t("import.next", { id: imported[0] })}`);
|
|
990
1160
|
}
|