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/menu.js
CHANGED
|
@@ -9,8 +9,9 @@ import { cmdRemoveProvider, cmdRename } from "./provider-actions.js";
|
|
|
9
9
|
import { listRemovableProviders } from "./remove.js";
|
|
10
10
|
import { providerIdFromBaseUrl } from "./slug.js";
|
|
11
11
|
import { targets } from "./targets/index.js";
|
|
12
|
+
import { ActionCancelled, out, setInMenu } from "./ui.js";
|
|
12
13
|
function bye() {
|
|
13
|
-
|
|
14
|
+
out(pc.dim(`\n${t("menu.bye")}`));
|
|
14
15
|
process.exit(0);
|
|
15
16
|
}
|
|
16
17
|
const cancel = { onCancel: () => bye() };
|
|
@@ -34,14 +35,14 @@ async function chooseLanguage() {
|
|
|
34
35
|
const store = loadStore();
|
|
35
36
|
store.language = language;
|
|
36
37
|
saveStore(store);
|
|
37
|
-
|
|
38
|
+
out(pc.green(t("language.saved")));
|
|
38
39
|
}
|
|
39
40
|
/** select an existing provider; prints a hint and returns undefined when none configured */
|
|
40
41
|
async function pickProvider(message) {
|
|
41
42
|
const store = loadStore();
|
|
42
43
|
const ids = Object.keys(store.providers);
|
|
43
44
|
if (ids.length === 0) {
|
|
44
|
-
|
|
45
|
+
out(pc.yellow(t("menu.noProvidersHint")));
|
|
45
46
|
return undefined;
|
|
46
47
|
}
|
|
47
48
|
const { id } = await prompts({
|
|
@@ -49,15 +50,18 @@ async function pickProvider(message) {
|
|
|
49
50
|
name: "id",
|
|
50
51
|
message,
|
|
51
52
|
hint: t("menu.selectInstructions"),
|
|
52
|
-
choices:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
choices: [
|
|
54
|
+
{ title: t("menu.back"), value: "__back__" },
|
|
55
|
+
...ids.map((pid) => {
|
|
56
|
+
const p = store.providers[pid];
|
|
57
|
+
return {
|
|
58
|
+
title: `${pid} · ${p.protocol} · ${t("menu.defaultModel")} ${p.defaultModel}${store.active === pid ? ` (${t("menu.active")})` : ""}`,
|
|
59
|
+
value: pid,
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
59
63
|
}, cancel);
|
|
60
|
-
if (!id)
|
|
64
|
+
if (!id || id === "__back__")
|
|
61
65
|
return undefined;
|
|
62
66
|
const p = store.providers[id];
|
|
63
67
|
return { id, defaultModel: p.defaultModel, models: p.models.map((m) => m.id) };
|
|
@@ -151,7 +155,7 @@ async function removeFromMenu() {
|
|
|
151
155
|
? listRemovableProviders(app)
|
|
152
156
|
: Object.values(loadStore().providers).map((provider) => ({ id: provider.id, name: provider.name }));
|
|
153
157
|
if (!entries.length) {
|
|
154
|
-
|
|
158
|
+
out(pc.dim(t("menu.noRemovable")));
|
|
155
159
|
return;
|
|
156
160
|
}
|
|
157
161
|
const { selected } = await prompts({
|
|
@@ -159,10 +163,13 @@ async function removeFromMenu() {
|
|
|
159
163
|
name: "selected",
|
|
160
164
|
message: t("menu.removeProvider"),
|
|
161
165
|
hint: t("menu.selectInstructions"),
|
|
162
|
-
choices:
|
|
163
|
-
title:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
choices: [
|
|
167
|
+
{ title: t("menu.back"), value: "__back__" },
|
|
168
|
+
...entries.map((entry, index) => ({
|
|
169
|
+
title: `${entry.id} · ${entry.app ?? "agentsw"}${entry.name && entry.name !== entry.id ? ` · ${entry.name}` : ""}`,
|
|
170
|
+
value: index,
|
|
171
|
+
})),
|
|
172
|
+
],
|
|
166
173
|
}, cancel);
|
|
167
174
|
const picked = entries[selected];
|
|
168
175
|
if (!picked)
|
|
@@ -179,16 +186,19 @@ async function removeFromMenu() {
|
|
|
179
186
|
}
|
|
180
187
|
}
|
|
181
188
|
export async function cmdMenu() {
|
|
182
|
-
|
|
189
|
+
let store = loadStore();
|
|
190
|
+
if (!store.language) {
|
|
183
191
|
await chooseLanguage();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
store = loadStore(); // chooseLanguage persists the preference, so re-read before trusting the rest
|
|
193
|
+
}
|
|
194
|
+
out(pc.bold("agentsw") + pc.dim(t("menu.title")));
|
|
195
|
+
if (Object.keys(store.providers).length === 0) {
|
|
196
|
+
out(pc.yellow(t("menu.noProviders")));
|
|
187
197
|
if (await askToggle(t("menu.firstScan"), true))
|
|
188
198
|
await cmdImport({});
|
|
189
199
|
}
|
|
190
200
|
for (;;) {
|
|
191
|
-
|
|
201
|
+
out("");
|
|
192
202
|
const { action } = await prompts({
|
|
193
203
|
type: "select",
|
|
194
204
|
name: "action",
|
|
@@ -214,114 +224,124 @@ export async function cmdMenu() {
|
|
|
214
224
|
}, cancel);
|
|
215
225
|
if (action === undefined || action === "quit")
|
|
216
226
|
return;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
227
|
+
// One guard for every action: a cancelled prompt unwinds to here, and any other failure reports itself
|
|
228
|
+
// and returns to the menu instead of ending the session.
|
|
229
|
+
setInMenu(true);
|
|
230
|
+
try {
|
|
231
|
+
if (action === "quickAdd") {
|
|
232
|
+
await cmdQuickAdd({});
|
|
233
|
+
}
|
|
234
|
+
else if (action === "add") {
|
|
235
|
+
const { src } = await prompts({
|
|
236
|
+
type: "select",
|
|
237
|
+
name: "src",
|
|
238
|
+
message: t("menu.modelSource"),
|
|
239
|
+
hint: t("menu.selectInstructions"),
|
|
240
|
+
choices: [
|
|
241
|
+
{ title: t("menu.modelDiscover"), value: "discover" },
|
|
242
|
+
{ title: t("menu.modelManual"), value: "manual" },
|
|
243
|
+
],
|
|
244
|
+
}, cancel);
|
|
245
|
+
await cmdAdd({ discover: src === "discover" });
|
|
246
|
+
}
|
|
247
|
+
else if (action === "import") {
|
|
248
|
+
await cmdImport({});
|
|
249
|
+
}
|
|
250
|
+
else if (action === "use") {
|
|
251
|
+
const picked = await pickProvider(t("menu.pickProvider"));
|
|
252
|
+
if (!picked)
|
|
253
|
+
continue;
|
|
254
|
+
const apps = await pickApps(t("menu.pickApps"));
|
|
255
|
+
const { model } = await prompts({
|
|
256
|
+
type: picked.models.length > 1 ? "select" : null,
|
|
257
|
+
name: "model",
|
|
258
|
+
message: t("menu.defaultModel"),
|
|
259
|
+
hint: t("menu.selectInstructions"),
|
|
260
|
+
choices: [
|
|
261
|
+
{ title: t("menu.keepDefault", { model: picked.defaultModel }), value: "" },
|
|
262
|
+
...picked.models.filter((m) => m !== picked.defaultModel).map((m) => ({ title: m, value: m })),
|
|
263
|
+
],
|
|
264
|
+
}, cancel);
|
|
265
|
+
await cmdUse(picked.id, { model: model || undefined, apps });
|
|
266
|
+
}
|
|
267
|
+
else if (action === "status") {
|
|
268
|
+
cmdStatus();
|
|
269
|
+
}
|
|
270
|
+
else if (action === "list") {
|
|
271
|
+
cmdList();
|
|
272
|
+
}
|
|
273
|
+
else if (action === "sync") {
|
|
274
|
+
await cmdSync({ apps: await pickApps(t("menu.pickApps")) });
|
|
275
|
+
}
|
|
276
|
+
else if (action === "discover") {
|
|
277
|
+
const picked = await pickProvider(t("menu.discoverFor"));
|
|
278
|
+
if (!picked)
|
|
279
|
+
continue;
|
|
280
|
+
const sync = await askToggle(t("menu.pushRefresh"));
|
|
281
|
+
await cmdDiscover(picked.id, { sync, apps: sync ? await pickApps(t("menu.pickApps")) : undefined });
|
|
282
|
+
}
|
|
283
|
+
else if (action === "metadata") {
|
|
284
|
+
const picked = await pickProvider(t("menu.metadataProvider"));
|
|
285
|
+
if (!picked)
|
|
286
|
+
continue;
|
|
287
|
+
const current = loadStore().providers[picked.id];
|
|
288
|
+
const { metadataMode } = await prompts({
|
|
289
|
+
type: "select",
|
|
290
|
+
name: "metadataMode",
|
|
291
|
+
message: t("menu.metadataMode"),
|
|
292
|
+
hint: t("menu.selectInstructions"),
|
|
293
|
+
initial: ["auto", "on", "off"].indexOf(getMetadataMode(current)),
|
|
294
|
+
choices: [
|
|
295
|
+
{ title: t("menu.metadataAuto"), description: t("menu.metadataAutoHelp"), value: "auto" },
|
|
296
|
+
{ title: t("menu.metadataOn"), description: t("menu.metadataOnHelp"), value: "on" },
|
|
297
|
+
{ title: t("menu.metadataOff"), description: t("menu.metadataOffHelp"), value: "off" },
|
|
298
|
+
],
|
|
299
|
+
}, cancel);
|
|
300
|
+
await cmdRefreshMeta({ provider: picked.id, metadataMode });
|
|
301
|
+
}
|
|
302
|
+
else if (action === "rename" || action === "remove") {
|
|
290
303
|
if (action === "rename")
|
|
291
304
|
await renameFromMenu();
|
|
292
305
|
else
|
|
293
306
|
await removeFromMenu();
|
|
294
307
|
}
|
|
295
|
-
|
|
296
|
-
|
|
308
|
+
else if (action === "apps") {
|
|
309
|
+
await cmdApps();
|
|
310
|
+
if (await askToggle(t("menu.upgrade")))
|
|
311
|
+
await cmdUpgrade([]);
|
|
297
312
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
313
|
+
else if (action === "install") {
|
|
314
|
+
const installable = appPackages
|
|
315
|
+
.filter((a) => !installedVersion(a) && appCommand(a, "install"))
|
|
316
|
+
.map((a) => ({ title: `${a.id} · ${a.name}`, value: a.id }));
|
|
317
|
+
if (installable.length === 0) {
|
|
318
|
+
out(pc.dim(t("menu.allInstalled")));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const { appId } = await prompts({
|
|
322
|
+
type: "select",
|
|
323
|
+
name: "appId",
|
|
324
|
+
message: t("menu.pickApp"),
|
|
325
|
+
hint: t("menu.selectInstructions"),
|
|
326
|
+
choices: installable,
|
|
327
|
+
}, cancel);
|
|
328
|
+
if (!appId)
|
|
329
|
+
continue;
|
|
330
|
+
await cmdInstall(appId);
|
|
311
331
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
332
|
+
else if (action === "language") {
|
|
333
|
+
await chooseLanguage();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
// Ctrl-C inside a sub-prompt means "abandon this action", not "quit the app".
|
|
338
|
+
if (error instanceof ActionCancelled)
|
|
339
|
+
out(pc.dim(t("menu.cancelledAction")));
|
|
340
|
+
else
|
|
341
|
+
console.error(pc.red(error instanceof Error ? error.message : String(error)));
|
|
322
342
|
}
|
|
323
|
-
|
|
324
|
-
|
|
343
|
+
finally {
|
|
344
|
+
setInMenu(false);
|
|
325
345
|
}
|
|
326
346
|
}
|
|
327
347
|
}
|
package/dist/modelsdev.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { configDir } from "./store.js";
|
|
4
4
|
import { readJsonIfExists, writeFileAtomic } from "./fsutil.js";
|
|
5
|
+
import { warn } from "./ui.js";
|
|
5
6
|
const API_URL = "https://models.dev/api.json";
|
|
6
7
|
const CACHE_FILE = path.join(configDir, "models-dev.json");
|
|
7
8
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -10,29 +11,56 @@ const catalogFetchedAt = new WeakMap();
|
|
|
10
11
|
export function getCatalogFetchedAt(catalog) {
|
|
11
12
|
return catalogFetchedAt.get(catalog);
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* models.dev maps provider ids to providers, each carrying a model dictionary.
|
|
16
|
+
* A 200 response that is not shaped like a catalog (an error document from a proxy
|
|
17
|
+
* or mirror, say) must never be cached: later runs would then re-read the broken
|
|
18
|
+
* file on every load, long after the network recovered.
|
|
19
|
+
*/
|
|
20
|
+
function isCatalog(value) {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
22
|
+
return false;
|
|
23
|
+
return Object.values(value).every((provider) => {
|
|
24
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider))
|
|
25
|
+
return false;
|
|
26
|
+
const models = provider.models;
|
|
27
|
+
return !!models && typeof models === "object" && !Array.isArray(models);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** A cache file that is missing, unreadable or malformed is simply not a catalog. */
|
|
13
31
|
function readCachedCatalog() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = readJsonIfExists(CACHE_FILE);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
if (!isCatalog(parsed))
|
|
40
|
+
return undefined;
|
|
41
|
+
const catalog = parsed;
|
|
42
|
+
try {
|
|
43
|
+
catalogFetchedAt.set(catalog, fs.statSync(CACHE_FILE).mtime.toISOString());
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A concurrently removed cache still supplies useful metadata without a fetch time.
|
|
22
47
|
}
|
|
23
48
|
return catalog;
|
|
24
49
|
}
|
|
50
|
+
function cacheIsFresh() {
|
|
51
|
+
try {
|
|
52
|
+
return Date.now() - fs.statSync(CACHE_FILE).mtimeMs < CACHE_TTL_MS;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
25
58
|
export async function loadCatalog(opts = {}) {
|
|
26
59
|
if (!opts.refresh) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return readCachedCatalog();
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
/* no cache */
|
|
35
|
-
}
|
|
60
|
+
const cached = readCachedCatalog();
|
|
61
|
+
// A malformed cache falls through to a fetch rather than failing the command.
|
|
62
|
+
if (cached && (opts.offline || cacheIsFresh()))
|
|
63
|
+
return cached;
|
|
36
64
|
}
|
|
37
65
|
if (opts.offline)
|
|
38
66
|
return readCachedCatalog();
|
|
@@ -41,17 +69,19 @@ export async function loadCatalog(opts = {}) {
|
|
|
41
69
|
if (!res.ok)
|
|
42
70
|
throw new Error(`models.dev responded ${res.status}`);
|
|
43
71
|
const text = await res.text();
|
|
44
|
-
const
|
|
72
|
+
const parsed = JSON.parse(text);
|
|
73
|
+
if (!isCatalog(parsed))
|
|
74
|
+
throw new Error("models.dev returned an unexpected catalog shape");
|
|
45
75
|
writeFileAtomic(CACHE_FILE, text);
|
|
46
|
-
catalogFetchedAt.set(
|
|
47
|
-
return
|
|
76
|
+
catalogFetchedAt.set(parsed, new Date().toISOString());
|
|
77
|
+
return parsed;
|
|
48
78
|
}
|
|
49
79
|
catch (err) {
|
|
50
80
|
// network failure: fall back to stale cache if present
|
|
51
81
|
const cached = readCachedCatalog();
|
|
52
82
|
if (cached)
|
|
53
83
|
return cached;
|
|
54
|
-
|
|
84
|
+
warn(`could not fetch models.dev catalog: ${err.message}`);
|
|
55
85
|
return undefined;
|
|
56
86
|
}
|
|
57
87
|
}
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { isJson, isQuiet, note } from "./ui.js";
|
|
3
|
+
const FRAMES = ["|", "/", "-", "\\"];
|
|
4
|
+
const CLEAR = "\r\u001b[2K";
|
|
5
|
+
/** `npm test` frequently runs in a real terminal, where stderr is still a TTY — never animate under it. */
|
|
6
|
+
function isTestRunner() {
|
|
7
|
+
return process.env.NODE_TEST_CONTEXT !== undefined || process.argv.includes("--test");
|
|
8
|
+
}
|
|
9
|
+
export function progressEnabled(options = {}) {
|
|
10
|
+
if (options.enabled !== undefined)
|
|
11
|
+
return options.enabled;
|
|
12
|
+
const stream = options.stream ?? process.stderr;
|
|
13
|
+
return stream.isTTY === true
|
|
14
|
+
&& !isJson()
|
|
15
|
+
&& !isQuiet()
|
|
16
|
+
&& !isTestRunner()
|
|
17
|
+
&& process.env.AGENTSW_NO_PROGRESS !== "1";
|
|
18
|
+
}
|
|
19
|
+
let live;
|
|
20
|
+
/**
|
|
21
|
+
* Progress feedback for slow awaits. Renders one animated stderr line on a TTY; everywhere else it degrades
|
|
22
|
+
* to the single dim line the CLI printed before, so pipes and CI logs keep exactly the same content.
|
|
23
|
+
*/
|
|
24
|
+
export function startProgress(message, options = {}) {
|
|
25
|
+
// A nested caller borrows the live line: it may retitle it, but stopping must not tear down the owner.
|
|
26
|
+
if (live) {
|
|
27
|
+
const owner = live;
|
|
28
|
+
const previous = owner.setText(message);
|
|
29
|
+
let finished = false;
|
|
30
|
+
return {
|
|
31
|
+
update(next) { owner.setText(next); },
|
|
32
|
+
stop() {
|
|
33
|
+
if (finished)
|
|
34
|
+
return;
|
|
35
|
+
finished = true;
|
|
36
|
+
owner.setText(previous);
|
|
37
|
+
},
|
|
38
|
+
succeed() { },
|
|
39
|
+
fail() { },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const enabled = progressEnabled(options);
|
|
43
|
+
const stream = options.stream ?? process.stderr;
|
|
44
|
+
const intervalMs = options.intervalMs ?? 80;
|
|
45
|
+
const delayMs = options.delayMs ?? 150;
|
|
46
|
+
const frames = options.frames ?? FRAMES;
|
|
47
|
+
let text = message;
|
|
48
|
+
let frame = 0;
|
|
49
|
+
let timer;
|
|
50
|
+
let painted = false;
|
|
51
|
+
let stopped = false;
|
|
52
|
+
// Disabled path: reproduce the old static line exactly once. Callers no longer print it themselves.
|
|
53
|
+
if (!enabled)
|
|
54
|
+
note(message);
|
|
55
|
+
const clear = () => {
|
|
56
|
+
if (painted)
|
|
57
|
+
stream.write(CLEAR);
|
|
58
|
+
painted = false;
|
|
59
|
+
};
|
|
60
|
+
const paint = () => {
|
|
61
|
+
stream.write(`${CLEAR}${pc.dim(frames[frame % frames.length])} ${pc.dim(text)}`);
|
|
62
|
+
painted = true;
|
|
63
|
+
frame++;
|
|
64
|
+
};
|
|
65
|
+
const onInterrupt = () => {
|
|
66
|
+
clear();
|
|
67
|
+
process.exit(130);
|
|
68
|
+
};
|
|
69
|
+
const finish = () => {
|
|
70
|
+
if (stopped)
|
|
71
|
+
return;
|
|
72
|
+
stopped = true;
|
|
73
|
+
if (timer) {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
clearInterval(timer);
|
|
76
|
+
timer = undefined;
|
|
77
|
+
}
|
|
78
|
+
if (enabled)
|
|
79
|
+
process.removeListener("SIGINT", onInterrupt);
|
|
80
|
+
clear();
|
|
81
|
+
if (live?.handle === handle)
|
|
82
|
+
live = undefined;
|
|
83
|
+
};
|
|
84
|
+
const handle = {
|
|
85
|
+
update(next) { text = next; },
|
|
86
|
+
stop: finish,
|
|
87
|
+
succeed(message) { finish(); if (message)
|
|
88
|
+
note(message); },
|
|
89
|
+
fail(message) { finish(); if (message)
|
|
90
|
+
stream.write(pc.red(message) + "\n"); },
|
|
91
|
+
};
|
|
92
|
+
live = { handle, setText(next) { const previous = text; text = next; return previous; } };
|
|
93
|
+
if (enabled) {
|
|
94
|
+
process.once("SIGINT", onInterrupt);
|
|
95
|
+
// Nothing is painted until the delay elapses, so cached and local work never flickers.
|
|
96
|
+
timer = setTimeout(() => {
|
|
97
|
+
if (stopped)
|
|
98
|
+
return;
|
|
99
|
+
paint();
|
|
100
|
+
timer = setInterval(() => { if (!stopped)
|
|
101
|
+
paint(); }, intervalMs);
|
|
102
|
+
timer.unref();
|
|
103
|
+
}, delayMs);
|
|
104
|
+
timer.unref();
|
|
105
|
+
}
|
|
106
|
+
return handle;
|
|
107
|
+
}
|
|
108
|
+
export async function withProgress(message, fn, options) {
|
|
109
|
+
const handle = startProgress(message, options);
|
|
110
|
+
try {
|
|
111
|
+
return await fn(handle);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
handle.stop();
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/provider-actions.js
CHANGED
|
@@ -2,36 +2,56 @@ import pc from "picocolors";
|
|
|
2
2
|
import { t } from "./i18n.js";
|
|
3
3
|
import { renameProvider } from "./rename.js";
|
|
4
4
|
import { listRemovableProviders, removeProvider } from "./remove.js";
|
|
5
|
+
import { emitJson, isJson, note, out, table } from "./ui.js";
|
|
6
|
+
import { listAppsReport } from "./report.js";
|
|
5
7
|
function reportChanges(result, dryRun) {
|
|
6
|
-
|
|
8
|
+
out(t(dryRun ? "manage.preview" : "manage.changed", { count: result.files.length }));
|
|
7
9
|
for (const file of result.files)
|
|
8
|
-
|
|
10
|
+
out(` ${file}`);
|
|
9
11
|
if (result.backupDir)
|
|
10
|
-
|
|
12
|
+
out(pc.dim(t("manage.backup", { path: result.backupDir })));
|
|
11
13
|
}
|
|
12
14
|
export async function cmdRename(id, newId, opts = {}) {
|
|
13
15
|
const result = await renameProvider(id, newId, opts);
|
|
16
|
+
if (isJson()) {
|
|
17
|
+
emitJson({ id, newId, dryRun: opts.dryRun === true, files: result.files, backupDir: result.backupDir ?? null });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
14
20
|
if (!opts.dryRun)
|
|
15
|
-
|
|
21
|
+
out(pc.green(t("rename.done", { oldId: id, newId })));
|
|
16
22
|
reportChanges(result, opts.dryRun);
|
|
17
23
|
}
|
|
18
24
|
export async function cmdRemoveProvider(id, opts = {}) {
|
|
19
25
|
const result = await removeProvider(id, opts);
|
|
26
|
+
if (isJson()) {
|
|
27
|
+
emitJson({
|
|
28
|
+
id,
|
|
29
|
+
apps: opts.apps ?? null,
|
|
30
|
+
prune: opts.prune === true,
|
|
31
|
+
dryRun: opts.dryRun === true,
|
|
32
|
+
files: result.files,
|
|
33
|
+
backupDir: result.backupDir ?? null,
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
20
37
|
if (!opts.dryRun) {
|
|
21
|
-
|
|
38
|
+
out(pc.green(opts.apps
|
|
22
39
|
? t("remove.localDone", { id, apps: opts.apps })
|
|
23
40
|
: t("remove.removed", { id })));
|
|
24
41
|
if (!opts.apps && !opts.prune)
|
|
25
|
-
|
|
42
|
+
note(t("remove.note"));
|
|
26
43
|
}
|
|
27
44
|
reportChanges(result, opts.dryRun);
|
|
28
45
|
}
|
|
29
46
|
export function cmdListLocalProviders(apps) {
|
|
30
47
|
const rows = listRemovableProviders(apps);
|
|
48
|
+
if (isJson()) {
|
|
49
|
+
emitJson(listAppsReport(apps, rows));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
31
52
|
if (!rows.length) {
|
|
32
|
-
|
|
53
|
+
out(t("list.localNone"));
|
|
33
54
|
return;
|
|
34
55
|
}
|
|
35
|
-
|
|
36
|
-
console.log(`${row.app.padEnd(12)} ${row.id}${row.name && row.name !== row.id ? ` · ${row.name}` : ""}`);
|
|
56
|
+
out(table(rows.map((row) => [row.app, row.id, row.name && row.name !== row.id ? row.name : ""]), undefined, { truncate: [2] }));
|
|
37
57
|
}
|