clawt 2.17.0 → 2.18.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/README.md +1 -0
- package/dist/index.js +197 -42
- package/dist/postinstall.js +8 -3
- package/docs/spec.md +101 -1
- package/package.json +1 -1
- package/src/commands/validate.ts +1 -4
- package/src/constants/config.ts +4 -0
- package/src/constants/index.ts +3 -1
- package/src/constants/messages/index.ts +4 -2
- package/src/constants/messages/update.ts +15 -0
- package/src/constants/messages/validate.ts +0 -3
- package/src/constants/paths.ts +3 -0
- package/src/constants/update.ts +11 -0
- package/src/index.ts +14 -2
- package/src/types/config.ts +2 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/update-checker.ts +213 -0
- package/tests/unit/commands/alias.test.ts +1 -0
- package/tests/unit/commands/config.test.ts +4 -0
- package/tests/unit/utils/config-strategy.test.ts +4 -1
- package/tests/unit/utils/update-checker.test.ts +439 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 各包管理器对应的全局安装命令 */
|
|
2
|
+
export const UPDATE_COMMANDS: Record<string, string> = {
|
|
3
|
+
npm: 'npm i -g clawt',
|
|
4
|
+
pnpm: 'pnpm add -g clawt',
|
|
5
|
+
yarn: 'yarn global add clawt',
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
/** 更新检查相关提示消息 */
|
|
9
|
+
export const UPDATE_MESSAGES = {
|
|
10
|
+
/** 版本更新提示 */
|
|
11
|
+
UPDATE_AVAILABLE: (currentVersion: string, latestVersion: string) =>
|
|
12
|
+
`clawt 有新版本可用: ${currentVersion} → ${latestVersion}`,
|
|
13
|
+
/** 更新操作提示 */
|
|
14
|
+
UPDATE_HINT: (command: string) => `执行 ${command} 进行更新`,
|
|
15
|
+
} as const;
|
|
@@ -59,9 +59,6 @@ export const VALIDATE_MESSAGES = {
|
|
|
59
59
|
/** 自动 sync 开始提示 */
|
|
60
60
|
VALIDATE_AUTO_SYNC_START: (branch: string) =>
|
|
61
61
|
`正在自动同步主分支到 ${branch} ...`,
|
|
62
|
-
/** 自动 sync 存在冲突,无法重试 */
|
|
63
|
-
VALIDATE_AUTO_SYNC_CONFLICT: (worktreePath: string) =>
|
|
64
|
-
`同步存在冲突,请进入目标 worktree 手动解决冲突后重试\n cd ${worktreePath}`,
|
|
65
62
|
/** 用户拒绝自动 sync */
|
|
66
63
|
VALIDATE_AUTO_SYNC_DECLINED: (branch: string) =>
|
|
67
64
|
`请手动执行 clawt sync -b ${branch} 同步主分支后重试`,
|
package/src/constants/paths.ts
CHANGED
|
@@ -18,3 +18,6 @@ export const VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, 'validate-snapshots');
|
|
|
18
18
|
|
|
19
19
|
/** Claude Code 项目会话目录 ~/.claude/projects/ */
|
|
20
20
|
export const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
|
|
21
|
+
|
|
22
|
+
/** 更新检查缓存文件路径 ~/.clawt/update-check.json */
|
|
23
|
+
export const UPDATE_CHECK_PATH = join(CLAWT_HOME, 'update-check.json');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 更新检查间隔,24 小时(毫秒) */
|
|
2
|
+
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
3
|
+
|
|
4
|
+
/** npm registry 查询地址 */
|
|
5
|
+
export const NPM_REGISTRY_URL = 'https://registry.npmjs.org/clawt/latest';
|
|
6
|
+
|
|
7
|
+
/** npm registry 请求超时时间(毫秒) */
|
|
8
|
+
export const NPM_REGISTRY_TIMEOUT_MS = 5000;
|
|
9
|
+
|
|
10
|
+
/** 包名 */
|
|
11
|
+
export const PACKAGE_NAME = 'clawt';
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { ClawtError } from './errors/index.js';
|
|
4
4
|
import { logger, enableConsoleTransport } from './logger/index.js';
|
|
5
5
|
import { EXIT_CODES } from './constants/index.js';
|
|
6
|
-
import { printError, ensureClawtDirs, loadConfig, applyAliases } from './utils/index.js';
|
|
6
|
+
import { printError, ensureClawtDirs, loadConfig, applyAliases, checkForUpdates } from './utils/index.js';
|
|
7
7
|
import { registerListCommand } from './commands/list.js';
|
|
8
8
|
import { registerCreateCommand } from './commands/create.js';
|
|
9
9
|
import { registerRemoveCommand } from './commands/remove.js';
|
|
@@ -83,4 +83,16 @@ process.on('unhandledRejection', (reason) => {
|
|
|
83
83
|
process.exit(EXIT_CODES.ERROR);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* 异步主入口函数
|
|
88
|
+
* 执行命令解析后,根据配置项决定是否进行自动更新检查
|
|
89
|
+
*/
|
|
90
|
+
async function main(): Promise<void> {
|
|
91
|
+
await program.parseAsync(process.argv);
|
|
92
|
+
|
|
93
|
+
if (config.autoUpdate) {
|
|
94
|
+
await checkForUpdates(version);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main();
|
package/src/types/config.ts
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -67,4 +67,5 @@ export { detectTerminalApp, openCommandInNewTerminalTab } from './terminal.js';
|
|
|
67
67
|
export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
|
|
68
68
|
export { applyAliases } from './alias.js';
|
|
69
69
|
export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue } from './config-strategy.js';
|
|
70
|
+
export { checkForUpdates } from './update-checker.js';
|
|
70
71
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { request } from 'node:https';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import stringWidth from 'string-width';
|
|
6
|
+
import {
|
|
7
|
+
UPDATE_CHECK_PATH,
|
|
8
|
+
UPDATE_CHECK_INTERVAL_MS,
|
|
9
|
+
NPM_REGISTRY_URL,
|
|
10
|
+
NPM_REGISTRY_TIMEOUT_MS,
|
|
11
|
+
PACKAGE_NAME,
|
|
12
|
+
} from '../constants/index.js';
|
|
13
|
+
import { UPDATE_MESSAGES, UPDATE_COMMANDS } from '../constants/messages/index.js';
|
|
14
|
+
|
|
15
|
+
/** 更新检查缓存结构 */
|
|
16
|
+
interface UpdateCache {
|
|
17
|
+
/** 上次检查时间戳 */
|
|
18
|
+
lastCheck: number;
|
|
19
|
+
/** 最新版本号 */
|
|
20
|
+
latestVersion: string;
|
|
21
|
+
/** 检查时的本地版本号 */
|
|
22
|
+
currentVersion: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 读取更新检查缓存文件
|
|
27
|
+
* @returns {UpdateCache | null} 缓存数据,文件不存在或解析失败返回 null
|
|
28
|
+
*/
|
|
29
|
+
function readUpdateCache(): UpdateCache | null {
|
|
30
|
+
try {
|
|
31
|
+
const raw = readFileSync(UPDATE_CHECK_PATH, 'utf-8');
|
|
32
|
+
return JSON.parse(raw) as UpdateCache;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 写入更新检查缓存文件
|
|
40
|
+
* @param {UpdateCache} cache - 缓存数据
|
|
41
|
+
*/
|
|
42
|
+
function writeUpdateCache(cache: UpdateCache): void {
|
|
43
|
+
try {
|
|
44
|
+
writeFileSync(UPDATE_CHECK_PATH, JSON.stringify(cache, null, 2), 'utf-8');
|
|
45
|
+
} catch {
|
|
46
|
+
// 写入失败时静默忽略,不影响 CLI 正常功能
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 判断缓存是否过期(超过 24 小时或本地版本已变化)
|
|
52
|
+
* @param {UpdateCache} cache - 缓存数据
|
|
53
|
+
* @param {string} currentVersion - 当前本地版本号
|
|
54
|
+
* @returns {boolean} 缓存是否已过期
|
|
55
|
+
*/
|
|
56
|
+
function isCacheExpired(cache: UpdateCache, currentVersion: string): boolean {
|
|
57
|
+
if (cache.currentVersion !== currentVersion) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return Date.now() - cache.lastCheck > UPDATE_CHECK_INTERVAL_MS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 简易 semver 版本比较(不引入新依赖)
|
|
65
|
+
* @param {string} latest - 最新版本号
|
|
66
|
+
* @param {string} current - 当前版本号
|
|
67
|
+
* @returns {boolean} latest 是否大于 current
|
|
68
|
+
*/
|
|
69
|
+
function isNewerVersion(latest: string, current: string): boolean {
|
|
70
|
+
const latestParts = latest.split('.').map(Number);
|
|
71
|
+
const currentParts = current.split('.').map(Number);
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < 3; i++) {
|
|
74
|
+
const l = latestParts[i] || 0;
|
|
75
|
+
const c = currentParts[i] || 0;
|
|
76
|
+
if (l > c) return true;
|
|
77
|
+
if (l < c) return false;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 从 npm registry 获取最新版本号(5 秒超时)
|
|
84
|
+
* @returns {Promise<string | null>} 最新版本号,请求失败返回 null
|
|
85
|
+
*/
|
|
86
|
+
function fetchLatestVersion(): Promise<string | null> {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
const req = request(NPM_REGISTRY_URL, { timeout: NPM_REGISTRY_TIMEOUT_MS }, (res) => {
|
|
89
|
+
let data = '';
|
|
90
|
+
res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
|
|
91
|
+
res.on('end', () => {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(data) as { version?: string };
|
|
94
|
+
resolve(parsed.version ?? null);
|
|
95
|
+
} catch {
|
|
96
|
+
resolve(null);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
req.on('error', () => resolve(null));
|
|
102
|
+
req.on('timeout', () => {
|
|
103
|
+
req.destroy();
|
|
104
|
+
resolve(null);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
req.end();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 探测当前全局安装 clawt 所用的包管理器
|
|
113
|
+
* 依次尝试 pnpm、yarn 的全局列表命令,匹配到则返回对应名称,否则默认 npm
|
|
114
|
+
* @returns {string} 包管理器名称:'pnpm' | 'yarn' | 'npm'
|
|
115
|
+
*/
|
|
116
|
+
function detectPackageManager(): string {
|
|
117
|
+
const checks = [
|
|
118
|
+
{ name: 'pnpm', command: `pnpm list -g --depth=0 ${PACKAGE_NAME}` },
|
|
119
|
+
{ name: 'yarn', command: `yarn global list --depth=0 2>/dev/null` },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
for (const { name, command } of checks) {
|
|
123
|
+
try {
|
|
124
|
+
const output = execSync(command, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
125
|
+
if (output.includes(PACKAGE_NAME)) {
|
|
126
|
+
return name;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// 命令不存在或执行失败,跳过
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return 'npm';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 绘制带边框的版本更新提示框
|
|
138
|
+
* @param {string} currentVersion - 当前版本号
|
|
139
|
+
* @param {string} latestVersion - 最新版本号
|
|
140
|
+
*/
|
|
141
|
+
function printUpdateNotification(currentVersion: string, latestVersion: string): void {
|
|
142
|
+
const updateText = UPDATE_MESSAGES.UPDATE_AVAILABLE(
|
|
143
|
+
chalk.red(currentVersion),
|
|
144
|
+
chalk.green(latestVersion),
|
|
145
|
+
);
|
|
146
|
+
const pm = detectPackageManager();
|
|
147
|
+
const updateCommand = UPDATE_COMMANDS[pm] || UPDATE_COMMANDS.npm;
|
|
148
|
+
const commandText = UPDATE_MESSAGES.UPDATE_HINT(chalk.cyan(updateCommand));
|
|
149
|
+
|
|
150
|
+
const updateTextWidth = stringWidth(updateText);
|
|
151
|
+
const commandTextWidth = stringWidth(commandText);
|
|
152
|
+
const innerWidth = Math.max(updateTextWidth, commandTextWidth) + 4;
|
|
153
|
+
|
|
154
|
+
const padLine = (text: string): string => {
|
|
155
|
+
const textWidth = stringWidth(text);
|
|
156
|
+
const leftPad = Math.floor((innerWidth - textWidth) / 2);
|
|
157
|
+
const rightPad = innerWidth - textWidth - leftPad;
|
|
158
|
+
return `│${' '.repeat(leftPad)}${text}${' '.repeat(rightPad)}│`;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const top = `╭${'─'.repeat(innerWidth)}╮`;
|
|
162
|
+
const bottom = `╰${'─'.repeat(innerWidth)}╯`;
|
|
163
|
+
const emptyLine = `│${' '.repeat(innerWidth)}│`;
|
|
164
|
+
|
|
165
|
+
console.log();
|
|
166
|
+
console.log(top);
|
|
167
|
+
console.log(emptyLine);
|
|
168
|
+
console.log(padLine(updateText));
|
|
169
|
+
console.log(padLine(commandText));
|
|
170
|
+
console.log(emptyLine);
|
|
171
|
+
console.log(bottom);
|
|
172
|
+
console.log();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 检查更新入口函数
|
|
177
|
+
* 读取缓存 → 缓存有效且有新版本则打印提示 → 缓存过期则请求 registry 刷新
|
|
178
|
+
* 所有异常静默处理,不影响 CLI 正常功能
|
|
179
|
+
* @param {string} currentVersion - 当前本地版本号
|
|
180
|
+
*/
|
|
181
|
+
export async function checkForUpdates(currentVersion: string): Promise<void> {
|
|
182
|
+
try {
|
|
183
|
+
const cache = readUpdateCache();
|
|
184
|
+
|
|
185
|
+
// 缓存有效:直接判断是否需要提示
|
|
186
|
+
if (cache && !isCacheExpired(cache, currentVersion)) {
|
|
187
|
+
if (isNewerVersion(cache.latestVersion, currentVersion)) {
|
|
188
|
+
printUpdateNotification(currentVersion, cache.latestVersion);
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 缓存过期或不存在:请求 registry
|
|
194
|
+
const latestVersion = await fetchLatestVersion();
|
|
195
|
+
if (!latestVersion) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 写入新缓存
|
|
200
|
+
writeUpdateCache({
|
|
201
|
+
lastCheck: Date.now(),
|
|
202
|
+
latestVersion,
|
|
203
|
+
currentVersion,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// 有新版本则打印提示
|
|
207
|
+
if (isNewerVersion(latestVersion, currentVersion)) {
|
|
208
|
+
printUpdateNotification(currentVersion, latestVersion);
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// 任何异常静默忽略,不影响 CLI 正常功能
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -51,6 +51,7 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
51
51
|
maxConcurrency: 0,
|
|
52
52
|
terminalApp: 'auto',
|
|
53
53
|
aliases: {},
|
|
54
|
+
autoUpdate: true,
|
|
54
55
|
},
|
|
55
56
|
CONFIG_DESCRIPTIONS: {
|
|
56
57
|
autoDeleteBranch: '自动删除分支',
|
|
@@ -60,6 +61,7 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
60
61
|
maxConcurrency: '最大并发数',
|
|
61
62
|
terminalApp: '终端应用',
|
|
62
63
|
aliases: '命令别名映射',
|
|
64
|
+
autoUpdate: '自动更新',
|
|
63
65
|
},
|
|
64
66
|
CONFIG_DEFINITIONS: {
|
|
65
67
|
autoDeleteBranch: { defaultValue: false, description: '自动删除分支' },
|
|
@@ -69,6 +71,7 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
69
71
|
maxConcurrency: { defaultValue: 0, description: '最大并发数' },
|
|
70
72
|
terminalApp: { defaultValue: 'auto', description: '终端应用', allowedValues: ['auto', 'iterm2', 'terminal'] },
|
|
71
73
|
aliases: { defaultValue: {}, description: '命令别名映射' },
|
|
74
|
+
autoUpdate: { defaultValue: true, description: '自动更新' },
|
|
72
75
|
},
|
|
73
76
|
CONFIG_ALIAS_DISABLED_HINT: '(通过 clawt alias 命令管理)',
|
|
74
77
|
MESSAGES: {
|
|
@@ -111,6 +114,7 @@ function createMockConfig() {
|
|
|
111
114
|
maxConcurrency: 0,
|
|
112
115
|
terminalApp: 'auto',
|
|
113
116
|
aliases: {},
|
|
117
|
+
autoUpdate: true,
|
|
114
118
|
};
|
|
115
119
|
}
|
|
116
120
|
|
|
@@ -29,6 +29,7 @@ vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
|
29
29
|
confirmDestructiveOps: true,
|
|
30
30
|
maxConcurrency: 0,
|
|
31
31
|
terminalApp: 'auto',
|
|
32
|
+
autoUpdate: true,
|
|
32
33
|
},
|
|
33
34
|
CONFIG_DEFINITIONS: {
|
|
34
35
|
autoDeleteBranch: { defaultValue: false, description: '自动删除分支' },
|
|
@@ -37,6 +38,7 @@ vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
|
37
38
|
confirmDestructiveOps: { defaultValue: true, description: '破坏性操作确认' },
|
|
38
39
|
maxConcurrency: { defaultValue: 0, description: '最大并发数' },
|
|
39
40
|
terminalApp: { defaultValue: 'auto', description: '终端应用', allowedValues: ['auto', 'iterm2', 'terminal'] },
|
|
41
|
+
autoUpdate: { defaultValue: true, description: '自动更新' },
|
|
40
42
|
},
|
|
41
43
|
MESSAGES: {
|
|
42
44
|
CONFIG_INVALID_BOOLEAN: (key: string) =>
|
|
@@ -86,7 +88,8 @@ describe('getValidConfigKeys', () => {
|
|
|
86
88
|
expect(keys).toContain('confirmDestructiveOps');
|
|
87
89
|
expect(keys).toContain('maxConcurrency');
|
|
88
90
|
expect(keys).toContain('terminalApp');
|
|
89
|
-
expect(keys).
|
|
91
|
+
expect(keys).toContain('autoUpdate');
|
|
92
|
+
expect(keys).toHaveLength(7);
|
|
90
93
|
});
|
|
91
94
|
});
|
|
92
95
|
|