hamster-wheel-cli 0.2.0 → 0.3.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/CHANGELOG.md +30 -0
- package/README.md +55 -18
- package/dist/cli.js +567 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.js +567 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +376 -18
- package/src/global-config.ts +251 -0
- package/src/logs-viewer.ts +13 -1
- package/src/loop.ts +92 -12
- package/src/utils.ts +13 -0
- package/src/webhook.ts +54 -9
- package/tests/e2e/cli.e2e.test.ts +41 -3
- package/tests/global-config.test.ts +89 -1
- package/tests/webhook.test.ts +10 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hamster-wheel-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "基于 AI CLI 的持续迭代开发工具,封装工作流、git worktree 与 gh PR 协作",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"wheel-ai": "dist/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"repository": "git@github.com:wszxdhr/hamster-wheel-cli.git",
|
|
18
|
-
"author": "ZX <
|
|
18
|
+
"author": "ZX <x@z-x.vip>",
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"type": "commonjs",
|
|
21
21
|
"engines": {
|
package/src/cli.ts
CHANGED
|
@@ -7,10 +7,16 @@ import {
|
|
|
7
7
|
getGlobalConfigPath,
|
|
8
8
|
loadGlobalConfig,
|
|
9
9
|
normalizeAliasName,
|
|
10
|
+
normalizeAgentName,
|
|
10
11
|
parseAliasEntries,
|
|
12
|
+
parseAgentEntries,
|
|
11
13
|
splitCommandArgs,
|
|
14
|
+
removeAliasEntry,
|
|
15
|
+
removeAgentEntry,
|
|
16
|
+
upsertAgentEntry,
|
|
12
17
|
upsertAliasEntry
|
|
13
18
|
} from './global-config';
|
|
19
|
+
import type { AgentEntry, AliasEntry } from './global-config';
|
|
14
20
|
import { getCurrentBranch } from './git';
|
|
15
21
|
import { buildAutoLogFilePath, formatCommandLine } from './logs';
|
|
16
22
|
import { runAliasViewer } from './alias-viewer';
|
|
@@ -77,6 +83,9 @@ const RUN_OPTION_FLAG_MAP = new Map<string, RunOptionSpec>(
|
|
|
77
83
|
RUN_OPTION_SPECS.flatMap(spec => spec.flags.map(flag => [flag, spec] as const))
|
|
78
84
|
);
|
|
79
85
|
|
|
86
|
+
const USE_ALIAS_FLAG = '--use-alias';
|
|
87
|
+
const USE_AGENT_FLAG = '--use-agent';
|
|
88
|
+
|
|
80
89
|
function parseInteger(value: string, defaultValue: number): number {
|
|
81
90
|
const parsed = Number.parseInt(value, 10);
|
|
82
91
|
if (Number.isNaN(parsed)) return defaultValue;
|
|
@@ -111,7 +120,23 @@ function buildBackgroundArgs(argv: string[], logFile: string, branchName?: strin
|
|
|
111
120
|
|
|
112
121
|
function extractAliasCommandArgs(argv: string[], name: string): string[] {
|
|
113
122
|
const args = argv.slice(2);
|
|
114
|
-
const start = args.findIndex((arg, index) =>
|
|
123
|
+
const start = args.findIndex((arg, index) => {
|
|
124
|
+
const legacyMatch = arg === 'set' && args[index + 1] === 'alias' && args[index + 2] === name;
|
|
125
|
+
const aliasMatch =
|
|
126
|
+
isAliasCommandToken(arg) && args[index + 1] === 'set' && args[index + 2] === name;
|
|
127
|
+
return legacyMatch || aliasMatch;
|
|
128
|
+
});
|
|
129
|
+
if (start < 0) return [];
|
|
130
|
+
const rest = args.slice(start + 3);
|
|
131
|
+
if (rest[0] === '--') return rest.slice(1);
|
|
132
|
+
return rest;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function extractAgentCommandArgs(argv: string[], action: 'add' | 'set', name: string): string[] {
|
|
136
|
+
const args = argv.slice(2);
|
|
137
|
+
const start = args.findIndex(
|
|
138
|
+
(arg, index) => arg === 'agent' && args[index + 1] === action && args[index + 2] === name
|
|
139
|
+
);
|
|
115
140
|
if (start < 0) return [];
|
|
116
141
|
const rest = args.slice(start + 3);
|
|
117
142
|
if (rest[0] === '--') return rest.slice(1);
|
|
@@ -133,7 +158,7 @@ function extractAliasRunArgs(argv: string[], name: string): string[] {
|
|
|
133
158
|
return rest;
|
|
134
159
|
}
|
|
135
160
|
|
|
136
|
-
function
|
|
161
|
+
function normalizeRunCommandArgs(args: string[]): string[] {
|
|
137
162
|
let start = 0;
|
|
138
163
|
if (args[start] === 'wheel-ai') {
|
|
139
164
|
start += 1;
|
|
@@ -213,21 +238,168 @@ function parseArgSegments(tokens: string[]): ParsedArgSegment[] {
|
|
|
213
238
|
return segments;
|
|
214
239
|
}
|
|
215
240
|
|
|
216
|
-
|
|
217
|
-
|
|
241
|
+
// 按选项名合并 run 参数,同名选项以“后出现覆盖前出现”为准。
|
|
242
|
+
function mergeRunCommandArgs(baseTokens: string[], additionTokens: string[]): string[] {
|
|
243
|
+
const baseSegments = parseArgSegments(baseTokens);
|
|
218
244
|
const additionSegments = parseArgSegments(additionTokens);
|
|
219
245
|
const overrideNames = new Set(
|
|
220
246
|
additionSegments.flatMap(segment => (segment.name ? [segment.name] : []))
|
|
221
247
|
);
|
|
222
248
|
|
|
223
249
|
const merged = [
|
|
224
|
-
...
|
|
250
|
+
...baseSegments.filter(segment => !segment.name || !overrideNames.has(segment.name)),
|
|
225
251
|
...additionSegments
|
|
226
252
|
];
|
|
227
253
|
|
|
228
254
|
return merged.flatMap(segment => segment.tokens);
|
|
229
255
|
}
|
|
230
256
|
|
|
257
|
+
interface ExpandedRunTokens {
|
|
258
|
+
readonly tokens: string[];
|
|
259
|
+
readonly expanded: boolean;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface UseOptionMatch {
|
|
263
|
+
readonly type: 'alias' | 'agent';
|
|
264
|
+
readonly name: string;
|
|
265
|
+
readonly nextIndex: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function extractRunCommandArgs(argv: string[]): string[] {
|
|
269
|
+
const args = argv.slice(2);
|
|
270
|
+
const start = args.findIndex(arg => arg === 'run');
|
|
271
|
+
if (start < 0) return [];
|
|
272
|
+
return args.slice(start + 1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function parseUseOptionToken(token: string, flag: string): { matched: boolean; value?: string } {
|
|
276
|
+
if (token === flag) {
|
|
277
|
+
return { matched: true };
|
|
278
|
+
}
|
|
279
|
+
if (token.startsWith(`${flag}=`)) {
|
|
280
|
+
return { matched: true, value: token.slice(flag.length + 1) };
|
|
281
|
+
}
|
|
282
|
+
return { matched: false };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function resolveUseOption(tokens: string[], index: number): UseOptionMatch | null {
|
|
286
|
+
const token = tokens[index];
|
|
287
|
+
const aliasMatch = parseUseOptionToken(token, USE_ALIAS_FLAG);
|
|
288
|
+
if (aliasMatch.matched) {
|
|
289
|
+
const value = aliasMatch.value ?? tokens[index + 1];
|
|
290
|
+
if (!value) {
|
|
291
|
+
throw new Error(`${USE_ALIAS_FLAG} 需要提供名称`);
|
|
292
|
+
}
|
|
293
|
+
const nextIndex = aliasMatch.value ? index + 1 : index + 2;
|
|
294
|
+
return { type: 'alias', name: value, nextIndex };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const agentMatch = parseUseOptionToken(token, USE_AGENT_FLAG);
|
|
298
|
+
if (agentMatch.matched) {
|
|
299
|
+
const value = agentMatch.value ?? tokens[index + 1];
|
|
300
|
+
if (!value) {
|
|
301
|
+
throw new Error(`${USE_AGENT_FLAG} 需要提供名称`);
|
|
302
|
+
}
|
|
303
|
+
const nextIndex = agentMatch.value ? index + 1 : index + 2;
|
|
304
|
+
return { type: 'agent', name: value, nextIndex };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function buildAliasRunArgs(entry: AliasEntry): string[] {
|
|
311
|
+
return normalizeRunCommandArgs(splitCommandArgs(entry.command));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function buildAgentRunArgs(entry: AgentEntry): string[] {
|
|
315
|
+
const tokens = normalizeRunCommandArgs(splitCommandArgs(entry.command));
|
|
316
|
+
if (tokens.length === 0) return [];
|
|
317
|
+
if (tokens[0].startsWith('-')) {
|
|
318
|
+
return tokens;
|
|
319
|
+
}
|
|
320
|
+
const [command, ...args] = tokens;
|
|
321
|
+
if (args.length === 0) {
|
|
322
|
+
return ['--ai-cli', command];
|
|
323
|
+
}
|
|
324
|
+
return ['--ai-cli', command, '--ai-args', ...args];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function expandRunTokens(
|
|
328
|
+
tokens: string[],
|
|
329
|
+
lookup: { aliasEntries: Map<string, AliasEntry>; agentEntries: Map<string, AgentEntry> },
|
|
330
|
+
stack: { alias: Set<string>; agent: Set<string> }
|
|
331
|
+
): ExpandedRunTokens {
|
|
332
|
+
let mergedTokens: string[] = [];
|
|
333
|
+
let buffer: string[] = [];
|
|
334
|
+
let expanded = false;
|
|
335
|
+
let index = 0;
|
|
336
|
+
|
|
337
|
+
while (index < tokens.length) {
|
|
338
|
+
const token = tokens[index];
|
|
339
|
+
if (token === '--') {
|
|
340
|
+
buffer.push(...tokens.slice(index));
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const match = resolveUseOption(tokens, index);
|
|
345
|
+
if (!match) {
|
|
346
|
+
buffer.push(token);
|
|
347
|
+
index += 1;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (buffer.length > 0) {
|
|
352
|
+
mergedTokens = mergeRunCommandArgs(mergedTokens, buffer);
|
|
353
|
+
buffer = [];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
expanded = true;
|
|
357
|
+
|
|
358
|
+
if (match.type === 'alias') {
|
|
359
|
+
const normalized = normalizeAliasName(match.name);
|
|
360
|
+
if (!normalized) {
|
|
361
|
+
throw new Error('alias 名称不能为空且不能包含空白字符');
|
|
362
|
+
}
|
|
363
|
+
if (stack.alias.has(normalized)) {
|
|
364
|
+
throw new Error(`alias 循环引用:${normalized}`);
|
|
365
|
+
}
|
|
366
|
+
const entry = lookup.aliasEntries.get(normalized);
|
|
367
|
+
if (!entry) {
|
|
368
|
+
throw new Error(`未找到 alias:${normalized}`);
|
|
369
|
+
}
|
|
370
|
+
stack.alias.add(normalized);
|
|
371
|
+
const resolved = expandRunTokens(buildAliasRunArgs(entry), lookup, stack);
|
|
372
|
+
stack.alias.delete(normalized);
|
|
373
|
+
mergedTokens = mergeRunCommandArgs(mergedTokens, resolved.tokens);
|
|
374
|
+
index = match.nextIndex;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const normalized = normalizeAgentName(match.name);
|
|
379
|
+
if (!normalized) {
|
|
380
|
+
throw new Error('agent 名称不能为空且不能包含空白字符');
|
|
381
|
+
}
|
|
382
|
+
if (stack.agent.has(normalized)) {
|
|
383
|
+
throw new Error(`agent 循环引用:${normalized}`);
|
|
384
|
+
}
|
|
385
|
+
const entry = lookup.agentEntries.get(normalized);
|
|
386
|
+
if (!entry) {
|
|
387
|
+
throw new Error(`未找到 agent:${normalized}`);
|
|
388
|
+
}
|
|
389
|
+
stack.agent.add(normalized);
|
|
390
|
+
const resolved = expandRunTokens(buildAgentRunArgs(entry), lookup, stack);
|
|
391
|
+
stack.agent.delete(normalized);
|
|
392
|
+
mergedTokens = mergeRunCommandArgs(mergedTokens, resolved.tokens);
|
|
393
|
+
index = match.nextIndex;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (buffer.length > 0) {
|
|
397
|
+
mergedTokens = mergeRunCommandArgs(mergedTokens, buffer);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return { tokens: mergedTokens, expanded };
|
|
401
|
+
}
|
|
402
|
+
|
|
231
403
|
async function runForegroundWithDetach(options: {
|
|
232
404
|
argv: string[];
|
|
233
405
|
logFile: string;
|
|
@@ -332,15 +504,17 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
332
504
|
program
|
|
333
505
|
.name('wheel-ai')
|
|
334
506
|
.description('基于 AI CLI 的持续迭代开发工具')
|
|
335
|
-
.version('0.2.
|
|
507
|
+
.version('0.2.1');
|
|
336
508
|
program.addHelpText(
|
|
337
509
|
'after',
|
|
338
|
-
'\n
|
|
510
|
+
'\nalias 管理:\n wheel-ai alias set <name> <options...>\n wheel-ai alias list\n wheel-ai alias delete <name>\n\nalias/agent 叠加:\n wheel-ai run --use-alias <name> [--use-alias <name>...]\n wheel-ai run --use-agent <name> [--use-agent <name>...]\n 同名选项按出现顺序覆盖。\n'
|
|
339
511
|
);
|
|
340
512
|
|
|
341
513
|
program
|
|
342
514
|
.command('run')
|
|
343
515
|
.option('-t, --task <task>', '需要完成的任务描述(可重复传入,独立处理)', collect, [])
|
|
516
|
+
.option('--use-alias <name>', '叠加 alias 配置(可重复)', collect, [])
|
|
517
|
+
.option('--use-agent <name>', '叠加 agent 配置(可重复)', collect, [])
|
|
344
518
|
.option('-i, --iterations <number>', '最大迭代次数', value => parseInteger(value, 5), 5)
|
|
345
519
|
.option('--ai-cli <command>', 'AI CLI 命令', 'claude')
|
|
346
520
|
.option('--ai-args <args...>', 'AI CLI 参数', [])
|
|
@@ -374,6 +548,46 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
374
548
|
.option('-v, --verbose', '输出调试日志', false)
|
|
375
549
|
.option('--skip-quality', '跳过代码质量检查', false)
|
|
376
550
|
.action(async (options) => {
|
|
551
|
+
const rawRunArgs = extractRunCommandArgs(effectiveArgv);
|
|
552
|
+
const hasUseOptions = rawRunArgs.some(
|
|
553
|
+
token =>
|
|
554
|
+
token === USE_ALIAS_FLAG ||
|
|
555
|
+
token.startsWith(`${USE_ALIAS_FLAG}=`) ||
|
|
556
|
+
token === USE_AGENT_FLAG ||
|
|
557
|
+
token.startsWith(`${USE_AGENT_FLAG}=`)
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
if (hasUseOptions) {
|
|
561
|
+
const filePath = getGlobalConfigPath();
|
|
562
|
+
const exists = await fs.pathExists(filePath);
|
|
563
|
+
const content = exists ? await fs.readFile(filePath, 'utf8') : '';
|
|
564
|
+
const aliasEntries = parseAliasEntries(content);
|
|
565
|
+
const agentEntries = parseAgentEntries(content);
|
|
566
|
+
const resolved = expandRunTokens(
|
|
567
|
+
rawRunArgs,
|
|
568
|
+
{
|
|
569
|
+
aliasEntries: new Map(aliasEntries.map(entry => [entry.name, entry])),
|
|
570
|
+
agentEntries: new Map(agentEntries.map(entry => [entry.name, entry]))
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
alias: new Set<string>(),
|
|
574
|
+
agent: new Set<string>()
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
if (resolved.expanded) {
|
|
579
|
+
const nextArgv = [process.argv[0], process.argv[1], 'run', ...resolved.tokens];
|
|
580
|
+
const originalArgv = process.argv;
|
|
581
|
+
process.argv = nextArgv;
|
|
582
|
+
try {
|
|
583
|
+
await runCli(nextArgv);
|
|
584
|
+
} finally {
|
|
585
|
+
process.argv = originalArgv;
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
377
591
|
const tasks = normalizeTaskList(options.task as string[] | string | undefined);
|
|
378
592
|
if (tasks.length === 0) {
|
|
379
593
|
throw new Error('需要至少提供一个任务描述');
|
|
@@ -558,12 +772,116 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
558
772
|
await runLogsViewer();
|
|
559
773
|
});
|
|
560
774
|
|
|
561
|
-
program
|
|
562
|
-
.command('
|
|
563
|
-
.description('
|
|
564
|
-
|
|
565
|
-
|
|
775
|
+
const agentCommand = program
|
|
776
|
+
.command('agent')
|
|
777
|
+
.description('管理 AI CLI agent 配置');
|
|
778
|
+
|
|
779
|
+
agentCommand
|
|
780
|
+
.command('add <name> [command...]')
|
|
781
|
+
.description('新增 agent')
|
|
782
|
+
.allowUnknownOption(true)
|
|
783
|
+
.allowExcessArguments(true)
|
|
784
|
+
.action(async (name: string) => {
|
|
785
|
+
const normalized = normalizeAgentName(name);
|
|
786
|
+
if (!normalized) {
|
|
787
|
+
throw new Error('agent 名称不能为空且不能包含空白字符');
|
|
788
|
+
}
|
|
789
|
+
const commandArgs = extractAgentCommandArgs(effectiveArgv, 'add', name);
|
|
790
|
+
const commandLine = formatCommandLine(commandArgs);
|
|
791
|
+
if (!commandLine) {
|
|
792
|
+
throw new Error('agent 命令不能为空');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const filePath = getGlobalConfigPath();
|
|
796
|
+
const exists = await fs.pathExists(filePath);
|
|
797
|
+
const content = exists ? await fs.readFile(filePath, 'utf8') : '';
|
|
798
|
+
const entries = parseAgentEntries(content);
|
|
799
|
+
if (entries.some(entry => entry.name === normalized)) {
|
|
800
|
+
throw new Error(`agent 已存在:${normalized}`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
await upsertAgentEntry(normalized, commandLine);
|
|
804
|
+
console.log(`已新增 agent:${normalized}`);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
agentCommand
|
|
808
|
+
.command('set <name> [command...]')
|
|
809
|
+
.description('写入 agent')
|
|
810
|
+
.allowUnknownOption(true)
|
|
811
|
+
.allowExcessArguments(true)
|
|
812
|
+
.action(async (name: string) => {
|
|
813
|
+
const normalized = normalizeAgentName(name);
|
|
814
|
+
if (!normalized) {
|
|
815
|
+
throw new Error('agent 名称不能为空且不能包含空白字符');
|
|
816
|
+
}
|
|
817
|
+
const commandArgs = extractAgentCommandArgs(effectiveArgv, 'set', name);
|
|
818
|
+
const commandLine = formatCommandLine(commandArgs);
|
|
819
|
+
if (!commandLine) {
|
|
820
|
+
throw new Error('agent 命令不能为空');
|
|
821
|
+
}
|
|
822
|
+
await upsertAgentEntry(normalized, commandLine);
|
|
823
|
+
console.log(`已写入 agent:${normalized}`);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
agentCommand
|
|
827
|
+
.command('delete <name>')
|
|
828
|
+
.description('删除 agent')
|
|
829
|
+
.action(async (name: string) => {
|
|
830
|
+
const normalized = normalizeAgentName(name);
|
|
831
|
+
if (!normalized) {
|
|
832
|
+
throw new Error('agent 名称不能为空且不能包含空白字符');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const filePath = getGlobalConfigPath();
|
|
836
|
+
const exists = await fs.pathExists(filePath);
|
|
837
|
+
if (!exists) {
|
|
838
|
+
throw new Error(`未找到 agent 配置文件:${filePath}`);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const removed = await removeAgentEntry(normalized);
|
|
842
|
+
if (!removed) {
|
|
843
|
+
throw new Error(`未找到 agent:${normalized}`);
|
|
844
|
+
}
|
|
845
|
+
console.log(`已删除 agent:${normalized}`);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
agentCommand
|
|
849
|
+
.command('list')
|
|
850
|
+
.description('列出 agent 配置')
|
|
851
|
+
.action(async () => {
|
|
852
|
+
const filePath = getGlobalConfigPath();
|
|
853
|
+
const exists = await fs.pathExists(filePath);
|
|
854
|
+
if (!exists) {
|
|
855
|
+
console.log('未发现 agent 配置');
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
860
|
+
const entries = parseAgentEntries(content);
|
|
861
|
+
if (entries.length === 0) {
|
|
862
|
+
console.log('未发现 agent 配置');
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
entries.forEach(entry => {
|
|
867
|
+
console.log(`${entry.name}: ${entry.command}`);
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
agentCommand.action(() => {
|
|
872
|
+
agentCommand.help();
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
const aliasCommand = program
|
|
876
|
+
.command('alias')
|
|
877
|
+
.alias('aliases')
|
|
878
|
+
.description('管理全局 alias 配置');
|
|
879
|
+
|
|
880
|
+
aliasCommand
|
|
881
|
+
.command('set <name> [options...]')
|
|
882
|
+
.description('写入 alias')
|
|
566
883
|
.allowUnknownOption(true)
|
|
884
|
+
.allowExcessArguments(true)
|
|
567
885
|
.action(async (name: string) => {
|
|
568
886
|
const normalized = normalizeAliasName(name);
|
|
569
887
|
if (!normalized) {
|
|
@@ -578,10 +896,50 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
578
896
|
console.log(`已写入 alias:${normalized}`);
|
|
579
897
|
});
|
|
580
898
|
|
|
581
|
-
|
|
582
|
-
.command('
|
|
583
|
-
.
|
|
584
|
-
.
|
|
899
|
+
aliasCommand
|
|
900
|
+
.command('list')
|
|
901
|
+
.description('列出 alias 配置')
|
|
902
|
+
.action(async () => {
|
|
903
|
+
const filePath = getGlobalConfigPath();
|
|
904
|
+
const exists = await fs.pathExists(filePath);
|
|
905
|
+
if (!exists) {
|
|
906
|
+
console.log('未发现 alias 配置');
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
911
|
+
const entries = parseAliasEntries(content).filter(entry => entry.source === 'alias');
|
|
912
|
+
if (entries.length === 0) {
|
|
913
|
+
console.log('未发现 alias 配置');
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
entries.forEach(entry => {
|
|
918
|
+
console.log(`${entry.name}: ${entry.command}`);
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
aliasCommand
|
|
923
|
+
.command('delete <name>')
|
|
924
|
+
.description('删除 alias')
|
|
925
|
+
.action(async (name: string) => {
|
|
926
|
+
const normalized = normalizeAliasName(name);
|
|
927
|
+
if (!normalized) {
|
|
928
|
+
throw new Error('alias 名称不能为空且不能包含空白字符');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const filePath = getGlobalConfigPath();
|
|
932
|
+
const exists = await fs.pathExists(filePath);
|
|
933
|
+
if (!exists) {
|
|
934
|
+
throw new Error(`未找到 alias 配置文件:${filePath}`);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const removed = await removeAliasEntry(normalized);
|
|
938
|
+
if (!removed) {
|
|
939
|
+
throw new Error(`未找到 alias:${normalized}`);
|
|
940
|
+
}
|
|
941
|
+
console.log(`已删除 alias:${normalized}`);
|
|
942
|
+
});
|
|
585
943
|
|
|
586
944
|
aliasCommand
|
|
587
945
|
.command('run <name> [addition...]')
|
|
@@ -607,9 +965,9 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
607
965
|
throw new Error(`未找到 alias:${normalized}`);
|
|
608
966
|
}
|
|
609
967
|
|
|
610
|
-
const aliasTokens =
|
|
968
|
+
const aliasTokens = normalizeRunCommandArgs(splitCommandArgs(entry.command));
|
|
611
969
|
const additionTokens = extractAliasRunArgs(effectiveArgv, normalized);
|
|
612
|
-
const mergedTokens =
|
|
970
|
+
const mergedTokens = mergeRunCommandArgs(aliasTokens, additionTokens);
|
|
613
971
|
if (mergedTokens.length === 0) {
|
|
614
972
|
throw new Error('alias 命令不能为空');
|
|
615
973
|
}
|