dash-devtools 1.0.0__py3-none-any.whl

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 (53) hide show
  1. dash_devtools/__init__.py +8 -0
  2. dash_devtools/__main__.py +11 -0
  3. dash_devtools/ai_engine.py +441 -0
  4. dash_devtools/browser.py +541 -0
  5. dash_devtools/cli.py +1452 -0
  6. dash_devtools/database.py +338 -0
  7. dash_devtools/dbdiagram.py +183 -0
  8. dash_devtools/e2e.py +329 -0
  9. dash_devtools/fixers/__init__.py +57 -0
  10. dash_devtools/fixers/migration_fixer.py +115 -0
  11. dash_devtools/fixers/ux_fixer.py +106 -0
  12. dash_devtools/fixers/version_bumper.py +115 -0
  13. dash_devtools/gas_mes_test.py +1241 -0
  14. dash_devtools/generators/__init__.py +84 -0
  15. dash_devtools/health.py +476 -0
  16. dash_devtools/hooks/__init__.py +250 -0
  17. dash_devtools/hooks/pre_commit.py +161 -0
  18. dash_devtools/hooks/pre_push.py +275 -0
  19. dash_devtools/init_test.py +352 -0
  20. dash_devtools/markdown_report.py +309 -0
  21. dash_devtools/migrators/__init__.py +21 -0
  22. dash_devtools/perf.py +321 -0
  23. dash_devtools/report.py +667 -0
  24. dash_devtools/reporters/__init__.py +11 -0
  25. dash_devtools/spec.py +230 -0
  26. dash_devtools/stats.py +355 -0
  27. dash_devtools/test_suite.py +690 -0
  28. dash_devtools/testing.py +416 -0
  29. dash_devtools/validators/__init__.py +157 -0
  30. dash_devtools/validators/backend/__init__.py +12 -0
  31. dash_devtools/validators/backend/nodejs.py +245 -0
  32. dash_devtools/validators/backend/python.py +439 -0
  33. dash_devtools/validators/code_quality.py +243 -0
  34. dash_devtools/validators/common/__init__.py +11 -0
  35. dash_devtools/validators/common/quality.py +319 -0
  36. dash_devtools/validators/common/security.py +270 -0
  37. dash_devtools/validators/common/spec.py +273 -0
  38. dash_devtools/validators/detector.py +394 -0
  39. dash_devtools/validators/frontend/__init__.py +14 -0
  40. dash_devtools/validators/frontend/angular.py +245 -0
  41. dash_devtools/validators/frontend/gas.py +310 -0
  42. dash_devtools/validators/frontend/vite.py +539 -0
  43. dash_devtools/validators/migration.py +292 -0
  44. dash_devtools/validators/performance.py +167 -0
  45. dash_devtools/validators/security.py +205 -0
  46. dash_devtools/vision/__init__.py +368 -0
  47. dash_devtools/watch.py +266 -0
  48. dash_devtools/word_report.py +690 -0
  49. dash_devtools-1.0.0.dist-info/METADATA +834 -0
  50. dash_devtools-1.0.0.dist-info/RECORD +53 -0
  51. dash_devtools-1.0.0.dist-info/WHEEL +5 -0
  52. dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
  53. dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
