Skip to content

Commit abe62d4

Browse files
authored
Merge pull request #147 from justlovemaki/main
feat(cli): add clone-and-analyze command for remote repositories
2 parents acff024 + dd548ef commit abe62d4

File tree

6 files changed

+505
-3
lines changed

6 files changed

+505
-3
lines changed
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/**
2+
* Clone and Analyze command
3+
* 克隆 git 仓库并分析代码质量
4+
*/
5+
6+
import { Command } from 'commander';
7+
import { resolve } from 'node:path';
8+
import { loadConfig, createRuntimeConfig } from '../../config/index.js';
9+
import { createAnalyzer } from '../../analyzer/index.js';
10+
import { ConsoleOutput } from '../output/console.js';
11+
import { MarkdownOutput } from '../output/markdown.js';
12+
import { JsonOutput } from '../output/json.js';
13+
import { HtmlOutput } from '../output/html.js';
14+
import { createSpinner, ProgressBar } from '../../utils/progress.js';
15+
import { exists, isDirectory } from '../../utils/fs.js';
16+
import { t } from '../../i18n/index.js';
17+
import { renderMarkdownToTerminal } from '../../utils/markdown.js';
18+
import chalk from 'chalk';
19+
import {
20+
gitClone,
21+
removeTempDir,
22+
isValidGitUrl,
23+
type GitCloneResult,
24+
} from '../../utils/git.js';
25+
26+
interface CloneAnalyzeOptions {
27+
verbose?: boolean;
28+
top?: number;
29+
format?: 'console' | 'markdown' | 'json' | 'html';
30+
output?: string;
31+
exclude?: string[];
32+
concurrency?: number;
33+
locale?: 'en' | 'zh' | 'ru';
34+
keepTemp?: boolean;
35+
}
36+
37+
export function createCloneAnalyzeCommand(): Command {
38+
const command = new Command('clone-and-analyze');
39+
40+
command
41+
.description(t('cmd_clone_analyze_description'))
42+
.argument('<git-url>', 'Git repository URL to clone and analyze')
43+
.option('-v, --verbose', 'Show verbose output')
44+
.option('-t, --top <number>', 'Show top N worst files (default: 10)', parseInt)
45+
.option(
46+
'-f, --format <format>',
47+
'Output format: console, markdown, json, html (default: console)'
48+
)
49+
.option('-o, --output <file>', 'Write output to file instead of stdout')
50+
.option('-e, --exclude <patterns...>', 'Additional glob patterns to exclude')
51+
.option('-c, --concurrency <number>', 'Number of concurrent workers (default: 8)', parseInt)
52+
.option('-l, --locale <locale>', 'Language: en, zh, ru (default: en)')
53+
.option('--keep-temp', 'Keep the temporary directory after analysis')
54+
.addHelpText(
55+
'after',
56+
`
57+
${t('cli_examples')}
58+
$ fuck-u-code clone-and-analyze https://github.com/user/repo.git # ${t('cmd_clone_analyze_example')}
59+
$ fuck-u-code clone-and-analyze git@github.com:user/repo.git # ${t('cmd_clone_analyze_example_url')}
60+
$ fuck-u-code clone-and-analyze https://github.com/user/repo.git -f markdown -o report.md # ${t('cmd_clone_analyze_example_output')}
61+
$ fuck-u-code clone-and-analyze https://github.com/user/repo.git --keep-temp # ${t('cmd_clone_analyze_example_keep')}
62+
`
63+
)
64+
.action(async (gitUrl: string, options: CloneAnalyzeOptions) => {
65+
await runCloneAnalyze(gitUrl, options);
66+
});
67+
68+
return command;
69+
}
70+
71+
async function runCloneAnalyze(gitUrl: string, options: CloneAnalyzeOptions): Promise<void> {
72+
// 验证 git URL
73+
if (!isValidGitUrl(gitUrl)) {
74+
console.error(chalk.red(t('error_invalid_git_url', { url: gitUrl })));
75+
process.exit(1);
76+
}
77+
78+
const cloneSpinner = createSpinner(t('progress_cloning'));
79+
const discoverySpinner = createSpinner(t('progress_discovering'));
80+
const state: { progressBar: ProgressBar | null } = { progressBar: null };
81+
let tempDir: string | undefined;
82+
let shouldCleanup = true;
83+
84+
try {
85+
// 克隆仓库
86+
cloneSpinner.start();
87+
const cloneResult: GitCloneResult = await gitClone(gitUrl, {
88+
verbose: options.verbose,
89+
});
90+
91+
if (!cloneResult.success) {
92+
cloneSpinner.fail(t('progress_clone_failed'));
93+
console.error(chalk.red(cloneResult.error));
94+
process.exit(1);
95+
}
96+
97+
tempDir = cloneResult.targetDir;
98+
cloneSpinner.succeed(t('progress_clone_success'));
99+
100+
if (options.verbose && tempDir) {
101+
console.log(chalk.green(t('info_temp_dir_created', { path: tempDir })));
102+
}
103+
104+
// 如果用户指定保留临时目录,则不清理
105+
if (options.keepTemp) {
106+
shouldCleanup = false;
107+
if (tempDir) {
108+
console.log(chalk.yellow(t('info_temp_dir_kept', { path: tempDir })));
109+
}
110+
}
111+
112+
const resolvedPath = resolve(tempDir!);
113+
114+
// 验证路径
115+
if (!(await exists(resolvedPath))) {
116+
console.error(chalk.red(t('error_path_not_found', { path: resolvedPath })));
117+
if (shouldCleanup && tempDir) {
118+
await removeTempDir(tempDir);
119+
}
120+
process.exit(1);
121+
}
122+
123+
if (!(await isDirectory(resolvedPath))) {
124+
console.error(chalk.red(t('error_not_a_directory', { path: resolvedPath })));
125+
if (shouldCleanup && tempDir) {
126+
await removeTempDir(tempDir);
127+
}
128+
process.exit(1);
129+
}
130+
131+
// 加载配置并分析
132+
const config = await loadConfig(resolvedPath);
133+
const runtimeConfig = createRuntimeConfig(resolvedPath, config, {
134+
verbose: options.verbose,
135+
concurrency: options.concurrency,
136+
exclude: options.exclude,
137+
output: {
138+
format: options.format ?? 'console',
139+
file: options.output,
140+
top: options.top ?? 10,
141+
maxIssues: 5,
142+
showDetails: true,
143+
},
144+
});
145+
146+
const analyzer = createAnalyzer(runtimeConfig, {
147+
onDiscoveryStart: () => {
148+
discoverySpinner.start();
149+
},
150+
onDiscoveryComplete: (fileCount) => {
151+
discoverySpinner.succeed(t('progress_discovered', { count: fileCount }));
152+
if (fileCount > 0) {
153+
state.progressBar = new ProgressBar(fileCount, t('progress_analyzing'));
154+
state.progressBar.start();
155+
}
156+
},
157+
onAnalysisProgress: (current) => {
158+
state.progressBar?.update(current);
159+
},
160+
});
161+
162+
const result = await analyzer.analyze();
163+
164+
state.progressBar?.succeed(t('analysisComplete'));
165+
166+
// 输出结果
167+
const outputFormat = runtimeConfig.output.format;
168+
const outputFile = runtimeConfig.output.file;
169+
170+
switch (outputFormat) {
171+
case 'markdown': {
172+
const mdOutput = new MarkdownOutput(runtimeConfig);
173+
const markdown = mdOutput.render(result);
174+
if (outputFile) {
175+
const { writeFile } = await import('node:fs/promises');
176+
await writeFile(outputFile, markdown, 'utf-8');
177+
console.log(t('outputWritten', { file: outputFile }));
178+
} else {
179+
console.log(renderMarkdownToTerminal(markdown));
180+
}
181+
break;
182+
}
183+
case 'json': {
184+
const jsonOutput = new JsonOutput();
185+
const json = jsonOutput.render(result);
186+
if (outputFile) {
187+
const { writeFile } = await import('node:fs/promises');
188+
await writeFile(outputFile, json, 'utf-8');
189+
console.log(t('outputWritten', { file: outputFile }));
190+
} else {
191+
console.log(json);
192+
}
193+
break;
194+
}
195+
case 'html': {
196+
const htmlOutput = new HtmlOutput(runtimeConfig);
197+
const html = htmlOutput.render(result);
198+
if (outputFile) {
199+
const { writeFile } = await import('node:fs/promises');
200+
await writeFile(outputFile, html, 'utf-8');
201+
console.log(t('outputWritten', { file: outputFile }));
202+
} else {
203+
console.log(chalk.yellow(t('output_html_requires_file')));
204+
const consoleOutputFallback = new ConsoleOutput(runtimeConfig);
205+
consoleOutputFallback.render(result);
206+
}
207+
break;
208+
}
209+
default: {
210+
const consoleOutput = new ConsoleOutput(runtimeConfig);
211+
consoleOutput.render(result);
212+
}
213+
}
214+
215+
// 清理临时目录
216+
if (shouldCleanup && tempDir) {
217+
const cleanSpinner = createSpinner(t('progress_cleaning'));
218+
cleanSpinner.start();
219+
const removed = await removeTempDir(tempDir);
220+
if (removed) {
221+
cleanSpinner.succeed(t('progress_clean_complete'));
222+
if (options.verbose) {
223+
console.log(t('info_temp_dir_removed', { path: tempDir }));
224+
}
225+
} else {
226+
cleanSpinner.fail(t('progress_clean_failed'));
227+
}
228+
}
229+
230+
process.exit(0);
231+
} catch (error) {
232+
cloneSpinner.fail(t('progress_clone_failed'));
233+
discoverySpinner.fail(t('analysisFailed'));
234+
state.progressBar?.fail(t('analysisFailed'));
235+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
236+
237+
// 发生错误时也要清理临时目录
238+
if (shouldCleanup && tempDir) {
239+
await removeTempDir(tempDir);
240+
}
241+
242+
process.exit(1);
243+
}
244+
}

