clawt 2.10.0 → 2.11.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 +14 -7
- package/.claude/agents/docs-sync-updater.md +11 -0
- package/README.md +80 -284
- package/dist/index.js +839 -307
- package/dist/postinstall.js +272 -0
- package/docs/spec.md +84 -22
- package/package.json +1 -1
- package/src/commands/remove.ts +21 -28
- package/src/commands/run.ts +68 -206
- package/src/constants/config.ts +4 -0
- package/src/constants/index.ts +11 -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 +46 -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/progress.ts +39 -0
- package/src/types/command.ts +4 -0
- package/src/types/config.ts +2 -0
- package/src/types/index.ts +1 -0
- package/src/types/taskFile.ts +13 -0
- package/src/utils/formatter.ts +16 -0
- package/src/utils/index.ts +8 -4
- package/src/utils/progress-render.ts +90 -0
- package/src/utils/progress.ts +213 -0
- package/src/utils/task-executor.ts +365 -0
- package/src/utils/task-file.ts +87 -0
- package/src/utils/worktree-matcher.ts +92 -0
- package/src/utils/worktree.ts +27 -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 +456 -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/config.test.ts +1 -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 +70 -1
- package/tests/unit/utils/git.test.ts +44 -0
- package/tests/unit/utils/progress.test.ts +255 -0
- package/tests/unit/utils/task-file.test.ts +236 -0
- package/tests/unit/utils/validate-snapshot.test.ts +25 -0
- package/tests/unit/utils/worktree-matcher.test.ts +81 -5
- package/tests/unit/utils/worktree.test.ts +26 -1
|
@@ -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
|
+
});
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
} from '../../../src/utils/git.js';
|
|
63
63
|
import { sanitizeBranchName, validateBranchesNotExist } from '../../../src/utils/branch.js';
|
|
64
64
|
import { ensureDir, removeEmptyDir } from '../../../src/utils/fs.js';
|
|
65
|
-
import { createWorktrees, getProjectWorktrees, cleanupWorktrees, getWorktreeStatus } from '../../../src/utils/worktree.js';
|
|
65
|
+
import { createWorktrees, createWorktreesByBranches, getProjectWorktrees, cleanupWorktrees, getWorktreeStatus } from '../../../src/utils/worktree.js';
|
|
66
66
|
import { createWorktreeInfo } from '../../helpers/fixtures.js';
|
|
67
67
|
|
|
68
68
|
const mockedExistsSync = vi.mocked(existsSync);
|
|
@@ -101,6 +101,31 @@ describe('createWorktrees', () => {
|
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
describe('createWorktreesByBranches', () => {
|
|
105
|
+
it('根据分支名列表创建 worktree', () => {
|
|
106
|
+
const result = createWorktreesByBranches(['feat-login', 'fix-bug']);
|
|
107
|
+
expect(result).toHaveLength(2);
|
|
108
|
+
expect(result[0].branch).toBe('feat-login');
|
|
109
|
+
expect(result[1].branch).toBe('fix-bug');
|
|
110
|
+
expect(mockedGitCreateWorktree).toHaveBeenCalledTimes(2);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('调用存在性校验但不调用分支名清理', () => {
|
|
114
|
+
createWorktreesByBranches(['feat-a', 'feat-b']);
|
|
115
|
+
// 不使用 sanitizeBranchName(调用方负责清理)
|
|
116
|
+
expect(sanitizeBranchName).not.toHaveBeenCalled();
|
|
117
|
+
expect(validateBranchesNotExist).toHaveBeenCalledWith(['feat-a', 'feat-b']);
|
|
118
|
+
expect(ensureDir).toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('单个分支名也能正常创建', () => {
|
|
122
|
+
const result = createWorktreesByBranches(['single-branch']);
|
|
123
|
+
expect(result).toHaveLength(1);
|
|
124
|
+
expect(result[0].branch).toBe('single-branch');
|
|
125
|
+
expect(mockedGitCreateWorktree).toHaveBeenCalledTimes(1);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
104
129
|
describe('getProjectWorktrees', () => {
|
|
105
130
|
it('项目目录不存在时返回空数组', () => {
|
|
106
131
|
mockedExistsSync.mockReturnValue(false);
|