Skip to content

Commit df529b8

Browse files
鲁工鲁工
authored andcommitted
feat: add ccmr update for self-updating from npm, v1.20.0
One command to reinstall the CLI globally at the latest published version, equivalent to `npm install -g claude-code-model-router@latest`. Two rules in here are one line each and non-obvious: - versions are compared numerically, not lexically. String comparison puts "1.9.0" above "1.10.0", which would tell a user on 1.9.0 they are already current. - a local build ahead of the registry counts as up to date. This repo routinely runs unpublished versions, and a plain "differs -> install" rule would silently downgrade the developer. Not hypothetical: the first real --check run hit exactly this (local 1.19.0 vs registry 1.18.0). --package-manager becomes argv[0] of a spawned process, so it is checked against a whitelist (npm/pnpm/yarn/bun) rather than forwarded; the package name and @latest are literals and the spawn uses an argv array with shell:false. Verified by passing `rm -rf /` and watching it be refused. npm's output is inherited rather than captured, because its permission errors (EACCES, prefix problems) are the whole diagnosis in the failure case. On success the command scans for running gateways still on the old version and tells the user which ports to restart -- a package upgrade is not config hot-reload -- without stopping processes it does not own. The registry lookup uses the built-in fetch (no dependency, no npm subprocess to read one field) and is tested against a local HTTP stub covering the 200, 503 and missing-version paths, so the suite stays offline.
1 parent 403fd74 commit df529b8

12 files changed

Lines changed: 581 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ npx claude-code-model-router doctor seed kimi # 只检查指定模型
8383
# 查看网关的按模型用量统计(请求数 / 错误数 / tokens)
8484
npx claude-code-model-router stats
8585

86+
# 更新到 npm 上的最新版本(等同 npm install -g claude-code-model-router@latest)
87+
npx claude-code-model-router update
88+
npx claude-code-model-router update --check # 只查有没有新版,不安装
89+
npx claude-code-model-router update --package-manager pnpm # npm / pnpm / yarn / bun
90+
8691
# 启动 Claude Code(网关模式;网关未启动时会自动拉起)
8792
npx claude-code-model-router claude
8893
npx claude-code-model-router claude --gateway-port 9000 # 自定义网关端口
@@ -543,6 +548,15 @@ DeepSeek Anthropic 兼容接口会忽略 `metadata` 字段,但某些 Claude Co
543548

544549
## 更新日志
545550

551+
### v1.20.0
552+
553+
- **新增 `ccmr update`**:一条命令更新到 npm 上的最新版本,等同 `npm install -g claude-code-model-router@latest`
554+
- 先向 npm registry 查已发布版本并与本机版本做**数值比较**(避免 `1.9.0` 被字符串比较误判为高于 `1.10.0`),已是最新则不做任何安装
555+
- **本地版本领先 registry 时也判定为最新**,不会把未发布的本地构建降级回线上版本
556+
- `--check` 只报告不安装;`--package-manager` 支持 `npm` / `pnpm` / `yarn` / `bun`,且限定在白名单内——该值会成为被拉起进程的 argv[0],不接受任意字符串
557+
- 安装直接透传 npm 自己的输出,权限错误等原文可见;失败时以 npm 的退出码退出,并提示改用用户可写的 prefix
558+
- 更新成功后会扫描本机运行中的网关,**列出仍在跑旧版本的实例并提示重启**(包升级不同于配置热重载,旧进程会继续跑旧代码),但不会替你去停用户的进程
559+
546560
### v1.19.0
547561

548562
- **1M 模型自动带 `[1m]` 后缀**:Claude Code 在自定义 base_url 后面默认按 ~200k 处理第三方模型,只有模型名以 `[1m]` 结尾时才会开启百万上下文——此前需要每次手动 `/model deepseek-flash[1m]`。现在两个出口都会自动补后缀:

‎dist/cli.js‎

