clawt 2.13.0 → 2.14.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/dist/index.js +201 -63
- package/dist/postinstall.js +25 -2
- package/package.json +1 -1
- package/src/commands/config.ts +115 -48
- package/src/constants/config.ts +2 -0
- package/src/constants/index.ts +1 -1
- package/src/constants/messages/config.ts +22 -0
- package/src/types/config.ts +2 -0
- package/src/utils/config-strategy.ts +196 -0
- package/src/utils/config.ts +8 -0
- package/src/utils/index.ts +2 -1
- package/tests/unit/commands/config.test.ts +324 -24
- package/tests/unit/constants/config.test.ts +24 -1
- package/tests/unit/utils/config-strategy.test.ts +217 -0
- package/tests/unit/utils/config.test.ts +13 -1
package/src/constants/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { INVALID_BRANCH_CHARS } from './branch.js';
|
|
|
3
3
|
export { MESSAGES } from './messages/index.js';
|
|
4
4
|
export { EXIT_CODES } from './exitCodes.js';
|
|
5
5
|
export { ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS, VALID_TERMINAL_APPS, ITERM2_APP_PATH } from './terminal.js';
|
|
6
|
-
export { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, APPEND_SYSTEM_PROMPT } from './config.js';
|
|
6
|
+
export { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, CONFIG_DEFINITIONS, APPEND_SYSTEM_PROMPT } from './config.js';
|
|
7
7
|
export { AUTO_SAVE_COMMIT_MESSAGE } from './git.js';
|
|
8
8
|
export { DEBUG_LOG_PREFIX, DEBUG_TIMESTAMP_FORMAT } from './logger.js';
|
|
9
9
|
export {
|
|
@@ -2,4 +2,26 @@
|
|
|
2
2
|
export const CONFIG_CMD_MESSAGES = {
|
|
3
3
|
/** 配置已恢复为默认值 */
|
|
4
4
|
CONFIG_RESET_SUCCESS: '✓ 配置已恢复为默认值',
|
|
5
|
+
/** 配置项设置成功 */
|
|
6
|
+
CONFIG_SET_SUCCESS: (key: string, value: string) => `✓ ${key} 已设置为 ${value}`,
|
|
7
|
+
/** 获取配置值显示 */
|
|
8
|
+
CONFIG_GET_VALUE: (key: string, value: string) => `${key} = ${value}`,
|
|
9
|
+
/** 无效配置项名称 */
|
|
10
|
+
CONFIG_INVALID_KEY: (key: string, validKeys: string[]) =>
|
|
11
|
+
`无效的配置项: ${key}\n可用的配置项: ${validKeys.join(', ')}`,
|
|
12
|
+
/** 布尔类型值无效 */
|
|
13
|
+
CONFIG_INVALID_BOOLEAN: (key: string) =>
|
|
14
|
+
`配置项 ${key} 为布尔类型,仅接受 true 或 false`,
|
|
15
|
+
/** 数字类型值无效 */
|
|
16
|
+
CONFIG_INVALID_NUMBER: (key: string) =>
|
|
17
|
+
`配置项 ${key} 为数字类型,请输入有效的数字`,
|
|
18
|
+
/** 枚举类型配置项值无效(通用版) */
|
|
19
|
+
CONFIG_INVALID_ENUM: (key: string, validValues: readonly string[]) =>
|
|
20
|
+
`配置项 ${key} 仅接受以下值: ${validValues.join(', ')}`,
|
|
21
|
+
/** 交互式选择配置项提示 */
|
|
22
|
+
CONFIG_SELECT_PROMPT: '选择要修改的配置项',
|
|
23
|
+
/** 交互式输入新值提示 */
|
|
24
|
+
CONFIG_INPUT_PROMPT: (key: string) => `输入 ${key} 的新值`,
|
|
25
|
+
/** 缺少 value 参数提示 */
|
|
26
|
+
CONFIG_MISSING_VALUE: (key: string) => `缺少配置值,用法: clawt config set ${key} <value>`,
|
|
5
27
|
} as const;
|
package/src/types/config.ts
CHANGED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Enquirer from 'enquirer';
|
|
3
|
+
import { DEFAULT_CONFIG, CONFIG_DEFINITIONS, MESSAGES } from '../constants/index.js';
|
|
4
|
+
import type { ClawtConfig } from '../types/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 校验 key 是否为有效的配置项名称
|
|
8
|
+
* @param {string} key - 待校验的配置项名称
|
|
9
|
+
* @returns {boolean} 是否有效
|
|
10
|
+
*/
|
|
11
|
+
export function isValidConfigKey(key: string): key is keyof ClawtConfig {
|
|
12
|
+
return key in DEFAULT_CONFIG;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 获取所有有效配置项名称列表
|
|
17
|
+
* @returns {string[]} 配置项名称数组
|
|
18
|
+
*/
|
|
19
|
+
export function getValidConfigKeys(): string[] {
|
|
20
|
+
return Object.keys(DEFAULT_CONFIG);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 将字符串值解析并校验为目标配置项的正确类型
|
|
25
|
+
*
|
|
26
|
+
* 策略分发规则:
|
|
27
|
+
* - boolean → 解析 'true'/'false'
|
|
28
|
+
* - number → Number() 解析
|
|
29
|
+
* - string + 有 allowedValues → 枚举校验
|
|
30
|
+
* - string + 无 allowedValues → 无额外校验
|
|
31
|
+
*
|
|
32
|
+
* @param {keyof ClawtConfig} key - 配置项名称
|
|
33
|
+
* @param {string} rawValue - 原始字符串值
|
|
34
|
+
* @returns {{ success: true; value: ClawtConfig[keyof ClawtConfig] } | { success: false; error: string }} 解析结果
|
|
35
|
+
*/
|
|
36
|
+
export function parseConfigValue(
|
|
37
|
+
key: keyof ClawtConfig,
|
|
38
|
+
rawValue: string,
|
|
39
|
+
): { success: true; value: ClawtConfig[keyof ClawtConfig] } | { success: false; error: string } {
|
|
40
|
+
const expectedType = typeof DEFAULT_CONFIG[key];
|
|
41
|
+
|
|
42
|
+
// 布尔类型策略
|
|
43
|
+
if (expectedType === 'boolean') {
|
|
44
|
+
if (rawValue === 'true') return { success: true, value: true };
|
|
45
|
+
if (rawValue === 'false') return { success: true, value: false };
|
|
46
|
+
return { success: false, error: MESSAGES.CONFIG_INVALID_BOOLEAN(key) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 数字类型策略
|
|
50
|
+
if (expectedType === 'number') {
|
|
51
|
+
const num = Number(rawValue);
|
|
52
|
+
if (Number.isNaN(num)) {
|
|
53
|
+
return { success: false, error: MESSAGES.CONFIG_INVALID_NUMBER(key) };
|
|
54
|
+
}
|
|
55
|
+
return { success: true, value: num };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 字符串类型:根据 allowedValues 自动选择校验策略
|
|
59
|
+
const definition = CONFIG_DEFINITIONS[key];
|
|
60
|
+
if (definition.allowedValues && !definition.allowedValues.includes(rawValue)) {
|
|
61
|
+
return { success: false, error: MESSAGES.CONFIG_INVALID_ENUM(key, definition.allowedValues) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { success: true, value: rawValue };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 交互式提示用户输入配置值
|
|
69
|
+
*
|
|
70
|
+
* 策略分发规则:
|
|
71
|
+
* - boolean → Select(true, false)
|
|
72
|
+
* - number → Input(带数字校验)
|
|
73
|
+
* - string + 有 allowedValues → Select(枚举列表)
|
|
74
|
+
* - string + 无 allowedValues → Input(自由输入)
|
|
75
|
+
*
|
|
76
|
+
* @param {keyof ClawtConfig} key - 配置项名称
|
|
77
|
+
* @param {ClawtConfig[keyof ClawtConfig]} currentValue - 当前值
|
|
78
|
+
* @returns {Promise<ClawtConfig[keyof ClawtConfig]>} 用户输入/选择的新值
|
|
79
|
+
*/
|
|
80
|
+
export async function promptConfigValue(
|
|
81
|
+
key: keyof ClawtConfig,
|
|
82
|
+
currentValue: ClawtConfig[keyof ClawtConfig],
|
|
83
|
+
): Promise<ClawtConfig[keyof ClawtConfig]> {
|
|
84
|
+
const expectedType = typeof currentValue;
|
|
85
|
+
|
|
86
|
+
// 布尔类型策略
|
|
87
|
+
if (expectedType === 'boolean') {
|
|
88
|
+
return promptBooleanValue(key, currentValue as boolean);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 数字类型策略
|
|
92
|
+
if (expectedType === 'number') {
|
|
93
|
+
return promptNumberValue(key, currentValue as number);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 字符串类型:根据 allowedValues 自动选择提示策略
|
|
97
|
+
const definition = CONFIG_DEFINITIONS[key];
|
|
98
|
+
if (definition.allowedValues) {
|
|
99
|
+
return promptEnumValue(key, currentValue as string, definition.allowedValues);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return promptStringValue(key, currentValue as string);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 格式化配置值的显示样式
|
|
107
|
+
* @param {ClawtConfig[keyof ClawtConfig]} value - 配置值
|
|
108
|
+
* @returns {string} 格式化后的字符串
|
|
109
|
+
*/
|
|
110
|
+
export function formatConfigValue(value: ClawtConfig[keyof ClawtConfig]): string {
|
|
111
|
+
if (typeof value === 'boolean') {
|
|
112
|
+
return value ? chalk.green('true') : chalk.yellow('false');
|
|
113
|
+
}
|
|
114
|
+
return chalk.cyan(String(value));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 交互式布尔值选择(内部辅助函数)
|
|
119
|
+
* @param {keyof ClawtConfig} key - 配置项名称
|
|
120
|
+
* @param {boolean} currentValue - 当前值
|
|
121
|
+
* @returns {Promise<boolean>} 用户选择的布尔值
|
|
122
|
+
*/
|
|
123
|
+
async function promptBooleanValue(key: keyof ClawtConfig, currentValue: boolean): Promise<boolean> {
|
|
124
|
+
const choices = [
|
|
125
|
+
{ name: 'true', message: 'true' },
|
|
126
|
+
{ name: 'false', message: 'false' },
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
// @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
|
|
130
|
+
const selected: string = await new Enquirer.Select({
|
|
131
|
+
message: MESSAGES.CONFIG_INPUT_PROMPT(key),
|
|
132
|
+
choices,
|
|
133
|
+
initial: currentValue ? 0 : 1,
|
|
134
|
+
}).run();
|
|
135
|
+
|
|
136
|
+
return selected === 'true';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 交互式数字输入(内部辅助函数)
|
|
141
|
+
* @param {keyof ClawtConfig} key - 配置项名称
|
|
142
|
+
* @param {number} currentValue - 当前值
|
|
143
|
+
* @returns {Promise<number>} 用户输入的数字值
|
|
144
|
+
*/
|
|
145
|
+
async function promptNumberValue(key: keyof ClawtConfig, currentValue: number): Promise<number> {
|
|
146
|
+
// @ts-expect-error enquirer 类型声明未导出 Input 类,但运行时存在
|
|
147
|
+
const input: string = await new Enquirer.Input({
|
|
148
|
+
message: MESSAGES.CONFIG_INPUT_PROMPT(key),
|
|
149
|
+
initial: String(currentValue),
|
|
150
|
+
validate: (val: string) => {
|
|
151
|
+
if (Number.isNaN(Number(val))) return '请输入有效的数字';
|
|
152
|
+
return true;
|
|
153
|
+
},
|
|
154
|
+
}).run();
|
|
155
|
+
|
|
156
|
+
return Number(input);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 交互式枚举值选择(内部辅助函数,用于有 allowedValues 的 string 配置项)
|
|
161
|
+
* @param {keyof ClawtConfig} key - 配置项名称
|
|
162
|
+
* @param {string} currentValue - 当前值
|
|
163
|
+
* @param {readonly string[]} allowedValues - 允许的枚举值列表
|
|
164
|
+
* @returns {Promise<string>} 用户选择的枚举值
|
|
165
|
+
*/
|
|
166
|
+
async function promptEnumValue(
|
|
167
|
+
key: keyof ClawtConfig,
|
|
168
|
+
currentValue: string,
|
|
169
|
+
allowedValues: readonly string[],
|
|
170
|
+
): Promise<string> {
|
|
171
|
+
const choices = allowedValues.map((v) => ({
|
|
172
|
+
name: v,
|
|
173
|
+
message: v,
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
// @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
|
|
177
|
+
return await new Enquirer.Select({
|
|
178
|
+
message: MESSAGES.CONFIG_INPUT_PROMPT(key),
|
|
179
|
+
choices,
|
|
180
|
+
initial: allowedValues.indexOf(currentValue),
|
|
181
|
+
}).run();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 交互式字符串自由输入(内部辅助函数,用于无 allowedValues 的 string 配置项)
|
|
186
|
+
* @param {keyof ClawtConfig} key - 配置项名称
|
|
187
|
+
* @param {string} currentValue - 当前值
|
|
188
|
+
* @returns {Promise<string>} 用户输入的字符串值
|
|
189
|
+
*/
|
|
190
|
+
async function promptStringValue(key: keyof ClawtConfig, currentValue: string): Promise<string> {
|
|
191
|
+
// @ts-expect-error enquirer 类型声明未导出 Input 类,但运行时存在
|
|
192
|
+
return await new Enquirer.Input({
|
|
193
|
+
message: MESSAGES.CONFIG_INPUT_PROMPT(key),
|
|
194
|
+
initial: currentValue,
|
|
195
|
+
}).run();
|
|
196
|
+
}
|
package/src/utils/config.ts
CHANGED
|
@@ -39,6 +39,14 @@ export function writeDefaultConfig(): void {
|
|
|
39
39
|
writeConfig(DEFAULT_CONFIG);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 将配置对象完整写入配置文件
|
|
44
|
+
* @param {ClawtConfig} config - 要持久化的配置对象
|
|
45
|
+
*/
|
|
46
|
+
export function saveConfig(config: ClawtConfig): void {
|
|
47
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
/**
|
|
43
51
|
* 获取配置中指定字段的值
|
|
44
52
|
* @param {keyof ClawtConfig} key - 配置字段名
|
package/src/utils/index.ts
CHANGED
|
@@ -48,7 +48,7 @@ export {
|
|
|
48
48
|
export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
49
49
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
|
|
50
50
|
export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus, createWorktreesByBranches } from './worktree.js';
|
|
51
|
-
export { loadConfig, writeDefaultConfig, writeConfig, getConfigValue, ensureClawtDirs } from './config.js';
|
|
51
|
+
export { loadConfig, writeDefaultConfig, writeConfig, saveConfig, getConfigValue, ensureClawtDirs } from './config.js';
|
|
52
52
|
export { printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration } from './formatter.js';
|
|
53
53
|
export { ensureDir, removeEmptyDir } from './fs.js';
|
|
54
54
|
export { multilineInput } from './prompt.js';
|
|
@@ -61,4 +61,5 @@ export { parseTaskFile, loadTaskFile } from './task-file.js';
|
|
|
61
61
|
export { executeBatchTasks } from './task-executor.js';
|
|
62
62
|
export { detectTerminalApp, openCommandInNewTerminalTab } from './terminal.js';
|
|
63
63
|
export { applyAliases } from './alias.js';
|
|
64
|
+
export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue } from './config-strategy.js';
|
|
64
65
|
|
|
@@ -1,85 +1,156 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
|
|
4
|
+
// mock enquirer(必须在所有 import 之前)
|
|
5
|
+
const { mockSelectRun, mockInputRun } = vi.hoisted(() => {
|
|
6
|
+
const mockSelectRun = vi.fn();
|
|
7
|
+
const mockInputRun = vi.fn();
|
|
8
|
+
return { mockSelectRun, mockInputRun };
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
vi.mock('enquirer', () => ({
|
|
12
|
+
default: {
|
|
13
|
+
Select: function MockSelect() { return { run: mockSelectRun }; },
|
|
14
|
+
Input: function MockInput() { return { run: mockInputRun }; },
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
4
18
|
// mock 依赖模块
|
|
5
19
|
vi.mock('../../../src/logger/index.js', () => ({
|
|
6
20
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
7
21
|
}));
|
|
8
22
|
|
|
9
|
-
vi.mock('../../../src/utils/index.js', () =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
vi.mock('../../../src/utils/index.js', async (importOriginal) => {
|
|
24
|
+
const original = await importOriginal<typeof import('../../../src/utils/index.js')>();
|
|
25
|
+
return {
|
|
26
|
+
loadConfig: vi.fn(),
|
|
27
|
+
writeDefaultConfig: vi.fn(),
|
|
28
|
+
saveConfig: vi.fn(),
|
|
29
|
+
printInfo: vi.fn(),
|
|
30
|
+
printSuccess: vi.fn(),
|
|
31
|
+
printError: vi.fn(),
|
|
32
|
+
confirmDestructiveAction: vi.fn(),
|
|
33
|
+
// 策略工具函数透传真实实现(因为常量已被 mock,工具函数可以正常工作)
|
|
34
|
+
isValidConfigKey: original.isValidConfigKey,
|
|
35
|
+
getValidConfigKeys: original.getValidConfigKeys,
|
|
36
|
+
parseConfigValue: original.parseConfigValue,
|
|
37
|
+
promptConfigValue: original.promptConfigValue,
|
|
38
|
+
formatConfigValue: original.formatConfigValue,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
17
41
|
|
|
18
42
|
vi.mock('../../../src/constants/index.js', () => ({
|
|
19
43
|
CONFIG_PATH: '/mock/.clawt/config.json',
|
|
20
44
|
DEFAULT_CONFIG: {
|
|
21
|
-
claudeCodeCommand: 'claude',
|
|
22
45
|
autoDeleteBranch: false,
|
|
46
|
+
claudeCodeCommand: 'claude',
|
|
23
47
|
autoPullPush: false,
|
|
24
48
|
confirmDestructiveOps: true,
|
|
49
|
+
maxConcurrency: 0,
|
|
50
|
+
terminalApp: 'auto',
|
|
25
51
|
},
|
|
26
52
|
CONFIG_DESCRIPTIONS: {
|
|
27
|
-
claudeCodeCommand: 'Claude Code CLI 命令',
|
|
28
53
|
autoDeleteBranch: '自动删除分支',
|
|
54
|
+
claudeCodeCommand: 'Claude Code CLI 命令',
|
|
29
55
|
autoPullPush: '自动 pull/push',
|
|
30
56
|
confirmDestructiveOps: '破坏性操作确认',
|
|
57
|
+
maxConcurrency: '最大并发数',
|
|
58
|
+
terminalApp: '终端应用',
|
|
59
|
+
},
|
|
60
|
+
CONFIG_DEFINITIONS: {
|
|
61
|
+
autoDeleteBranch: { defaultValue: false, description: '自动删除分支' },
|
|
62
|
+
claudeCodeCommand: { defaultValue: 'claude', description: 'Claude Code CLI 命令' },
|
|
63
|
+
autoPullPush: { defaultValue: false, description: '自动 pull/push' },
|
|
64
|
+
confirmDestructiveOps: { defaultValue: true, description: '破坏性操作确认' },
|
|
65
|
+
maxConcurrency: { defaultValue: 0, description: '最大并发数' },
|
|
66
|
+
terminalApp: { defaultValue: 'auto', description: '终端应用', allowedValues: ['auto', 'iterm2', 'terminal'] },
|
|
31
67
|
},
|
|
32
68
|
MESSAGES: {
|
|
33
69
|
CONFIG_RESET_SUCCESS: '配置已恢复为默认值',
|
|
34
70
|
DESTRUCTIVE_OP_CANCELLED: '已取消操作',
|
|
71
|
+
CONFIG_SET_SUCCESS: (key: string, value: string) => `✓ ${key} 已设置为 ${value}`,
|
|
72
|
+
CONFIG_GET_VALUE: (key: string, value: string) => `${key} = ${value}`,
|
|
73
|
+
CONFIG_INVALID_KEY: (key: string, validKeys: string[]) =>
|
|
74
|
+
`无效的配置项: ${key}\n可用的配置项: ${validKeys.join(', ')}`,
|
|
75
|
+
CONFIG_INVALID_BOOLEAN: (key: string) =>
|
|
76
|
+
`配置项 ${key} 为布尔类型,仅接受 true 或 false`,
|
|
77
|
+
CONFIG_INVALID_NUMBER: (key: string) =>
|
|
78
|
+
`配置项 ${key} 为数字类型,请输入有效的数字`,
|
|
79
|
+
CONFIG_INVALID_ENUM: (key: string, validValues: readonly string[]) =>
|
|
80
|
+
`配置项 ${key} 仅接受以下值: ${validValues.join(', ')}`,
|
|
81
|
+
CONFIG_SELECT_PROMPT: '选择要修改的配置项',
|
|
82
|
+
CONFIG_INPUT_PROMPT: (key: string) => `输入 ${key} 的新值`,
|
|
83
|
+
CONFIG_MISSING_VALUE: (key: string) => `缺少配置值,用法: clawt config set ${key} <value>`,
|
|
35
84
|
},
|
|
36
85
|
}));
|
|
37
86
|
|
|
38
87
|
import { registerConfigCommand } from '../../../src/commands/config.js';
|
|
39
|
-
import { loadConfig, writeDefaultConfig, printInfo, printSuccess, confirmDestructiveAction } from '../../../src/utils/index.js';
|
|
88
|
+
import { loadConfig, writeDefaultConfig, saveConfig, printInfo, printSuccess, printError, confirmDestructiveAction } from '../../../src/utils/index.js';
|
|
40
89
|
|
|
41
90
|
const mockedLoadConfig = vi.mocked(loadConfig);
|
|
42
91
|
const mockedWriteDefaultConfig = vi.mocked(writeDefaultConfig);
|
|
92
|
+
const mockedSaveConfig = vi.mocked(saveConfig);
|
|
43
93
|
const mockedPrintInfo = vi.mocked(printInfo);
|
|
44
94
|
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
95
|
+
const mockedPrintError = vi.mocked(printError);
|
|
45
96
|
const mockedConfirmDestructiveAction = vi.mocked(confirmDestructiveAction);
|
|
46
97
|
|
|
98
|
+
/** 创建默认配置对象用于 mock */
|
|
99
|
+
function createMockConfig() {
|
|
100
|
+
return {
|
|
101
|
+
autoDeleteBranch: false,
|
|
102
|
+
claudeCodeCommand: 'claude',
|
|
103
|
+
autoPullPush: false,
|
|
104
|
+
confirmDestructiveOps: true,
|
|
105
|
+
maxConcurrency: 0,
|
|
106
|
+
terminalApp: 'auto',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
47
110
|
beforeEach(() => {
|
|
48
111
|
mockedLoadConfig.mockReset();
|
|
49
112
|
mockedWriteDefaultConfig.mockReset();
|
|
113
|
+
mockedSaveConfig.mockReset();
|
|
50
114
|
mockedPrintInfo.mockReset();
|
|
51
115
|
mockedPrintSuccess.mockReset();
|
|
116
|
+
mockedPrintError.mockReset();
|
|
52
117
|
mockedConfirmDestructiveAction.mockReset();
|
|
118
|
+
mockSelectRun.mockReset();
|
|
119
|
+
mockInputRun.mockReset();
|
|
53
120
|
});
|
|
54
121
|
|
|
55
122
|
describe('registerConfigCommand', () => {
|
|
56
|
-
it('注册 config
|
|
123
|
+
it('注册 config 命令及所有子命令', () => {
|
|
57
124
|
const program = new Command();
|
|
58
125
|
registerConfigCommand(program);
|
|
59
126
|
const configCmd = program.commands.find((c) => c.name() === 'config');
|
|
60
127
|
expect(configCmd).toBeDefined();
|
|
61
|
-
|
|
62
|
-
|
|
128
|
+
|
|
129
|
+
const subcommandNames = configCmd!.commands.map((c) => c.name());
|
|
130
|
+
expect(subcommandNames).toContain('reset');
|
|
131
|
+
expect(subcommandNames).toContain('set');
|
|
132
|
+
expect(subcommandNames).toContain('get');
|
|
63
133
|
});
|
|
64
134
|
});
|
|
65
135
|
|
|
66
136
|
describe('handleConfig(通过 action 间接测试)', () => {
|
|
67
|
-
it('
|
|
68
|
-
mockedLoadConfig.mockReturnValue(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
137
|
+
it('无子命令时进入交互式配置', async () => {
|
|
138
|
+
mockedLoadConfig.mockReturnValue(createMockConfig());
|
|
139
|
+
// 第一次 Select.run 选择配置项
|
|
140
|
+
mockSelectRun.mockResolvedValueOnce('autoDeleteBranch');
|
|
141
|
+
// 第二次 Select.run 选择布尔值
|
|
142
|
+
mockSelectRun.mockResolvedValueOnce('true');
|
|
74
143
|
|
|
75
144
|
const program = new Command();
|
|
76
145
|
program.exitOverride();
|
|
77
146
|
registerConfigCommand(program);
|
|
78
|
-
program.
|
|
147
|
+
await program.parseAsync(['config'], { from: 'user' });
|
|
79
148
|
|
|
80
149
|
expect(mockedLoadConfig).toHaveBeenCalled();
|
|
81
|
-
|
|
82
|
-
|
|
150
|
+
expect(mockedSaveConfig).toHaveBeenCalledWith(
|
|
151
|
+
expect.objectContaining({ autoDeleteBranch: true }),
|
|
152
|
+
);
|
|
153
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
83
154
|
});
|
|
84
155
|
});
|
|
85
156
|
|
|
@@ -108,3 +179,232 @@ describe('handleConfigReset(通过 action 间接测试)', () => {
|
|
|
108
179
|
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
109
180
|
});
|
|
110
181
|
});
|
|
182
|
+
|
|
183
|
+
describe('handleConfigSet — 直接模式', () => {
|
|
184
|
+
it('设置布尔值 true', async () => {
|
|
185
|
+
mockedLoadConfig.mockReturnValue(createMockConfig());
|
|
186
|
+
|
|
187
|
+
const program = new Command();
|
|
188
|
+
program.exitOverride();
|
|
189
|
+
registerConfigCommand(program);
|
|
190
|
+
await program.parseAsync(['config', 'set', 'autoDeleteBranch', 'true'], { from: 'user' });
|
|
191
|
+
|
|
192
|
+
expect(mockedSaveConfig).toHaveBeenCalledWith(
|
|
193
|
+
expect.objectContaining({ autoDeleteBranch: true }),
|
|
194
|
+
);
|
|
195
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('设置布尔值 false', async () => {
|
|
199
|
+
mockedLoadConfig.mockReturnValue({ ...createMockConfig(), confirmDestructiveOps: true });
|
|
200
|
+
|
|
201
|
+
const program = new Command();
|
|
202
|
+
program.exitOverride();
|
|
203
|
+
registerConfigCommand(program);
|
|
204
|
+
await program.parseAsync(['config', 'set', 'confirmDestructiveOps', 'false'], { from: 'user' });
|
|
205
|
+
|
|
206
|
+
expect(mockedSaveConfig).toHaveBeenCalledWith(
|
|
207
|
+
expect.objectContaining({ confirmDestructiveOps: false }),
|
|
208
|
+
);
|
|
209
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('布尔值无效时报错', async () => {
|
|
213
|
+
const program = new Command();
|
|
214
|
+
program.exitOverride();
|
|
215
|
+
registerConfigCommand(program);
|
|
216
|
+
await program.parseAsync(['config', 'set', 'autoDeleteBranch', 'abc'], { from: 'user' });
|
|
217
|
+
|
|
218
|
+
expect(mockedSaveConfig).not.toHaveBeenCalled();
|
|
219
|
+
expect(mockedPrintError).toHaveBeenCalled();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('设置数字值', async () => {
|
|
223
|
+
mockedLoadConfig.mockReturnValue(createMockConfig());
|
|
224
|
+
|
|
225
|
+
const program = new Command();
|
|
226
|
+
program.exitOverride();
|
|
227
|
+
registerConfigCommand(program);
|
|
228
|
+
await program.parseAsync(['config', 'set', 'maxConcurrency', '4'], { from: 'user' });
|
|
229
|
+
|
|
230
|
+
expect(mockedSaveConfig).toHaveBeenCalledWith(
|
|
231
|
+
expect.objectContaining({ maxConcurrency: 4 }),
|
|
232
|
+
);
|
|
233
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('数字值无效时报错', async () => {
|
|
237
|
+
const program = new Command();
|
|
238
|
+
program.exitOverride();
|
|
239
|
+
registerConfigCommand(program);
|
|
240
|
+
await program.parseAsync(['config', 'set', 'maxConcurrency', 'abc'], { from: 'user' });
|
|
241
|
+
|
|
242
|
+
expect(mockedSaveConfig).not.toHaveBeenCalled();
|
|
243
|
+
expect(mockedPrintError).toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('设置字符串值', async () => {
|
|
247
|
+
mockedLoadConfig.mockReturnValue(createMockConfig());
|
|
248
|
+
|
|
249
|
+
const program = new Command();
|
|
250
|
+
program.exitOverride();
|
|
251
|
+
registerConfigCommand(program);
|
|
252
|
+
await program.parseAsync(['config', 'set', 'claudeCodeCommand', 'cc'], { from: 'user' });
|
|
253
|
+
|
|
254
|
+
expect(mockedSaveConfig).toHaveBeenCalledWith(
|
|
255
|
+
expect.objectContaining({ claudeCodeCommand: 'cc' }),
|
|
256
|
+
);
|
|
257
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('设置 terminalApp 有效值', async () => {
|
|
261
|
+
mockedLoadConfig.mockReturnValue(createMockConfig());
|
|
262
|
+
|
|
263
|
+
const program = new Command();
|
|
264
|
+
program.exitOverride();
|
|
265
|
+
registerConfigCommand(program);
|
|
266
|
+
await program.parseAsync(['config', 'set', 'terminalApp', 'iterm2'], { from: 'user' });
|
|
267
|
+
|
|
268
|
+
expect(mockedSaveConfig).toHaveBeenCalledWith(
|
|
269
|
+
expect.objectContaining({ terminalApp: 'iterm2' }),
|
|
270
|
+
);
|
|
271
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('设置 terminalApp 无效值时报错', async () => {
|
|
275
|
+
const program = new Command();
|
|
276
|
+
program.exitOverride();
|
|
277
|
+
registerConfigCommand(program);
|
|
278
|
+
await program.parseAsync(['config', 'set', 'terminalApp', 'invalid'], { from: 'user' });
|
|
279
|
+
|
|
280
|
+
expect(mockedSaveConfig).not.toHaveBeenCalled();
|
|
281
|
+
expect(mockedPrintError).toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('无效 key 时报错', async () => {
|
|
285
|
+
const program = new Command();
|
|
286
|
+
program.exitOverride();
|
|
287
|
+
registerConfigCommand(program);
|
|
288
|
+
await program.parseAsync(['config', 'set', 'foobar', 'true'], { from: 'user' });
|
|
289
|
+
|
|
290
|
+
expect(mockedSaveConfig).not.toHaveBeenCalled();
|
|
291
|
+
expect(mockedPrintError).toHaveBeenCalled();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('缺少 value 参数时报错', async () => {
|
|
295
|
+
const program = new Command();
|
|
296
|
+
program.exitOverride();
|
|
297
|
+
registerConfigCommand(program);
|
|
298
|
+
await program.parseAsync(['config', 'set', 'autoDeleteBranch'], { from: 'user' });
|
|
299
|
+
|
|
300
|
+
expect(mockedSaveConfig).not.toHaveBeenCalled();
|
|
301
|
+
expect(mockedPrintError).toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('handleConfigSet — 交互模式', () => {
|
|
306
|
+
it('交互选择布尔配置项并修改', async () => {
|
|
307
|
+
mockedLoadConfig.mockReturnValue(createMockConfig());
|
|
308
|
+
// 第一次 Select.run 选择配置项
|
|
309
|
+
mockSelectRun.mockResolvedValueOnce('autoDeleteBranch');
|
|
310
|
+
// 第二次 Select.run 选择布尔值
|
|
311
|
+
mockSelectRun.mockResolvedValueOnce('true');
|
|
312
|
+
|
|
313
|
+
const program = new Command();
|
|
314
|
+
program.exitOverride();
|
|
315
|
+
registerConfigCommand(program);
|
|
316
|
+
await program.parseAsync(['config', 'set'], { from: 'user' });
|
|
317
|
+
|
|
318
|
+
expect(mockedSaveConfig).toHaveBeenCalledWith(
|
|
319
|
+
expect.objectContaining({ autoDeleteBranch: true }),
|
|
320
|
+
);
|
|
321
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('交互选择数字配置项并修改', async () => {
|
|
325
|
+
mockedLoadConfig.mockReturnValue(createMockConfig());
|
|
326
|
+
// 选择配置项
|
|
327
|
+
mockSelectRun.mockResolvedValueOnce('maxConcurrency');
|
|
328
|
+
// 输入数字
|
|
329
|
+
mockInputRun.mockResolvedValueOnce('8');
|
|
330
|
+
|
|
331
|
+
const program = new Command();
|
|
332
|
+
program.exitOverride();
|
|
333
|
+
registerConfigCommand(program);
|
|
334
|
+
await program.parseAsync(['config', 'set'], { from: 'user' });
|
|
335
|
+
|
|
336
|
+
expect(mockedSaveConfig).toHaveBeenCalledWith(
|
|
337
|
+
expect.objectContaining({ maxConcurrency: 8 }),
|
|
338
|
+
);
|
|
339
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('交互选择字符串配置项并修改', async () => {
|
|
343
|
+
mockedLoadConfig.mockReturnValue(createMockConfig());
|
|
344
|
+
// 选择配置项
|
|
345
|
+
mockSelectRun.mockResolvedValueOnce('claudeCodeCommand');
|
|
346
|
+
// 输入字符串
|
|
347
|
+
mockInputRun.mockResolvedValueOnce('cc');
|
|
348
|
+
|
|
349
|
+
const program = new Command();
|
|
350
|
+
program.exitOverride();
|
|
351
|
+
registerConfigCommand(program);
|
|
352
|
+
await program.parseAsync(['config', 'set'], { from: 'user' });
|
|
353
|
+
|
|
354
|
+
expect(mockedSaveConfig).toHaveBeenCalledWith(
|
|
355
|
+
expect.objectContaining({ claudeCodeCommand: 'cc' }),
|
|
356
|
+
);
|
|
357
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('交互选择 terminalApp 配置项时使用 Select', async () => {
|
|
361
|
+
mockedLoadConfig.mockReturnValue(createMockConfig());
|
|
362
|
+
// 选择配置项
|
|
363
|
+
mockSelectRun.mockResolvedValueOnce('terminalApp');
|
|
364
|
+
// 选择 terminalApp 值
|
|
365
|
+
mockSelectRun.mockResolvedValueOnce('iterm2');
|
|
366
|
+
|
|
367
|
+
const program = new Command();
|
|
368
|
+
program.exitOverride();
|
|
369
|
+
registerConfigCommand(program);
|
|
370
|
+
await program.parseAsync(['config', 'set'], { from: 'user' });
|
|
371
|
+
|
|
372
|
+
expect(mockedSaveConfig).toHaveBeenCalledWith(
|
|
373
|
+
expect.objectContaining({ terminalApp: 'iterm2' }),
|
|
374
|
+
);
|
|
375
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('handleConfigGet', () => {
|
|
380
|
+
it('获取有效配置项的值', () => {
|
|
381
|
+
mockedLoadConfig.mockReturnValue(createMockConfig());
|
|
382
|
+
|
|
383
|
+
const program = new Command();
|
|
384
|
+
program.exitOverride();
|
|
385
|
+
registerConfigCommand(program);
|
|
386
|
+
program.parse(['config', 'get', 'maxConcurrency'], { from: 'user' });
|
|
387
|
+
|
|
388
|
+
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('获取布尔配置项的值', () => {
|
|
392
|
+
mockedLoadConfig.mockReturnValue({ ...createMockConfig(), autoDeleteBranch: true });
|
|
393
|
+
|
|
394
|
+
const program = new Command();
|
|
395
|
+
program.exitOverride();
|
|
396
|
+
registerConfigCommand(program);
|
|
397
|
+
program.parse(['config', 'get', 'autoDeleteBranch'], { from: 'user' });
|
|
398
|
+
|
|
399
|
+
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('无效 key 时报错', () => {
|
|
403
|
+
const program = new Command();
|
|
404
|
+
program.exitOverride();
|
|
405
|
+
registerConfigCommand(program);
|
|
406
|
+
program.parse(['config', 'get', 'invalidKey'], { from: 'user' });
|
|
407
|
+
|
|
408
|
+
expect(mockedPrintError).toHaveBeenCalled();
|
|
409
|
+
});
|
|
410
|
+
});
|