src/cli/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createConfigCommand } from './commands/config.js';
99
import { createMcpInstallCommand } from './commands/mcp-install.js';
1010
import { createUninstallCommand } from './commands/uninstall.js';
1111
import { createUpdateCommand } from './commands/update.js';
12+
import { createCloneAnalyzeCommand } from './commands/clone-and-analyze.js';
1213
import { t, setLocale, type Locale } from '../i18n/index.js';
1314
import { loadLocaleFromConfig } from '../config/index.js';
1415
import { getSupportedLanguageNames } from '../parser/index.js';
@@ -31,6 +32,7 @@ ${t('cli_examples')}
3132
$ fuck-u-code analyze ./src --top 10 # ${t('cli_example_analyze_top')}
3233
$ fuck-u-code analyze . --format markdown # ${t('cli_example_analyze_markdown')}
3334
$ fuck-u-code analyze . --locale zh # ${t('cli_example_analyze_locale')}
35+
$ fuck-u-code clone-and-analyze https://github.com/user/repo.git # ${t('cmd_clone_analyze_example')}
3436
$ fuck-u-code ai-review . --model gpt-4o # ${t('cli_example_ai_review')}
3537
$ fuck-u-code mcp-install claude # ${t('cli_example_mcp_install')}
3638
$ fuck-u-code update # ${t('cmd_update_example')}
@@ -47,6 +49,7 @@ ${t('cli_supported_languages')}
4749
program.addCommand(createMcpInstallCommand());
4850
program.addCommand(createUpdateCommand());
4951
program.addCommand(createUninstallCommand());
52+
program.addCommand(createCloneAnalyzeCommand());
5053

