clawt 3.7.1 → 3.8.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/.clawt/postCreate.sh +2 -0
- package/CLAUDE.md +10 -0
- package/README.md +19 -2
- package/dist/index.js +172 -26
- package/dist/postinstall.js +30 -1
- package/docs/create.md +6 -2
- package/docs/init.md +2 -2
- package/docs/post-create-hook.md +142 -0
- package/docs/project-config.md +9 -2
- package/docs/run.md +10 -6
- package/docs/spec.md +4 -1
- package/docs/tasks.md +4 -4
- package/package.json +1 -1
- package/src/commands/create.ts +5 -0
- package/src/commands/run.ts +12 -0
- package/src/commands/tasks.ts +1 -1
- package/src/constants/config.ts +1 -1
- package/src/constants/messages/index.ts +2 -0
- package/src/constants/messages/post-create.ts +29 -0
- package/src/constants/project-config.ts +4 -0
- package/src/constants/tasks-template.ts +1 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/post-create.ts +198 -0
- package/src/types/command.ts +4 -0
- package/src/types/index.ts +1 -0
- package/src/types/postCreateHook.ts +24 -0
- package/src/types/projectConfig.ts +2 -0
- package/src/utils/claude.ts +2 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/task-executor.ts +5 -1
- package/tests/unit/commands/create.test.ts +1 -0
- package/tests/unit/commands/run.test.ts +1 -0
- package/tests/unit/commands/tasks.test.ts +5 -5
- package/tests/unit/constants/messages-post-create.test.ts +112 -0
- package/tests/unit/hooks/post-create.test.ts +434 -0
- package/tests/unit/utils/claude.test.ts +76 -1
package/src/types/command.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface CreateOptions {
|
|
|
4
4
|
branch: string;
|
|
5
5
|
/** 创建数量(Commander 传入为字符串),默认 '1' */
|
|
6
6
|
number: string;
|
|
7
|
+
/** 是否执行 postCreate hook,--no-post-create 时为 false */
|
|
8
|
+
postCreate?: boolean;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
/** run 命令选项 */
|
|
@@ -18,6 +20,8 @@ export interface RunOptions {
|
|
|
18
20
|
file?: string;
|
|
19
21
|
/** 预览模式,仅展示任务计划不实际执行 */
|
|
20
22
|
dryRun?: boolean;
|
|
23
|
+
/** 是否执行 postCreate hook,--no-post-create 时为 false */
|
|
24
|
+
postCreate?: boolean;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/** validate 命令选项 */
|
package/src/types/index.ts
CHANGED
|
@@ -7,3 +7,4 @@ export type { WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, Snapshot
|
|
|
7
7
|
export type { TaskFileEntry, ParseTaskFileOptions } from './taskFile.js';
|
|
8
8
|
export type { ProjectOverview, ProjectWorktreeDetail, ProjectDetailResult, ProjectsOverviewResult } from './project.js';
|
|
9
9
|
export type { ProjectConfig, ProjectConfigItemDefinition, ProjectConfigDefinitions } from './projectConfig.js';
|
|
10
|
+
export type { PostCreateHookSource, PostCreateHookResult, ResolvedHook } from './postCreateHook.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** postCreate hook 的来源 */
|
|
2
|
+
export type PostCreateHookSource = 'projectConfig' | 'postCreateScript';
|
|
3
|
+
|
|
4
|
+
/** 单个 worktree 的 hook 执行结果 */
|
|
5
|
+
export interface PostCreateHookResult {
|
|
6
|
+
/** worktree 绝对路径 */
|
|
7
|
+
worktreePath: string;
|
|
8
|
+
/** 分支名 */
|
|
9
|
+
branch: string;
|
|
10
|
+
/** 是否执行成功 */
|
|
11
|
+
success: boolean;
|
|
12
|
+
/** hook 来源 */
|
|
13
|
+
source: PostCreateHookSource;
|
|
14
|
+
/** 失败时的错误信息 */
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** hook 解析结果 */
|
|
19
|
+
export interface ResolvedHook {
|
|
20
|
+
/** 待执行的命令 */
|
|
21
|
+
command: string;
|
|
22
|
+
/** hook 来源 */
|
|
23
|
+
source: PostCreateHookSource;
|
|
24
|
+
}
|
package/src/utils/claude.ts
CHANGED
|
@@ -108,9 +108,10 @@ function escapeShellSingleQuote(str: string): string {
|
|
|
108
108
|
*/
|
|
109
109
|
export function buildClaudeCommand(worktree: WorktreeInfo, hasPreviousSession: boolean): string {
|
|
110
110
|
const commandStr = getConfigValue('claudeCodeCommand');
|
|
111
|
+
const systemPrompt = APPEND_SYSTEM_PROMPT;
|
|
111
112
|
|
|
112
113
|
const escapedPath = escapeShellSingleQuote(worktree.path);
|
|
113
|
-
const escapedPrompt = escapeShellSingleQuote(
|
|
114
|
+
const escapedPrompt = escapeShellSingleQuote(systemPrompt);
|
|
114
115
|
const continueFlag = hasPreviousSession ? ' --continue' : '';
|
|
115
116
|
|
|
116
117
|
return `cd '${escapedPath}' && ${commandStr} --append-system-prompt '${escapedPrompt}'${continueFlag}`;
|
package/src/utils/index.ts
CHANGED
|
@@ -89,4 +89,5 @@ export { InteractivePanel } from './interactive-panel.js';
|
|
|
89
89
|
export { buildPanelFrame, buildGroupedWorktreeLines, buildDisplayOrder, renderDateSeparator, renderWorktreeBlock, renderSnapshotSummary, renderFooter, calculateVisibleRows } from './interactive-panel-render.js';
|
|
90
90
|
export type { PanelLine } from './interactive-panel-render.js';
|
|
91
91
|
export { buildConflictResolvePrompt, invokeClaudeForConflictResolve, resolveConflictsWithAI, determineConflictResolveMode, handleMergeConflict } from './conflict-resolver.js';
|
|
92
|
+
export { resolvePostCreateHook, executePostCreateHooks, runPostCreateHooks } from '../hooks/index.js';
|
|
92
93
|
|
|
@@ -5,6 +5,7 @@ import type { ClaudeCodeResult, TaskResult, TaskSummary, WorktreeInfo } from '..
|
|
|
5
5
|
import { spawnProcess, killAllChildProcesses } from './shell.js';
|
|
6
6
|
import { cleanupWorktrees } from './worktree.js';
|
|
7
7
|
import { getConfigValue } from './config.js';
|
|
8
|
+
import { APPEND_SYSTEM_PROMPT } from '../constants/index.js';
|
|
8
9
|
import { printSuccess, printWarning, printInfo, printDoubleSeparator, confirmAction } from './formatter.js';
|
|
9
10
|
import { ProgressRenderer } from './progress.js';
|
|
10
11
|
import { createLineBuffer, parseStreamLine, parseStreamEvent, truncateText } from './stream-parser.js';
|
|
@@ -34,8 +35,11 @@ type ActivityCallback = (activityText: string) => void;
|
|
|
34
35
|
* @returns {ClaudeTaskHandle} 包含子进程引用和结果 Promise
|
|
35
36
|
*/
|
|
36
37
|
function executeClaudeTask(worktree: WorktreeInfo, task: string, onActivity?: ActivityCallback, continueSession?: boolean): ClaudeTaskHandle {
|
|
38
|
+
// 使用统一的系统提示常量
|
|
39
|
+
const systemPrompt = APPEND_SYSTEM_PROMPT;
|
|
40
|
+
|
|
37
41
|
// 旧版使用 --output-format json,现改为 stream-json --verbose 以支持实时活动信息
|
|
38
|
-
const args = ['-p', task, '--output-format', 'stream-json', '--verbose', '--permission-mode', 'bypassPermissions'];
|
|
42
|
+
const args = ['-p', task, '--output-format', 'stream-json', '--verbose', '--permission-mode', 'bypassPermissions', '--append-system-prompt', systemPrompt];
|
|
39
43
|
|
|
40
44
|
// 追问模式:追加 --continue 继续该目录下最新会话
|
|
41
45
|
if (continueSession) {
|
|
@@ -39,6 +39,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
39
39
|
printSeparator: vi.fn(),
|
|
40
40
|
guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
41
41
|
guardMainWorkBranchExists: vi.fn(),
|
|
42
|
+
runPostCreateHooks: vi.fn().mockReturnValue(false),
|
|
42
43
|
}));
|
|
43
44
|
|
|
44
45
|
import { registerCreateCommand } from '../../../src/commands/create.js';
|
|
@@ -79,6 +79,7 @@ vi.mock('../../../src/utils/index.js', async (importOriginal) => {
|
|
|
79
79
|
ensureOnMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
80
80
|
guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
81
81
|
guardMainWorkBranchExists: vi.fn(),
|
|
82
|
+
runPostCreateHooks: vi.fn(),
|
|
82
83
|
};
|
|
83
84
|
});
|
|
84
85
|
|
|
@@ -11,7 +11,7 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
11
11
|
TASK_INIT_SUCCESS: (path: string) => `✓ 任务模板已生成: ${path}`,
|
|
12
12
|
TASK_INIT_HINT: (path: string) => `使用 clawt run -f ${path} 执行任务`,
|
|
13
13
|
},
|
|
14
|
-
TASK_TEMPLATE_OUTPUT_DIR: 'clawt/tasks',
|
|
14
|
+
TASK_TEMPLATE_OUTPUT_DIR: '.clawt/tasks',
|
|
15
15
|
TASK_TEMPLATE_FILENAME_PREFIX: 'clawt-tasks',
|
|
16
16
|
TASK_TEMPLATE_CONTENT: '# 模板内容',
|
|
17
17
|
}));
|
|
@@ -90,17 +90,17 @@ describe('handleTasksInit', () => {
|
|
|
90
90
|
await program.parseAsync(['tasks', 'init'], { from: 'user' });
|
|
91
91
|
|
|
92
92
|
expect(mockGenerateTaskFilename).toHaveBeenCalledWith('clawt-tasks');
|
|
93
|
-
// 默认路径应输出到 clawt/tasks/ 目录下
|
|
93
|
+
// 默认路径应输出到 .clawt/tasks/ 目录下
|
|
94
94
|
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
|
95
|
-
expect.stringContaining('clawt/tasks/clawt-tasks-2026-01-01-00-00-00.md'),
|
|
95
|
+
expect.stringContaining('.clawt/tasks/clawt-tasks-2026-01-01-00-00-00.md'),
|
|
96
96
|
'# 模板内容',
|
|
97
97
|
'utf-8',
|
|
98
98
|
);
|
|
99
99
|
expect(mockedPrintSuccess).toHaveBeenCalledWith(
|
|
100
|
-
expect.stringContaining('clawt/tasks/clawt-tasks-2026-01-01-00-00-00.md'),
|
|
100
|
+
expect.stringContaining('.clawt/tasks/clawt-tasks-2026-01-01-00-00-00.md'),
|
|
101
101
|
);
|
|
102
102
|
expect(mockedPrintHint).toHaveBeenCalledWith(
|
|
103
|
-
expect.stringContaining('clawt/tasks/clawt-tasks-2026-01-01-00-00-00.md'),
|
|
103
|
+
expect.stringContaining('.clawt/tasks/clawt-tasks-2026-01-01-00-00-00.md'),
|
|
104
104
|
);
|
|
105
105
|
});
|
|
106
106
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { POST_CREATE_MESSAGES } from '../../../src/constants/messages/post-create.js';
|
|
3
|
+
|
|
4
|
+
describe('POST_CREATE_MESSAGES', () => {
|
|
5
|
+
describe('纯字符串消息', () => {
|
|
6
|
+
it('HOOK_SKIPPED 包含 --no-post-create', () => {
|
|
7
|
+
expect(POST_CREATE_MESSAGES.HOOK_SKIPPED).toContain('--no-post-create');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('HOOK_SKIPPED 不包含旧的 --no-deps', () => {
|
|
11
|
+
expect(POST_CREATE_MESSAGES.HOOK_SKIPPED).not.toContain('--no-deps');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('HOOK_NOT_CONFIGURED 不包含"依赖"字样', () => {
|
|
15
|
+
expect(POST_CREATE_MESSAGES.HOOK_NOT_CONFIGURED).not.toContain('依赖');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('HOOK_NOT_CONFIGURED 包含"未配置"和"跳过"', () => {
|
|
19
|
+
expect(POST_CREATE_MESSAGES.HOOK_NOT_CONFIGURED).toContain('未配置');
|
|
20
|
+
expect(POST_CREATE_MESSAGES.HOOK_NOT_CONFIGURED).toContain('跳过');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('模板函数消息', () => {
|
|
25
|
+
it('HOOK_SOURCE_INFO 包含来源描述', () => {
|
|
26
|
+
const result = POST_CREATE_MESSAGES.HOOK_SOURCE_INFO('项目配置 (postCreate)');
|
|
27
|
+
expect(result).toContain('项目配置 (postCreate)');
|
|
28
|
+
expect(result).toContain('postCreate hook 来源');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('HOOK_EXECUTING 包含分支名和命令', () => {
|
|
32
|
+
const result = POST_CREATE_MESSAGES.HOOK_EXECUTING('feat-login', 'npm install');
|
|
33
|
+
expect(result).toContain('feat-login');
|
|
34
|
+
expect(result).toContain('npm install');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('HOOK_SUCCESS 包含分支名', () => {
|
|
38
|
+
const result = POST_CREATE_MESSAGES.HOOK_SUCCESS('feat-login');
|
|
39
|
+
expect(result).toContain('feat-login');
|
|
40
|
+
expect(result).toContain('成功');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('HOOK_FAILED 包含分支名和错误信息', () => {
|
|
44
|
+
const result = POST_CREATE_MESSAGES.HOOK_FAILED('feat-login', '命令退出码: 1');
|
|
45
|
+
expect(result).toContain('feat-login');
|
|
46
|
+
expect(result).toContain('命令退出码: 1');
|
|
47
|
+
expect(result).toContain('失败');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('HOOK_SUMMARY 包含成功和失败计数', () => {
|
|
51
|
+
const result = POST_CREATE_MESSAGES.HOOK_SUMMARY(3, 1);
|
|
52
|
+
expect(result).toContain('3');
|
|
53
|
+
expect(result).toContain('1');
|
|
54
|
+
expect(result).toContain('成功');
|
|
55
|
+
expect(result).toContain('失败');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('HOOK_SUMMARY 全部成功时失败数为 0', () => {
|
|
59
|
+
const result = POST_CREATE_MESSAGES.HOOK_SUMMARY(5, 0);
|
|
60
|
+
expect(result).toContain('5 成功');
|
|
61
|
+
expect(result).toContain('0 失败');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('POST_CREATE_SCRIPT_NOT_EXECUTABLE 包含路径和手动 chmod 提示', () => {
|
|
65
|
+
const result = POST_CREATE_MESSAGES.POST_CREATE_SCRIPT_NOT_EXECUTABLE('/repo/.clawt/postCreate.sh');
|
|
66
|
+
expect(result).toContain('/repo/.clawt/postCreate.sh');
|
|
67
|
+
expect(result).toContain('chmod +x');
|
|
68
|
+
expect(result).toContain('不可执行');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('POST_CREATE_SCRIPT_AUTO_CHMOD 包含路径和自动修复提示', () => {
|
|
72
|
+
const result = POST_CREATE_MESSAGES.POST_CREATE_SCRIPT_AUTO_CHMOD('/repo/.clawt/postCreate.sh');
|
|
73
|
+
expect(result).toContain('/repo/.clawt/postCreate.sh');
|
|
74
|
+
expect(result).toContain('已自动添加执行权限');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('HOOK_BACKGROUND_START 包含 worktree 数量和命令', () => {
|
|
78
|
+
const result = POST_CREATE_MESSAGES.HOOK_BACKGROUND_START(3, 'npm install');
|
|
79
|
+
expect(result).toContain('3');
|
|
80
|
+
expect(result).toContain('npm install');
|
|
81
|
+
expect(result).toContain('后台执行');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('HOOK_BACKGROUND_START 单个 worktree 时正确显示', () => {
|
|
85
|
+
const result = POST_CREATE_MESSAGES.HOOK_BACKGROUND_START(1, 'pnpm install');
|
|
86
|
+
expect(result).toContain('1 个 worktree');
|
|
87
|
+
expect(result).toContain('pnpm install');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('语义修正验证', () => {
|
|
92
|
+
it('不存在旧的 HOOK_SKIPPED_NO_DEPS 键', () => {
|
|
93
|
+
expect('HOOK_SKIPPED_NO_DEPS' in POST_CREATE_MESSAGES).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('不存在旧的 SETUP_SCRIPT_NOT_EXECUTABLE 键', () => {
|
|
97
|
+
expect('SETUP_SCRIPT_NOT_EXECUTABLE' in POST_CREATE_MESSAGES).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('存在新的 HOOK_SKIPPED 键', () => {
|
|
101
|
+
expect('HOOK_SKIPPED' in POST_CREATE_MESSAGES).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('存在新的 POST_CREATE_SCRIPT_NOT_EXECUTABLE 键', () => {
|
|
105
|
+
expect('POST_CREATE_SCRIPT_NOT_EXECUTABLE' in POST_CREATE_MESSAGES).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('存在新的 POST_CREATE_SCRIPT_AUTO_CHMOD 键', () => {
|
|
109
|
+
expect('POST_CREATE_SCRIPT_AUTO_CHMOD' in POST_CREATE_MESSAGES).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|