agent-ide 0.1.10 → 0.2.0

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