5154
return program;
5255
}

src/i18n/locales/en.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,5 +426,26 @@
426426
"json_severity_info": "No issues detected, code quality is good",
427427
"json_severity_warning": "Minor issues that should be addressed",
428428
"json_severity_error": "Significant issues that need attention",
429-
"json_severity_critical": "Critical issues that must be fixed"
429+
"json_severity_critical": "Critical issues that must be fixed",
430+
431+
"cmd_clone_analyze_description": "Clone git repository and analyze code quality",
432+
"cmd_clone_analyze_example": "Clone and analyze remote repository",
433+
"cmd_clone_analyze_example_url": "Clone and analyze specified git repository",
434+
"cmd_clone_analyze_example_output": "Clone and analyze, output Markdown report",
435+
"cmd_clone_analyze_example_keep": "Clone and analyze, keep temp directory",
436+
"progress_cloning": "Cloning repository...",
437+
"progress_clone_success": "Repository cloned successfully",
438+
"progress_clone_failed": "Failed to clone repository",
439+
"progress_cleaning": "Cleaning up temp directory...",
440+
"progress_clean_complete": "Temp directory cleaned",
441+
"progress_clean_failed": "Failed to clean temp directory",
442+
"error_git_clone_failed": "Failed to clone {url}: {reason}",
443+
"error_target_dir_not_created": "Target directory not created",
444+
"error_remove_temp_dir_failed": "Failed to remove temp directory {path}: {error}",
445+
"error_invalid_git_url": "Invalid git URL: {url}",
446+
"error_git_not_installed": "git is not installed or not in PATH",
447+
"info_temp_dir_created": "Temp directory created: {path}",
448+
"info_temp_dir_kept": "Temp directory kept: {path}",
449+
"info_temp_dir_removed": "Temp directory removed: {path}",
450+
"warn_git_url_not_valid": "Git URL may be invalid: {url}"
430451
}

