hamster-wheel-cli 0.1.0 → 0.2.0-beta.2
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/CHANGELOG.md +31 -1
- package/README.md +36 -0
- package/dist/cli.js +1917 -309
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +1917 -309
- package/dist/index.js.map +1 -1
- package/docs/ai-workflow.md +27 -36
- package/package.json +1 -1
- package/src/ai.ts +346 -1
- package/src/alias-viewer.ts +221 -0
- package/src/cli.ts +480 -29
- package/src/config.ts +6 -2
- package/src/gh.ts +20 -0
- package/src/git.ts +38 -3
- package/src/global-config.ts +149 -11
- package/src/log-tailer.ts +103 -0
- package/src/logs-viewer.ts +33 -11
- package/src/logs.ts +1 -0
- package/src/loop.ts +458 -120
- package/src/monitor.ts +240 -23
- package/src/multi-task.ts +117 -0
- package/src/plan.ts +61 -0
- package/src/quality.ts +48 -0
- package/src/runtime-tracker.ts +2 -1
- package/src/types.ts +23 -0
- package/tests/branch-name.test.ts +28 -0
- package/tests/e2e/cli.e2e.test.ts +41 -0
- package/tests/global-config.test.ts +52 -1
- package/tests/monitor.test.ts +17 -0
- package/tests/multi-task.test.ts +77 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { parseBranchName } from '../src/ai';
|
|
4
|
+
|
|
5
|
+
test('parseBranchName 规范化 JSON 分支名', () => {
|
|
6
|
+
const output = '{"branch":"feat/Add new_feature"}';
|
|
7
|
+
assert.equal(parseBranchName(output), 'feat/add-new-feature');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('parseBranchName 支持类型别名', () => {
|
|
11
|
+
const output = '{"branch":"feature/new-flow"}';
|
|
12
|
+
assert.equal(parseBranchName(output), 'feat/new-flow');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('parseBranchName 支持文本格式', () => {
|
|
16
|
+
const output = '分支名: fix/bug-123';
|
|
17
|
+
assert.equal(parseBranchName(output), 'fix/bug-123');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('parseBranchName 拒绝非法类型', () => {
|
|
21
|
+
const output = '{"branch":"release/prepare"}';
|
|
22
|
+
assert.equal(parseBranchName(output), null);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('parseBranchName 拒绝过短 slug', () => {
|
|
26
|
+
const output = '{"branch":"fix/a"}';
|
|
27
|
+
assert.equal(parseBranchName(output), null);
|
|
28
|
+
});
|
|
@@ -22,6 +22,7 @@ test('CLI 帮助信息可正常输出', async () => {
|
|
|
22
22
|
assert.ok(stdout.includes('--skip-install'));
|
|
23
23
|
assert.ok(stdout.includes('--webhook'));
|
|
24
24
|
assert.ok(stdout.includes('--webhook-timeout'));
|
|
25
|
+
assert.ok(stdout.includes('--multi-task-mode'));
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
test('CLI monitor 帮助信息可正常输出', async () => {
|
|
@@ -35,6 +36,7 @@ test('CLI monitor 帮助信息可正常输出', async () => {
|
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
assert.ok(stdout.includes('Usage: wheel-ai monitor'));
|
|
39
|
+
assert.ok(stdout.includes('t 终止任务'));
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
test('CLI monitor 在非 TTY 下输出提示', async () => {
|
|
@@ -75,3 +77,42 @@ test('CLI logs 在非 TTY 下输出提示', async () => {
|
|
|
75
77
|
|
|
76
78
|
assert.ok(stdout.includes('当前终端不支持交互式 logs。'));
|
|
77
79
|
});
|
|
80
|
+
|
|
81
|
+
test('CLI set alias 帮助信息可正常输出', async () => {
|
|
82
|
+
const execFileAsync = promisify(execFile);
|
|
83
|
+
const cliPath = path.join(process.cwd(), 'src', 'cli.ts');
|
|
84
|
+
const { stdout } = await execFileAsync('node', ['--require', 'ts-node/register', cliPath, 'set', 'alias', '--help'], {
|
|
85
|
+
env: {
|
|
86
|
+
...process.env,
|
|
87
|
+
FORCE_COLOR: '0'
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
assert.ok(stdout.includes('Usage: wheel-ai set alias'));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('CLI alias 帮助信息可正常输出', async () => {
|
|
95
|
+
const execFileAsync = promisify(execFile);
|
|
96
|
+
const cliPath = path.join(process.cwd(), 'src', 'cli.ts');
|
|
97
|
+
const { stdout } = await execFileAsync('node', ['--require', 'ts-node/register', cliPath, 'alias', '--help'], {
|
|
98
|
+
env: {
|
|
99
|
+
...process.env,
|
|
100
|
+
FORCE_COLOR: '0'
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
assert.ok(stdout.includes('Usage: wheel-ai alias'));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('CLI alias 在非 TTY 下输出提示', async () => {
|
|
108
|
+
const execFileAsync = promisify(execFile);
|
|
109
|
+
const cliPath = path.join(process.cwd(), 'src', 'cli.ts');
|
|
110
|
+
const { stdout } = await execFileAsync('node', ['--require', 'ts-node/register', cliPath, 'alias'], {
|
|
111
|
+
env: {
|
|
112
|
+
...process.env,
|
|
113
|
+
FORCE_COLOR: '0'
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assert.ok(stdout.includes('当前终端不支持交互式 alias。'));
|
|
118
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { test } from 'node:test';
|
|
3
|
-
import { applyShortcutArgv, parseGlobalConfig, splitCommandArgs } from '../src/global-config';
|
|
3
|
+
import { applyShortcutArgv, parseAliasEntries, parseGlobalConfig, splitCommandArgs, updateAliasContent } from '../src/global-config';
|
|
4
4
|
|
|
5
5
|
test('parseGlobalConfig 读取 shortcut 配置', () => {
|
|
6
6
|
const content = `
|
|
@@ -50,3 +50,54 @@ command = "run --task \\\"demo\\\" --run-tests"
|
|
|
50
50
|
'--run-e2e'
|
|
51
51
|
]);
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
test('parseAliasEntries 读取 alias 并附带 shortcut', () => {
|
|
55
|
+
const content = `
|
|
56
|
+
[alias]
|
|
57
|
+
daily = "--task \\"补充文档\\""
|
|
58
|
+
weekly = "run --task \\"补充测试\\" --run-tests"
|
|
59
|
+
|
|
60
|
+
[shortcut]
|
|
61
|
+
name = "quick"
|
|
62
|
+
command = "--run-e2e"
|
|
63
|
+
`;
|
|
64
|
+
const entries = parseAliasEntries(content);
|
|
65
|
+
assert.deepEqual(entries, [
|
|
66
|
+
{
|
|
67
|
+
name: 'daily',
|
|
68
|
+
command: '--task "补充文档"',
|
|
69
|
+
source: 'alias'
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'weekly',
|
|
73
|
+
command: 'run --task "补充测试" --run-tests',
|
|
74
|
+
source: 'alias'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'quick',
|
|
78
|
+
command: '--run-e2e',
|
|
79
|
+
source: 'shortcut'
|
|
80
|
+
}
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('updateAliasContent 会在空文件中写入 alias', () => {
|
|
85
|
+
const result = updateAliasContent('', 'daily', '--task "补充文档"');
|
|
86
|
+
assert.ok(result.includes('[alias]'));
|
|
87
|
+
assert.ok(result.includes('daily = "--task \\"补充文档\\""'));
|
|
88
|
+
assert.ok(result.endsWith('\n'));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('updateAliasContent 会更新已有 alias 并保留其他配置', () => {
|
|
92
|
+
const content = `
|
|
93
|
+
[alias]
|
|
94
|
+
daily = "--task \\"旧命令\\""
|
|
95
|
+
|
|
96
|
+
[shortcut]
|
|
97
|
+
name = "quick"
|
|
98
|
+
command = "--run-e2e"
|
|
99
|
+
`;
|
|
100
|
+
const result = updateAliasContent(content, 'daily', '--task "新命令"');
|
|
101
|
+
assert.ok(result.includes('daily = "--task \\"新命令\\""'));
|
|
102
|
+
assert.ok(result.includes('[shortcut]'));
|
|
103
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { buildConfirmDialogLines, resolveTerminationTarget } from '../src/monitor';
|
|
4
|
+
|
|
5
|
+
test('resolveTerminationTarget 在不同平台输出不同目标', () => {
|
|
6
|
+
assert.equal(resolveTerminationTarget(123, 'win32'), 123);
|
|
7
|
+
assert.equal(resolveTerminationTarget(123, 'linux'), -123);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('buildConfirmDialogLines 会生成有限宽度的确认框', () => {
|
|
11
|
+
const lines = buildConfirmDialogLines('alpha.log', 24);
|
|
12
|
+
assert.ok(lines.length >= 3);
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
assert.ok(line.length <= 24);
|
|
15
|
+
}
|
|
16
|
+
assert.ok(lines.some(line => line.includes('alpha.log')));
|
|
17
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { buildTaskPlans, normalizeTaskList, parseMultiTaskMode } from '../src/multi-task';
|
|
4
|
+
|
|
5
|
+
test('parseMultiTaskMode 支持默认与中文别名', () => {
|
|
6
|
+
assert.equal(parseMultiTaskMode(undefined), 'relay');
|
|
7
|
+
assert.equal(parseMultiTaskMode(''), 'relay');
|
|
8
|
+
assert.equal(parseMultiTaskMode('relay'), 'relay');
|
|
9
|
+
assert.equal(parseMultiTaskMode('串行执行'), 'serial');
|
|
10
|
+
assert.equal(parseMultiTaskMode('串行执行但是失败也继续'), 'serial-continue');
|
|
11
|
+
assert.equal(parseMultiTaskMode('并行执行'), 'parallel');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('parseMultiTaskMode 对未知模式抛错', () => {
|
|
15
|
+
assert.throws(() => parseMultiTaskMode('unknown-mode'));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('normalizeTaskList 过滤空白任务', () => {
|
|
19
|
+
assert.deepEqual(normalizeTaskList(undefined), []);
|
|
20
|
+
assert.deepEqual(normalizeTaskList(' '), []);
|
|
21
|
+
assert.deepEqual(normalizeTaskList('task-a'), ['task-a']);
|
|
22
|
+
assert.deepEqual(normalizeTaskList(['task-a', ' ', 'task-b ']), ['task-a', 'task-b']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('buildTaskPlans 在接力模式下使用上一任务分支作为基线', () => {
|
|
26
|
+
const plans = buildTaskPlans({
|
|
27
|
+
tasks: ['task-a', 'task-b', 'task-c'],
|
|
28
|
+
mode: 'relay',
|
|
29
|
+
useWorktree: true,
|
|
30
|
+
baseBranch: 'main',
|
|
31
|
+
branchInput: 'feat/demo',
|
|
32
|
+
worktreePath: '/tmp/wt/demo',
|
|
33
|
+
logFile: '/tmp/logs/demo.log'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
assert.equal(plans.length, 3);
|
|
37
|
+
assert.equal(plans[0].branchName, 'feat/demo');
|
|
38
|
+
assert.equal(plans[1].branchName, 'feat/demo-2');
|
|
39
|
+
assert.equal(plans[2].branchName, 'feat/demo-3');
|
|
40
|
+
assert.equal(plans[0].baseBranch, 'main');
|
|
41
|
+
assert.equal(plans[1].baseBranch, 'feat/demo');
|
|
42
|
+
assert.equal(plans[2].baseBranch, 'feat/demo-2');
|
|
43
|
+
assert.equal(plans[0].worktreePath, '/tmp/wt/demo');
|
|
44
|
+
assert.equal(plans[1].worktreePath, '/tmp/wt/demo-task-2');
|
|
45
|
+
assert.equal(plans[2].worktreePath, '/tmp/wt/demo-task-3');
|
|
46
|
+
assert.equal(plans[0].logFile, '/tmp/logs/demo.log');
|
|
47
|
+
assert.equal(plans[1].logFile, '/tmp/logs/demo-task-2.log');
|
|
48
|
+
assert.equal(plans[2].logFile, '/tmp/logs/demo-task-3.log');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('buildTaskPlans 在串行模式下保持基线分支不变', () => {
|
|
52
|
+
const plans = buildTaskPlans({
|
|
53
|
+
tasks: ['task-a', 'task-b'],
|
|
54
|
+
mode: 'serial',
|
|
55
|
+
useWorktree: true,
|
|
56
|
+
baseBranch: 'develop',
|
|
57
|
+
branchInput: 'feat/serial',
|
|
58
|
+
worktreePath: '/tmp/wt/serial',
|
|
59
|
+
logFile: '/tmp/logs/serial.log'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
assert.equal(plans[0].baseBranch, 'develop');
|
|
63
|
+
assert.equal(plans[1].baseBranch, 'develop');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('buildTaskPlans 在非 worktree 下保留传入分支名', () => {
|
|
67
|
+
const plans = buildTaskPlans({
|
|
68
|
+
tasks: ['task-a', 'task-b'],
|
|
69
|
+
mode: 'serial',
|
|
70
|
+
useWorktree: false,
|
|
71
|
+
baseBranch: 'main',
|
|
72
|
+
branchInput: 'feat/no-worktree'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
assert.equal(plans[0].branchName, 'feat/no-worktree');
|
|
76
|
+
assert.equal(plans[1].branchName, 'feat/no-worktree');
|
|
77
|
+
});
|