Skip to content

Commit 091b24e

Browse files
committed
release: v0.5.0
1 parent 10b8100 commit 091b24e

5 files changed

Lines changed: 197 additions & 10 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ boss help
6363
| 命令 | 说明 |
6464
| --- | --- |
6565
| `boss login` | 打开 Boss直聘登录页(扫码/验证后手动完成) |
66+
| `boss update` | 通过 npm 安装最新版 boss-cli |
6667
| `boss list [--unread]` | 读取聊天列表;`--unread` 仅未读 |
6768
| `boss chat <姓名> [--strict]` | 打开指定候选人会话 |
6869
| `boss send [--text <内容>]` | 向当前会话发送消息 |

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@joohw/boss-cli",
3-
"version": "0.4.1",
3+
"version": "0.5.0",
44
"description": "Boss直聘自动化 CLI:批量发消息、自动打招呼、候选人管理、深度搜索。基于 Puppeteer/CDP 驱动本机 Chrome,支持 AI Agent 编排",
55
"keywords": [
66
"boss",

src/cli/cliRouter.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import {
2222
type ChatPageAction,
2323
} from '../toolset/index.js';
2424
import { printBossInteractiveBanner } from './banner.js';
25-
import { printVersionInfo } from './version.js';
25+
import {
26+
printPackageUpdateNoticeIfDue,
27+
printVersionInfo,
28+
runPackageUpdate,
29+
} from './version.js';
2630

2731
class CliError extends Error {
2832
constructor(message: string) {
@@ -133,6 +137,8 @@ function printHelp(): void {
133137
显示本帮助
134138
boss version | ver(或 -v / --version)
135139
显示当前版本并检查 npm 是否有更新
140+
boss update
141+
使用 npm 安装最新版 boss-cli
136142
boss login
137143
打开登录页(需要用户在浏览器中自行完成登录,这个命令会直接返回)
138144
boss list [--unread]
@@ -278,6 +284,14 @@ export async function executeCommand(argv: string[]): Promise<string> {
278284
return implLogin();
279285
}
280286

287+
if (cmd === 'update') {
288+
const { rest, opts, flags } = parseOpts(tail);
289+
if (rest.length > 0 || Object.keys(opts).length > 0 || flags.size > 0) {
290+
die('❌ 用法: update');
291+
}
292+
return runPackageUpdate();
293+
}
294+
281295
if (cmd === 'list') {
282296
const { flags } = parseOpts(tail);
283297
if (flags.has('unread')) {
@@ -440,6 +454,7 @@ export async function runOneCommand(argv: string[]): Promise<void> {
440454

441455
async function runInteractiveLoop(): Promise<void> {
442456
const rl = createInterface({ input, output, terminal: true });
457+
await printPackageUpdateNoticeIfDue();
443458
printBossInteractiveBanner();
444459
try {
445460
for (;;) {
@@ -506,6 +521,10 @@ export async function runCli(argv: string[]): Promise<void> {
506521
return;
507522
}
508523

524+
if (normalizeSubcommand(argv[0] ?? '') !== 'update') {
525+
await printPackageUpdateNoticeIfDue();
526+
}
527+
509528
if (argv[0] === 'help' || argv[0] === '--help' || argv[0] === '-h') {
510529
printHelp();
511530
return;

src/cli/version.ts

Lines changed: 173 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import { readFileSync } from 'node:fs';
1+
import { spawn } from 'node:child_process';
2+
import { existsSync, readFileSync } from 'node:fs';
3+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
24
import { dirname, join } from 'node:path';
5+
import process from 'node:process';
36
import { fileURLToPath } from 'node:url';
7+
import { CACHE_DIR } from '../config.js';
8+
9+
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
10+
const UPDATE_CHECK_STATE_FILE = join(CACHE_DIR, 'version-check.json');
411

512
function getPackageJsonPath(): string {
613
return join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
@@ -18,7 +25,7 @@ export function getPackageMeta(): { name: string; version: string } {
1825
}
1926

2027
/** 比较 semver x.y.z(忽略预发布标签,仅比较主版本段) */
21-
function compareSemver(a: string, b: string): number {
28+
export function compareSemver(a: string, b: string): number {
2229
const core = (s: string) => s.split('-')[0] ?? '';
2330
const pa = core(a).split('.').map((x) => parseInt(x, 10));
2431
const pb = core(b).split('.').map((x) => parseInt(x, 10));
@@ -36,6 +43,30 @@ function compareSemver(a: string, b: string): number {
3643
return 0;
3744
}
3845

46+
export type PackageUpdateCheckResult = {
47+
checked: boolean;
48+
current: string;
49+
latest: string | null;
50+
updateAvailable: boolean;
51+
};
52+
53+
type PackageUpdateCheckState = {
54+
checkedAt: string;
55+
packageName: string;
56+
currentVersion: string;
57+
latestVersion: string;
58+
};
59+
60+
type CheckPackageUpdateOptions = {
61+
currentVersion?: string;
62+
fetchLatestVersion?: (packageName: string) => Promise<string>;
63+
force?: boolean;
64+
intervalMs?: number;
65+
now?: Date;
66+
packageName?: string;
67+
statePath?: string;
68+
};
69+
3970
export async function fetchNpmLatestVersion(packageName: string): Promise<string> {
4071
const path = `${encodeURIComponent(packageName)}/latest`;
4172
const url = `https://registry.npmjs.org/${path}`;
@@ -52,12 +83,148 @@ export async function fetchNpmLatestVersion(packageName: string): Promise<string
5283
return data.version;
5384
}
5485

86+
function getUpdateCheckStatePath(): string {
87+
return process.env.BOSS_CLI_UPDATE_CHECK_STATE_PATH?.trim() || UPDATE_CHECK_STATE_FILE;
88+
}
89+
90+
function shouldCheckFromState(
91+
state: PackageUpdateCheckState | null,
92+
now: Date,
93+
intervalMs: number,
94+
): boolean {
95+
if (!state) {
96+
return true;
97+
}
98+
const checkedAtMs = Date.parse(state.checkedAt);
99+
if (Number.isNaN(checkedAtMs)) {
100+
throw new Error(`版本检查缓存 checkedAt 无法解析:${state.checkedAt}`);
101+
}
102+
return now.getTime() - checkedAtMs > intervalMs;
103+
}
104+
105+
async function readUpdateCheckState(statePath: string): Promise<PackageUpdateCheckState | null> {
106+
if (!existsSync(statePath)) {
107+
return null;
108+
}
109+
const raw = await readFile(statePath, 'utf8');
110+
const parsed = JSON.parse(raw) as Partial<PackageUpdateCheckState>;
111+
if (
112+
typeof parsed.checkedAt !== 'string' ||
113+
typeof parsed.packageName !== 'string' ||
114+
typeof parsed.currentVersion !== 'string' ||
115+
typeof parsed.latestVersion !== 'string'
116+
) {
117+
throw new Error(`版本检查缓存格式无效:${statePath}`);
118+
}
119+
return {
120+
checkedAt: parsed.checkedAt,
121+
packageName: parsed.packageName,
122+
currentVersion: parsed.currentVersion,
123+
latestVersion: parsed.latestVersion,
124+
};
125+
}
126+
127+
async function writeUpdateCheckState(
128+
statePath: string,
129+
state: PackageUpdateCheckState,
130+
): Promise<void> {
131+
await mkdir(dirname(statePath), { recursive: true });
132+
const tmpPath = `${statePath}.tmp`;
133+
await writeFile(tmpPath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
134+
await rename(tmpPath, statePath);
135+
}
136+
137+
export function formatPackageUpdateNotice(result: PackageUpdateCheckResult): string {
138+
if (!result.updateAvailable || !result.latest) {
139+
throw new Error('没有可用更新,无法生成升级提示');
140+
}
141+
return `[boss-cli] boss-cli 需要升级:当前 ${result.current},最新 ${result.latest}。请运行 boss update`;
142+
}
143+
144+
export async function checkPackageUpdate(
145+
options: CheckPackageUpdateOptions = {},
146+
): Promise<PackageUpdateCheckResult> {
147+
const meta = getPackageMeta();
148+
const packageName = options.packageName ?? meta.name;
149+
const current = options.currentVersion ?? meta.version;
150+
const now = options.now ?? new Date();
151+
const intervalMs = options.intervalMs ?? UPDATE_CHECK_INTERVAL_MS;
152+
const statePath = options.statePath ?? getUpdateCheckStatePath();
153+
const fetchLatestVersion = options.fetchLatestVersion ?? fetchNpmLatestVersion;
154+
const state = await readUpdateCheckState(statePath);
155+
156+
if (!options.force && !shouldCheckFromState(state, now, intervalMs)) {
157+
const latest = state?.latestVersion ?? null;
158+
return {
159+
checked: false,
160+
current,
161+
latest,
162+
updateAvailable: latest ? compareSemver(latest, current) > 0 : false,
163+
};
164+
}
165+
166+
const latest = await fetchLatestVersion(packageName);
167+
const result = {
168+
checked: true,
169+
current,
170+
latest,
171+
updateAvailable: compareSemver(latest, current) > 0,
172+
};
173+
await writeUpdateCheckState(statePath, {
174+
checkedAt: now.toISOString(),
175+
packageName,
176+
currentVersion: current,
177+
latestVersion: latest,
178+
});
179+
return result;
180+
}
181+
182+
export async function printPackageUpdateNoticeIfDue(): Promise<void> {
183+
try {
184+
const result = await checkPackageUpdate();
185+
if (result.checked && result.updateAvailable) {
186+
console.error(formatPackageUpdateNotice(result));
187+
}
188+
} catch (e) {
189+
const message = e instanceof Error ? e.message : String(e);
190+
console.error(`[boss-cli] 版本更新检查失败:${message}`);
191+
}
192+
}
193+
55194
export async function printVersionInfo(): Promise<void> {
56195
const { name, version: current } = getPackageMeta();
57196
console.log(`${name} ${current}`);
58-
const latest = await fetchNpmLatestVersion(name);
59-
const cmp = compareSemver(latest, current);
60-
if (cmp > 0) {
61-
console.log(`新版本可用: ${latest}(当前 ${current})。更新: npm i -g ${name}`);
197+
const result = await checkPackageUpdate({ force: true });
198+
if (result.updateAvailable) {
199+
console.log(formatPackageUpdateNotice(result));
200+
} else if (result.latest) {
201+
console.log(`未发现更新:当前 ${result.current},npm latest ${result.latest}`);
202+
}
203+
}
204+
205+
export async function runPackageUpdate(): Promise<string> {
206+
const { name } = getPackageMeta();
207+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
208+
const args = ['install', '-g', `${name}@latest`];
209+
console.error(`[boss-cli] 正在执行:npm install -g ${name}@latest`);
210+
211+
const code = await new Promise<number>((resolve, reject) => {
212+
const child = spawn(npmCommand, args, {
213+
shell: process.platform === 'win32',
214+
stdio: 'inherit',
215+
});
216+
child.on('error', reject);
217+
child.on('close', (exitCode, signal) => {
218+
if (signal) {
219+
reject(new Error(`npm 更新进程被信号中断:${signal}`));
220+
return;
221+
}
222+
resolve(exitCode ?? 0);
223+
});
224+
});
225+
226+
if (code !== 0) {
227+
throw new Error(`npm 更新失败,退出码 ${code}`);
62228
}
229+
return 'boss-cli 更新完成。请重新运行 boss version 确认当前版本。';
63230
}

0 commit comments

Comments
 (0)