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.
Files changed (68) hide show
  1. package/dist/adapters/command.schemas.js +18 -0
  2. package/dist/application/analysis.app.service.js +264 -0
  3. package/dist/application/bootstrap.js +21 -0
  4. package/dist/application/services/llm.analysis.service.js +170 -0
  5. package/dist/common/config.js +213 -0
  6. package/dist/common/constants.js +11 -0
  7. package/dist/common/errors.js +37 -0
  8. package/dist/common/logger.js +77 -0
  9. package/dist/common/types.js +2 -0
  10. package/dist/common/ui.js +201 -0
  11. package/dist/common/utils.js +117 -0
  12. package/dist/domain/index.js +17 -0
  13. package/dist/domain/interfaces.js +2 -0
  14. package/dist/domain/services/analysis.service.js +696 -0
  15. package/dist/domain/services/incremental.service.js +81 -0
  16. package/dist/infrastructure/blacklist.service.js +71 -0
  17. package/dist/infrastructure/cache/file.hash.cache.js +140 -0
  18. package/dist/infrastructure/git/git.service.js +159 -0
  19. package/dist/infrastructure/git.service.js +157 -0
  20. package/dist/infrastructure/index.service.js +108 -0
  21. package/dist/infrastructure/llm/llm.usage.tracker.js +58 -0
  22. package/dist/infrastructure/llm/openai.client.js +141 -0
  23. package/{src/infrastructure/llm/prompt.template.ts → dist/infrastructure/llm/prompt.template.js} +31 -36
  24. package/dist/infrastructure/llm.service.js +61 -0
  25. package/dist/infrastructure/skill/skill.generator.js +83 -0
  26. package/{src/infrastructure/skill/templates/resolve.script.ts → dist/infrastructure/skill/templates/resolve.script.js} +18 -15
  27. package/dist/infrastructure/skill/templates/skill.md.template.js +47 -0
  28. package/dist/infrastructure/splitter/code.splitter.js +137 -0
  29. package/dist/infrastructure/storage.service.js +409 -0
  30. package/dist/infrastructure/worker-pool/parse.worker.impl.js +137 -0
  31. package/dist/infrastructure/worker-pool/parse.worker.js +43 -0
  32. package/dist/infrastructure/worker-pool/worker-pool.service.js +171 -0
  33. package/package.json +5 -1
  34. package/jest.config.js +0 -27
  35. package/src/adapters/command.schemas.ts +0 -21
  36. package/src/application/analysis.app.service.ts +0 -272
  37. package/src/application/bootstrap.ts +0 -35
  38. package/src/application/services/llm.analysis.service.ts +0 -237
  39. package/src/cli.ts +0 -297
  40. package/src/common/config.ts +0 -209
  41. package/src/common/constants.ts +0 -8
  42. package/src/common/errors.ts +0 -34
  43. package/src/common/logger.ts +0 -82
  44. package/src/common/types.ts +0 -385
  45. package/src/common/ui.ts +0 -228
  46. package/src/common/utils.ts +0 -81
  47. package/src/domain/index.ts +0 -1
  48. package/src/domain/interfaces.ts +0 -188
  49. package/src/domain/services/analysis.service.ts +0 -735
  50. package/src/domain/services/incremental.service.ts +0 -50
  51. package/src/index.ts +0 -6
  52. package/src/infrastructure/blacklist.service.ts +0 -37
  53. package/src/infrastructure/cache/file.hash.cache.ts +0 -119
  54. package/src/infrastructure/git/git.service.ts +0 -120
  55. package/src/infrastructure/git.service.ts +0 -121
  56. package/src/infrastructure/index.service.ts +0 -94
  57. package/src/infrastructure/llm/llm.usage.tracker.ts +0 -65
  58. package/src/infrastructure/llm/openai.client.ts +0 -162
  59. package/src/infrastructure/llm.service.ts +0 -70
  60. package/src/infrastructure/skill/skill.generator.ts +0 -53
  61. package/src/infrastructure/skill/templates/skill.md.template.ts +0 -45
  62. package/src/infrastructure/splitter/code.splitter.ts +0 -176
  63. package/src/infrastructure/storage.service.ts +0 -413
  64. package/src/infrastructure/worker-pool/parse.worker.impl.ts +0 -135
  65. package/src/infrastructure/worker-pool/parse.worker.ts +0 -9
  66. package/src/infrastructure/worker-pool/worker-pool.service.ts +0 -173
  67. package/tsconfig.json +0 -24
  68. 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.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
- }