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/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
- return u.replace(/\/+$/, "");
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 a base URL already names the API version, e.g. `.../v1`. */
8
- const hasApiVersion = (baseUrl) => /\/v\d+(?:beta\d*)?$/i.test(baseUrl);
9
- /**
10
- * Endpoint identity for dedupe. Apps disagree about who owns the `/v1` segment:
11
- * omp/pi/opencode store it in the base URL, while Codex (and cc-switch's Codex
12
- * rows) leave it off because the client appends it. `https://host` and
13
- * `https://host/v1` on one protocol are the same upstream, so they must merge
14
- * into one provider instead of importing twice.
15
- */
16
- const matchKey = (protocol, baseUrl) => `${protocol}|${normalizeUrl(baseUrl).replace(/\/v\d+(?:beta\d*)?$/i, "")}`;
17
- /** pure merge: dedupe candidates by protocol+baseUrl, union models, keep first resolved key */
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 key = matchKey(c.protocol, c.baseUrl);
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.apiKey && c.apiKey) {
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.find((e) => matchKey(e.protocol, e.baseUrl) === matchKey(m.protocol, m.baseUrl));
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).map((p) => ({ id: p.id, protocol: p.protocol, baseUrl: p.baseUrl })));
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, cmdRemove, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
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.command("list").alias("ls").description(t("cmd.list")).action(cmdList);
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
- .action(cmdRemove);
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, cmdRemove, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
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.add"), value: "add" },
78
- { title: t("menu.quickAdd"), value: "quickAdd" },
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.remove"), value: "remove" },
86
- { title: t("menu.apps"), value: "apps" },
87
- { title: t("menu.installApp"), value: "install" },
88
- { title: t("menu.language"), value: "language" },
89
- { title: t("menu.quit"), value: "quit" },
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
- const picked = await pickProvider(t("menu.removeProvider"));
147
- if (!picked)
148
- continue;
149
- if (!(await askToggle(t("menu.reallyRemove", { id: pc.bold(picked.id) }))))
150
- continue;
151
- const prune = await askToggle(t("menu.pruneConfigs"));
152
- await cmdRemove(picked.id, { prune });
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
+ }