dash_devtools/cli.py ADDED
@@ -0,0 +1,1452 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ DashAI DevTools CLI - 統一入口
4
+
5
+ 使用方式:
6
+ dash validate /path/to/project
7
+ dash migrate /path/to/project
8
+ dash docs claude /path/to/project
9
+ dash release status
10
+ """
11
+
12
+ import click
13
+ from pathlib import Path
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+
17
+ console = Console()
18
+
19
+ # 預設專案清單
20
+ DEFAULT_PROJECTS = [
21
+ # 工廠系統
22
+ '/Users/dash/Documents/github/MES',
23
+ '/Users/dash/Documents/github/MCS',
24
+ '/Users/dash/Documents/github/MIDS',
25
+ '/Users/dash/Documents/github/RFID',
26
+ '/Users/dash/Documents/github/VAC',
27
+ '/Users/dash/Documents/github/EAP',
28
+ '/Users/dash/Documents/github/BPM',
29
+ '/Users/dash/Documents/github/RMS',
30
+ '/Users/dash/Documents/github/SMAI_8D',
31
+ '/Users/dash/Documents/github/AOI-8D',
32
+ '/Users/dash/Documents/github/MSW',
33
+ '/Users/dash/Documents/github/SPC',
34
+ '/Users/dash/Documents/github/SSL',
35
+ # 平台服務
36
+ '/Users/dash/Documents/github/SSO',
37
+ '/Users/dash/Documents/github/API_Center',
38
+ '/Users/dash/Documents/github/smai-mcp-center',
39
+ '/Users/dash/Documents/github/smai-recruit-test',
40
+ '/Users/dash/Documents/github/smai-ui',
41
+ '/Users/dash/Documents/github/smai-process-vision',
42
+ # 產品專案
43
+ '/Users/dash/Documents/github/Dash-GHG',
44
+ '/Users/dash/Documents/github/DashAstro',
45
+ '/Users/dash/Documents/github/DashTrade',
46
+ '/Users/dash/Documents/github/VisionAI',
47
+ '/Users/dash/Documents/github/SEO',
48
+ # 個人專案
49
+ '/Users/dash/Documents/github/sinoauto',
50
+ '/Users/dash/Documents/github/jlpt-n1-learner',
51
+ '/Users/dash/Documents/github/sukuyodo',
52
+ '/Users/dash/Documents/github/jinkochino',
53
+ '/Users/dash/Documents/github/dash-amc-ai',
54
+ '/Users/dash/Documents/github/job-crawler',
55
+ ]
56
+
57
+
58
+ @click.group()
59
+ @click.version_option(version="2.0.0")
60
+ def main():
61
+ """DashAI DevTools v2 - 大許開發工具集
62
+
63
+ 新功能:
64
+ health 專案健康評分
65
+ stats 程式碼統計
66
+ watch 即時監控
67
+ """
68
+ pass
69
+
70
+
71
+ @main.command()
72
+ @click.argument('project', type=click.Path(), required=False)
73
+ @click.option('--all', 'validate_all', is_flag=True, help='驗證所有專案')
74
+ @click.option('--check', type=click.Choice(['security', 'migration', 'performance', 'code_quality', 'all', 'smart']),
75
+ default='smart', help='指定檢查項目 (smart=自動偵測專案類型)')
76
+ @click.option('--fix', is_flag=True, help='自動修復發現的問題')
77
+ @click.option('--output', '-o', type=click.Path(), help='輸出報告路徑')
78
+ def validate(project, validate_all, check, fix, output):
79
+ """驗證專案符合開發規範"""
80
+ from .validators import run_validation
81
+ from .fixers import run_auto_fix
82
+
83
+ if validate_all:
84
+ projects = DEFAULT_PROJECTS
85
+ elif project:
86
+ projects = [project]
87
+ else:
88
+ console.print("[red]請指定專案路徑或使用 --all[/red]")
89
+ return
90
+
91
+ results = run_validation(projects, checks=check, output=output)
92
+
93
+ # 如果有錯誤且啟用自動修復
94
+ has_errors = any(not r['passed'] for r in results)
95
+ if fix and has_errors:
96
+ console.print("\n[yellow][FIX] 執行自動修復...[/yellow]")
97
+ fix_results = run_auto_fix(projects)
98
+ for fr in fix_results:
99
+ if fr['fixes']:
100
+ console.print(f" [green]✓[/green] {fr['project']}: 修復 {len(fr['fixes'])} 個問題")
101
+ for f in fr['fixes']:
102
+ console.print(f" • {f}")
103
+
104
+ # 重新驗證
105
+ console.print("\n[cyan]重新驗證...[/cyan]")
106
+ results = run_validation(projects, checks=check, output=output)
107
+
108
+ # 顯示結果表格
109
+ table = Table(title="驗證結果")
110
+ table.add_column("專案", style="cyan")
111
+ table.add_column("狀態", style="green")
112
+ table.add_column("錯誤", style="red")
113
+ table.add_column("警告", style="yellow")
114
+
115
+ for r in results:
116
+ status = "✓ 通過" if r['passed'] else "✗ 失敗"
117
+ table.add_row(
118
+ r['project'],
119
+ status,
120
+ str(len(r.get('errors', []))),
121
+ str(len(r.get('warnings', [])))
122
+ )
123
+
124
+ console.print(table)
125
+
126
+ # 如果仍有錯誤,顯示詳細資訊
127
+ failed = [r for r in results if not r['passed']]
128
+ has_warnings = any(r.get('warnings') for r in results)
129
+
130
+ if failed:
131
+ console.print("\n[red]錯誤詳情:[/red]")
132
+ for r in failed:
133
+ console.print(f" [cyan]{r['project']}[/cyan]")
134
+ for e in r.get('errors', []):
135
+ console.print(f" [red]• {e}[/red]")
136
+
137
+ # 顯示修復提示
138
+ if not fix and (failed or has_warnings):
139
+ console.print("\n[yellow]━━━ 修復提示 ━━━[/yellow]")
140
+ console.print("[yellow] dash validate <專案路徑> --fix[/yellow]")
141
+ console.print("[dim] 自動修復:HTML 標籤修復、sl-icon-button label 屬性等[/dim]")
142
+
143
+
144
+ @main.command()
145
+ @click.argument('project', type=click.Path())
146
+ @click.option('--dry-run', is_flag=True, help='預覽模式,不實際修改')
147
+ @click.option('--from', 'from_framework', default='shoelace', help='來源框架')
148
+ @click.option('--to', 'to_framework', default='daisyui', help='目標框架')
149
+ def migrate(project, dry_run, from_framework, to_framework):
150
+ """遷移 UI 框架"""
151
+ from .migrators import run_migration
152
+
153
+ console.print(f"[cyan]遷移專案: {project}[/cyan]")
154
+ console.print(f"[cyan]{from_framework} → {to_framework}[/cyan]")
155
+
156
+ if dry_run:
157
+ console.print("[yellow]預覽模式 - 不會實際修改檔案[/yellow]")
158
+
159
+ result = run_migration(project, dry_run=dry_run,
160
+ from_framework=from_framework,
161
+ to_framework=to_framework)
162
+
163
+ if result['success']:
164
+ console.print("[green]遷移完成![/green]")
165
+ else:
166
+ console.print(f"[red]遷移失敗: {result.get('error')}[/red]")
167
+
168
+
169
+ @main.group()
170
+ def docs():
171
+ """文件產生工具"""
172
+ pass
173
+
174
+
175
+ @docs.command()
176
+ @click.argument('project', type=click.Path(), required=False)
177
+ @click.option('--all', 'gen_all', is_flag=True, help='產生所有專案的 CLAUDE.md')
178
+ def claude(project, gen_all):
179
+ """產生 CLAUDE.md"""
180
+ from .generators import generate_claude_md
181
+
182
+ if gen_all:
183
+ projects = DEFAULT_PROJECTS
184
+ elif project:
185
+ projects = [project]
186
+ else:
187
+ console.print("[red]請指定專案路徑或使用 --all[/red]")
188
+ return
189
+
190
+ for p in projects:
191
+ result = generate_claude_md(p)
192
+ if result['success']:
193
+ console.print(f"[green]✓[/green] {Path(p).name}")
194
+ else:
195
+ console.print(f"[red]✗[/red] {Path(p).name}: {result.get('error')}")
196
+
197
+
198
+ @main.group()
199
+ def release():
200
+ """版本發布管理"""
201
+ pass
202
+
203
+
204
+ @release.command()
205
+ def status():
206
+ """檢視版本狀態"""
207
+ from .generators import get_release_status
208
+
209
+ status = get_release_status()
210
+
211
+ table = Table(title="專案版本狀態")
212
+ table.add_column("專案", style="cyan")
213
+ table.add_column("版本", style="green")
214
+ table.add_column("最後更新", style="yellow")
215
+
216
+ for project, info in status.items():
217
+ table.add_row(project, info['version'], info['last_update'])
218
+
219
+ console.print(table)
220
+
221
+
222
+ @release.command()
223
+ @click.argument('project', type=click.Path())
224
+ @click.option('--version', '-v', required=True, help='版本號')
225
+ def publish(project, version):
226
+ """發布新版本"""
227
+ from .generators import publish_release
228
+
229
+ result = publish_release(project, version)
230
+
231
+ if result['success']:
232
+ console.print(f"[green]✓ 已發布 {version}[/green]")
233
+ else:
234
+ console.print(f"[red]✗ 發布失敗: {result.get('error')}[/red]")
235
+
236
+
237
+ @main.command()
238
+ @click.argument('image', type=click.Path())
239
+ @click.option('--output', '-o', type=click.Path(), help='輸出路徑')
240
+ def vision(image, output):
241
+ """視覺 AI 分析"""
242
+ from .vision import analyze_image
243
+
244
+ result = analyze_image(image, output=output)
245
+ console.print(result)
246
+
247
+
248
+ @main.command()
249
+ @click.argument('project', type=click.Path(), default='.')
250
+ def scan(project):
251
+ """掃描機敏資料"""
252
+ from .hooks import run_pre_push_check
253
+
254
+ console.print("[yellow]🔍 掃描機敏資料...[/yellow]")
255
+ result = run_pre_push_check(project)
256
+
257
+ # 顯示使用的掃描引擎
258
+ engine = result.get('engine', '本地規則')
259
+ if engine == 'GitGuardian':
260
+ console.print("[dim] 使用 GitGuardian 引擎[/dim]")
261
+
262
+ if result['passed']:
263
+ console.print("[green]✓ 安全檢查通過[/green]")
264
+ else:
265
+ console.print("[red]✗ 發現機敏資料![/red]")
266
+ for issue in result['issues']:
267
+ console.print(f" [red]• {issue['file']}: {issue['type']}[/red]")
268
+ raise SystemExit(1)
269
+
270
+
271
+ @main.group()
272
+ def hooks():
273
+ """Git Hooks 管理"""
274
+ pass
275
+
276
+
277
+ @hooks.command()
278
+ @click.argument('project', type=click.Path(), default='.')
279
+ @click.option('--strict', is_flag=True, help='嚴格模式:測試失敗會阻止推送')
280
+ @click.option('--e2e', type=str, default=None, help='E2E 測試網址 (每次推送會執行煙霧測試)')
281
+ @click.option('--strict-e2e', is_flag=True, help='嚴格 E2E 模式:E2E 失敗會阻止推送')
282
+ @click.option('--mobile-e2e', is_flag=True, help='手機版 E2E 測試:同時檢查手機版水平溢出')
283
+ def install(project, strict, e2e, strict_e2e, mobile_e2e):
284
+ """安裝 Git Hooks 到專案
285
+
286
+ Pre-push 會執行:
287
+ 1. 檢查 Emoji
288
+ 2. 掃描機敏資料
289
+ 3. 驗證專案規範
290
+ 4. 執行測試
291
+ 5. E2E 煙霧測試 (如有設定)
292
+ 6. 手機版 E2E 測試 (如有設定)
293
+
294
+ 使用範例:
295
+ dash hooks install .
296
+ dash hooks install . --strict
297
+ dash hooks install . --e2e https://example.com
298
+ dash hooks install . --e2e https://example.com --strict-e2e
299
+ dash hooks install . --e2e https://example.com --mobile-e2e
300
+ """
301
+ from .hooks import install_hooks
302
+
303
+ result = install_hooks(project, strict_test=strict, e2e_url=e2e, strict_e2e=strict_e2e, mobile_e2e=mobile_e2e)
304
+
305
+ if result['success']:
306
+ console.print("[green]Git Hooks 已安裝[/green]")
307
+ console.print(" 已安裝:pre-commit, pre-push")
308
+ console.print()
309
+ console.print(" [dim]Pre-push 檢查項目:[/dim]")
310
+ console.print(" 1. 檢查 Emoji")
311
+ console.print(" 2. 掃描機敏資料")
312
+ console.print(" 3. 驗證專案規範")
313
+ console.print(" 4. 執行測試")
314
+ console.print(" 5. E2E 煙霧測試")
315
+ if mobile_e2e:
316
+ console.print(" 6. 手機版 E2E 測試 (水平溢出檢查)")
317
+ if strict:
318
+ console.print()
319
+ console.print(" [yellow]嚴格模式已啟用:測試失敗會阻止推送[/yellow]")
320
+ if e2e:
321
+ console.print()
322
+ console.print(f" [cyan]E2E 測試:{e2e}[/cyan]")
323
+ if strict_e2e:
324
+ console.print(" [yellow]嚴格 E2E 模式已啟用:E2E 失敗會阻止推送[/yellow]")
325
+ if mobile_e2e:
326
+ console.print(" [cyan]手機版 E2E 已啟用:會同時檢查 375x812 水平溢出[/cyan]")
327
+ else:
328
+ console.print(f"[red]安裝失敗: {result.get('error')}[/red]")
329
+
330
+
331
+ @main.command()
332
+ @click.argument('project', type=click.Path(), default='.')
333
+ @click.option('--copy', 'do_copy', is_flag=True, help='複製連結到剪貼簿')
334
+ @click.option('--open', 'do_open', is_flag=True, help='在瀏覽器開啟')
335
+ @click.option('--save', is_flag=True, help='儲存連結到 docs/dbdiagram-link.txt')
336
+ def dbdiagram(project, do_copy, do_open, save):
337
+ """產生 dbdiagram.io 資料庫圖表連結
338
+
339
+ 從 Prisma schema 或 DBML 檔案產生可分享的連結。
340
+
341
+ 使用範例:
342
+ dash dbdiagram /path/to/project
343
+ dash dbdiagram . --open
344
+ dash dbdiagram . --copy
345
+ """
346
+ from .dbdiagram import generate_dbdiagram_link, save_link_to_file
347
+
348
+ console.print("[yellow]📊 產生 dbdiagram.io 連結...[/yellow]")
349
+
350
+ result = generate_dbdiagram_link(project)
351
+
352
+ if not result['success']:
353
+ console.print(f"[red]✗ {result['error']}[/red]")
354
+ raise SystemExit(1)
355
+
356
+ link = result['link']
357
+ console.print(f"[green]✓ 連結已產生[/green]")
358
+ console.print(f"[dim] 來源: {result.get('dbml_path', 'N/A')}[/dim]")
359
+ console.print("")
360
+ console.print(f"[cyan]連結: {link[:80]}...[/cyan]")
361
+
362
+ if save:
363
+ output_path = save_link_to_file(project, link)
364
+ console.print(f"[green]✓ 已儲存至 {output_path}[/green]")
365
+
366
+ if do_copy:
367
+ try:
368
+ import subprocess
369
+ subprocess.run(['pbcopy'], input=link.encode(), check=True)
370
+ console.print("[green]✓ 已複製到剪貼簿[/green]")
371
+ except Exception:
372
+ console.print("[yellow]無法複製到剪貼簿,請手動複製[/yellow]")
373
+ console.print(link)
374
+
375
+ if do_open:
376
+ try:
377
+ import webbrowser
378
+ webbrowser.open(link)
379
+ console.print("[green]✓ 已在瀏覽器開啟[/green]")
380
+ except Exception:
381
+ console.print("[yellow]無法開啟瀏覽器[/yellow]")
382
+
383
+
384
+ # ============================================================
385
+ # 資料庫遷移指令
386
+ # ============================================================
387
+
388
+ @main.group()
389
+ def db():
390
+ """資料庫遷移管理 (Alembic)
391
+
392
+ 子指令:
393
+ init 初始化 Alembic
394
+ status 檢視遷移狀態
395
+ generate 產生新的遷移檔
396
+ upgrade 升級到最新版本
397
+ downgrade 降級到指定版本
398
+ """
399
+ pass
400
+
401
+
402
+ @db.command()
403
+ @click.argument('project', type=click.Path(), default='.')
404
+ def init(project):
405
+ """初始化 Alembic 遷移環境
406
+
407
+ 使用範例:
408
+ dash db init .
409
+ dash db init /path/to/project
410
+ """
411
+ from .database import init_alembic
412
+
413
+ console.print("[cyan]初始化 Alembic...[/cyan]")
414
+ result = init_alembic(project)
415
+
416
+ if result['success']:
417
+ console.print("[green]✓ Alembic 初始化完成[/green]")
418
+ console.print(f" [dim]已建立: {result.get('alembic_dir')}[/dim]")
419
+ else:
420
+ console.print(f"[red]✗ 初始化失敗: {result.get('error')}[/red]")
421
+ raise SystemExit(1)
422
+
423
+
424
+ @db.command('status')
425
+ @click.argument('project', type=click.Path(), default='.')
426
+ def db_status(project):
427
+ """檢視遷移狀態
428
+
429
+ 顯示:
430
+ - 目前資料庫版本
431
+ - 待套用的遷移
432
+ - Model 與遷移是否同步
433
+
434
+ 使用範例:
435
+ dash db status .
436
+ """
437
+ from .database import get_migration_status
438
+
439
+ console.print("[cyan]檢查遷移狀態...[/cyan]")
440
+ result = get_migration_status(project)
441
+
442
+ if not result['success']:
443
+ console.print(f"[red]✗ {result.get('error')}[/red]")
444
+ raise SystemExit(1)
445
+
446
+ console.print(f" 目前版本: [cyan]{result.get('current', 'N/A')}[/cyan]")
447
+ console.print(f" 最新版本: [cyan]{result.get('head', 'N/A')}[/cyan]")
448
+
449
+ pending = result.get('pending', [])
450
+ if pending:
451
+ console.print(f"\n [yellow]待套用遷移 ({len(pending)}):[/yellow]")
452
+ for p in pending:
453
+ console.print(f" • {p}")
454
+ else:
455
+ console.print("\n [green]✓ 已是最新版本[/green]")
456
+
457
+
458
+ @db.command()
459
+ @click.argument('project', type=click.Path(), default='.')
460
+ @click.option('--message', '-m', required=True, help='遷移描述')
461
+ @click.option('--autogenerate', '-a', is_flag=True, default=True, help='自動偵測 Model 變更')
462
+ def generate(project, message, autogenerate):
463
+ """產生新的遷移檔
464
+
465
+ 使用範例:
466
+ dash db generate . -m "add user table"
467
+ dash db generate . -m "add index to email"
468
+ """
469
+ from .database import generate_migration
470
+
471
+ console.print(f"[cyan]產生遷移: {message}[/cyan]")
472
+ result = generate_migration(project, message, autogenerate=autogenerate)
473
+
474
+ if result['success']:
475
+ console.print("[green]✓ 遷移檔已產生[/green]")
476
+ console.print(f" [dim]{result.get('migration_file')}[/dim]")
477
+
478
+ # 安全檢查
479
+ if result.get('warnings'):
480
+ console.print("\n[yellow]警告:[/yellow]")
481
+ for w in result['warnings']:
482
+ console.print(f" [yellow]• {w}[/yellow]")
483
+ else:
484
+ console.print(f"[red]✗ 產生失敗: {result.get('error')}[/red]")
485
+ raise SystemExit(1)
486
+
487
+
488
+ @db.command()
489
+ @click.argument('project', type=click.Path(), default='.')
490
+ @click.option('--revision', '-r', default='head', help='目標版本 (預設: head)')
491
+ @click.option('--dry-run', is_flag=True, help='預覽模式,顯示 SQL 但不執行')
492
+ def upgrade(project, revision, dry_run):
493
+ """升級資料庫到指定版本
494
+
495
+ 使用範例:
496
+ dash db upgrade .
497
+ dash db upgrade . -r abc123
498
+ dash db upgrade . --dry-run
499
+ """
500
+ from .database import run_upgrade
501
+
502
+ if dry_run:
503
+ console.print(f"[yellow]預覽模式 - 升級到 {revision}[/yellow]")
504
+ else:
505
+ console.print(f"[cyan]升級資料庫到 {revision}...[/cyan]")
506
+
507
+ result = run_upgrade(project, revision, dry_run=dry_run)
508
+
509
+ if result['success']:
510
+ if dry_run:
511
+ console.print("\n[dim]將執行的 SQL:[/dim]")
512
+ console.print(result.get('sql', '(無變更)'))
513
+ else:
514
+ console.print("[green]✓ 升級完成[/green]")
515
+ console.print(f" [dim]新版本: {result.get('current')}[/dim]")
516
+ else:
517
+ console.print(f"[red]✗ 升級失敗: {result.get('error')}[/red]")
518
+ raise SystemExit(1)
519
+
520
+
521
+ @db.command()
522
+ @click.argument('project', type=click.Path(), default='.')
523
+ @click.option('--revision', '-r', required=True, help='目標版本')
524
+ @click.option('--confirm', is_flag=True, help='確認執行危險操作')
525
+ def downgrade(project, revision, confirm):
526
+ """降級資料庫到指定版本
527
+
528
+ 危險操作!會刪除資料。
529
+
530
+ 使用範例:
531
+ dash db downgrade . -r abc123 --confirm
532
+ dash db downgrade . -r -1 --confirm # 降一個版本
533
+ """
534
+ from .database import run_downgrade
535
+
536
+ if not confirm:
537
+ console.print("[red]危險操作!降級可能導致資料遺失。[/red]")
538
+ console.print("[yellow]請加上 --confirm 確認執行[/yellow]")
539
+ raise SystemExit(1)
540
+
541
+ console.print(f"[yellow]降級資料庫到 {revision}...[/yellow]")
542
+ result = run_downgrade(project, revision)
543
+
544
+ if result['success']:
545
+ console.print("[green]✓ 降級完成[/green]")
546
+ console.print(f" [dim]新版本: {result.get('current')}[/dim]")
547
+ else:
548
+ console.print(f"[red]✗ 降級失敗: {result.get('error')}[/red]")
549
+ raise SystemExit(1)
550
+
551
+
552
+ # ============================================================
553
+ # 新功能 v2.0
554
+ # ============================================================
555
+
556
+ @main.command()
557
+ @click.argument('project', type=click.Path(), default='.')
558
+ @click.option('--all', 'check_all', is_flag=True, help='檢查所有專案')
559
+ @click.option('--json', 'output_json', is_flag=True, help='輸出 JSON 格式')
560
+ def health(project, check_all, output_json):
561
+ """專案健康評分
562
+
563
+ 類似 Lighthouse 的評分機制,量化專案品質:
564
+ - 安全性: 機敏資料、依賴漏洞
565
+ - 品質: 程式碼規範、檔案結構
566
+ - 維護性: 技術債務、文件完整度
567
+ - 效能: Bundle 大小、依賴數量
568
+
569
+ 使用範例:
570
+ dash health .
571
+ dash health /path/to/project
572
+ dash health --all
573
+ """
574
+ from .health import run_health_check, HealthChecker
575
+ import json as json_module
576
+
577
+ if check_all:
578
+ projects = DEFAULT_PROJECTS
579
+ else:
580
+ projects = [project]
581
+
582
+ results = []
583
+ for p in projects:
584
+ try:
585
+ if output_json:
586
+ checker = HealthChecker(p)
587
+ scores = checker.check_all()
588
+ total = sum(s.score for s in scores.values()) // len(scores)
589
+ results.append({
590
+ 'project': checker.project_name,
591
+ 'total_score': total,
592
+ 'scores': {k: v.score for k, v in scores.items()}
593
+ })
594
+ else:
595
+ result = run_health_check(p)
596
+ results.append(result)
597
+ except Exception as e:
598
+ console.print(f"[red]錯誤: {p} - {e}[/red]")
599
+
600
+ if output_json:
601
+ console.print(json_module.dumps(results, indent=2, ensure_ascii=False))
602
+
603
+
604
+ @main.command()
605
+ @click.argument('project', type=click.Path(), default='.')
606
+ @click.option('--all', 'stats_all', is_flag=True, help='統計所有專案並比較')
607
+ def stats(project, stats_all):
608
+ """程式碼統計
609
+
610
+ 視覺化專案統計資訊:
611
+ - 語言分佈
612
+ - 檔案數量與行數
613
+ - 最大檔案排行
614
+ - 複雜度警告
615
+
616
+ 使用範例:
617
+ dash stats .
618
+ dash stats /path/to/project
619
+ dash stats --all
620
+ """
621
+ from .stats import run_stats, run_stats_all
622
+
623
+ if stats_all:
624
+ run_stats_all(DEFAULT_PROJECTS)
625
+ else:
626
+ run_stats(project)
627
+
628
+
629
+ @main.command('init-test')
630
+ @click.argument('project', type=click.Path(), default='.')
631
+ @click.option('--e2e', is_flag=True, help='同時設定 Playwright E2E 測試')
632
+ def init_test(project, e2e):
633
+ """初始化測試框架
634
+
635
+ 自動偵測專案類型並設定適合的測試框架:
636
+ - Vite 專案 → Vitest
637
+ - Angular 專案 → Jest
638
+ - 可選 Playwright E2E
639
+
640
+ 使用範例:
641
+ dash init-test .
642
+ dash init-test . --e2e
643
+ """
644
+ from .init_test import run_init_test
645
+
646
+ run_init_test(project, include_e2e=e2e)
647
+
648
+
649
+ @main.command()
650
+ @click.argument('project', type=click.Path(), default='.')
651
+ @click.option('--all', 'test_all', is_flag=True, help='測試所有專案')
652
+ @click.option('--coverage', '-c', is_flag=True, help='產生覆蓋率報告')
653
+ @click.option('--verbose', '-v', is_flag=True, help='詳細輸出')
654
+ def test(project, test_all, coverage, verbose):
655
+ """執行專案測試
656
+
657
+ 自動偵測測試框架並執行:
658
+ - pytest (Python)
659
+ - vitest/jest (JavaScript/TypeScript)
660
+ - karma (Angular)
661
+
662
+ 使用範例:
663
+ dash test .
664
+ dash test . --coverage
665
+ dash test --all
666
+ """
667
+ from .testing import run_test, run_test_all
668
+
669
+ if test_all:
670
+ run_test_all(DEFAULT_PROJECTS, coverage=coverage)
671
+ else:
672
+ run_test(project, coverage=coverage, verbose=verbose)
673
+
674
+
675
+ @main.command()
676
+ @click.argument('url', type=str)
677
+ @click.option('--check', type=click.Choice(['errors', 'load', 'all']), default='errors',
678
+ help='檢查類型 (errors=JS錯誤, load=頁面載入, all=全部)')
679
+ @click.option('--timeout', '-t', type=int, default=30000, help='超時時間 (毫秒)')
680
+ @click.option('--screenshot', '-s', is_flag=True, help='失敗時自動截圖')
681
+ @click.option('--mobile', '-m', is_flag=True, help='手機版測試 (375x812),檢查水平溢出')
682
+ @click.option('--json', 'output_json', is_flag=True, help='輸出 JSON 格式')
683
+ def e2e(url, check, timeout, screenshot, mobile, output_json):
684
+ """E2E 煙霧測試
685
+
686
+ 使用 agent-browser 載入頁面並檢查:
687
+ - JS console 錯誤 (Vue/React TypeError 等)
688
+ - 頁面載入狀態
689
+ - 載入時間
690
+ - 手機版水平溢出 (--mobile)
691
+
692
+ 需要先安裝 agent-browser:
693
+ npm install -g agent-browser
694
+ agent-browser install
695
+
696
+ 使用範例:
697
+ dash e2e https://example.com
698
+ dash e2e https://example.com --check load
699
+ dash e2e https://example.com --timeout 60000
700
+ dash e2e https://example.com --screenshot
701
+ dash e2e https://example.com --mobile
702
+ dash e2e https://example.com --mobile --screenshot
703
+ dash e2e https://example.com --json
704
+ """
705
+ from .e2e import run_e2e_test, check_agent_browser_installed
706
+ import json as json_module
707
+
708
+ # 檢查 agent-browser 是否安裝
709
+ if not check_agent_browser_installed():
710
+ console.print("[red]agent-browser 未安裝[/red]")
711
+ console.print("[yellow]請執行: npm install -g agent-browser && agent-browser install[/yellow]")
712
+ raise SystemExit(1)
713
+
714
+ device_mode = "手機版 (375x812)" if mobile else "桌面版 (1920x1080)"
715
+ console.print(f"[cyan]E2E 測試: {url}[/cyan]")
716
+ options = [f"裝置: {device_mode}", f"檢查類型: {check}", f"超時: {timeout}ms"]
717
+ if screenshot:
718
+ options.append("失敗截圖: ON")
719
+ console.print(f"[dim] {' | '.join(options)}[/dim]")
720
+
721
+ result = run_e2e_test(url, timeout=timeout, check_type=check, screenshot_on_fail=screenshot, mobile=mobile)
722
+
723
+ if output_json:
724
+ console.print(json_module.dumps(result, indent=2, ensure_ascii=False))
725
+ else:
726
+ if result['success']:
727
+ console.print(f"[green]v 測試通過[/green]")
728
+ console.print(f" 載入時間: {result['loadTime']}ms")
729
+ console.print(f" HTTP 狀態: {result['status']}")
730
+ if mobile:
731
+ console.print(f" [green]手機版無水平溢出[/green]")
732
+ if result.get('warnings'):
733
+ console.print(f" [yellow]警告: {len(result['warnings'])} 個[/yellow]")
734
+ else:
735
+ console.print(f"[red]x 測試失敗[/red]")
736
+ console.print(f" HTTP 狀態: {result['status']}")
737
+
738
+ # 手機版特別提示
739
+ if result.get('hasHorizontalScroll'):
740
+ console.print(f"\n[red]手機版水平溢出問題:[/red]")
741
+ console.print(" 內容超出螢幕寬度,請檢查:")
742
+ console.print(" 1. overflow-x: hidden/auto 設定")
743
+ console.print(" 2. 頁籤/表格是否有 flex-wrap: nowrap + overflow-x: auto")
744
+ console.print(" 3. 寬度是否使用 100% 或 max-width")
745
+
746
+ if result.get('errors'):
747
+ console.print(f"\n[red]錯誤 ({len(result['errors'])}):[/red]")
748
+ for err in result['errors'][:5]:
749
+ console.print(f" - {err[:100]}...")
750
+
751
+ # 顯示截圖路徑
752
+ if result.get('screenshot'):
753
+ console.print(f"\n[yellow]截圖已儲存: {result['screenshot']}[/yellow]")
754
+ console.print("[dim] 使用 Read 工具查看截圖進行除錯[/dim]")
755
+
756
+ raise SystemExit(1)
757
+
758
+
759
+ @main.command('test-suite')
760
+ @click.argument('project', type=click.Path(), default='.')
761
+ @click.option('--types', '-t', type=str, default='UIT,Smoke,E2E,UAT',
762
+ help='測試類型 (逗號分隔): UIT,Smoke,E2E,UAT')
763
+ @click.option('--coverage', '-c', is_flag=True, default=True, help='包含覆蓋率報告')
764
+ @click.option('--report', '-r', type=click.Path(), help='輸出 JSON 報告路徑')
765
+ @click.option('--word', '-w', type=click.Path(), help='輸出 Word 報告路徑')
766
+ @click.option('--md', '-m', type=click.Path(), help='輸出 Markdown 報告路徑')
767
+ @click.option('--no-screenshots', is_flag=True, help='不擷取系統截圖')
768
+ def test_suite(project, types, coverage, report, word, md, no_screenshots):
769
+ """四大類型測試套件
770
+
771
+ 執行完整測試套件,包含:
772
+ - UIT: 單元測試 (Vitest/Jest/Pytest) + 覆蓋率
773
+ - Smoke: 煙霧測試 (Playwright smoke.spec.ts)
774
+ - E2E: 端對端測試 (Playwright mes-system.spec.ts)
775
+ - UAT: 使用者驗收測試 (Playwright uat.spec.ts)
776
+
777
+ 報告格式:
778
+ - --word: Word 文件 (含圖表、截圖)
779
+ - --md: Markdown 文件 (適合 GitHub)
780
+ - --report: JSON 原始資料
781
+
782
+ 使用範例:
783
+ dash test-suite .
784
+ dash test-suite . --types UIT,Smoke
785
+ dash test-suite . --report ./test-report.json
786
+ dash test-suite . --word ./test-report.docx
787
+ dash test-suite . --md ./test-report.md
788
+ dash test-suite . --word report.docx --no-screenshots
789
+ """
790
+ from .test_suite import run_test_suite, run_test_suite_report
791
+
792
+ test_types = [t.strip() for t in types.split(',')]
793
+
794
+ # 如果指定 Word 報告,使用 word_report 模組
795
+ if word:
796
+ from .word_report import run_and_generate_report
797
+ result = run_and_generate_report(
798
+ project,
799
+ output_path=word,
800
+ test_types=test_types,
801
+ include_screenshots=not no_screenshots
802
+ )
803
+ elif md:
804
+ from .markdown_report import run_and_generate_markdown_report
805
+ result = run_and_generate_markdown_report(
806
+ project,
807
+ output_path=md,
808
+ test_types=test_types,
809
+ )
810
+ elif report:
811
+ result = run_test_suite_report(project, output_path=report)
812
+ else:
813
+ result = run_test_suite(project, test_types=test_types, coverage=coverage)
814
+
815
+ if not result.get('success', True):
816
+ raise SystemExit(1)
817
+
818
+
819
+ @main.command('gas-test')
820
+ @click.argument('project', type=click.Path(), default='/Users/dash/Documents/github/GAS/mes')
821
+ @click.option('--types', '-t', type=str, default='UIT,Smoke,E2E,UAT',
822
+ help='測試類型 (逗號分隔): UIT,Smoke,E2E,UAT')
823
+ @click.option('--word', '-w', type=click.Path(), help='輸出 Word 報告路徑')
824
+ @click.option('--url', '-u', type=str, help='GAS 部署 URL (預設使用 MES 正式環境)')
825
+ def gas_test(project, types, word, url):
826
+ """GAS MES 四大測試套件
827
+
828
+ 針對 Google Apps Script MES 系統的專用測試:
829
+ - UIT: 程式碼靜態分析 (Code.js, Database.js, HTML)
830
+ - Smoke: 頁面載入測試 (各頁籤)
831
+ - E2E: 完整流程測試 (登入→操作→驗證)
832
+ - UAT: 角色權限驗證
833
+
834
+ 使用範例:
835
+ dash gas-test
836
+ dash gas-test /path/to/gas/mes
837
+ dash gas-test --types UIT,Smoke
838
+ dash gas-test --word report.docx
839
+ dash gas-test --url https://script.google.com/macros/s/xxx/exec
840
+ """
841
+ from .gas_mes_test import run_gas_test, run_gas_test_with_report
842
+
843
+ test_types = [t.strip() for t in types.split(',')]
844
+
845
+ if word:
846
+ result = run_gas_test_with_report(
847
+ project,
848
+ output_path=word,
849
+ test_types=test_types,
850
+ url=url
851
+ )
852
+ else:
853
+ result = run_gas_test(project, test_types=test_types, url=url)
854
+
855
+ if not result.get('success', True):
856
+ raise SystemExit(1)
857
+
858
+
859
+ @main.command()
860
+ @click.argument('url', type=str)
861
+ @click.option('--category', '-c', type=str, default='performance,accessibility,best-practices,seo',
862
+ help='測試類別 (逗號分隔)')
863
+ @click.option('--timeout', '-t', type=int, default=120000, help='超時時間 (毫秒)')
864
+ @click.option('--json', 'output_json', is_flag=True, help='輸出 JSON 格式')
865
+ @click.option('--verbose', '-v', is_flag=True, help='詳細輸出')
866
+ def perf(url, category, timeout, output_json, verbose):
867
+ """Lighthouse 效能測試
868
+
869
+ 分析網站效能並提供改善建議:
870
+ - Performance (效能分數)
871
+ - Accessibility (無障礙)
872
+ - Best Practices (最佳實踐)
873
+ - SEO (搜尋引擎優化)
874
+
875
+ 使用範例:
876
+ dash perf https://example.com
877
+ dash perf https://example.com -c performance
878
+ dash perf https://example.com --json
879
+ dash perf https://example.com -v
880
+ """
881
+ from .perf import run_perf_test, print_perf_report, check_lighthouse_installed
882
+ import json as json_module
883
+
884
+ console.print(f"[cyan]Lighthouse 效能測試: {url}[/cyan]")
885
+ console.print(f"[dim] 類別: {category} | 超時: {timeout}ms[/dim]")
886
+ console.print()
887
+
888
+ with console.status("[bold green]正在分析效能..."):
889
+ result = run_perf_test(url, categories=category, timeout=timeout)
890
+
891
+ if output_json:
892
+ console.print(json_module.dumps(result, indent=2, ensure_ascii=False))
893
+ else:
894
+ print_perf_report(result, verbose=verbose)
895
+
896
+ # 效能分數低於 50 則 exit 1
897
+ if result.get('success') and result.get('scores', {}).get('performance', 0) < 50:
898
+ raise SystemExit(1)
899
+ elif not result.get('success'):
900
+ raise SystemExit(1)
901
+
902
+
903
+ @main.command()
904
+ @click.argument('project', type=click.Path(), default='.')
905
+ @click.option('--test/--no-test', 'include_test', default=True, help='是否執行測試')
906
+ @click.option('--screenshot', '-s', is_flag=True, help='擷取 UI 截圖')
907
+ @click.option('--url', '-u', multiple=True, help='截圖的 URL (可多個)')
908
+ @click.option('--open/--no-open', 'open_browser', default=True, help='是否開啟瀏覽器')
909
+ def report(project, include_test, screenshot, url, open_browser):
910
+ """產生完整專案報告
911
+
912
+ 整合健康評分、程式碼統計、測試結果、UI 截圖,
913
+ 產生專業的 HTML 報告。
914
+
915
+ 使用範例:
916
+ dash report .
917
+ dash report . --screenshot
918
+ dash report . --screenshot -u http://localhost:3000
919
+ dash report . --no-test
920
+ """
921
+ from .report import run_report
922
+
923
+ urls = list(url) if url else None
924
+ run_report(
925
+ project,
926
+ include_tests=include_test,
927
+ include_screenshots=screenshot,
928
+ urls=urls,
929
+ open_browser=open_browser
930
+ )
931
+
932
+
933
+ @main.command()
934
+ @click.argument('project', type=click.Path(), default='.')
935
+ @click.option('--fix', 'auto_fix', is_flag=True, help='發現問題自動修復')
936
+ @click.option('--interval', '-i', type=float, default=1.0, help='檢查間隔(秒)')
937
+ def watch(project, auto_fix, interval):
938
+ """即時監控模式
939
+
940
+ 監控檔案變更並自動執行驗證:
941
+ - 檔案儲存時自動驗證
942
+ - 即時顯示問題
943
+ - 可選自動修復
944
+
945
+ 使用範例:
946
+ dash watch .
947
+ dash watch /path/to/project
948
+ dash watch . --fix
949
+ """
950
+ from .watch import run_watch
951
+
952
+ run_watch(project, auto_fix=auto_fix, interval=interval)
953
+
954
+
955
+ # ============================================================
956
+ # AI 引擎指令
957
+ # ============================================================
958
+
959
+ def _handle_ai_error(e: Exception) -> None:
960
+ """處理 AI 相關錯誤,提供精確的修復建議"""
961
+ error_msg = str(e).lower()
962
+
963
+ if 'google.genai' in error_msg or 'google-genai' in error_msg:
964
+ console.print("[red]缺少 Google GenAI SDK[/red]")
965
+ console.print("[yellow]請執行: pip install google-genai[/yellow]")
966
+ elif 'dotenv' in error_msg:
967
+ console.print("[red]缺少 python-dotenv[/red]")
968
+ console.print("[yellow]請執行: pip install python-dotenv[/yellow]")
969
+ elif 'gemini_api_key' in error_msg:
970
+ # 顯示完整的錯誤訊息(包含診斷資訊)
971
+ console.print(f"[red]{e}[/red]")
972
+ elif isinstance(e, ImportError):
973
+ console.print(f"[red]模組載入失敗: {e}[/red]")
974
+ console.print("[yellow]請執行: pip install google-genai python-dotenv[/yellow]")
975
+ elif isinstance(e, ValueError):
976
+ console.print(f"[red]設定錯誤: {e}[/red]")
977
+ else:
978
+ console.print(f"[red]錯誤: {e}[/red]")
979
+
980
+
981
+ @main.group()
982
+ def ai():
983
+ """AI 程式碼助手 (Gemini 2.5)
984
+
985
+ 使用 Google GenAI SDK (新版)。
986
+ 需設定環境變數 GEMINI_API_KEY。
987
+
988
+ 子指令:
989
+ analyze 分析程式碼
990
+ fix 建議修復方案
991
+ test 生成測試
992
+ explain 解釋程式碼
993
+ review 審查 commit
994
+ """
995
+ pass
996
+
997
+
998
+ @ai.command()
999
+ @click.argument('file', type=click.Path())
1000
+ @click.option('--focus', '-f', type=click.Choice(['general', 'security', 'performance', 'quality']),
1001
+ default='general', help='分析重點')
1002
+ def analyze(file, focus):
1003
+ """分析程式碼
1004
+
1005
+ 使用範例:
1006
+ dash ai analyze src/main.py
1007
+ dash ai analyze src/api.ts --focus security
1008
+ """
1009
+ try:
1010
+ from .ai_engine import get_ai
1011
+ ai_engine = get_ai()
1012
+
1013
+ with open(file, 'r', encoding='utf-8') as f:
1014
+ code = f.read()
1015
+
1016
+ console.print(f"[cyan]分析中: {file}[/cyan]")
1017
+ console.print(f"[dim]重點: {focus}[/dim]\n")
1018
+
1019
+ response = ai_engine.analyze_code(code, focus=focus)
1020
+ if response.success:
1021
+ console.print(response.content)
1022
+ else:
1023
+ console.print(f"[red]錯誤: {response.error}[/red]")
1024
+ except Exception as e:
1025
+ _handle_ai_error(e)
1026
+
1027
+
1028
+ @ai.command()
1029
+ @click.argument('file', type=click.Path())
1030
+ @click.option('--error', '-e', required=True, help='錯誤訊息')
1031
+ def fix(file, error):
1032
+ """建議修復方案
1033
+
1034
+ 使用範例:
1035
+ dash ai fix src/main.py -e "TypeError: Cannot read property"
1036
+ """
1037
+ try:
1038
+ from .ai_engine import get_ai
1039
+ ai_engine = get_ai()
1040
+
1041
+ with open(file, 'r', encoding='utf-8') as f:
1042
+ code = f.read()
1043
+
1044
+ console.print(f"[cyan]分析錯誤: {file}[/cyan]\n")
1045
+
1046
+ response = ai_engine.suggest_fix(code, error)
1047
+ if response.success:
1048
+ console.print(response.content)
1049
+ else:
1050
+ console.print(f"[red]錯誤: {response.error}[/red]")
1051
+ except Exception as e:
1052
+ _handle_ai_error(e)
1053
+
1054
+
1055
+ @ai.command('test')
1056
+ @click.argument('file', type=click.Path())
1057
+ @click.option('--framework', '-f', default='auto', help='測試框架 (auto/pytest/jest/vitest)')
1058
+ @click.option('--coverage', '-c', type=click.Choice(['basic', 'comprehensive', 'edge-cases']),
1059
+ default='comprehensive', help='覆蓋範圍')
1060
+ def generate_test(file, framework, coverage):
1061
+ """生成測試程式碼
1062
+
1063
+ 使用範例:
1064
+ dash ai test src/utils.py
1065
+ dash ai test src/api.ts --framework jest
1066
+ """
1067
+ try:
1068
+ from .ai_engine import get_ai
1069
+ ai_engine = get_ai()
1070
+
1071
+ with open(file, 'r', encoding='utf-8') as f:
1072
+ code = f.read()
1073
+
1074
+ console.print(f"[cyan]產生測試: {file}[/cyan]\n")
1075
+
1076
+ response = ai_engine.generate_tests(code, framework=framework, coverage=coverage)
1077
+ if response.success:
1078
+ console.print(response.content)
1079
+ else:
1080
+ console.print(f"[red]錯誤: {response.error}[/red]")
1081
+ except Exception as e:
1082
+ _handle_ai_error(e)
1083
+
1084
+
1085
+ @ai.command()
1086
+ @click.argument('file', type=click.Path())
1087
+ @click.option('--detail', '-d', type=click.Choice(['brief', 'medium', 'detailed']),
1088
+ default='medium', help='詳細程度')
1089
+ def explain(file, detail):
1090
+ """解釋程式碼
1091
+
1092
+ 使用範例:
1093
+ dash ai explain src/complex-algo.py
1094
+ dash ai explain src/auth.ts --detail detailed
1095
+ """
1096
+ try:
1097
+ from .ai_engine import get_ai
1098
+ ai_engine = get_ai()
1099
+
1100
+ with open(file, 'r', encoding='utf-8') as f:
1101
+ code = f.read()
1102
+
1103
+ console.print(f"[cyan]解釋: {file}[/cyan]\n")
1104
+
1105
+ response = ai_engine.explain_code(code, detail_level=detail)
1106
+ if response.success:
1107
+ console.print(response.content)
1108
+ else:
1109
+ console.print(f"[red]錯誤: {response.error}[/red]")
1110
+ except Exception as e:
1111
+ _handle_ai_error(e)
1112
+
1113
+
1114
+ @ai.command()
1115
+ @click.argument('project', type=click.Path(), default='.')
1116
+ def review(project):
1117
+ """審查最新 commit
1118
+
1119
+ 使用範例:
1120
+ dash ai review .
1121
+ """
1122
+ import subprocess
1123
+ try:
1124
+ from .ai_engine import get_ai
1125
+ ai_engine = get_ai()
1126
+
1127
+ # 取得最新 commit 的 diff
1128
+ result = subprocess.run(
1129
+ ['git', 'diff', 'HEAD~1', 'HEAD'],
1130
+ cwd=project,
1131
+ capture_output=True,
1132
+ text=True
1133
+ )
1134
+ diff = result.stdout
1135
+
1136
+ # 取得 commit message
1137
+ msg_result = subprocess.run(
1138
+ ['git', 'log', '-1', '--pretty=%B'],
1139
+ cwd=project,
1140
+ capture_output=True,
1141
+ text=True
1142
+ )
1143
+ commit_msg = msg_result.stdout.strip()
1144
+
1145
+ console.print(f"[cyan]審查 commit: {commit_msg[:50]}...[/cyan]\n")
1146
+
1147
+ response = ai_engine.review_commit(diff, commit_msg)
1148
+ if response.success:
1149
+ console.print(response.content)
1150
+ else:
1151
+ console.print(f"[red]錯誤: {response.error}[/red]")
1152
+ except Exception as e:
1153
+ _handle_ai_error(e)
1154
+
1155
+
1156
+ # ============================================================
1157
+ # OpenSpec 指令 (Spec-Driven Development)
1158
+ # ============================================================
1159
+
1160
+ @main.group()
1161
+ def spec():
1162
+ """OpenSpec 規格驅動開發
1163
+
1164
+ 使用 Spec-Driven Development (SDD) 工作流程管理功能規格。
1165
+
1166
+ 子指令:
1167
+ init 初始化 OpenSpec
1168
+ list 列出活動變更
1169
+ view 互動式儀表板
1170
+ show 顯示變更詳情
1171
+ validate 驗證規格格式
1172
+ archive 歸檔完成的變更
1173
+ status 快速狀態總覽
1174
+ """
1175
+ pass
1176
+
1177
+
1178
+ @spec.command('init')
1179
+ @click.argument('project', type=click.Path(), default='.')
1180
+ def spec_init(project):
1181
+ """初始化 OpenSpec
1182
+
1183
+ 在專案中建立 openspec/ 目錄結構。
1184
+
1185
+ 使用範例:
1186
+ dash spec init .
1187
+ dash spec init /path/to/project
1188
+ """
1189
+ from .spec import OpenSpecWrapper
1190
+
1191
+ console.print("[cyan]初始化 OpenSpec...[/cyan]")
1192
+ result = OpenSpecWrapper.init(project)
1193
+
1194
+ if result.success:
1195
+ console.print("[green]v OpenSpec 初始化完成[/green]")
1196
+ console.print(f" [dim]openspec/ 目錄已建立[/dim]")
1197
+ console.print(f" [dim]specs/ - 功能規格[/dim]")
1198
+ console.print(f" [dim]changes/ - 活動變更[/dim]")
1199
+ else:
1200
+ console.print(f"[red]x {result.error}[/red]")
1201
+ raise SystemExit(1)
1202
+
1203
+
1204
+ @spec.command('list')
1205
+ @click.argument('project', type=click.Path(), default='.')
1206
+ def spec_list(project):
1207
+ """列出活動變更
1208
+
1209
+ 顯示所有待處理的變更提案。
1210
+
1211
+ 使用範例:
1212
+ dash spec list .
1213
+ """
1214
+ from .spec import OpenSpecWrapper
1215
+
1216
+ result = OpenSpecWrapper.list_changes(project)
1217
+
1218
+ if result.success:
1219
+ changes = result.data.get('changes', [])
1220
+ count = result.data.get('count', 0)
1221
+
1222
+ if count == 0:
1223
+ console.print("[dim]目前沒有活動變更[/dim]")
1224
+ else:
1225
+ console.print(f"[cyan]活動變更 ({count}):[/cyan]")
1226
+ for change in changes:
1227
+ console.print(f" - {change}")
1228
+ else:
1229
+ console.print(f"[red]x {result.error}[/red]")
1230
+ raise SystemExit(1)
1231
+
1232
+
1233
+ @spec.command('view')
1234
+ @click.argument('project', type=click.Path(), default='.')
1235
+ def spec_view(project):
1236
+ """互動式儀表板
1237
+
1238
+ 開啟 OpenSpec 互動式 TUI 儀表板。
1239
+
1240
+ 使用範例:
1241
+ dash spec view .
1242
+ """
1243
+ from .spec import OpenSpecWrapper
1244
+
1245
+ if not OpenSpecWrapper._check_installed():
1246
+ console.print("[red]openspec 未安裝[/red]")
1247
+ console.print("[yellow]請執行: npm install -g @fission-ai/openspec@latest[/yellow]")
1248
+ raise SystemExit(1)
1249
+
1250
+ OpenSpecWrapper.view(project)
1251
+
1252
+
1253
+ @spec.command('show')
1254
+ @click.argument('project', type=click.Path(), default='.')
1255
+ @click.argument('change_name', type=str)
1256
+ def spec_show(project, change_name):
1257
+ """顯示變更詳情
1258
+
1259
+ 顯示指定變更提案的完整內容。
1260
+
1261
+ 使用範例:
1262
+ dash spec show . my-feature
1263
+ """
1264
+ from .spec import OpenSpecWrapper
1265
+
1266
+ result = OpenSpecWrapper.show(project, change_name)
1267
+
1268
+ if result.success:
1269
+ console.print(f"[cyan]變更: {change_name}[/cyan]")
1270
+ console.print()
1271
+ console.print(result.data.get('content', ''))
1272
+ else:
1273
+ console.print(f"[red]x {result.error}[/red]")
1274
+ raise SystemExit(1)
1275
+
1276
+
1277
+ @spec.command('validate')
1278
+ @click.argument('project', type=click.Path(), default='.')
1279
+ @click.argument('change_name', type=str)
1280
+ def spec_validate(project, change_name):
1281
+ """驗證規格格式
1282
+
1283
+ 檢查變更提案的格式是否正確。
1284
+
1285
+ 使用範例:
1286
+ dash spec validate . my-feature
1287
+ """
1288
+ from .spec import OpenSpecWrapper
1289
+
1290
+ console.print(f"[cyan]驗證: {change_name}[/cyan]")
1291
+ result = OpenSpecWrapper.validate(project, change_name)
1292
+
1293
+ if result.success and result.data.get('valid'):
1294
+ console.print("[green]v 規格格式正確[/green]")
1295
+ else:
1296
+ console.print(f"[red]x 驗證失敗[/red]")
1297
+ if result.error:
1298
+ console.print(f" [red]{result.error}[/red]")
1299
+ raise SystemExit(1)
1300
+
1301
+
1302
+ @spec.command('archive')
1303
+ @click.argument('project', type=click.Path(), default='.')
1304
+ @click.argument('change_name', type=str)
1305
+ @click.option('--yes', '-y', is_flag=True, help='自動確認歸檔')
1306
+ def spec_archive(project, change_name, yes):
1307
+ """歸檔完成的變更
1308
+
1309
+ 將已完成的變更提案移至歸檔目錄。
1310
+
1311
+ 使用範例:
1312
+ dash spec archive . my-feature
1313
+ dash spec archive . my-feature -y
1314
+ """
1315
+ from .spec import OpenSpecWrapper
1316
+
1317
+ if not yes:
1318
+ console.print(f"[yellow]確定要歸檔 '{change_name}'?[/yellow]")
1319
+ console.print("[dim]使用 -y 跳過確認[/dim]")
1320
+ if not click.confirm("繼續"):
1321
+ return
1322
+
1323
+ result = OpenSpecWrapper.archive(project, change_name, yes=True)
1324
+
1325
+ if result.success:
1326
+ console.print(f"[green]v {result.message}[/green]")
1327
+ else:
1328
+ console.print(f"[red]x {result.error}[/red]")
1329
+ raise SystemExit(1)
1330
+
1331
+
1332
+ @spec.command('status')
1333
+ @click.argument('project', type=click.Path(), default='.')
1334
+ def spec_status(project):
1335
+ """快速狀態總覽
1336
+
1337
+ 顯示 OpenSpec 的狀態摘要。
1338
+
1339
+ 使用範例:
1340
+ dash spec status .
1341
+ """
1342
+ from .spec import get_spec_status
1343
+
1344
+ status = get_spec_status(project)
1345
+
1346
+ if not status['initialized']:
1347
+ console.print("[yellow]OpenSpec 尚未初始化[/yellow]")
1348
+ console.print("[dim]執行: dash spec init .[/dim]")
1349
+ return
1350
+
1351
+ console.print("[cyan]OpenSpec 狀態[/cyan]")
1352
+ console.print(f" 規格數量: {status['specs_count']}")
1353
+ console.print(f" 活動變更: {status['changes_count']}")
1354
+ console.print(f" 已歸檔: {status['archive_count']}")
1355
+
1356
+ if status['stale_changes']:
1357
+ console.print()
1358
+ console.print(f"[yellow]過期變更 (超過 7 天):[/yellow]")
1359
+ for change in status['stale_changes']:
1360
+ console.print(f" [yellow]-[/yellow] {change}")
1361
+
1362
+
1363
+ @main.command()
1364
+ def doctor():
1365
+ """診斷開發環境
1366
+
1367
+ 顯示系統資訊、Python 路徑、套件版本等,方便偵錯。
1368
+
1369
+ 使用範例:
1370
+ dash doctor
1371
+ """
1372
+ import sys
1373
+ import os
1374
+ import platform
1375
+ from pathlib import Path
1376
+
1377
+ console.print("[cyan]═══ DashAI DevTools 診斷資訊 ═══[/cyan]\n")
1378
+
1379
+ # 系統資訊
1380
+ console.print("[yellow]系統資訊[/yellow]")
1381
+ console.print(f" 作業系統: {platform.system()} {platform.release()}")
1382
+ console.print(f" Python 版本: {sys.version.split()[0]}")
1383
+ console.print(f" Python 執行檔: {sys.executable}")
1384
+ console.print()
1385
+
1386
+ # 工作目錄
1387
+ console.print("[yellow]工作目錄[/yellow]")
1388
+ console.print(f" 當前目錄: {os.getcwd()}")
1389
+ console.print(f" 家目錄: {Path.home()}")
1390
+ console.print()
1391
+
1392
+ # Python 路徑
1393
+ console.print("[yellow]Python 路徑 (sys.path)[/yellow]")
1394
+ for i, p in enumerate(sys.path, 1):
1395
+ console.print(f" {i}. {p}")
1396
+ console.print()
1397
+
1398
+ # 套件資訊
1399
+ console.print("[yellow]已安裝套件[/yellow]")
1400
+ try:
1401
+ import importlib.metadata
1402
+ dist = importlib.metadata.distribution('dash-devtools')
1403
+ console.print(f" dash-devtools: {dist.version}")
1404
+ console.print(f" 安裝位置: {dist.locate_file('')}")
1405
+ except Exception as e:
1406
+ console.print(f" [red]無法取得套件資訊: {e}[/red]")
1407
+ console.print()
1408
+
1409
+ # 依賴套件
1410
+ console.print("[yellow]核心依賴套件[/yellow]")
1411
+ deps = ['click', 'rich', 'pyyaml', 'jinja2']
1412
+ for dep in deps:
1413
+ try:
1414
+ import importlib.metadata
1415
+ ver = importlib.metadata.version(dep)
1416
+ console.print(f" ✓ {dep}: {ver}")
1417
+ except:
1418
+ console.print(f" ✗ {dep}: [red]未安裝[/red]")
1419
+ console.print()
1420
+
1421
+ # 可選依賴
1422
+ console.print("[yellow]可選依賴套件[/yellow]")
1423
+ optional_deps = [
1424
+ ('google-genai', 'AI 功能'),
1425
+ ('opencv-python', 'Vision 功能'),
1426
+ ('pillow', 'Vision 功能'),
1427
+ ]
1428
+ for dep, desc in optional_deps:
1429
+ try:
1430
+ import importlib.metadata
1431
+ ver = importlib.metadata.version(dep)
1432
+ console.print(f" ✓ {dep}: {ver} ({desc})")
1433
+ except:
1434
+ console.print(f" ✗ {dep}: [dim]未安裝 ({desc})[/dim]")
1435
+ console.print()
1436
+
1437
+ # 環境變數
1438
+ console.print("[yellow]相關環境變數[/yellow]")
1439
+ env_vars = ['GEMINI_API_KEY', 'GITGUARDIAN_API_KEY']
1440
+ for var in env_vars:
1441
+ val = os.environ.get(var)
1442
+ if val:
1443
+ console.print(f" ✓ {var}: [dim]已設定[/dim]")
1444
+ else:
1445
+ console.print(f" ✗ {var}: [dim]未設定[/dim]")
1446
+ console.print()
1447
+
1448
+ console.print("[green]診斷完成![/green]")
1449
+
1450
+
1451
+ if __name__ == '__main__':
1452
+ main()