clawt 2.9.1 → 2.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/.claude/agent-memory/docs-sync-updater/MEMORY.md +14 -10
- package/.claude/agents/docs-sync-updater.md +11 -0
- package/README.md +63 -268
- package/dist/index.js +420 -103
- package/dist/postinstall.js +242 -0
- package/docs/spec.md +162 -14
- package/package.json +1 -1
- package/src/commands/remove.ts +21 -28
- package/src/commands/status.ts +327 -0
- package/src/constants/index.ts +1 -1
- package/src/constants/messages/common.ts +41 -0
- package/src/constants/messages/config.ts +5 -0
- package/src/constants/messages/create.ts +5 -0
- package/src/constants/messages/index.ts +29 -0
- package/src/constants/messages/merge.ts +42 -0
- package/src/constants/messages/remove.ts +15 -0
- package/src/constants/messages/reset.ts +7 -0
- package/src/constants/messages/resume.ts +12 -0
- package/src/constants/messages/run.ts +16 -0
- package/src/constants/messages/status.ts +25 -0
- package/src/constants/messages/sync.ts +24 -0
- package/src/constants/messages/validate.ts +25 -0
- package/src/constants/messages.ts +22 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +6 -0
- package/src/types/index.ts +2 -1
- package/src/types/status.ts +49 -0
- package/src/utils/git.ts +16 -0
- package/src/utils/index.ts +4 -3
- package/src/utils/validate-snapshot.ts +17 -0
- package/src/utils/worktree-matcher.ts +92 -0
- package/tests/unit/commands/config.test.ts +110 -0
- package/tests/unit/commands/create.test.ts +115 -0
- package/tests/unit/commands/list.test.ts +118 -0
- package/tests/unit/commands/merge.test.ts +323 -0
- package/tests/unit/commands/remove.test.ts +240 -0
- package/tests/unit/commands/reset.test.ts +124 -0
- package/tests/unit/commands/resume.test.ts +91 -0
- package/tests/unit/commands/run.test.ts +207 -0
- package/tests/unit/commands/status.test.ts +214 -0
- package/tests/unit/commands/sync.test.ts +208 -0
- package/tests/unit/commands/validate.test.ts +382 -0
- package/tests/unit/constants/messages.test.ts +1 -1
- package/tests/unit/utils/config.test.ts +21 -1
- package/tests/unit/utils/formatter.test.ts +44 -1
- package/tests/unit/utils/git.test.ts +44 -0
- package/tests/unit/utils/validate-snapshot.test.ts +25 -0
- package/tests/unit/utils/worktree-matcher.test.ts +81 -5
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
gitCommit,
|
|
33
33
|
gitMerge,
|
|
34
34
|
hasMergeConflict,
|
|
35
|
+
gitPull,
|
|
36
|
+
gitPush,
|
|
35
37
|
gitResetHard,
|
|
36
38
|
gitCleanForce,
|
|
37
39
|
gitStashPush,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
gitWorktreePrune,
|
|
45
47
|
hasLocalCommits,
|
|
46
48
|
getCommitCountAhead,
|
|
49
|
+
getCommitCountBehind,
|
|
47
50
|
getDiffStat,
|
|
48
51
|
gitDiffCachedBinary,
|
|
49
52
|
gitApplyCachedFromStdin,
|
|
@@ -530,3 +533,44 @@ describe('gitApplyCachedCheck', () => {
|
|
|
530
533
|
expect(gitApplyCachedCheck(patch, '/repo')).toBe(false);
|
|
531
534
|
});
|
|
532
535
|
});
|
|
536
|
+
|
|
537
|
+
describe('gitPull', () => {
|
|
538
|
+
it('执行 git pull', () => {
|
|
539
|
+
gitPull('/repo');
|
|
540
|
+
expect(mockedExecCommand).toHaveBeenCalledWith('git pull', { cwd: '/repo' });
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('不传 cwd 时 cwd 为 undefined', () => {
|
|
544
|
+
gitPull();
|
|
545
|
+
expect(mockedExecCommand).toHaveBeenCalledWith('git pull', { cwd: undefined });
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
describe('gitPush', () => {
|
|
550
|
+
it('执行 git push', () => {
|
|
551
|
+
gitPush('/repo');
|
|
552
|
+
expect(mockedExecCommand).toHaveBeenCalledWith('git push', { cwd: '/repo' });
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('不传 cwd 时 cwd 为 undefined', () => {
|
|
556
|
+
gitPush();
|
|
557
|
+
expect(mockedExecCommand).toHaveBeenCalledWith('git push', { cwd: undefined });
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
describe('getCommitCountBehind', () => {
|
|
562
|
+
it('返回正确的落后提交数', () => {
|
|
563
|
+
mockedExecCommand.mockReturnValue('3');
|
|
564
|
+
expect(getCommitCountBehind('feature')).toBe(3);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('返回 0 当输出无法解析', () => {
|
|
568
|
+
mockedExecCommand.mockReturnValue('');
|
|
569
|
+
expect(getCommitCountBehind('feature')).toBe(0);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('命令失败时返回 0', () => {
|
|
573
|
+
mockedExecCommand.mockImplementation(() => { throw new Error('fail'); });
|
|
574
|
+
expect(getCommitCountBehind('feature')).toBe(0);
|
|
575
|
+
});
|
|
576
|
+
});
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
writeSnapshot,
|
|
40
40
|
removeSnapshot,
|
|
41
41
|
removeProjectSnapshots,
|
|
42
|
+
getProjectSnapshotBranches,
|
|
42
43
|
} from '../../../src/utils/validate-snapshot.js';
|
|
43
44
|
|
|
44
45
|
const mockedExistsSync = vi.mocked(existsSync);
|
|
@@ -149,3 +150,27 @@ describe('removeProjectSnapshots', () => {
|
|
|
149
150
|
expect(mockedUnlinkSync).not.toHaveBeenCalled();
|
|
150
151
|
});
|
|
151
152
|
});
|
|
153
|
+
|
|
154
|
+
describe('getProjectSnapshotBranches', () => {
|
|
155
|
+
it('返回所有存在快照的分支名', () => {
|
|
156
|
+
mockedExistsSync.mockReturnValue(true);
|
|
157
|
+
// @ts-expect-error readdirSync 返回类型简化
|
|
158
|
+
mockedReaddirSync.mockReturnValue(['feat-a.tree', 'feat-a.head', 'feat-b.tree', 'feat-b.head']);
|
|
159
|
+
const result = getProjectSnapshotBranches('proj');
|
|
160
|
+
expect(result).toEqual(['feat-a', 'feat-b']);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('没有 .tree 文件时返回空数组', () => {
|
|
164
|
+
mockedExistsSync.mockReturnValue(true);
|
|
165
|
+
// @ts-expect-error readdirSync 返回类型简化
|
|
166
|
+
mockedReaddirSync.mockReturnValue(['feat-a.head']);
|
|
167
|
+
const result = getProjectSnapshotBranches('proj');
|
|
168
|
+
expect(result).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('项目目录不存在时返回空数组', () => {
|
|
172
|
+
mockedExistsSync.mockReturnValue(false);
|
|
173
|
+
const result = getProjectSnapshotBranches('proj');
|
|
174
|
+
expect(result).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { findExactMatch, findFuzzyMatches, resolveTargetWorktree } from '../../../src/utils/worktree-matcher.js';
|
|
2
|
+
import { findExactMatch, findFuzzyMatches, resolveTargetWorktree, resolveTargetWorktrees } from '../../../src/utils/worktree-matcher.js';
|
|
3
3
|
import { createWorktreeInfo, createWorktreeList } from '../../helpers/fixtures.js';
|
|
4
4
|
import { ClawtError } from '../../../src/errors/index.js';
|
|
5
|
-
import type { WorktreeResolveMessages } from '../../../src/utils/worktree-matcher.js';
|
|
5
|
+
import type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from '../../../src/utils/worktree-matcher.js';
|
|
6
6
|
|
|
7
7
|
// mock enquirer
|
|
8
8
|
vi.mock('enquirer', () => ({
|
|
9
9
|
default: {
|
|
10
|
-
Select: vi.fn().mockImplementation(({ choices }: { choices: Array<{ name: string }> })
|
|
11
|
-
run
|
|
12
|
-
})
|
|
10
|
+
Select: vi.fn().mockImplementation(function({ choices }: { choices: Array<{ name: string }> }) {
|
|
11
|
+
this.run = vi.fn().mockResolvedValue(choices[0].name);
|
|
12
|
+
}),
|
|
13
|
+
MultiSelect: vi.fn().mockImplementation(function({ choices }: { choices: Array<{ name: string }> }) {
|
|
14
|
+
this.run = vi.fn().mockResolvedValue(choices.map((c: { name: string }) => c.name));
|
|
15
|
+
}),
|
|
13
16
|
},
|
|
14
17
|
}));
|
|
15
18
|
|
|
@@ -114,3 +117,76 @@ describe('resolveTargetWorktree', () => {
|
|
|
114
117
|
await expect(resolveTargetWorktree(worktrees, testMessages, 'xyz')).rejects.toThrow(ClawtError);
|
|
115
118
|
});
|
|
116
119
|
});
|
|
120
|
+
|
|
121
|
+
/** 多选场景测试用消息配置 */
|
|
122
|
+
const testMultiMessages: WorktreeMultiResolveMessages = {
|
|
123
|
+
noWorktrees: '无可用 worktree',
|
|
124
|
+
selectBranch: '请选择要移除的分支',
|
|
125
|
+
multipleMatches: (keyword: string) => `"${keyword}" 匹配到多个分支`,
|
|
126
|
+
noMatch: (keyword: string, branches: string[]) =>
|
|
127
|
+
`未找到匹配 "${keyword}",可用:${branches.join(', ')}`,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
describe('resolveTargetWorktrees', () => {
|
|
131
|
+
it('空列表抛出 ClawtError', async () => {
|
|
132
|
+
await expect(resolveTargetWorktrees([], testMultiMessages, 'any')).rejects.toThrow(ClawtError);
|
|
133
|
+
await expect(resolveTargetWorktrees([], testMultiMessages, 'any')).rejects.toThrow('无可用 worktree');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('单个 worktree 且不传分支名时直接返回数组', async () => {
|
|
137
|
+
const worktrees = [createWorktreeInfo({ branch: 'only-one' })];
|
|
138
|
+
const result = await resolveTargetWorktrees(worktrees, testMultiMessages);
|
|
139
|
+
expect(result).toHaveLength(1);
|
|
140
|
+
expect(result[0].branch).toBe('only-one');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('精确匹配优先,返回单元素数组', async () => {
|
|
144
|
+
const worktrees = [
|
|
145
|
+
createWorktreeInfo({ branch: 'feat' }),
|
|
146
|
+
createWorktreeInfo({ branch: 'feature' }),
|
|
147
|
+
];
|
|
148
|
+
const result = await resolveTargetWorktrees(worktrees, testMultiMessages, 'feat');
|
|
149
|
+
expect(result).toHaveLength(1);
|
|
150
|
+
expect(result[0].branch).toBe('feat');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('模糊匹配唯一结果直接返回单元素数组', async () => {
|
|
154
|
+
const worktrees = [
|
|
155
|
+
createWorktreeInfo({ branch: 'feature-login' }),
|
|
156
|
+
createWorktreeInfo({ branch: 'bugfix-auth' }),
|
|
157
|
+
];
|
|
158
|
+
const result = await resolveTargetWorktrees(worktrees, testMultiMessages, 'login');
|
|
159
|
+
expect(result).toHaveLength(1);
|
|
160
|
+
expect(result[0].branch).toBe('feature-login');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('模糊匹配多个结果时调用多选交互', async () => {
|
|
164
|
+
const worktrees = [
|
|
165
|
+
createWorktreeInfo({ branch: 'feature-login' }),
|
|
166
|
+
createWorktreeInfo({ branch: 'feature-logout' }),
|
|
167
|
+
createWorktreeInfo({ branch: 'bugfix-auth' }),
|
|
168
|
+
];
|
|
169
|
+
const result = await resolveTargetWorktrees(worktrees, testMultiMessages, 'log');
|
|
170
|
+
// mock 的 MultiSelect 返回所有 choices,因此应返回匹配到的 2 个
|
|
171
|
+
expect(result).toHaveLength(2);
|
|
172
|
+
expect(result.map((w) => w.branch)).toEqual(['feature-login', 'feature-logout']);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('不传分支名时多个 worktree 调用多选交互', async () => {
|
|
176
|
+
const worktrees = [
|
|
177
|
+
createWorktreeInfo({ branch: 'feature-a' }),
|
|
178
|
+
createWorktreeInfo({ branch: 'feature-b' }),
|
|
179
|
+
];
|
|
180
|
+
const result = await resolveTargetWorktrees(worktrees, testMultiMessages);
|
|
181
|
+
// mock 的 MultiSelect 返回所有 choices
|
|
182
|
+
expect(result).toHaveLength(2);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('无匹配抛出 ClawtError 并包含可用分支', async () => {
|
|
186
|
+
const worktrees = [
|
|
187
|
+
createWorktreeInfo({ branch: 'feature-a' }),
|
|
188
|
+
createWorktreeInfo({ branch: 'feature-b' }),
|
|
189
|
+
];
|
|
190
|
+
await expect(resolveTargetWorktrees(worktrees, testMultiMessages, 'xyz')).rejects.toThrow(ClawtError);
|
|
191
|
+
});
|
|
192
|
+
});
|