Skip to content

Commit 45d4688

Browse files
committed
feat: win bl update exe file test
1 parent 389c932 commit 45d4688

17 files changed

Lines changed: 769 additions & 105 deletions

File tree

INSTALL.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
2424

2525
带参时先落盘再执行(`irm | iex` 不便传参),或使用仓外静态资源文档中的预发入口。
2626

27+
二进制安装布局为 `versions/<ver>/` + `current` 指针;`bl update` 只切换指针并清理旧版本(保留当前与上一版)。更新进程退出后,下次执行 `bl` 即使用新版本(无需「重启应用」)。
28+
2729
校验:
2830

2931
```bash
@@ -79,9 +81,10 @@ bl auth status --output json
7981

8082
## 5. 常见问题
8183

82-
| 现象 | 可能原因 | 建议动作 |
83-
| ----------------------- | ---------------------------- | -------------------------------------- |
84-
| `bl: command not found` | bin 不在 PATH | 检查 `~/.local/bin``npm prefix -g` |
85-
| curl 安装 404 | GitHub Release 资产未上传 | 改用 `npm install -g bailian-cli` |
86-
| `plugin` 需要 npm | 二进制安装无本机 npm | 安装 Node,或改用 npm 版 CLI |
87-
| 安装报错 engines | Node 版本过低(仅 npm 路径) | 升级到 ≥ 18.17.0 |
84+
| 现象 | 可能原因 | 建议动作 |
85+
| ------------------------ | ---------------------------- | ------------------------------------------------ |
86+
| `bl: command not found` | bin 不在 PATH | 检查 `~/.local/bin``npm prefix -g` |
87+
| curl 安装 404 | GitHub Release 资产未上传 | 改用 `npm install -g bailian-cli` |
88+
| Windows `bl update` 失败 | 旧布局 / 文件锁 / 网络 | 重跑 `irm .../install.ps1 \| iex` 迁移布局后重试 |
89+
| `plugin` 需要 npm | 二进制安装无本机 npm | 安装 Node,或改用 npm 版 CLI |
90+
| 安装报错 engines | Node 版本过低(仅 npm 路径) | 升级到 ≥ 18.17.0 |

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,9 @@ bl config set --key base_url --value https://dashscope-us.aliyuncs.com
216216
bl config set --key default_text_model --value qwen-turbo
217217
bl config set --key timeout --value 600
218218

219-
# Self-update to latest version
219+
# Self-update to latest or a specific version
220220
bl update
221+
bl update --to 0.1.14
221222
```
222223

223224
Config file location: `~/.bailian/config.json`

README.zh.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ bl config set --key timeout --value 600
216216

