agentsw 0.6.2 → 0.7.1
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 +22 -0
- package/README.zh-CN.md +21 -0
- package/dist/commands.js +30 -40
- package/dist/config-transaction.js +138 -0
- package/dist/envfile.js +27 -0
- package/dist/i18n.js +74 -31
- package/dist/import.js +35 -21
- package/dist/index.js +16 -3
- package/dist/menu.js +111 -24
- package/dist/provider-actions.js +37 -0
- package/dist/remove.js +651 -0
- package/dist/rename.js +458 -0
- package/dist/slug.js +17 -8
- package/dist/sources/ccswitch.js +4 -2
- package/dist/targets/claudecode.js +4 -3
- package/dist/targets/workbuddy.js +11 -10
- package/package.json +1 -1
package/dist/import.js
CHANGED
|
@@ -1,38 +1,52 @@
|
|
|
1
1
|
import { loadStore } from "./store.js";
|
|
2
2
|
import { ccSwitchCandidates } from "./sources/ccswitch.js";
|
|
3
3
|
import { targets } from "./targets/index.js";
|
|
4
|
+
/** Normalize host casing and trailing path slashes without rewriting paths or explicit ports. */
|
|
4
5
|
export function normalizeUrl(u) {
|
|
5
|
-
|
|
6
|
+
const parts = /^([a-z][a-z0-9+.-]*:\/\/)([^/?#]*)([^?#]*)([?#].*)?$/i.exec(u);
|
|
7
|
+
if (!parts)
|
|
8
|
+
return u.replace(/\/+$/, "");
|
|
9
|
+
const [, scheme, authority, pathname, suffix] = parts;
|
|
10
|
+
const at = authority.lastIndexOf("@") + 1;
|
|
11
|
+
return `${scheme.toLowerCase()}${authority.slice(0, at)}${authority.slice(at).toLowerCase()}${pathname.replace(/\/+$/, "")}${suffix ?? ""}`;
|
|
6
12
|
}
|
|
7
|
-
/** True when
|
|
8
|
-
const hasApiVersion = (baseUrl) => /\/v\d+(?:beta\d*)
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
/** True when the path already names the API version, e.g. `.../v1`. */
|
|
14
|
+
const hasApiVersion = (baseUrl) => /\/v\d+(?:beta\d*)?$/.test(baseUrl.split(/[?#]/, 1)[0]);
|
|
15
|
+
/** Apps may append their own API version; all other URL components remain significant. */
|
|
16
|
+
export function endpointKey(baseUrl) {
|
|
17
|
+
const normalized = normalizeUrl(baseUrl);
|
|
18
|
+
const suffixAt = normalized.search(/[?#]/);
|
|
19
|
+
const path = suffixAt < 0 ? normalized : normalized.slice(0, suffixAt);
|
|
20
|
+
return path.replace(/\/v\d+(?:beta\d*)?$/, "") + (suffixAt < 0 ? "" : normalized.slice(suffixAt));
|
|
21
|
+
}
|
|
22
|
+
/** An unresolved credential is not evidence that two configurations use the same account. */
|
|
23
|
+
export function findMatchingProvider(existing, candidate) {
|
|
24
|
+
if (!candidate.apiKey)
|
|
25
|
+
return undefined;
|
|
26
|
+
const endpoint = endpointKey(candidate.baseUrl);
|
|
27
|
+
return existing.find((p) => p.protocol === candidate.protocol && p.apiKey === candidate.apiKey && endpointKey(p.baseUrl) === endpoint);
|
|
28
|
+
}
|
|
29
|
+
/** Pure merge: union models only within the same credential-qualified endpoint. */
|
|
18
30
|
export function mergeCandidates(rows, existing) {
|
|
19
31
|
const merged = new Map();
|
|
20
32
|
for (const { source, ...c } of rows) {
|
|
21
|
-
const
|
|
33
|
+
const credentials = c.apiKey ? ["literal", c.apiKey] : c.keyEnv ? ["env", c.keyEnv] : ["missing", source, c.id];
|
|
34
|
+
const key = JSON.stringify([c.protocol, endpointKey(c.baseUrl), credentials]);
|
|
22
35
|
const cur = merged.get(key);
|
|
23
36
|
if (!cur) {
|
|
24
37
|
merged.set(key, { ...c, baseUrl: normalizeUrl(c.baseUrl), models: [...new Set(c.models)], sources: [source] });
|
|
25
38
|
continue;
|
|
26
39
|
}
|
|
40
|
+
// Explicit app identities outrank endpoint-derived suggestions, regardless of scan order.
|
|
41
|
+
if (cur.generatedId && !c.generatedId) {
|
|
42
|
+
cur.id = c.id;
|
|
43
|
+
cur.name = c.name;
|
|
44
|
+
delete cur.generatedId;
|
|
45
|
+
}
|
|
27
46
|
if (!cur.sources.includes(source))
|
|
28
47
|
cur.sources.push(source);
|
|
29
|
-
if (!cur.
|
|
30
|
-
cur.apiKey = c.apiKey;
|
|
48
|
+
if (!cur.keyEnv && c.keyEnv)
|
|
31
49
|
cur.keyEnv = c.keyEnv;
|
|
32
|
-
}
|
|
33
|
-
else if (!cur.keyEnv && c.keyEnv) {
|
|
34
|
-
cur.keyEnv = c.keyEnv;
|
|
35
|
-
}
|
|
36
50
|
for (const m of c.models)
|
|
37
51
|
if (!cur.models.includes(m))
|
|
38
52
|
cur.models.push(m);
|
|
@@ -49,7 +63,7 @@ export function mergeCandidates(rows, existing) {
|
|
|
49
63
|
}
|
|
50
64
|
const out = [...merged.values()];
|
|
51
65
|
for (const m of out) {
|
|
52
|
-
const hit = existing
|
|
66
|
+
const hit = findMatchingProvider(existing, m);
|
|
53
67
|
if (hit)
|
|
54
68
|
m.configured = hit.id;
|
|
55
69
|
}
|
|
@@ -75,5 +89,5 @@ export function scanCandidates() {
|
|
|
75
89
|
// a cc-switch store this build cannot read is not this scan's problem
|
|
76
90
|
}
|
|
77
91
|
const store = loadStore();
|
|
78
|
-
return mergeCandidates(rows, Object.values(store.providers)
|
|
92
|
+
return mergeCandidates(rows, Object.values(store.providers));
|
|
79
93
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { Command, InvalidArgumentError } from "commander";
|
|
4
|
-
import { cmdAdd, cmdQuickAdd, cmdApps, cmdDiscover, cmdImport, cmdInstall, cmdList, cmdModels, cmdRefreshMeta, cmdPrune,
|
|
4
|
+
import { cmdAdd, cmdQuickAdd, cmdApps, cmdDiscover, cmdImport, cmdInstall, cmdList, cmdModels, cmdRefreshMeta, cmdPrune, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
|
|
5
5
|
import { detectSystemLocale, extractCliLocale, normalizeLocale, setLocale, t } from "./i18n.js";
|
|
6
6
|
import { cmdMenu } from "./menu.js";
|
|
7
7
|
import { loadStore } from "./store.js";
|
|
8
|
+
import { cmdListLocalProviders, cmdRemoveProvider, cmdRename } from "./provider-actions.js";
|
|
8
9
|
// Locale precedence: CLI flag > environment > saved preference > system locale.
|
|
9
10
|
setLocale(extractCliLocale() ?? process.env.AGENTSW_LANG ?? loadStore().language ?? detectSystemLocale());
|
|
10
11
|
function parseLocale(value) {
|
|
@@ -68,13 +69,25 @@ program
|
|
|
68
69
|
.option("--no-dedup", t("opt.noDedup"))
|
|
69
70
|
.option("-y, --yes", t("opt.yes"))
|
|
70
71
|
.action(cmdQuickAdd);
|
|
71
|
-
program
|
|
72
|
+
program
|
|
73
|
+
.command("list")
|
|
74
|
+
.alias("ls")
|
|
75
|
+
.description(t("cmd.list"))
|
|
76
|
+
.option("-a, --apps <apps>", t("opt.apps"))
|
|
77
|
+
.action((opts) => opts.apps ? cmdListLocalProviders(opts.apps) : cmdList());
|
|
78
|
+
program
|
|
79
|
+
.command("rename <id> <new-id>")
|
|
80
|
+
.description(t("cmd.rename"))
|
|
81
|
+
.option("-n, --dry-run", t("opt.manageDryRun"))
|
|
82
|
+
.action(cmdRename);
|
|
72
83
|
program
|
|
73
84
|
.command("remove <id>")
|
|
74
85
|
.alias("rm")
|
|
75
86
|
.description(t("cmd.remove"))
|
|
76
87
|
.option("--prune", t("opt.prune"))
|
|
77
|
-
.
|
|
88
|
+
.option("-a, --apps <apps>", t("opt.removeApps"))
|
|
89
|
+
.option("-n, --dry-run", t("opt.manageDryRun"))
|
|
90
|
+
.action(cmdRemoveProvider);
|
|
78
91
|
program
|
|
79
92
|
.command("prune <id>")
|
|
80
93
|
.description(t("cmd.prune"))
|
package/dist/menu.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import prompts from "prompts";
|
|
3
|
-
import { cmdAdd, cmdQuickAdd, cmdApps, cmdInstall, cmdDiscover, cmdImport, cmdList,
|
|
3
|
+
import { cmdAdd, cmdQuickAdd, cmdApps, cmdInstall, cmdDiscover, cmdImport, cmdList, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
|
|
4
4
|
import { getLocale, setLocale, t } from "./i18n.js";
|
|
5
5
|
import { loadStore, saveStore } from "./store.js";
|
|
6
6
|
import { appPackages, appCommand, installedVersion } from "./apps.js";
|
|
7
|
+
import { cmdRemoveProvider, cmdRename } from "./provider-actions.js";
|
|
8
|
+
import { listRemovableProviders } from "./remove.js";
|
|
9
|
+
import { providerIdFromBaseUrl } from "./slug.js";
|
|
10
|
+
import { targets } from "./targets/index.js";
|
|
7
11
|
function bye() {
|
|
8
12
|
console.log(pc.dim(`\n${t("menu.bye")}`));
|
|
9
13
|
process.exit(0);
|
|
10
14
|
}
|
|
11
15
|
const cancel = { onCancel: () => bye() };
|
|
12
|
-
async function askToggle(message, initial = false) {
|
|
13
|
-
const { v } = await prompts({ type: "toggle", name: "v", message, active: t("menu.yes"), inactive: t("menu.no"), initial }, cancel);
|
|
16
|
+
async function askToggle(message, initial = false, labels) {
|
|
17
|
+
const { v } = await prompts({ type: "toggle", name: "v", message, active: labels?.active ?? t("menu.yes"), inactive: labels?.inactive ?? t("menu.no"), initial }, cancel);
|
|
14
18
|
return v === true;
|
|
15
19
|
}
|
|
16
20
|
async function chooseLanguage() {
|
|
@@ -57,6 +61,86 @@ async function pickProvider(message) {
|
|
|
57
61
|
const p = store.providers[id];
|
|
58
62
|
return { id, defaultModel: p.defaultModel, models: p.models.map((m) => m.id) };
|
|
59
63
|
}
|
|
64
|
+
async function renameFromMenu() {
|
|
65
|
+
const picked = await pickProvider(t("menu.renameProvider"));
|
|
66
|
+
if (!picked)
|
|
67
|
+
return;
|
|
68
|
+
const provider = loadStore().providers[picked.id];
|
|
69
|
+
const { newId } = await prompts({
|
|
70
|
+
type: "text",
|
|
71
|
+
name: "newId",
|
|
72
|
+
message: t("menu.newId"),
|
|
73
|
+
initial: providerIdFromBaseUrl(provider.baseUrl, provider.protocol),
|
|
74
|
+
validate: (value) => /^[a-z0-9][a-z0-9_-]*$/.test(value.trim()) || t("add.idInvalid"),
|
|
75
|
+
}, cancel);
|
|
76
|
+
if (!newId)
|
|
77
|
+
return;
|
|
78
|
+
const next = String(newId).trim();
|
|
79
|
+
await cmdRename(picked.id, next, { dryRun: true });
|
|
80
|
+
if (await askToggle(t("menu.renameConfirm", { oldId: picked.id, newId: next }), false, {
|
|
81
|
+
active: t("menu.confirmRename"), inactive: t("menu.cancelAction"),
|
|
82
|
+
})) {
|
|
83
|
+
await cmdRename(picked.id, next);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function removeFromMenu() {
|
|
87
|
+
const { scope } = await prompts({
|
|
88
|
+
type: "select",
|
|
89
|
+
name: "scope",
|
|
90
|
+
message: t("menu.removeScope"),
|
|
91
|
+
hint: t("menu.selectInstructions"),
|
|
92
|
+
choices: [
|
|
93
|
+
{ title: t("menu.removeStore"), description: t("menu.removeStoreHelp"), value: "store" },
|
|
94
|
+
{ title: t("menu.removeLocal"), description: t("menu.removeLocalHelp"), value: "local" },
|
|
95
|
+
{ title: t("menu.removeEverywhere"), description: t("menu.removeEverywhereHelp"), value: "everywhere" },
|
|
96
|
+
],
|
|
97
|
+
}, cancel);
|
|
98
|
+
if (!scope)
|
|
99
|
+
return;
|
|
100
|
+
let app;
|
|
101
|
+
if (scope === "local") {
|
|
102
|
+
const answer = await prompts({
|
|
103
|
+
type: "select",
|
|
104
|
+
name: "app",
|
|
105
|
+
message: t("menu.removeApp"),
|
|
106
|
+
hint: t("menu.selectInstructions"),
|
|
107
|
+
choices: targets.map((target) => ({ title: `${target.id} · ${target.name}`, value: target.id })),
|
|
108
|
+
}, cancel);
|
|
109
|
+
app = answer.app;
|
|
110
|
+
if (!app)
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const entries = app
|
|
114
|
+
? listRemovableProviders(app)
|
|
115
|
+
: Object.values(loadStore().providers).map((provider) => ({ id: provider.id, name: provider.name }));
|
|
116
|
+
if (!entries.length) {
|
|
117
|
+
console.log(pc.dim(t("menu.noRemovable")));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const { selected } = await prompts({
|
|
121
|
+
type: "select",
|
|
122
|
+
name: "selected",
|
|
123
|
+
message: t("menu.removeProvider"),
|
|
124
|
+
hint: t("menu.selectInstructions"),
|
|
125
|
+
choices: entries.map((entry, index) => ({
|
|
126
|
+
title: `${entry.id} · ${entry.app ?? "agentsw"}${entry.name && entry.name !== entry.id ? ` · ${entry.name}` : ""}`,
|
|
127
|
+
value: index,
|
|
128
|
+
})),
|
|
129
|
+
}, cancel);
|
|
130
|
+
const picked = entries[selected];
|
|
131
|
+
if (!picked)
|
|
132
|
+
return;
|
|
133
|
+
const opts = scope === "local" ? { apps: picked.app } : { prune: scope === "everywhere" };
|
|
134
|
+
await cmdRemoveProvider(picked.id, { ...opts, dryRun: true });
|
|
135
|
+
const confirmation = picked.app
|
|
136
|
+
? t("menu.removeConfirmLocal", { id: picked.id, app: picked.app })
|
|
137
|
+
: t(scope === "everywhere" ? "menu.removeConfirmEverywhere" : "menu.removeConfirmStore", { id: picked.id });
|
|
138
|
+
if (await askToggle(confirmation, false, {
|
|
139
|
+
active: t("menu.confirmRemove"), inactive: t("menu.cancelAction"),
|
|
140
|
+
})) {
|
|
141
|
+
await cmdRemoveProvider(picked.id, opts);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
60
144
|
export async function cmdMenu() {
|
|
61
145
|
if (!loadStore().language)
|
|
62
146
|
await chooseLanguage();
|
|
@@ -74,19 +158,20 @@ export async function cmdMenu() {
|
|
|
74
158
|
message: t("menu.what"),
|
|
75
159
|
hint: t("menu.selectInstructions"),
|
|
76
160
|
choices: [
|
|
77
|
-
{ title: t("menu.
|
|
78
|
-
{ title: t("menu.
|
|
79
|
-
{ title: t("menu.import"), value: "import" },
|
|
80
|
-
{ title: t("menu.use"), value: "use" },
|
|
81
|
-
{ title: t("menu.status"), value: "status" },
|
|
82
|
-
{ title: t("menu.list"), value: "list" },
|
|
83
|
-
{ title: t("menu.sync"), value: "sync" },
|
|
84
|
-
{ title: t("menu.discover"), value: "discover" },
|
|
85
|
-
{ title: t("menu.
|
|
86
|
-
{ title: t("menu.
|
|
87
|
-
{ title: t("menu.
|
|
88
|
-
{ title: t("menu.
|
|
89
|
-
{ title: t("menu.
|
|
161
|
+
{ title: t("menu.quickAdd"), description: t("menu.quickAddHelp"), value: "quickAdd" },
|
|
162
|
+
{ title: t("menu.add"), description: t("menu.addHelp"), value: "add" },
|
|
163
|
+
{ title: t("menu.import"), description: t("menu.importHelp"), value: "import" },
|
|
164
|
+
{ title: t("menu.use"), description: t("menu.useHelp"), value: "use" },
|
|
165
|
+
{ title: t("menu.status"), description: t("menu.statusHelp"), value: "status" },
|
|
166
|
+
{ title: t("menu.list"), description: t("menu.listHelp"), value: "list" },
|
|
167
|
+
{ title: t("menu.sync"), description: t("menu.syncHelp"), value: "sync" },
|
|
168
|
+
{ title: t("menu.discover"), description: t("menu.discoverHelp"), value: "discover" },
|
|
169
|
+
{ title: t("menu.rename"), description: t("menu.renameHelp"), value: "rename" },
|
|
170
|
+
{ title: t("menu.remove"), description: t("menu.removeHelp"), value: "remove" },
|
|
171
|
+
{ title: t("menu.apps"), description: t("menu.appsHelp"), value: "apps" },
|
|
172
|
+
{ title: t("menu.installApp"), description: t("menu.installAppHelp"), value: "install" },
|
|
173
|
+
{ title: t("menu.language"), description: t("menu.languageHelp"), value: "language" },
|
|
174
|
+
{ title: t("menu.quit"), description: t("menu.quitHelp"), value: "quit" },
|
|
90
175
|
],
|
|
91
176
|
}, cancel);
|
|
92
177
|
if (action === undefined || action === "quit")
|
|
@@ -142,14 +227,16 @@ export async function cmdMenu() {
|
|
|
142
227
|
const sync = await askToggle(t("menu.pushRefresh"));
|
|
143
228
|
await cmdDiscover(picked.id, { sync });
|
|
144
229
|
}
|
|
145
|
-
else if (action === "remove") {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
230
|
+
else if (action === "rename" || action === "remove") {
|
|
231
|
+
try {
|
|
232
|
+
if (action === "rename")
|
|
233
|
+
await renameFromMenu();
|
|
234
|
+
else
|
|
235
|
+
await removeFromMenu();
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
console.error(pc.red(error instanceof Error ? error.message : String(error)));
|
|
239
|
+
}
|
|
153
240
|
}
|
|
154
241
|
else if (action === "apps") {
|
|
155
242
|
await cmdApps();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { t } from "./i18n.js";
|
|
3
|
+
import { renameProvider } from "./rename.js";
|
|
4
|
+
import { listRemovableProviders, removeProvider } from "./remove.js";
|
|
5
|
+
function reportChanges(result, dryRun) {
|
|
6
|
+
console.log(t(dryRun ? "manage.preview" : "manage.changed", { count: result.files.length }));
|
|
7
|
+
for (const file of result.files)
|
|
8
|
+
console.log(` ${file}`);
|
|
9
|
+
if (result.backupDir)
|
|
10
|
+
console.log(pc.dim(t("manage.backup", { path: result.backupDir })));
|
|
11
|
+
}
|
|
12
|
+
export async function cmdRename(id, newId, opts = {}) {
|
|
13
|
+
const result = await renameProvider(id, newId, opts);
|
|
14
|
+
if (!opts.dryRun)
|
|
15
|
+
console.log(pc.green(t("rename.done", { oldId: id, newId })));
|
|
16
|
+
reportChanges(result, opts.dryRun);
|
|
17
|
+
}
|
|
18
|
+
export async function cmdRemoveProvider(id, opts = {}) {
|
|
19
|
+
const result = await removeProvider(id, opts);
|
|
20
|
+
if (!opts.dryRun) {
|
|
21
|
+
console.log(pc.green(opts.apps
|
|
22
|
+
? t("remove.localDone", { id, apps: opts.apps })
|
|
23
|
+
: t("remove.removed", { id })));
|
|
24
|
+
if (!opts.apps && !opts.prune)
|
|
25
|
+
console.log(pc.dim(t("remove.note")));
|
|
26
|
+
}
|
|
27
|
+
reportChanges(result, opts.dryRun);
|
|
28
|
+
}
|
|
29
|
+
export function cmdListLocalProviders(apps) {
|
|
30
|
+
const rows = listRemovableProviders(apps);
|
|
31
|
+
if (!rows.length) {
|
|
32
|
+
console.log(t("list.localNone"));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const row of rows)
|
|
36
|
+
console.log(`${row.app.padEnd(12)} ${row.id}${row.name && row.name !== row.id ? ` · ${row.name}` : ""}`);
|
|
37
|
+
}
|