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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { CONFIG_DEFINITIONS, DEFAULT_CONFIG, CONFIG_DESCRIPTIONS } from '../../../src/constants/config.js';
|
|
3
|
+
import { VALID_TERMINAL_APPS } from '../../../src/constants/terminal.js';
|
|
3
4
|
|
|
4
5
|
describe('CONFIG_DEFINITIONS', () => {
|
|
5
6
|
it('所有配置项都有 defaultValue 和 description', () => {
|
|
@@ -21,7 +22,7 @@ describe('DEFAULT_CONFIG', () => {
|
|
|
21
22
|
|
|
22
23
|
it('每个 key 的值等于对应 CONFIG_DEFINITIONS 的 defaultValue', () => {
|
|
23
24
|
for (const [key, def] of Object.entries(CONFIG_DEFINITIONS)) {
|
|
24
|
-
expect((DEFAULT_CONFIG as Record<string, unknown>)[key]).toBe(def.defaultValue);
|
|
25
|
+
expect((DEFAULT_CONFIG as unknown as Record<string, unknown>)[key]).toBe(def.defaultValue);
|
|
25
26
|
}
|
|
26
27
|
});
|
|
27
28
|
|
|
@@ -55,3 +56,25 @@ describe('CONFIG_DESCRIPTIONS', () => {
|
|
|
55
56
|
}
|
|
56
57
|
});
|
|
57
58
|
});
|
|
59
|
+
|
|
60
|
+
describe('CONFIG_DEFINITIONS — allowedValues', () => {
|
|
61
|
+
it('terminalApp 的 allowedValues 与 VALID_TERMINAL_APPS 一致', () => {
|
|
62
|
+
expect(CONFIG_DEFINITIONS.terminalApp.allowedValues).toEqual(VALID_TERMINAL_APPS);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('有 allowedValues 的配置项,defaultValue 必须在其中', () => {
|
|
66
|
+
for (const [, def] of Object.entries(CONFIG_DEFINITIONS)) {
|
|
67
|
+
if (def.allowedValues) {
|
|
68
|
+
expect(def.allowedValues).toContain(def.defaultValue);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('非 string 类型的配置项不应有 allowedValues', () => {
|
|
74
|
+
for (const [, def] of Object.entries(CONFIG_DEFINITIONS)) {
|
|
75
|
+
if (typeof def.defaultValue !== 'string') {
|
|
76
|
+
expect(def.allowedValues).toBeUndefined();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock enquirer(必须在所有 import 之前)
|
|
4
|
+
const { mockSelectRun, mockInputRun } = vi.hoisted(() => {
|
|
5
|
+
const mockSelectRun = vi.fn();
|
|
6
|
+
const mockInputRun = vi.fn();
|
|
7
|
+
return { mockSelectRun, mockInputRun };
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
vi.mock('enquirer', () => ({
|
|
11
|
+
default: {
|
|
12
|
+
Select: function MockSelect() { return { run: mockSelectRun }; },
|
|
13
|
+
Input: function MockInput() { return { run: mockInputRun }; },
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
18
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
22
|
+
const original = await importOriginal<typeof import('../../../src/constants/index.js')>();
|
|
23
|
+
return {
|
|
24
|
+
...original,
|
|
25
|
+
DEFAULT_CONFIG: {
|
|
26
|
+
autoDeleteBranch: false,
|
|
27
|
+
claudeCodeCommand: 'claude',
|
|
28
|
+
autoPullPush: false,
|
|
29
|
+
confirmDestructiveOps: true,
|
|
30
|
+
maxConcurrency: 0,
|
|
31
|
+
terminalApp: 'auto',
|
|
32
|
+
},
|
|
33
|
+
CONFIG_DEFINITIONS: {
|
|
34
|
+
autoDeleteBranch: { defaultValue: false, description: '自动删除分支' },
|
|
35
|
+
claudeCodeCommand: { defaultValue: 'claude', description: 'Claude Code CLI 命令' },
|
|
36
|
+
autoPullPush: { defaultValue: false, description: '自动 pull/push' },
|
|
37
|
+
confirmDestructiveOps: { defaultValue: true, description: '破坏性操作确认' },
|
|
38
|
+
maxConcurrency: { defaultValue: 0, description: '最大并发数' },
|
|
39
|
+
terminalApp: { defaultValue: 'auto', description: '终端应用', allowedValues: ['auto', 'iterm2', 'terminal'] },
|
|
40
|
+
},
|
|
41
|
+
MESSAGES: {
|
|
42
|
+
CONFIG_INVALID_BOOLEAN: (key: string) =>
|
|
43
|
+
`配置项 ${key} 为布尔类型,仅接受 true 或 false`,
|
|
44
|
+
CONFIG_INVALID_NUMBER: (key: string) =>
|
|
45
|
+
`配置项 ${key} 为数字类型,请输入有效的数字`,
|
|
46
|
+
CONFIG_INVALID_ENUM: (key: string, validValues: readonly string[]) =>
|
|
47
|
+
`配置项 ${key} 仅接受以下值: ${validValues.join(', ')}`,
|
|
48
|
+
CONFIG_INPUT_PROMPT: (key: string) => `输入 ${key} 的新值`,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
import {
|
|
54
|
+
isValidConfigKey,
|
|
55
|
+
getValidConfigKeys,
|
|
56
|
+
parseConfigValue,
|
|
57
|
+
promptConfigValue,
|
|
58
|
+
formatConfigValue,
|
|
59
|
+
} from '../../../src/utils/config-strategy.js';
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
mockSelectRun.mockReset();
|
|
63
|
+
mockInputRun.mockReset();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('isValidConfigKey', () => {
|
|
67
|
+
it('有效 key 返回 true', () => {
|
|
68
|
+
expect(isValidConfigKey('autoDeleteBranch')).toBe(true);
|
|
69
|
+
expect(isValidConfigKey('maxConcurrency')).toBe(true);
|
|
70
|
+
expect(isValidConfigKey('terminalApp')).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('无效 key 返回 false', () => {
|
|
74
|
+
expect(isValidConfigKey('foobar')).toBe(false);
|
|
75
|
+
expect(isValidConfigKey('')).toBe(false);
|
|
76
|
+
expect(isValidConfigKey('AUTODELETE')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('getValidConfigKeys', () => {
|
|
81
|
+
it('返回所有配置项名称', () => {
|
|
82
|
+
const keys = getValidConfigKeys();
|
|
83
|
+
expect(keys).toContain('autoDeleteBranch');
|
|
84
|
+
expect(keys).toContain('claudeCodeCommand');
|
|
85
|
+
expect(keys).toContain('autoPullPush');
|
|
86
|
+
expect(keys).toContain('confirmDestructiveOps');
|
|
87
|
+
expect(keys).toContain('maxConcurrency');
|
|
88
|
+
expect(keys).toContain('terminalApp');
|
|
89
|
+
expect(keys).toHaveLength(6);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('parseConfigValue', () => {
|
|
94
|
+
describe('布尔类型策略', () => {
|
|
95
|
+
it('解析 "true" 为 true', () => {
|
|
96
|
+
const result = parseConfigValue('autoDeleteBranch', 'true');
|
|
97
|
+
expect(result).toEqual({ success: true, value: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('解析 "false" 为 false', () => {
|
|
101
|
+
const result = parseConfigValue('autoDeleteBranch', 'false');
|
|
102
|
+
expect(result).toEqual({ success: true, value: false });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('无效布尔值返回错误', () => {
|
|
106
|
+
const result = parseConfigValue('autoDeleteBranch', 'abc');
|
|
107
|
+
expect(result.success).toBe(false);
|
|
108
|
+
if (!result.success) {
|
|
109
|
+
expect(result.error).toContain('布尔类型');
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('数字类型策略', () => {
|
|
115
|
+
it('解析有效数字', () => {
|
|
116
|
+
const result = parseConfigValue('maxConcurrency', '4');
|
|
117
|
+
expect(result).toEqual({ success: true, value: 4 });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('解析 0', () => {
|
|
121
|
+
const result = parseConfigValue('maxConcurrency', '0');
|
|
122
|
+
expect(result).toEqual({ success: true, value: 0 });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('无效数字返回错误', () => {
|
|
126
|
+
const result = parseConfigValue('maxConcurrency', 'abc');
|
|
127
|
+
expect(result.success).toBe(false);
|
|
128
|
+
if (!result.success) {
|
|
129
|
+
expect(result.error).toContain('数字类型');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('字符串 + 有 allowedValues 策略(枚举)', () => {
|
|
135
|
+
it('有效枚举值通过校验', () => {
|
|
136
|
+
const result = parseConfigValue('terminalApp', 'iterm2');
|
|
137
|
+
expect(result).toEqual({ success: true, value: 'iterm2' });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('所有枚举值均可接受', () => {
|
|
141
|
+
expect(parseConfigValue('terminalApp', 'auto')).toEqual({ success: true, value: 'auto' });
|
|
142
|
+
expect(parseConfigValue('terminalApp', 'terminal')).toEqual({ success: true, value: 'terminal' });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('无效枚举值返回错误', () => {
|
|
146
|
+
const result = parseConfigValue('terminalApp', 'invalid');
|
|
147
|
+
expect(result.success).toBe(false);
|
|
148
|
+
if (!result.success) {
|
|
149
|
+
expect(result.error).toContain('仅接受以下值');
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('字符串 + 无 allowedValues 策略(自由输入)', () => {
|
|
155
|
+
it('任意字符串值通过校验', () => {
|
|
156
|
+
const result = parseConfigValue('claudeCodeCommand', 'cc');
|
|
157
|
+
expect(result).toEqual({ success: true, value: 'cc' });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('空字符串也通过校验', () => {
|
|
161
|
+
const result = parseConfigValue('claudeCodeCommand', '');
|
|
162
|
+
expect(result).toEqual({ success: true, value: '' });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('promptConfigValue', () => {
|
|
168
|
+
it('布尔类型使用 Select 提示', async () => {
|
|
169
|
+
mockSelectRun.mockResolvedValueOnce('true');
|
|
170
|
+
const result = await promptConfigValue('autoDeleteBranch', false);
|
|
171
|
+
expect(result).toBe(true);
|
|
172
|
+
expect(mockSelectRun).toHaveBeenCalledTimes(1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('数字类型使用 Input 提示', async () => {
|
|
176
|
+
mockInputRun.mockResolvedValueOnce('8');
|
|
177
|
+
const result = await promptConfigValue('maxConcurrency', 0);
|
|
178
|
+
expect(result).toBe(8);
|
|
179
|
+
expect(mockInputRun).toHaveBeenCalledTimes(1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('字符串 + 有 allowedValues 使用 Select 提示', async () => {
|
|
183
|
+
mockSelectRun.mockResolvedValueOnce('iterm2');
|
|
184
|
+
const result = await promptConfigValue('terminalApp', 'auto');
|
|
185
|
+
expect(result).toBe('iterm2');
|
|
186
|
+
expect(mockSelectRun).toHaveBeenCalledTimes(1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('字符串 + 无 allowedValues 使用 Input 提示', async () => {
|
|
190
|
+
mockInputRun.mockResolvedValueOnce('cc');
|
|
191
|
+
const result = await promptConfigValue('claudeCodeCommand', 'claude');
|
|
192
|
+
expect(result).toBe('cc');
|
|
193
|
+
expect(mockInputRun).toHaveBeenCalledTimes(1);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('formatConfigValue', () => {
|
|
198
|
+
it('true 显示为绿色', () => {
|
|
199
|
+
const result = formatConfigValue(true);
|
|
200
|
+
expect(result).toContain('true');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('false 显示为黄色', () => {
|
|
204
|
+
const result = formatConfigValue(false);
|
|
205
|
+
expect(result).toContain('false');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('数字显示为 cyan', () => {
|
|
209
|
+
const result = formatConfigValue(42);
|
|
210
|
+
expect(result).toContain('42');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('字符串显示为 cyan', () => {
|
|
214
|
+
const result = formatConfigValue('hello');
|
|
215
|
+
expect(result).toContain('hello');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -18,7 +18,7 @@ vi.mock('../../../src/utils/fs.js', () => ({
|
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
|
-
import { loadConfig, getConfigValue, writeDefaultConfig, writeConfig, ensureClawtDirs } from '../../../src/utils/config.js';
|
|
21
|
+
import { loadConfig, getConfigValue, writeDefaultConfig, writeConfig, saveConfig, ensureClawtDirs } from '../../../src/utils/config.js';
|
|
22
22
|
import { DEFAULT_CONFIG } from '../../../src/constants/index.js';
|
|
23
23
|
import { ensureDir } from '../../../src/utils/fs.js';
|
|
24
24
|
|
|
@@ -95,3 +95,15 @@ describe('ensureClawtDirs', () => {
|
|
|
95
95
|
expect(mockedEnsureDir).toHaveBeenCalledTimes(3);
|
|
96
96
|
});
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
describe('saveConfig', () => {
|
|
100
|
+
it('将配置对象写入配置文件', () => {
|
|
101
|
+
const customConfig = { ...DEFAULT_CONFIG, autoDeleteBranch: true };
|
|
102
|
+
saveConfig(customConfig);
|
|
103
|
+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
|
|
104
|
+
expect.any(String),
|
|
105
|
+
JSON.stringify(customConfig, null, 2),
|
|
106
|
+
'utf-8',
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|