agentsw 0.9.1 → 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 +404 -212
- package/dist/discover.js +21 -11
- package/dist/gateway.js +5 -4
- package/dist/i18n.js +160 -3
- package/dist/index.js +14 -2
- package/dist/menu.js +141 -121
- package/dist/metadata.js +69 -20
- 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 {
|
|
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,11 +48,25 @@ function modelRows(models) {
|
|
|
70
48
|
]);
|
|
71
49
|
}
|
|
72
50
|
const MODEL_HEADER = ["MODEL", "CTX", "IN", "OUT", "REASONING", "$IN/$OUT per M"];
|
|
73
|
-
/**
|
|
74
|
-
|
|
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
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Explain why some models stayed bare: only ambiguous ids are worth the user's
|
|
59
|
+
* attention, so those are named separately from ids nothing can resolve.
|
|
60
|
+
*/
|
|
61
|
+
function printUncatalogedHint(models, catalog, hint) {
|
|
75
62
|
const bare = models.filter((m) => m.contextWindow === undefined && !m.cost);
|
|
76
|
-
if (bare.length
|
|
77
|
-
|
|
63
|
+
if (bare.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
out(pc.dim(t("meta.gap", { count: bare.length })));
|
|
66
|
+
const { ambiguous, unknown, noCreatorRow } = classifyUnresolved(catalog, bare.map((m) => m.id), hint);
|
|
67
|
+
for (const [key, ids] of [["meta.ambiguous", ambiguous], ["meta.noCreatorRow", noCreatorRow], ["meta.unknown", unknown]]) {
|
|
68
|
+
if (ids.length)
|
|
69
|
+
out(pc.dim(` ${t(key, { count: ids.length, ids: ids.join(", "), id: ids[0] })}`));
|
|
78
70
|
}
|
|
79
71
|
}
|
|
80
72
|
/** Guess the models.dev provider whose API host matches the configured baseUrl. */
|
|
@@ -114,14 +106,14 @@ function parseFilterOpts(opts, previous) {
|
|
|
114
106
|
function reportDropped(dropped) {
|
|
115
107
|
if (!dropped.length)
|
|
116
108
|
return;
|
|
117
|
-
|
|
109
|
+
out(pc.dim(`filtered out ${dropped.length} model(s):`));
|
|
118
110
|
for (const d of dropped)
|
|
119
|
-
|
|
111
|
+
out(pc.dim(` - ${d.id} (${d.reason})`));
|
|
120
112
|
}
|
|
121
113
|
export async function cmdAdd(opts) {
|
|
122
114
|
const store = loadStore();
|
|
123
115
|
resolveMetadataOptions(opts);
|
|
124
|
-
const interactive = process.stdin.isTTY && !opts.yes;
|
|
116
|
+
const interactive = process.stdin.isTTY === true && !opts.yes && !isJson();
|
|
125
117
|
let answers = {};
|
|
126
118
|
if (interactive) {
|
|
127
119
|
prompts.override({
|
|
@@ -169,7 +161,7 @@ export async function cmdAdd(opts) {
|
|
|
169
161
|
name: "models",
|
|
170
162
|
message: t("add.models"),
|
|
171
163
|
},
|
|
172
|
-
], { onCancel: () =>
|
|
164
|
+
], { onCancel: () => cancelInteractive() });
|
|
173
165
|
}
|
|
174
166
|
else {
|
|
175
167
|
const required = opts.discover
|
|
@@ -197,8 +189,15 @@ export async function cmdAdd(opts) {
|
|
|
197
189
|
fail(t("add.openaiApiInvalid"));
|
|
198
190
|
const baseUrl = normalizeUrl(answers.baseUrl);
|
|
199
191
|
const explicitId = answers.id || undefined;
|
|
200
|
-
const
|
|
201
|
-
|
|
192
|
+
const byId = explicitId ? store.providers[explicitId] : undefined;
|
|
193
|
+
// Endpoint plus credential is the account identity, so an explicit id never
|
|
194
|
+
// licenses a second entry for an account that is already configured.
|
|
195
|
+
const sameAccount = byId ? undefined : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey: answers.apiKey });
|
|
196
|
+
const existing = byId ?? sameAccount;
|
|
197
|
+
if (byId === undefined && explicitId && sameAccount) {
|
|
198
|
+
out(pc.yellow(t("add.alreadyConfigured", { id: pc.bold(sameAccount.id) })));
|
|
199
|
+
}
|
|
200
|
+
const id = byId?.id ?? sameAccount?.id ?? explicitId ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
202
201
|
const openaiApi = protocol === "openai" ? wire ?? existing?.openaiApi : undefined;
|
|
203
202
|
const metadataOptions = resolveMetadataOptions(opts, existing);
|
|
204
203
|
const manualIds = (answers.models ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -207,11 +206,30 @@ export async function cmdAdd(opts) {
|
|
|
207
206
|
const shouldDiscover = opts.discover || manualIds.length === 0;
|
|
208
207
|
if (shouldDiscover) {
|
|
209
208
|
if (!opts.discover) {
|
|
210
|
-
|
|
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
|
+
}
|
|
211
230
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
console.log(t("add.providerLists", { count: discovered.length }));
|
|
231
|
+
if (discovered.length)
|
|
232
|
+
out(t("add.providerLists", { count: discovered.length }));
|
|
215
233
|
modelIds = [...new Set([...manualIds, ...discovered])];
|
|
216
234
|
}
|
|
217
235
|
const retained = [opts.defaultModel === undefined ? existing?.defaultModel : undefined, opts.smallModel === undefined ? existing?.smallModel : undefined].filter((id) => !!id);
|
|
@@ -222,9 +240,9 @@ export async function cmdAdd(opts) {
|
|
|
222
240
|
modelIds = outcome.kept;
|
|
223
241
|
if (modelIds.length === 0)
|
|
224
242
|
fail(t("add.atLeastOne"));
|
|
225
|
-
const catalog = await loadCatalog();
|
|
243
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
226
244
|
const hint = opts.modelsDev ?? existing?.modelsDevId ?? guessProviderHint(catalog, answers.baseUrl);
|
|
227
|
-
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 }));
|
|
228
246
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
229
247
|
const defaultModel = opts.defaultModel ?? existing?.defaultModel ?? modelIds[0];
|
|
230
248
|
if (!modelIds.includes(defaultModel))
|
|
@@ -253,19 +271,23 @@ export async function cmdAdd(opts) {
|
|
|
253
271
|
store.active = provider.id;
|
|
254
272
|
saveStore(store);
|
|
255
273
|
const savedStatus = t(existed ? "add.updated" : "add.added");
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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));
|
|
281
|
+
printUncatalogedHint(models, catalog, hint);
|
|
282
|
+
note(`\n${t("add.next", { id: provider.id })}`);
|
|
261
283
|
}
|
|
262
284
|
/** Shared logic to create and save a single provider from discovered models. */
|
|
263
285
|
async function createProvider(opts) {
|
|
264
286
|
const { store, id, name, protocol, openaiApi, baseUrl, apiKey, modelIds, modelFilter } = opts;
|
|
265
287
|
const metadataOptions = resolveMetadataOptions(opts, opts.existing);
|
|
266
|
-
const catalog = await loadCatalog();
|
|
288
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
267
289
|
const hint = opts.modelsDev ?? opts.existing?.modelsDevId ?? guessProviderHint(catalog, baseUrl);
|
|
268
|
-
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 }));
|
|
269
291
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
270
292
|
const defaultModel = opts.defaultModel ?? opts.existing?.defaultModel ?? modelIds[0];
|
|
271
293
|
if (!modelIds.includes(defaultModel))
|
|
@@ -294,10 +316,10 @@ async function createProvider(opts) {
|
|
|
294
316
|
if (!store.active)
|
|
295
317
|
store.active = provider.id;
|
|
296
318
|
const savedStatus = t(existed ? "add.updated" : "add.added");
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
printUncatalogedHint(models);
|
|
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));
|
|
322
|
+
printUncatalogedHint(models, catalog, hint);
|
|
301
323
|
return provider;
|
|
302
324
|
}
|
|
303
325
|
/**
|
|
@@ -308,14 +330,14 @@ async function createProvider(opts) {
|
|
|
308
330
|
export async function cmdQuickAdd(opts) {
|
|
309
331
|
const store = loadStore();
|
|
310
332
|
resolveMetadataOptions(opts);
|
|
311
|
-
const interactive = process.stdin.isTTY && !opts.yes;
|
|
333
|
+
const interactive = process.stdin.isTTY === true && !opts.yes && !isJson();
|
|
312
334
|
let baseUrl;
|
|
313
335
|
let apiKey;
|
|
314
336
|
if (interactive) {
|
|
315
337
|
const answers = await prompts([
|
|
316
338
|
{ type: "text", name: "baseUrl", message: t("add.baseUrl"), validate: (v) => (/^https?:\/\//.test(v) ? true : t("add.baseUrlInvalid")) },
|
|
317
339
|
{ type: "password", name: "apiKey", message: t("add.apiKey") },
|
|
318
|
-
], { onCancel: () =>
|
|
340
|
+
], { onCancel: () => cancelInteractive() });
|
|
319
341
|
baseUrl = normalizeUrl(answers.baseUrl);
|
|
320
342
|
apiKey = answers.apiKey;
|
|
321
343
|
}
|
|
@@ -327,8 +349,25 @@ export async function cmdQuickAdd(opts) {
|
|
|
327
349
|
baseUrl = normalizeUrl(opts.baseUrl);
|
|
328
350
|
apiKey = opts.apiKey;
|
|
329
351
|
}
|
|
330
|
-
|
|
331
|
-
|
|
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
|
+
}
|
|
332
371
|
if (protocols.length === 0) {
|
|
333
372
|
fail(t("quick.noProtocol"));
|
|
334
373
|
}
|
|
@@ -339,13 +378,19 @@ export async function cmdQuickAdd(opts) {
|
|
|
339
378
|
const gatewayLoader = sharedGatewayLoader();
|
|
340
379
|
for (const protocol of protocols) {
|
|
341
380
|
const explicitId = opts.id === undefined ? undefined : `${opts.id}${multi ? `-${protocol}` : ""}`;
|
|
342
|
-
const
|
|
343
|
-
const
|
|
381
|
+
const byId = explicitId ? store.providers[explicitId] : undefined;
|
|
382
|
+
const sameAccount = byId ? undefined : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey });
|
|
383
|
+
const existing = byId ?? sameAccount;
|
|
384
|
+
if (byId === undefined && explicitId && sameAccount) {
|
|
385
|
+
out(pc.yellow(t("add.alreadyConfigured", { id: pc.bold(sameAccount.id) })));
|
|
386
|
+
}
|
|
387
|
+
const id = byId?.id ?? sameAccount?.id ?? explicitId ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
344
388
|
const name = opts.name ?? existing?.name ?? (opts.id === undefined ? providerNameFromBaseUrl(baseUrl, protocol) : multi ? `${opts.id} (${protocol})` : opts.id);
|
|
345
389
|
const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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 }));
|
|
349
394
|
const retained = [opts.defaultModel === undefined ? existing?.defaultModel : undefined, opts.smallModel === undefined ? existing?.smallModel : undefined].filter((id) => !!id);
|
|
350
395
|
let modelIds = [...new Set([...discovered, ...retained])];
|
|
351
396
|
const pinned = [...retained, ...(opts.defaultModel ? [opts.defaultModel] : []), ...(opts.smallModel ? [opts.smallModel] : [])];
|
|
@@ -353,7 +398,7 @@ export async function cmdQuickAdd(opts) {
|
|
|
353
398
|
reportDropped(outcome.dropped);
|
|
354
399
|
modelIds = outcome.kept;
|
|
355
400
|
if (modelIds.length === 0) {
|
|
356
|
-
|
|
401
|
+
out(pc.yellow(t("quick.noModelsAfterFilter", { id })));
|
|
357
402
|
continue;
|
|
358
403
|
}
|
|
359
404
|
const provider = await createProvider({
|
|
@@ -377,18 +422,26 @@ export async function cmdQuickAdd(opts) {
|
|
|
377
422
|
gatewayLoader,
|
|
378
423
|
});
|
|
379
424
|
createdIds.push(provider.id);
|
|
380
|
-
|
|
425
|
+
note(`\n${t("add.next", { id: provider.id })}`);
|
|
381
426
|
}
|
|
382
427
|
saveStore(store);
|
|
428
|
+
if (isJson()) {
|
|
429
|
+
emitJson({ providers: createdIds });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
383
432
|
if (createdIds.length > 0) {
|
|
384
|
-
|
|
433
|
+
out(pc.green(t("quick.summary", { count: createdIds.length, ids: createdIds.join(", ") })));
|
|
385
434
|
}
|
|
386
435
|
}
|
|
387
436
|
export function cmdList() {
|
|
388
437
|
const store = loadStore();
|
|
438
|
+
if (isJson()) {
|
|
439
|
+
emitJson(listReport(store));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
389
442
|
const ids = Object.keys(store.providers);
|
|
390
443
|
if (ids.length === 0) {
|
|
391
|
-
|
|
444
|
+
out(t("list.none", { file: configFile }));
|
|
392
445
|
return;
|
|
393
446
|
}
|
|
394
447
|
const rows = ids.map((id) => {
|
|
@@ -402,23 +455,37 @@ export function cmdList() {
|
|
|
402
455
|
String(p.models.length),
|
|
403
456
|
];
|
|
404
457
|
});
|
|
405
|
-
|
|
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
|
+
}));
|
|
406
462
|
}
|
|
407
463
|
export async function cmdPrune(id, opts) {
|
|
408
464
|
const store = loadStore();
|
|
409
465
|
const provider = getProvider(store, id);
|
|
410
|
-
|
|
411
|
-
|
|
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);
|
|
412
479
|
}
|
|
413
480
|
function reportResults(results) {
|
|
414
481
|
for (const r of results) {
|
|
415
482
|
if (r.skipped) {
|
|
416
|
-
|
|
483
|
+
out(`${pc.yellow(t("common.skip"))} ${r.app.padEnd(9)} ${pc.dim(r.skipped)}`);
|
|
417
484
|
continue;
|
|
418
485
|
}
|
|
419
|
-
|
|
486
|
+
out(`${pc.green("ok ")} ${r.app.padEnd(9)} ${r.changed.join(", ")}`);
|
|
420
487
|
for (const note of r.notes)
|
|
421
|
-
|
|
488
|
+
out(` ${" ".repeat(9)} ${pc.dim(note)}`);
|
|
422
489
|
}
|
|
423
490
|
}
|
|
424
491
|
async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
@@ -444,7 +511,7 @@ async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
|
444
511
|
}
|
|
445
512
|
catch (err) {
|
|
446
513
|
const safe = redactErrors && !(err instanceof SafeConfigError);
|
|
447
|
-
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}`) });
|
|
448
515
|
process.exitCode = 1;
|
|
449
516
|
}
|
|
450
517
|
}
|
|
@@ -481,10 +548,10 @@ function previewDocument(file, text) {
|
|
|
481
548
|
function printFileDiff(file, next, apiKey) {
|
|
482
549
|
const before = readTextIfExists(file) ?? "";
|
|
483
550
|
if (before === next) {
|
|
484
|
-
|
|
551
|
+
out(`${pc.dim(t("preview.unchanged"))} ${file}`);
|
|
485
552
|
return;
|
|
486
553
|
}
|
|
487
|
-
|
|
554
|
+
out(pc.bold(t("preview.header", { file })));
|
|
488
555
|
let oldText;
|
|
489
556
|
let newText;
|
|
490
557
|
try {
|
|
@@ -534,7 +601,7 @@ function printFileDiff(file, next, apiKey) {
|
|
|
534
601
|
newText = JSON.stringify(newDocument, redact, 2) ?? "";
|
|
535
602
|
}
|
|
536
603
|
catch {
|
|
537
|
-
|
|
604
|
+
out(pc.dim(t("preview.contentWithheld")));
|
|
538
605
|
return;
|
|
539
606
|
}
|
|
540
607
|
const oldLines = oldText.split("\n");
|
|
@@ -550,22 +617,28 @@ function printFileDiff(file, next, apiKey) {
|
|
|
550
617
|
if (count > 0)
|
|
551
618
|
newSeen.set(line, count - 1);
|
|
552
619
|
else
|
|
553
|
-
|
|
620
|
+
out(pc.red(`- ${line}`));
|
|
554
621
|
}
|
|
555
622
|
for (const line of newLines) {
|
|
556
623
|
const count = oldSeen.get(line) ?? 0;
|
|
557
624
|
if (count > 0)
|
|
558
625
|
oldSeen.set(line, count - 1);
|
|
559
626
|
else
|
|
560
|
-
|
|
627
|
+
out(pc.green(`+ ${line}`));
|
|
561
628
|
}
|
|
562
629
|
if (oldText === newText)
|
|
563
|
-
|
|
630
|
+
out(pc.dim(t("preview.onlyRedacted")));
|
|
564
631
|
}
|
|
565
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
|
+
});
|
|
566
638
|
if (!dryRun) {
|
|
567
|
-
|
|
568
|
-
|
|
639
|
+
const results = await runTargets(op, provider, apps);
|
|
640
|
+
reportResults(results);
|
|
641
|
+
return summarize(results, results.flatMap((r) => (r.skipped ? [] : r.changed)));
|
|
569
642
|
}
|
|
570
643
|
setDryRun(true);
|
|
571
644
|
try {
|
|
@@ -573,11 +646,13 @@ async function runWithOptionalDryRun(op, provider, apps, dryRun) {
|
|
|
573
646
|
const writes = drainPendingWrites();
|
|
574
647
|
for (const r of results) {
|
|
575
648
|
if (r.skipped)
|
|
576
|
-
|
|
649
|
+
out(`${pc.yellow(t("dryRun.skip"))} ${r.app.padEnd(9)} ${pc.dim(r.skipped)}`);
|
|
577
650
|
}
|
|
578
|
-
|
|
651
|
+
out(pc.bold(`\n${t("dryRun.wouldWrite", { count: writes.length })}\n`));
|
|
579
652
|
for (const w of writes)
|
|
580
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));
|
|
581
656
|
}
|
|
582
657
|
finally {
|
|
583
658
|
setDryRun(false);
|
|
@@ -597,8 +672,11 @@ export async function cmdUse(id, opts) {
|
|
|
597
672
|
store.active = id;
|
|
598
673
|
saveStore(store);
|
|
599
674
|
}
|
|
600
|
-
|
|
601
|
-
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
|
+
}
|
|
602
680
|
}
|
|
603
681
|
export async function cmdSync(opts) {
|
|
604
682
|
resolveTargets(opts.apps);
|
|
@@ -607,59 +685,85 @@ export async function cmdSync(opts) {
|
|
|
607
685
|
if (!id)
|
|
608
686
|
fail(t("sync.noActive"));
|
|
609
687
|
const provider = getProvider(store, id);
|
|
610
|
-
|
|
611
|
-
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
|
+
}
|
|
612
693
|
}
|
|
613
694
|
export function cmdStatus() {
|
|
614
695
|
const store = loadStore();
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
target.
|
|
619
|
-
target.
|
|
620
|
-
target.
|
|
621
|
-
|
|
622
|
-
|
|
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),
|
|
623
715
|
]);
|
|
624
|
-
|
|
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;
|
|
625
726
|
}
|
|
626
727
|
export async function cmdModels(query, opts) {
|
|
627
728
|
if (opts.metadata && !opts.provider)
|
|
628
|
-
fail("
|
|
729
|
+
fail(t("models.metadataRequiresProvider"));
|
|
629
730
|
if (opts.provider) {
|
|
630
731
|
const store = loadStore();
|
|
631
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.
|
|
632
738
|
if (opts.metadata) {
|
|
633
|
-
|
|
634
|
-
provider: provider.id,
|
|
635
|
-
gatewayMetadata: provider.gatewayMetadata ?? "auto",
|
|
636
|
-
metadataMode: getMetadataMode(provider),
|
|
637
|
-
gatewayModelAliases: provider.gatewayModelAliases ?? {},
|
|
638
|
-
models: provider.models.map(({ id, metadata }) => ({ id, metadata })),
|
|
639
|
-
}, null, 2));
|
|
739
|
+
out(JSON.stringify(providerMetadataReport(provider), null, 2));
|
|
640
740
|
return;
|
|
641
741
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
printUncatalogedHint(provider.models);
|
|
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);
|
|
645
745
|
return;
|
|
646
746
|
}
|
|
647
|
-
const catalog = await loadCatalog({ refresh: opts.refresh });
|
|
747
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog({ refresh: opts.refresh }));
|
|
648
748
|
if (!catalog)
|
|
649
|
-
fail("models.
|
|
749
|
+
fail(t("models.catalogUnavailable"));
|
|
650
750
|
if (!query)
|
|
651
|
-
fail("usage
|
|
652
|
-
const limit =
|
|
751
|
+
fail(t("models.usage"));
|
|
752
|
+
const limit = parseLimit(opts.limit);
|
|
653
753
|
const hits = searchCatalog(catalog, query, limit);
|
|
754
|
+
if (isJson()) {
|
|
755
|
+
emitJson(modelsQueryReport(query, hits));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
654
758
|
if (hits.length === 0) {
|
|
655
|
-
|
|
759
|
+
out(t("models.noMatch", { query }));
|
|
656
760
|
return;
|
|
657
761
|
}
|
|
658
762
|
const rows = hits.map((h) => {
|
|
659
763
|
const row = modelRows([h.spec])[0];
|
|
660
764
|
return [h.provider, ...row];
|
|
661
765
|
});
|
|
662
|
-
|
|
766
|
+
out(table(rows, ["PROVIDER", ...MODEL_HEADER], MODEL_QUERY_OPTIONS));
|
|
663
767
|
}
|
|
664
768
|
export async function cmdRefreshMeta(opts = {}) {
|
|
665
769
|
const store = loadStore();
|
|
@@ -667,22 +771,28 @@ export async function cmdRefreshMeta(opts = {}) {
|
|
|
667
771
|
resolveMetadataOptions(opts);
|
|
668
772
|
for (const provider of providers)
|
|
669
773
|
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
670
|
-
const catalog = await loadCatalog({ refresh: true });
|
|
774
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog({ refresh: true }));
|
|
671
775
|
const gatewayLoader = sharedGatewayLoader(true);
|
|
672
776
|
let updated = 0;
|
|
673
777
|
for (const provider of providers) {
|
|
674
778
|
const before = JSON.stringify(provider.models);
|
|
675
|
-
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), {
|
|
676
780
|
...provider,
|
|
677
781
|
modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl),
|
|
678
|
-
}, { gatewayLoader });
|
|
782
|
+
}, { gatewayLoader }));
|
|
679
783
|
if (JSON.stringify(provider.models) !== before)
|
|
680
784
|
updated++;
|
|
681
785
|
}
|
|
682
786
|
saveStore(store);
|
|
683
|
-
|
|
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 })));
|
|
684
792
|
if (updated > 0)
|
|
685
|
-
|
|
793
|
+
note(t("refresh.next"));
|
|
794
|
+
for (const provider of providers)
|
|
795
|
+
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
686
796
|
}
|
|
687
797
|
export async function cmdDiscover(id, opts) {
|
|
688
798
|
resolveTargets(opts.apps);
|
|
@@ -695,133 +805,182 @@ export async function cmdDiscover(id, opts) {
|
|
|
695
805
|
provider.modelFilter = undefined;
|
|
696
806
|
else if (flagFilter)
|
|
697
807
|
provider.modelFilter = flagFilter;
|
|
698
|
-
|
|
699
|
-
|
|
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
|
+
}));
|
|
700
811
|
const pinned = [provider.defaultModel, provider.smallModel].filter((model) => !!model);
|
|
701
812
|
const outcome = applyModelFilter(listed, provider.modelFilter, pinned);
|
|
702
813
|
reportDropped(outcome.dropped);
|
|
703
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);
|
|
704
818
|
const known = provider.models.map((m) => m.id);
|
|
705
819
|
const added = ids.filter((m) => !known.includes(m));
|
|
706
|
-
const gone = known.filter((m) => !
|
|
707
|
-
const catalog = await loadCatalog();
|
|
708
|
-
|
|
709
|
-
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) }));
|
|
710
823
|
if (!ids.includes(provider.defaultModel)) {
|
|
711
|
-
|
|
824
|
+
out(pc.yellow(t("discover.defaultMissing", { model: provider.defaultModel })));
|
|
712
825
|
}
|
|
713
826
|
saveStore(store);
|
|
714
|
-
|
|
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}`)})` +
|
|
715
832
|
(added.length ? `\n new: ${added.join(", ")}` : "") +
|
|
716
833
|
(gone.length ? `\n removed upstream: ${gone.join(", ")}` : ""));
|
|
717
|
-
|
|
718
|
-
printUncatalogedHint(provider.models);
|
|
834
|
+
out(table(modelRows(provider.models), MODEL_HEADER, MODEL_TABLE_OPTIONS));
|
|
835
|
+
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
719
836
|
if (opts.sync) {
|
|
720
|
-
|
|
837
|
+
out("");
|
|
721
838
|
await cmdSync({ provider: id, apps: opts.apps });
|
|
722
839
|
}
|
|
723
840
|
else {
|
|
724
|
-
|
|
841
|
+
note(`\n${t("discover.next")}`);
|
|
725
842
|
}
|
|
726
843
|
}
|
|
727
|
-
async function collectAppRows() {
|
|
844
|
+
async function collectAppRows(onProgress) {
|
|
845
|
+
const total = appPackages.length;
|
|
846
|
+
let done = 0;
|
|
728
847
|
return Promise.all(appPackages.map(async (app) => {
|
|
729
|
-
|
|
730
|
-
Promise.
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
+
}
|
|
753
877
|
}));
|
|
754
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
|
+
}
|
|
755
895
|
export async function cmdApps() {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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) => [
|
|
759
911
|
r.id,
|
|
760
|
-
r.installed ?? pc.dim("
|
|
912
|
+
r.installed ?? pc.dim(t("apps.notInstalled")),
|
|
761
913
|
r.latest ?? pc.dim("?"),
|
|
762
|
-
r
|
|
763
|
-
? pc.yellow("upgrade available")
|
|
764
|
-
: !r.installed
|
|
765
|
-
? r.installable
|
|
766
|
-
? pc.dim("installable")
|
|
767
|
-
: pc.dim("-")
|
|
768
|
-
: r.checkFailed || !normalizeAppVersion(r.latest)
|
|
769
|
-
? pc.dim("unknown")
|
|
770
|
-
: pc.green("up to date"),
|
|
914
|
+
appStatusLabel(appStatus(r)),
|
|
771
915
|
]), ["APP", "INSTALLED", "LATEST", "STATUS"]));
|
|
772
916
|
const upgradable = rows.filter((r) => r.upgradable).map((r) => r.id);
|
|
773
917
|
if (upgradable.length)
|
|
774
|
-
|
|
918
|
+
note(`\n${t("apps.upgradeWith", { ids: upgradable.join(" ") })}`);
|
|
775
919
|
}
|
|
776
920
|
export async function cmdInstall(id) {
|
|
777
921
|
const app = appPackages.find((a) => a.id === id);
|
|
778
922
|
if (!app)
|
|
779
|
-
fail(
|
|
923
|
+
fail(t("install.unknownApp", { value: id, apps: appPackages.map((a) => a.id).join(", ") }));
|
|
780
924
|
const installCmd = appCommand(app, "install");
|
|
781
925
|
if (!installCmd)
|
|
782
|
-
fail(
|
|
926
|
+
fail(t("install.notInstallable", { name: app.name, platform: process.platform }));
|
|
783
927
|
const installed = installedVersion(app);
|
|
784
928
|
if (installed) {
|
|
785
|
-
|
|
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 }));
|
|
786
934
|
return;
|
|
787
935
|
}
|
|
788
|
-
|
|
936
|
+
out(t("install.installing", { name: app.name, command: pc.dim(installCmd) }));
|
|
789
937
|
runShell(installCmd);
|
|
790
938
|
const detected = installedVersion(app);
|
|
791
939
|
if (!detected)
|
|
792
|
-
throw new Error(
|
|
940
|
+
throw new Error(t("install.notDetected", { name: app.name }));
|
|
793
941
|
const version = normalizeAppVersion(detected);
|
|
794
942
|
if (!version) {
|
|
795
|
-
|
|
943
|
+
if (isJson())
|
|
944
|
+
error(t("install.versionUnknown", { name: app.name }));
|
|
945
|
+
out(pc.yellow(t("install.versionUnknown", { name: app.name })));
|
|
796
946
|
process.exitCode = 1;
|
|
797
947
|
return;
|
|
798
948
|
}
|
|
799
|
-
|
|
949
|
+
if (isJson()) {
|
|
950
|
+
emitJson({ app: id, status: "installed", version });
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
out(pc.green(t("install.installed", { name: app.name, version })));
|
|
800
954
|
}
|
|
801
955
|
export async function cmdUpgrade(ids) {
|
|
802
956
|
let selected = appPackages.filter((a) => ids.length === 0 || ids.includes(a.id));
|
|
803
957
|
const unknown = ids.filter((id) => !appPackages.some((a) => a.id === id));
|
|
804
958
|
if (unknown.length)
|
|
805
|
-
fail(
|
|
959
|
+
fail(t("upgrade.unknownApps", { ids: unknown.join(", ") }));
|
|
806
960
|
const expectedVersions = new Map();
|
|
961
|
+
const upgraded = [];
|
|
962
|
+
const skipped = [];
|
|
807
963
|
if (ids.length === 0) {
|
|
808
964
|
// no args: upgrade everything that is installed and outdated
|
|
809
|
-
|
|
810
|
-
const rows = await collectAppRows();
|
|
965
|
+
const rows = await withProgress(t("upgrade.checking"), (progress) => collectAppRows((done, total) => progress.update(t("progress.apps", { done, total }))));
|
|
811
966
|
const managed = rows.filter((row) => !!appCommand(appPackages.find((app) => app.id === row.id), "upgrade"));
|
|
812
967
|
const failed = managed.filter((row) => row.checkFailed);
|
|
813
968
|
for (const row of failed)
|
|
814
|
-
|
|
969
|
+
out(pc.yellow(t("upgrade.statusUnknown", { id: row.id, reason: row.checkFailed })));
|
|
815
970
|
if (failed.length)
|
|
816
971
|
process.exitCode = 1;
|
|
817
972
|
const upgradable = managed.filter((row) => row.upgradable);
|
|
818
973
|
if (upgradable.length === 0) {
|
|
974
|
+
if (isJson()) {
|
|
975
|
+
emitJson({ upgraded, skipped: failed.map((row) => ({ id: row.id, reason: row.checkFailed })) });
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
819
978
|
if (failed.length)
|
|
820
|
-
|
|
979
|
+
out(t("upgrade.cannotDetermine"));
|
|
821
980
|
else if (managed.some((row) => row.installed))
|
|
822
|
-
|
|
981
|
+
out(t("upgrade.allCurrent"));
|
|
823
982
|
else
|
|
824
|
-
|
|
983
|
+
out(t("upgrade.none"));
|
|
825
984
|
return;
|
|
826
985
|
}
|
|
827
986
|
for (const row of upgradable)
|
|
@@ -831,57 +990,68 @@ export async function cmdUpgrade(ids) {
|
|
|
831
990
|
for (const app of selected) {
|
|
832
991
|
const cmd = appCommand(app, "upgrade");
|
|
833
992
|
if (!cmd) {
|
|
834
|
-
|
|
993
|
+
out(`${pc.yellow(t("dryRun.skip"))} ${t("upgrade.notCli", { id: app.id })}`);
|
|
994
|
+
skipped.push({ id: app.id, reason: "not-cli-managed" });
|
|
835
995
|
process.exitCode = 1;
|
|
836
996
|
continue;
|
|
837
997
|
}
|
|
838
998
|
try {
|
|
839
999
|
if (!installedVersion(app)) {
|
|
840
|
-
|
|
1000
|
+
out(`${pc.yellow(t("dryRun.skip"))} ${t("upgrade.notInstalled", { id: app.id })}`);
|
|
1001
|
+
skipped.push({ id: app.id, reason: "not-installed" });
|
|
841
1002
|
process.exitCode = 1;
|
|
842
1003
|
continue;
|
|
843
1004
|
}
|
|
844
|
-
|
|
1005
|
+
out(t("upgrade.upgrading", { name: app.name, command: pc.dim(cmd) }));
|
|
845
1006
|
runShell(cmd);
|
|
846
1007
|
const version = normalizeAppVersion(installedVersion(app));
|
|
847
1008
|
if (!version)
|
|
848
|
-
throw new Error("upgrade
|
|
1009
|
+
throw new Error(t("upgrade.unknownVersion"));
|
|
849
1010
|
const expected = expectedVersions.get(app.id);
|
|
850
1011
|
if (expected && isNewer(version, expected))
|
|
851
|
-
throw new Error(
|
|
852
|
-
|
|
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 });
|
|
853
1015
|
}
|
|
854
1016
|
catch (err) {
|
|
855
|
-
|
|
1017
|
+
out(pc.red(t("upgrade.failed", { id: app.id, message: err.message })));
|
|
1018
|
+
skipped.push({ id: app.id, reason: err.message });
|
|
856
1019
|
process.exitCode = 1;
|
|
857
1020
|
}
|
|
858
1021
|
}
|
|
1022
|
+
if (isJson())
|
|
1023
|
+
emitJson({ upgraded, skipped });
|
|
859
1024
|
}
|
|
860
1025
|
/** Collect custom providers, dedupe by endpoint and credentials, and import what is new. */
|
|
861
1026
|
export async function cmdImport(opts) {
|
|
1027
|
+
const interactive = process.stdin.isTTY === true && !opts.all && !isJson();
|
|
862
1028
|
const metadataOptions = resolveMetadataOptions(opts);
|
|
863
1029
|
const rows = scanCandidates();
|
|
864
1030
|
const fresh = rows.filter((r) => !r.configured);
|
|
865
1031
|
for (const r of rows) {
|
|
866
1032
|
if (r.configured) {
|
|
867
|
-
|
|
1033
|
+
out(`${pc.yellow(t("import.skip"))} ${pc.bold(r.id)} · ${r.protocol} · ${r.baseUrl} — ${t("import.already", { id: pc.bold(r.configured) })}`);
|
|
868
1034
|
}
|
|
869
1035
|
}
|
|
870
1036
|
if (fresh.length === 0) {
|
|
871
|
-
|
|
1037
|
+
if (isJson()) {
|
|
1038
|
+
emitJson({ providers: [], candidates: rows.length });
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
out(rows.length ? t("import.noneNew") : t("import.noneFound"));
|
|
872
1042
|
return;
|
|
873
1043
|
}
|
|
874
|
-
|
|
875
|
-
|
|
1044
|
+
out("");
|
|
1045
|
+
out(table(fresh.map((r) => [
|
|
876
1046
|
r.id,
|
|
877
1047
|
r.protocol,
|
|
878
1048
|
r.baseUrl,
|
|
879
1049
|
String(r.models.length),
|
|
880
1050
|
r.sources.join(","),
|
|
881
1051
|
r.apiKey ? pc.green(t("import.keyYes")) : r.keyEnv ? pc.yellow(t("import.keyEnv", { name: r.keyEnv })) : pc.red(t("import.keyMissing")),
|
|
882
|
-
]), ["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] }));
|
|
883
1053
|
let chosen;
|
|
884
|
-
if (
|
|
1054
|
+
if (interactive) {
|
|
885
1055
|
const { pick } = await prompts({
|
|
886
1056
|
type: "multiselect",
|
|
887
1057
|
name: "pick",
|
|
@@ -892,9 +1062,9 @@ export async function cmdImport(opts) {
|
|
|
892
1062
|
value: i,
|
|
893
1063
|
selected: true,
|
|
894
1064
|
})),
|
|
895
|
-
}, { onCancel: () =>
|
|
1065
|
+
}, { onCancel: () => cancelInteractive() });
|
|
896
1066
|
if (!Array.isArray(pick) || pick.length === 0) {
|
|
897
|
-
|
|
1067
|
+
out(pc.dim(t("import.nothingSelected")));
|
|
898
1068
|
return;
|
|
899
1069
|
}
|
|
900
1070
|
chosen = pick.map((i) => fresh[i]).filter((r) => r !== undefined);
|
|
@@ -903,14 +1073,14 @@ export async function cmdImport(opts) {
|
|
|
903
1073
|
chosen = fresh;
|
|
904
1074
|
}
|
|
905
1075
|
const store = loadStore();
|
|
906
|
-
const catalog = await loadCatalog();
|
|
1076
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
907
1077
|
const gatewayLoader = sharedGatewayLoader();
|
|
908
1078
|
const imported = [];
|
|
909
1079
|
for (const c of chosen) {
|
|
910
1080
|
let apiKey = c.apiKey;
|
|
911
1081
|
if (!apiKey) {
|
|
912
1082
|
const why = c.keyEnv ? t("import.keyRefMissing", { name: c.keyEnv }) : t("import.keyNotStored");
|
|
913
|
-
if (!
|
|
1083
|
+
if (!interactive) {
|
|
914
1084
|
fail(t("import.missingKey", { id: c.id, why }));
|
|
915
1085
|
}
|
|
916
1086
|
const a = await prompts({
|
|
@@ -918,31 +1088,49 @@ export async function cmdImport(opts) {
|
|
|
918
1088
|
name: "key",
|
|
919
1089
|
message: t("import.keyPrompt", { id: c.id, url: c.baseUrl, why }),
|
|
920
1090
|
validate: (v) => (v.trim() ? true : t("import.required")),
|
|
921
|
-
}, { onCancel: () =>
|
|
1091
|
+
}, { onCancel: () => cancelInteractive() });
|
|
922
1092
|
apiKey = a.key;
|
|
923
1093
|
}
|
|
924
1094
|
// A prompted credential can identify a provider imported earlier in this same batch.
|
|
925
1095
|
const existing = findMatchingProvider(Object.values(store.providers), { protocol: c.protocol, baseUrl: c.baseUrl, apiKey });
|
|
926
1096
|
if (existing) {
|
|
927
|
-
|
|
1097
|
+
out(`${pc.yellow(t("import.skip"))} ${pc.bold(c.id)} · ${c.protocol} · ${c.baseUrl} — ${t("import.already", { id: pc.bold(existing.id) })}`);
|
|
928
1098
|
continue;
|
|
929
1099
|
}
|
|
930
1100
|
let ids = [...c.models];
|
|
931
1101
|
if (ids.length === 0) {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
+
}
|
|
935
1121
|
}
|
|
936
|
-
|
|
937
|
-
|
|
1122
|
+
if (listed === undefined) {
|
|
1123
|
+
out(pc.yellow(t("import.skippedAfterFailure", { id: c.id })));
|
|
1124
|
+
continue;
|
|
938
1125
|
}
|
|
1126
|
+
ids = listed;
|
|
939
1127
|
ids = applyModelFilter(ids, undefined, []).kept;
|
|
940
1128
|
}
|
|
941
1129
|
if (ids.length === 0)
|
|
942
1130
|
fail(t("import.noModelsImport", { id: c.id }));
|
|
943
1131
|
const id = availableProviderId(c.id, store.providers);
|
|
944
1132
|
const hint = guessProviderHint(catalog, c.baseUrl);
|
|
945
|
-
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 }));
|
|
946
1134
|
const defaultModel = c.defaultModel && ids.includes(c.defaultModel) ? c.defaultModel : ids[0];
|
|
947
1135
|
const existed = store.providers[id] !== undefined;
|
|
948
1136
|
store.providers[id] = {
|
|
@@ -960,9 +1148,13 @@ export async function cmdImport(opts) {
|
|
|
960
1148
|
if (!store.active)
|
|
961
1149
|
store.active = id;
|
|
962
1150
|
imported.push(id);
|
|
963
|
-
|
|
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(", ")}]`);
|
|
964
1152
|
}
|
|
965
1153
|
saveStore(store);
|
|
1154
|
+
if (isJson()) {
|
|
1155
|
+
emitJson({ providers: imported });
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
966
1158
|
if (imported.length)
|
|
967
|
-
|
|
1159
|
+
note(`\n${t("import.next", { id: imported[0] })}`);
|
|
968
1160
|
}
|