217217
# 自更新到最新版本
218218
bl update
219+
220+
# 安装指定版本
221+
bl update --to 0.1.14
219222
```
220223

221224
配置文件位置:`~/.bailian/config.json`

packages/commands/src/commands/update.ts

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ import { execSync } from "child_process";
22
import { writeFileSync } from "fs";
33
import { join } from "path";
44
import {
5+
BailianError,
6+
DEFAULT_INSTALL_PS1_URL,
57
DEFAULT_INSTALL_SCRIPT_URL,
68
defineCommand,
79
getConfigDir,
8-
getInstallMethod,
10+
getUpdateInstallMethod,
911
type InstallMethod,
1012
} from "bailian-cli-core";
1113
import {
1214
ansi,
1315
fetchLatestVersion,
1416
fetchBinaryChannelVersion,
17+
normalizeBinaryVersion,
1518
performBinaryUpdate,
1619
type AnsiStyles,
1720
} from "bailian-cli-runtime";
@@ -50,64 +53,108 @@ async function resolveLatest(method: InstallMethod, npmPackage: string): Promise
5053
return fetchLatestVersion(5000, npmPackage);
5154
}
5255

56+
function binaryReinstallHint(): string {
57+
if (process.platform === "win32") {
58+
return ` irm ${DEFAULT_INSTALL_PS1_URL} | iex\n`;
59+
}
60+
return ` curl -fsSL ${DEFAULT_INSTALL_SCRIPT_URL} | bash\n`;
61+
}
62+
5363
export default defineCommand({
54-
description: "Update the CLI to the latest version",
64+
description: "Update the CLI to the latest or a specified version",
5565
auth: "none",
56-
exampleArgs: [""],
66+
usageArgs: "[--to <version>]",
67+
flags: {
68+
to: {
69+
type: "string",
70+
valueHint: "<version>",
71+
description: "Install this exact version instead of the latest",
72+
},
73+
},
74+
exampleArgs: ["", "--to 0.1.14"],
75+
validate(flags) {
76+
if (flags.to !== undefined && !flags.to.trim()) {
77+
return "--to requires a non-empty version";
78+
}
79+
return undefined;
80+
},
5781
async run(ctx) {
5882
const { identity } = ctx;
5983
const npmPackage = identity.npmPackage;
6084
const binName = identity.binName;
6185
const currentVersion = identity.version;
6286
const color = ansi(process.stderr);
63-
const method = getInstallMethod();
87+
const method = getUpdateInstallMethod(identity);
88+
const requestedTo = ctx.flags.to?.trim();
89+
const pinnedVersion = requestedTo ? normalizeBinaryVersion(requestedTo) : undefined;
6490

6591
process.stderr.write(`Current version: ${color.yellow(currentVersion)}\n`);
6692
process.stderr.write(`Install method: ${color.dim(method)}\n`);
67-
process.stderr.write("Checking for updates...\n");
93+
if (pinnedVersion) {
94+
process.stderr.write(`Target version: ${color.green(pinnedVersion)}\n`);
95+
} else {
96+
process.stderr.write("Checking for updates...\n");
97+
}
6898

6999
if (method === "brew" || method === "winget") {
70100
const cmd =
71101
method === "brew" ? "brew upgrade bailian-cli" : "winget upgrade Aliyun.BailianCLI";
72102
process.stderr.write(
73103
`${color.yellow(`This CLI was installed via ${method}. Update with:`)}\n ${cmd}\n`,
74104
);
105+
if (pinnedVersion) {
106+
process.stderr.write(
107+
`${color.dim(`Note: --to is not supported for ${method} installs.`)}\n`,
108+
);
109+
}
75110
return;
76111
}
77112

78-
const latest = await resolveLatest(method, npmPackage);
113+
const targetVersion = pinnedVersion ?? (await resolveLatest(method, npmPackage));
114+
115+
if (!targetVersion) {
116+
process.stderr.write(`${color.yellow("Could not determine the latest version.")}\n`);
117+
return;
118+
}
79119

80-
if (latest && latest === currentVersion) {
81-
process.stderr.write(`${color.green(`\u2713 Already up to date (${currentVersion}).`)}\n`);
120+
if (targetVersion === currentVersion) {
121+
const message = pinnedVersion
122+
? `\u2713 Already at ${currentVersion}.`
123+
: `\u2713 Already up to date (${currentVersion}).`;
124+
process.stderr.write(`${color.green(message)}\n`);
82125
if (method === "npm") updateAgentSkill(color);
83126
return;
84127
}
85128

86-
if (latest) {
87-
process.stderr.write(`Latest version: ${color.green(latest)}\n\n`);
129+
if (!pinnedVersion) {
130+
process.stderr.write(`Latest version: ${color.green(targetVersion)}\n\n`);
88131
} else {
89-
process.stderr.write(`${color.yellow("Could not determine the latest version.")}\n`);
90-
return;
132+
process.stderr.write("\n");
91133
}
92134

93135
if (method === "binary") {
94136
process.stderr.write(`Updating via binary channel...\n\n`);
95137
try {
96-
const newVer = await performBinaryUpdate(latest);
138+
const newVer = await performBinaryUpdate(targetVersion);
97139
process.stderr.write(
98140
`\n${color.green(`\u2713 Update complete: ${currentVersion} \u2192 ${newVer}`)}\n`,
99141
);
100142
writeUpdateState(newVer);
101143
} catch (error) {
102144
const message = error instanceof Error ? error.message : String(error);
145+
const reinstall =
146+
error instanceof BailianError && error.hint
147+
? error.hint.replace(/^Re-run:\s*/i, "")
148+
: binaryReinstallHint().trim();
103149
process.stderr.write(`\nAutomatic binary update failed: ${message}\n`);
104150
process.stderr.write("Re-run the install script:\n");
105-
process.stderr.write(` curl -fsSL ${DEFAULT_INSTALL_SCRIPT_URL} | bash\n\n`);
151+
process.stderr.write(` ${reinstall}\n\n`);
106152
}
107153
return;
108154
}
109155

110-
const cmd = `npm install -g ${npmPackage}@latest`;
156+
const npmSpec = pinnedVersion ? `${npmPackage}@${pinnedVersion}` : `${npmPackage}@latest`;
157+
const cmd = `npm install -g ${npmSpec}`;
111158
process.stderr.write(`Updating ${npmPackage} via npm...\n\n`);
112159

113160
try {

packages/commands/tests/e2e/topic-routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export const AUTH_ROUTES: E2eRouteExports = {
1010
"auth logout": "authLogout",
1111
};
1212

13+
export const UPDATE_ROUTES: E2eRouteExports = {
14+
update: "update",
15+
};
16+
1317
export const TEXT_CHAT_ROUTES: E2eRouteExports = { "text chat": "textChat" };
1418

1519
export const CONFIG_ROUTES: E2eRouteExports = {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, test } from "vite-plus/test";
2+
import { runCommandE2e } from "./helpers.ts";
3+
import { UPDATE_ROUTES } from "./topic-routes.ts";
4+
5+
describe("e2e: update", () => {
6+
test("update --help 正常退出并展示 --to", async () => {
7+
const { stderr, exitCode } = await runCommandE2e(UPDATE_ROUTES, ["update", "--help"]);
8+
expect(exitCode, stderr).toBe(0);
9+
expect(stderr).toMatch(/--to/);
10+
expect(stderr).toMatch(/<version>/);
11+
});
12+
13+
test("update --help 包含 --to 示例", async () => {
14+
const { stderr, exitCode } = await runCommandE2e(UPDATE_ROUTES, ["update", "--help"]);
15+
expect(exitCode, stderr).toBe(0);
16+
expect(stderr).toContain("--to 0.1.14");
17+
});
18+
19+
test("update --to 缺值时退出为用法错误 (2)", async () => {
20+
const { stderr, exitCode } = await runCommandE2e(UPDATE_ROUTES, ["update", "--to"]);
21+
expect(exitCode, stderr).toBe(2);
22+
});
23+
});

packages/core/src/install/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
export {
2+
BINARY_PRODUCT_CLIENT_NAME,
23
detectInstallMethod,
34
getInstallMethod,
5+
getUpdateInstallMethod,
46
isCompiledBinary,
57
writeInstallMethodSync,
68
type InstallMethod,
9+
type InstallMethodIdentity,
710
} from "./method.ts";
811
export {
912
DEFAULT_CLI_CDN_BASE,

packages/core/src/install/method.ts

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,19 @@ import { getConfigDir } from "../config/paths.ts";
55
/** How the CLI was installed on this machine. */
66
export type InstallMethod = "binary" | "npm" | "brew" | "winget" | "unknown";
77

8+
/** Product that currently ships standalone binary artifacts (`bl` / `bailian`). */
9+
export const BINARY_PRODUCT_CLIENT_NAME = "bailian-cli";
10+
811
const INSTALL_METHOD_FILE = "install-method";
912
const VALID_METHODS = new Set<InstallMethod>(["binary", "npm", "brew", "winget", "unknown"]);
1013

11-
function installMethodPath(): string {
12-
return join(getConfigDir(), INSTALL_METHOD_FILE);
14+
export type InstallMethodIdentity = {
15+
clientName: string;
16+
};
17+
18+
function installMethodPath(clientName?: string): string {
19+
if (!clientName) return join(getConfigDir(), INSTALL_METHOD_FILE);
20+
return join(getConfigDir(), `${INSTALL_METHOD_FILE}.${clientName}`);
1321
}
1422

1523
/**
@@ -32,6 +40,15 @@ function parseInstallMethod(raw: string | undefined): InstallMethod | null {
3240
return VALID_METHODS.has(value) ? value : null;
3341
}
3442

43+
function readInstallMethodFile(path: string): InstallMethod | null {
44+
try {
45+
const raw = readFileSync(path, "utf-8");
46+
return parseInstallMethod(raw.split("\n")[0]);
47+
} catch {
48+
return null;
49+
}
50+
}
51+
3552
/** Infer install method when no marker file / env override is present. */
3653
export function detectInstallMethod(): InstallMethod {
3754
const fromEnv = parseInstallMethod(process.env.BAILIAN_INSTALL_METHOD);
@@ -46,30 +63,68 @@ export function detectInstallMethod(): InstallMethod {
4663
return "npm";
4764
}
4865

49-
/** Read the persisted install method, falling back to detection. */
50-
export function getInstallMethod(): InstallMethod {
66+
/**
67+
* Read the persisted install method, falling back to detection.
68+
*
69+
* When `identity` is provided, prefer `install-method.<clientName>`.
70+
* Legacy `~/.bailian/install-method` is only consulted for `bailian-cli`
71+
* so other products (e.g. kscli) are not polluted by a shared binary marker.
72+
*/
73+
export function getInstallMethod(identity?: InstallMethodIdentity): InstallMethod {
5174
const fromEnv = parseInstallMethod(process.env.BAILIAN_INSTALL_METHOD);
5275
if (fromEnv) return fromEnv;
5376

54-
try {
55-
const raw = readFileSync(installMethodPath(), "utf-8");
56-
const parsed = parseInstallMethod(raw.split("\n")[0]);
57-
if (parsed) return parsed;
58-
} catch {
59-
/* missing or unreadable */
77+
if (identity?.clientName) {
78+
const productMethod = readInstallMethodFile(installMethodPath(identity.clientName));
79+
if (productMethod) return productMethod;
80+
81+
if (identity.clientName === BINARY_PRODUCT_CLIENT_NAME) {
82+
const legacyMethod = readInstallMethodFile(installMethodPath());
83+
if (legacyMethod) return legacyMethod;
84+
}
85+
86+
return detectInstallMethod();
6087
}
6188

89+
const legacyMethod = readInstallMethodFile(installMethodPath());
90+
if (legacyMethod) return legacyMethod;
91+
6292
return detectInstallMethod();
6393
}
6494

65-
/** Persist install method under `~/.bailian/install-method` (best-effort). */
66-
export function writeInstallMethodSync(method: InstallMethod): void {
95+
/**
96+
* Install method for update / auto-update routing.
97+
* Only `bailian-cli` may follow the binary channel; other products always use npm
98+
* even if env or a mistaken marker claims `binary`.
99+
*/
100+
export function getUpdateInstallMethod(identity: {
101+
clientName: string;
102+
npmPackage: string;
103+
}): InstallMethod {
104+
const method = getInstallMethod(identity);
105+
if (method === "binary" && identity.npmPackage !== BINARY_PRODUCT_CLIENT_NAME) {
106+
return "npm";
107+
}
108+
return method;
109+
}
110+
111+
/**
112+
* Persist install method under `~/.bailian/install-method.<clientName>` (best-effort).
113+
* For `bailian-cli`, also write the legacy `install-method` file for older readers.
114+
*/
115+
export function writeInstallMethodSync(
116+
method: InstallMethod,
117+
identity: InstallMethodIdentity = { clientName: BINARY_PRODUCT_CLIENT_NAME },
118+
): void {
67119
try {
68120
const dir = getConfigDir();
69121
if (!existsSync(dir)) {
70122
mkdirSync(dir, { recursive: true, mode: 0o700 });
71123
}
72-
writeFileSync(installMethodPath(), `${method}\n`, { mode: 0o600 });
124+
writeFileSync(installMethodPath(identity.clientName), `${method}\n`, { mode: 0o600 });
125+
if (identity.clientName === BINARY_PRODUCT_CLIENT_NAME) {
126+
writeFileSync(installMethodPath(), `${method}\n`, { mode: 0o600 });
127+
}
73128
} catch {
74129
/* best effort */
75130
}

0 commit comments

Comments
 (0)