clawt 2.17.1 → 2.19.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 +11 -0
- package/dist/index.js +477 -63
- package/dist/postinstall.js +35 -0
- package/docs/spec.md +242 -1
- package/package.json +1 -1
- package/src/commands/projects.ts +324 -0
- package/src/constants/config.ts +4 -0
- package/src/constants/index.ts +3 -1
- package/src/constants/messages/index.ts +6 -2
- package/src/constants/messages/projects.ts +25 -0
- package/src/constants/messages/update.ts +15 -0
- package/src/constants/paths.ts +3 -0
- package/src/constants/update.ts +11 -0
- package/src/index.ts +16 -2
- package/src/types/command.ts +8 -0
- package/src/types/config.ts +2 -0
- package/src/types/index.ts +2 -1
- package/src/types/project.ts +45 -0
- package/src/utils/formatter.ts +46 -0
- package/src/utils/fs.ts +32 -1
- package/src/utils/index.ts +3 -2
- 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/formatter.test.ts +91 -1
- package/tests/unit/utils/fs.test.ts +125 -2
- package/tests/unit/utils/update-checker.test.ts +439 -0
package/src/constants/config.ts
CHANGED
package/src/constants/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR, CLAUDE_PROJECTS_DIR } from './paths.js';
|
|
1
|
+
export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR, CLAUDE_PROJECTS_DIR, UPDATE_CHECK_PATH } from './paths.js';
|
|
2
2
|
export { INVALID_BRANCH_CHARS } from './branch.js';
|
|
3
3
|
export { MESSAGES } from './messages/index.js';
|
|
4
4
|
export { CONFIG_ALIAS_DISABLED_HINT } from './messages/index.js';
|
|
5
|
+
export { UPDATE_MESSAGES, UPDATE_COMMANDS } from './messages/update.js';
|
|
5
6
|
export { EXIT_CODES } from './exitCodes.js';
|
|
6
7
|
export { ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS, VALID_TERMINAL_APPS, ITERM2_APP_PATH } from './terminal.js';
|
|
7
8
|
export { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, CONFIG_DEFINITIONS, APPEND_SYSTEM_PROMPT } from './config.js';
|
|
8
9
|
export { AUTO_SAVE_COMMIT_MESSAGE } from './git.js';
|
|
9
10
|
export { DEBUG_LOG_PREFIX, DEBUG_TIMESTAMP_FORMAT } from './logger.js';
|
|
11
|
+
export { UPDATE_CHECK_INTERVAL_MS, NPM_REGISTRY_URL, NPM_REGISTRY_TIMEOUT_MS, PACKAGE_NAME } from './update.js';
|
|
10
12
|
export {
|
|
11
13
|
SPINNER_FRAMES,
|
|
12
14
|
SPINNER_INTERVAL_MS,
|
|
@@ -8,11 +8,14 @@ import { RESUME_MESSAGES } from './resume.js';
|
|
|
8
8
|
import { REMOVE_MESSAGES } from './remove.js';
|
|
9
9
|
import { RESET_MESSAGES } from './reset.js';
|
|
10
10
|
import { CONFIG_CMD_MESSAGES, CONFIG_ALIAS_DISABLED_HINT } from './config.js';
|
|
11
|
-
|
|
12
|
-
export { CONFIG_ALIAS_DISABLED_HINT };
|
|
13
11
|
import { STATUS_MESSAGES } from './status.js';
|
|
14
12
|
import { ALIAS_MESSAGES } from './alias.js';
|
|
13
|
+
import { PROJECTS_MESSAGES } from './projects.js';
|
|
15
14
|
import { COMPLETION_MESSAGES } from './completion.js';
|
|
15
|
+
import { UPDATE_MESSAGES, UPDATE_COMMANDS } from './update.js';
|
|
16
|
+
|
|
17
|
+
export { CONFIG_ALIAS_DISABLED_HINT };
|
|
18
|
+
export { UPDATE_MESSAGES, UPDATE_COMMANDS };
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* 提示消息模板
|
|
@@ -31,5 +34,6 @@ export const MESSAGES = {
|
|
|
31
34
|
...CONFIG_CMD_MESSAGES,
|
|
32
35
|
...STATUS_MESSAGES,
|
|
33
36
|
...ALIAS_MESSAGES,
|
|
37
|
+
...PROJECTS_MESSAGES,
|
|
34
38
|
...COMPLETION_MESSAGES,
|
|
35
39
|
} as const;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** projects 命令专属提示消息 */
|
|
2
|
+
export const PROJECTS_MESSAGES = {
|
|
3
|
+
/** projects 命令全局概览标题 */
|
|
4
|
+
PROJECTS_OVERVIEW_TITLE: '项目概览',
|
|
5
|
+
/** projects 命令指定项目详情标题 */
|
|
6
|
+
PROJECTS_DETAIL_TITLE: (projectName: string) => `项目详情: ${projectName}`,
|
|
7
|
+
/** 无项目提示 */
|
|
8
|
+
PROJECTS_NO_PROJECTS: '(暂无项目,worktrees 目录为空)',
|
|
9
|
+
/** 项目不存在提示 */
|
|
10
|
+
PROJECTS_NOT_FOUND: (name: string) => `项目 ${name} 不存在`,
|
|
11
|
+
/** worktree 数量标签 */
|
|
12
|
+
PROJECTS_WORKTREE_COUNT: (count: number) => `${count} 个 worktree`,
|
|
13
|
+
/** 最近活跃时间标签 */
|
|
14
|
+
PROJECTS_LAST_ACTIVE: (relativeTime: string) => `最近活跃: ${relativeTime}`,
|
|
15
|
+
/** 磁盘占用标签 */
|
|
16
|
+
PROJECTS_DISK_USAGE: (size: string) => `磁盘占用: ${size}`,
|
|
17
|
+
/** 总磁盘占用标签 */
|
|
18
|
+
PROJECTS_TOTAL_DISK_USAGE: (size: string) => `总占用: ${size}`,
|
|
19
|
+
/** projects 详情无 worktree */
|
|
20
|
+
PROJECTS_DETAIL_NO_WORKTREES: '(该项目下无 worktree)',
|
|
21
|
+
/** 路径标签 */
|
|
22
|
+
PROJECTS_PATH: (path: string) => `路径: ${path}`,
|
|
23
|
+
/** 最后修改时间标签 */
|
|
24
|
+
PROJECTS_LAST_MODIFIED: (relativeTime: string) => `最后修改: ${relativeTime}`,
|
|
25
|
+
} as const;
|
|
@@ -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;
|
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';
|
|
@@ -16,6 +16,7 @@ import { registerSyncCommand } from './commands/sync.js';
|
|
|
16
16
|
import { registerResetCommand } from './commands/reset.js';
|
|
17
17
|
import { registerStatusCommand } from './commands/status.js';
|
|
18
18
|
import { registerAliasCommand } from './commands/alias.js';
|
|
19
|
+
import { registerProjectsCommand } from './commands/projects.js';
|
|
19
20
|
import { registerCompletionCommand } from './commands/completion.js';
|
|
20
21
|
|
|
21
22
|
// 从 package.json 读取版本号,避免硬编码
|
|
@@ -53,6 +54,7 @@ registerSyncCommand(program);
|
|
|
53
54
|
registerResetCommand(program);
|
|
54
55
|
registerStatusCommand(program);
|
|
55
56
|
registerAliasCommand(program);
|
|
57
|
+
registerProjectsCommand(program);
|
|
56
58
|
registerCompletionCommand(program);
|
|
57
59
|
|
|
58
60
|
// 加载配置并应用命令别名
|
|
@@ -83,4 +85,16 @@ process.on('unhandledRejection', (reason) => {
|
|
|
83
85
|
process.exit(EXIT_CODES.ERROR);
|
|
84
86
|
});
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
/**
|
|
89
|
+
* 异步主入口函数
|
|
90
|
+
* 执行命令解析后,根据配置项决定是否进行自动更新检查
|
|
91
|
+
*/
|
|
92
|
+
async function main(): Promise<void> {
|
|
93
|
+
await program.parseAsync(process.argv);
|
|
94
|
+
|
|
95
|
+
if (config.autoUpdate) {
|
|
96
|
+
await checkForUpdates(version);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
main();
|
package/src/types/command.ts
CHANGED
package/src/types/config.ts
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export type { ClawtConfig, ConfigItemDefinition, ConfigDefinitions } from './config.js';
|
|
2
|
-
export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions } from './command.js';
|
|
2
|
+
export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions, ProjectsOptions } from './command.js';
|
|
3
3
|
export type { WorktreeInfo, WorktreeStatus } from './worktree.js';
|
|
4
4
|
export type { ClaudeCodeResult } from './claudeCode.js';
|
|
5
5
|
export type { TaskResult, TaskSummary } from './taskResult.js';
|
|
6
6
|
export type { WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, SnapshotSummary, StatusResult } from './status.js';
|
|
7
7
|
export type { TaskFileEntry, ParseTaskFileOptions } from './taskFile.js';
|
|
8
|
+
export type { ProjectOverview, ProjectWorktreeDetail, ProjectDetailResult, ProjectsOverviewResult } from './project.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** 单个项目的概览信息 */
|
|
2
|
+
export interface ProjectOverview {
|
|
3
|
+
/** 项目名称 */
|
|
4
|
+
name: string;
|
|
5
|
+
/** worktree 数量 */
|
|
6
|
+
worktreeCount: number;
|
|
7
|
+
/** 最近活跃时间(ISO 8601 格式),无 worktree 时为目录修改时间 */
|
|
8
|
+
lastActiveTime: string;
|
|
9
|
+
/** 磁盘占用(字节) */
|
|
10
|
+
diskUsage: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 单个项目的 worktree 详情 */
|
|
14
|
+
export interface ProjectWorktreeDetail {
|
|
15
|
+
/** 分支名 */
|
|
16
|
+
branch: string;
|
|
17
|
+
/** worktree 路径 */
|
|
18
|
+
path: string;
|
|
19
|
+
/** 最后修改时间(ISO 8601 格式) */
|
|
20
|
+
lastModifiedTime: string;
|
|
21
|
+
/** 磁盘占用(字节) */
|
|
22
|
+
diskUsage: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** projects 命令展示指定项目时的完整结果 */
|
|
26
|
+
export interface ProjectDetailResult {
|
|
27
|
+
/** 项目名称 */
|
|
28
|
+
name: string;
|
|
29
|
+
/** 项目 worktree 根目录 */
|
|
30
|
+
projectDir: string;
|
|
31
|
+
/** worktree 详情列表(按最近活跃时间排序) */
|
|
32
|
+
worktrees: ProjectWorktreeDetail[];
|
|
33
|
+
/** 总磁盘占用(字节) */
|
|
34
|
+
totalDiskUsage: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** projects 命令展示所有项目概览时的完整结果 */
|
|
38
|
+
export interface ProjectsOverviewResult {
|
|
39
|
+
/** 所有项目概览列表(按最近活跃时间排序) */
|
|
40
|
+
projects: ProjectOverview[];
|
|
41
|
+
/** 项目总数 */
|
|
42
|
+
totalProjects: number;
|
|
43
|
+
/** 总磁盘占用(字节) */
|
|
44
|
+
totalDiskUsage: number;
|
|
45
|
+
}
|
package/src/utils/formatter.ts
CHANGED
|
@@ -184,3 +184,49 @@ export function formatRelativeTime(isoDateString: string): string | null {
|
|
|
184
184
|
const years = Math.floor(diffDays / 365);
|
|
185
185
|
return `${years} 年前`;
|
|
186
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 将字节数格式化为带单位的磁盘大小字符串
|
|
190
|
+
* @param {number} bytes - 字节数
|
|
191
|
+
* @returns {string} 格式化后的大小字符串,如 "1.5 GB"、"256.0 MB"、"10.2 KB"
|
|
192
|
+
*/
|
|
193
|
+
export function formatDiskSize(bytes: number): string {
|
|
194
|
+
/** 1 KB 的字节数 */
|
|
195
|
+
const KB = 1024;
|
|
196
|
+
/** 1 MB 的字节数 */
|
|
197
|
+
const MB = KB * 1024;
|
|
198
|
+
/** 1 GB 的字节数 */
|
|
199
|
+
const GB = MB * 1024;
|
|
200
|
+
|
|
201
|
+
if (bytes >= GB) {
|
|
202
|
+
return `${(bytes / GB).toFixed(1)} GB`;
|
|
203
|
+
}
|
|
204
|
+
if (bytes >= MB) {
|
|
205
|
+
return `${(bytes / MB).toFixed(1)} MB`;
|
|
206
|
+
}
|
|
207
|
+
if (bytes >= KB) {
|
|
208
|
+
return `${(bytes / KB).toFixed(1)} KB`;
|
|
209
|
+
}
|
|
210
|
+
return `${bytes} B`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 将 Date 对象格式化为本机时区的 ISO 8601 字符串
|
|
215
|
+
* 输出格式: YYYY-MM-DDTHH:mm:ss.sss+HH:MM
|
|
216
|
+
* @param {Date} date - 日期对象
|
|
217
|
+
* @returns {string} 本机时区的 ISO 8601 格式字符串
|
|
218
|
+
*/
|
|
219
|
+
export function formatLocalISOString(date: Date): string {
|
|
220
|
+
const tzOffsetMs = date.getTimezoneOffset() * 60 * 1000;
|
|
221
|
+
const localDate = new Date(date.getTime() - tzOffsetMs);
|
|
222
|
+
const iso = localDate.toISOString().slice(0, -1);
|
|
223
|
+
|
|
224
|
+
// 拼接时区偏移量
|
|
225
|
+
const totalMinutes = -date.getTimezoneOffset();
|
|
226
|
+
const sign = totalMinutes >= 0 ? '+' : '-';
|
|
227
|
+
const absMinutes = Math.abs(totalMinutes);
|
|
228
|
+
const hours = String(Math.floor(absMinutes / 60)).padStart(2, '0');
|
|
229
|
+
const minutes = String(absMinutes % 60).padStart(2, '0');
|
|
230
|
+
|
|
231
|
+
return `${iso}${sign}${hours}:${minutes}`;
|
|
232
|
+
}
|
package/src/utils/fs.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, rmdirSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 确保目录存在,不存在则递归创建
|
|
@@ -26,3 +27,33 @@ export function removeEmptyDir(dirPath: string): boolean {
|
|
|
26
27
|
}
|
|
27
28
|
return false;
|
|
28
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 递归计算目录占用的磁盘大小(字节)
|
|
33
|
+
* @param {string} dirPath - 目录路径
|
|
34
|
+
* @returns {number} 目录总大小(字节)
|
|
35
|
+
*/
|
|
36
|
+
export function calculateDirSize(dirPath: string): number {
|
|
37
|
+
let totalSize = 0;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
41
|
+
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const fullPath = join(dirPath, entry.name);
|
|
44
|
+
try {
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
totalSize += calculateDirSize(fullPath);
|
|
47
|
+
} else if (entry.isFile()) {
|
|
48
|
+
totalSize += statSync(fullPath).size;
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// 忽略无法访问的文件
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// 忽略无法读取的目录
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return totalSize;
|
|
59
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -51,8 +51,8 @@ export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } fro
|
|
|
51
51
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
|
|
52
52
|
export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus, createWorktreesByBranches } from './worktree.js';
|
|
53
53
|
export { loadConfig, writeDefaultConfig, writeConfig, saveConfig, getConfigValue, ensureClawtDirs, parseConcurrency } from './config.js';
|
|
54
|
-
export { printSuccess, printError, printWarning, printInfo, printHint, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration, formatRelativeTime } from './formatter.js';
|
|
55
|
-
export { ensureDir, removeEmptyDir } from './fs.js';
|
|
54
|
+
export { printSuccess, printError, printWarning, printInfo, printHint, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration, formatRelativeTime, formatDiskSize, formatLocalISOString } from './formatter.js';
|
|
55
|
+
export { ensureDir, removeEmptyDir, calculateDirSize } from './fs.js';
|
|
56
56
|
export { multilineInput } from './prompt.js';
|
|
57
57
|
export { launchInteractiveClaude, hasClaudeSessionHistory, launchInteractiveClaudeInNewTerminal } from './claude.js';
|
|
58
58
|
export { getSnapshotPath, hasSnapshot, getSnapshotModifiedTime, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
|
|
@@ -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
|
|