src/i18n/locales/ru.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,5 +426,26 @@
426426
"json_severity_info": "Проблем не обнаружено, качество кода хорошее",
427427
"json_severity_warning": "Незначительные проблемы, которые следует устранить",
428428
"json_severity_error": "Значительные проблемы, требующие внимания",
429-
"json_severity_critical": "Критические проблемы, которые необходимо исправить"
429+
"json_severity_critical": "Критические проблемы, которые необходимо исправить",
430+
431+
"cmd_clone_analyze_description": "Клонировать git репозиторий и проанализировать качество кода",
432+
"cmd_clone_analyze_example": "Клонировать и проанализировать удаленный репозиторий",
433+
"cmd_clone_analyze_example_url": "Клонировать и проанализировать указанный git репозиторий",
434+
"cmd_clone_analyze_example_output": "Клонировать и проанализировать, вывести отчёт в Markdown",
435+
"cmd_clone_analyze_example_keep": "Клонировать и проанализировать, сохранить временную директорию",
436+
"progress_cloning": "Клонирование репозитория...",
437+
"progress_clone_success": "Репозиторий успешно клонирован",
438+
"progress_clone_failed": "Не удалось клонировать репозиторий",
439+
"progress_cleaning": "Очистка временной директории...",
440+
"progress_clean_complete": "Временная директория очищена",
441+
"progress_clean_failed": "Не удалось очистить временную директорию",
442+
"error_git_clone_failed": "Не удалось клонировать {url}: {reason}",
443+
"error_target_dir_not_created": "Целевая директория не создана",
444+
"error_remove_temp_dir_failed": "Не удалось удалить временную директорию {path}: {error}",
445+
"error_invalid_git_url": "Неверный git URL: {url}",
446+
"error_git_not_installed": "git не установлен или не в PATH",
447+
"info_temp_dir_created": "Временная директория создана: {path}",
448+
"info_temp_dir_kept": "Временная директория сохранена: {path}",
449+
"info_temp_dir_removed": "Временная директория удалена: {path}",
450+
"warn_git_url_not_valid": "Git URL может быть недействительным: {url}"
430451
}

src/i18n/locales/zh.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,5 +427,26 @@
427427
"json_severity_info": "未检测到问题,代码质量良好",
428428
"json_severity_warning": "应该处理的轻微问题",
429429
"json_severity_error": "需要关注的重大问题",
430-
"json_severity_critical": "必须修复的严重问题"
430+
"json_severity_critical": "必须修复的严重问题",
431+
432+
"cmd_clone_analyze_description": "克隆 git 仓库并分析代码质量",
433+
"cmd_clone_analyze_example": "克隆并分析远程仓库",
434+
"cmd_clone_analyze_example_url": "克隆并分析指定的 git 仓库",
435+
"cmd_clone_analyze_example_output": "克隆并分析,输出 Markdown 报告",
436+
"cmd_clone_analyze_example_keep": "克隆并分析,保留临时目录",
437+
"progress_cloning": "正在克隆仓库...",
438+
"progress_clone_success": "仓库克隆成功",
439+
"progress_clone_failed": "仓库克隆失败",
440+
"progress_cleaning": "正在清理临时目录...",
441+
"progress_clean_complete": "临时目录已清理",
442+
"progress_clean_failed": "清理临时目录失败",
443+
"error_git_clone_failed": "克隆 {url} 失败:{reason}",
444+
"error_target_dir_not_created": "目标目录未创建",
445+
"error_remove_temp_dir_failed": "删除临时目录 {path} 失败:{error}",
446+
"error_invalid_git_url": "无效的 git URL: {url}",
447+
"error_git_not_installed": "git 未安装或不在 PATH 中",
448+
"info_temp_dir_created": "临时目录已创建:{path}",
449+
"info_temp_dir_kept": "临时目录已保留:{path}",
450+
"info_temp_dir_removed": "临时目录已删除:{path}",
451+
"warn_git_url_not_valid": "git URL 可能无效:{url}"
431452
}

0 commit comments

Comments
 (0)