agentsw 0.3.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/dist/filter.js ADDED
@@ -0,0 +1,61 @@
1
+ function globToRegex(glob) {
2
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
3
+ // bare substring patterns match anywhere; wildcards anchor the whole id
4
+ return glob.includes("*") || glob.includes("?") ? new RegExp(`^${escaped}$`, "i") : new RegExp(escaped, "i");
5
+ }
6
+ /**
7
+ * Strip snapshot noise from a model id to get its dedup group key:
8
+ * trailing -latest, -YYYYMMDD, -YYYY-MM-DD, -MMDD, @YYYYMMDD, and date-stamped tails
9
+ * like "-250414". Real variants (-air, -flash, -thinking, -mini, :free) are preserved.
10
+ */
11
+ export function snapshotBase(id) {
12
+ return id
13
+ .replace(/[-@.](20\d{6}|\d{6}|\d{4}-\d{2}-\d{2})$/, "")
14
+ .replace(/-(latest|\d{4})$/, "")
15
+ .toLowerCase();
16
+ }
17
+ /**
18
+ * Apply include/exclude globs, then drop snapshot duplicates.
19
+ * A suffixed id (-latest, date stamps) is a duplicate only when its bare base id is
20
+ * also listed; snapshot-only models are kept as-is (no collapsing onto a "winner").
21
+ * Dropping duplicates is the DEFAULT; disable with filter.dedup === false.
22
+ * `pinned` ids (explicit --models entries, current default model) are never dropped.
23
+ */
24
+ export function applyModelFilter(ids, filter, pinned = []) {
25
+ const includes = (filter?.include ?? []).map(globToRegex);
26
+ const excludes = (filter?.exclude ?? []).map(globToRegex);
27
+ const dropped = [];
28
+ let kept = ids.filter((id) => {
29
+ if (pinned.includes(id))
30
+ return true;
31
+ if (includes.length && !includes.some((re) => re.test(id))) {
32
+ dropped.push({ id, reason: "not in --include" });
33
+ return false;
34
+ }
35
+ const hit = excludes.find((re) => re.test(id));
36
+ if (hit) {
37
+ dropped.push({ id, reason: `--exclude ${hit.source}` });
38
+ return false;
39
+ }
40
+ return true;
41
+ });
42
+ if (filter?.dedup !== false) {
43
+ const present = {};
44
+ for (const id of kept)
45
+ present[id.toLowerCase()] = id;
46
+ kept = kept.filter((id) => {
47
+ if (pinned.includes(id))
48
+ return true;
49
+ const base = snapshotBase(id);
50
+ if (base === id.toLowerCase())
51
+ return true; // already a bare id
52
+ const bare = present[base];
53
+ if (bare !== undefined) {
54
+ dropped.push({ id, reason: `duplicate of ${bare}` });
55
+ return false;
56
+ }
57
+ return true; // snapshot-only model: nothing it duplicates
58
+ });
59
+ }
60
+ return { kept, dropped };
61
+ }
package/dist/fsutil.js ADDED
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export const home = os.homedir();
5
+ export function expandHome(p) {
6
+ return p.startsWith("~") ? path.join(home, p.slice(1)) : p;
7
+ }
8
+ export function ensureDir(dir) {
9
+ fs.mkdirSync(dir, { recursive: true });
10
+ }
11
+ export function readTextIfExists(file) {
12
+ try {
13
+ return fs.readFileSync(file, "utf8");
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
19
+ export function readJsonIfExists(file) {
20
+ const text = readTextIfExists(file);
21
+ if (text === undefined)
22
+ return undefined;
23
+ return JSON.parse(text);
24
+ }
25
+ /** dry-run mode: writeFileAtomic records intents instead of touching disk; backups are skipped */
26
+ let dryRun = false;
27
+ const pendingWrites = [];
28
+ export function setDryRun(on) {
29
+ dryRun = on;
30
+ pendingWrites.length = 0;
31
+ }
32
+ export function drainPendingWrites() {
33
+ return pendingWrites.splice(0);
34
+ }
35
+ /** Atomic write: tmp file + rename. Creates parent dirs. In dry-run mode, records the intent instead. */
36
+ export function writeFileAtomic(file, content, mode) {
37
+ if (dryRun) {
38
+ pendingWrites.push({ file, content });
39
+ return;
40
+ }
41
+ ensureDir(path.dirname(file));
42
+ const tmp = `${file}.tmp-${process.pid}`;
43
+ fs.writeFileSync(tmp, content, mode !== undefined ? { mode } : undefined);
44
+ fs.renameSync(tmp, file);
45
+ }
46
+ export const backupsDir = path.join(home, ".config", "agentsw", "backups");
47
+ /** Copy `file` into the backups dir with a timestamp suffix. No-op if file missing. */
48
+ export function backupFile(file) {
49
+ if (!fs.existsSync(file) || dryRun)
50
+ return undefined;
51
+ ensureDir(backupsDir);
52
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
53
+ const dest = path.join(backupsDir, `${path.basename(file)}.${stamp}`);
54
+ fs.copyFileSync(file, dest);
55
+ return dest;
56
+ }
package/dist/i18n.js ADDED
@@ -0,0 +1,214 @@
1
+ const messages = {
2
+ "root.description": {
3
+ en: "Manage OpenAI/Anthropic-protocol model providers and sync them into coding agents\n(Claude Code, Codex, omp, pi, prime-agent, opencode, Hermes, WorkBuddy, DeepSeek Harness).\nModel metadata (context window, input/output limits, reasoning levels) is enriched from\nmodels.dev; existing providers can be imported from the agents and from cc-switch.\n\nRun without arguments (or via `npx agentsw`) for the interactive menu.",
4
+ "zh-CN": "管理 OpenAI / Anthropic 协议的模型供应商并同步到编码智能体\n(Claude Code、Codex、omp、pi、prime-agent、opencode、Hermes、WorkBuddy、DeepSeek Harness)。\n模型元数据(上下文、输入/输出上限、推理等级)由 models.dev 补全;\n也可从各智能体与 cc-switch 导入已有供应商。\n\n不带参数运行(或 `npx agentsw`)即可打开交互菜单。",
5
+ },
6
+ "help.language": { en: "UI language: en | zh-CN (also AGENTSW_LANG)", "zh-CN": "界面语言: en | zh-CN (也可用 AGENTSW_LANG)" },
7
+ "error.language": { en: "unsupported language \"{value}\"; use en or zh-CN", "zh-CN": "不支持语言 \"{value}\";请使用 en 或 zh-CN" },
8
+ "error.unknownCommand": { en: "unknown command '{value}', see --help", "zh-CN": "未知命令 '{value}',请查看 --help" },
9
+ "cmd.add": { en: "add or update a provider (interactive when flags are omitted)", "zh-CN": "添加或更新供应商(省略参数时进入交互流程)" },
10
+ "cmd.list": { en: "list configured providers", "zh-CN": "列出已配置供应商" },
11
+ "cmd.remove": { en: "remove a provider", "zh-CN": "删除供应商" },
12
+ "cmd.prune": { en: "remove a provider's entries from app configs (keeps it in agentsw)", "zh-CN": "从应用配置移除供应商条目(仍保留在 agentsw)" },
13
+ "cmd.use": { en: "set the active provider and write it into app configs", "zh-CN": "设为当前供应商并写入应用配置" },
14
+ "cmd.sync": { en: "re-apply the active provider (or --provider) to app configs", "zh-CN": "将当前供应商(或 --provider)重新应用到应用配置" },
15
+ "cmd.status": { en: "show detected apps and what they currently point at", "zh-CN": "显示检测到的应用及当前供应商" },
16
+ "cmd.models": { en: "search models.dev, or show a configured provider's models", "zh-CN": "搜索 models.dev 或显示已配置供应商的模型" },
17
+ "cmd.discover": { en: "refresh a provider's models via /v1/models and models.dev", "zh-CN": "通过 /v1/models 和 models.dev 刷新供应商模型" },
18
+ "cmd.import": { en: "scan app configs, preview and dedupe existing custom providers, then import", "zh-CN": "扫描应用配置,预览并去重已有自定义供应商后导入" },
19
+ "cmd.apps": { en: "check installed agents: current version vs latest", "zh-CN": "检查智能体已安装版本与最新版本" },
20
+ "cmd.install": { en: "install an agent CLI", "zh-CN": "安装智能体 CLI" },
21
+ "cmd.upgrade": { en: "upgrade agent CLIs (no args = everything outdated)", "zh-CN": "升级智能体 CLI(无参数时升级全部过期项)" },
22
+ "cmd.refresh": { en: "re-fetch models.dev metadata for all providers", "zh-CN": "重新拉取所有供应商的 models.dev 元数据" },
23
+ "opt.id": { en: "provider id, e.g. glm, my-proxy", "zh-CN": "供应商 id,例如 glm、my-proxy" },
24
+ "opt.name": { en: "display name", "zh-CN": "显示名称" },
25
+ "opt.protocol": { en: "wire protocol: openai | anthropic", "zh-CN": "接口协议: openai | anthropic" },
26
+ "opt.openaiApi": {
27
+ en: "openai endpoint flavor: completions | responses (default: completions)",
28
+ "zh-CN": "openai 接口形态: completions | responses(默认 completions)",
29
+ },
30
+ "opt.baseUrl": { en: "API base URL", "zh-CN": "API base URL" },
31
+ "opt.apiKey": { en: "API key", "zh-CN": "API key" },
32
+ "opt.models": { en: "comma-separated model ids", "zh-CN": "逗号分隔的模型 id" },
33
+ "opt.defaultModel": { en: "default model (defaults to first)", "zh-CN": "默认模型(默认取第一个)" },
34
+ "opt.smallModel": { en: "small/fast model (Claude Code haiku slot)", "zh-CN": "小型/快速模型(Claude Code haiku 槽位)" },
35
+ "opt.reasoning": { en: "preferred reasoning effort (codex): minimal|low|medium|high", "zh-CN": "Codex 首选推理等级: minimal|low|medium|high" },
36
+ "opt.discover": { en: "list model ids from the provider's /v1/models", "zh-CN": "从供应商 /v1/models 获取模型 id" },
37
+ "opt.include": { en: "keep only models matching comma-separated globs", "zh-CN": "仅保留匹配逗号分隔 glob 的模型" },
38
+ "opt.exclude": { en: "drop models matching comma-separated globs", "zh-CN": "排除匹配逗号分隔 glob 的模型" },
39
+ "opt.noDedup": { en: "keep snapshot duplicates (-latest/date suffixes)", "zh-CN": "保留快照重复项(-latest/日期后缀)" },
40
+ "opt.modelsDev": { en: "models.dev provider id for metadata matching", "zh-CN": "用于元数据匹配的 models.dev 供应商 id" },
41
+ "opt.yes": { en: "non-interactive; require all flags", "zh-CN": "非交互模式;要求提供全部参数" },
42
+ "opt.prune": { en: "also remove entries from app configs", "zh-CN": "同时从应用配置中移除条目" },
43
+ "opt.apps": { en: "comma-separated apps or 'all'", "zh-CN": "逗号分隔的应用或 'all'" },
44
+ "opt.appsDetailed": { en: "comma-separated apps (claude,codex,omp,pi,prime,opencode,hermes,workbuddy) or 'all'", "zh-CN": "逗号分隔的应用(claude,codex,omp,pi,prime,opencode,hermes,workbuddy)或 'all'" },
45
+ "opt.model": { en: "override default model while switching", "zh-CN": "切换时覆盖默认模型" },
46
+ "opt.dryRun": { en: "preview config diff without writing", "zh-CN": "仅预览配置差异,不写入" },
47
+ "opt.provider": { en: "sync this provider instead of the active one", "zh-CN": "同步指定供应商而非当前供应商" },
48
+ "opt.showProvider": { en: "show models of a configured provider", "zh-CN": "显示已配置供应商的模型" },
49
+ "opt.refresh": { en: "force-refresh models.dev cache", "zh-CN": "强制刷新 models.dev 缓存" },
50
+ "opt.limit": { en: "maximum results", "zh-CN": "最大结果数" },
51
+ "opt.syncAfter": { en: "push refreshed provider into app configs", "zh-CN": "刷新后推送到应用配置" },
52
+ "opt.appsSync": { en: "apps to sync when --sync is set", "zh-CN": "--sync 时要同步的应用" },
53
+ "opt.setInclude": { en: "set and persist include globs", "zh-CN": "设置并持久化 include glob" },
54
+ "opt.setExclude": { en: "set and persist exclude globs", "zh-CN": "设置并持久化 exclude glob" },
55
+ "opt.noFilter": { en: "clear persisted discovery filter", "zh-CN": "清除持久化的发现过滤器" },
56
+ "opt.all": { en: "import every new provider without selection", "zh-CN": "跳过多选,导入全部新供应商" },
57
+ "language.prompt": { en: "Language / 语言", "zh-CN": "语言 / Language" },
58
+ "language.saved": { en: "language saved: English", "zh-CN": "语言已保存:简体中文" },
59
+ "menu.selectInstructions": { en: "↑/↓ move · Enter select", "zh-CN": "↑/↓ 移动 · Enter 选择" },
60
+ "import.multiInstructions": { en: "↑/↓ move · Space toggle · Enter confirm", "zh-CN": "↑/↓ 移动 · 空格切换 · Enter 确认" },
61
+ "menu.title": { en: " · interactive menu — Ctrl+C quits, ↑/↓ selects", "zh-CN": " · 交互菜单 — Ctrl+C 退出,↑/↓ 选择" },
62
+ "menu.noProviders": { en: "no providers configured yet", "zh-CN": "尚未配置供应商" },
63
+ "menu.noProvidersHint": { en: "no providers configured — choose import existing providers or add/update provider first", "zh-CN": "尚未配置供应商——请先选择导入已有供应商或添加/更新供应商" },
64
+ "menu.firstScan": { en: "scan existing app configs and import custom providers now?", "zh-CN": "现在扫描应用配置并导入已有自定义供应商?" },
65
+ "menu.what": { en: "what to do?", "zh-CN": "请选择操作" },
66
+ "menu.add": { en: "add / update provider (id · protocol · base URL · API key)", "zh-CN": "添加 / 更新供应商 (id · 协议 · base URL · API key)" },
67
+ "menu.import": { en: "import existing providers (scan app configs · auto-dedupe)", "zh-CN": "导入已有供应商 (扫描应用配置 · 自动去重)" },
68
+ "menu.use": { en: "switch provider (use and sync into app configs)", "zh-CN": "切换供应商 (写入并同步到应用配置)" },
69
+ "menu.status": { en: "status (what each app currently points at)", "zh-CN": "查看状态 (各应用当前指向)" },
70
+ "menu.list": { en: "list providers", "zh-CN": "列出供应商" },
71
+ "menu.sync": { en: "sync active provider (re-apply to app configs)", "zh-CN": "同步当前供应商 (重新应用到应用配置)" },
72
+ "menu.discover": { en: "discover models (refresh provider list + metadata)", "zh-CN": "发现模型 (刷新供应商模型与元数据)" },
73
+ "menu.remove": { en: "remove provider", "zh-CN": "删除供应商" },
74
+ "menu.apps": { en: "agent versions (check installed CLIs / upgrade)", "zh-CN": "智能体版本 (检查已安装 CLI / 升级)" },
75
+ "menu.language": { en: "language / 语言", "zh-CN": "语言 / Language" },
76
+ "menu.quit": { en: "quit", "zh-CN": "退出" },
77
+ "menu.yes": { en: "yes", "zh-CN": "是" },
78
+ "menu.no": { en: "no", "zh-CN": "否" },
79
+ "menu.modelSource": { en: "how to fill the model list?", "zh-CN": "如何填写模型列表?" },
80
+ "menu.modelDiscover": { en: "discover from the provider's /v1/models (recommended)", "zh-CN": "从供应商 /v1/models 自动发现(推荐)" },
81
+ "menu.modelManual": { en: "type model ids manually", "zh-CN": "手动输入模型 id" },
82
+ "menu.pickProvider": { en: "switch to provider", "zh-CN": "切换到供应商" },
83
+ "menu.defaultModel": { en: "default model", "zh-CN": "默认模型" },
84
+ "menu.keepDefault": { en: "keep current default ({model})", "zh-CN": "保持当前默认模型({model})" },
85
+ "menu.active": { en: "active", "zh-CN": "当前" },
86
+ "menu.discoverFor": { en: "discover models for", "zh-CN": "为供应商发现模型" },
87
+ "menu.pushRefresh": { en: "push refreshed provider into app configs?", "zh-CN": "将刷新后的供应商推送到应用配置?" },
88
+ "menu.removeProvider": { en: "remove provider", "zh-CN": "删除供应商" },
89
+ "menu.reallyRemove": { en: "really remove {id}?", "zh-CN": "确认删除 {id}?" },
90
+ "menu.pruneConfigs": { en: "also remove its entries from app configs?", "zh-CN": "同时从应用配置中移除其条目?" },
91
+ "menu.upgrade": { en: "upgrade everything outdated?", "zh-CN": "升级全部过期项?" },
92
+ "menu.bye": { en: "bye", "zh-CN": "再见" },
93
+ "add.id": { en: "provider id (slug)", "zh-CN": "供应商 id(slug)" },
94
+ "add.idInvalid": { en: "lowercase slug, e.g. glm or my-proxy", "zh-CN": "请用小写 slug,例如 glm 或 my-proxy" },
95
+ "add.name": { en: "display name", "zh-CN": "显示名称" },
96
+ "add.protocol": { en: "wire protocol", "zh-CN": "接口协议" },
97
+ "add.openai": { en: "openai (chat completions)", "zh-CN": "openai (chat completions)" },
98
+ "add.anthropic": { en: "anthropic (messages)", "zh-CN": "anthropic (messages)" },
99
+ "add.baseUrl": { en: "base URL", "zh-CN": "base URL" },
100
+ "add.baseUrlInvalid": { en: "must start with http(s)://", "zh-CN": "必须以 http(s):// 开头" },
101
+ "add.apiKey": { en: "API key", "zh-CN": "API key" },
102
+ "add.models": { en: "model ids (comma separated)", "zh-CN": "模型 id(逗号分隔)" },
103
+ "add.cancelled": { en: "cancelled", "zh-CN": "已取消" },
104
+ "import.already": { en: "already configured as {id}", "zh-CN": "已配置为 {id}" },
105
+ "import.noneNew": { en: "nothing new to import (every discovered provider is already configured)", "zh-CN": "没有可导入的新供应商(发现项均已配置)" },
106
+ "import.noneFound": { en: "no custom providers found in supported app configs or in cc-switch", "zh-CN": "支持的应用配置与 cc-switch 中均未发现自定义供应商" },
107
+ "import.keyYes": { en: "yes", "zh-CN": "有" },
108
+ "import.keyEnv": { en: "env {name}", "zh-CN": "环境变量 {name}" },
109
+ "import.keyMissing": { en: "missing", "zh-CN": "缺失" },
110
+ "import.which": { en: "import which providers?", "zh-CN": "请选择要导入的供应商" },
111
+ "import.noModels": { en: "no models listed", "zh-CN": "未列出模型" },
112
+ "import.nothingSelected": { en: "nothing selected", "zh-CN": "未选择任何供应商" },
113
+ "import.keyRefMissing": { en: "env var {name} is referenced by a config but not set", "zh-CN": "配置引用了环境变量 {name},但该变量未设置" },
114
+ "import.keyNotStored": { en: "not stored in any config", "zh-CN": "任何配置中都未存储" },
115
+ "import.missingKey": { en: "{id}: no API key ({why}); export it or run interactive import", "zh-CN": "{id}:缺少 API key({why});请导出环境变量或运行交互导入" },
116
+ "import.keyPrompt": { en: "API key for {id} ({url}) — {why}", "zh-CN": "请输入 {id} 的 API key({url})— {why}" },
117
+ "import.required": { en: "required", "zh-CN": "必填" },
118
+ "import.discovering": { en: "{id}: no model ids in configs; discovering via /v1/models ...", "zh-CN": "{id}:配置中没有模型 id;正在通过 /v1/models 发现..." },
119
+ "import.discoveryFailed": { en: "{id}: no models in configs and discovery failed ({error}) — use agentsw add", "zh-CN": "{id}:配置中无模型且发现失败({error})——请改用 agentsw add" },
120
+ "import.noModelsImport": { en: "{id}: no models to import", "zh-CN": "{id}:没有可导入模型" },
121
+ "import.imported": { en: "imported", "zh-CN": "已导入" },
122
+ "import.updated": { en: "updated", "zh-CN": "已更新" },
123
+ "import.from": { en: "from", "zh-CN": "来源" },
124
+ "import.next": { en: "next: agentsw use {id}", "zh-CN": "下一步: agentsw use {id}" },
125
+ "add.fieldRequired": { en: "--{field} is required in non-interactive mode", "zh-CN": "非交互模式必须提供 --{field}" },
126
+ "add.protocolInvalid": { en: "protocol must be openai or anthropic", "zh-CN": "协议必须为 openai 或 anthropic" },
127
+ "add.openaiApiInvalid": {
128
+ en: "--openai-api must be completions or responses",
129
+ "zh-CN": "--openai-api 必须为 completions 或 responses",
130
+ },
131
+ "add.openaiApi": { en: "openai endpoint flavor", "zh-CN": "openai 接口形态" },
132
+ "add.completions": {
133
+ en: "chat completions (/v1/chat/completions) — the common one",
134
+ "zh-CN": "chat completions(/v1/chat/completions)——最常见",
135
+ },
136
+ "add.responses": { en: "responses (/v1/responses)", "zh-CN": "responses(/v1/responses)" },
137
+ "add.discovering": { en: "discovering models from {url} ...", "zh-CN": "正在从 {url} 发现模型..." },
138
+ "add.providerLists": { en: "provider lists {count} model(s) via /v1/models", "zh-CN": "供应商通过 /v1/models 返回 {count} 个模型" },
139
+ "add.atLeastOne": { en: "at least one model id is required (or use --discover)", "zh-CN": "至少需要一个模型 id(或使用 --discover)" },
140
+ "add.defaultMissing": { en: "default model {model} is not in the model list", "zh-CN": "默认模型 {model} 不在模型列表中" },
141
+ "add.smallMissing": { en: "small model {model} is not in the model list", "zh-CN": "小型模型 {model} 不在模型列表中" },
142
+ "add.added": { en: "added", "zh-CN": "已添加" },
143
+ "add.updated": { en: "updated", "zh-CN": "已更新" },
144
+ "add.saved": { en: "{status} provider {id} ({protocol})", "zh-CN": "{status}供应商 {id} ({protocol})" },
145
+ "add.metadata": { en: "models.dev metadata: {matched}/{total} models matched", "zh-CN": "models.dev 元数据:匹配 {matched}/{total} 个模型" },
146
+ "add.providerHint": { en: " (provider hint: {hint})", "zh-CN": " (供应商提示:{hint})" },
147
+ "add.next": { en: "next: agentsw use {id}", "zh-CN": "下一步: agentsw use {id}" },
148
+ "import.skip": { en: "skip", "zh-CN": "跳过" },
149
+ "import.modelsCount": { en: "{count} models", "zh-CN": "{count} 个模型" },
150
+ "table.protocol": { en: "PROTOCOL", "zh-CN": "协议" },
151
+ "table.defaultModel": { en: "DEFAULT MODEL", "zh-CN": "默认模型" },
152
+ "table.models": { en: "MODELS", "zh-CN": "模型数" },
153
+ "table.from": { en: "FROM", "zh-CN": "来源" },
154
+ "table.key": { en: "KEY", "zh-CN": "密钥" },
155
+ "table.found": { en: "FOUND", "zh-CN": "已发现" },
156
+ "table.protocols": { en: "PROTOCOLS", "zh-CN": "协议" },
157
+ "table.current": { en: "CURRENT", "zh-CN": "当前" },
158
+ "table.config": { en: "CONFIG", "zh-CN": "配置" },
159
+ "list.none": { en: "no providers configured (config: {file})\nrun: agentsw add or agentsw import", "zh-CN": "尚未配置供应商(配置:{file})\n请运行 agentsw add 或 agentsw import" },
160
+ "remove.pruning": { en: "pruning {id} from app configs", "zh-CN": "正在从应用配置清理 {id}" },
161
+ "remove.removed": { en: "removed provider {id}", "zh-CN": "已删除供应商 {id}" },
162
+ "remove.note": { en: "note: app configs are unchanged; use --prune to clean them", "zh-CN": "注意:应用配置未改动;可使用 --prune 清理" },
163
+ "common.skip": { en: "skip", "zh-CN": "跳过" },
164
+ "common.yes": { en: "yes", "zh-CN": "是" },
165
+ "common.no": { en: "no", "zh-CN": "否" },
166
+ "use.modelMissing": { en: "model {model} is not configured on provider {id} (have: {have})", "zh-CN": "供应商 {id} 未配置模型 {model}(已有:{have})" },
167
+ "use.switching": { en: "switching to {id} ({protocol}) · default model {model}", "zh-CN": "正在切换到 {id} ({protocol})· 默认模型 {model}" },
168
+ "sync.noActive": { en: "no active provider; run agentsw use <id> first", "zh-CN": "没有当前供应商;请先运行 agentsw use <id>" },
169
+ "sync.syncing": { en: "syncing provider {id} · default model {model}", "zh-CN": "正在同步供应商 {id} · 默认模型 {model}" },
170
+ "status.config": { en: "config: {file}", "zh-CN": "配置:{file}" },
171
+ "status.active": { en: "active provider: {id}", "zh-CN": "当前供应商:{id}" },
172
+ "status.none": { en: "(none)", "zh-CN": "(无)" },
173
+ };
174
+ let current = detectSystemLocale();
175
+ export function normalizeLocale(value) {
176
+ if (!value)
177
+ return undefined;
178
+ const locale = value.trim().toLowerCase().replace(/_/g, "-");
179
+ if (locale.startsWith("zh"))
180
+ return "zh-CN";
181
+ if (locale.startsWith("en") || locale === "c" || locale === "posix")
182
+ return "en";
183
+ return undefined;
184
+ }
185
+ export function detectSystemLocale(env = process.env) {
186
+ for (const key of ["LC_ALL", "LC_MESSAGES", "LANG"]) {
187
+ const hit = normalizeLocale(env[key]);
188
+ if (hit)
189
+ return hit;
190
+ }
191
+ return normalizeLocale(Intl.DateTimeFormat().resolvedOptions().locale) ?? "en";
192
+ }
193
+ export function extractCliLocale(argv = process.argv.slice(2)) {
194
+ for (let i = 0; i < argv.length; i++) {
195
+ if (argv[i] === "--lang")
196
+ return argv[i + 1];
197
+ if (argv[i]?.startsWith("--lang="))
198
+ return argv[i].slice("--lang=".length);
199
+ }
200
+ return undefined;
201
+ }
202
+ export function setLocale(value) {
203
+ current = normalizeLocale(value) ?? "en";
204
+ return current;
205
+ }
206
+ export function getLocale() {
207
+ return current;
208
+ }
209
+ export function t(key, vars = {}) {
210
+ let out = messages[key][current];
211
+ for (const [name, value] of Object.entries(vars))
212
+ out = out.replaceAll(`{${name}}`, String(value));
213
+ return out;
214
+ }
package/dist/import.js ADDED
@@ -0,0 +1,66 @@
1
+ import { loadStore } from "./store.js";
2
+ import { ccSwitchCandidates } from "./sources/ccswitch.js";
3
+ import { targets } from "./targets/index.js";
4
+ export function normalizeUrl(u) {
5
+ return u.replace(/\/+$/, "");
6
+ }
7
+ const matchKey = (protocol, baseUrl) => `${protocol}|${normalizeUrl(baseUrl)}`;
8
+ /** pure merge: dedupe candidates by protocol+baseUrl, union models, keep first resolved key */
9
+ export function mergeCandidates(rows, existing) {
10
+ const merged = new Map();
11
+ for (const { source, ...c } of rows) {
12
+ const key = matchKey(c.protocol, c.baseUrl);
13
+ const cur = merged.get(key);
14
+ if (!cur) {
15
+ merged.set(key, { ...c, baseUrl: normalizeUrl(c.baseUrl), models: [...new Set(c.models)], sources: [source] });
16
+ continue;
17
+ }
18
+ if (!cur.sources.includes(source))
19
+ cur.sources.push(source);
20
+ if (!cur.apiKey && c.apiKey) {
21
+ cur.apiKey = c.apiKey;
22
+ cur.keyEnv = c.keyEnv;
23
+ }
24
+ else if (!cur.keyEnv && c.keyEnv) {
25
+ cur.keyEnv = c.keyEnv;
26
+ }
27
+ for (const m of c.models)
28
+ if (!cur.models.includes(m))
29
+ cur.models.push(m);
30
+ if (!cur.defaultModel && c.defaultModel)
31
+ cur.defaultModel = c.defaultModel;
32
+ // responses wins: an endpoint one app drives over /v1/responses serves it for all of them,
33
+ // and codex speaks nothing else
34
+ if (c.openaiApi === "responses" || !cur.openaiApi)
35
+ cur.openaiApi = c.openaiApi ?? cur.openaiApi;
36
+ }
37
+ const out = [...merged.values()];
38
+ for (const m of out) {
39
+ const hit = existing.find((e) => matchKey(e.protocol, e.baseUrl) === matchKey(m.protocol, m.baseUrl));
40
+ if (hit)
41
+ m.configured = hit.id;
42
+ }
43
+ return out.sort((a, b) => a.id.localeCompare(b.id));
44
+ }
45
+ /** scan every detected app config — and cc-switch's own store — for custom providers */
46
+ export function scanCandidates() {
47
+ const rows = [];
48
+ for (const t of targets) {
49
+ if (!t.candidates)
50
+ continue;
51
+ try {
52
+ rows.push(...t.candidates());
53
+ }
54
+ catch {
55
+ // adapter configs that fail to parse are reported by status/apply; skip during scan
56
+ }
57
+ }
58
+ try {
59
+ rows.push(...ccSwitchCandidates());
60
+ }
61
+ catch {
62
+ // a cc-switch store this build cannot read is not this scan's problem
63
+ }
64
+ const store = loadStore();
65
+ return mergeCandidates(rows, Object.values(store.providers).map((p) => ({ id: p.id, protocol: p.protocol, baseUrl: p.baseUrl })));
66
+ }
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { Command, InvalidArgumentError } from "commander";
4
+ import { cmdAdd, cmdApps, cmdDiscover, cmdImport, cmdInstall, cmdList, cmdModels, cmdRefreshMeta, cmdPrune, cmdRemove, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
5
+ import { detectSystemLocale, extractCliLocale, normalizeLocale, setLocale, t } from "./i18n.js";
6
+ import { cmdMenu } from "./menu.js";
7
+ import { loadStore } from "./store.js";
8
+ // Locale precedence: CLI flag > environment > saved preference > system locale.
9
+ setLocale(extractCliLocale() ?? process.env.AGENTSW_LANG ?? loadStore().language ?? detectSystemLocale());
10
+ function parseLocale(value) {
11
+ const locale = normalizeLocale(value);
12
+ if (!locale)
13
+ throw new InvalidArgumentError(t("error.language", { value }));
14
+ setLocale(locale);
15
+ return locale;
16
+ }
17
+ const program = new Command();
18
+ // read the real version from package.json instead of a drifting literal
19
+ const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
20
+ program
21
+ .name("agentsw")
22
+ .description(t("root.description"))
23
+ .option("--lang <locale>", t("help.language"), parseLocale)
24
+ .version(version);
25
+ // bare invocation -> interactive menu (help on non-TTY); unknown subcommand -> error.
26
+ // excess arguments are surfaced by us, so the message names the offending command.
27
+ program
28
+ .allowExcessArguments()
29
+ .action((...handlerArgs) => {
30
+ const cmd = handlerArgs[handlerArgs.length - 1];
31
+ if (cmd.args.length > 0)
32
+ program.error(t("error.unknownCommand", { value: cmd.args[0] }));
33
+ if (process.stdin.isTTY)
34
+ return cmdMenu();
35
+ program.outputHelp();
36
+ });
37
+ program
38
+ .command("add")
39
+ .description(t("cmd.add"))
40
+ .option("--id <slug>", t("opt.id"))
41
+ .option("--name <name>", t("opt.name"))
42
+ .option("--protocol <p>", t("opt.protocol"))
43
+ .option("--openai-api <flavor>", t("opt.openaiApi"))
44
+ .option("--base-url <url>", t("opt.baseUrl"))
45
+ .option("--api-key <key>", t("opt.apiKey"))
46
+ .option("--models <ids>", t("opt.models"))
47
+ .option("--default-model <id>", t("opt.defaultModel"))
48
+ .option("--small-model <id>", t("opt.smallModel"))
49
+ .option("--reasoning-effort <level>", t("opt.reasoning"))
50
+ .option("-d, --discover", t("opt.discover"))
51
+ .option("--include <globs>", t("opt.include"))
52
+ .option("--exclude <globs>", t("opt.exclude"))
53
+ .option("--no-dedup", t("opt.noDedup"))
54
+ .option("--models-dev <id>", t("opt.modelsDev"))
55
+ .option("-y, --yes", t("opt.yes"))
56
+ .action(cmdAdd);
57
+ program.command("list").alias("ls").description(t("cmd.list")).action(cmdList);
58
+ program
59
+ .command("remove <id>")
60
+ .alias("rm")
61
+ .description(t("cmd.remove"))
62
+ .option("--prune", t("opt.prune"))
63
+ .action(cmdRemove);
64
+ program
65
+ .command("prune <id>")
66
+ .description(t("cmd.prune"))
67
+ .option("-a, --apps <apps>", t("opt.apps"))
68
+ .action(cmdPrune);
69
+ program
70
+ .command("use <id>")
71
+ .description(t("cmd.use"))
72
+ .option("-a, --apps <apps>", t("opt.appsDetailed"))
73
+ .option("-m, --model <id>", t("opt.model"))
74
+ .option("-n, --dry-run", t("opt.dryRun"))
75
+ .action(cmdUse);
76
+ program
77
+ .command("sync")
78
+ .description(t("cmd.sync"))
79
+ .option("-a, --apps <apps>", t("opt.apps"))
80
+ .option("-p, --provider <id>", t("opt.provider"))
81
+ .option("-n, --dry-run", t("opt.dryRun"))
82
+ .action(cmdSync);
83
+ program.command("status").description(t("cmd.status")).action(cmdStatus);
84
+ program
85
+ .command("models [query]")
86
+ .description(t("cmd.models"))
87
+ .option("-p, --provider <id>", t("opt.showProvider"))
88
+ .option("-r, --refresh", t("opt.refresh"))
89
+ .option("-l, --limit <n>", t("opt.limit"), "30")
90
+ .action(cmdModels);
91
+ program
92
+ .command("discover <id>")
93
+ .description(t("cmd.discover"))
94
+ .option("-s, --sync", t("opt.syncAfter"))
95
+ .option("-a, --apps <apps>", t("opt.appsSync"))
96
+ .option("--include <globs>", t("opt.setInclude"))
97
+ .option("--exclude <globs>", t("opt.setExclude"))
98
+ .option("--no-dedup", t("opt.noDedup"))
99
+ .option("--no-filter", t("opt.noFilter"))
100
+ .action(cmdDiscover);
101
+ program
102
+ .command("import")
103
+ .description(t("cmd.import"))
104
+ .option("--all", t("opt.all"))
105
+ .action(cmdImport);
106
+ program.command("apps").description(t("cmd.apps")).action(cmdApps);
107
+ program.command("install <app>").description(t("cmd.install")).action(cmdInstall);
108
+ program.command("upgrade [apps...]").description(t("cmd.upgrade")).action(cmdUpgrade);
109
+ program.command("refresh").description(t("cmd.refresh")).action(cmdRefreshMeta);
110
+ program.parseAsync().catch((err) => {
111
+ process.stderr.write(`error: ${err.message}\n`);
112
+ process.exit(1);
113
+ });