agent-ide 0.1.10 → 0.2.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.
- package/README.md +89 -12
- package/dist/application/services/module-coordinator.service.d.ts +0 -1
- package/dist/application/services/module-coordinator.service.d.ts.map +1 -1
- package/dist/application/services/module-coordinator.service.js +2 -8
- package/dist/application/services/module-coordinator.service.js.map +1 -1
- package/dist/core/analysis/index.d.ts +1 -4
- package/dist/core/analysis/index.d.ts.map +1 -1
- package/dist/core/analysis/index.js +1 -7
- package/dist/core/analysis/index.js.map +1 -1
- package/dist/core/dependency/dependency-analyzer.d.ts.map +1 -1
- package/dist/core/dependency/dependency-analyzer.js +10 -0
- package/dist/core/dependency/dependency-analyzer.js.map +1 -1
- package/dist/core/indexing/index-engine.d.ts +4 -0
- package/dist/core/indexing/index-engine.d.ts.map +1 -1
- package/dist/core/indexing/index-engine.js +25 -1
- package/dist/core/indexing/index-engine.js.map +1 -1
- package/dist/core/indexing/symbol-index.d.ts +4 -0
- package/dist/core/indexing/symbol-index.d.ts.map +1 -1
- package/dist/core/indexing/symbol-index.js +17 -0
- package/dist/core/indexing/symbol-index.js.map +1 -1
- package/dist/core/move/import-resolver.d.ts.map +1 -1
- package/dist/core/move/import-resolver.js +8 -0
- package/dist/core/move/import-resolver.js.map +1 -1
- package/dist/core/move/move-service.js +7 -7
- package/dist/core/move/move-service.js.map +1 -1
- package/dist/core/refactor/swift-extractor.d.ts +98 -0
- package/dist/core/refactor/swift-extractor.d.ts.map +1 -0
- package/dist/core/refactor/swift-extractor.js +283 -0
- package/dist/core/refactor/swift-extractor.js.map +1 -0
- package/dist/core/rename/reference-updater.d.ts.map +1 -1
- package/dist/core/rename/reference-updater.js +16 -8
- package/dist/core/rename/reference-updater.js.map +1 -1
- package/dist/core/search/engines/text-engine.js +1 -1
- package/dist/core/search/engines/text-engine.js.map +1 -1
- package/dist/core/shit-score/grading.d.ts +39 -0
- package/dist/core/shit-score/grading.d.ts.map +1 -0
- package/dist/core/shit-score/grading.js +253 -0
- package/dist/core/shit-score/grading.js.map +1 -0
- package/dist/core/shit-score/index.d.ts +9 -0
- package/dist/core/shit-score/index.d.ts.map +1 -0
- package/dist/core/shit-score/index.js +8 -0
- package/dist/core/shit-score/index.js.map +1 -0
- package/dist/core/shit-score/score-calculator.d.ts +75 -0
- package/dist/core/shit-score/score-calculator.d.ts.map +1 -0
- package/dist/core/shit-score/score-calculator.js +240 -0
- package/dist/core/shit-score/score-calculator.js.map +1 -0
- package/dist/core/shit-score/shit-score-analyzer.d.ts +84 -0
- package/dist/core/shit-score/shit-score-analyzer.d.ts.map +1 -0
- package/dist/core/shit-score/shit-score-analyzer.js +595 -0
- package/dist/core/shit-score/shit-score-analyzer.js.map +1 -0
- package/dist/core/shit-score/types.d.ts +231 -0
- package/dist/core/shit-score/types.d.ts.map +1 -0
- package/dist/core/shit-score/types.js +73 -0
- package/dist/core/shit-score/types.js.map +1 -0
- package/dist/core/snapshot/code-compressor.d.ts +39 -0
- package/dist/core/snapshot/code-compressor.d.ts.map +1 -0
- package/dist/core/snapshot/code-compressor.js +211 -0
- package/dist/core/snapshot/code-compressor.js.map +1 -0
- package/dist/core/snapshot/config.d.ts +60 -0
- package/dist/core/snapshot/config.d.ts.map +1 -0
- package/dist/core/snapshot/config.js +136 -0
- package/dist/core/snapshot/config.js.map +1 -0
- package/dist/core/snapshot/index.d.ts +23 -0
- package/dist/core/snapshot/index.d.ts.map +1 -0
- package/dist/core/snapshot/index.js +27 -0
- package/dist/core/snapshot/index.js.map +1 -0
- package/dist/core/snapshot/snapshot-differ.d.ts +54 -0
- package/dist/core/snapshot/snapshot-differ.d.ts.map +1 -0
- package/dist/core/snapshot/snapshot-differ.js +262 -0
- package/dist/core/snapshot/snapshot-differ.js.map +1 -0
- package/dist/core/snapshot/snapshot-engine.d.ts +94 -0
- package/dist/core/snapshot/snapshot-engine.d.ts.map +1 -0
- package/dist/core/snapshot/snapshot-engine.js +492 -0
- package/dist/core/snapshot/snapshot-engine.js.map +1 -0
- package/dist/core/snapshot/types.d.ts +216 -0
- package/dist/core/snapshot/types.d.ts.map +1 -0
- package/dist/core/snapshot/types.js +79 -0
- package/dist/core/snapshot/types.js.map +1 -0
- package/dist/infrastructure/parser/analysis-types.d.ts +198 -0
- package/dist/infrastructure/parser/analysis-types.d.ts.map +1 -0
- package/dist/infrastructure/parser/analysis-types.js +6 -0
- package/dist/infrastructure/parser/analysis-types.js.map +1 -0
- package/dist/infrastructure/parser/base.d.ts +36 -0
- package/dist/infrastructure/parser/base.d.ts.map +1 -1
- package/dist/infrastructure/parser/base.js +72 -0
- package/dist/infrastructure/parser/base.js.map +1 -1
- package/dist/infrastructure/parser/index.d.ts +1 -0
- package/dist/infrastructure/parser/index.d.ts.map +1 -1
- package/dist/infrastructure/parser/index.js.map +1 -1
- package/dist/infrastructure/parser/interface.d.ts +63 -0
- package/dist/infrastructure/parser/interface.d.ts.map +1 -1
- package/dist/infrastructure/parser/interface.js +11 -1
- package/dist/infrastructure/parser/interface.js.map +1 -1
- package/dist/interfaces/cli/cli.d.ts +24 -0
- package/dist/interfaces/cli/cli.d.ts.map +1 -1
- package/dist/interfaces/cli/cli.js +1417 -141
- package/dist/interfaces/cli/cli.js.map +1 -1
- package/dist/plugins/javascript/parser.d.ts +41 -0
- package/dist/plugins/javascript/parser.d.ts.map +1 -1
- package/dist/plugins/javascript/parser.js +284 -0
- package/dist/plugins/javascript/parser.js.map +1 -1
- package/dist/plugins/swift/analyzers/complexity-analyzer.d.ts +41 -0
- package/dist/plugins/swift/analyzers/complexity-analyzer.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/complexity-analyzer.js +206 -0
- package/dist/plugins/swift/analyzers/complexity-analyzer.js.map +1 -0
- package/dist/plugins/swift/analyzers/duplication-detector.d.ts +89 -0
- package/dist/plugins/swift/analyzers/duplication-detector.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/duplication-detector.js +271 -0
- package/dist/plugins/swift/analyzers/duplication-detector.js.map +1 -0
- package/dist/plugins/swift/analyzers/error-handling-checker.d.ts +34 -0
- package/dist/plugins/swift/analyzers/error-handling-checker.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/error-handling-checker.js +135 -0
- package/dist/plugins/swift/analyzers/error-handling-checker.js.map +1 -0
- package/dist/plugins/swift/analyzers/naming-checker.d.ts +47 -0
- package/dist/plugins/swift/analyzers/naming-checker.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/naming-checker.js +161 -0
- package/dist/plugins/swift/analyzers/naming-checker.js.map +1 -0
- package/dist/plugins/swift/analyzers/pattern-detector.d.ts +78 -0
- package/dist/plugins/swift/analyzers/pattern-detector.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/pattern-detector.js +247 -0
- package/dist/plugins/swift/analyzers/pattern-detector.js.map +1 -0
- package/dist/plugins/swift/analyzers/security-checker.d.ts +38 -0
- package/dist/plugins/swift/analyzers/security-checker.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/security-checker.js +135 -0
- package/dist/plugins/swift/analyzers/security-checker.js.map +1 -0
- package/dist/plugins/swift/analyzers/test-coverage-checker.d.ts +26 -0
- package/dist/plugins/swift/analyzers/test-coverage-checker.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/test-coverage-checker.js +63 -0
- package/dist/plugins/swift/analyzers/test-coverage-checker.js.map +1 -0
- package/dist/plugins/swift/analyzers/type-safety-checker.d.ts +41 -0
- package/dist/plugins/swift/analyzers/type-safety-checker.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/type-safety-checker.js +121 -0
- package/dist/plugins/swift/analyzers/type-safety-checker.js.map +1 -0
- package/dist/plugins/swift/analyzers/unused-symbol-detector.d.ts +38 -0
- package/dist/plugins/swift/analyzers/unused-symbol-detector.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/unused-symbol-detector.js +211 -0
- package/dist/plugins/swift/analyzers/unused-symbol-detector.js.map +1 -0
- package/dist/plugins/swift/dependency-analyzer.d.ts +33 -0
- package/dist/plugins/swift/dependency-analyzer.d.ts.map +1 -0
- package/dist/plugins/swift/dependency-analyzer.js +95 -0
- package/dist/plugins/swift/dependency-analyzer.js.map +1 -0
- package/dist/plugins/swift/index.d.ts +14 -0
- package/dist/plugins/swift/index.d.ts.map +1 -0
- package/dist/plugins/swift/index.js +19 -0
- package/dist/plugins/swift/index.js.map +1 -0
- package/dist/plugins/swift/parser.d.ts +160 -0
- package/dist/plugins/swift/parser.d.ts.map +1 -0
- package/dist/plugins/swift/parser.js +670 -0
- package/dist/plugins/swift/parser.js.map +1 -0
- package/dist/plugins/swift/swift-bridge/swift-parser +0 -0
- package/dist/plugins/swift/symbol-extractor.d.ts +46 -0
- package/dist/plugins/swift/symbol-extractor.d.ts.map +1 -0
- package/dist/plugins/swift/symbol-extractor.js +187 -0
- package/dist/plugins/swift/symbol-extractor.js.map +1 -0
- package/dist/plugins/swift/types.d.ts +137 -0
- package/dist/plugins/swift/types.d.ts.map +1 -0
- package/dist/plugins/swift/types.js +212 -0
- package/dist/plugins/swift/types.js.map +1 -0
- package/dist/plugins/typescript/analyzers/complexity-analyzer.d.ts +39 -0
- package/dist/plugins/typescript/analyzers/complexity-analyzer.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/complexity-analyzer.js +196 -0
- package/dist/plugins/typescript/analyzers/complexity-analyzer.js.map +1 -0
- package/dist/{core/analysis → plugins/typescript/analyzers}/duplication-detector.d.ts +34 -3
- package/dist/plugins/typescript/analyzers/duplication-detector.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/duplication-detector.js +695 -0
- package/dist/plugins/typescript/analyzers/duplication-detector.js.map +1 -0
- package/dist/plugins/typescript/analyzers/error-handling-checker.d.ts +26 -0
- package/dist/plugins/typescript/analyzers/error-handling-checker.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/error-handling-checker.js +84 -0
- package/dist/plugins/typescript/analyzers/error-handling-checker.js.map +1 -0
- package/dist/plugins/typescript/analyzers/naming-checker.d.ts +30 -0
- package/dist/plugins/typescript/analyzers/naming-checker.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/naming-checker.js +116 -0
- package/dist/plugins/typescript/analyzers/naming-checker.js.map +1 -0
- package/dist/plugins/typescript/analyzers/pattern-detector.d.ts +80 -0
- package/dist/plugins/typescript/analyzers/pattern-detector.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/pattern-detector.js +267 -0
- package/dist/plugins/typescript/analyzers/pattern-detector.js.map +1 -0
- package/dist/plugins/typescript/analyzers/security-checker.d.ts +34 -0
- package/dist/plugins/typescript/analyzers/security-checker.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/security-checker.js +126 -0
- package/dist/plugins/typescript/analyzers/security-checker.js.map +1 -0
- package/dist/plugins/typescript/analyzers/test-coverage-checker.d.ts +22 -0
- package/dist/plugins/typescript/analyzers/test-coverage-checker.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/test-coverage-checker.js +62 -0
- package/dist/plugins/typescript/analyzers/test-coverage-checker.js.map +1 -0
- package/dist/plugins/typescript/analyzers/type-safety-checker.d.ts +32 -0
- package/dist/plugins/typescript/analyzers/type-safety-checker.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/type-safety-checker.js +86 -0
- package/dist/plugins/typescript/analyzers/type-safety-checker.js.map +1 -0
- package/dist/plugins/typescript/analyzers/unused-symbol-detector.d.ts +47 -0
- package/dist/plugins/typescript/analyzers/unused-symbol-detector.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/unused-symbol-detector.js +152 -0
- package/dist/plugins/typescript/analyzers/unused-symbol-detector.js.map +1 -0
- package/dist/plugins/typescript/parser.d.ts +41 -0
- package/dist/plugins/typescript/parser.d.ts.map +1 -1
- package/dist/plugins/typescript/parser.js +336 -0
- package/dist/plugins/typescript/parser.js.map +1 -1
- package/dist/shared/types/symbol.d.ts +7 -1
- package/dist/shared/types/symbol.d.ts.map +1 -1
- package/dist/shared/types/symbol.js +8 -2
- package/dist/shared/types/symbol.js.map +1 -1
- package/package.json +17 -7
- package/bin/mcp-server.js +0 -20
- package/dist/core/analysis/complexity-analyzer.d.ts +0 -81
- package/dist/core/analysis/complexity-analyzer.d.ts.map +0 -1
- package/dist/core/analysis/complexity-analyzer.js +0 -255
- package/dist/core/analysis/complexity-analyzer.js.map +0 -1
- package/dist/core/analysis/dead-code-detector.d.ts +0 -152
- package/dist/core/analysis/dead-code-detector.d.ts.map +0 -1
- package/dist/core/analysis/dead-code-detector.js +0 -351
- package/dist/core/analysis/dead-code-detector.js.map +0 -1
- package/dist/core/analysis/duplication-detector.d.ts.map +0 -1
- package/dist/core/analysis/duplication-detector.js +0 -433
- package/dist/core/analysis/duplication-detector.js.map +0 -1
- package/dist/interfaces/mcp/index.d.ts +0 -7
- package/dist/interfaces/mcp/index.d.ts.map +0 -1
- package/dist/interfaces/mcp/index.js +0 -6
- package/dist/interfaces/mcp/index.js.map +0 -1
- package/dist/interfaces/mcp/mcp-server.d.ts +0 -34
- package/dist/interfaces/mcp/mcp-server.d.ts.map +0 -1
- package/dist/interfaces/mcp/mcp-server.js +0 -162
- package/dist/interfaces/mcp/mcp-server.js.map +0 -1
- package/dist/interfaces/mcp/mcp.d.ts +0 -52
- package/dist/interfaces/mcp/mcp.d.ts.map +0 -1
- package/dist/interfaces/mcp/mcp.js +0 -853
- package/dist/interfaces/mcp/mcp.js.map +0 -1
|
@@ -12,8 +12,9 @@ import { createIndexConfig } from '../../core/indexing/types.js';
|
|
|
12
12
|
import { ParserRegistry } from '../../infrastructure/parser/registry.js';
|
|
13
13
|
import { TypeScriptParser } from '../../plugins/typescript/parser.js';
|
|
14
14
|
import { JavaScriptParser } from '../../plugins/javascript/parser.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { SwiftParser } from '../../plugins/swift/parser.js';
|
|
16
|
+
import { ShitScoreAnalyzer } from '../../core/shit-score/shit-score-analyzer.js';
|
|
17
|
+
import { SnapshotEngine, SnapshotDiffer, ConfigManager, CompressionLevel } from '../../core/snapshot/index.js';
|
|
17
18
|
import { OutputFormatter, OutputFormat } from './output-formatter.js';
|
|
18
19
|
import * as fs from 'fs/promises';
|
|
19
20
|
import { readFileSync } from 'fs';
|
|
@@ -91,6 +92,20 @@ export class AgentIdeCLI {
|
|
|
91
92
|
console.debug('JavaScript parser loading failed:', jsError);
|
|
92
93
|
console.debug('JavaScript Parser initialization warning:', jsError);
|
|
93
94
|
}
|
|
95
|
+
// 嘗試註冊內建的 Swift Parser
|
|
96
|
+
try {
|
|
97
|
+
// 解析 Swift CLI Bridge 路徑
|
|
98
|
+
const swiftBridgePath = path.resolve(__dirname, '../../plugins/swift/swift-bridge/swift-parser');
|
|
99
|
+
const swiftParser = new SwiftParser(swiftBridgePath);
|
|
100
|
+
if (!registry.getParserByName('swift')) {
|
|
101
|
+
registry.register(swiftParser);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (swiftError) {
|
|
105
|
+
// 如果 Swift Parser 載入失敗,記錄錯誤
|
|
106
|
+
console.debug('Swift parser loading failed:', swiftError);
|
|
107
|
+
console.debug('Swift Parser initialization warning:', swiftError);
|
|
108
|
+
}
|
|
94
109
|
}
|
|
95
110
|
catch (error) {
|
|
96
111
|
// 靜默處理初始化錯誤,避免影響 CLI 啟動
|
|
@@ -109,6 +124,8 @@ export class AgentIdeCLI {
|
|
|
109
124
|
this.setupSearchCommand();
|
|
110
125
|
this.setupAnalyzeCommand();
|
|
111
126
|
this.setupDepsCommand();
|
|
127
|
+
this.setupShitCommand();
|
|
128
|
+
this.setupSnapshotCommand();
|
|
112
129
|
this.setupPluginsCommand();
|
|
113
130
|
}
|
|
114
131
|
setupIndexCommand() {
|
|
@@ -117,7 +134,7 @@ export class AgentIdeCLI {
|
|
|
117
134
|
.description('建立或更新程式碼索引')
|
|
118
135
|
.option('-p, --path <path>', '專案路徑', process.cwd())
|
|
119
136
|
.option('-u, --update', '增量更新索引')
|
|
120
|
-
.option('-e, --extensions <exts>', '包含的檔案副檔名', '.ts,.js,.tsx,.jsx')
|
|
137
|
+
.option('-e, --extensions <exts>', '包含的檔案副檔名', '.ts,.js,.tsx,.jsx,.swift')
|
|
121
138
|
.option('-x, --exclude <patterns>', '排除模式', 'node_modules/**,*.test.*')
|
|
122
139
|
.option('--format <format>', '輸出格式 (markdown|plain|json|minimal)', 'plain')
|
|
123
140
|
.action(async (options) => {
|
|
@@ -143,13 +160,14 @@ export class AgentIdeCLI {
|
|
|
143
160
|
setupRefactorCommand() {
|
|
144
161
|
this.program
|
|
145
162
|
.command('refactor <action>')
|
|
146
|
-
.description('重構程式碼 (extract-function | inline-function)')
|
|
163
|
+
.description('重構程式碼 (extract-function | extract-closure | inline-function)')
|
|
147
164
|
.option('-f, --file <file>', '檔案路徑')
|
|
165
|
+
.option('--path <path>', '檔案路徑(--file 的別名)')
|
|
148
166
|
.option('-s, --start-line <line>', '起始行號')
|
|
149
167
|
.option('-e, --end-line <line>', '結束行號')
|
|
150
168
|
.option('-n, --function-name <name>', '函式名稱')
|
|
169
|
+
.option('--new-name <name>', '新名稱(--function-name 的別名)')
|
|
151
170
|
.option('-t, --target-file <file>', '目標檔案路徑(跨檔案提取)')
|
|
152
|
-
.option('-p, --path <path>', '專案路徑', '.')
|
|
153
171
|
.option('--preview', '預覽變更而不執行')
|
|
154
172
|
.option('--format <format>', '輸出格式 (markdown|plain|json|minimal)', 'plain')
|
|
155
173
|
.action(async (action, options) => {
|
|
@@ -158,14 +176,28 @@ export class AgentIdeCLI {
|
|
|
158
176
|
}
|
|
159
177
|
setupMoveCommand() {
|
|
160
178
|
this.program
|
|
161
|
-
.command('move')
|
|
179
|
+
.command('move [source] [target]')
|
|
162
180
|
.description('移動檔案或目錄')
|
|
163
|
-
.
|
|
164
|
-
.
|
|
181
|
+
.option('-s, --source <path>', '來源路徑')
|
|
182
|
+
.option('-t, --target <path>', '目標路徑')
|
|
165
183
|
.option('--update-imports', '自動更新 import 路徑', true)
|
|
166
184
|
.option('--preview', '預覽變更而不執行')
|
|
167
185
|
.option('--format <format>', '輸出格式 (markdown|plain|json|minimal)', 'plain')
|
|
168
|
-
.action(async (
|
|
186
|
+
.action(async (sourceArg, targetArg, options) => {
|
|
187
|
+
// 支援兩種語法:
|
|
188
|
+
// 1. move <source> <target> (位置參數)
|
|
189
|
+
// 2. move --source <source> --target <target> (選項參數)
|
|
190
|
+
const source = sourceArg || options.source;
|
|
191
|
+
const target = targetArg || options.target;
|
|
192
|
+
if (!source || !target) {
|
|
193
|
+
console.error('❌ 必須指定來源和目標路徑');
|
|
194
|
+
console.error(' 使用方式: agent-ide move <source> <target>');
|
|
195
|
+
console.error(' 或: agent-ide move --source <source> --target <target>');
|
|
196
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
169
201
|
await this.handleMoveCommand(source, target, options);
|
|
170
202
|
});
|
|
171
203
|
}
|
|
@@ -173,20 +205,64 @@ export class AgentIdeCLI {
|
|
|
173
205
|
this.program
|
|
174
206
|
.command('search')
|
|
175
207
|
.description('搜尋程式碼')
|
|
176
|
-
.argument('
|
|
177
|
-
.option('-t, --type <type>', '搜尋類型 (text|regex|fuzzy)', 'text')
|
|
208
|
+
.argument('[query]', '搜尋查詢字串(簡化語法,等同於 text 搜尋)')
|
|
209
|
+
.option('-t, --type <type>', '搜尋類型 (text|regex|fuzzy|symbol|function|class|protocol|variable|enum)', 'text')
|
|
178
210
|
.option('-p, --path <path>', '搜尋路徑', '.')
|
|
179
|
-
.option('-e, --extensions <exts>', '檔案副檔名', '.ts,.js,.tsx,.jsx')
|
|
211
|
+
.option('-e, --extensions <exts>', '檔案副檔名', '.ts,.js,.tsx,.jsx,.swift')
|
|
180
212
|
.option('-l, --limit <num>', '結果數量限制', '50')
|
|
181
213
|
.option('-c, --context <lines>', '上下文行數', '2')
|
|
182
214
|
.option('--case-sensitive', '大小寫敏感')
|
|
215
|
+
.option('--case-insensitive', '大小寫不敏感')
|
|
183
216
|
.option('--whole-word', '全字匹配')
|
|
184
217
|
.option('--multiline', '多行匹配')
|
|
185
218
|
.option('--include <patterns>', '包含模式')
|
|
186
219
|
.option('--exclude <patterns>', '排除模式', 'node_modules/**,*.test.*')
|
|
187
|
-
.option('--format <format>', '輸出格式 (list|json|minimal)', 'list')
|
|
188
|
-
.
|
|
189
|
-
|
|
220
|
+
.option('--format <format>', '輸出格式 (list|json|minimal|summary)', 'list')
|
|
221
|
+
.option('-q, --query <name>', '搜尋查詢字串')
|
|
222
|
+
.option('--pattern <pattern>', '符號名稱模式(用於 structural 搜尋)')
|
|
223
|
+
.option('--regex', '使用正則表達式')
|
|
224
|
+
.option('--file-pattern <pattern>', '檔案模式過濾')
|
|
225
|
+
.option('--with-attribute <attr>', '過濾帶有特定屬性的符號')
|
|
226
|
+
.option('--with-modifier <mod>', '過濾帶有特定修飾符的符號')
|
|
227
|
+
.option('--implements <protocol>', '過濾實作特定協定的類別')
|
|
228
|
+
.option('--extends <class>', '過濾繼承特定類別的子類別')
|
|
229
|
+
.action(async (queryOrSubcommand, options) => {
|
|
230
|
+
// 支援三種語法:
|
|
231
|
+
// 1. search <query> --path <path> (簡化語法,預設為 text 搜尋)
|
|
232
|
+
// 2. search text --query <query> --path <path>
|
|
233
|
+
// 3. search symbol --query <query> --path <path>
|
|
234
|
+
// 4. search structural --type <type> --path <path>
|
|
235
|
+
// 檢查空字串或未提供
|
|
236
|
+
if (!queryOrSubcommand || queryOrSubcommand.trim() === '') {
|
|
237
|
+
console.error('❌ 請提供搜尋查詢或子命令');
|
|
238
|
+
console.error(' 使用方式: agent-ide search <query>');
|
|
239
|
+
console.error(' 或: agent-ide search text --query <query>');
|
|
240
|
+
console.error(' 或: agent-ide search symbol --query <query>');
|
|
241
|
+
process.exitCode = 1;
|
|
242
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// 判斷是子命令還是查詢字串
|
|
248
|
+
const knownSubcommands = ['text', 'symbol', 'structural'];
|
|
249
|
+
const isSubcommand = knownSubcommands.includes(queryOrSubcommand);
|
|
250
|
+
if (isSubcommand) {
|
|
251
|
+
// 使用子命令語法
|
|
252
|
+
if (queryOrSubcommand === 'symbol') {
|
|
253
|
+
await this.handleSymbolSearchCommand(options);
|
|
254
|
+
}
|
|
255
|
+
else if (queryOrSubcommand === 'text') {
|
|
256
|
+
await this.handleTextSearchCommand(options);
|
|
257
|
+
}
|
|
258
|
+
else if (queryOrSubcommand === 'structural') {
|
|
259
|
+
await this.handleStructuralSearchCommand(options);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// 簡化語法:直接使用查詢字串
|
|
264
|
+
await this.handleSearchCommand(queryOrSubcommand, options);
|
|
265
|
+
}
|
|
190
266
|
});
|
|
191
267
|
}
|
|
192
268
|
setupAnalyzeCommand() {
|
|
@@ -203,15 +279,29 @@ export class AgentIdeCLI {
|
|
|
203
279
|
}
|
|
204
280
|
setupDepsCommand() {
|
|
205
281
|
this.program
|
|
206
|
-
.command('deps')
|
|
207
|
-
.description('分析依賴關係')
|
|
282
|
+
.command('deps [subcommand]')
|
|
283
|
+
.description('分析依賴關係 (subcommand: graph|cycles|impact|orphans)')
|
|
208
284
|
.option('-p, --path <path>', '分析路徑', '.')
|
|
209
|
-
.option('-t, --type <type>', '分析類型 (graph|cycles|impact)')
|
|
210
285
|
.option('-f, --file <file>', '特定檔案分析')
|
|
211
286
|
.option('--format <format>', '輸出格式 (json|dot|summary)', 'summary')
|
|
212
287
|
.option('--all', '顯示完整依賴圖(預設只顯示循環依賴和孤立檔案)', false)
|
|
288
|
+
.action(async (subcommand, options) => {
|
|
289
|
+
await this.handleDepsCommand(subcommand, options);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
setupShitCommand() {
|
|
293
|
+
this.program
|
|
294
|
+
.command('shit')
|
|
295
|
+
.description('分析程式碼垃圾度(分數越高越糟糕)')
|
|
296
|
+
.option('-p, --path <path>', '分析路徑', '.')
|
|
297
|
+
.option('-d, --detailed', '顯示詳細資訊(topShit + recommendations)', false)
|
|
298
|
+
.option('-t, --top <num>', '顯示前 N 個最糟項目', '10')
|
|
299
|
+
.option('-m, --max-allowed <score>', '最大允許分數(超過則 exit 1)')
|
|
300
|
+
.option('--format <format>', '輸出格式 (json|summary)', 'summary')
|
|
301
|
+
.option('--show-files', '顯示問題檔案列表(detailedFiles)', false)
|
|
302
|
+
.option('-o, --output <file>', '輸出到檔案')
|
|
213
303
|
.action(async (options) => {
|
|
214
|
-
await this.
|
|
304
|
+
await this.handleShitCommand(options);
|
|
215
305
|
});
|
|
216
306
|
}
|
|
217
307
|
setupPluginsCommand() {
|
|
@@ -233,6 +323,22 @@ export class AgentIdeCLI {
|
|
|
233
323
|
await this.handlePluginInfoCommand(pluginName);
|
|
234
324
|
});
|
|
235
325
|
}
|
|
326
|
+
setupSnapshotCommand() {
|
|
327
|
+
this.program
|
|
328
|
+
.command('snapshot [action]')
|
|
329
|
+
.description('生成或管理程式碼快照')
|
|
330
|
+
.option('-p, --path <path>', '專案路徑', process.cwd())
|
|
331
|
+
.option('-o, --output <path>', '輸出檔案路徑')
|
|
332
|
+
.option('-i, --incremental', '增量更新', false)
|
|
333
|
+
.option('-l, --level <level>', '壓縮層級 (minimal|medium|full)', 'full')
|
|
334
|
+
.option('--multi-level', '生成多層級快照', false)
|
|
335
|
+
.option('--output-dir <dir>', '多層級輸出目錄', './snapshots')
|
|
336
|
+
.option('--format <format>', '輸出格式 (json|summary)', 'summary')
|
|
337
|
+
.option('--include-tests', '包含測試檔案', false)
|
|
338
|
+
.action(async (action, options) => {
|
|
339
|
+
await this.handleSnapshotCommand(action || 'generate', options);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
236
342
|
// Command handlers
|
|
237
343
|
async handleIndexCommand(options) {
|
|
238
344
|
const formatter = this.createFormatter(options.format);
|
|
@@ -294,20 +400,45 @@ export class AgentIdeCLI {
|
|
|
294
400
|
// 支援多種參數名稱
|
|
295
401
|
const from = options.symbol || options.from;
|
|
296
402
|
const to = options.newName || options.to;
|
|
403
|
+
const isJsonFormat = options.format === 'json';
|
|
297
404
|
if (!from || !to) {
|
|
298
|
-
|
|
299
|
-
|
|
405
|
+
if (isJsonFormat) {
|
|
406
|
+
console.error(JSON.stringify({ error: '必須指定符號名稱和新名稱' }));
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
console.error('❌ 必須指定符號名稱和新名稱');
|
|
410
|
+
console.error(' 使用方式: agent-ide rename --symbol <name> --new-name <name>');
|
|
411
|
+
}
|
|
300
412
|
if (process.env.NODE_ENV !== 'test') {
|
|
301
413
|
process.exit(1);
|
|
302
414
|
}
|
|
303
415
|
return;
|
|
304
416
|
}
|
|
305
|
-
|
|
417
|
+
if (!isJsonFormat) {
|
|
418
|
+
console.log(`🔄 重新命名 ${from} → ${to}`);
|
|
419
|
+
}
|
|
306
420
|
try {
|
|
307
|
-
|
|
421
|
+
let workspacePath = options.path || process.cwd();
|
|
422
|
+
// 如果路徑指向檔案,取其所在目錄
|
|
423
|
+
const stats = await fs.stat(workspacePath);
|
|
424
|
+
if (stats.isFile()) {
|
|
425
|
+
workspacePath = path.dirname(workspacePath);
|
|
426
|
+
// 往上查找專案根目錄(包含 package.json、.git 等)
|
|
427
|
+
let currentDir = workspacePath;
|
|
428
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
429
|
+
const hasPackageJson = await this.fileExists(path.join(currentDir, 'package.json'));
|
|
430
|
+
const hasGit = await this.fileExists(path.join(currentDir, '.git'));
|
|
431
|
+
const hasSwiftPackage = await this.fileExists(path.join(currentDir, 'Package.swift'));
|
|
432
|
+
if (hasPackageJson || hasGit || hasSwiftPackage) {
|
|
433
|
+
workspacePath = currentDir;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
currentDir = path.dirname(currentDir);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
308
439
|
// 初始化索引引擎(每次都重新索引以確保資料是最新的)
|
|
309
440
|
const config = createIndexConfig(workspacePath, {
|
|
310
|
-
includeExtensions: ['.ts', '.tsx', '.js', '.jsx'],
|
|
441
|
+
includeExtensions: ['.ts', '.tsx', '.js', '.jsx', '.swift'],
|
|
311
442
|
excludePatterns: ['node_modules/**', '*.test.*']
|
|
312
443
|
});
|
|
313
444
|
this.indexEngine = new IndexEngine(config);
|
|
@@ -317,16 +448,20 @@ export class AgentIdeCLI {
|
|
|
317
448
|
this.renameEngine = new RenameEngine();
|
|
318
449
|
}
|
|
319
450
|
// 1. 查找符號
|
|
320
|
-
|
|
451
|
+
if (!isJsonFormat) {
|
|
452
|
+
console.log(`🔍 查找符號 "${from}"...`);
|
|
453
|
+
}
|
|
321
454
|
const searchResults = await this.indexEngine.findSymbol(from);
|
|
322
455
|
if (searchResults.length === 0) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
process.exit(1);
|
|
456
|
+
if (isJsonFormat) {
|
|
457
|
+
console.error(JSON.stringify({ error: `找不到符號 "${from}"` }));
|
|
326
458
|
}
|
|
327
|
-
|
|
459
|
+
else {
|
|
460
|
+
console.log(`❌ 找不到符號 "${from}"`);
|
|
461
|
+
}
|
|
462
|
+
process.exit(1);
|
|
328
463
|
}
|
|
329
|
-
if (searchResults.length > 1) {
|
|
464
|
+
if (searchResults.length > 1 && !isJsonFormat) {
|
|
330
465
|
console.log('⚠️ 找到多個符號,使用第一個:');
|
|
331
466
|
searchResults.forEach((result, index) => {
|
|
332
467
|
console.log(` ${index + 1}. ${result.symbol.name} 在 ${result.symbol.location.filePath}:${result.symbol.location.range.start.line}`);
|
|
@@ -335,41 +470,62 @@ export class AgentIdeCLI {
|
|
|
335
470
|
const targetSymbol = searchResults[0].symbol;
|
|
336
471
|
// 2. 預覽變更
|
|
337
472
|
if (options.preview) {
|
|
338
|
-
|
|
473
|
+
if (!isJsonFormat) {
|
|
474
|
+
console.log('🔍 預覽變更...');
|
|
475
|
+
}
|
|
339
476
|
try {
|
|
340
477
|
// 取得所有專案檔案以進行跨檔案引用查找
|
|
341
|
-
|
|
478
|
+
// 使用 workspacePath(已解析為目錄)而不是 options.path(可能是檔案)
|
|
479
|
+
const allProjectFiles = await this.getAllProjectFiles(workspacePath);
|
|
342
480
|
const preview = await this.renameEngine.previewRename({
|
|
343
481
|
symbol: targetSymbol,
|
|
344
482
|
newName: to,
|
|
345
483
|
filePaths: allProjectFiles
|
|
346
484
|
});
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
485
|
+
if (isJsonFormat) {
|
|
486
|
+
console.log(JSON.stringify({
|
|
487
|
+
preview: true,
|
|
488
|
+
affectedFiles: preview.affectedFiles.length,
|
|
489
|
+
operations: preview.operations.length,
|
|
490
|
+
conflicts: preview.conflicts
|
|
491
|
+
}, null, 2));
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
console.log('📝 預計變更:');
|
|
495
|
+
console.log(` 檔案數: ${preview.affectedFiles.length}`);
|
|
496
|
+
console.log(` 操作數: ${preview.operations.length}`);
|
|
497
|
+
if (preview.conflicts.length > 0) {
|
|
498
|
+
console.log('⚠️ 發現衝突:');
|
|
499
|
+
preview.conflicts.forEach(conflict => {
|
|
500
|
+
console.log(` - ${conflict.message}`);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
preview.operations.forEach(op => {
|
|
504
|
+
console.log(` ${op.filePath}: "${op.oldText}" → "${op.newText}"`);
|
|
354
505
|
});
|
|
506
|
+
console.log('✅ 預覽完成');
|
|
355
507
|
}
|
|
356
|
-
preview.operations.forEach(op => {
|
|
357
|
-
console.log(` ${op.filePath}: "${op.oldText}" → "${op.newText}"`);
|
|
358
|
-
});
|
|
359
|
-
console.log('✅ 預覽完成');
|
|
360
508
|
return;
|
|
361
509
|
}
|
|
362
510
|
catch (previewError) {
|
|
363
|
-
|
|
511
|
+
if (isJsonFormat) {
|
|
512
|
+
console.error(JSON.stringify({ error: previewError instanceof Error ? previewError.message : String(previewError) }));
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
console.error('❌ 預覽失敗:', previewError instanceof Error ? previewError.message : previewError);
|
|
516
|
+
}
|
|
364
517
|
if (process.env.NODE_ENV !== 'test') {
|
|
365
518
|
process.exit(1);
|
|
366
519
|
}
|
|
367
520
|
}
|
|
368
521
|
}
|
|
369
522
|
// 3. 執行重新命名(處理跨檔案引用)
|
|
370
|
-
|
|
523
|
+
if (!isJsonFormat) {
|
|
524
|
+
console.log('✏️ 執行重新命名...');
|
|
525
|
+
}
|
|
371
526
|
// 取得所有專案檔案(使用與 preview 相同的邏輯)
|
|
372
|
-
|
|
527
|
+
// 使用 workspacePath(已解析為目錄)而不是 options.path(可能是檔案)
|
|
528
|
+
const allProjectFiles = await this.getAllProjectFiles(workspacePath);
|
|
373
529
|
// 使用 renameEngine 執行重新命名(與 preview 使用相同的引擎)
|
|
374
530
|
const renameResult = await this.renameEngine.rename({
|
|
375
531
|
symbol: targetSymbol,
|
|
@@ -377,43 +533,86 @@ export class AgentIdeCLI {
|
|
|
377
533
|
filePaths: allProjectFiles
|
|
378
534
|
});
|
|
379
535
|
if (renameResult.success) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
536
|
+
if (isJsonFormat) {
|
|
537
|
+
console.log(JSON.stringify({
|
|
538
|
+
success: true,
|
|
539
|
+
affectedFiles: renameResult.affectedFiles.length,
|
|
540
|
+
operations: renameResult.operations.length,
|
|
541
|
+
files: renameResult.affectedFiles
|
|
542
|
+
}, null, 2));
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
console.log('✅ 重新命名成功!');
|
|
546
|
+
console.log(`📊 統計: ${renameResult.affectedFiles.length} 檔案, ${renameResult.operations.length} 變更`);
|
|
547
|
+
renameResult.operations.forEach(operation => {
|
|
548
|
+
console.log(` ✓ ${operation.filePath}: "${operation.oldText}" → "${operation.newText}"`);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
385
551
|
}
|
|
386
552
|
else {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
553
|
+
if (isJsonFormat) {
|
|
554
|
+
console.error(JSON.stringify({
|
|
555
|
+
success: false,
|
|
556
|
+
errors: renameResult.errors || ['重新命名失敗']
|
|
557
|
+
}));
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
console.error('❌ 重新命名失敗:');
|
|
561
|
+
renameResult.errors?.forEach(error => {
|
|
562
|
+
console.error(` - ${error}`);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
391
565
|
if (process.env.NODE_ENV !== 'test') {
|
|
392
566
|
process.exit(1);
|
|
393
567
|
}
|
|
394
568
|
}
|
|
395
569
|
}
|
|
396
570
|
catch (error) {
|
|
397
|
-
|
|
571
|
+
if (isJsonFormat) {
|
|
572
|
+
console.error(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
console.error('❌ 重新命名失敗:', error instanceof Error ? error.message : error);
|
|
576
|
+
}
|
|
398
577
|
if (process.env.NODE_ENV !== 'test') {
|
|
399
578
|
process.exit(1);
|
|
400
579
|
}
|
|
401
580
|
}
|
|
402
581
|
}
|
|
403
582
|
async handleRefactorCommand(action, options) {
|
|
404
|
-
|
|
405
|
-
|
|
583
|
+
// 支援 --path 作為 --file 的別名
|
|
584
|
+
const fileOption = options.file || options.path;
|
|
585
|
+
if (!fileOption) {
|
|
586
|
+
console.error('❌ 必須指定 --file 或 --path 參數');
|
|
587
|
+
process.exitCode = 1;
|
|
406
588
|
if (process.env.NODE_ENV !== 'test') {
|
|
407
589
|
process.exit(1);
|
|
408
590
|
}
|
|
409
591
|
return;
|
|
410
592
|
}
|
|
411
|
-
|
|
593
|
+
// 支援 --new-name 作為 --function-name 的別名
|
|
594
|
+
const functionNameOption = options.functionName || options.newName;
|
|
595
|
+
const isJsonFormat = options.format === 'json';
|
|
596
|
+
if (!isJsonFormat) {
|
|
597
|
+
console.log(`🔧 重構: ${action}`);
|
|
598
|
+
}
|
|
412
599
|
try {
|
|
413
|
-
const filePath = path.resolve(
|
|
414
|
-
if (action === 'extract-function') {
|
|
415
|
-
if (!options.startLine || !options.endLine || !
|
|
416
|
-
console.error(
|
|
600
|
+
const filePath = path.resolve(fileOption);
|
|
601
|
+
if (action === 'extract-function' || action === 'extract-closure') {
|
|
602
|
+
if (!options.startLine || !options.endLine || !functionNameOption) {
|
|
603
|
+
console.error(`❌ ${action} 缺少必要參數: --start-line, --end-line 和 --function-name (或 --new-name)`);
|
|
604
|
+
process.exitCode = 1;
|
|
605
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
// 驗證行號範圍
|
|
611
|
+
const startLine = parseInt(options.startLine);
|
|
612
|
+
const endLine = parseInt(options.endLine);
|
|
613
|
+
if (startLine > endLine) {
|
|
614
|
+
console.error(`❌ 無效的行號範圍: 起始行號 (${startLine}) 大於結束行號 (${endLine})`);
|
|
615
|
+
process.exitCode = 1;
|
|
417
616
|
if (process.env.NODE_ENV !== 'test') {
|
|
418
617
|
process.exit(1);
|
|
419
618
|
}
|
|
@@ -421,18 +620,80 @@ export class AgentIdeCLI {
|
|
|
421
620
|
}
|
|
422
621
|
// 讀取檔案內容
|
|
423
622
|
const fs = await import('fs/promises');
|
|
623
|
+
// 檢查檔案是否存在
|
|
624
|
+
try {
|
|
625
|
+
await fs.access(filePath);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
console.error(`❌ 找不到檔案: ${filePath}`);
|
|
629
|
+
process.exitCode = 1;
|
|
630
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
424
635
|
const code = await fs.readFile(filePath, 'utf-8');
|
|
425
636
|
// 建立範圍
|
|
426
637
|
const range = {
|
|
427
|
-
start: { line:
|
|
428
|
-
end: { line:
|
|
638
|
+
start: { line: startLine, column: 0 },
|
|
639
|
+
end: { line: endLine, column: 0 }
|
|
429
640
|
};
|
|
430
|
-
//
|
|
641
|
+
// 檢測檔案類型
|
|
642
|
+
const isSwift = filePath.endsWith('.swift');
|
|
643
|
+
if (isSwift) {
|
|
644
|
+
// 使用 Swift 提取器
|
|
645
|
+
const { SwiftExtractor } = await import('../../core/refactor/swift-extractor.js');
|
|
646
|
+
const extractor = new SwiftExtractor();
|
|
647
|
+
const extractConfig = {
|
|
648
|
+
functionName: functionNameOption,
|
|
649
|
+
generateComments: true,
|
|
650
|
+
preserveFormatting: true
|
|
651
|
+
};
|
|
652
|
+
const result = action === 'extract-closure'
|
|
653
|
+
? await extractor.extractClosure(code, range, extractConfig)
|
|
654
|
+
: await extractor.extractFunction(code, range, extractConfig);
|
|
655
|
+
if (result.success) {
|
|
656
|
+
if (isJsonFormat) {
|
|
657
|
+
console.log(JSON.stringify({
|
|
658
|
+
success: true,
|
|
659
|
+
extractedFunction: result.extractedFunction
|
|
660
|
+
}, null, 2));
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
console.log('✅ 重構完成');
|
|
664
|
+
console.log(`📝 提取的函式: ${result.extractedFunction.signature}`);
|
|
665
|
+
}
|
|
666
|
+
if (!options.preview) {
|
|
667
|
+
await fs.writeFile(filePath, result.modifiedCode, 'utf-8');
|
|
668
|
+
if (!isJsonFormat) {
|
|
669
|
+
console.log(`✓ 已更新 ${filePath}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
if (!isJsonFormat) {
|
|
674
|
+
console.log('預覽模式 - 未寫入檔案');
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
if (isJsonFormat) {
|
|
680
|
+
console.error(JSON.stringify({ success: false, errors: result.errors }));
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
console.error('❌ 重構失敗:', result.errors.join(', '));
|
|
684
|
+
}
|
|
685
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
// TypeScript/JavaScript 提取器(原有邏輯)
|
|
431
692
|
const { FunctionExtractor } = await import('../../core/refactor/extract-function.js');
|
|
432
693
|
const extractor = new FunctionExtractor();
|
|
433
694
|
// 執行提取
|
|
434
695
|
const extractConfig = {
|
|
435
|
-
functionName:
|
|
696
|
+
functionName: functionNameOption,
|
|
436
697
|
generateComments: true,
|
|
437
698
|
preserveFormatting: true,
|
|
438
699
|
validateExtraction: true,
|
|
@@ -499,12 +760,14 @@ export class AgentIdeCLI {
|
|
|
499
760
|
}
|
|
500
761
|
else if (action === 'inline-function') {
|
|
501
762
|
console.error('❌ inline-function 尚未實作');
|
|
763
|
+
process.exitCode = 1;
|
|
502
764
|
if (process.env.NODE_ENV !== 'test') {
|
|
503
765
|
process.exit(1);
|
|
504
766
|
}
|
|
505
767
|
}
|
|
506
768
|
else {
|
|
507
769
|
console.error(`❌ 未知的重構操作: ${action}`);
|
|
770
|
+
process.exitCode = 1;
|
|
508
771
|
if (process.env.NODE_ENV !== 'test') {
|
|
509
772
|
process.exit(1);
|
|
510
773
|
}
|
|
@@ -512,21 +775,54 @@ export class AgentIdeCLI {
|
|
|
512
775
|
}
|
|
513
776
|
catch (error) {
|
|
514
777
|
console.error('❌ 重構失敗:', error instanceof Error ? error.message : error);
|
|
778
|
+
process.exitCode = 1;
|
|
515
779
|
if (process.env.NODE_ENV !== 'test') {
|
|
516
780
|
process.exit(1);
|
|
517
781
|
}
|
|
518
782
|
}
|
|
519
783
|
}
|
|
520
784
|
async handleMoveCommand(source, target, options) {
|
|
521
|
-
|
|
785
|
+
const isJsonFormat = options.format === 'json';
|
|
786
|
+
if (!isJsonFormat) {
|
|
787
|
+
console.log(`📦 移動 ${source} → ${target}`);
|
|
788
|
+
}
|
|
522
789
|
try {
|
|
523
790
|
// 檢查源檔案是否存在
|
|
524
791
|
const sourceExists = await this.fileExists(source);
|
|
525
792
|
if (!sourceExists) {
|
|
526
|
-
|
|
793
|
+
const errorMsg = `源檔案找不到: ${source}`;
|
|
794
|
+
if (isJsonFormat) {
|
|
795
|
+
console.log(JSON.stringify({
|
|
796
|
+
success: false,
|
|
797
|
+
error: errorMsg
|
|
798
|
+
}, null, 2));
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
console.log(`❌ 移動失敗: ${errorMsg}`);
|
|
802
|
+
}
|
|
803
|
+
process.exitCode = 1;
|
|
527
804
|
if (process.env.NODE_ENV !== 'test') {
|
|
528
805
|
process.exit(1);
|
|
529
806
|
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
// 檢查源和目標是否相同
|
|
810
|
+
const normalizedSource = path.resolve(source);
|
|
811
|
+
const normalizedTarget = path.resolve(target);
|
|
812
|
+
if (normalizedSource === normalizedTarget) {
|
|
813
|
+
// 源和目標相同時,視為 no-op,成功返回
|
|
814
|
+
const message = 'Source and target are identical. No changes made.';
|
|
815
|
+
if (isJsonFormat) {
|
|
816
|
+
console.log(JSON.stringify({
|
|
817
|
+
success: true,
|
|
818
|
+
message,
|
|
819
|
+
changes: []
|
|
820
|
+
}, null, 2));
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
console.log(`✓ ${message}`);
|
|
824
|
+
}
|
|
825
|
+
return;
|
|
530
826
|
}
|
|
531
827
|
// 初始化移動服務
|
|
532
828
|
if (!this.moveService) {
|
|
@@ -534,13 +830,13 @@ export class AgentIdeCLI {
|
|
|
534
830
|
const pathAliases = await this.loadPathAliases(process.cwd());
|
|
535
831
|
this.moveService = new MoveService({
|
|
536
832
|
pathAliases,
|
|
537
|
-
supportedExtensions: ['.ts', '.tsx', '.js', '.jsx', '.vue'],
|
|
833
|
+
supportedExtensions: ['.ts', '.tsx', '.js', '.jsx', '.vue', '.swift'],
|
|
538
834
|
includeNodeModules: false
|
|
539
835
|
});
|
|
540
836
|
}
|
|
541
837
|
const moveOperation = {
|
|
542
|
-
source:
|
|
543
|
-
target:
|
|
838
|
+
source: normalizedSource,
|
|
839
|
+
target: normalizedTarget,
|
|
544
840
|
updateImports: options.updateImports
|
|
545
841
|
};
|
|
546
842
|
const moveOptions = {
|
|
@@ -550,39 +846,67 @@ export class AgentIdeCLI {
|
|
|
550
846
|
// 執行移動操作
|
|
551
847
|
const result = await this.moveService.moveFile(moveOperation, moveOptions);
|
|
552
848
|
if (result.success) {
|
|
553
|
-
if (
|
|
554
|
-
console.log(
|
|
849
|
+
if (isJsonFormat) {
|
|
850
|
+
console.log(JSON.stringify({
|
|
851
|
+
moved: result.moved,
|
|
852
|
+
affectedFiles: result.pathUpdates.length,
|
|
853
|
+
pathUpdates: result.pathUpdates
|
|
854
|
+
}, null, 2));
|
|
555
855
|
}
|
|
556
856
|
else {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
result.pathUpdates.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
console.log(` 第 ${update.line} 行: "${path.basename(source)}" → "${path.basename(target)}"`);
|
|
857
|
+
if (options.preview) {
|
|
858
|
+
console.log('🔍 預覽移動操作:');
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
console.log('✅ 移動成功!');
|
|
862
|
+
}
|
|
863
|
+
console.log(`📊 統計: ${result.pathUpdates.length} 個 import 需要更新`);
|
|
864
|
+
if (result.pathUpdates.length > 0) {
|
|
865
|
+
console.log('📝 影響的檔案:');
|
|
866
|
+
const fileGroups = new Map();
|
|
867
|
+
result.pathUpdates.forEach(update => {
|
|
868
|
+
if (!fileGroups.has(update.filePath)) {
|
|
869
|
+
fileGroups.set(update.filePath, []);
|
|
870
|
+
}
|
|
871
|
+
fileGroups.get(update.filePath).push(update);
|
|
573
872
|
});
|
|
873
|
+
for (const [filePath, updates] of fileGroups) {
|
|
874
|
+
console.log(` 📄 ${path.relative(process.cwd(), filePath)}:`);
|
|
875
|
+
updates.forEach(update => {
|
|
876
|
+
console.log(` 第 ${update.line} 行: "${path.basename(source)}" → "${path.basename(target)}"`);
|
|
877
|
+
});
|
|
878
|
+
}
|
|
574
879
|
}
|
|
575
880
|
}
|
|
576
881
|
}
|
|
577
882
|
else {
|
|
578
|
-
|
|
883
|
+
if (isJsonFormat) {
|
|
884
|
+
console.log(JSON.stringify({
|
|
885
|
+
success: false,
|
|
886
|
+
error: result.error
|
|
887
|
+
}, null, 2));
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
console.error('❌ 移動失敗:', result.error);
|
|
891
|
+
}
|
|
892
|
+
process.exitCode = 1;
|
|
579
893
|
if (process.env.NODE_ENV !== 'test') {
|
|
580
894
|
process.exit(1);
|
|
581
895
|
}
|
|
582
896
|
}
|
|
583
897
|
}
|
|
584
898
|
catch (error) {
|
|
585
|
-
|
|
899
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
900
|
+
if (isJsonFormat) {
|
|
901
|
+
console.log(JSON.stringify({
|
|
902
|
+
success: false,
|
|
903
|
+
error: errorMsg
|
|
904
|
+
}, null, 2));
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
console.error('❌ 移動失敗:', errorMsg);
|
|
908
|
+
}
|
|
909
|
+
process.exitCode = 1;
|
|
586
910
|
if (process.env.NODE_ENV !== 'test') {
|
|
587
911
|
process.exit(1);
|
|
588
912
|
}
|
|
@@ -655,8 +979,12 @@ export class AgentIdeCLI {
|
|
|
655
979
|
* 建構搜尋選項
|
|
656
980
|
*/
|
|
657
981
|
buildSearchOptions(options) {
|
|
658
|
-
|
|
982
|
+
let includeFiles = options.include ? options.include.split(',') : undefined;
|
|
659
983
|
const excludeFiles = options.exclude ? options.exclude.split(',') : undefined;
|
|
984
|
+
// --file-pattern 參數轉換為 includeFiles
|
|
985
|
+
if (options.filePattern) {
|
|
986
|
+
includeFiles = [options.filePattern];
|
|
987
|
+
}
|
|
660
988
|
return {
|
|
661
989
|
scope: {
|
|
662
990
|
type: 'directory',
|
|
@@ -664,9 +992,9 @@ export class AgentIdeCLI {
|
|
|
664
992
|
recursive: true
|
|
665
993
|
},
|
|
666
994
|
maxResults: parseInt(options.limit),
|
|
667
|
-
caseSensitive: options.caseSensitive || false,
|
|
995
|
+
caseSensitive: options.caseInsensitive ? false : (options.caseSensitive || false),
|
|
668
996
|
wholeWord: options.wholeWord || false,
|
|
669
|
-
regex: options.type === 'regex',
|
|
997
|
+
regex: options.regex || options.type === 'regex',
|
|
670
998
|
fuzzy: options.type === 'fuzzy',
|
|
671
999
|
multiline: options.multiline || false,
|
|
672
1000
|
showContext: options.context > 0,
|
|
@@ -683,11 +1011,21 @@ export class AgentIdeCLI {
|
|
|
683
1011
|
switch (options.format) {
|
|
684
1012
|
case 'json':
|
|
685
1013
|
// 測試期望的格式是 { results: [...] } 而不是 { matches: [...] }
|
|
686
|
-
//
|
|
687
|
-
const resultsWithRelativePaths = result.matches.map((match) =>
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1014
|
+
// 將絕對路徑轉換為相對路徑,並增加 contextBefore/contextAfter
|
|
1015
|
+
const resultsWithRelativePaths = result.matches.map((match) => {
|
|
1016
|
+
const formatted = {
|
|
1017
|
+
...match,
|
|
1018
|
+
filePath: this.formatFilePath(match.file)
|
|
1019
|
+
};
|
|
1020
|
+
// 移除 'file'
|
|
1021
|
+
delete formatted.file;
|
|
1022
|
+
// 增加 contextBefore/contextAfter(測試需要這些欄位)
|
|
1023
|
+
if (match.context) {
|
|
1024
|
+
formatted.contextBefore = match.context.before || [];
|
|
1025
|
+
formatted.contextAfter = match.context.after || [];
|
|
1026
|
+
}
|
|
1027
|
+
return formatted;
|
|
1028
|
+
});
|
|
691
1029
|
console.log(JSON.stringify({ results: resultsWithRelativePaths }, null, 2));
|
|
692
1030
|
break;
|
|
693
1031
|
case 'minimal':
|
|
@@ -745,6 +1083,283 @@ export class AgentIdeCLI {
|
|
|
745
1083
|
return text;
|
|
746
1084
|
}
|
|
747
1085
|
}
|
|
1086
|
+
/**
|
|
1087
|
+
* 處理文字搜尋命令
|
|
1088
|
+
*/
|
|
1089
|
+
async handleTextSearchCommand(options) {
|
|
1090
|
+
const query = options.query;
|
|
1091
|
+
if (!query) {
|
|
1092
|
+
console.error('❌ 文字搜尋需要指定 --query 參數');
|
|
1093
|
+
console.error(' 使用方式: agent-ide search text --query <text>');
|
|
1094
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
1095
|
+
process.exit(1);
|
|
1096
|
+
}
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
// 使用既有的 handleSearchCommand 邏輯
|
|
1100
|
+
await this.handleSearchCommand(query, options);
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* 處理結構化搜尋命令
|
|
1104
|
+
*/
|
|
1105
|
+
async handleStructuralSearchCommand(options) {
|
|
1106
|
+
const pattern = options.pattern;
|
|
1107
|
+
const type = options.type;
|
|
1108
|
+
if (!type) {
|
|
1109
|
+
console.error('❌ 結構化搜尋需要指定 --type 參數');
|
|
1110
|
+
console.error(' 使用方式: agent-ide search structural --type <class|protocol|function|...> [--pattern <pattern>]');
|
|
1111
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
1112
|
+
process.exit(1);
|
|
1113
|
+
}
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const isMinimalOrJson = options.format === 'minimal' || options.format === 'json';
|
|
1117
|
+
if (!isMinimalOrJson) {
|
|
1118
|
+
console.log(`🔍 結構化搜尋: ${type}${pattern ? ` (pattern: ${pattern})` : ''}`);
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
const searchPath = path.resolve(options.path || process.cwd());
|
|
1122
|
+
// 初始化索引引擎
|
|
1123
|
+
const config = createIndexConfig(searchPath, {
|
|
1124
|
+
includeExtensions: ['.ts', '.tsx', '.js', '.jsx', '.swift'],
|
|
1125
|
+
excludePatterns: ['node_modules/**', '*.test.*', 'dist/**']
|
|
1126
|
+
});
|
|
1127
|
+
this.indexEngine = new IndexEngine(config);
|
|
1128
|
+
// 建立索引
|
|
1129
|
+
if (!isMinimalOrJson) {
|
|
1130
|
+
console.log('📝 正在建立索引...');
|
|
1131
|
+
}
|
|
1132
|
+
await this.indexEngine.indexProject(searchPath);
|
|
1133
|
+
// 獲取所有符號
|
|
1134
|
+
const allSymbols = await this.indexEngine.getAllSymbols();
|
|
1135
|
+
// 過濾符號
|
|
1136
|
+
let filteredSymbols = allSymbols.filter(symbolResult => {
|
|
1137
|
+
const symbol = symbolResult.symbol;
|
|
1138
|
+
// 1. 過濾檔案模式
|
|
1139
|
+
if (options.filePattern) {
|
|
1140
|
+
const regex = new RegExp(options.filePattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
|
|
1141
|
+
if (!regex.test(symbol.location.filePath)) {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
// 2. 過濾符號類型
|
|
1146
|
+
if (type) {
|
|
1147
|
+
if (symbol.type !== type) {
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
// 3. 過濾符號名稱模式
|
|
1152
|
+
if (pattern) {
|
|
1153
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
|
|
1154
|
+
if (!regex.test(symbol.name)) {
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
// 4. 過濾屬性
|
|
1159
|
+
if (options.withAttribute) {
|
|
1160
|
+
if (!symbol.attributes || !symbol.attributes.includes(options.withAttribute)) {
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
// 5. 過濾修飾符
|
|
1165
|
+
if (options.withModifier) {
|
|
1166
|
+
if (!symbol.modifiers || !symbol.modifiers.includes(options.withModifier)) {
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
// 6. 過濾實作的協定
|
|
1171
|
+
if (options.implements) {
|
|
1172
|
+
if (!symbol.implements || !symbol.implements.includes(options.implements)) {
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
// 7. 過濾繼承的類別
|
|
1177
|
+
if (options.extends) {
|
|
1178
|
+
if (symbol.superclass !== options.extends) {
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return true;
|
|
1183
|
+
});
|
|
1184
|
+
// 應用 limit
|
|
1185
|
+
const limit = options.limit ? parseInt(options.limit) : 50;
|
|
1186
|
+
if (filteredSymbols.length > limit) {
|
|
1187
|
+
filteredSymbols = filteredSymbols.slice(0, limit);
|
|
1188
|
+
}
|
|
1189
|
+
if (filteredSymbols.length === 0) {
|
|
1190
|
+
if (options.format === 'json') {
|
|
1191
|
+
console.log(JSON.stringify({ results: [] }, null, 2));
|
|
1192
|
+
}
|
|
1193
|
+
else if (!isMinimalOrJson) {
|
|
1194
|
+
console.log('📝 沒有找到符合條件的符號');
|
|
1195
|
+
}
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (!isMinimalOrJson && options.format !== 'summary') {
|
|
1199
|
+
console.log(`✅ 找到 ${filteredSymbols.length} 個符號`);
|
|
1200
|
+
}
|
|
1201
|
+
// 格式化輸出
|
|
1202
|
+
this.formatSymbolSearchResults(filteredSymbols, options);
|
|
1203
|
+
}
|
|
1204
|
+
catch (error) {
|
|
1205
|
+
if (isMinimalOrJson) {
|
|
1206
|
+
if (options.format === 'json') {
|
|
1207
|
+
console.log(JSON.stringify({
|
|
1208
|
+
results: [],
|
|
1209
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1210
|
+
}));
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
console.error(`Error: ${error instanceof Error ? error.message : error}`);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
console.error('❌ 結構化搜尋失敗:', error instanceof Error ? error.message : error);
|
|
1218
|
+
}
|
|
1219
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
1220
|
+
process.exit(1);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* 處理符號搜尋命令
|
|
1226
|
+
*/
|
|
1227
|
+
async handleSymbolSearchCommand(options) {
|
|
1228
|
+
const symbolName = options.query;
|
|
1229
|
+
if (!symbolName) {
|
|
1230
|
+
console.error('❌ 符號搜尋需要指定 --query 參數');
|
|
1231
|
+
console.error(' 使用方式: agent-ide search symbol --query <name>');
|
|
1232
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
1233
|
+
process.exit(1);
|
|
1234
|
+
}
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const isMinimalOrJson = options.format === 'minimal' || options.format === 'json';
|
|
1238
|
+
if (!isMinimalOrJson) {
|
|
1239
|
+
console.log(`🔍 搜尋符號: "${symbolName}"`);
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
const searchPath = path.resolve(options.path || process.cwd());
|
|
1243
|
+
// 初始化索引引擎(每次都重新建立以確保索引是最新的)
|
|
1244
|
+
const config = createIndexConfig(searchPath, {
|
|
1245
|
+
includeExtensions: ['.ts', '.tsx', '.js', '.jsx', '.swift'],
|
|
1246
|
+
excludePatterns: ['node_modules/**', '*.test.*', 'dist/**']
|
|
1247
|
+
});
|
|
1248
|
+
this.indexEngine = new IndexEngine(config);
|
|
1249
|
+
// 建立索引
|
|
1250
|
+
if (!isMinimalOrJson) {
|
|
1251
|
+
console.log('📝 正在建立索引...');
|
|
1252
|
+
}
|
|
1253
|
+
await this.indexEngine.indexProject(searchPath);
|
|
1254
|
+
// 搜尋符號:如果包含 wildcard,使用模式搜尋
|
|
1255
|
+
let results;
|
|
1256
|
+
if (symbolName.includes('*') || symbolName.includes('?')) {
|
|
1257
|
+
// Wildcard 模式搜尋
|
|
1258
|
+
const allSymbols = await this.indexEngine.getAllSymbols();
|
|
1259
|
+
const pattern = symbolName
|
|
1260
|
+
.replace(/\*/g, '.*')
|
|
1261
|
+
.replace(/\?/g, '.');
|
|
1262
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
1263
|
+
results = allSymbols.filter(result => regex.test(result.symbol.name));
|
|
1264
|
+
// 應用 limit
|
|
1265
|
+
const limit = options.limit ? parseInt(options.limit) : 50;
|
|
1266
|
+
if (results.length > limit) {
|
|
1267
|
+
results = results.slice(0, limit);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
// 精確名稱搜尋
|
|
1272
|
+
results = await this.indexEngine.findSymbol(symbolName);
|
|
1273
|
+
}
|
|
1274
|
+
if (results.length === 0) {
|
|
1275
|
+
if (options.format === 'json') {
|
|
1276
|
+
console.log(JSON.stringify({ results: [] }, null, 2));
|
|
1277
|
+
}
|
|
1278
|
+
else if (!isMinimalOrJson) {
|
|
1279
|
+
console.log(`📝 找不到符號 "${symbolName}"`);
|
|
1280
|
+
}
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (!isMinimalOrJson) {
|
|
1284
|
+
console.log(`✅ 找到 ${results.length} 個符號`);
|
|
1285
|
+
}
|
|
1286
|
+
// 格式化輸出
|
|
1287
|
+
this.formatSymbolSearchResults(results, options);
|
|
1288
|
+
}
|
|
1289
|
+
catch (error) {
|
|
1290
|
+
if (isMinimalOrJson) {
|
|
1291
|
+
if (options.format === 'json') {
|
|
1292
|
+
console.log(JSON.stringify({
|
|
1293
|
+
results: [],
|
|
1294
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1295
|
+
}));
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
console.error(`Error: ${error instanceof Error ? error.message : error}`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
console.error('❌ 符號搜尋失敗:', error instanceof Error ? error.message : error);
|
|
1303
|
+
}
|
|
1304
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
1305
|
+
process.exit(1);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* 格式化符號搜尋結果輸出
|
|
1311
|
+
*/
|
|
1312
|
+
formatSymbolSearchResults(results, options) {
|
|
1313
|
+
switch (options.format) {
|
|
1314
|
+
case 'json':
|
|
1315
|
+
// 轉換為測試期望的格式
|
|
1316
|
+
const formattedResults = results.map(result => {
|
|
1317
|
+
const formatted = {
|
|
1318
|
+
name: result.symbol.name,
|
|
1319
|
+
type: result.symbol.type,
|
|
1320
|
+
filePath: this.formatFilePath(result.symbol.location.filePath),
|
|
1321
|
+
line: result.symbol.location.range.start.line,
|
|
1322
|
+
column: result.symbol.location.range.start.column
|
|
1323
|
+
};
|
|
1324
|
+
// 只在有值時才加入可選欄位
|
|
1325
|
+
if (result.symbol.attributes && result.symbol.attributes.length > 0) {
|
|
1326
|
+
formatted.attributes = result.symbol.attributes;
|
|
1327
|
+
}
|
|
1328
|
+
if (result.symbol.modifiers && result.symbol.modifiers.length > 0) {
|
|
1329
|
+
formatted.modifiers = result.symbol.modifiers;
|
|
1330
|
+
}
|
|
1331
|
+
if (result.symbol.superclass) {
|
|
1332
|
+
formatted.superclass = result.symbol.superclass;
|
|
1333
|
+
}
|
|
1334
|
+
if (result.symbol.implements && result.symbol.implements.length > 0) {
|
|
1335
|
+
formatted.implements = result.symbol.implements;
|
|
1336
|
+
}
|
|
1337
|
+
return formatted;
|
|
1338
|
+
});
|
|
1339
|
+
console.log(JSON.stringify({ results: formattedResults }, null, 2));
|
|
1340
|
+
break;
|
|
1341
|
+
case 'minimal':
|
|
1342
|
+
results.forEach(result => {
|
|
1343
|
+
const symbol = result.symbol;
|
|
1344
|
+
console.log(`${symbol.location.filePath}:${symbol.location.range.start.line}:${symbol.location.range.start.column}:${symbol.type}:${symbol.name}`);
|
|
1345
|
+
});
|
|
1346
|
+
break;
|
|
1347
|
+
case 'list':
|
|
1348
|
+
default:
|
|
1349
|
+
results.forEach((result, index) => {
|
|
1350
|
+
const symbol = result.symbol;
|
|
1351
|
+
console.log(`\n${index + 1}. ${symbol.name} (${symbol.type})`);
|
|
1352
|
+
console.log(` ${this.formatFilePath(symbol.location.filePath)}:${symbol.location.range.start.line}:${symbol.location.range.start.column}`);
|
|
1353
|
+
if (symbol.attributes && symbol.attributes.length > 0) {
|
|
1354
|
+
console.log(` 屬性: ${symbol.attributes.join(', ')}`);
|
|
1355
|
+
}
|
|
1356
|
+
if (symbol.modifiers && symbol.modifiers.length > 0) {
|
|
1357
|
+
console.log(` 修飾符: ${symbol.modifiers.join(', ')}`);
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
break;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
748
1363
|
async handleAnalyzeCommand(type, options) {
|
|
749
1364
|
const analyzeType = type || 'complexity';
|
|
750
1365
|
if (options.format !== 'json') {
|
|
@@ -754,10 +1369,25 @@ export class AgentIdeCLI {
|
|
|
754
1369
|
const analyzePath = options.path || process.cwd();
|
|
755
1370
|
// 根據分析類型執行對應分析
|
|
756
1371
|
if (analyzeType === 'complexity') {
|
|
757
|
-
|
|
758
|
-
|
|
1372
|
+
// 使用 ParserPlugin 分析複雜度
|
|
1373
|
+
const registry = ParserRegistry.getInstance();
|
|
759
1374
|
const files = await this.getAllProjectFiles(analyzePath);
|
|
760
|
-
const results =
|
|
1375
|
+
const results = [];
|
|
1376
|
+
for (const file of files) {
|
|
1377
|
+
try {
|
|
1378
|
+
const parser = registry.getParser(path.extname(file));
|
|
1379
|
+
if (!parser) {
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
1383
|
+
const ast = await parser.parse(content, file);
|
|
1384
|
+
const complexity = await parser.analyzeComplexity(content, ast);
|
|
1385
|
+
results.push({ file, complexity });
|
|
1386
|
+
}
|
|
1387
|
+
catch {
|
|
1388
|
+
// 忽略無法分析的檔案
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
761
1391
|
// 過濾高複雜度檔案(evaluation === 'high' 或 complexity > 10)
|
|
762
1392
|
const highComplexityFiles = results.filter(r => r.complexity.evaluation === 'high' || r.complexity.cyclomaticComplexity > 10);
|
|
763
1393
|
// 計算統計資訊
|
|
@@ -775,18 +1405,16 @@ export class AgentIdeCLI {
|
|
|
775
1405
|
issuesFound: highComplexityFiles.length,
|
|
776
1406
|
averageComplexity,
|
|
777
1407
|
maxComplexity
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (options.all) {
|
|
781
|
-
outputData.all = results.map(r => ({
|
|
1408
|
+
},
|
|
1409
|
+
issues: highComplexityFiles.map(r => ({
|
|
782
1410
|
path: r.file,
|
|
783
1411
|
complexity: r.complexity.cyclomaticComplexity,
|
|
784
1412
|
cognitiveComplexity: r.complexity.cognitiveComplexity,
|
|
785
1413
|
evaluation: r.complexity.evaluation
|
|
786
|
-
}))
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
outputData.
|
|
1414
|
+
}))
|
|
1415
|
+
};
|
|
1416
|
+
if (options.all) {
|
|
1417
|
+
outputData.all = results.map(r => ({
|
|
790
1418
|
path: r.file,
|
|
791
1419
|
complexity: r.complexity.cyclomaticComplexity,
|
|
792
1420
|
cognitiveComplexity: r.complexity.cognitiveComplexity,
|
|
@@ -801,7 +1429,7 @@ export class AgentIdeCLI {
|
|
|
801
1429
|
console.log(` 平均複雜度: ${averageComplexity.toFixed(2)}`);
|
|
802
1430
|
console.log(` 最高複雜度: ${maxComplexity}`);
|
|
803
1431
|
if (!options.all && highComplexityFiles.length > 0) {
|
|
804
|
-
console.log(
|
|
1432
|
+
console.log('\n⚠️ 高複雜度檔案:');
|
|
805
1433
|
highComplexityFiles.forEach(r => {
|
|
806
1434
|
console.log(` - ${r.file}: ${r.complexity.cyclomaticComplexity}`);
|
|
807
1435
|
});
|
|
@@ -809,10 +1437,26 @@ export class AgentIdeCLI {
|
|
|
809
1437
|
}
|
|
810
1438
|
}
|
|
811
1439
|
else if (analyzeType === 'dead-code') {
|
|
812
|
-
|
|
813
|
-
|
|
1440
|
+
// 使用 ParserPlugin 檢測死代碼
|
|
1441
|
+
const registry = ParserRegistry.getInstance();
|
|
814
1442
|
const files = await this.getAllProjectFiles(analyzePath);
|
|
815
|
-
const results =
|
|
1443
|
+
const results = [];
|
|
1444
|
+
for (const file of files) {
|
|
1445
|
+
try {
|
|
1446
|
+
const parser = registry.getParser(path.extname(file));
|
|
1447
|
+
if (!parser) {
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
1451
|
+
const ast = await parser.parse(content, file);
|
|
1452
|
+
const symbols = await parser.extractSymbols(ast);
|
|
1453
|
+
const deadCode = await parser.detectUnusedSymbols(ast, symbols);
|
|
1454
|
+
results.push({ file, deadCode });
|
|
1455
|
+
}
|
|
1456
|
+
catch {
|
|
1457
|
+
// 忽略無法分析的檔案
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
816
1460
|
// 過濾有 dead code 的檔案
|
|
817
1461
|
const filesWithDeadCode = results.filter(r => r.deadCode.length > 0);
|
|
818
1462
|
// 統計結果
|
|
@@ -827,7 +1471,11 @@ export class AgentIdeCLI {
|
|
|
827
1471
|
totalDeadFunctions: deadFunctions.length,
|
|
828
1472
|
totalDeadVariables: deadVariables.length,
|
|
829
1473
|
totalDeadCode: allDeadCode.length
|
|
830
|
-
}
|
|
1474
|
+
},
|
|
1475
|
+
issues: filesWithDeadCode.map(r => ({
|
|
1476
|
+
path: r.file,
|
|
1477
|
+
deadCode: r.deadCode
|
|
1478
|
+
}))
|
|
831
1479
|
};
|
|
832
1480
|
if (options.all) {
|
|
833
1481
|
outputData.all = results.map(r => ({
|
|
@@ -835,12 +1483,6 @@ export class AgentIdeCLI {
|
|
|
835
1483
|
deadCode: r.deadCode
|
|
836
1484
|
}));
|
|
837
1485
|
}
|
|
838
|
-
else {
|
|
839
|
-
outputData.issues = filesWithDeadCode.map(r => ({
|
|
840
|
-
path: r.file,
|
|
841
|
-
deadCode: r.deadCode
|
|
842
|
-
}));
|
|
843
|
-
}
|
|
844
1486
|
outputData.deadFunctions = deadFunctions;
|
|
845
1487
|
outputData.deadVariables = deadVariables;
|
|
846
1488
|
console.log(JSON.stringify(outputData, null, 2));
|
|
@@ -852,7 +1494,7 @@ export class AgentIdeCLI {
|
|
|
852
1494
|
console.log(` 未使用函式: ${deadFunctions.length} 個`);
|
|
853
1495
|
console.log(` 未使用變數: ${deadVariables.length} 個`);
|
|
854
1496
|
if (!options.all && filesWithDeadCode.length > 0) {
|
|
855
|
-
console.log(
|
|
1497
|
+
console.log('\n⚠️ 有死代碼的檔案:');
|
|
856
1498
|
filesWithDeadCode.forEach(r => {
|
|
857
1499
|
console.log(` - ${r.file}: ${r.deadCode.length} 項`);
|
|
858
1500
|
});
|
|
@@ -936,27 +1578,507 @@ export class AgentIdeCLI {
|
|
|
936
1578
|
console.log(`📊 發現模式: ${patterns.join(', ')}`);
|
|
937
1579
|
}
|
|
938
1580
|
}
|
|
1581
|
+
else if (analyzeType === 'quality') {
|
|
1582
|
+
// 品質分析(整合多個維度)
|
|
1583
|
+
const registry = ParserRegistry.getInstance();
|
|
1584
|
+
const files = await this.getAllProjectFiles(analyzePath);
|
|
1585
|
+
// 檢查路徑是否存在或沒有找到檔案
|
|
1586
|
+
if (files.length === 0) {
|
|
1587
|
+
const pathExists = await this.fileExists(analyzePath);
|
|
1588
|
+
if (!pathExists) {
|
|
1589
|
+
throw new Error(`路徑不存在: ${analyzePath}`);
|
|
1590
|
+
}
|
|
1591
|
+
throw new Error(`在路徑 ${analyzePath} 中找不到支援的檔案`);
|
|
1592
|
+
}
|
|
1593
|
+
// 統計資料
|
|
1594
|
+
const summary = {
|
|
1595
|
+
totalScanned: files.length,
|
|
1596
|
+
totalIssues: 0,
|
|
1597
|
+
qualityScore: 0
|
|
1598
|
+
};
|
|
1599
|
+
// 各維度問題列表
|
|
1600
|
+
const allIssues = [];
|
|
1601
|
+
const recommendations = [];
|
|
1602
|
+
// 各維度分數(權重參考 ShitScore QA 維度)
|
|
1603
|
+
let typeSafetyScore = 100;
|
|
1604
|
+
let errorHandlingScore = 100;
|
|
1605
|
+
let securityScore = 100;
|
|
1606
|
+
let namingScore = 100;
|
|
1607
|
+
let testCoverageScore = 0;
|
|
1608
|
+
let testFileCount = 0;
|
|
1609
|
+
for (const file of files) {
|
|
1610
|
+
try {
|
|
1611
|
+
const parser = registry.getParser(path.extname(file));
|
|
1612
|
+
if (!parser) {
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
1616
|
+
const ast = await parser.parse(content, file);
|
|
1617
|
+
// 判斷是否為測試檔案
|
|
1618
|
+
if (parser.isTestFile && parser.isTestFile(file)) {
|
|
1619
|
+
testFileCount++;
|
|
1620
|
+
continue; // 跳過測試檔案
|
|
1621
|
+
}
|
|
1622
|
+
// 1. 型別安全檢測
|
|
1623
|
+
if (parser.checkTypeSafety) {
|
|
1624
|
+
const typeSafetyIssues = await parser.checkTypeSafety(content, ast);
|
|
1625
|
+
typeSafetyIssues.forEach((issue) => {
|
|
1626
|
+
allIssues.push({
|
|
1627
|
+
type: 'type-safety',
|
|
1628
|
+
severity: issue.severity === 'error' ? 'high' : 'medium',
|
|
1629
|
+
message: issue.message,
|
|
1630
|
+
filePath: issue.location.filePath,
|
|
1631
|
+
line: issue.location.line
|
|
1632
|
+
});
|
|
1633
|
+
typeSafetyScore -= issue.severity === 'error' ? 10 : 5;
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
// 2. 錯誤處理檢測
|
|
1637
|
+
if (parser.checkErrorHandling) {
|
|
1638
|
+
const errorHandlingIssues = await parser.checkErrorHandling(content, ast);
|
|
1639
|
+
errorHandlingIssues.forEach((issue) => {
|
|
1640
|
+
allIssues.push({
|
|
1641
|
+
type: 'error-handling',
|
|
1642
|
+
severity: issue.severity === 'error' ? 'high' : 'medium',
|
|
1643
|
+
message: issue.message,
|
|
1644
|
+
filePath: issue.location.filePath,
|
|
1645
|
+
line: issue.location.line
|
|
1646
|
+
});
|
|
1647
|
+
errorHandlingScore -= issue.severity === 'error' ? 10 : 5;
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
// 3. 安全性檢測
|
|
1651
|
+
if (parser.checkSecurity) {
|
|
1652
|
+
const securityIssues = await parser.checkSecurity(content, ast);
|
|
1653
|
+
securityIssues.forEach((issue) => {
|
|
1654
|
+
allIssues.push({
|
|
1655
|
+
type: 'security',
|
|
1656
|
+
severity: issue.severity === 'critical' ? 'high' : 'medium',
|
|
1657
|
+
message: issue.message,
|
|
1658
|
+
filePath: issue.location.filePath,
|
|
1659
|
+
line: issue.location.line
|
|
1660
|
+
});
|
|
1661
|
+
securityScore -= issue.severity === 'critical' ? 15 : 10;
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
// 4. 命名規範檢測
|
|
1665
|
+
if (parser.checkNamingConventions) {
|
|
1666
|
+
const symbols = await parser.extractSymbols(ast);
|
|
1667
|
+
const namingIssues = await parser.checkNamingConventions(symbols, file);
|
|
1668
|
+
namingIssues.forEach((issue) => {
|
|
1669
|
+
allIssues.push({
|
|
1670
|
+
type: 'naming',
|
|
1671
|
+
severity: 'low',
|
|
1672
|
+
message: issue.message,
|
|
1673
|
+
filePath: issue.location.filePath,
|
|
1674
|
+
line: issue.location.line
|
|
1675
|
+
});
|
|
1676
|
+
namingScore -= 3;
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
catch {
|
|
1681
|
+
// 忽略無法分析的檔案
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
// 5. 測試覆蓋率評估
|
|
1685
|
+
const testFileRatio = files.length > 0 ? testFileCount / files.length : 0;
|
|
1686
|
+
testCoverageScore = Math.min(100, testFileRatio * 200); // 50% 測試覆蓋率 = 100 分
|
|
1687
|
+
// 確保分數不低於 0
|
|
1688
|
+
typeSafetyScore = Math.max(0, typeSafetyScore);
|
|
1689
|
+
errorHandlingScore = Math.max(0, errorHandlingScore);
|
|
1690
|
+
securityScore = Math.max(0, securityScore);
|
|
1691
|
+
namingScore = Math.max(0, namingScore);
|
|
1692
|
+
// 計算整體品質評分(加權平均,參考 ShitScore QA 維度權重)
|
|
1693
|
+
const overallScore = Math.round(typeSafetyScore * 0.30 + // Type Safety 30%
|
|
1694
|
+
testCoverageScore * 0.25 + // Test Coverage 25%
|
|
1695
|
+
errorHandlingScore * 0.20 + // Error Handling 20%
|
|
1696
|
+
namingScore * 0.15 + // Naming 15%
|
|
1697
|
+
securityScore * 0.10 // Security 10%
|
|
1698
|
+
);
|
|
1699
|
+
summary.totalIssues = allIssues.length;
|
|
1700
|
+
summary.qualityScore = overallScore;
|
|
1701
|
+
// 產生改善建議
|
|
1702
|
+
if (typeSafetyScore < 80) {
|
|
1703
|
+
recommendations.push('型別安全:建議使用可選綁定(if let, guard let)代替強制解包');
|
|
1704
|
+
}
|
|
1705
|
+
if (errorHandlingScore < 80) {
|
|
1706
|
+
recommendations.push('錯誤處理:建議使用 do-catch 明確處理錯誤,避免空 catch 區塊');
|
|
1707
|
+
}
|
|
1708
|
+
if (securityScore < 80) {
|
|
1709
|
+
recommendations.push('安全性:建議使用 Keychain 或環境變數儲存敏感資訊');
|
|
1710
|
+
}
|
|
1711
|
+
if (namingScore < 80) {
|
|
1712
|
+
recommendations.push('命名規範:建議遵循 Swift API Design Guidelines 命名規範');
|
|
1713
|
+
}
|
|
1714
|
+
if (testCoverageScore < 50) {
|
|
1715
|
+
recommendations.push('測試覆蓋率:建議提升測試覆蓋率至 50% 以上');
|
|
1716
|
+
}
|
|
1717
|
+
if (options.format === 'json') {
|
|
1718
|
+
console.log(JSON.stringify({
|
|
1719
|
+
summary,
|
|
1720
|
+
issues: allIssues,
|
|
1721
|
+
complexity: {
|
|
1722
|
+
score: 100 // 預留位置(可選擇整合複雜度分析)
|
|
1723
|
+
},
|
|
1724
|
+
maintainability: {
|
|
1725
|
+
score: 100 // 預留位置(可選擇整合維護性分析)
|
|
1726
|
+
},
|
|
1727
|
+
typeSafety: {
|
|
1728
|
+
score: typeSafetyScore,
|
|
1729
|
+
issues: allIssues.filter((i) => i.type === 'type-safety')
|
|
1730
|
+
},
|
|
1731
|
+
errorHandling: {
|
|
1732
|
+
score: errorHandlingScore,
|
|
1733
|
+
issues: allIssues.filter((i) => i.type === 'error-handling')
|
|
1734
|
+
},
|
|
1735
|
+
security: {
|
|
1736
|
+
score: securityScore,
|
|
1737
|
+
issues: allIssues.filter((i) => i.type === 'security')
|
|
1738
|
+
},
|
|
1739
|
+
namingConventions: {
|
|
1740
|
+
score: namingScore,
|
|
1741
|
+
issues: allIssues.filter((i) => i.type === 'naming')
|
|
1742
|
+
},
|
|
1743
|
+
testCoverage: {
|
|
1744
|
+
score: testCoverageScore,
|
|
1745
|
+
testFileRatio,
|
|
1746
|
+
testFiles: testFileCount,
|
|
1747
|
+
totalFiles: files.length
|
|
1748
|
+
},
|
|
1749
|
+
overallScore,
|
|
1750
|
+
recommendations
|
|
1751
|
+
}, null, 2));
|
|
1752
|
+
}
|
|
1753
|
+
else {
|
|
1754
|
+
console.log('✅ 品質分析完成!');
|
|
1755
|
+
console.log(`📊 整體評分: ${overallScore}/100`);
|
|
1756
|
+
console.log(` 總問題數: ${summary.totalIssues}`);
|
|
1757
|
+
console.log('\n維度評分:');
|
|
1758
|
+
console.log(` 型別安全: ${typeSafetyScore.toFixed(1)}/100`);
|
|
1759
|
+
console.log(` 錯誤處理: ${errorHandlingScore.toFixed(1)}/100`);
|
|
1760
|
+
console.log(` 安全性: ${securityScore.toFixed(1)}/100`);
|
|
1761
|
+
console.log(` 命名規範: ${namingScore.toFixed(1)}/100`);
|
|
1762
|
+
console.log(` 測試覆蓋率: ${testCoverageScore.toFixed(1)}/100 (${(testFileRatio * 100).toFixed(1)}%)`);
|
|
1763
|
+
if (recommendations.length > 0) {
|
|
1764
|
+
console.log('\n改善建議:');
|
|
1765
|
+
recommendations.forEach((rec, index) => {
|
|
1766
|
+
console.log(` ${index + 1}. ${rec}`);
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
939
1771
|
else {
|
|
940
1772
|
throw new Error(`不支援的分析類型: ${analyzeType}`);
|
|
941
1773
|
}
|
|
942
1774
|
}
|
|
943
1775
|
catch (error) {
|
|
1776
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
944
1777
|
if (options.format === 'json') {
|
|
945
|
-
console.
|
|
1778
|
+
console.error(JSON.stringify({ error: errorMessage }));
|
|
946
1779
|
}
|
|
947
1780
|
else {
|
|
948
|
-
console.error('❌ 分析失敗:',
|
|
1781
|
+
console.error('❌ 分析失敗:', errorMessage);
|
|
949
1782
|
}
|
|
1783
|
+
process.exitCode = 1;
|
|
950
1784
|
if (process.env.NODE_ENV !== 'test') {
|
|
951
|
-
|
|
952
|
-
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
async handleShitCommand(options) {
|
|
1790
|
+
if (options.format !== 'json') {
|
|
1791
|
+
console.log('💩 分析程式碼垃圾度...');
|
|
1792
|
+
}
|
|
1793
|
+
try {
|
|
1794
|
+
const analyzePath = options.path || process.cwd();
|
|
1795
|
+
const topCount = parseInt(options.top) || 10;
|
|
1796
|
+
const maxAllowed = options.maxAllowed ? parseFloat(options.maxAllowed) : undefined;
|
|
1797
|
+
const registry = ParserRegistry.getInstance();
|
|
1798
|
+
const analyzer = new ShitScoreAnalyzer(registry);
|
|
1799
|
+
const result = await analyzer.analyze(analyzePath, {
|
|
1800
|
+
detailed: options.detailed,
|
|
1801
|
+
topCount,
|
|
1802
|
+
maxAllowed,
|
|
1803
|
+
showFiles: options.showFiles
|
|
1804
|
+
});
|
|
1805
|
+
if (options.format === 'json') {
|
|
1806
|
+
const output = JSON.stringify(result, null, 2);
|
|
1807
|
+
if (options.output) {
|
|
1808
|
+
await fs.writeFile(options.output, output, 'utf-8');
|
|
1809
|
+
console.log(`✅ 結果已儲存至 ${options.output}`);
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
console.log(output);
|
|
953
1813
|
}
|
|
954
1814
|
}
|
|
1815
|
+
else {
|
|
1816
|
+
console.log('\n' + '='.repeat(50));
|
|
1817
|
+
console.log(`垃圾度評分報告 ${result.gradeInfo.emoji}`);
|
|
1818
|
+
console.log('='.repeat(50));
|
|
1819
|
+
console.log(`\n總分: ${result.shitScore} / 100 [${result.gradeInfo.emoji} ${result.grade}級]`);
|
|
1820
|
+
console.log(`評語: ${result.gradeInfo.message}\n`);
|
|
1821
|
+
console.log('維度分析:');
|
|
1822
|
+
console.log(` 複雜度垃圾: ${result.dimensions.complexity.score.toFixed(1)} (${(result.dimensions.complexity.weight * 100).toFixed(0)}%) → 貢獻 ${result.dimensions.complexity.weightedScore.toFixed(1)} 分`);
|
|
1823
|
+
console.log(` 維護性垃圾: ${result.dimensions.maintainability.score.toFixed(1)} (${(result.dimensions.maintainability.weight * 100).toFixed(0)}%) → 貢獻 ${result.dimensions.maintainability.weightedScore.toFixed(1)} 分`);
|
|
1824
|
+
console.log(` 架構垃圾: ${result.dimensions.architecture.score.toFixed(1)} (${(result.dimensions.architecture.weight * 100).toFixed(0)}%) → 貢獻 ${result.dimensions.architecture.weightedScore.toFixed(1)} 分\n`);
|
|
1825
|
+
const criticalCount = result.topShit ? result.topShit.filter(s => s.severity === 'critical').length : 0;
|
|
1826
|
+
const highCount = result.topShit ? result.topShit.filter(s => s.severity === 'high').length : 0;
|
|
1827
|
+
const mediumCount = result.topShit ? result.topShit.filter(s => s.severity === 'medium').length : 0;
|
|
1828
|
+
const lowCount = result.topShit ? result.topShit.filter(s => s.severity === 'low').length : 0;
|
|
1829
|
+
console.log('問題統計:');
|
|
1830
|
+
console.log(` 🔴 嚴重問題: ${criticalCount} 個`);
|
|
1831
|
+
console.log(` 🟠 高優先級: ${highCount} 個`);
|
|
1832
|
+
console.log(` 🟡 中優先級: ${mediumCount} 個`);
|
|
1833
|
+
console.log(` 🟢 低優先級: ${lowCount} 個\n`);
|
|
1834
|
+
console.log(`掃描檔案: ${result.summary.analyzedFiles} 個(共 ${result.summary.totalFiles} 個)`);
|
|
1835
|
+
console.log(`總問題數: ${result.summary.totalShit} 個`);
|
|
1836
|
+
if (options.detailed && result.topShit && result.topShit.length > 0) {
|
|
1837
|
+
console.log('\n' + '='.repeat(50));
|
|
1838
|
+
console.log(`最糟的 ${result.topShit.length} 個項目:`);
|
|
1839
|
+
console.log('='.repeat(50));
|
|
1840
|
+
result.topShit.forEach((item, index) => {
|
|
1841
|
+
console.log(`\n${index + 1}. [${item.severity.toUpperCase()}] ${item.type}`);
|
|
1842
|
+
console.log(` 檔案: ${item.filePath}${item.location ? `:${item.location.line}` : ''}`);
|
|
1843
|
+
console.log(` 分數: ${item.score.toFixed(1)}`);
|
|
1844
|
+
console.log(` 描述: ${item.description}`);
|
|
1845
|
+
});
|
|
1846
|
+
if (result.recommendations && result.recommendations.length > 0) {
|
|
1847
|
+
console.log('\n' + '='.repeat(50));
|
|
1848
|
+
console.log('修復建議:');
|
|
1849
|
+
console.log('='.repeat(50));
|
|
1850
|
+
result.recommendations.forEach((rec, index) => {
|
|
1851
|
+
console.log(`\n${index + 1}. [優先級 ${rec.priority}] ${rec.category}`);
|
|
1852
|
+
console.log(` 建議: ${rec.suggestion}`);
|
|
1853
|
+
console.log(` 預期改善: ${rec.estimatedImpact.toFixed(1)} 分`);
|
|
1854
|
+
console.log(` 影響檔案: ${rec.affectedFiles.length} 個`);
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
console.log('\n' + '='.repeat(50));
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
catch (error) {
|
|
1862
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1863
|
+
if (options.format === 'json') {
|
|
1864
|
+
console.error(JSON.stringify({ error: errorMessage }));
|
|
1865
|
+
}
|
|
1866
|
+
else {
|
|
1867
|
+
console.error('\n❌ 垃圾度分析失敗:', errorMessage);
|
|
1868
|
+
}
|
|
1869
|
+
process.exitCode = 1;
|
|
1870
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
1871
|
+
process.exit(1);
|
|
1872
|
+
}
|
|
955
1873
|
}
|
|
956
1874
|
}
|
|
957
|
-
async
|
|
1875
|
+
async handleSnapshotCommand(action, options) {
|
|
1876
|
+
const isJsonFormat = options.format === 'json';
|
|
1877
|
+
try {
|
|
1878
|
+
const projectPath = options.path || process.cwd();
|
|
1879
|
+
const configManager = new ConfigManager();
|
|
1880
|
+
// 讀取配置檔
|
|
1881
|
+
const projectConfig = await configManager.loadConfig(projectPath);
|
|
1882
|
+
// 合併選項
|
|
1883
|
+
const snapshotOptions = {
|
|
1884
|
+
projectPath,
|
|
1885
|
+
outputPath: options.output,
|
|
1886
|
+
incremental: options.incremental,
|
|
1887
|
+
level: options.level,
|
|
1888
|
+
includeTests: options.includeTests,
|
|
1889
|
+
multiLevel: options.multiLevel,
|
|
1890
|
+
outputDir: options.outputDir,
|
|
1891
|
+
silent: isJsonFormat
|
|
1892
|
+
};
|
|
1893
|
+
const finalOptions = configManager.mergeOptions(projectPath, snapshotOptions, projectConfig);
|
|
1894
|
+
// 如果沒有指定輸出路徑,使用預設值
|
|
1895
|
+
if (!finalOptions.outputPath) {
|
|
1896
|
+
finalOptions.outputPath = path.join(projectPath, '.agent-ide', 'snapshot.json');
|
|
1897
|
+
}
|
|
1898
|
+
const engine = new SnapshotEngine();
|
|
1899
|
+
switch (action) {
|
|
1900
|
+
case 'generate':
|
|
1901
|
+
await this.handleSnapshotGenerate(engine, finalOptions, isJsonFormat);
|
|
1902
|
+
break;
|
|
1903
|
+
case 'info':
|
|
1904
|
+
await this.handleSnapshotInfo(finalOptions, isJsonFormat);
|
|
1905
|
+
break;
|
|
1906
|
+
case 'diff':
|
|
1907
|
+
await this.handleSnapshotDiff(options, isJsonFormat);
|
|
1908
|
+
break;
|
|
1909
|
+
case 'init':
|
|
1910
|
+
await this.handleSnapshotInit(configManager, projectPath, isJsonFormat);
|
|
1911
|
+
break;
|
|
1912
|
+
default:
|
|
1913
|
+
// 預設執行生成
|
|
1914
|
+
await this.handleSnapshotGenerate(engine, finalOptions, isJsonFormat);
|
|
1915
|
+
break;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
catch (error) {
|
|
1919
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1920
|
+
if (isJsonFormat) {
|
|
1921
|
+
console.error(JSON.stringify({ error: errorMessage }));
|
|
1922
|
+
}
|
|
1923
|
+
else {
|
|
1924
|
+
console.error('\n❌ 快照操作失敗:', errorMessage);
|
|
1925
|
+
}
|
|
1926
|
+
process.exitCode = 1;
|
|
1927
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
1928
|
+
process.exit(1);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
async handleSnapshotGenerate(engine, options, isJsonFormat) {
|
|
1933
|
+
if (!isJsonFormat) {
|
|
1934
|
+
console.log('📸 生成程式碼快照...');
|
|
1935
|
+
if (options.incremental) {
|
|
1936
|
+
console.log(' 模式: 增量更新');
|
|
1937
|
+
}
|
|
1938
|
+
else {
|
|
1939
|
+
console.log(' 模式: 完整生成');
|
|
1940
|
+
}
|
|
1941
|
+
console.log(` 壓縮層級: ${options.level}`);
|
|
1942
|
+
}
|
|
1943
|
+
const startTime = Date.now();
|
|
1944
|
+
const snapshot = await engine.generate(options);
|
|
1945
|
+
const stats = engine.getStats(snapshot);
|
|
1946
|
+
const duration = Date.now() - startTime;
|
|
1947
|
+
stats.generationTime = duration;
|
|
1948
|
+
// 保存快照
|
|
1949
|
+
if (options.outputPath) {
|
|
1950
|
+
await engine.save(snapshot, options.outputPath);
|
|
1951
|
+
}
|
|
1952
|
+
// 如果是多層級模式,生成其他層級
|
|
1953
|
+
if (options.multiLevel && options.outputDir) {
|
|
1954
|
+
if (!isJsonFormat) {
|
|
1955
|
+
console.log('\n📚 生成多層級快照...');
|
|
1956
|
+
}
|
|
1957
|
+
const levels = [
|
|
1958
|
+
CompressionLevel.Minimal,
|
|
1959
|
+
CompressionLevel.Medium,
|
|
1960
|
+
CompressionLevel.Full
|
|
1961
|
+
];
|
|
1962
|
+
for (const level of levels) {
|
|
1963
|
+
const levelOptions = { ...options, level, incremental: false };
|
|
1964
|
+
const levelSnapshot = await engine.generate(levelOptions);
|
|
1965
|
+
const outputPath = path.join(options.outputDir, `snapshot-${level}.json`);
|
|
1966
|
+
await engine.save(levelSnapshot, outputPath);
|
|
1967
|
+
if (!isJsonFormat) {
|
|
1968
|
+
const levelStats = engine.getStats(levelSnapshot);
|
|
1969
|
+
console.log(` ✅ ${level}: ${levelStats.estimatedTokens} tokens`);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
if (isJsonFormat) {
|
|
1974
|
+
console.log(JSON.stringify({
|
|
1975
|
+
success: true,
|
|
1976
|
+
snapshot: options.outputPath,
|
|
1977
|
+
stats
|
|
1978
|
+
}, null, 2));
|
|
1979
|
+
}
|
|
1980
|
+
else {
|
|
1981
|
+
console.log('\n✅ 快照生成完成');
|
|
1982
|
+
console.log(` 輸出位置: ${options.outputPath}`);
|
|
1983
|
+
console.log('\n統計資訊:');
|
|
1984
|
+
console.log(` 檔案數量: ${stats.fileCount}`);
|
|
1985
|
+
console.log(` 程式碼行數: ${stats.totalLines}`);
|
|
1986
|
+
console.log(` 符號數量: ${stats.symbolCount}`);
|
|
1987
|
+
console.log(` 依賴關係: ${stats.dependencyCount}`);
|
|
1988
|
+
console.log(` 估計 token 數: ${stats.estimatedTokens}`);
|
|
1989
|
+
console.log(` 壓縮率: ${stats.compressionRatio.toFixed(1)}%`);
|
|
1990
|
+
console.log(` 生成耗時: ${stats.generationTime}ms`);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
async handleSnapshotInfo(options, isJsonFormat) {
|
|
1994
|
+
if (!options.outputPath) {
|
|
1995
|
+
throw new Error('請指定快照檔案路徑 (--output)');
|
|
1996
|
+
}
|
|
1997
|
+
const engine = new SnapshotEngine();
|
|
1998
|
+
const snapshot = await engine.load(options.outputPath);
|
|
1999
|
+
const stats = engine.getStats(snapshot);
|
|
2000
|
+
if (isJsonFormat) {
|
|
2001
|
+
console.log(JSON.stringify({
|
|
2002
|
+
snapshot: {
|
|
2003
|
+
version: snapshot.v,
|
|
2004
|
+
project: snapshot.p,
|
|
2005
|
+
timestamp: snapshot.t,
|
|
2006
|
+
level: snapshot.l
|
|
2007
|
+
},
|
|
2008
|
+
stats
|
|
2009
|
+
}, null, 2));
|
|
2010
|
+
}
|
|
2011
|
+
else {
|
|
2012
|
+
console.log('\n📊 快照資訊');
|
|
2013
|
+
console.log('='.repeat(50));
|
|
2014
|
+
console.log(` 專案: ${snapshot.p}`);
|
|
2015
|
+
console.log(` 版本: ${snapshot.v}`);
|
|
2016
|
+
console.log(` 時間: ${new Date(snapshot.t).toLocaleString()}`);
|
|
2017
|
+
console.log(` 壓縮層級: ${snapshot.l}`);
|
|
2018
|
+
console.log('\n統計資訊:');
|
|
2019
|
+
console.log(` 檔案數量: ${stats.fileCount}`);
|
|
2020
|
+
console.log(` 程式碼行數: ${stats.totalLines}`);
|
|
2021
|
+
console.log(` 符號數量: ${stats.symbolCount}`);
|
|
2022
|
+
console.log(` 估計 token 數: ${stats.estimatedTokens}`);
|
|
2023
|
+
console.log(` 語言: ${snapshot.md.lg.join(', ')}`);
|
|
2024
|
+
console.log('='.repeat(50));
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
async handleSnapshotDiff(options, isJsonFormat) {
|
|
2028
|
+
const oldPath = options.old;
|
|
2029
|
+
const newPath = options.new;
|
|
2030
|
+
if (!oldPath || !newPath) {
|
|
2031
|
+
throw new Error('請指定兩個快照檔案路徑 (--old <path> --new <path>)');
|
|
2032
|
+
}
|
|
2033
|
+
const engine = new SnapshotEngine();
|
|
2034
|
+
const differ = new SnapshotDiffer();
|
|
2035
|
+
const oldSnapshot = await engine.load(oldPath);
|
|
2036
|
+
const newSnapshot = await engine.load(newPath);
|
|
2037
|
+
const diff = differ.diff(oldSnapshot, newSnapshot);
|
|
2038
|
+
if (isJsonFormat) {
|
|
2039
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
2040
|
+
}
|
|
2041
|
+
else {
|
|
2042
|
+
console.log('\n📊 快照差異');
|
|
2043
|
+
console.log('='.repeat(50));
|
|
2044
|
+
console.log(` 新增檔案: ${diff.added.length}`);
|
|
2045
|
+
console.log(` 修改檔案: ${diff.modified.length}`);
|
|
2046
|
+
console.log(` 刪除檔案: ${diff.deleted.length}`);
|
|
2047
|
+
console.log(` 總變更: ${diff.summary.totalChanges}`);
|
|
2048
|
+
console.log(` 變更行數: ${diff.summary.linesChanged}`);
|
|
2049
|
+
console.log('='.repeat(50));
|
|
2050
|
+
if (diff.added.length > 0) {
|
|
2051
|
+
console.log('\n新增檔案:');
|
|
2052
|
+
diff.added.forEach(file => console.log(` + ${file}`));
|
|
2053
|
+
}
|
|
2054
|
+
if (diff.modified.length > 0) {
|
|
2055
|
+
console.log('\n修改檔案:');
|
|
2056
|
+
diff.modified.forEach(file => console.log(` ~ ${file}`));
|
|
2057
|
+
}
|
|
2058
|
+
if (diff.deleted.length > 0) {
|
|
2059
|
+
console.log('\n刪除檔案:');
|
|
2060
|
+
diff.deleted.forEach(file => console.log(` - ${file}`));
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
async handleSnapshotInit(configManager, projectPath, isJsonFormat) {
|
|
2065
|
+
await configManager.createExampleConfig(projectPath);
|
|
2066
|
+
if (isJsonFormat) {
|
|
2067
|
+
console.log(JSON.stringify({ success: true, config: '.agent-ide.json' }));
|
|
2068
|
+
}
|
|
2069
|
+
else {
|
|
2070
|
+
console.log('✅ 已建立配置檔: .agent-ide.json');
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
async handleDepsCommand(subcommand, options) {
|
|
958
2074
|
if (options.format !== 'json') {
|
|
959
|
-
|
|
2075
|
+
const titles = {
|
|
2076
|
+
'graph': '🕸️ 依賴圖分析...',
|
|
2077
|
+
'cycles': '🔄 循環依賴分析...',
|
|
2078
|
+
'impact': '💥 影響分析...',
|
|
2079
|
+
'orphans': '🏝️ 孤立檔案分析...'
|
|
2080
|
+
};
|
|
2081
|
+
console.log(titles[subcommand] || '🕸️ 分析依賴關係...');
|
|
960
2082
|
}
|
|
961
2083
|
try {
|
|
962
2084
|
const analyzePath = options.path || process.cwd();
|
|
@@ -974,8 +2096,153 @@ export class AgentIdeCLI {
|
|
|
974
2096
|
const cycles = cycleDetector.detectCycles(graph);
|
|
975
2097
|
// 輸出結果
|
|
976
2098
|
if (options.format === 'json') {
|
|
977
|
-
//
|
|
978
|
-
if (
|
|
2099
|
+
// 根據子命令決定輸出格式
|
|
2100
|
+
if (subcommand === 'graph') {
|
|
2101
|
+
// graph 子命令:輸出完整依賴圖(nodes, edges, summary)
|
|
2102
|
+
const allNodes = graph.getAllNodes();
|
|
2103
|
+
const allNodesSet = new Set(allNodes);
|
|
2104
|
+
// 計算每個節點的入度和出度
|
|
2105
|
+
const inDegreeMap = new Map();
|
|
2106
|
+
const outDegreeMap = new Map();
|
|
2107
|
+
for (const nodeId of allNodes) {
|
|
2108
|
+
const deps = graph.getDependencies(nodeId);
|
|
2109
|
+
outDegreeMap.set(nodeId, deps.length);
|
|
2110
|
+
for (const depId of deps) {
|
|
2111
|
+
if (allNodesSet.has(depId)) {
|
|
2112
|
+
inDegreeMap.set(depId, (inDegreeMap.get(depId) || 0) + 1);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
const nodes = allNodes.map((nodeId) => ({
|
|
2117
|
+
id: nodeId,
|
|
2118
|
+
dependencies: graph.getDependencies(nodeId),
|
|
2119
|
+
inDegree: inDegreeMap.get(nodeId) || 0,
|
|
2120
|
+
outDegree: outDegreeMap.get(nodeId) || 0
|
|
2121
|
+
}));
|
|
2122
|
+
// 判斷是否為系統框架
|
|
2123
|
+
const isSystemFramework = (name) => {
|
|
2124
|
+
const systemFrameworks = [
|
|
2125
|
+
'Foundation', 'UIKit', 'SwiftUI', 'Combine', 'CoreData',
|
|
2126
|
+
'CoreGraphics', 'CoreLocation', 'AVFoundation', 'MapKit',
|
|
2127
|
+
'WebKit', 'Security', 'PackageDescription'
|
|
2128
|
+
];
|
|
2129
|
+
return systemFrameworks.includes(name);
|
|
2130
|
+
};
|
|
2131
|
+
const edges = [];
|
|
2132
|
+
for (const nodeId of allNodes) {
|
|
2133
|
+
for (const depId of graph.getDependencies(nodeId)) {
|
|
2134
|
+
// 系統框架一律標記為 external
|
|
2135
|
+
const isExternal = isSystemFramework(depId) || !allNodesSet.has(depId);
|
|
2136
|
+
edges.push({
|
|
2137
|
+
source: nodeId,
|
|
2138
|
+
target: depId,
|
|
2139
|
+
type: isExternal ? 'external' : 'internal'
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
// graph 子命令:保持原格式(nodes, edges, summary)
|
|
2144
|
+
console.log(JSON.stringify({
|
|
2145
|
+
nodes,
|
|
2146
|
+
edges,
|
|
2147
|
+
summary: {
|
|
2148
|
+
totalFiles: stats.totalFiles,
|
|
2149
|
+
totalDependencies: stats.totalDependencies,
|
|
2150
|
+
averageDependenciesPerFile: stats.averageDependenciesPerFile,
|
|
2151
|
+
maxDependenciesInFile: stats.maxDependenciesInFile
|
|
2152
|
+
}
|
|
2153
|
+
}, null, 2));
|
|
2154
|
+
}
|
|
2155
|
+
else if (subcommand === 'impact' && options.file) {
|
|
2156
|
+
// impact 子命令:分析檔案修改的影響範圍
|
|
2157
|
+
const targetFile = path.resolve(options.file);
|
|
2158
|
+
let actualTargetFile = targetFile;
|
|
2159
|
+
const directDependents = graph.getDependents(targetFile);
|
|
2160
|
+
// 如果找不到依賴關係,可能是路徑格式不匹配
|
|
2161
|
+
if (directDependents.length === 0) {
|
|
2162
|
+
// 嘗試在 graph 中找到匹配的路徑
|
|
2163
|
+
const allNodes = graph.getAllNodes();
|
|
2164
|
+
const matchingNode = allNodes.find((node) => node.endsWith(options.file) || options.file.endsWith(node));
|
|
2165
|
+
if (matchingNode) {
|
|
2166
|
+
// 找到匹配的節點,使用該路徑重新查詢
|
|
2167
|
+
actualTargetFile = matchingNode;
|
|
2168
|
+
const altDependents = graph.getDependents(matchingNode);
|
|
2169
|
+
directDependents.length = 0;
|
|
2170
|
+
directDependents.push(...altDependents);
|
|
2171
|
+
}
|
|
2172
|
+
else {
|
|
2173
|
+
// 檔案不在專案中或未被索引
|
|
2174
|
+
console.error(`❌ 錯誤:檔案不存在或未被索引: ${options.file}`);
|
|
2175
|
+
process.exit(1);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
// BFS 計算傳遞依賴
|
|
2179
|
+
const transitiveDependents = new Set();
|
|
2180
|
+
const visited = new Set();
|
|
2181
|
+
const queue = [...directDependents];
|
|
2182
|
+
while (queue.length > 0) {
|
|
2183
|
+
const current = queue.shift();
|
|
2184
|
+
if (visited.has(current)) {
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
visited.add(current);
|
|
2188
|
+
transitiveDependents.add(current);
|
|
2189
|
+
const deps = graph.getDependents(current);
|
|
2190
|
+
for (const dep of deps) {
|
|
2191
|
+
if (!visited.has(dep)) {
|
|
2192
|
+
queue.push(dep);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
// 計算影響等級
|
|
2197
|
+
const totalImpacted = transitiveDependents.size;
|
|
2198
|
+
let impactLevel;
|
|
2199
|
+
if (totalImpacted > 10) {
|
|
2200
|
+
impactLevel = 'high';
|
|
2201
|
+
}
|
|
2202
|
+
else if (totalImpacted > 3) {
|
|
2203
|
+
impactLevel = 'medium';
|
|
2204
|
+
}
|
|
2205
|
+
else {
|
|
2206
|
+
impactLevel = 'low';
|
|
2207
|
+
}
|
|
2208
|
+
// 計算影響評分 (0-100)
|
|
2209
|
+
const impactScore = Math.min(100, Math.round((totalImpacted / stats.totalFiles) * 100 * 2));
|
|
2210
|
+
console.log(JSON.stringify({
|
|
2211
|
+
file: options.file,
|
|
2212
|
+
impactLevel,
|
|
2213
|
+
impactScore,
|
|
2214
|
+
directDependents,
|
|
2215
|
+
transitiveDependents: Array.from(transitiveDependents),
|
|
2216
|
+
summary: {
|
|
2217
|
+
totalImpacted,
|
|
2218
|
+
directCount: directDependents.length,
|
|
2219
|
+
transitiveCount: transitiveDependents.size
|
|
2220
|
+
}
|
|
2221
|
+
}, null, 2));
|
|
2222
|
+
}
|
|
2223
|
+
else if (subcommand === 'orphans') {
|
|
2224
|
+
// orphans 子命令:檢測孤立檔案
|
|
2225
|
+
const allNodes = graph.getAllNodes();
|
|
2226
|
+
const orphans = [];
|
|
2227
|
+
for (const node of allNodes) {
|
|
2228
|
+
const dependents = graph.getDependents(node);
|
|
2229
|
+
if (dependents.length === 0) {
|
|
2230
|
+
orphans.push({
|
|
2231
|
+
filePath: node,
|
|
2232
|
+
reason: 'No files depend on this file'
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
console.log(JSON.stringify({
|
|
2237
|
+
orphans,
|
|
2238
|
+
summary: {
|
|
2239
|
+
totalOrphans: orphans.length,
|
|
2240
|
+
totalFiles: stats.totalFiles,
|
|
2241
|
+
orphanPercentage: Math.round((orphans.length / stats.totalFiles) * 100)
|
|
2242
|
+
}
|
|
2243
|
+
}, null, 2));
|
|
2244
|
+
}
|
|
2245
|
+
else if (options.file) {
|
|
979
2246
|
// 單檔案依賴查詢模式
|
|
980
2247
|
const targetFile = path.resolve(options.file);
|
|
981
2248
|
const dependencies = {};
|
|
@@ -985,15 +2252,8 @@ export class AgentIdeCLI {
|
|
|
985
2252
|
}, null, 2));
|
|
986
2253
|
}
|
|
987
2254
|
else {
|
|
988
|
-
//
|
|
2255
|
+
// 其他子命令(cycles)或無子命令:輸出問題導向格式
|
|
989
2256
|
const outputData = {
|
|
990
|
-
summary: {
|
|
991
|
-
totalFiles: stats.totalFiles,
|
|
992
|
-
totalDependencies: stats.totalDependencies,
|
|
993
|
-
averageDependenciesPerFile: stats.averageDependenciesPerFile,
|
|
994
|
-
maxDependenciesInFile: stats.maxDependenciesInFile,
|
|
995
|
-
issuesFound: cycles.length + stats.orphanedFiles
|
|
996
|
-
},
|
|
997
2257
|
issues: {
|
|
998
2258
|
cycles: cycles.map(c => ({
|
|
999
2259
|
cycle: c.cycle,
|
|
@@ -1002,6 +2262,14 @@ export class AgentIdeCLI {
|
|
|
1002
2262
|
})),
|
|
1003
2263
|
circularDependencies: cycles.length,
|
|
1004
2264
|
orphanedFiles: stats.orphanedFiles
|
|
2265
|
+
},
|
|
2266
|
+
summary: {
|
|
2267
|
+
totalFiles: stats.totalFiles,
|
|
2268
|
+
totalDependencies: stats.totalDependencies,
|
|
2269
|
+
averageDependenciesPerFile: stats.averageDependenciesPerFile,
|
|
2270
|
+
maxDependenciesInFile: stats.maxDependenciesInFile,
|
|
2271
|
+
cyclesFound: cycles.length,
|
|
2272
|
+
issuesFound: cycles.length + stats.orphanedFiles
|
|
1005
2273
|
}
|
|
1006
2274
|
};
|
|
1007
2275
|
// 只有在 --all 時才輸出完整依賴圖
|
|
@@ -1025,7 +2293,13 @@ export class AgentIdeCLI {
|
|
|
1025
2293
|
}
|
|
1026
2294
|
}
|
|
1027
2295
|
else {
|
|
1028
|
-
|
|
2296
|
+
const completeTitles = {
|
|
2297
|
+
'graph': '✅ 依賴圖分析',
|
|
2298
|
+
'cycles': '✅ 循環依賴分析',
|
|
2299
|
+
'impact': '✅ 影響分析',
|
|
2300
|
+
'orphans': '✅ 孤立檔案分析'
|
|
2301
|
+
};
|
|
2302
|
+
console.log(completeTitles[subcommand] || '✅ 依賴分析完成!');
|
|
1029
2303
|
console.log('📊 統計:');
|
|
1030
2304
|
console.log(` 總檔案數: ${stats.totalFiles}`);
|
|
1031
2305
|
console.log(` 總依賴數: ${stats.totalDependencies}`);
|
|
@@ -1180,7 +2454,9 @@ export class AgentIdeCLI {
|
|
|
1180
2454
|
*/
|
|
1181
2455
|
async getAllProjectFiles(projectPath) {
|
|
1182
2456
|
const files = [];
|
|
1183
|
-
|
|
2457
|
+
// 從 ParserRegistry 獲取所有支援的副檔名
|
|
2458
|
+
const registry = ParserRegistry.getInstance();
|
|
2459
|
+
const allowedExtensions = registry.getSupportedExtensions();
|
|
1184
2460
|
const excludePatterns = ['node_modules', 'dist', '.git', 'coverage'];
|
|
1185
2461
|
// 檢查路徑是檔案還是目錄
|
|
1186
2462
|
try {
|