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.
- dash_devtools/__init__.py +8 -0
- dash_devtools/__main__.py +11 -0
- dash_devtools/ai_engine.py +441 -0
- dash_devtools/browser.py +541 -0
- dash_devtools/cli.py +1452 -0
- dash_devtools/database.py +338 -0
- dash_devtools/dbdiagram.py +183 -0
- dash_devtools/e2e.py +329 -0
- dash_devtools/fixers/__init__.py +57 -0
- dash_devtools/fixers/migration_fixer.py +115 -0
- dash_devtools/fixers/ux_fixer.py +106 -0
- dash_devtools/fixers/version_bumper.py +115 -0
- dash_devtools/gas_mes_test.py +1241 -0
- dash_devtools/generators/__init__.py +84 -0
- dash_devtools/health.py +476 -0
- dash_devtools/hooks/__init__.py +250 -0
- dash_devtools/hooks/pre_commit.py +161 -0
- dash_devtools/hooks/pre_push.py +275 -0
- dash_devtools/init_test.py +352 -0
- dash_devtools/markdown_report.py +309 -0
- dash_devtools/migrators/__init__.py +21 -0
- dash_devtools/perf.py +321 -0
- dash_devtools/report.py +667 -0
- dash_devtools/reporters/__init__.py +11 -0
- dash_devtools/spec.py +230 -0
- dash_devtools/stats.py +355 -0
- dash_devtools/test_suite.py +690 -0
- dash_devtools/testing.py +416 -0
- dash_devtools/validators/__init__.py +157 -0
- dash_devtools/validators/backend/__init__.py +12 -0
- dash_devtools/validators/backend/nodejs.py +245 -0
- dash_devtools/validators/backend/python.py +439 -0
- dash_devtools/validators/code_quality.py +243 -0
- dash_devtools/validators/common/__init__.py +11 -0
- dash_devtools/validators/common/quality.py +319 -0
- dash_devtools/validators/common/security.py +270 -0
- dash_devtools/validators/common/spec.py +273 -0
- dash_devtools/validators/detector.py +394 -0
- dash_devtools/validators/frontend/__init__.py +14 -0
- dash_devtools/validators/frontend/angular.py +245 -0
- dash_devtools/validators/frontend/gas.py +310 -0
- dash_devtools/validators/frontend/vite.py +539 -0
- dash_devtools/validators/migration.py +292 -0
- dash_devtools/validators/performance.py +167 -0
- dash_devtools/validators/security.py +205 -0
- dash_devtools/vision/__init__.py +368 -0
- dash_devtools/watch.py +266 -0
- dash_devtools/word_report.py +690 -0
- dash_devtools-1.0.0.dist-info/METADATA +834 -0
- dash_devtools-1.0.0.dist-info/RECORD +53 -0
- dash_devtools-1.0.0.dist-info/WHEEL +5 -0
- dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
- 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()
|