clawt 3.4.5 → 3.5.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.
Files changed (45) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/README.md +0 -4
  3. package/dist/index.js +430 -306
  4. package/dist/postinstall.js +12 -1
  5. package/docs/alias.md +7 -1
  6. package/docs/completion.md +1 -1
  7. package/docs/config.md +4 -3
  8. package/docs/cover-validate.md +4 -3
  9. package/docs/create.md +28 -12
  10. package/docs/home.md +12 -8
  11. package/docs/init.md +16 -9
  12. package/docs/list.md +13 -7
  13. package/docs/merge.md +12 -12
  14. package/docs/remove.md +24 -13
  15. package/docs/reset.md +6 -4
  16. package/docs/resume.md +3 -4
  17. package/docs/status.md +75 -30
  18. package/docs/sync.md +26 -26
  19. package/docs/validate.md +13 -7
  20. package/package.json +1 -1
  21. package/src/commands/init.ts +6 -2
  22. package/src/commands/tasks.ts +51 -0
  23. package/src/constants/index.ts +3 -0
  24. package/src/constants/interactive-panel.ts +6 -0
  25. package/src/constants/messages/index.ts +4 -2
  26. package/src/constants/messages/interactive-panel.ts +12 -0
  27. package/src/constants/messages/tasks.ts +9 -0
  28. package/src/constants/tasks-template.ts +28 -0
  29. package/src/index.ts +2 -0
  30. package/src/types/command.ts +6 -0
  31. package/src/types/index.ts +1 -1
  32. package/src/utils/formatter.ts +19 -0
  33. package/src/utils/git-branch.ts +116 -0
  34. package/src/utils/git-core.ts +369 -0
  35. package/src/utils/git-worktree.ts +40 -0
  36. package/src/utils/git.ts +3 -521
  37. package/src/utils/index.ts +1 -1
  38. package/src/utils/interactive-panel-render.ts +12 -6
  39. package/src/utils/interactive-panel-state.ts +137 -0
  40. package/src/utils/interactive-panel.ts +44 -188
  41. package/src/utils/keyboard-controller.ts +48 -0
  42. package/src/utils/ui-prompts.ts +240 -0
  43. package/src/utils/worktree-matcher.ts +21 -251
  44. package/tests/unit/commands/tasks.test.ts +153 -0
  45. package/tests/unit/utils/formatter.test.ts +26 -1
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { Command } from 'commander';
3
+
4
+ vi.mock('../../../src/logger/index.js', () => ({
5
+ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
6
+ }));
7
+
8
+ vi.mock('../../../src/constants/index.js', () => ({
9
+ MESSAGES: {
10
+ TASK_INIT_FILE_EXISTS: (path: string) => `文件已存在: ${path},如需覆盖请先删除`,
11
+ TASK_INIT_SUCCESS: (path: string) => `✓ 任务模板已生成: ${path}`,
12
+ TASK_INIT_HINT: (path: string) => `使用 clawt run -f ${path} 执行任务`,
13
+ },
14
+ TASK_TEMPLATE_OUTPUT_DIR: 'clawt/tasks',
15
+ TASK_TEMPLATE_FILENAME_PREFIX: 'clawt-tasks',
16
+ TASK_TEMPLATE_CONTENT: '# 模板内容',
17
+ }));
18
+
19
+ const mockExistsSync = vi.fn();
20
+ const mockWriteFileSync = vi.fn();
21
+
22
+ vi.mock('node:fs', () => ({
23
+ existsSync: (...args: unknown[]) => mockExistsSync(...args),
24
+ writeFileSync: (...args: unknown[]) => mockWriteFileSync(...args),
25
+ }));
26
+
27
+ const mockGenerateTaskFilename = vi.fn().mockReturnValue('clawt-tasks-2026-01-01-00-00-00.md');
28
+
29
+ vi.mock('../../../src/utils/index.js', () => ({
30
+ printSuccess: vi.fn(),
31
+ printHint: vi.fn(),
32
+ ensureDir: vi.fn(),
33
+ generateTaskFilename: (...args: unknown[]) => mockGenerateTaskFilename(...args),
34
+ }));
35
+
36
+ vi.mock('../../../src/errors/index.js', () => {
37
+ class ClawtError extends Error {
38
+ public readonly exitCode: number;
39
+ constructor(message: string, exitCode: number = 1) {
40
+ super(message);
41
+ this.name = 'ClawtError';
42
+ this.exitCode = exitCode;
43
+ }
44
+ }
45
+ return { ClawtError };
46
+ });
47
+
48
+ import { registerTasksCommand } from '../../../src/commands/tasks.js';
49
+ import { printSuccess, printHint, ensureDir } from '../../../src/utils/index.js';
50
+ import { ClawtError } from '../../../src/errors/index.js';
51
+
52
+ const mockedPrintSuccess = vi.mocked(printSuccess);
53
+ const mockedPrintHint = vi.mocked(printHint);
54
+ const mockedEnsureDir = vi.mocked(ensureDir);
55
+
56
+ beforeEach(() => {
57
+ mockExistsSync.mockReset();
58
+ mockWriteFileSync.mockReset();
59
+ mockGenerateTaskFilename.mockReset();
60
+ mockGenerateTaskFilename.mockReturnValue('clawt-tasks-2026-01-01-00-00-00.md');
61
+ mockedPrintSuccess.mockReset();
62
+ mockedPrintHint.mockReset();
63
+ mockedEnsureDir.mockReset();
64
+ });
65
+
66
+ describe('registerTasksCommand', () => {
67
+ it('注册 tasks 命令', () => {
68
+ const program = new Command();
69
+ registerTasksCommand(program);
70
+ const cmd = program.commands.find((c) => c.name() === 'tasks');
71
+ expect(cmd).toBeDefined();
72
+ });
73
+
74
+ it('注册 tasks init 子命令', () => {
75
+ const program = new Command();
76
+ registerTasksCommand(program);
77
+ const taskCmd = program.commands.find((c) => c.name() === 'tasks');
78
+ const initCmd = taskCmd?.commands.find((c) => c.name() === 'init');
79
+ expect(initCmd).toBeDefined();
80
+ });
81
+ });
82
+
83
+ describe('handleTasksInit', () => {
84
+ it('默认路径生成带时间戳的文件', async () => {
85
+ mockExistsSync.mockReturnValue(false);
86
+
87
+ const program = new Command();
88
+ program.exitOverride();
89
+ registerTasksCommand(program);
90
+ await program.parseAsync(['tasks', 'init'], { from: 'user' });
91
+
92
+ expect(mockGenerateTaskFilename).toHaveBeenCalledWith('clawt-tasks');
93
+ // 默认路径应输出到 clawt/tasks/ 目录下
94
+ expect(mockWriteFileSync).toHaveBeenCalledWith(
95
+ expect.stringContaining('clawt/tasks/clawt-tasks-2026-01-01-00-00-00.md'),
96
+ '# 模板内容',
97
+ 'utf-8',
98
+ );
99
+ expect(mockedPrintSuccess).toHaveBeenCalledWith(
100
+ expect.stringContaining('clawt/tasks/clawt-tasks-2026-01-01-00-00-00.md'),
101
+ );
102
+ expect(mockedPrintHint).toHaveBeenCalledWith(
103
+ expect.stringContaining('clawt/tasks/clawt-tasks-2026-01-01-00-00-00.md'),
104
+ );
105
+ });
106
+
107
+ it('指定路径生成自定义文件', async () => {
108
+ mockExistsSync.mockReturnValue(false);
109
+
110
+ const program = new Command();
111
+ program.exitOverride();
112
+ registerTasksCommand(program);
113
+ await program.parseAsync(['tasks', 'init', 'my-tasks.md'], { from: 'user' });
114
+
115
+ expect(mockGenerateTaskFilename).not.toHaveBeenCalled();
116
+ expect(mockWriteFileSync).toHaveBeenCalledWith(
117
+ expect.stringContaining('my-tasks.md'),
118
+ '# 模板内容',
119
+ 'utf-8',
120
+ );
121
+ expect(mockedPrintSuccess).toHaveBeenCalledWith(
122
+ expect.stringContaining('my-tasks.md'),
123
+ );
124
+ });
125
+
126
+ it('文件已存在时抛出 ClawtError', async () => {
127
+ mockExistsSync.mockReturnValue(true);
128
+
129
+ const program = new Command();
130
+ program.exitOverride();
131
+ registerTasksCommand(program);
132
+
133
+ await expect(
134
+ program.parseAsync(['tasks', 'init'], { from: 'user' }),
135
+ ).rejects.toThrow(ClawtError);
136
+
137
+ expect(mockWriteFileSync).not.toHaveBeenCalled();
138
+ });
139
+
140
+ it('父目录不存在时自动创建', async () => {
141
+ mockExistsSync.mockReturnValue(false);
142
+
143
+ const program = new Command();
144
+ program.exitOverride();
145
+ registerTasksCommand(program);
146
+ await program.parseAsync(['tasks', 'init', 'sub/dir/tasks.md'], { from: 'user' });
147
+
148
+ expect(mockedEnsureDir).toHaveBeenCalledWith(
149
+ expect.stringContaining('sub/dir'),
150
+ );
151
+ expect(mockWriteFileSync).toHaveBeenCalled();
152
+ });
153
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import { formatWorktreeStatus, printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, isWorktreeIdle, formatDuration, formatRelativeTime, formatDiskSize, formatLocalISOString } from '../../../src/utils/formatter.js';
2
+ import { formatWorktreeStatus, printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, isWorktreeIdle, formatDuration, formatRelativeTime, formatDiskSize, formatLocalISOString, generateTaskFilename } from '../../../src/utils/formatter.js';
3
3
  import { createWorktreeStatus } from '../../helpers/fixtures.js';
4
4
 
5
5
  describe('formatWorktreeStatus', () => {
@@ -267,3 +267,28 @@ describe('formatLocalISOString', () => {
267
267
  expect(result).toContain('.123');
268
268
  });
269
269
  });
270
+
271
+ describe('generateTaskFilename', () => {
272
+ it('生成格式为 prefix-YYYY-MM-DD-HH-mm-ss.md 的文件名', () => {
273
+ const result = generateTaskFilename('clawt-tasks');
274
+ expect(result).toMatch(/^clawt-tasks-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.md$/);
275
+ });
276
+
277
+ it('使用自定义前缀', () => {
278
+ const result = generateTaskFilename('my-prefix');
279
+ expect(result).toMatch(/^my-prefix-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.md$/);
280
+ });
281
+
282
+ it('文件名以 .md 结尾', () => {
283
+ const result = generateTaskFilename('test');
284
+ expect(result).toMatch(/\.md$/);
285
+ });
286
+
287
+ it('时间戳各段使用两位数字(年份除外)', () => {
288
+ vi.useFakeTimers();
289
+ vi.setSystemTime(new Date('2026-01-05T03:07:09'));
290
+ const result = generateTaskFilename('clawt-tasks');
291
+ expect(result).toBe('clawt-tasks-2026-01-05-03-07-09.md');
292
+ vi.useRealTimers();
293
+ });
294
+ });