clawt 3.9.13 → 3.10.1
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 +80 -1
- package/README.zh-CN.md +81 -0
- package/dist/index.js +1935 -592
- package/dist/postinstall.js +1626 -283
- package/docs/config-file.md +2 -0
- package/docs/config.md +2 -1
- package/docs/init.md +3 -2
- package/docs/project-config.md +10 -1
- package/docs/spec.md +69 -2
- package/package.json +1 -1
- package/src/commands/alias.ts +5 -4
- package/src/commands/completion.ts +2 -1
- package/src/commands/config.ts +25 -7
- package/src/commands/cover-validate.ts +3 -2
- package/src/commands/create.ts +8 -7
- package/src/commands/home.ts +2 -1
- package/src/commands/init.ts +13 -6
- package/src/commands/list.ts +6 -4
- package/src/commands/merge.ts +8 -7
- package/src/commands/projects.ts +5 -3
- package/src/commands/remove.ts +7 -6
- package/src/commands/reset.ts +3 -2
- package/src/commands/resume.ts +10 -7
- package/src/commands/run.ts +8 -7
- package/src/commands/status.ts +16 -11
- package/src/commands/sync.ts +4 -3
- package/src/commands/tasks.ts +8 -6
- package/src/commands/validate.ts +7 -6
- package/src/constants/ai-prompts.ts +11 -11
- package/src/constants/config.ts +30 -0
- package/src/constants/index.ts +3 -2
- package/src/constants/messages/alias.ts +44 -14
- package/src/constants/messages/cli-descriptions.ts +91 -0
- package/src/constants/messages/common.ts +221 -36
- package/src/constants/messages/completion.ts +43 -14
- package/src/constants/messages/config.ts +61 -18
- package/src/constants/messages/cover-validate.ts +43 -14
- package/src/constants/messages/create.ts +16 -5
- package/src/constants/messages/home.ts +19 -6
- package/src/constants/messages/index.ts +2 -0
- package/src/constants/messages/init.ts +45 -14
- package/src/constants/messages/interactive-panel.ts +183 -29
- package/src/constants/messages/merge.ts +140 -38
- package/src/constants/messages/post-create.ts +59 -19
- package/src/constants/messages/projects.ts +51 -14
- package/src/constants/messages/remove.ts +50 -15
- package/src/constants/messages/reset.ts +14 -4
- package/src/constants/messages/resume.ts +116 -19
- package/src/constants/messages/run.ts +165 -35
- package/src/constants/messages/status.ts +84 -23
- package/src/constants/messages/sync.ts +54 -17
- package/src/constants/messages/tasks.ts +21 -7
- package/src/constants/messages/update.ts +35 -11
- package/src/constants/messages/validate.ts +218 -57
- package/src/constants/progress.ts +17 -6
- package/src/constants/project-config.ts +17 -0
- package/src/constants/prompt.ts +18 -2
- package/src/constants/tasks-template.ts +56 -2
- package/src/hooks/post-create.ts +5 -2
- package/src/index.ts +6 -5
- package/src/types/config.ts +2 -0
- package/src/utils/alias.ts +2 -1
- package/src/utils/claude.ts +10 -9
- package/src/utils/config-strategy.ts +3 -3
- package/src/utils/dry-run.ts +2 -2
- package/src/utils/formatter.ts +18 -11
- package/src/utils/i18n.ts +63 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/interactive-panel-render.ts +6 -3
- package/src/utils/progress-render.ts +11 -9
- package/src/utils/prompt.ts +2 -1
- package/src/utils/task-executor.ts +10 -7
- package/src/utils/task-file.ts +2 -1
- package/src/utils/terminal.ts +9 -9
- package/src/utils/ui-prompts.ts +4 -3
- package/src/utils/update-checker.ts +1 -1
- package/src/utils/validate-branch.ts +16 -9
- package/src/utils/validate-core.ts +2 -1
- package/src/utils/validate-runner.ts +2 -2
- package/src/utils/worktree-matcher.ts +9 -7
- package/tests/unit/commands/alias.test.ts +4 -0
- package/tests/unit/commands/completion.test.ts +14 -0
- package/tests/unit/commands/config.test.ts +61 -28
- package/tests/unit/commands/cover-validate.test.ts +13 -2
- package/tests/unit/commands/init.test.ts +6 -2
- package/tests/unit/commands/merge.test.ts +14 -0
- package/tests/unit/commands/run.test.ts +17 -0
- package/tests/unit/commands/tasks.test.ts +39 -9
- package/tests/unit/constants/config.test.ts +16 -1
- package/tests/unit/constants/messages-post-create.test.ts +93 -1
- package/tests/unit/constants/messages.test.ts +85 -1
- package/tests/unit/hooks/post-create.test.ts +32 -0
- package/tests/unit/utils/alias.test.ts +14 -0
- package/tests/unit/utils/claude.test.ts +24 -4
- package/tests/unit/utils/config-strategy.test.ts +21 -0
- package/tests/unit/utils/conflict-resolver.test.ts +24 -4
- package/tests/unit/utils/formatter.test.ts +21 -0
- package/tests/unit/utils/i18n.test.ts +91 -0
- package/tests/unit/utils/progress.test.ts +39 -18
- package/tests/unit/utils/prompt.test.ts +25 -2
- package/tests/unit/utils/task-file.test.ts +73 -10
- package/tests/unit/utils/terminal-cmux.test.ts +19 -4
- package/tests/unit/utils/update-checker.test.ts +2 -0
- package/tests/unit/utils/validate-branch.test.ts +26 -1
- package/tests/unit/utils/validation.test.ts +2 -2
- package/tests/unit/utils/worktree-matcher.test.ts +2 -0
|
@@ -5,6 +5,7 @@ import { checkBranchExists, createBranch, deleteBranch, getCurrentBranch, gitChe
|
|
|
5
5
|
import { getMainWorkBranch } from './project-config.js';
|
|
6
6
|
import { printWarning, confirmAction } from './formatter.js';
|
|
7
7
|
import { ClawtError } from '../errors/index.js';
|
|
8
|
+
import { getCurrentLanguage } from './i18n.js';
|
|
8
9
|
import { isNonInteractive } from './interactive.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -94,35 +95,41 @@ export async function handleDirtyWorkingDir(cwd?: string): Promise<void> {
|
|
|
94
95
|
gitAddAll(cwd);
|
|
95
96
|
gitStashPush('clawt:auto-stash', cwd);
|
|
96
97
|
if (!isWorkingDirClean(cwd)) {
|
|
97
|
-
throw new ClawtError(
|
|
98
|
+
throw new ClawtError(MESSAGES.WORKSPACE_STILL_DIRTY);
|
|
98
99
|
}
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
printWarning(
|
|
103
|
+
printWarning(MESSAGES.UNCOMMITTED_CHANGES_ON_BRANCH);
|
|
103
104
|
|
|
104
105
|
// @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
|
|
105
106
|
const choice = await new Enquirer.Select({
|
|
106
|
-
message:
|
|
107
|
+
message: MESSAGES.SELECT_ACTION,
|
|
107
108
|
choices: [
|
|
108
109
|
{
|
|
109
110
|
name: 'reset',
|
|
110
|
-
message:
|
|
111
|
+
message: getCurrentLanguage() === 'en'
|
|
112
|
+
? 'reset - Discard all changes (git reset --hard HEAD && git clean -fd)'
|
|
113
|
+
: 'reset - 丢弃所有更改 (git reset --hard HEAD && git clean -fd)',
|
|
111
114
|
},
|
|
112
115
|
{
|
|
113
116
|
name: 'stash',
|
|
114
|
-
message:
|
|
117
|
+
message: getCurrentLanguage() === 'en'
|
|
118
|
+
? 'stash - Stash changes (git add . && git stash)'
|
|
119
|
+
: 'stash - 暂存更改 (git add . && git stash)',
|
|
115
120
|
},
|
|
116
121
|
{
|
|
117
122
|
name: 'exit',
|
|
118
|
-
message:
|
|
123
|
+
message: getCurrentLanguage() === 'en'
|
|
124
|
+
? 'exit - Exit, handle manually'
|
|
125
|
+
: 'exit - 退出,手动处理',
|
|
119
126
|
},
|
|
120
127
|
],
|
|
121
128
|
initial: 0,
|
|
122
129
|
}).run();
|
|
123
130
|
|
|
124
131
|
if (choice === 'exit') {
|
|
125
|
-
throw new ClawtError(
|
|
132
|
+
throw new ClawtError(MESSAGES.USER_CHOSE_EXIT);
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
if (choice === 'reset') {
|
|
@@ -135,7 +142,7 @@ export async function handleDirtyWorkingDir(cwd?: string): Promise<void> {
|
|
|
135
142
|
|
|
136
143
|
// 再次检查是否干净
|
|
137
144
|
if (!isWorkingDirClean(cwd)) {
|
|
138
|
-
throw new ClawtError(
|
|
145
|
+
throw new ClawtError(MESSAGES.WORKSPACE_STILL_DIRTY);
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
148
|
|
|
@@ -171,7 +178,7 @@ export async function ensureOnMainWorkBranch(cwd?: string): Promise<void> {
|
|
|
171
178
|
// 当前在其他分支上,警告并确认后处理脏工作区再切换
|
|
172
179
|
printWarning(MESSAGES.GUARD_BRANCH_MISMATCH(mainBranch, currentBranch));
|
|
173
180
|
// 非交互模式下自动确认继续
|
|
174
|
-
const confirmed = isNonInteractive() ? true : await confirmAction(
|
|
181
|
+
const confirmed = isNonInteractive() ? true : await confirmAction(MESSAGES.CONFIRM_CONTINUE_VALIDATE);
|
|
175
182
|
if (!confirmed) {
|
|
176
183
|
throw new ClawtError(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
|
|
177
184
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { logger } from '../logger/index.js';
|
|
2
2
|
import { ClawtError } from '../errors/index.js';
|
|
3
3
|
import { MESSAGES } from '../constants/index.js';
|
|
4
|
+
import { getCurrentLanguage } from './i18n.js';
|
|
4
5
|
import {
|
|
5
6
|
gitAddAll,
|
|
6
7
|
gitCommit,
|
|
@@ -137,7 +138,7 @@ export function loadOldSnapshotToStage(oldTreeHash: string, oldHeadCommitHash: s
|
|
|
137
138
|
return { success: true, stagedTreeHash };
|
|
138
139
|
} else if (oldChangePatch.length > 0) {
|
|
139
140
|
// 有冲突:降级为全量模式(暂存区保持为空)
|
|
140
|
-
logger.warn('旧变更 patch 与当前 HEAD 冲突,降级为全量模式');
|
|
141
|
+
logger.warn(getCurrentLanguage() === 'en' ? 'Old changes patch conflicts with current HEAD, falling back to full mode' : '旧变更 patch 与当前 HEAD 冲突,降级为全量模式');
|
|
141
142
|
return { success: false, stagedTreeHash: '' };
|
|
142
143
|
}
|
|
143
144
|
// oldChangePatch 为空表示旧变更为空,暂存区保持干净即可
|
|
@@ -36,7 +36,7 @@ function buildSingleErrorClipboard(command: string, stderr: string, exitCode: nu
|
|
|
36
36
|
if (stderr.trim()) {
|
|
37
37
|
return MESSAGES.VALIDATE_CLIPBOARD_SINGLE_ERROR(command, stderr.trim());
|
|
38
38
|
}
|
|
39
|
-
return
|
|
39
|
+
return MESSAGES.COMMAND_EXEC_ERROR(command, exitCode);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -94,7 +94,7 @@ function reportParallelResults(results: ParallelCommandResultWithStderr[]): void
|
|
|
94
94
|
// 收集错误信息用于剪贴板
|
|
95
95
|
const errorContent = result.stderr.trim()
|
|
96
96
|
? result.stderr.trim()
|
|
97
|
-
:
|
|
97
|
+
: MESSAGES.EXIT_CODE_LABEL(result.exitCode);
|
|
98
98
|
errorClipboardParts.push(
|
|
99
99
|
MESSAGES.VALIDATE_CLIPBOARD_PARALLEL_ERROR(result.command, errorContent),
|
|
100
100
|
);
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import type { WorktreeInfo } from '../types/index.js';
|
|
13
13
|
import { promptSelectBranch, promptGroupedMultiSelectBranches } from './ui-prompts.js';
|
|
14
14
|
import type { GroupedChoice } from './ui-prompts.js';
|
|
15
|
+
import { getCurrentLanguage } from './i18n.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* 分支解析时使用的消息文案配置
|
|
@@ -213,26 +214,27 @@ export function getWorktreeCreatedTime(dirPath: string): string | null {
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
/**
|
|
216
|
-
* 将 YYYY-MM-DD
|
|
217
|
+
* 将 YYYY-MM-DD 日期字符串格式化为相对日期描述
|
|
217
218
|
* 基于自然日计算,适用于日期分组场景
|
|
218
219
|
* @param {string} dateStr - YYYY-MM-DD 格式的日期字符串
|
|
219
|
-
* @returns {string}
|
|
220
|
+
* @returns {string} 相对日期描述,如"今天"/"Today"、"昨天"/"Yesterday"、"3 天前"/"3 days ago"
|
|
220
221
|
*/
|
|
221
222
|
export function formatRelativeDate(dateStr: string): string {
|
|
223
|
+
const lang = getCurrentLanguage();
|
|
222
224
|
const today = formatLocalDate(new Date());
|
|
223
225
|
const todayMs = new Date(today).getTime();
|
|
224
226
|
const targetMs = new Date(dateStr).getTime();
|
|
225
227
|
const diffDays = Math.round((todayMs - targetMs) / (1000 * 60 * 60 * 24));
|
|
226
228
|
|
|
227
|
-
if (diffDays === 0) return '今天';
|
|
228
|
-
if (diffDays === 1) return '昨天';
|
|
229
|
-
if (diffDays < 30) return `${diffDays} 天前`;
|
|
229
|
+
if (diffDays === 0) return lang === 'en' ? 'Today' : '今天';
|
|
230
|
+
if (diffDays === 1) return lang === 'en' ? 'Yesterday' : '昨天';
|
|
231
|
+
if (diffDays < 30) return lang === 'en' ? `${diffDays} day${diffDays > 1 ? 's' : ''} ago` : `${diffDays} 天前`;
|
|
230
232
|
if (diffDays < 365) {
|
|
231
233
|
const months = Math.floor(diffDays / 30);
|
|
232
|
-
return `${months} 个月前`;
|
|
234
|
+
return lang === 'en' ? `${months} month${months > 1 ? 's' : ''} ago` : `${months} 个月前`;
|
|
233
235
|
}
|
|
234
236
|
const years = Math.floor(diffDays / 365);
|
|
235
|
-
return `${years} 年前`;
|
|
237
|
+
return lang === 'en' ? `${years} year${years > 1 ? 's' : ''} ago` : `${years} 年前`;
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
/**
|
|
@@ -43,14 +43,18 @@ beforeEach(() => {
|
|
|
43
43
|
/** 构造默认配置 mock 数据 */
|
|
44
44
|
function mockDefaultConfig(aliases: Record<string, string> = {}) {
|
|
45
45
|
return {
|
|
46
|
+
language: 'en' as const,
|
|
46
47
|
autoDeleteBranch: false,
|
|
47
48
|
claudeCodeCommand: 'claude',
|
|
48
49
|
autoPullPush: false,
|
|
49
50
|
confirmDestructiveOps: true,
|
|
50
51
|
maxConcurrency: 0,
|
|
51
52
|
terminalApp: 'auto',
|
|
53
|
+
resumeInPlace: false,
|
|
52
54
|
aliases,
|
|
53
55
|
autoUpdate: true,
|
|
56
|
+
conflictResolveMode: 'ask' as const,
|
|
57
|
+
conflictResolveTimeoutMs: 900000,
|
|
54
58
|
};
|
|
55
59
|
}
|
|
56
60
|
|
|
@@ -28,6 +28,20 @@ vi.mock('../../../src/logger/index.js', () => ({
|
|
|
28
28
|
}));
|
|
29
29
|
vi.mock('../../../src/utils/fs.js', () => ({}));
|
|
30
30
|
|
|
31
|
+
// mock i18n 模块,避免循环依赖导致 currentLanguage 未初始化
|
|
32
|
+
vi.mock('../../../src/utils/i18n.js', () => ({
|
|
33
|
+
getCurrentLanguage: vi.fn().mockReturnValue('en'),
|
|
34
|
+
resetLanguageCache: vi.fn(),
|
|
35
|
+
setCurrentLanguage: vi.fn(),
|
|
36
|
+
createMessages: vi.fn((i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
|
|
37
|
+
const result: any = {};
|
|
38
|
+
for (const key of Object.keys(i18nMap)) {
|
|
39
|
+
result[key] = i18nMap[key]['en'];
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
|
|
31
45
|
/**
|
|
32
46
|
* 创建 statSync 的 mock 返回值
|
|
33
47
|
* @param {boolean} isDir - 是否为目录
|
|
@@ -45,52 +45,81 @@ vi.mock('../../../src/utils/index.js', async (importOriginal) => {
|
|
|
45
45
|
vi.mock('../../../src/constants/index.js', () => ({
|
|
46
46
|
CONFIG_PATH: '/mock/.clawt/config.json',
|
|
47
47
|
DEFAULT_CONFIG: {
|
|
48
|
+
language: 'en',
|
|
48
49
|
autoDeleteBranch: false,
|
|
49
50
|
claudeCodeCommand: 'claude',
|
|
50
51
|
autoPullPush: false,
|
|
51
52
|
confirmDestructiveOps: true,
|
|
52
53
|
maxConcurrency: 0,
|
|
53
54
|
terminalApp: 'auto',
|
|
55
|
+
resumeInPlace: false,
|
|
54
56
|
aliases: {},
|
|
55
57
|
autoUpdate: true,
|
|
58
|
+
conflictResolveMode: 'ask',
|
|
59
|
+
conflictResolveTimeoutMs: 900000,
|
|
56
60
|
},
|
|
57
61
|
CONFIG_DESCRIPTIONS: {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
language: 'Interface language',
|
|
63
|
+
autoDeleteBranch: 'Whether to auto-delete the local branch when removing a worktree',
|
|
64
|
+
claudeCodeCommand: 'Claude Code CLI launch command',
|
|
65
|
+
autoPullPush: 'Whether to auto-run git pull and git push after merge',
|
|
66
|
+
confirmDestructiveOps: 'Whether to prompt for confirmation before destructive operations',
|
|
67
|
+
maxConcurrency: 'Default max concurrency for run command, 0 means unlimited',
|
|
68
|
+
terminalApp: 'Terminal app for batch resume',
|
|
69
|
+
resumeInPlace: 'Whether to resume in current terminal',
|
|
70
|
+
aliases: 'Command alias mapping',
|
|
71
|
+
autoUpdate: 'Whether to enable auto-update checks',
|
|
72
|
+
conflictResolveMode: 'Merge conflict resolution mode',
|
|
73
|
+
conflictResolveTimeoutMs: 'Claude Code conflict resolution timeout',
|
|
66
74
|
},
|
|
67
75
|
CONFIG_DEFINITIONS: {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
language: { defaultValue: 'en', description: 'Interface language', allowedValues: ['en', 'zh-CN'] },
|
|
77
|
+
autoDeleteBranch: { defaultValue: false, description: 'Whether to auto-delete the local branch when removing a worktree' },
|
|
78
|
+
claudeCodeCommand: { defaultValue: 'claude', description: 'Claude Code CLI launch command' },
|
|
79
|
+
autoPullPush: { defaultValue: false, description: 'Whether to auto-run git pull and git push after merge' },
|
|
80
|
+
confirmDestructiveOps: { defaultValue: true, description: 'Whether to prompt for confirmation before destructive operations' },
|
|
81
|
+
maxConcurrency: { defaultValue: 0, description: 'Default max concurrency for run command, 0 means unlimited' },
|
|
82
|
+
terminalApp: { defaultValue: 'auto', description: 'Terminal app for batch resume', allowedValues: ['auto', 'iterm2', 'terminal'] },
|
|
83
|
+
resumeInPlace: { defaultValue: false, description: 'Whether to resume in current terminal' },
|
|
84
|
+
aliases: { defaultValue: {}, description: 'Command alias mapping' },
|
|
85
|
+
autoUpdate: { defaultValue: true, description: 'Whether to enable auto-update checks' },
|
|
86
|
+
conflictResolveMode: { defaultValue: 'ask', description: 'Merge conflict resolution mode', allowedValues: ['ask', 'auto', 'manual'] },
|
|
87
|
+
conflictResolveTimeoutMs: { defaultValue: 900000, description: 'Claude Code conflict resolution timeout' },
|
|
76
88
|
},
|
|
77
|
-
CONFIG_ALIAS_DISABLED_HINT: '(
|
|
89
|
+
CONFIG_ALIAS_DISABLED_HINT: '(Manage via clawt alias command)',
|
|
90
|
+
getI18nConfigDescriptions: () => ({
|
|
91
|
+
autoDeleteBranch: 'Whether to auto-delete the local branch when removing a worktree',
|
|
92
|
+
claudeCodeCommand: 'Claude Code CLI launch command',
|
|
93
|
+
autoPullPush: 'Whether to auto-run git pull and git push after merge',
|
|
94
|
+
confirmDestructiveOps: 'Whether to prompt for confirmation before destructive operations',
|
|
95
|
+
maxConcurrency: 'Default max concurrency for run command, 0 means unlimited',
|
|
96
|
+
terminalApp: 'Terminal app for batch resume',
|
|
97
|
+
aliases: 'Command alias mapping',
|
|
98
|
+
autoUpdate: 'Whether to enable auto-update checks',
|
|
99
|
+
resumeInPlace: 'Whether to resume in current terminal',
|
|
100
|
+
conflictResolveMode: 'Merge conflict resolution mode',
|
|
101
|
+
conflictResolveTimeoutMs: 'Claude Code conflict resolution timeout',
|
|
102
|
+
language: 'Interface language',
|
|
103
|
+
}),
|
|
78
104
|
MESSAGES: {
|
|
79
|
-
CONFIG_RESET_SUCCESS: '
|
|
80
|
-
DESTRUCTIVE_OP_CANCELLED: '
|
|
81
|
-
CONFIG_SET_SUCCESS: (key: string, value: string) => `✓ ${key}
|
|
105
|
+
CONFIG_RESET_SUCCESS: 'Configuration reset to defaults',
|
|
106
|
+
DESTRUCTIVE_OP_CANCELLED: 'Operation cancelled',
|
|
107
|
+
CONFIG_SET_SUCCESS: (key: string, value: string) => `✓ ${key} set to ${value}`,
|
|
82
108
|
CONFIG_GET_VALUE: (key: string, value: string) => `${key} = ${value}`,
|
|
83
109
|
CONFIG_INVALID_KEY: (key: string, validKeys: string[]) =>
|
|
84
|
-
|
|
110
|
+
`Invalid config key: ${key}\nAvailable keys: ${validKeys.join(', ')}`,
|
|
85
111
|
CONFIG_INVALID_BOOLEAN: (key: string) =>
|
|
86
|
-
|
|
112
|
+
`Config key ${key} is boolean, only true or false accepted`,
|
|
87
113
|
CONFIG_INVALID_NUMBER: (key: string) =>
|
|
88
|
-
|
|
114
|
+
`Config key ${key} is number, please enter a valid number`,
|
|
89
115
|
CONFIG_INVALID_ENUM: (key: string, validValues: readonly string[]) =>
|
|
90
|
-
|
|
91
|
-
CONFIG_SELECT_PROMPT: '
|
|
92
|
-
CONFIG_INPUT_PROMPT: (key: string) =>
|
|
93
|
-
CONFIG_MISSING_VALUE: (key: string) =>
|
|
116
|
+
`Config key ${key} only accepts: ${validValues.join(', ')}`,
|
|
117
|
+
CONFIG_SELECT_PROMPT: 'Select a config key to modify',
|
|
118
|
+
CONFIG_INPUT_PROMPT: (key: string) => `Enter new value for ${key}`,
|
|
119
|
+
CONFIG_MISSING_VALUE: (key: string) => `Missing value, usage: clawt config set ${key} <value>`,
|
|
120
|
+
CONFIG_RESET_WARNING: 'This will reset all configuration to defaults',
|
|
121
|
+
NON_INTERACTIVE_CONFIG_EDITOR: 'Cannot edit config in non-interactive mode',
|
|
122
|
+
NOT_SET: '(not set)',
|
|
94
123
|
},
|
|
95
124
|
}));
|
|
96
125
|
|
|
@@ -108,14 +137,18 @@ const mockedConfirmDestructiveAction = vi.mocked(confirmDestructiveAction);
|
|
|
108
137
|
/** 创建默认配置对象用于 mock */
|
|
109
138
|
function createMockConfig() {
|
|
110
139
|
return {
|
|
140
|
+
language: 'en' as const,
|
|
111
141
|
autoDeleteBranch: false,
|
|
112
142
|
claudeCodeCommand: 'claude',
|
|
113
143
|
autoPullPush: false,
|
|
114
144
|
confirmDestructiveOps: true,
|
|
115
145
|
maxConcurrency: 0,
|
|
116
146
|
terminalApp: 'auto',
|
|
147
|
+
resumeInPlace: false,
|
|
117
148
|
aliases: {},
|
|
118
149
|
autoUpdate: true,
|
|
150
|
+
conflictResolveMode: 'ask' as const,
|
|
151
|
+
conflictResolveTimeoutMs: 900000,
|
|
119
152
|
};
|
|
120
153
|
}
|
|
121
154
|
|
|
@@ -403,7 +436,7 @@ describe('handleConfigSet — 交互模式', () => {
|
|
|
403
436
|
const selectOpts = mockSelectConstructorArgs[0] as { choices: Array<{ name: string; disabled?: string }> };
|
|
404
437
|
const aliasesChoice = selectOpts.choices.find((c) => c.name === 'aliases');
|
|
405
438
|
expect(aliasesChoice).toBeDefined();
|
|
406
|
-
expect(aliasesChoice!.disabled).toBe('(
|
|
439
|
+
expect(aliasesChoice!.disabled).toBe('(Manage via clawt alias command)');
|
|
407
440
|
|
|
408
441
|
// 普通配置项不应有 disabled 属性
|
|
409
442
|
const normalChoice = selectOpts.choices.find((c) => c.name === 'autoDeleteBranch');
|
|
@@ -5,6 +5,17 @@ vi.mock('../../../src/logger/index.js', () => ({
|
|
|
5
5
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
6
|
}));
|
|
7
7
|
|
|
8
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'zh-CN' 以匹配中文断言
|
|
9
|
+
// 同时导出 createMessages 供 constants 模块使用
|
|
10
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
15
|
+
resetLanguageCache: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
8
19
|
vi.mock('../../../src/errors/index.js', () => ({
|
|
9
20
|
ClawtError: class ClawtError extends Error {
|
|
10
21
|
exitCode: number;
|
|
@@ -37,7 +48,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
37
48
|
getProjectWorktrees: vi.fn().mockReturnValue([{ path: '/path/feature', branch: 'feature' }]),
|
|
38
49
|
findExactMatch: vi.fn().mockReturnValue({ path: '/path/feature', branch: 'feature' }),
|
|
39
50
|
hasSnapshot: vi.fn().mockReturnValue(true),
|
|
40
|
-
readSnapshot: vi.fn().mockReturnValue({ treeHash: 'snapshot-tree-hash' }),
|
|
51
|
+
readSnapshot: vi.fn().mockReturnValue({ treeHash: 'snapshot-tree-hash', headCommitHash: '', stagedTreeHash: '' }),
|
|
41
52
|
writeSnapshot: vi.fn(),
|
|
42
53
|
gitAddAll: vi.fn(),
|
|
43
54
|
gitWriteTree: vi.fn().mockReturnValue('current-tree-hash'),
|
|
@@ -95,7 +106,7 @@ beforeEach(() => {
|
|
|
95
106
|
mockedGetProjectWorktrees.mockReturnValue([{ path: '/path/feature', branch: 'feature' }]);
|
|
96
107
|
mockedFindExactMatch.mockReturnValue({ path: '/path/feature', branch: 'feature' });
|
|
97
108
|
mockedHasSnapshot.mockReturnValue(true);
|
|
98
|
-
mockedReadSnapshot.mockReturnValue({ treeHash: 'snapshot-tree-hash' });
|
|
109
|
+
mockedReadSnapshot.mockReturnValue({ treeHash: 'snapshot-tree-hash', headCommitHash: '', stagedTreeHash: '' });
|
|
99
110
|
mockedIsWorkingDirClean.mockReturnValue(false);
|
|
100
111
|
mockedConfirmAction.mockResolvedValue(true);
|
|
101
112
|
mockedGitWriteTree.mockReturnValue('current-tree-hash');
|
|
@@ -16,9 +16,13 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
16
16
|
INIT_SET_SUCCESS: (key: string, value: string) => `✓ 项目配置 ${key} 已设置为 ${value}`,
|
|
17
17
|
},
|
|
18
18
|
PROJECT_CONFIG_DEFINITIONS: {
|
|
19
|
-
clawtMainWorkBranch: { defaultValue: '', description: '
|
|
20
|
-
validateRunCommand: { defaultValue: undefined, description: 'validate
|
|
19
|
+
clawtMainWorkBranch: { defaultValue: '', description: 'Main worktree branch name' },
|
|
20
|
+
validateRunCommand: { defaultValue: undefined, description: 'Command to auto-run after validate succeeds' },
|
|
21
21
|
},
|
|
22
|
+
getI18nProjectConfigDescriptions: () => ({
|
|
23
|
+
clawtMainWorkBranch: 'Main worktree branch name',
|
|
24
|
+
validateRunCommand: 'Command to auto-run after validate succeeds',
|
|
25
|
+
}),
|
|
22
26
|
}));
|
|
23
27
|
|
|
24
28
|
vi.mock('../../../src/utils/index.js', () => ({
|
|
@@ -5,6 +5,17 @@ vi.mock('../../../src/logger/index.js', () => ({
|
|
|
5
5
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
6
|
}));
|
|
7
7
|
|
|
8
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'zh-CN' 以匹配中文断言
|
|
9
|
+
// 同时导出 createMessages 供 constants 模块使用
|
|
10
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
15
|
+
resetLanguageCache: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
8
19
|
vi.mock('../../../src/errors/index.js', () => ({
|
|
9
20
|
ClawtError: class ClawtError extends Error {
|
|
10
21
|
exitCode: number;
|
|
@@ -40,6 +51,9 @@ vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
|
40
51
|
MERGE_SUCCESS: (branch: string, message: string, autoPullPush: boolean) => `合并成功: ${branch}`,
|
|
41
52
|
MERGE_SUCCESS_NO_MESSAGE: (branch: string, autoPullPush: boolean) => `合并成功: ${branch}`,
|
|
42
53
|
WORKTREE_CLEANED: (branch: string) => `已清理: ${branch}`,
|
|
54
|
+
// i18n 新增的消息键(merge.ts shouldCleanupAfterMerge 中使用)
|
|
55
|
+
AUTO_DELETE_CONFIGURED: (branch: string) => `已配置自动删除,merge 成功后将自动清理 worktree 和分支: ${branch}`,
|
|
56
|
+
CONFIRM_DELETE_WORKTREE_BRANCH: (branch: string) => `是否删除对应的 worktree 和分支 (${branch})?`,
|
|
43
57
|
},
|
|
44
58
|
AUTO_SAVE_COMMIT_MESSAGE_PREFIX: 'clawt: auto-save before merging',
|
|
45
59
|
};
|
|
@@ -6,6 +6,17 @@ vi.mock('../../../src/logger/index.js', () => ({
|
|
|
6
6
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
7
7
|
}));
|
|
8
8
|
|
|
9
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'zh-CN' 以匹配中文断言
|
|
10
|
+
// 同时导出 createMessages 供 constants 模块使用
|
|
11
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
16
|
+
resetLanguageCache: vi.fn(),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
9
20
|
vi.mock('../../../src/errors/index.js', () => ({
|
|
10
21
|
ClawtError: class ClawtError extends Error {
|
|
11
22
|
exitCode: number;
|
|
@@ -42,6 +53,12 @@ vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
|
42
53
|
DRY_RUN_INTERACTIVE_MODE: '模式: 交互式(无预设任务)',
|
|
43
54
|
DRY_RUN_READY: '预览完成,无冲突。移除 --dry-run 即可正式执行。',
|
|
44
55
|
DRY_RUN_HAS_CONFLICT: '存在分支冲突,实际执行时将会报错。请先处理冲突的分支。',
|
|
56
|
+
// i18n 新增的消息键(task-executor.ts 中使用)
|
|
57
|
+
ALL_TASKS_COMPLETED: (total: number) => `全部任务已完成 (${total}/${total})`,
|
|
58
|
+
SUCCESS_LABEL: '成功:',
|
|
59
|
+
FAILURE_LABEL: '失败:',
|
|
60
|
+
TOTAL_DURATION_LABEL: '总耗时:',
|
|
61
|
+
TOTAL_COST_LABEL: '总花费:',
|
|
45
62
|
},
|
|
46
63
|
};
|
|
47
64
|
});
|
|
@@ -1,19 +1,49 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
|
|
4
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'en' 以匹配英文断言
|
|
5
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
getCurrentLanguage: vi.fn().mockReturnValue('en'),
|
|
10
|
+
resetLanguageCache: vi.fn(),
|
|
11
|
+
createMessages: <T extends Record<string, { en: any; 'zh-CN': any }>>(
|
|
12
|
+
i18nMap: T,
|
|
13
|
+
) => {
|
|
14
|
+
const result: any = {};
|
|
15
|
+
for (const key of Object.keys(i18nMap)) {
|
|
16
|
+
result[key] = i18nMap[key]['en'];
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
4
23
|
vi.mock('../../../src/logger/index.js', () => ({
|
|
5
24
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
25
|
}));
|
|
7
26
|
|
|
8
|
-
vi.mock('../../../src/constants/index.js', () =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
28
|
+
const actual = await importOriginal<typeof import('../../../src/constants/index.js')>();
|
|
29
|
+
return {
|
|
30
|
+
...actual,
|
|
31
|
+
MESSAGES: {
|
|
32
|
+
...actual.MESSAGES,
|
|
33
|
+
TASK_INIT_FILE_EXISTS: (path: string) => `File already exists: ${path}, delete it first to overwrite`,
|
|
34
|
+
TASK_INIT_SUCCESS: (path: string) => `✓ Task template generated: ${path}`,
|
|
35
|
+
TASK_INIT_HINT: (path: string) =>
|
|
36
|
+
`Run task:\n clawt run -f ${path} # Create worktree and execute\n clawt resume -f ${path} # Resume in existing worktree`,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// mock tasks-template 模块,使 getTaskTemplateContent 返回英文模板
|
|
42
|
+
vi.mock('../../../src/constants/tasks-template.js', () => ({
|
|
43
|
+
getTaskTemplateContent: vi.fn().mockReturnValue('# Template content'),
|
|
44
|
+
TASK_TEMPLATE_CONTENT: '# Template content',
|
|
14
45
|
TASK_TEMPLATE_OUTPUT_DIR: '.clawt/tasks',
|
|
15
46
|
TASK_TEMPLATE_FILENAME_PREFIX: 'clawt-tasks',
|
|
16
|
-
TASK_TEMPLATE_CONTENT: '# 模板内容',
|
|
17
47
|
}));
|
|
18
48
|
|
|
19
49
|
const mockExistsSync = vi.fn();
|
|
@@ -93,7 +123,7 @@ describe('handleTasksInit', () => {
|
|
|
93
123
|
// 默认路径应输出到 .clawt/tasks/ 目录下
|
|
94
124
|
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
|
95
125
|
expect.stringContaining('.clawt/tasks/clawt-tasks-2026-01-01-00-00-00.md'),
|
|
96
|
-
'#
|
|
126
|
+
'# Template content',
|
|
97
127
|
'utf-8',
|
|
98
128
|
);
|
|
99
129
|
expect(mockedPrintSuccess).toHaveBeenCalledWith(
|
|
@@ -115,7 +145,7 @@ describe('handleTasksInit', () => {
|
|
|
115
145
|
expect(mockGenerateTaskFilename).not.toHaveBeenCalled();
|
|
116
146
|
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
|
117
147
|
expect.stringContaining('my-tasks.md'),
|
|
118
|
-
'#
|
|
148
|
+
'# Template content',
|
|
119
149
|
'utf-8',
|
|
120
150
|
);
|
|
121
151
|
expect(mockedPrintSuccess).toHaveBeenCalledWith(
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock i18n 模块,避免循环依赖导致 currentLanguage 未初始化
|
|
4
|
+
vi.mock('../../../src/utils/i18n.js', () => ({
|
|
5
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
6
|
+
resetLanguageCache: vi.fn(),
|
|
7
|
+
setCurrentLanguage: vi.fn(),
|
|
8
|
+
createMessages: vi.fn((i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
|
|
9
|
+
const result: any = {};
|
|
10
|
+
for (const key of Object.keys(i18nMap)) {
|
|
11
|
+
result[key] = i18nMap[key]['zh-CN'];
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
2
17
|
import { CONFIG_DEFINITIONS, DEFAULT_CONFIG, CONFIG_DESCRIPTIONS } from '../../../src/constants/config.js';
|
|
3
18
|
import { VALID_TERMINAL_APPS } from '../../../src/constants/terminal.js';
|
|
4
19
|
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'zh-CN' 以匹配中文断言
|
|
4
|
+
vi.mock('../../../src/utils/i18n.js', () => ({
|
|
5
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
6
|
+
resetLanguageCache: vi.fn(),
|
|
7
|
+
setCurrentLanguage: vi.fn(),
|
|
8
|
+
createMessages: vi.fn((i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
|
|
9
|
+
const result: any = {};
|
|
10
|
+
for (const key of Object.keys(i18nMap)) {
|
|
11
|
+
result[key] = i18nMap[key]['zh-CN'];
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
2
17
|
import { POST_CREATE_MESSAGES } from '../../../src/constants/messages/post-create.js';
|
|
3
18
|
|
|
4
19
|
describe('POST_CREATE_MESSAGES', () => {
|
|
@@ -110,3 +125,80 @@ describe('POST_CREATE_MESSAGES', () => {
|
|
|
110
125
|
});
|
|
111
126
|
});
|
|
112
127
|
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 英文版 post-create 消息测试
|
|
131
|
+
* 使用 vi.resetModules + vi.doMock 动态切换语言为 en,
|
|
132
|
+
* 然后重新加载 POST_CREATE_MESSAGES 模块验证英文版消息内容
|
|
133
|
+
*/
|
|
134
|
+
describe('POST_CREATE_MESSAGES — 英文版', () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
vi.resetModules();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('纯字符串消息在英文版下返回英文文本', async () => {
|
|
140
|
+
vi.doMock('../../../src/utils/i18n.js', () => ({
|
|
141
|
+
getCurrentLanguage: () => 'en',
|
|
142
|
+
resetLanguageCache: vi.fn(),
|
|
143
|
+
setCurrentLanguage: vi.fn(),
|
|
144
|
+
createMessages: (i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
|
|
145
|
+
const result: any = {};
|
|
146
|
+
for (const key of Object.keys(i18nMap)) {
|
|
147
|
+
result[key] = i18nMap[key]['en'];
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
},
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
const { POST_CREATE_MESSAGES: EN_MSGS } = await import('../../../src/constants/messages/post-create.js');
|
|
154
|
+
|
|
155
|
+
// HOOK_SKIPPED 英文版包含 Skipped
|
|
156
|
+
expect(EN_MSGS.HOOK_SKIPPED).toContain('Skipped');
|
|
157
|
+
|
|
158
|
+
// HOOK_NOT_CONFIGURED 英文版包含 "not configured" 和 "skipping"
|
|
159
|
+
expect(EN_MSGS.HOOK_NOT_CONFIGURED).toContain('not configured');
|
|
160
|
+
expect(EN_MSGS.HOOK_NOT_CONFIGURED).toContain('skipping');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('模板函数消息在英文版下返回英文文本', async () => {
|
|
164
|
+
vi.doMock('../../../src/utils/i18n.js', () => ({
|
|
165
|
+
getCurrentLanguage: () => 'en',
|
|
166
|
+
resetLanguageCache: vi.fn(),
|
|
167
|
+
setCurrentLanguage: vi.fn(),
|
|
168
|
+
createMessages: (i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
|
|
169
|
+
const result: any = {};
|
|
170
|
+
for (const key of Object.keys(i18nMap)) {
|
|
171
|
+
result[key] = i18nMap[key]['en'];
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
},
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
const { POST_CREATE_MESSAGES: EN_MSGS } = await import('../../../src/constants/messages/post-create.js');
|
|
178
|
+
|
|
179
|
+
// HOOK_SOURCE_INFO 英文版包含 "postCreate hook source"
|
|
180
|
+
expect(EN_MSGS.HOOK_SOURCE_INFO('project config')).toContain('postCreate hook source');
|
|
181
|
+
|
|
182
|
+
// HOOK_SUCCESS 英文版包含 "successfully" 而非 "成功"
|
|
183
|
+
expect(EN_MSGS.HOOK_SUCCESS('feat-login')).toContain('successfully');
|
|
184
|
+
expect(EN_MSGS.HOOK_SUCCESS('feat-login')).not.toContain('成功');
|
|
185
|
+
|
|
186
|
+
// HOOK_FAILED 英文版包含 "failed" 而非 "失败"
|
|
187
|
+
expect(EN_MSGS.HOOK_FAILED('feat-login', 'error')).toContain('failed');
|
|
188
|
+
expect(EN_MSGS.HOOK_FAILED('feat-login', 'error')).not.toContain('失败');
|
|
189
|
+
|
|
190
|
+
// HOOK_SUMMARY 英文版包含 "succeeded"/"failed" 而非 "成功"/"失败"
|
|
191
|
+
const summary = EN_MSGS.HOOK_SUMMARY(5, 0);
|
|
192
|
+
expect(summary).toContain('5 succeeded');
|
|
193
|
+
expect(summary).toContain('0 failed');
|
|
194
|
+
|
|
195
|
+
// POST_CREATE_SCRIPT_NOT_EXECUTABLE 英文版包含 "not executable"
|
|
196
|
+
expect(EN_MSGS.POST_CREATE_SCRIPT_NOT_EXECUTABLE('/repo/.clawt/postCreate.sh')).toContain('not executable');
|
|
197
|
+
|
|
198
|
+
// POST_CREATE_SCRIPT_AUTO_CHMOD 英文版包含 "auto-added execute permission"
|
|
199
|
+
expect(EN_MSGS.POST_CREATE_SCRIPT_AUTO_CHMOD('/repo/.clawt/postCreate.sh')).toContain('auto-added execute permission');
|
|
200
|
+
|
|
201
|
+
// HOOK_BACKGROUND_START 英文版包含 "running in background"
|
|
202
|
+
expect(EN_MSGS.HOOK_BACKGROUND_START(3, 'npm install')).toContain('running in background');
|
|
203
|
+
});
|
|
204
|
+
});
|