@spaceflow/review 0.29.3 → 0.31.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.
@@ -1,244 +0,0 @@
1
- import { Command, CommandRunner, Option, t } from "@spaceflow/core";
2
- import type { LLMMode, VerboseLevel } from "@spaceflow/core";
3
- import type { AnalyzeDeletionsMode } from "./review.config";
4
- import type { ReportFormat } from "./review-report";
5
- import { ReviewService } from "./review.service";
6
-
7
- export interface ReviewOptions {
8
- dryRun: boolean;
9
- ci: boolean;
10
- prNumber?: number;
11
- base?: string;
12
- head?: string;
13
- references?: string[];
14
- verbose?: VerboseLevel;
15
- includes?: string[];
16
- llmMode?: LLMMode;
17
- files?: string[];
18
- commits?: string[];
19
- verifyFixes?: boolean;
20
- verifyConcurrency?: number;
21
- analyzeDeletions?: AnalyzeDeletionsMode;
22
- /** 仅执行删除代码分析,跳过常规代码审查 */
23
- deletionOnly?: boolean;
24
- /** 删除代码分析模式:openai 使用标准模式,claude-agent 使用 Agent 模式 */
25
- deletionAnalysisMode?: LLMMode;
26
- /** 输出格式:markdown, terminal, json。不指定则智能选择 */
27
- outputFormat?: ReportFormat;
28
- /** 是否使用 AI 生成 PR 功能描述 */
29
- generateDescription?: boolean;
30
- /** 显示所有问题,不过滤非变更行的问题 */
31
- showAll?: boolean;
32
- /** PR 事件类型(opened, synchronize, closed 等) */
33
- eventAction?: string;
34
- concurrency?: number;
35
- timeout?: number;
36
- retries?: number;
37
- retryDelay?: number;
38
- }
39
-
40
- /**
41
- * Review 命令
42
- *
43
- * 在 GitHub Actions 中执行,用于自动代码审查
44
- *
45
- * 环境变量:
46
- * - GITHUB_TOKEN: GitHub API Token
47
- * - GITHUB_REPOSITORY: 仓库名称 (owner/repo 格式)
48
- * - GITHUB_REF_NAME: 当前分支名称
49
- * - GITHUB_EVENT_PATH: 事件文件路径(包含 PR 信息)
50
- */
51
- @Command({
52
- name: "review",
53
- description: t("review:description"),
54
- })
55
- export class ReviewCommand extends CommandRunner {
56
- constructor(protected readonly reviewService: ReviewService) {
57
- super();
58
- }
59
-
60
- async run(_passedParams: string[], options: ReviewOptions): Promise<void> {
61
- try {
62
- const context = await this.reviewService.getContextFromEnv(options);
63
- await this.reviewService.execute(context);
64
- } catch (error) {
65
- if (error instanceof Error) {
66
- console.error(t("common.executionFailed", { error: error.message }));
67
- if (error.stack) {
68
- console.error(t("common.stackTrace", { stack: error.stack }));
69
- }
70
- } else {
71
- console.error(t("common.executionFailed", { error }));
72
- }
73
- process.exit(1);
74
- }
75
- }
76
-
77
- @Option({
78
- flags: "-d, --dry-run",
79
- description: t("review:options.dryRun"),
80
- })
81
- parseDryRun(val: boolean): boolean {
82
- return val;
83
- }
84
-
85
- @Option({
86
- flags: "-c, --ci",
87
- description: t("common.options.ci"),
88
- })
89
- parseCi(val: boolean): boolean {
90
- return val;
91
- }
92
-
93
- @Option({
94
- flags: "-p, --pr-number <number>",
95
- description: t("review:options.prNumber"),
96
- })
97
- parsePrNumber(val: string): number {
98
- return parseInt(val, 10);
99
- }
100
-
101
- @Option({
102
- flags: "-b, --base <ref>",
103
- description: t("review:options.base"),
104
- })
105
- parseBase(val: string): string {
106
- return val;
107
- }
108
-
109
- @Option({
110
- flags: "--head <ref>",
111
- description: t("review:options.head"),
112
- })
113
- parseHead(val: string): string {
114
- return val;
115
- }
116
-
117
- @Option({
118
- flags: "-v, --verbose",
119
- description: t("common.options.verboseDebug"),
120
- })
121
- parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
122
- const current = typeof previous === "number" ? previous : previous ? 1 : 0;
123
- return Math.min(current + 1, 3) as VerboseLevel;
124
- }
125
-
126
- @Option({
127
- flags: "-i, --includes <patterns...>",
128
- description: t("review:options.includes"),
129
- })
130
- parseIncludes(val: string, previous: string[] = []): string[] {
131
- return [...previous, val];
132
- }
133
-
134
- @Option({
135
- flags: "-l, --llm-mode <mode>",
136
- description: t("review:options.llmMode"),
137
- choices: ["claude-code", "openai", "gemini"],
138
- })
139
- parseLlmMode(val: string): LLMMode {
140
- return val as LLMMode;
141
- }
142
-
143
- @Option({
144
- flags: "-f, --files <files...>",
145
- description: t("review:options.files"),
146
- })
147
- parseFiles(val: string, previous: string[] = []): string[] {
148
- return [...previous, val];
149
- }
150
-
151
- @Option({
152
- flags: "--commits <commits...>",
153
- description: t("review:options.commits"),
154
- })
155
- parseCommits(val: string, previous: string[] = []): string[] {
156
- return [...previous, val];
157
- }
158
-
159
- @Option({
160
- flags: "--verify-fixes",
161
- description: t("review:options.verifyFixes"),
162
- })
163
- parseVerifyFixes(val: boolean): boolean {
164
- return val;
165
- }
166
-
167
- @Option({
168
- flags: "--no-verify-fixes",
169
- description: t("review:options.noVerifyFixes"),
170
- })
171
- parseNoVerifyFixes(val: boolean): boolean {
172
- return !val;
173
- }
174
-
175
- @Option({
176
- flags: "--verify-concurrency <number>",
177
- description: t("review:options.verifyConcurrency"),
178
- })
179
- parseVerifyConcurrency(val: string): number {
180
- return parseInt(val, 10);
181
- }
182
-
183
- @Option({
184
- flags: "--analyze-deletions [mode]",
185
- description: t("review:options.analyzeDeletions"),
186
- })
187
- parseAnalyzeDeletions(val: string | boolean): AnalyzeDeletionsMode {
188
- if (val === true || val === "true") return true;
189
- if (val === false || val === "false") return false;
190
- if (val === "ci" || val === "pr" || val === "terminal") return val;
191
- // 默认为 true(当只传 --analyze-deletions 不带值时)
192
- return true;
193
- }
194
-
195
- @Option({
196
- flags: "--deletion-analysis-mode <mode>",
197
- description: t("review:options.deletionAnalysisMode"),
198
- choices: ["openai", "claude-code"],
199
- })
200
- parseDeletionAnalysisMode(val: string): LLMMode {
201
- return val as LLMMode;
202
- }
203
-
204
- @Option({
205
- flags: "--deletion-only",
206
- description: t("review:options.deletionOnly"),
207
- })
208
- parseDeletionOnly(val: boolean): boolean {
209
- return val;
210
- }
211
-
212
- @Option({
213
- flags: "-o, --output-format <format>",
214
- description: t("review:options.outputFormat"),
215
- choices: ["markdown", "terminal", "json"],
216
- })
217
- parseOutputFormat(val: string): ReportFormat {
218
- return val as ReportFormat;
219
- }
220
-
221
- @Option({
222
- flags: "--generate-description",
223
- description: t("review:options.generateDescription"),
224
- })
225
- parseGenerateDescription(val: boolean): boolean {
226
- return val;
227
- }
228
-
229
- @Option({
230
- flags: "--show-all",
231
- description: t("review:options.showAll"),
232
- })
233
- parseShowAll(val: boolean): boolean {
234
- return val;
235
- }
236
-
237
- @Option({
238
- flags: "--event-action <action>",
239
- description: t("review:options.eventAction"),
240
- })
241
- parseEventAction(val: string): string {
242
- return val;
243
- }
244
- }
package/src/review.mcp.ts DELETED
@@ -1,184 +0,0 @@
1
- /**
2
- * Review MCP 服务
3
- * 提供代码审查规则查询的 MCP 工具
4
- */
5
-
6
- import { McpServer, McpTool, ConfigReaderService, t } from "@spaceflow/core";
7
- import { ReviewSpecService } from "./review-spec/review-spec.service";
8
- import { join } from "path";
9
- import { existsSync } from "fs";
10
- import { ListRulesInput, GetRulesForFileInput, GetRuleDetailInput } from "./dto/mcp.dto";
11
- import type { ReviewConfig } from "./review.config";
12
-
13
- @McpServer({ name: "review-mcp", version: "1.0.0", description: t("review:mcp.serverDescription") })
14
- export class ReviewMcp {
15
- constructor(
16
- private readonly specService: ReviewSpecService,
17
- private readonly configReader: ConfigReaderService,
18
- ) {}
19
-
20
- /**
21
- * 获取项目的规则目录
22
- */
23
- private async getSpecDirs(cwd: string): Promise<string[]> {
24
- const dirs: string[] = [];
25
-
26
- // 1. 通过 ConfigReaderService 读取 review 配置
27
- try {
28
- const reviewConfig = this.configReader.getPluginConfig<ReviewConfig>("review");
29
- if (reviewConfig?.references?.length) {
30
- const resolved = await this.specService.resolveSpecSources(reviewConfig.references);
31
- dirs.push(...resolved);
32
- }
33
- } catch {
34
- // 忽略配置读取错误
35
- }
36
-
37
- // 2. 检查默认目录
38
- const defaultDirs = [
39
- join(cwd, ".claude", "skills"),
40
- join(cwd, ".cursor", "skills"),
41
- join(cwd, "review-specs"),
42
- ];
43
-
44
- for (const dir of defaultDirs) {
45
- if (existsSync(dir)) {
46
- dirs.push(dir);
47
- }
48
- }
49
-
50
- return [...new Set(dirs)]; // 去重
51
- }
52
-
53
- /**
54
- * 加载所有规则
55
- */
56
- private async loadAllSpecs(cwd: string) {
57
- const specDirs = await this.getSpecDirs(cwd);
58
- const allSpecs = [];
59
-
60
- for (const dir of specDirs) {
61
- const specs = await this.specService.loadReviewSpecs(dir);
62
- allSpecs.push(...specs);
63
- }
64
-
65
- // 只去重,不应用 override(MCP 工具应返回所有规则,override 在实际审查时应用)
66
- return this.specService.deduplicateSpecs(allSpecs);
67
- }
68
-
69
- @McpTool({
70
- name: "list_rules",
71
- description: t("review:mcp.listRules"),
72
- dto: ListRulesInput,
73
- })
74
- async listRules(input: ListRulesInput) {
75
- const workDir = input.cwd || process.cwd();
76
- const specs = await this.loadAllSpecs(workDir);
77
-
78
- const rules = specs.flatMap((spec) =>
79
- spec.rules.map((rule) => ({
80
- id: rule.id,
81
- title: rule.title,
82
- description: rule.description.slice(0, 200) + (rule.description.length > 200 ? "..." : ""),
83
- severity: rule.severity || spec.severity,
84
- extensions: spec.extensions,
85
- specFile: spec.filename,
86
- includes: spec.includes,
87
- hasExamples: rule.examples.length > 0,
88
- })),
89
- );
90
-
91
- return {
92
- total: rules.length,
93
- rules,
94
- };
95
- }
96
-
97
- @McpTool({
98
- name: "get_rules_for_file",
99
- description: t("review:mcp.getRulesForFile"),
100
- dto: GetRulesForFileInput,
101
- })
102
- async getRulesForFile(input: GetRulesForFileInput) {
103
- const workDir = input.cwd || process.cwd();
104
- const allSpecs = await this.loadAllSpecs(workDir);
105
-
106
- // 根据文件过滤适用的规则
107
- const applicableSpecs = this.specService.filterApplicableSpecs(allSpecs, [
108
- { filename: input.filePath },
109
- ]);
110
-
111
- // 进一步根据 includes 过滤(支持规则级 includes 覆盖文件级)
112
- const micromatchModule = await import("micromatch");
113
- const micromatch = micromatchModule.default || micromatchModule;
114
-
115
- const rules = applicableSpecs.flatMap((spec) =>
116
- spec.rules
117
- .filter((rule) => {
118
- // 规则级 includes 优先于文件级
119
- const includes = rule.includes || spec.includes;
120
- if (includes.length === 0) return true;
121
- return micromatch.isMatch(input.filePath, includes, { matchBase: true });
122
- })
123
- .map((rule) => ({
124
- id: rule.id,
125
- title: rule.title,
126
- description: rule.description,
127
- severity: rule.severity || spec.severity,
128
- specFile: spec.filename,
129
- ...(input.includeExamples && rule.examples.length > 0
130
- ? {
131
- examples: rule.examples.map((ex) => ({
132
- type: ex.type,
133
- lang: ex.lang,
134
- code: ex.code,
135
- })),
136
- }
137
- : {}),
138
- })),
139
- );
140
-
141
- return {
142
- file: input.filePath,
143
- total: rules.length,
144
- rules,
145
- };
146
- }
147
-
148
- @McpTool({
149
- name: "get_rule_detail",
150
- description: t("review:mcp.getRuleDetail"),
151
- dto: GetRuleDetailInput,
152
- })
153
- async getRuleDetail(input: GetRuleDetailInput) {
154
- const workDir = input.cwd || process.cwd();
155
- const specs = await this.loadAllSpecs(workDir);
156
-
157
- const result = this.specService.findRuleById(input.ruleId, specs);
158
-
159
- if (!result) {
160
- return { error: t("review:mcp.ruleNotFound", { ruleId: input.ruleId }) };
161
- }
162
-
163
- const { rule, spec } = result;
164
-
165
- return {
166
- id: rule.id,
167
- title: rule.title,
168
- description: rule.description,
169
- severity: rule.severity || spec.severity,
170
- specFile: spec.filename,
171
- extensions: spec.extensions,
172
- includes: spec.includes,
173
- overrides: rule.overrides,
174
- examples: rule.examples.map((ex) => ({
175
- type: ex.type,
176
- lang: ex.lang,
177
- code: ex.code,
178
- })),
179
- };
180
- }
181
- }
182
-
183
- // ReviewMcpService 类已通过 @McpServer 装饰器标记
184
- // CLI 的 `spaceflow mcp` 命令会自动扫描并发现该类
@@ -1,52 +0,0 @@
1
- import {
2
- Module,
3
- ConfigModule,
4
- ConfigService,
5
- ConfigReaderModule,
6
- ConfigReaderService,
7
- GitProviderModule,
8
- ciConfig,
9
- llmConfig,
10
- ClaudeSetupModule,
11
- LlmProxyModule,
12
- GitSdkModule,
13
- type LlmConfig,
14
- } from "@spaceflow/core";
15
- import { ReviewSpecModule } from "./review-spec";
16
- import { ReviewReportModule } from "./review-report";
17
- import { ReviewCommand } from "./review.command";
18
- import { ReviewService } from "./review.service";
19
- import { IssueVerifyService } from "./issue-verify.service";
20
- import { DeletionImpactService } from "./deletion-impact.service";
21
- import { ReviewMcp } from "./review.mcp";
22
- import type { ReviewConfig } from "./review.config";
23
-
24
- @Module({
25
- imports: [
26
- ConfigModule.forFeature(ciConfig),
27
- ConfigModule.forFeature(llmConfig),
28
- ConfigReaderModule,
29
- GitProviderModule.forFeature(),
30
- ClaudeSetupModule,
31
- ReviewSpecModule,
32
- ReviewReportModule,
33
- GitSdkModule,
34
- LlmProxyModule.forRootAsync({
35
- imports: [ConfigReaderModule, ConfigModule],
36
- useFactory: (configReader: ConfigReaderService, configService: ConfigService) => {
37
- const reviewConf = configReader.getPluginConfig<ReviewConfig>("review");
38
- const llm = configService.get<LlmConfig>("llm")!;
39
- return {
40
- defaultAdapter: reviewConf?.llmMode || "openai",
41
- claudeCode: llm.claudeCode,
42
- openai: llm.openai,
43
- openCode: llm.openCode,
44
- };
45
- },
46
- inject: [ConfigReaderService, ConfigService],
47
- }),
48
- ],
49
- providers: [ReviewCommand, ReviewService, IssueVerifyService, DeletionImpactService, ReviewMcp],
50
- exports: [ReviewMcp],
51
- })
52
- export class ReviewModule {}