skill-any-code 1.0.0 → 1.0.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/dist/adapters/command.schemas.js +18 -0
- package/dist/application/analysis.app.service.js +264 -0
- package/dist/application/bootstrap.js +21 -0
- package/dist/application/services/llm.analysis.service.js +170 -0
- package/dist/common/config.js +213 -0
- package/dist/common/constants.js +11 -0
- package/dist/common/errors.js +37 -0
- package/dist/common/logger.js +77 -0
- package/dist/common/types.js +2 -0
- package/dist/common/ui.js +201 -0
- package/dist/common/utils.js +117 -0
- package/dist/domain/index.js +17 -0
- package/dist/domain/interfaces.js +2 -0
- package/dist/domain/services/analysis.service.js +696 -0
- package/dist/domain/services/incremental.service.js +81 -0
- package/dist/infrastructure/blacklist.service.js +71 -0
- package/dist/infrastructure/cache/file.hash.cache.js +140 -0
- package/dist/infrastructure/git/git.service.js +159 -0
- package/dist/infrastructure/git.service.js +157 -0
- package/dist/infrastructure/index.service.js +108 -0
- package/dist/infrastructure/llm/llm.usage.tracker.js +58 -0
- package/dist/infrastructure/llm/openai.client.js +141 -0
- package/{src/infrastructure/llm/prompt.template.ts → dist/infrastructure/llm/prompt.template.js} +31 -36
- package/dist/infrastructure/llm.service.js +61 -0
- package/dist/infrastructure/skill/skill.generator.js +83 -0
- package/{src/infrastructure/skill/templates/resolve.script.ts → dist/infrastructure/skill/templates/resolve.script.js} +18 -15
- package/dist/infrastructure/skill/templates/skill.md.template.js +47 -0
- package/dist/infrastructure/splitter/code.splitter.js +137 -0
- package/dist/infrastructure/storage.service.js +409 -0
- package/dist/infrastructure/worker-pool/parse.worker.impl.js +137 -0
- package/dist/infrastructure/worker-pool/parse.worker.js +43 -0
- package/dist/infrastructure/worker-pool/worker-pool.service.js +171 -0
- package/package.json +5 -1
- package/jest.config.js +0 -27
- package/src/adapters/command.schemas.ts +0 -21
- package/src/application/analysis.app.service.ts +0 -272
- package/src/application/bootstrap.ts +0 -35
- package/src/application/services/llm.analysis.service.ts +0 -237
- package/src/cli.ts +0 -297
- package/src/common/config.ts +0 -209
- package/src/common/constants.ts +0 -8
- package/src/common/errors.ts +0 -34
- package/src/common/logger.ts +0 -82
- package/src/common/types.ts +0 -385
- package/src/common/ui.ts +0 -228
- package/src/common/utils.ts +0 -81
- package/src/domain/index.ts +0 -1
- package/src/domain/interfaces.ts +0 -188
- package/src/domain/services/analysis.service.ts +0 -735
- package/src/domain/services/incremental.service.ts +0 -50
- package/src/index.ts +0 -6
- package/src/infrastructure/blacklist.service.ts +0 -37
- package/src/infrastructure/cache/file.hash.cache.ts +0 -119
- package/src/infrastructure/git/git.service.ts +0 -120
- package/src/infrastructure/git.service.ts +0 -121
- package/src/infrastructure/index.service.ts +0 -94
- package/src/infrastructure/llm/llm.usage.tracker.ts +0 -65
- package/src/infrastructure/llm/openai.client.ts +0 -162
- package/src/infrastructure/llm.service.ts +0 -70
- package/src/infrastructure/skill/skill.generator.ts +0 -53
- package/src/infrastructure/skill/templates/skill.md.template.ts +0 -45
- package/src/infrastructure/splitter/code.splitter.ts +0 -176
- package/src/infrastructure/storage.service.ts +0 -413
- package/src/infrastructure/worker-pool/parse.worker.impl.ts +0 -135
- package/src/infrastructure/worker-pool/parse.worker.ts +0 -9
- package/src/infrastructure/worker-pool/worker-pool.service.ts +0 -173
- package/tsconfig.json +0 -24
- package/tsconfig.test.json +0 -5
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const workerpool = __importStar(require("workerpool"));
|
|
37
|
+
const parse_worker_impl_1 = require("./parse.worker.impl");
|
|
38
|
+
// 注册Worker方法
|
|
39
|
+
workerpool.worker({
|
|
40
|
+
parseFile: parse_worker_impl_1.parseFile,
|
|
41
|
+
aggregateDirectory: parse_worker_impl_1.aggregateDirectory,
|
|
42
|
+
validateResult: parse_worker_impl_1.validateResult
|
|
43
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.WorkerPoolService = void 0;
|
|
40
|
+
const workerpool = __importStar(require("workerpool"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const errors_1 = require("../../common/errors");
|
|
44
|
+
const constants_1 = require("../../common/constants");
|
|
45
|
+
const os_1 = __importDefault(require("os"));
|
|
46
|
+
const parse_worker_impl_1 = require("./parse.worker.impl");
|
|
47
|
+
class WorkerPoolService {
|
|
48
|
+
pool;
|
|
49
|
+
pendingTasks = [];
|
|
50
|
+
currentConcurrency;
|
|
51
|
+
llmConfig;
|
|
52
|
+
localMode;
|
|
53
|
+
constructor(llmConfig, concurrency = constants_1.DEFAULT_CONCURRENCY) {
|
|
54
|
+
this.currentConcurrency = concurrency;
|
|
55
|
+
this.llmConfig = {
|
|
56
|
+
...llmConfig,
|
|
57
|
+
cache_dir: WorkerPoolService.expandTilde(llmConfig.cache_dir),
|
|
58
|
+
};
|
|
59
|
+
const workerScript = path.join(__dirname, 'parse.worker.js');
|
|
60
|
+
this.localMode = !fs.existsSync(workerScript);
|
|
61
|
+
// Jest/ts-jest 直接执行 src 时,parse.worker.js 不存在。此时回退到“本进程执行”以保证核心逻辑可测。
|
|
62
|
+
// 生产构建(dist)与 CLI 运行时,parse.worker.js 存在,继续使用 worker threads 提升性能。
|
|
63
|
+
if (!this.localMode) {
|
|
64
|
+
this.pool = workerpool.pool(workerScript, {
|
|
65
|
+
maxWorkers: concurrency,
|
|
66
|
+
workerType: 'thread',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
static expandTilde(p) {
|
|
71
|
+
if (!p)
|
|
72
|
+
return p;
|
|
73
|
+
if (p.startsWith('~') && (p.length === 1 || p[1] === '/' || p[1] === '\\')) {
|
|
74
|
+
return path.join(os_1.default.homedir(), p.slice(1));
|
|
75
|
+
}
|
|
76
|
+
return p;
|
|
77
|
+
}
|
|
78
|
+
async submitFileAnalysisTask(filePath, fileContent, fileHash, language) {
|
|
79
|
+
if (this.localMode) {
|
|
80
|
+
const task = (0, parse_worker_impl_1.parseFile)(filePath, fileContent, fileHash, language, this.llmConfig);
|
|
81
|
+
this.pendingTasks.push(task);
|
|
82
|
+
try {
|
|
83
|
+
return await task;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const task = this.pool.exec('parseFile', [filePath, fileContent, fileHash, language, this.llmConfig]);
|
|
91
|
+
this.pendingTasks.push(task);
|
|
92
|
+
const result = await task;
|
|
93
|
+
// 移除已完成的任务
|
|
94
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
throw new errors_1.AppError(errors_1.ErrorCode.WORKER_SCHEDULE_FAILED, 'File analysis task failed', e.message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async submitDirectoryAggregationTask(dirPath, payload) {
|
|
102
|
+
if (this.localMode) {
|
|
103
|
+
const task = (0, parse_worker_impl_1.aggregateDirectory)(dirPath, payload, this.llmConfig);
|
|
104
|
+
this.pendingTasks.push(task);
|
|
105
|
+
try {
|
|
106
|
+
return await task;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const task = this.pool.exec('aggregateDirectory', [dirPath, payload, this.llmConfig]);
|
|
114
|
+
this.pendingTasks.push(task);
|
|
115
|
+
const result = await task;
|
|
116
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
throw new errors_1.AppError(errors_1.ErrorCode.WORKER_SCHEDULE_FAILED, 'Directory aggregation task failed', e.message);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async submitValidationTask(parentResult, childResult) {
|
|
124
|
+
if (this.localMode) {
|
|
125
|
+
const task = (0, parse_worker_impl_1.validateResult)(parentResult, childResult);
|
|
126
|
+
this.pendingTasks.push(task);
|
|
127
|
+
try {
|
|
128
|
+
return await task;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const task = this.pool.exec('validateResult', [parentResult, childResult]);
|
|
136
|
+
this.pendingTasks.push(task);
|
|
137
|
+
const result = await task;
|
|
138
|
+
this.pendingTasks = this.pendingTasks.filter(t => t !== task);
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
throw new errors_1.AppError(errors_1.ErrorCode.WORKER_SCHEDULE_FAILED, 'Validation task failed', e.message);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
setConcurrency(concurrency) {
|
|
146
|
+
this.currentConcurrency = concurrency;
|
|
147
|
+
if (this.localMode) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// 立即终止旧 worker,避免遗留线程占用资源
|
|
151
|
+
void this.pool.terminate(true).catch(() => { });
|
|
152
|
+
this.pool = workerpool.pool(path.join(__dirname, 'parse.worker.js'), {
|
|
153
|
+
maxWorkers: concurrency,
|
|
154
|
+
workerType: 'thread'
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async waitAll() {
|
|
158
|
+
await Promise.all(this.pendingTasks);
|
|
159
|
+
}
|
|
160
|
+
cancelAll() {
|
|
161
|
+
void this.pool?.terminate(true).catch(() => { });
|
|
162
|
+
this.pendingTasks = [];
|
|
163
|
+
}
|
|
164
|
+
async terminate(force = true) {
|
|
165
|
+
if (this.pool) {
|
|
166
|
+
await this.pool.terminate(force);
|
|
167
|
+
}
|
|
168
|
+
this.pendingTasks = [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
exports.WorkerPoolService = WorkerPoolService;
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-any-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Skill Any Code: CLI tool for large codebase understanding and analysis",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"skill-any-code": "dist/cli.js",
|
|
8
8
|
"sac": "dist/cli.js"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
10
14
|
"scripts": {
|
|
11
15
|
"build": "tsc",
|
|
12
16
|
"dev": "tsc --watch",
|
package/jest.config.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/** @type {import('jest').Config} */
|
|
2
|
-
module.exports = {
|
|
3
|
-
preset: 'ts-jest',
|
|
4
|
-
testEnvironment: 'node',
|
|
5
|
-
rootDir: '.',
|
|
6
|
-
// Windows 下大量文件/子进程/临时目录操作容易触发 EBUSY,串行执行更稳定
|
|
7
|
-
maxWorkers: 1,
|
|
8
|
-
testMatch: [
|
|
9
|
-
'<rootDir>/tests/**/*.test.ts'
|
|
10
|
-
],
|
|
11
|
-
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
|
12
|
-
transform: {
|
|
13
|
-
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.test.json' }]
|
|
14
|
-
},
|
|
15
|
-
collectCoverageFrom: [
|
|
16
|
-
'src/**/*.ts',
|
|
17
|
-
'!src/**/*.d.ts'
|
|
18
|
-
],
|
|
19
|
-
coverageDirectory: '<rootDir>/tests/coverage',
|
|
20
|
-
coverageReporters: ['text', 'lcov', 'html'],
|
|
21
|
-
cacheDirectory: '<rootDir>/tests/.cache/jest',
|
|
22
|
-
testTimeout: 300000, // 5分钟超时,适配大型项目测试
|
|
23
|
-
// 旧版 OpenCode/Skill 的测试辅助已废弃(V2.x 使用 CLI + LLM Mock Server)
|
|
24
|
-
moduleNameMapper: {
|
|
25
|
-
'^@/(.*)$': '<rootDir>/src/$1'
|
|
26
|
-
}
|
|
27
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
export const AnalyzeProjectCommandSchema = z.object({
|
|
4
|
-
path: z.string().optional().describe('Project root path to analyze (default: current working directory)'),
|
|
5
|
-
mode: z.enum(['full', 'incremental', 'auto']).default('auto').describe('Analysis mode'),
|
|
6
|
-
depth: z.number().int().min(1).optional().describe('Max directory depth (-1 = unlimited)'),
|
|
7
|
-
concurrency: z.number().int().min(1).optional().describe('Max concurrency (default: CPU cores * 2)'),
|
|
8
|
-
outputDir: z.string().optional().describe('Output directory for analysis results'),
|
|
9
|
-
skillsProviders: z.array(z.string()).optional().describe('AI tool providers to deploy the skill to'),
|
|
10
|
-
noSkills: z.boolean().optional().describe('Skip skill generation')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
export type AnalyzeProjectCommandParams = z.infer<typeof AnalyzeProjectCommandSchema>
|
|
14
|
-
|
|
15
|
-
export const ProjectCodeQuerySkillSchema = z.object({
|
|
16
|
-
path: z.string().describe('Relative path of the file/directory from the project root'),
|
|
17
|
-
type: z.enum(['summary', 'full', 'diagram']).default('summary').describe('Query type'),
|
|
18
|
-
projectSlug: z.string().optional().describe('Project identifier (default: current project)')
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
export type ProjectCodeQuerySkillParams = z.infer<typeof ProjectCodeQuerySkillSchema>
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AnalyzeProjectCommandParams,
|
|
3
|
-
AnalyzeProjectCommandResult,
|
|
4
|
-
LLMConfig,
|
|
5
|
-
AnalysisObject,
|
|
6
|
-
ObjectResultMeta,
|
|
7
|
-
} from '../common/types'
|
|
8
|
-
import { GitService } from '../infrastructure/git.service'
|
|
9
|
-
import { LocalStorageService } from '../infrastructure/storage.service'
|
|
10
|
-
import { BlacklistService } from '../infrastructure/blacklist.service'
|
|
11
|
-
import { SkillGenerator } from '../infrastructure/skill/skill.generator'
|
|
12
|
-
import { AnalysisService } from '../domain/services/analysis.service'
|
|
13
|
-
import { generateProjectSlug, getStoragePath, getFileOutputPath } from '../common/utils'
|
|
14
|
-
import { AppError, ErrorCode } from '../common/errors'
|
|
15
|
-
import { DEFAULT_CONCURRENCY } from '../common/constants'
|
|
16
|
-
import { logger } from '../common/logger'
|
|
17
|
-
import type { SkillProvider } from '../domain/interfaces'
|
|
18
|
-
import type { Config } from '../common/config'
|
|
19
|
-
import * as path from 'path'
|
|
20
|
-
import * as fs from 'fs-extra'
|
|
21
|
-
import { createHash } from 'crypto'
|
|
22
|
-
import { OpenAIClient } from '../infrastructure/llm/openai.client'
|
|
23
|
-
|
|
24
|
-
export class AnalysisAppService {
|
|
25
|
-
private totalObjects = 0
|
|
26
|
-
private completedObjects = 0
|
|
27
|
-
private activeObjects: Set<string> = new Set()
|
|
28
|
-
private progressEnabled = false
|
|
29
|
-
private onProgress?: AnalyzeProjectCommandParams['onProgress']
|
|
30
|
-
private concurrency = DEFAULT_CONCURRENCY
|
|
31
|
-
private lastRenderedCurrentKey: string | null = null
|
|
32
|
-
|
|
33
|
-
async runAnalysis(params: AnalyzeProjectCommandParams & { outputDir?: string }): Promise<AnalyzeProjectCommandResult> {
|
|
34
|
-
const projectRoot = params.path || process.cwd()
|
|
35
|
-
logger.info(`Analysis started. Project root: ${projectRoot}`)
|
|
36
|
-
this.progressEnabled = typeof params.onProgress === 'function'
|
|
37
|
-
this.onProgress = params.onProgress
|
|
38
|
-
this.concurrency = params.concurrency || DEFAULT_CONCURRENCY
|
|
39
|
-
this.totalObjects = 0
|
|
40
|
-
this.completedObjects = 0
|
|
41
|
-
this.activeObjects = new Set()
|
|
42
|
-
this.lastRenderedCurrentKey = null
|
|
43
|
-
const outputDir = params.outputDir
|
|
44
|
-
const gitService = new GitService(projectRoot)
|
|
45
|
-
const storageService = new LocalStorageService(projectRoot, outputDir)
|
|
46
|
-
|
|
47
|
-
// 检测是否为Git项目
|
|
48
|
-
const isGit = await gitService.isGitProject()
|
|
49
|
-
let projectSlug: string
|
|
50
|
-
let currentCommit = ''
|
|
51
|
-
let currentBranch = ''
|
|
52
|
-
logger.debug(`Project path: ${projectRoot}, isGit: ${isGit}`)
|
|
53
|
-
|
|
54
|
-
if (isGit) {
|
|
55
|
-
currentCommit = await gitService.getCurrentCommit()
|
|
56
|
-
currentBranch = await gitService.getCurrentBranch()
|
|
57
|
-
const gitSlug = await gitService.getProjectSlug()
|
|
58
|
-
projectSlug = generateProjectSlug(projectRoot, true, gitSlug)
|
|
59
|
-
logger.debug(`Git info: branch=${currentBranch}, commit=${currentCommit}, slug=${gitSlug}`)
|
|
60
|
-
} else {
|
|
61
|
-
projectSlug = generateProjectSlug(projectRoot, false)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 检测解析模式
|
|
65
|
-
let mode: 'full' | 'incremental' = params.mode === 'full' ? 'full' : 'incremental'
|
|
66
|
-
if (params.mode === 'auto') {
|
|
67
|
-
const hasAnyResult = await storageService.hasAnyResult(projectSlug)
|
|
68
|
-
mode = hasAnyResult ? 'incremental' : 'full'
|
|
69
|
-
logger.debug(
|
|
70
|
-
`Auto mode detection: hasAnyResult=${hasAnyResult}, selected=${mode}`,
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
logger.info(`Analysis mode: ${mode}`)
|
|
74
|
-
|
|
75
|
-
let runConfig: Config
|
|
76
|
-
try {
|
|
77
|
-
const { configManager } = await import('../common/config')
|
|
78
|
-
runConfig = configManager.getConfig()
|
|
79
|
-
} catch {
|
|
80
|
-
const { configManager } = await import('../common/config')
|
|
81
|
-
runConfig = await configManager.load()
|
|
82
|
-
}
|
|
83
|
-
const blacklistService = new BlacklistService()
|
|
84
|
-
await blacklistService.load(runConfig.analyze.blacklist, projectRoot)
|
|
85
|
-
const llmConfig = params.llmConfig as LLMConfig
|
|
86
|
-
const storageRoot = getStoragePath(projectRoot, outputDir)
|
|
87
|
-
|
|
88
|
-
if (!params.noSkills) {
|
|
89
|
-
try {
|
|
90
|
-
const skillGenerator = new SkillGenerator()
|
|
91
|
-
const providers = (params.skillsProviders ?? runConfig.skills.default_providers) as SkillProvider[]
|
|
92
|
-
await skillGenerator.generate({ projectRoot, storageRoot, providers })
|
|
93
|
-
} catch (error: unknown) {
|
|
94
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
95
|
-
logger.warn(`Skill generation failed: ${msg}`)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const analysisService = new AnalysisService(
|
|
100
|
-
gitService,
|
|
101
|
-
storageService,
|
|
102
|
-
blacklistService,
|
|
103
|
-
projectSlug,
|
|
104
|
-
currentCommit,
|
|
105
|
-
llmConfig,
|
|
106
|
-
params.onTokenUsageSnapshot,
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
const startTime = Date.now()
|
|
110
|
-
|
|
111
|
-
// ===================================================================
|
|
112
|
-
// 构建文件过滤器(全量 vs 增量唯一的差异点)
|
|
113
|
-
// ===================================================================
|
|
114
|
-
let fileFilter: (relPath: string, absPath: string) => Promise<boolean>
|
|
115
|
-
let commitHash: string = currentCommit
|
|
116
|
-
|
|
117
|
-
if (mode === 'full') {
|
|
118
|
-
// 全量:所有文件均需解析
|
|
119
|
-
fileFilter = async () => true
|
|
120
|
-
} else {
|
|
121
|
-
// 增量:优先对比 commitId,不可用时 fallback 到内容 hash
|
|
122
|
-
const lastCommit = null
|
|
123
|
-
logger.debug(`Last analyzed commit: ${lastCommit || 'N/A'}`)
|
|
124
|
-
|
|
125
|
-
// Git 项目:使用 git diff 批量获取变更文件集合(等价于逐文件 commitId 对比的批量优化)
|
|
126
|
-
let gitChangedFiles: Set<string> | null = null
|
|
127
|
-
if (isGit && lastCommit) {
|
|
128
|
-
try {
|
|
129
|
-
const diffFiles = await gitService.diffCommits(lastCommit, currentCommit)
|
|
130
|
-
logger.debug(`git diff detected changed files: ${diffFiles.length}`)
|
|
131
|
-
gitChangedFiles = new Set(diffFiles)
|
|
132
|
-
|
|
133
|
-
// 将未提交变更也纳入候选集合
|
|
134
|
-
const uncommitted = await gitService.getUncommittedChanges()
|
|
135
|
-
if (uncommitted.length > 0) {
|
|
136
|
-
logger.debug(`Incremental mode detected uncommitted changes: ${uncommitted.length}`)
|
|
137
|
-
for (const f of uncommitted) gitChangedFiles.add(f)
|
|
138
|
-
}
|
|
139
|
-
logger.debug(`Merged changed files count: ${gitChangedFiles.size}`)
|
|
140
|
-
} catch (e) {
|
|
141
|
-
logger.warn(`Commit diff failed; falling back to hash comparison: ${(e as Error).message}`)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// 非 Git 项目:需要逐文件 hash 对比,预先无法批量确定变更集
|
|
146
|
-
// 直接在 filter 内逐文件处理
|
|
147
|
-
|
|
148
|
-
fileFilter = async (relPath: string, absPath: string): Promise<boolean> => {
|
|
149
|
-
// 检查已有结果是否存在
|
|
150
|
-
const existing = await storageService.getFileAnalysis(projectSlug, relPath, 'summary')
|
|
151
|
-
if (!existing) return true // 结果缺失 → 需要解析
|
|
152
|
-
|
|
153
|
-
if (isGit) {
|
|
154
|
-
if (gitChangedFiles !== null) {
|
|
155
|
-
// 批量优化:git diff 隐式对比了 commitId
|
|
156
|
-
return gitChangedFiles.has(relPath)
|
|
157
|
-
}
|
|
158
|
-
// git diff 不可用,回退到逐文件 commitId 对比
|
|
159
|
-
const currentFileCommitId = await gitService.getFileLastCommit(projectRoot, relPath)
|
|
160
|
-
if (currentFileCommitId && existing.fileGitCommitId) {
|
|
161
|
-
return currentFileCommitId !== existing.fileGitCommitId
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// 非 Git 或 commitId 不可用:回退到内容 hash 对比
|
|
166
|
-
if (existing.fileHashWhenAnalyzed) {
|
|
167
|
-
const content = await fs.readFile(absPath, 'utf-8')
|
|
168
|
-
const currentHash = createHash('sha256').update(content).digest('hex')
|
|
169
|
-
return existing.fileHashWhenAnalyzed !== currentHash
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return true // 无法判断 → 安全起见重新解析
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ===================================================================
|
|
177
|
-
// 调用统一解析管线
|
|
178
|
-
// ===================================================================
|
|
179
|
-
logger.debug(`Analysis params: depth=${params.depth}, concurrency=${params.concurrency || DEFAULT_CONCURRENCY}`)
|
|
180
|
-
const analysisResult = await analysisService.analyze({
|
|
181
|
-
projectRoot,
|
|
182
|
-
depth: params.depth,
|
|
183
|
-
concurrency: params.concurrency || DEFAULT_CONCURRENCY,
|
|
184
|
-
mode,
|
|
185
|
-
commitHash,
|
|
186
|
-
fileFilter,
|
|
187
|
-
onTotalKnown: (total) => {
|
|
188
|
-
this.totalObjects = total
|
|
189
|
-
params.onTotalKnown?.(total)
|
|
190
|
-
},
|
|
191
|
-
onObjectPlanned: obj => this.handleObjectPlanned(obj),
|
|
192
|
-
onObjectStarted: obj => this.handleObjectStarted(obj),
|
|
193
|
-
onObjectCompleted: (obj, meta) => this.handleObjectCompleted(obj, meta, params),
|
|
194
|
-
onScanProgress: params.onScanProgress,
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
const duration = Date.now() - startTime
|
|
198
|
-
const summaryPath = analysisResult.summaryPath
|
|
199
|
-
const tokenUsage = analysisService.getTokenUsage()
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
success: analysisResult.success,
|
|
203
|
-
code: analysisResult.success ? ErrorCode.SUCCESS : ErrorCode.ANALYSIS_EXCEPTION,
|
|
204
|
-
message: analysisResult.success ? 'Analysis completed' : `Analysis completed with ${analysisResult.errors.length} error(s)`,
|
|
205
|
-
data: {
|
|
206
|
-
projectName: projectSlug,
|
|
207
|
-
mode,
|
|
208
|
-
analyzedFilesCount: analysisResult.analyzedFilesCount,
|
|
209
|
-
duration,
|
|
210
|
-
summaryPath,
|
|
211
|
-
tokenUsage,
|
|
212
|
-
},
|
|
213
|
-
errors: analysisResult.errors.length > 0 ? analysisResult.errors : undefined
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private handleObjectPlanned(_obj: AnalysisObject): void {
|
|
218
|
-
// totalObjects 由 analyze 内部的 onTotalKnown 回调设置
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private handleObjectStarted(obj: AnalysisObject): void {
|
|
222
|
-
if (!this.progressEnabled) return
|
|
223
|
-
const normalized = this.normalizeObjectPath(obj)
|
|
224
|
-
this.activeObjects.add(normalized)
|
|
225
|
-
this.emitProgressSnapshot(new Set(this.activeObjects), normalized)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private handleObjectCompleted(
|
|
229
|
-
obj: AnalysisObject,
|
|
230
|
-
_meta: ObjectResultMeta,
|
|
231
|
-
params: AnalyzeProjectCommandParams,
|
|
232
|
-
): void {
|
|
233
|
-
this.completedObjects++
|
|
234
|
-
if (!this.progressEnabled) {
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const normalized = this.normalizeObjectPath(obj)
|
|
239
|
-
this.activeObjects.delete(normalized)
|
|
240
|
-
this.concurrency = params.concurrency || DEFAULT_CONCURRENCY
|
|
241
|
-
this.emitProgressSnapshot(new Set(this.activeObjects), normalized)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private normalizeObjectPath(obj: AnalysisObject): string {
|
|
245
|
-
const p = obj.path.replace(/\\/g, '/')
|
|
246
|
-
if (obj.type === 'directory') {
|
|
247
|
-
if (p === '.') return './'
|
|
248
|
-
return p.endsWith('/') ? p : `${p}/`
|
|
249
|
-
}
|
|
250
|
-
return p
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
private emitProgressSnapshot(snapshot: Set<string>, fallbackNormalized: string): void {
|
|
254
|
-
if (!this.onProgress) return
|
|
255
|
-
|
|
256
|
-
const activePaths = Array.from(snapshot)
|
|
257
|
-
.map(p => p.replace(/\\/g, '/'))
|
|
258
|
-
.sort()
|
|
259
|
-
|
|
260
|
-
const topN = activePaths.slice(0, this.concurrency)
|
|
261
|
-
const displayLines = topN
|
|
262
|
-
const key = displayLines.join('\n')
|
|
263
|
-
if (key === this.lastRenderedCurrentKey) {
|
|
264
|
-
return
|
|
265
|
-
}
|
|
266
|
-
this.lastRenderedCurrentKey = key
|
|
267
|
-
|
|
268
|
-
this.onProgress(this.completedObjects, this.totalObjects, {
|
|
269
|
-
path: key,
|
|
270
|
-
})
|
|
271
|
-
}
|
|
272
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { AnalysisAppService } from './analysis.app.service'
|
|
2
|
-
import { GitService } from '../infrastructure/git.service'
|
|
3
|
-
import { LocalStorageService } from '../infrastructure/storage.service'
|
|
4
|
-
import { AnalysisService } from '../domain/services/analysis.service'
|
|
5
|
-
import { IncrementalService } from '../domain/services/incremental.service'
|
|
6
|
-
import type { LLMConfig, TokenUsageStats } from '../common/types'
|
|
7
|
-
|
|
8
|
-
export interface AppServices {
|
|
9
|
-
analysisAppService: AnalysisAppService
|
|
10
|
-
analysisService: AnalysisService
|
|
11
|
-
incrementalService: IncrementalService
|
|
12
|
-
gitService: GitService
|
|
13
|
-
storageService: LocalStorageService
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function createAppServices(
|
|
17
|
-
projectRoot?: string,
|
|
18
|
-
llmConfig?: LLMConfig,
|
|
19
|
-
onTokenUsageSnapshot?: (stats: TokenUsageStats) => void,
|
|
20
|
-
): AppServices {
|
|
21
|
-
const root = projectRoot || process.cwd()
|
|
22
|
-
const gitService = new GitService(root)
|
|
23
|
-
const storageService = new LocalStorageService(root)
|
|
24
|
-
const incrementalService = new IncrementalService(gitService, storageService)
|
|
25
|
-
|
|
26
|
-
const analysisAppService = new AnalysisAppService()
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
analysisAppService,
|
|
30
|
-
analysisService: {} as AnalysisService,
|
|
31
|
-
incrementalService,
|
|
32
|
-
gitService,
|
|
33
|
-
storageService,
|
|
34
|
-
}
|
|
35
|
-
}
|