clawt 2.11.0 → 2.12.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/.claude/agent-memory/docs-sync-updater/MEMORY.md +16 -7
- package/README.md +9 -1
- package/dist/index.js +222 -32
- package/dist/postinstall.js +17 -4
- package/docs/spec.md +40 -9
- package/package.json +1 -1
- package/src/commands/resume.ts +81 -11
- package/src/constants/config.ts +4 -0
- package/src/constants/index.ts +3 -2
- package/src/constants/messages/resume.ts +13 -4
- package/src/constants/paths.ts +3 -0
- package/src/constants/prompt.ts +5 -0
- package/src/constants/terminal.ts +6 -0
- package/src/types/config.ts +2 -0
- package/src/utils/claude.ts +92 -2
- package/src/utils/index.ts +2 -1
- package/src/utils/terminal.ts +134 -0
- package/src/utils/worktree-matcher.ts +68 -5
- package/tests/unit/commands/resume.test.ts +1 -1
- package/tests/unit/utils/claude.test.ts +115 -1
|
@@ -10,6 +10,12 @@ vi.mock('node:child_process', () => ({
|
|
|
10
10
|
spawnSync: vi.fn(),
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
|
+
// mock node:fs
|
|
14
|
+
vi.mock('node:fs', () => ({
|
|
15
|
+
existsSync: vi.fn(),
|
|
16
|
+
readdirSync: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
13
19
|
// mock config
|
|
14
20
|
vi.mock('../../../src/utils/config.js', () => ({
|
|
15
21
|
getConfigValue: vi.fn(),
|
|
@@ -22,7 +28,8 @@ vi.mock('../../../src/utils/formatter.js', () => ({
|
|
|
22
28
|
}));
|
|
23
29
|
|
|
24
30
|
import { spawnSync } from 'node:child_process';
|
|
25
|
-
import {
|
|
31
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
32
|
+
import { launchInteractiveClaude, hasClaudeSessionHistory } from '../../../src/utils/claude.js';
|
|
26
33
|
import { getConfigValue } from '../../../src/utils/config.js';
|
|
27
34
|
import { printInfo, printWarning } from '../../../src/utils/formatter.js';
|
|
28
35
|
import { ClawtError } from '../../../src/errors/index.js';
|
|
@@ -32,6 +39,45 @@ const mockedSpawnSync = vi.mocked(spawnSync);
|
|
|
32
39
|
const mockedGetConfigValue = vi.mocked(getConfigValue);
|
|
33
40
|
const mockedPrintInfo = vi.mocked(printInfo);
|
|
34
41
|
const mockedPrintWarning = vi.mocked(printWarning);
|
|
42
|
+
const mockedExistsSync = vi.mocked(existsSync);
|
|
43
|
+
const mockedReaddirSync = vi.mocked(readdirSync);
|
|
44
|
+
|
|
45
|
+
describe('hasClaudeSessionHistory', () => {
|
|
46
|
+
it('项目目录不存在时返回 false', () => {
|
|
47
|
+
mockedExistsSync.mockReturnValue(false);
|
|
48
|
+
|
|
49
|
+
expect(hasClaudeSessionHistory('/Users/test/project')).toBe(false);
|
|
50
|
+
expect(mockedExistsSync).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('项目目录存在但无 .jsonl 文件时返回 false', () => {
|
|
54
|
+
mockedExistsSync.mockReturnValue(true);
|
|
55
|
+
mockedReaddirSync.mockReturnValue(['memory', 'CLAUDE.md'] as unknown as ReturnType<typeof readdirSync>);
|
|
56
|
+
|
|
57
|
+
expect(hasClaudeSessionHistory('/Users/test/project')).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('项目目录存在且有 .jsonl 文件时返回 true', () => {
|
|
61
|
+
mockedExistsSync.mockReturnValue(true);
|
|
62
|
+
mockedReaddirSync.mockReturnValue([
|
|
63
|
+
'abc-123.jsonl',
|
|
64
|
+
'memory',
|
|
65
|
+
] as unknown as ReturnType<typeof readdirSync>);
|
|
66
|
+
|
|
67
|
+
expect(hasClaudeSessionHistory('/Users/test/project')).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('路径编码规则正确(非字母数字字符替换为 -)', () => {
|
|
71
|
+
mockedExistsSync.mockReturnValue(false);
|
|
72
|
+
|
|
73
|
+
hasClaudeSessionHistory('/Users/qihoo/.clawt/worktrees/clawt/resume');
|
|
74
|
+
|
|
75
|
+
// 验证 existsSync 被调用时路径包含编码后的目录名
|
|
76
|
+
// /Users/qihoo/.clawt/worktrees/clawt/resume → -Users-qihoo--clawt-worktrees-clawt-resume
|
|
77
|
+
const calledPath = mockedExistsSync.mock.calls[0][0] as string;
|
|
78
|
+
expect(calledPath).toContain('-Users-qihoo--clawt-worktrees-clawt-resume');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
35
81
|
|
|
36
82
|
describe('launchInteractiveClaude', () => {
|
|
37
83
|
const worktree = createWorktreeInfo({
|
|
@@ -41,6 +87,7 @@ describe('launchInteractiveClaude', () => {
|
|
|
41
87
|
|
|
42
88
|
it('正常启动 Claude Code(退出码为 0)', () => {
|
|
43
89
|
mockedGetConfigValue.mockReturnValue('claude');
|
|
90
|
+
mockedExistsSync.mockReturnValue(false);
|
|
44
91
|
mockedSpawnSync.mockReturnValue({
|
|
45
92
|
status: 0,
|
|
46
93
|
error: undefined,
|
|
@@ -66,6 +113,7 @@ describe('launchInteractiveClaude', () => {
|
|
|
66
113
|
|
|
67
114
|
it('输出分支和路径信息', () => {
|
|
68
115
|
mockedGetConfigValue.mockReturnValue('claude');
|
|
116
|
+
mockedExistsSync.mockReturnValue(false);
|
|
69
117
|
mockedSpawnSync.mockReturnValue({
|
|
70
118
|
status: 0,
|
|
71
119
|
error: undefined,
|
|
@@ -84,6 +132,7 @@ describe('launchInteractiveClaude', () => {
|
|
|
84
132
|
|
|
85
133
|
it('支持带参数的命令(如 npx claude)', () => {
|
|
86
134
|
mockedGetConfigValue.mockReturnValue('npx claude');
|
|
135
|
+
mockedExistsSync.mockReturnValue(false);
|
|
87
136
|
mockedSpawnSync.mockReturnValue({
|
|
88
137
|
status: 0,
|
|
89
138
|
error: undefined,
|
|
@@ -105,6 +154,7 @@ describe('launchInteractiveClaude', () => {
|
|
|
105
154
|
|
|
106
155
|
it('spawnSync 返回 error 时抛出 ClawtError', () => {
|
|
107
156
|
mockedGetConfigValue.mockReturnValue('claude');
|
|
157
|
+
mockedExistsSync.mockReturnValue(false);
|
|
108
158
|
mockedSpawnSync.mockReturnValue({
|
|
109
159
|
status: null,
|
|
110
160
|
error: new Error('命令未找到'),
|
|
@@ -121,6 +171,7 @@ describe('launchInteractiveClaude', () => {
|
|
|
121
171
|
|
|
122
172
|
it('非零退出码时调用 printWarning', () => {
|
|
123
173
|
mockedGetConfigValue.mockReturnValue('claude');
|
|
174
|
+
mockedExistsSync.mockReturnValue(false);
|
|
124
175
|
mockedSpawnSync.mockReturnValue({
|
|
125
176
|
status: 1,
|
|
126
177
|
error: undefined,
|
|
@@ -138,6 +189,7 @@ describe('launchInteractiveClaude', () => {
|
|
|
138
189
|
|
|
139
190
|
it('退出码为 null 时不调用 printWarning', () => {
|
|
140
191
|
mockedGetConfigValue.mockReturnValue('claude');
|
|
192
|
+
mockedExistsSync.mockReturnValue(false);
|
|
141
193
|
mockedSpawnSync.mockReturnValue({
|
|
142
194
|
status: null,
|
|
143
195
|
error: undefined,
|
|
@@ -155,6 +207,7 @@ describe('launchInteractiveClaude', () => {
|
|
|
155
207
|
|
|
156
208
|
it('退出码为 0 时不调用 printWarning', () => {
|
|
157
209
|
mockedGetConfigValue.mockReturnValue('claude');
|
|
210
|
+
mockedExistsSync.mockReturnValue(false);
|
|
158
211
|
mockedSpawnSync.mockReturnValue({
|
|
159
212
|
status: 0,
|
|
160
213
|
error: undefined,
|
|
@@ -169,4 +222,65 @@ describe('launchInteractiveClaude', () => {
|
|
|
169
222
|
|
|
170
223
|
expect(mockedPrintWarning).not.toHaveBeenCalled();
|
|
171
224
|
});
|
|
225
|
+
|
|
226
|
+
it('autoContinue 启用且有会话历史时追加 --continue 参数', () => {
|
|
227
|
+
mockedGetConfigValue.mockReturnValue('claude');
|
|
228
|
+
mockedExistsSync.mockReturnValue(true);
|
|
229
|
+
mockedReaddirSync.mockReturnValue(['session-abc.jsonl'] as unknown as ReturnType<typeof readdirSync>);
|
|
230
|
+
mockedSpawnSync.mockReturnValue({
|
|
231
|
+
status: 0,
|
|
232
|
+
error: undefined,
|
|
233
|
+
stdout: '',
|
|
234
|
+
stderr: '',
|
|
235
|
+
pid: 1234,
|
|
236
|
+
output: [],
|
|
237
|
+
signal: null,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
launchInteractiveClaude(worktree, { autoContinue: true });
|
|
241
|
+
|
|
242
|
+
const callArgs = mockedSpawnSync.mock.calls[0][1] as string[];
|
|
243
|
+
expect(callArgs).toContain('--continue');
|
|
244
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('继续上次对话'));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('autoContinue 启用但无会话历史时不追加 --continue 参数', () => {
|
|
248
|
+
mockedGetConfigValue.mockReturnValue('claude');
|
|
249
|
+
mockedExistsSync.mockReturnValue(false);
|
|
250
|
+
mockedSpawnSync.mockReturnValue({
|
|
251
|
+
status: 0,
|
|
252
|
+
error: undefined,
|
|
253
|
+
stdout: '',
|
|
254
|
+
stderr: '',
|
|
255
|
+
pid: 1234,
|
|
256
|
+
output: [],
|
|
257
|
+
signal: null,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
launchInteractiveClaude(worktree, { autoContinue: true });
|
|
261
|
+
|
|
262
|
+
const callArgs = mockedSpawnSync.mock.calls[0][1] as string[];
|
|
263
|
+
expect(callArgs).not.toContain('--continue');
|
|
264
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('新对话'));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('不传 autoContinue 时即使有会话历史也不追加 --continue', () => {
|
|
268
|
+
mockedGetConfigValue.mockReturnValue('claude');
|
|
269
|
+
mockedExistsSync.mockReturnValue(true);
|
|
270
|
+
mockedReaddirSync.mockReturnValue(['session-abc.jsonl'] as unknown as ReturnType<typeof readdirSync>);
|
|
271
|
+
mockedSpawnSync.mockReturnValue({
|
|
272
|
+
status: 0,
|
|
273
|
+
error: undefined,
|
|
274
|
+
stdout: '',
|
|
275
|
+
stderr: '',
|
|
276
|
+
pid: 1234,
|
|
277
|
+
output: [],
|
|
278
|
+
signal: null,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
launchInteractiveClaude(worktree);
|
|
282
|
+
|
|
283
|
+
const callArgs = mockedSpawnSync.mock.calls[0][1] as string[];
|
|
284
|
+
expect(callArgs).not.toContain('--continue');
|
|
285
|
+
});
|
|
172
286
|
});
|