hamster-wheel-cli 0.2.0-beta.1 → 0.2.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/CHANGELOG.md +1 -0
- package/README.md +8 -1
- package/dist/cli.js +172 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +172 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +223 -6
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -2,7 +2,15 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { buildLoopConfig, CliOptions, defaultNotesPath, defaultPlanPath, defaultWorkflowDoc } from './config';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
applyShortcutArgv,
|
|
7
|
+
getGlobalConfigPath,
|
|
8
|
+
loadGlobalConfig,
|
|
9
|
+
normalizeAliasName,
|
|
10
|
+
parseAliasEntries,
|
|
11
|
+
splitCommandArgs,
|
|
12
|
+
upsertAliasEntry
|
|
13
|
+
} from './global-config';
|
|
6
14
|
import { getCurrentBranch } from './git';
|
|
7
15
|
import { buildAutoLogFilePath, formatCommandLine } from './logs';
|
|
8
16
|
import { runAliasViewer } from './alias-viewer';
|
|
@@ -16,6 +24,59 @@ import { resolvePath } from './utils';
|
|
|
16
24
|
|
|
17
25
|
const FOREGROUND_CHILD_ENV = 'WHEEL_AI_FOREGROUND_CHILD';
|
|
18
26
|
|
|
27
|
+
type OptionValueMode = 'none' | 'single' | 'variadic';
|
|
28
|
+
|
|
29
|
+
interface RunOptionSpec {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly flags: readonly string[];
|
|
32
|
+
readonly valueMode: OptionValueMode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ParsedArgSegment {
|
|
36
|
+
readonly name?: string;
|
|
37
|
+
readonly tokens: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const RUN_OPTION_SPECS: RunOptionSpec[] = [
|
|
41
|
+
{ name: 'task', flags: ['-t', '--task'], valueMode: 'single' },
|
|
42
|
+
{ name: 'iterations', flags: ['-i', '--iterations'], valueMode: 'single' },
|
|
43
|
+
{ name: 'ai-cli', flags: ['--ai-cli'], valueMode: 'single' },
|
|
44
|
+
{ name: 'ai-args', flags: ['--ai-args'], valueMode: 'variadic' },
|
|
45
|
+
{ name: 'ai-prompt-arg', flags: ['--ai-prompt-arg'], valueMode: 'single' },
|
|
46
|
+
{ name: 'notes-file', flags: ['--notes-file'], valueMode: 'single' },
|
|
47
|
+
{ name: 'plan-file', flags: ['--plan-file'], valueMode: 'single' },
|
|
48
|
+
{ name: 'workflow-doc', flags: ['--workflow-doc'], valueMode: 'single' },
|
|
49
|
+
{ name: 'worktree', flags: ['--worktree'], valueMode: 'none' },
|
|
50
|
+
{ name: 'branch', flags: ['--branch'], valueMode: 'single' },
|
|
51
|
+
{ name: 'worktree-path', flags: ['--worktree-path'], valueMode: 'single' },
|
|
52
|
+
{ name: 'base-branch', flags: ['--base-branch'], valueMode: 'single' },
|
|
53
|
+
{ name: 'skip-install', flags: ['--skip-install'], valueMode: 'none' },
|
|
54
|
+
{ name: 'run-tests', flags: ['--run-tests'], valueMode: 'none' },
|
|
55
|
+
{ name: 'run-e2e', flags: ['--run-e2e'], valueMode: 'none' },
|
|
56
|
+
{ name: 'unit-command', flags: ['--unit-command'], valueMode: 'single' },
|
|
57
|
+
{ name: 'e2e-command', flags: ['--e2e-command'], valueMode: 'single' },
|
|
58
|
+
{ name: 'auto-commit', flags: ['--auto-commit'], valueMode: 'none' },
|
|
59
|
+
{ name: 'auto-push', flags: ['--auto-push'], valueMode: 'none' },
|
|
60
|
+
{ name: 'pr', flags: ['--pr'], valueMode: 'none' },
|
|
61
|
+
{ name: 'pr-title', flags: ['--pr-title'], valueMode: 'single' },
|
|
62
|
+
{ name: 'pr-body', flags: ['--pr-body'], valueMode: 'single' },
|
|
63
|
+
{ name: 'draft', flags: ['--draft'], valueMode: 'none' },
|
|
64
|
+
{ name: 'reviewer', flags: ['--reviewer'], valueMode: 'variadic' },
|
|
65
|
+
{ name: 'auto-merge', flags: ['--auto-merge'], valueMode: 'none' },
|
|
66
|
+
{ name: 'webhook', flags: ['--webhook'], valueMode: 'single' },
|
|
67
|
+
{ name: 'webhook-timeout', flags: ['--webhook-timeout'], valueMode: 'single' },
|
|
68
|
+
{ name: 'multi-task-mode', flags: ['--multi-task-mode'], valueMode: 'single' },
|
|
69
|
+
{ name: 'stop-signal', flags: ['--stop-signal'], valueMode: 'single' },
|
|
70
|
+
{ name: 'log-file', flags: ['--log-file'], valueMode: 'single' },
|
|
71
|
+
{ name: 'background', flags: ['--background'], valueMode: 'none' },
|
|
72
|
+
{ name: 'verbose', flags: ['-v', '--verbose'], valueMode: 'none' },
|
|
73
|
+
{ name: 'skip-quality', flags: ['--skip-quality'], valueMode: 'none' }
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const RUN_OPTION_FLAG_MAP = new Map<string, RunOptionSpec>(
|
|
77
|
+
RUN_OPTION_SPECS.flatMap(spec => spec.flags.map(flag => [flag, spec] as const))
|
|
78
|
+
);
|
|
79
|
+
|
|
19
80
|
function parseInteger(value: string, defaultValue: number): number {
|
|
20
81
|
const parsed = Number.parseInt(value, 10);
|
|
21
82
|
if (Number.isNaN(parsed)) return defaultValue;
|
|
@@ -57,6 +118,116 @@ function extractAliasCommandArgs(argv: string[], name: string): string[] {
|
|
|
57
118
|
return rest;
|
|
58
119
|
}
|
|
59
120
|
|
|
121
|
+
function isAliasCommandToken(token: string): boolean {
|
|
122
|
+
return token === 'alias' || token === 'aliases';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function extractAliasRunArgs(argv: string[], name: string): string[] {
|
|
126
|
+
const args = argv.slice(2);
|
|
127
|
+
const start = args.findIndex(
|
|
128
|
+
(arg, index) => isAliasCommandToken(arg) && args[index + 1] === 'run' && args[index + 2] === name
|
|
129
|
+
);
|
|
130
|
+
if (start < 0) return [];
|
|
131
|
+
const rest = args.slice(start + 3);
|
|
132
|
+
if (rest[0] === '--') return rest.slice(1);
|
|
133
|
+
return rest;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeAliasCommandArgs(args: string[]): string[] {
|
|
137
|
+
let start = 0;
|
|
138
|
+
if (args[start] === 'wheel-ai') {
|
|
139
|
+
start += 1;
|
|
140
|
+
}
|
|
141
|
+
if (args[start] === 'run') {
|
|
142
|
+
start += 1;
|
|
143
|
+
}
|
|
144
|
+
return args.slice(start);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveRunOptionSpec(token: string): { spec: RunOptionSpec; inlineValue?: string } | null {
|
|
148
|
+
const equalIndex = token.indexOf('=');
|
|
149
|
+
const flag = equalIndex > 0 ? token.slice(0, equalIndex) : token;
|
|
150
|
+
const spec = RUN_OPTION_FLAG_MAP.get(flag);
|
|
151
|
+
if (!spec) return null;
|
|
152
|
+
if (equalIndex > 0) {
|
|
153
|
+
return { spec, inlineValue: token.slice(equalIndex + 1) };
|
|
154
|
+
}
|
|
155
|
+
return { spec };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseArgSegments(tokens: string[]): ParsedArgSegment[] {
|
|
159
|
+
const segments: ParsedArgSegment[] = [];
|
|
160
|
+
let index = 0;
|
|
161
|
+
while (index < tokens.length) {
|
|
162
|
+
const token = tokens[index];
|
|
163
|
+
if (token === '--') {
|
|
164
|
+
segments.push({ tokens: tokens.slice(index) });
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const match = resolveRunOptionSpec(token);
|
|
169
|
+
if (!match) {
|
|
170
|
+
segments.push({ tokens: [token] });
|
|
171
|
+
index += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (match.inlineValue !== undefined) {
|
|
176
|
+
segments.push({ name: match.spec.name, tokens: [token] });
|
|
177
|
+
index += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (match.spec.valueMode === 'none') {
|
|
182
|
+
segments.push({ name: match.spec.name, tokens: [token] });
|
|
183
|
+
index += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (match.spec.valueMode === 'single') {
|
|
188
|
+
const next = tokens[index + 1];
|
|
189
|
+
if (next !== undefined) {
|
|
190
|
+
segments.push({ name: match.spec.name, tokens: [token, next] });
|
|
191
|
+
index += 2;
|
|
192
|
+
} else {
|
|
193
|
+
segments.push({ name: match.spec.name, tokens: [token] });
|
|
194
|
+
index += 1;
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const values: string[] = [];
|
|
200
|
+
let cursor = index + 1;
|
|
201
|
+
while (cursor < tokens.length) {
|
|
202
|
+
const next = tokens[cursor];
|
|
203
|
+
if (next === '--') break;
|
|
204
|
+
const nextMatch = resolveRunOptionSpec(next);
|
|
205
|
+
if (nextMatch) break;
|
|
206
|
+
values.push(next);
|
|
207
|
+
cursor += 1;
|
|
208
|
+
}
|
|
209
|
+
segments.push({ name: match.spec.name, tokens: [token, ...values] });
|
|
210
|
+
index = cursor;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return segments;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function mergeAliasCommandArgs(aliasTokens: string[], additionTokens: string[]): string[] {
|
|
217
|
+
const aliasSegments = parseArgSegments(aliasTokens);
|
|
218
|
+
const additionSegments = parseArgSegments(additionTokens);
|
|
219
|
+
const overrideNames = new Set(
|
|
220
|
+
additionSegments.flatMap(segment => (segment.name ? [segment.name] : []))
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const merged = [
|
|
224
|
+
...aliasSegments.filter(segment => !segment.name || !overrideNames.has(segment.name)),
|
|
225
|
+
...additionSegments
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
return merged.flatMap(segment => segment.tokens);
|
|
229
|
+
}
|
|
230
|
+
|
|
60
231
|
async function runForegroundWithDetach(options: {
|
|
61
232
|
argv: string[];
|
|
62
233
|
logFile: string;
|
|
@@ -161,7 +332,11 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
161
332
|
program
|
|
162
333
|
.name('wheel-ai')
|
|
163
334
|
.description('基于 AI CLI 的持续迭代开发工具')
|
|
164
|
-
.version('
|
|
335
|
+
.version('0.2.0');
|
|
336
|
+
program.addHelpText(
|
|
337
|
+
'after',
|
|
338
|
+
'\n别名执行:\n wheel-ai alias run <alias> <addition...>\n 追加命令与 alias 重叠时,以追加为准。\n'
|
|
339
|
+
);
|
|
165
340
|
|
|
166
341
|
program
|
|
167
342
|
.command('run')
|
|
@@ -403,14 +578,56 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
403
578
|
console.log(`已写入 alias:${normalized}`);
|
|
404
579
|
});
|
|
405
580
|
|
|
406
|
-
program
|
|
581
|
+
const aliasCommand = program
|
|
407
582
|
.command('alias')
|
|
408
583
|
.alias('aliases')
|
|
409
|
-
.description('浏览全局 alias
|
|
410
|
-
|
|
411
|
-
|
|
584
|
+
.description('浏览全局 alias 配置(alias run 可执行并追加命令)');
|
|
585
|
+
|
|
586
|
+
aliasCommand
|
|
587
|
+
.command('run <name> [addition...]')
|
|
588
|
+
.description('执行 alias 并追加命令')
|
|
589
|
+
.allowUnknownOption(true)
|
|
590
|
+
.allowExcessArguments(true)
|
|
591
|
+
.action(async (name: string) => {
|
|
592
|
+
const normalized = normalizeAliasName(name);
|
|
593
|
+
if (!normalized) {
|
|
594
|
+
throw new Error('alias 名称不能为空且不能包含空白字符');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const filePath = getGlobalConfigPath();
|
|
598
|
+
const exists = await fs.pathExists(filePath);
|
|
599
|
+
if (!exists) {
|
|
600
|
+
throw new Error(`未找到 alias 配置文件:${filePath}`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
604
|
+
const entries = parseAliasEntries(content);
|
|
605
|
+
const entry = entries.find(item => item.name === normalized);
|
|
606
|
+
if (!entry) {
|
|
607
|
+
throw new Error(`未找到 alias:${normalized}`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const aliasTokens = normalizeAliasCommandArgs(splitCommandArgs(entry.command));
|
|
611
|
+
const additionTokens = extractAliasRunArgs(effectiveArgv, normalized);
|
|
612
|
+
const mergedTokens = mergeAliasCommandArgs(aliasTokens, additionTokens);
|
|
613
|
+
if (mergedTokens.length === 0) {
|
|
614
|
+
throw new Error('alias 命令不能为空');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const nextArgv = [process.argv[0], process.argv[1], 'run', ...mergedTokens];
|
|
618
|
+
const originalArgv = process.argv;
|
|
619
|
+
process.argv = nextArgv;
|
|
620
|
+
try {
|
|
621
|
+
await runCli(nextArgv);
|
|
622
|
+
} finally {
|
|
623
|
+
process.argv = originalArgv;
|
|
624
|
+
}
|
|
412
625
|
});
|
|
413
626
|
|
|
627
|
+
aliasCommand.action(async () => {
|
|
628
|
+
await runAliasViewer();
|
|
629
|
+
});
|
|
630
|
+
|
|
414
631
|
await program.parseAsync(effectiveArgv);
|
|
415
632
|
}
|
|
416
633
|
|