Lines changed: 90 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/cli.js.map‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/update.d.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Self-update for the CLI: look up the published version and reinstall
3+
* globally from npm.
4+
*
5+
* The install is spawned with argv arrays and never a shell string, and the
6+
* package name and dist-tag are literals here, so nothing from the network or
7+
* the user's config reaches a command line.
8+
*/
9+
export declare const PACKAGE_NAME = "claude-code-model-router";
10+
/**
11+
* Compare two versions numerically. Negative when `a` is older, positive when
12+
* newer, zero when equal. A prerelease sorts before its release.
13+
*/
14+
export declare function compareVersions(a: string, b: string): number;
15+
export type UpdatePlan = {
16+
action: 'up-to-date';
17+
current: string;
18+
latest: string;
19+
} | {
20+
action: 'install';
21+
current: string;
22+
latest: string;
23+
};
24+
/**
25+
* Decide whether to reinstall. A local build ahead of the registry (an
26+
* unpublished version) counts as up to date -- installing would downgrade it.
27+
*/
28+
export declare function planUpdate(current: string, latest: string): UpdatePlan;
29+
export interface FetchLatestOptions {
30+
registryUrl?: string;
31+
timeoutMs?: number;
32+
}
33+
/** Read the published version from the npm registry. */
34+
export declare function fetchLatestVersion(options?: FetchLatestOptions): Promise<string>;
35+
export interface InstallResult {
36+
command: string;
37+
exitCode: number | null;
38+
}
39+
/**
40+
* Run the global install, streaming npm's own output so the user sees
41+
* progress and any permission error verbatim.
42+
*/
43+
export declare function installLatest(packageManager?: string): Promise<InstallResult>;
44+
//# sourceMappingURL=update.d.ts.map

‎dist/update.d.ts.map‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/update.js‎

Lines changed: 91 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/update.js.map‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# A self-update command is mostly edge cases, and two of them are one-liners that bite
2+
3+
**Problem (one line):** Add `ccmr update`, "equivalent to `npm install -g claude-code-model-router@latest`" — where the interesting work is entirely in what happens *around* that install.
4+
5+
## Approach
6+
7+
1. **Split the network lookup from the decision from the install.** `fetchLatestVersion` (registry I/O), `compareVersions` + `planUpdate` (pure), `installLatest` (process spawn). Only the middle pair holds the logic worth protecting, and being pure they test without network, npm, or mocks.
8+
2. **Test the registry call against a local HTTP server, never the real registry.** Same pattern the repo already uses for upstream providers: a `http.createServer` stub on port 0 covering the 200, the 503, and the malformed-payload paths. Fast, offline, deterministic.
9+
3. **Compare versions numerically, and say why in the test.** String comparison puts `1.9.0` above `1.10.0`, which would tell a user on 1.9.0 they were current — the exact failure a naive `latest !== current` or `latest > current` produces. The test names that bug so nobody "simplifies" it back.
10+
4. **Treat a local build ahead of the registry as up to date.** This repo routinely runs unpublished versions (1.19.0 locally while npm had 1.18.0), so a plain "differs → install" rule would silently *downgrade* the developer. This was not hypothetical: it is exactly what the first real `--check` run hit, which is also how it got verified.
11+
5. **Constrain anything that becomes argv[0].** `--package-manager` is spawned as a command name, so it is checked against a four-entry whitelist rather than forwarded; the package name and `@latest` are literals, and the spawn uses an argv array with `shell: false`. Verified by passing `rm -rf /` and watching it be refused.
12+
6. **Say what the upgrade does not do.** A package upgrade is not config hot-reload: gateways already running keep serving the old code. The command discovers them, prints which ports are stale, and tells the user how to restart — without stopping processes it does not own.
13+
14+
## Judgment calls
15+
16+
- **No `--force`.** Reinstalling an identical version only helps a corrupted install, which is rare enough to leave to a manual npm command; adding it would also give the downgrade path a way back in.
17+
- **`stdio: 'inherit'` instead of capturing output.** npm's permission errors (`EACCES`, prefix problems) are the whole diagnosis in the failure case, and paraphrasing them is how this repo has burned time before.
18+
- **Registry over `npm view`.** The built-in `fetch` needs no dependency and no npm subprocess just to read one field, and it keeps the check working even where npm is slow or misconfigured.
19+
- **CLI action left untested.** It calls `process.exit` and spawns npm; the repo's agreed seams are ConfigManager / ModelRouter / HTTP endpoints, so the logic lives in tested pure functions and the wiring was exercised by running `update --help`, `--check`, and the whitelist rejection for real.
20+
21+
## Reusable rule
22+
23+
For any self-update path, the two rules that are one line each and non-obvious: compare versions numerically, and treat "local is newer" as up to date — otherwise the command will confidently downgrade the person most likely to run it, the developer on an unpublished build.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "claude-code-model-router",
3-
"version": "1.19.0",
3+
"version": "1.20.0",
44
"description": "A lightweight API gateway for routing Claude Code requests to multiple AI models",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

0 commit comments

Comments
 (0)