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.
Files changed (227) hide show
  1. package/README.md +103 -17
  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 +1483 -157
  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 -843
  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() {
@@ -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.handleDepsCommand(options);
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
- console.error('❌ 必須指定符號名稱和新名稱');
297
- 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
+ }
298
412
  if (process.env.NODE_ENV !== 'test') {
299
413
  process.exit(1);
300
414
  }
301
415
  return;
302
416
  }
303
- console.log(`🔄 重新命名 ${from} → ${to}`);
417
+ if (!isJsonFormat) {
418
+ console.log(`🔄 重新命名 ${from} → ${to}`);
419
+ }
304
420
  try {
305
- 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
+ }
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
- console.log(`🔍 查找符號 "${from}"...`);
451
+ if (!isJsonFormat) {
452
+ console.log(`🔍 查找符號 "${from}"...`);
453
+ }
319
454
  const searchResults = await this.indexEngine.findSymbol(from);
320
455
  if (searchResults.length === 0) {
321
- console.log(`❌ 找不到符號 "${from}"`);
322
- if (process.env.NODE_ENV !== 'test') {
323
- process.exit(1);
456
+ if (isJsonFormat) {
457
+ console.error(JSON.stringify({ error: `找不到符號 "${from}"` }));
324
458
  }
325
- return;
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
- console.log('🔍 預覽變更...');
473
+ if (!isJsonFormat) {
474
+ console.log('🔍 預覽變更...');
475
+ }
337
476
  try {
338
477
  // 取得所有專案檔案以進行跨檔案引用查找
339
- const allProjectFiles = await this.getAllProjectFiles(options.path || workspacePath);
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
- console.log('📝 預計變更:');
346
- console.log(` 檔案數: ${preview.affectedFiles.length}`);
347
- console.log(` 操作數: ${preview.operations.length}`);
348
- if (preview.conflicts.length > 0) {
349
- console.log('⚠️ 發現衝突:');
350
- preview.conflicts.forEach(conflict => {
351
- 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}"`);
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
- 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
+ }
362
517
  if (process.env.NODE_ENV !== 'test') {
363
518
  process.exit(1);
364
519
  }
365
520
  }
366
521
  }
367
522
  // 3. 執行重新命名(處理跨檔案引用)
368
- console.log('✏️ 執行重新命名...');
523
+ if (!isJsonFormat) {
524
+ console.log('✏️ 執行重新命名...');
525
+ }
369
526
  // 取得所有專案檔案(使用與 preview 相同的邏輯)
370
- const allProjectFiles = await this.getAllProjectFiles(options.path || workspacePath);
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
- console.log('✅ 重新命名成功!');
379
- console.log(`📊 統計: ${renameResult.affectedFiles.length} 檔案, ${renameResult.operations.length} 變更`);
380
- renameResult.operations.forEach(operation => {
381
- console.log(` ✓ ${operation.filePath}: "${operation.oldText}" → "${operation.newText}"`);
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
- console.error('❌ 重新命名失敗:');
386
- renameResult.errors?.forEach(error => {
387
- console.error(` - ${error}`);
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
- 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
+ }
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
- if (!options.file) {
403
- 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;
404
588
  if (process.env.NODE_ENV !== 'test') {
405
589
  process.exit(1);
406
590
  }
407
591
  return;
408
592
  }
409
- 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
+ }
410
599
  try {
411
- const filePath = path.resolve(options.file);
412
- if (action === 'extract-function') {
413
- if (!options.startLine || !options.endLine || !options.functionName) {
414
- 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;
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: parseInt(options.startLine), column: 0 },
426
- end: { line: parseInt(options.endLine), column: 0 }
638
+ start: { line: startLine, column: 0 },
639
+ end: { line: endLine, column: 0 }
427
640
  };
428
- // 初始化 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 提取器(原有邏輯)
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: options.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
- console.log(`📦 移動 ${source} ${target}`);
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
- 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;
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: path.resolve(source),
541
- target: path.resolve(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 (options.preview) {
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
- console.log('✅ 移動成功!');
556
- }
557
- console.log(`📊 統計: ${result.pathUpdates.length} 個 import 需要更新`);
558
- if (result.pathUpdates.length > 0) {
559
- console.log('📝 影響的檔案:');
560
- const fileGroups = new Map();
561
- result.pathUpdates.forEach(update => {
562
- if (!fileGroups.has(update.filePath)) {
563
- fileGroups.set(update.filePath, []);
564
- }
565
- fileGroups.get(update.filePath).push(update);
566
- });
567
- for (const [filePath, updates] of fileGroups) {
568
- console.log(` 📄 ${path.relative(process.cwd(), filePath)}:`);
569
- updates.forEach(update => {
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
- 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;
577
893
  if (process.env.NODE_ENV !== 'test') {
578
894
  process.exit(1);
579
895
  }
580
896
  }
581
897
  }
582
898
  catch (error) {
583
- 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;
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
- const includeFiles = options.include ? options.include.split(',') : undefined;
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
- ...match,
687
- file: this.formatFilePath(match.file)
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
- const analyzer = new ComplexityAnalyzer();
756
- // 獲取需要分析的檔案列表
1372
+ // 使用 ParserPlugin 分析複雜度
1373
+ const registry = ParserRegistry.getInstance();
757
1374
  const files = await this.getAllProjectFiles(analyzePath);
758
- 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
+ }
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
- console.log(JSON.stringify({
769
- files: results.map(r => ({
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
- summary: {
776
- averageComplexity,
777
- maxComplexity,
778
- totalFiles: results.length
779
- }
780
- }, null, 2));
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
- const detector = new DeadCodeDetector();
791
- // 獲取需要分析的檔案列表
1440
+ // 使用 ParserPlugin 檢測死代碼
1441
+ const registry = ParserRegistry.getInstance();
792
1442
  const files = await this.getAllProjectFiles(analyzePath);
793
- 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
+ }
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
- console.log(JSON.stringify({
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
- }, null, 2));
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.log(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1778
+ console.error(JSON.stringify({ error: errorMessage }));
904
1779
  }
905
1780
  else {
906
- console.error('❌ 分析失敗:', error instanceof Error ? error.message : error);
1781
+ console.error('❌ 分析失敗:', errorMessage);
907
1782
  }
1783
+ process.exitCode = 1;
908
1784
  if (process.env.NODE_ENV !== 'test') {
909
- if (process.env.NODE_ENV !== 'test') {
910
- 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);
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 handleDepsCommand(options) {
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
- console.log('🕸️ 分析依賴關係...');
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
- // 建立 nodes 和 edges 格式(為了符合測試期望)
936
- const nodes = graph.getAllNodes().map((nodeId) => ({
937
- id: nodeId,
938
- dependencies: graph.getDependencies(nodeId)
939
- }));
940
- const edges = [];
941
- for (const nodeId of graph.getAllNodes()) {
942
- for (const depId of graph.getDependencies(nodeId)) {
943
- edges.push({ source: nodeId, target: depId });
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
- // 根據 --file 選項決定輸出格式
947
- if (options.file) {
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
- console.log(JSON.stringify({
959
- nodes,
960
- edges,
961
- cycles: cycles.map(c => ({
962
- cycle: c.cycle,
963
- length: c.length,
964
- severity: c.severity
965
- })),
966
- stats: {
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
- circularDependencies: cycles.length,
972
- orphanedFiles: stats.orphanedFiles
2271
+ cyclesFound: cycles.length,
2272
+ issuesFound: cycles.length + stats.orphanedFiles
973
2273
  }
974
- }, null, 2));
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
- console.log('✅ 依賴分析完成!');
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
- const allowedExtensions = ['.ts', '.tsx', '.js', '.jsx'];
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 {