agent-ide 0.1.9 → 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 +103 -17
- 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 +1483 -157
- 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 -843
- 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() {
|
|
@@ -196,20 +272,36 @@ export class AgentIdeCLI {
|
|
|
196
272
|
.option('-p, --path <path>', '分析路徑', '.')
|
|
197
273
|
.option('--pattern <pattern>', '分析模式')
|
|
198
274
|
.option('--format <format>', '輸出格式 (json|table|summary)', 'summary')
|
|
275
|
+
.option('--all', '顯示所有掃描結果(預設只顯示有問題的項目)', false)
|
|
199
276
|
.action(async (type, options) => {
|
|
200
277
|
await this.handleAnalyzeCommand(type, options);
|
|
201
278
|
});
|
|
202
279
|
}
|
|
203
280
|
setupDepsCommand() {
|
|
204
281
|
this.program
|
|
205
|
-
.command('deps')
|
|
206
|
-
.description('分析依賴關係')
|
|
282
|
+
.command('deps [subcommand]')
|
|
283
|
+
.description('分析依賴關係 (subcommand: graph|cycles|impact|orphans)')
|
|
207
284
|
.option('-p, --path <path>', '分析路徑', '.')
|
|
208
|
-
.option('-t, --type <type>', '分析類型 (graph|cycles|impact)')
|
|
209
285
|
.option('-f, --file <file>', '特定檔案分析')
|
|
210
286
|
.option('--format <format>', '輸出格式 (json|dot|summary)', 'summary')
|
|
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>', '輸出到檔案')
|
|
211
303
|
.action(async (options) => {
|
|
212
|
-
await this.
|
|
304
|
+
await this.handleShitCommand(options);
|
|
213
305
|
});
|
|
214
306
|
}
|
|
215
307
|
setupPluginsCommand() {
|
|
@@ -231,6 +323,22 @@ export class AgentIdeCLI {
|
|
|
231
323
|
await this.handlePluginInfoCommand(pluginName);
|
|
232
324
|
});
|
|
233
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
|
+
}
|
|
234
342
|
// Command handlers
|
|
235
343
|
async handleIndexCommand(options) {
|
|
236
344
|
const formatter = this.createFormatter(options.format);
|
|
@@ -292,20 +400,45 @@ export class AgentIdeCLI {
|
|
|
292
400
|
// 支援多種參數名稱
|
|
293
401
|
const from = options.symbol || options.from;
|
|
294
402
|
const to = options.newName || options.to;
|
|
403
|
+
const isJsonFormat = options.format === 'json';
|
|
295
404
|
if (!from || !to) {
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
}
|
|
298
412
|
if (process.env.NODE_ENV !== 'test') {
|
|
299
413
|
process.exit(1);
|
|
300
414
|
}
|
|
301
415
|
return;
|
|
302
416
|
}
|
|
303
|
-
|
|
417
|
+
if (!isJsonFormat) {
|
|
418
|
+
console.log(`🔄 重新命名 ${from} → ${to}`);
|
|
419
|
+
}
|
|
304
420
|
try {
|
|
305
|
-
|
|
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
|
+
}
|
|
306
439
|
// 初始化索引引擎(每次都重新索引以確保資料是最新的)
|
|
307
440
|
const config = createIndexConfig(workspacePath, {
|
|
308
|
-
includeExtensions: ['.ts', '.tsx', '.js', '.jsx'],
|
|
441
|
+
includeExtensions: ['.ts', '.tsx', '.js', '.jsx', '.swift'],
|
|
309
442
|
excludePatterns: ['node_modules/**', '*.test.*']
|
|
310
443
|
});
|
|
311
444
|
this.indexEngine = new IndexEngine(config);
|
|
@@ -315,16 +448,20 @@ export class AgentIdeCLI {
|
|
|
315
448
|
this.renameEngine = new RenameEngine();
|
|
316
449
|
}
|
|
317
450
|
// 1. 查找符號
|
|
318
|
-
|
|
451
|
+
if (!isJsonFormat) {
|
|
452
|
+
console.log(`🔍 查找符號 "${from}"...`);
|
|
453
|
+
}
|
|
319
454
|
const searchResults = await this.indexEngine.findSymbol(from);
|
|
320
455
|
if (searchResults.length === 0) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
process.exit(1);
|
|
456
|
+
if (isJsonFormat) {
|
|
457
|
+
console.error(JSON.stringify({ error: `找不到符號 "${from}"` }));
|
|
324
458
|
}
|
|
325
|
-
|
|
459
|
+
else {
|
|
460
|
+
console.log(`❌ 找不到符號 "${from}"`);
|
|
461
|
+
}
|
|
462
|
+
process.exit(1);
|
|
326
463
|
}
|
|
327
|
-
if (searchResults.length > 1) {
|
|
464
|
+
if (searchResults.length > 1 && !isJsonFormat) {
|
|
328
465
|
console.log('⚠️ 找到多個符號,使用第一個:');
|
|
329
466
|
searchResults.forEach((result, index) => {
|
|
330
467
|
console.log(` ${index + 1}. ${result.symbol.name} 在 ${result.symbol.location.filePath}:${result.symbol.location.range.start.line}`);
|
|
@@ -333,41 +470,62 @@ export class AgentIdeCLI {
|
|
|
333
470
|
const targetSymbol = searchResults[0].symbol;
|
|
334
471
|
// 2. 預覽變更
|
|
335
472
|
if (options.preview) {
|
|
336
|
-
|
|
473
|
+
if (!isJsonFormat) {
|
|
474
|
+
console.log('🔍 預覽變更...');
|
|
475
|
+
}
|
|
337
476
|
try {
|
|
338
477
|
// 取得所有專案檔案以進行跨檔案引用查找
|
|
339
|
-
|
|
478
|
+
// 使用 workspacePath(已解析為目錄)而不是 options.path(可能是檔案)
|
|
479
|
+
const allProjectFiles = await this.getAllProjectFiles(workspacePath);
|
|
340
480
|
const preview = await this.renameEngine.previewRename({
|
|
341
481
|
symbol: targetSymbol,
|
|
342
482
|
newName: to,
|
|
343
483
|
filePaths: allProjectFiles
|
|
344
484
|
});
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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}"`);
|
|
352
505
|
});
|
|
506
|
+
console.log('✅ 預覽完成');
|
|
353
507
|
}
|
|
354
|
-
preview.operations.forEach(op => {
|
|
355
|
-
console.log(` ${op.filePath}: "${op.oldText}" → "${op.newText}"`);
|
|
356
|
-
});
|
|
357
|
-
console.log('✅ 預覽完成');
|
|
358
508
|
return;
|
|
359
509
|
}
|
|
360
510
|
catch (previewError) {
|
|
361
|
-
|
|
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
|
+
}
|
|
362
517
|
if (process.env.NODE_ENV !== 'test') {
|
|
363
518
|
process.exit(1);
|
|
364
519
|
}
|
|
365
520
|
}
|
|
366
521
|
}
|
|
367
522
|
// 3. 執行重新命名(處理跨檔案引用)
|
|
368
|
-
|
|
523
|
+
if (!isJsonFormat) {
|
|
524
|
+
console.log('✏️ 執行重新命名...');
|
|
525
|
+
}
|
|
369
526
|
// 取得所有專案檔案(使用與 preview 相同的邏輯)
|
|
370
|
-
|
|
527
|
+
// 使用 workspacePath(已解析為目錄)而不是 options.path(可能是檔案)
|
|
528
|
+
const allProjectFiles = await this.getAllProjectFiles(workspacePath);
|
|
371
529
|
// 使用 renameEngine 執行重新命名(與 preview 使用相同的引擎)
|
|
372
530
|
const renameResult = await this.renameEngine.rename({
|
|
373
531
|
symbol: targetSymbol,
|
|
@@ -375,43 +533,86 @@ export class AgentIdeCLI {
|
|
|
375
533
|
filePaths: allProjectFiles
|
|
376
534
|
});
|
|
377
535
|
if (renameResult.success) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
+
}
|
|
383
551
|
}
|
|
384
552
|
else {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
+
}
|
|
389
565
|
if (process.env.NODE_ENV !== 'test') {
|
|
390
566
|
process.exit(1);
|
|
391
567
|
}
|
|
392
568
|
}
|
|
393
569
|
}
|
|
394
570
|
catch (error) {
|
|
395
|
-
|
|
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
|
+
}
|
|
396
577
|
if (process.env.NODE_ENV !== 'test') {
|
|
397
578
|
process.exit(1);
|
|
398
579
|
}
|
|
399
580
|
}
|
|
400
581
|
}
|
|
401
582
|
async handleRefactorCommand(action, options) {
|
|
402
|
-
|
|
403
|
-
|
|
583
|
+
// 支援 --path 作為 --file 的別名
|
|
584
|
+
const fileOption = options.file || options.path;
|
|
585
|
+
if (!fileOption) {
|
|
586
|
+
console.error('❌ 必須指定 --file 或 --path 參數');
|
|
587
|
+
process.exitCode = 1;
|
|
404
588
|
if (process.env.NODE_ENV !== 'test') {
|
|
405
589
|
process.exit(1);
|
|
406
590
|
}
|
|
407
591
|
return;
|
|
408
592
|
}
|
|
409
|
-
|
|
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
|
+
}
|
|
410
599
|
try {
|
|
411
|
-
const filePath = path.resolve(
|
|
412
|
-
if (action === 'extract-function') {
|
|
413
|
-
if (!options.startLine || !options.endLine || !
|
|
414
|
-
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;
|
|
415
616
|
if (process.env.NODE_ENV !== 'test') {
|
|
416
617
|
process.exit(1);
|
|
417
618
|
}
|
|
@@ -419,18 +620,80 @@ export class AgentIdeCLI {
|
|
|
419
620
|
}
|
|
420
621
|
// 讀取檔案內容
|
|
421
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
|
+
}
|
|
422
635
|
const code = await fs.readFile(filePath, 'utf-8');
|
|
423
636
|
// 建立範圍
|
|
424
637
|
const range = {
|
|
425
|
-
start: { line:
|
|
426
|
-
end: { line:
|
|
638
|
+
start: { line: startLine, column: 0 },
|
|
639
|
+
end: { line: endLine, column: 0 }
|
|
427
640
|
};
|
|
428
|
-
//
|
|
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 提取器(原有邏輯)
|
|
429
692
|
const { FunctionExtractor } = await import('../../core/refactor/extract-function.js');
|
|
430
693
|
const extractor = new FunctionExtractor();
|
|
431
694
|
// 執行提取
|
|
432
695
|
const extractConfig = {
|
|
433
|
-
functionName:
|
|
696
|
+
functionName: functionNameOption,
|
|
434
697
|
generateComments: true,
|
|
435
698
|
preserveFormatting: true,
|
|
436
699
|
validateExtraction: true,
|
|
@@ -497,12 +760,14 @@ export class AgentIdeCLI {
|
|
|
497
760
|
}
|
|
498
761
|
else if (action === 'inline-function') {
|
|
499
762
|
console.error('❌ inline-function 尚未實作');
|
|
763
|
+
process.exitCode = 1;
|
|
500
764
|
if (process.env.NODE_ENV !== 'test') {
|
|
501
765
|
process.exit(1);
|
|
502
766
|
}
|
|
503
767
|
}
|
|
504
768
|
else {
|
|
505
769
|
console.error(`❌ 未知的重構操作: ${action}`);
|
|
770
|
+
process.exitCode = 1;
|
|
506
771
|
if (process.env.NODE_ENV !== 'test') {
|
|
507
772
|
process.exit(1);
|
|
508
773
|
}
|
|
@@ -510,21 +775,54 @@ export class AgentIdeCLI {
|
|
|
510
775
|
}
|
|
511
776
|
catch (error) {
|
|
512
777
|
console.error('❌ 重構失敗:', error instanceof Error ? error.message : error);
|
|
778
|
+
process.exitCode = 1;
|
|
513
779
|
if (process.env.NODE_ENV !== 'test') {
|
|
514
780
|
process.exit(1);
|
|
515
781
|
}
|
|
516
782
|
}
|
|
517
783
|
}
|
|
518
784
|
async handleMoveCommand(source, target, options) {
|
|
519
|
-
|
|
785
|
+
const isJsonFormat = options.format === 'json';
|
|
786
|
+
if (!isJsonFormat) {
|
|
787
|
+
console.log(`📦 移動 ${source} → ${target}`);
|
|
788
|
+
}
|
|
520
789
|
try {
|
|
521
790
|
// 檢查源檔案是否存在
|
|
522
791
|
const sourceExists = await this.fileExists(source);
|
|
523
792
|
if (!sourceExists) {
|
|
524
|
-
|
|
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;
|
|
525
804
|
if (process.env.NODE_ENV !== 'test') {
|
|
526
805
|
process.exit(1);
|
|
527
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;
|
|
528
826
|
}
|
|
529
827
|
// 初始化移動服務
|
|
530
828
|
if (!this.moveService) {
|
|
@@ -532,13 +830,13 @@ export class AgentIdeCLI {
|
|
|
532
830
|
const pathAliases = await this.loadPathAliases(process.cwd());
|
|
533
831
|
this.moveService = new MoveService({
|
|
534
832
|
pathAliases,
|
|
535
|
-
supportedExtensions: ['.ts', '.tsx', '.js', '.jsx', '.vue'],
|
|
833
|
+
supportedExtensions: ['.ts', '.tsx', '.js', '.jsx', '.vue', '.swift'],
|
|
536
834
|
includeNodeModules: false
|
|
537
835
|
});
|
|
538
836
|
}
|
|
539
837
|
const moveOperation = {
|
|
540
|
-
source:
|
|
541
|
-
target:
|
|
838
|
+
source: normalizedSource,
|
|
839
|
+
target: normalizedTarget,
|
|
542
840
|
updateImports: options.updateImports
|
|
543
841
|
};
|
|
544
842
|
const moveOptions = {
|
|
@@ -548,39 +846,67 @@ export class AgentIdeCLI {
|
|
|
548
846
|
// 執行移動操作
|
|
549
847
|
const result = await this.moveService.moveFile(moveOperation, moveOptions);
|
|
550
848
|
if (result.success) {
|
|
551
|
-
if (
|
|
552
|
-
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));
|
|
553
855
|
}
|
|
554
856
|
else {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
result.pathUpdates.
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
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);
|
|
571
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
|
+
}
|
|
572
879
|
}
|
|
573
880
|
}
|
|
574
881
|
}
|
|
575
882
|
else {
|
|
576
|
-
|
|
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;
|
|
577
893
|
if (process.env.NODE_ENV !== 'test') {
|
|
578
894
|
process.exit(1);
|
|
579
895
|
}
|
|
580
896
|
}
|
|
581
897
|
}
|
|
582
898
|
catch (error) {
|
|
583
|
-
|
|
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;
|
|
584
910
|
if (process.env.NODE_ENV !== 'test') {
|
|
585
911
|
process.exit(1);
|
|
586
912
|
}
|
|
@@ -653,8 +979,12 @@ export class AgentIdeCLI {
|
|
|
653
979
|
* 建構搜尋選項
|
|
654
980
|
*/
|
|
655
981
|
buildSearchOptions(options) {
|
|
656
|
-
|
|
982
|
+
let includeFiles = options.include ? options.include.split(',') : undefined;
|
|
657
983
|
const excludeFiles = options.exclude ? options.exclude.split(',') : undefined;
|
|
984
|
+
// --file-pattern 參數轉換為 includeFiles
|
|
985
|
+
if (options.filePattern) {
|
|
986
|
+
includeFiles = [options.filePattern];
|
|
987
|
+
}
|
|
658
988
|
return {
|
|
659
989
|
scope: {
|
|
660
990
|
type: 'directory',
|
|
@@ -662,9 +992,9 @@ export class AgentIdeCLI {
|
|
|
662
992
|
recursive: true
|
|
663
993
|
},
|
|
664
994
|
maxResults: parseInt(options.limit),
|
|
665
|
-
caseSensitive: options.caseSensitive || false,
|
|
995
|
+
caseSensitive: options.caseInsensitive ? false : (options.caseSensitive || false),
|
|
666
996
|
wholeWord: options.wholeWord || false,
|
|
667
|
-
regex: options.type === 'regex',
|
|
997
|
+
regex: options.regex || options.type === 'regex',
|
|
668
998
|
fuzzy: options.type === 'fuzzy',
|
|
669
999
|
multiline: options.multiline || false,
|
|
670
1000
|
showContext: options.context > 0,
|
|
@@ -681,11 +1011,21 @@ export class AgentIdeCLI {
|
|
|
681
1011
|
switch (options.format) {
|
|
682
1012
|
case 'json':
|
|
683
1013
|
// 測試期望的格式是 { results: [...] } 而不是 { matches: [...] }
|
|
684
|
-
//
|
|
685
|
-
const resultsWithRelativePaths = result.matches.map((match) =>
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
+
});
|
|
689
1029
|
console.log(JSON.stringify({ results: resultsWithRelativePaths }, null, 2));
|
|
690
1030
|
break;
|
|
691
1031
|
case 'minimal':
|
|
@@ -743,6 +1083,283 @@ export class AgentIdeCLI {
|
|
|
743
1083
|
return text;
|
|
744
1084
|
}
|
|
745
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
|
+
}
|
|
746
1363
|
async handleAnalyzeCommand(type, options) {
|
|
747
1364
|
const analyzeType = type || 'complexity';
|
|
748
1365
|
if (options.format !== 'json') {
|
|
@@ -752,10 +1369,27 @@ export class AgentIdeCLI {
|
|
|
752
1369
|
const analyzePath = options.path || process.cwd();
|
|
753
1370
|
// 根據分析類型執行對應分析
|
|
754
1371
|
if (analyzeType === 'complexity') {
|
|
755
|
-
|
|
756
|
-
|
|
1372
|
+
// 使用 ParserPlugin 分析複雜度
|
|
1373
|
+
const registry = ParserRegistry.getInstance();
|
|
757
1374
|
const files = await this.getAllProjectFiles(analyzePath);
|
|
758
|
-
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
|
+
}
|
|
1391
|
+
// 過濾高複雜度檔案(evaluation === 'high' 或 complexity > 10)
|
|
1392
|
+
const highComplexityFiles = results.filter(r => r.complexity.evaluation === 'high' || r.complexity.cyclomaticComplexity > 10);
|
|
759
1393
|
// 計算統計資訊
|
|
760
1394
|
const complexities = results.map(r => r.complexity.cyclomaticComplexity);
|
|
761
1395
|
const averageComplexity = complexities.length > 0
|
|
@@ -765,56 +1399,106 @@ export class AgentIdeCLI {
|
|
|
765
1399
|
? Math.max(...complexities)
|
|
766
1400
|
: 0;
|
|
767
1401
|
if (options.format === 'json') {
|
|
768
|
-
|
|
769
|
-
|
|
1402
|
+
const outputData = {
|
|
1403
|
+
summary: {
|
|
1404
|
+
totalScanned: results.length,
|
|
1405
|
+
issuesFound: highComplexityFiles.length,
|
|
1406
|
+
averageComplexity,
|
|
1407
|
+
maxComplexity
|
|
1408
|
+
},
|
|
1409
|
+
issues: highComplexityFiles.map(r => ({
|
|
770
1410
|
path: r.file,
|
|
771
1411
|
complexity: r.complexity.cyclomaticComplexity,
|
|
772
1412
|
cognitiveComplexity: r.complexity.cognitiveComplexity,
|
|
773
1413
|
evaluation: r.complexity.evaluation
|
|
774
|
-
}))
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1414
|
+
}))
|
|
1415
|
+
};
|
|
1416
|
+
if (options.all) {
|
|
1417
|
+
outputData.all = results.map(r => ({
|
|
1418
|
+
path: r.file,
|
|
1419
|
+
complexity: r.complexity.cyclomaticComplexity,
|
|
1420
|
+
cognitiveComplexity: r.complexity.cognitiveComplexity,
|
|
1421
|
+
evaluation: r.complexity.evaluation
|
|
1422
|
+
}));
|
|
1423
|
+
}
|
|
1424
|
+
console.log(JSON.stringify(outputData, null, 2));
|
|
781
1425
|
}
|
|
782
1426
|
else {
|
|
783
1427
|
console.log('✅ 複雜度分析完成!');
|
|
784
|
-
console.log(`📊 統計: ${results.length}
|
|
1428
|
+
console.log(`📊 統計: ${results.length} 個檔案,${highComplexityFiles.length} 個高複雜度檔案`);
|
|
785
1429
|
console.log(` 平均複雜度: ${averageComplexity.toFixed(2)}`);
|
|
786
1430
|
console.log(` 最高複雜度: ${maxComplexity}`);
|
|
1431
|
+
if (!options.all && highComplexityFiles.length > 0) {
|
|
1432
|
+
console.log('\n⚠️ 高複雜度檔案:');
|
|
1433
|
+
highComplexityFiles.forEach(r => {
|
|
1434
|
+
console.log(` - ${r.file}: ${r.complexity.cyclomaticComplexity}`);
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
787
1437
|
}
|
|
788
1438
|
}
|
|
789
1439
|
else if (analyzeType === 'dead-code') {
|
|
790
|
-
|
|
791
|
-
|
|
1440
|
+
// 使用 ParserPlugin 檢測死代碼
|
|
1441
|
+
const registry = ParserRegistry.getInstance();
|
|
792
1442
|
const files = await this.getAllProjectFiles(analyzePath);
|
|
793
|
-
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
|
+
}
|
|
1460
|
+
// 過濾有 dead code 的檔案
|
|
1461
|
+
const filesWithDeadCode = results.filter(r => r.deadCode.length > 0);
|
|
794
1462
|
// 統計結果
|
|
795
1463
|
const allDeadCode = results.flatMap(r => r.deadCode);
|
|
796
1464
|
const deadFunctions = allDeadCode.filter(d => d.type === 'function');
|
|
797
1465
|
const deadVariables = allDeadCode.filter(d => d.type === 'variable');
|
|
798
1466
|
if (options.format === 'json') {
|
|
799
|
-
|
|
800
|
-
files: results.map(r => ({
|
|
801
|
-
path: r.file,
|
|
802
|
-
deadCode: r.deadCode
|
|
803
|
-
})),
|
|
804
|
-
deadFunctions: allDeadCode.filter(d => d.type === 'function'),
|
|
805
|
-
deadVariables: allDeadCode.filter(d => d.type === 'variable'),
|
|
1467
|
+
const outputData = {
|
|
806
1468
|
summary: {
|
|
1469
|
+
totalScanned: results.length,
|
|
1470
|
+
filesWithIssues: filesWithDeadCode.length,
|
|
807
1471
|
totalDeadFunctions: deadFunctions.length,
|
|
808
1472
|
totalDeadVariables: deadVariables.length,
|
|
809
1473
|
totalDeadCode: allDeadCode.length
|
|
810
|
-
}
|
|
811
|
-
|
|
1474
|
+
},
|
|
1475
|
+
issues: filesWithDeadCode.map(r => ({
|
|
1476
|
+
path: r.file,
|
|
1477
|
+
deadCode: r.deadCode
|
|
1478
|
+
}))
|
|
1479
|
+
};
|
|
1480
|
+
if (options.all) {
|
|
1481
|
+
outputData.all = results.map(r => ({
|
|
1482
|
+
path: r.file,
|
|
1483
|
+
deadCode: r.deadCode
|
|
1484
|
+
}));
|
|
1485
|
+
}
|
|
1486
|
+
outputData.deadFunctions = deadFunctions;
|
|
1487
|
+
outputData.deadVariables = deadVariables;
|
|
1488
|
+
console.log(JSON.stringify(outputData, null, 2));
|
|
812
1489
|
}
|
|
813
1490
|
else {
|
|
814
1491
|
console.log('✅ 死代碼檢測完成!');
|
|
1492
|
+
console.log(`📊 統計: ${results.length} 個檔案,${filesWithDeadCode.length} 個有死代碼`);
|
|
815
1493
|
console.log('📊 發現:');
|
|
816
1494
|
console.log(` 未使用函式: ${deadFunctions.length} 個`);
|
|
817
1495
|
console.log(` 未使用變數: ${deadVariables.length} 個`);
|
|
1496
|
+
if (!options.all && filesWithDeadCode.length > 0) {
|
|
1497
|
+
console.log('\n⚠️ 有死代碼的檔案:');
|
|
1498
|
+
filesWithDeadCode.forEach(r => {
|
|
1499
|
+
console.log(` - ${r.file}: ${r.deadCode.length} 項`);
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
818
1502
|
}
|
|
819
1503
|
}
|
|
820
1504
|
else if (analyzeType === 'best-practices') {
|
|
@@ -894,27 +1578,507 @@ export class AgentIdeCLI {
|
|
|
894
1578
|
console.log(`📊 發現模式: ${patterns.join(', ')}`);
|
|
895
1579
|
}
|
|
896
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
|
+
}
|
|
897
1771
|
else {
|
|
898
1772
|
throw new Error(`不支援的分析類型: ${analyzeType}`);
|
|
899
1773
|
}
|
|
900
1774
|
}
|
|
901
1775
|
catch (error) {
|
|
1776
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
902
1777
|
if (options.format === 'json') {
|
|
903
|
-
console.
|
|
1778
|
+
console.error(JSON.stringify({ error: errorMessage }));
|
|
904
1779
|
}
|
|
905
1780
|
else {
|
|
906
|
-
console.error('❌ 分析失敗:',
|
|
1781
|
+
console.error('❌ 分析失敗:', errorMessage);
|
|
907
1782
|
}
|
|
1783
|
+
process.exitCode = 1;
|
|
908
1784
|
if (process.env.NODE_ENV !== 'test') {
|
|
909
|
-
|
|
910
|
-
|
|
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);
|
|
1813
|
+
}
|
|
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
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
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`);
|
|
911
1970
|
}
|
|
912
1971
|
}
|
|
913
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
|
+
}
|
|
914
2063
|
}
|
|
915
|
-
async
|
|
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) {
|
|
916
2074
|
if (options.format !== 'json') {
|
|
917
|
-
|
|
2075
|
+
const titles = {
|
|
2076
|
+
'graph': '🕸️ 依賴圖分析...',
|
|
2077
|
+
'cycles': '🔄 循環依賴分析...',
|
|
2078
|
+
'impact': '💥 影響分析...',
|
|
2079
|
+
'orphans': '🏝️ 孤立檔案分析...'
|
|
2080
|
+
};
|
|
2081
|
+
console.log(titles[subcommand] || '🕸️ 分析依賴關係...');
|
|
918
2082
|
}
|
|
919
2083
|
try {
|
|
920
2084
|
const analyzePath = options.path || process.cwd();
|
|
@@ -932,19 +2096,153 @@ export class AgentIdeCLI {
|
|
|
932
2096
|
const cycles = cycleDetector.detectCycles(graph);
|
|
933
2097
|
// 輸出結果
|
|
934
2098
|
if (options.format === 'json') {
|
|
935
|
-
//
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
+
}
|
|
944
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));
|
|
945
2154
|
}
|
|
946
|
-
|
|
947
|
-
|
|
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) {
|
|
948
2246
|
// 單檔案依賴查詢模式
|
|
949
2247
|
const targetFile = path.resolve(options.file);
|
|
950
2248
|
const dependencies = {};
|
|
@@ -954,28 +2252,54 @@ export class AgentIdeCLI {
|
|
|
954
2252
|
}, null, 2));
|
|
955
2253
|
}
|
|
956
2254
|
else {
|
|
957
|
-
//
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
2255
|
+
// 其他子命令(cycles)或無子命令:輸出問題導向格式
|
|
2256
|
+
const outputData = {
|
|
2257
|
+
issues: {
|
|
2258
|
+
cycles: cycles.map(c => ({
|
|
2259
|
+
cycle: c.cycle,
|
|
2260
|
+
length: c.length,
|
|
2261
|
+
severity: c.severity
|
|
2262
|
+
})),
|
|
2263
|
+
circularDependencies: cycles.length,
|
|
2264
|
+
orphanedFiles: stats.orphanedFiles
|
|
2265
|
+
},
|
|
2266
|
+
summary: {
|
|
967
2267
|
totalFiles: stats.totalFiles,
|
|
968
2268
|
totalDependencies: stats.totalDependencies,
|
|
969
2269
|
averageDependenciesPerFile: stats.averageDependenciesPerFile,
|
|
970
2270
|
maxDependenciesInFile: stats.maxDependenciesInFile,
|
|
971
|
-
|
|
972
|
-
|
|
2271
|
+
cyclesFound: cycles.length,
|
|
2272
|
+
issuesFound: cycles.length + stats.orphanedFiles
|
|
973
2273
|
}
|
|
974
|
-
}
|
|
2274
|
+
};
|
|
2275
|
+
// 只有在 --all 時才輸出完整依賴圖
|
|
2276
|
+
if (options.all) {
|
|
2277
|
+
const nodes = graph.getAllNodes().map((nodeId) => ({
|
|
2278
|
+
id: nodeId,
|
|
2279
|
+
dependencies: graph.getDependencies(nodeId)
|
|
2280
|
+
}));
|
|
2281
|
+
const edges = [];
|
|
2282
|
+
for (const nodeId of graph.getAllNodes()) {
|
|
2283
|
+
for (const depId of graph.getDependencies(nodeId)) {
|
|
2284
|
+
edges.push({ source: nodeId, target: depId });
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
outputData.all = {
|
|
2288
|
+
nodes,
|
|
2289
|
+
edges
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
console.log(JSON.stringify(outputData, null, 2));
|
|
975
2293
|
}
|
|
976
2294
|
}
|
|
977
2295
|
else {
|
|
978
|
-
|
|
2296
|
+
const completeTitles = {
|
|
2297
|
+
'graph': '✅ 依賴圖分析',
|
|
2298
|
+
'cycles': '✅ 循環依賴分析',
|
|
2299
|
+
'impact': '✅ 影響分析',
|
|
2300
|
+
'orphans': '✅ 孤立檔案分析'
|
|
2301
|
+
};
|
|
2302
|
+
console.log(completeTitles[subcommand] || '✅ 依賴分析完成!');
|
|
979
2303
|
console.log('📊 統計:');
|
|
980
2304
|
console.log(` 總檔案數: ${stats.totalFiles}`);
|
|
981
2305
|
console.log(` 總依賴數: ${stats.totalDependencies}`);
|
|
@@ -1130,7 +2454,9 @@ export class AgentIdeCLI {
|
|
|
1130
2454
|
*/
|
|
1131
2455
|
async getAllProjectFiles(projectPath) {
|
|
1132
2456
|
const files = [];
|
|
1133
|
-
|
|
2457
|
+
// 從 ParserRegistry 獲取所有支援的副檔名
|
|
2458
|
+
const registry = ParserRegistry.getInstance();
|
|
2459
|
+
const allowedExtensions = registry.getSupportedExtensions();
|
|
1134
2460
|
const excludePatterns = ['node_modules', 'dist', '.git', 'coverage'];
|
|
1135
2461
|
// 檢查路徑是檔案還是目錄
|
|
1136
2462
|
try {
|