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
@@ -0,0 +1,84 @@
1
+ """
2
+ 文件產生工具
3
+ """
4
+
5
+ from pathlib import Path
6
+ import json
7
+
8
+ __all__ = ['generate_claude_md', 'get_release_status', 'publish_release']
9
+
10
+
11
+ def generate_claude_md(project_path):
12
+ """產生 CLAUDE.md"""
13
+ project = Path(project_path)
14
+ claude_dir = project / '.claude'
15
+ claude_md = claude_dir / 'CLAUDE.md'
16
+
17
+ if not project.exists():
18
+ return {'success': False, 'error': '專案不存在'}
19
+
20
+ # 基本模板
21
+ template = f"""# {project.name}
22
+
23
+ ## 專案概述
24
+
25
+ [待補充]
26
+
27
+ ## 技術堆疊
28
+
29
+ | 類別 | 技術 |
30
+ |------|------|
31
+ | 前端 | [待補充] |
32
+ | 後端 | [待補充] |
33
+ | 資料庫 | [待補充] |
34
+
35
+ ## 開發規範
36
+
37
+ 遵循 DashAI 開發規範,詳見全域 CLAUDE.md
38
+ """
39
+
40
+ try:
41
+ claude_dir.mkdir(exist_ok=True)
42
+ claude_md.write_text(template, encoding='utf-8')
43
+ return {'success': True}
44
+ except Exception as e:
45
+ return {'success': False, 'error': str(e)}
46
+
47
+
48
+ def get_release_status():
49
+ """取得版本狀態"""
50
+ projects_dir = Path('/Users/dash/Documents/github')
51
+ status = {}
52
+
53
+ for project in projects_dir.iterdir():
54
+ if not project.is_dir():
55
+ continue
56
+ pkg_path = project / 'package.json'
57
+ if pkg_path.exists():
58
+ try:
59
+ pkg = json.loads(pkg_path.read_text())
60
+ status[project.name] = {
61
+ 'version': pkg.get('version', 'N/A'),
62
+ 'last_update': 'N/A'
63
+ }
64
+ except Exception:
65
+ pass
66
+
67
+ return status
68
+
69
+
70
+ def publish_release(project_path, version):
71
+ """發布版本"""
72
+ project = Path(project_path)
73
+ pkg_path = project / 'package.json'
74
+
75
+ if not pkg_path.exists():
76
+ return {'success': False, 'error': 'package.json 不存在'}
77
+
78
+ try:
79
+ pkg = json.loads(pkg_path.read_text())
80
+ pkg['version'] = version
81
+ pkg_path.write_text(json.dumps(pkg, indent=2, ensure_ascii=False))
82
+ return {'success': True}
83
+ except Exception as e:
84
+ return {'success': False, 'error': str(e)}
@@ -0,0 +1,476 @@
1
+ """
2
+ 專案健康評分系統
3
+
4
+ 類似 Lighthouse 的評分機制,量化專案品質:
5
+ - 安全性 (Security): 機敏資料、依賴漏洞
6
+ - 品質 (Quality): 程式碼規範、檔案結構
7
+ - 維護性 (Maintainability): 技術債務、文件完整度
8
+ - 效能 (Performance): Bundle 大小、載入時間
9
+ """
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from dataclasses import dataclass, field
14
+ from typing import Dict, List
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.progress import Progress, BarColumn, TextColumn
18
+ from rich.table import Table
19
+ from rich.text import Text
20
+
21
+ console = Console()
22
+
23
+
24
+ @dataclass
25
+ class HealthScore:
26
+ """健康評分結果"""
27
+ category: str
28
+ score: int # 0-100
29
+ max_score: int = 100
30
+ issues: List[str] = field(default_factory=list)
31
+ recommendations: List[str] = field(default_factory=list)
32
+
33
+ @property
34
+ def percentage(self) -> float:
35
+ return (self.score / self.max_score) * 100
36
+
37
+ @property
38
+ def grade(self) -> str:
39
+ """轉換為等級"""
40
+ if self.score >= 90:
41
+ return 'A'
42
+ elif self.score >= 80:
43
+ return 'B'
44
+ elif self.score >= 70:
45
+ return 'C'
46
+ elif self.score >= 60:
47
+ return 'D'
48
+ else:
49
+ return 'F'
50
+
51
+ @property
52
+ def color(self) -> str:
53
+ """根據分數決定顏色"""
54
+ if self.score >= 90:
55
+ return 'green'
56
+ elif self.score >= 70:
57
+ return 'yellow'
58
+ elif self.score >= 50:
59
+ return 'orange1'
60
+ else:
61
+ return 'red'
62
+
63
+
64
+ class HealthChecker:
65
+ """專案健康檢查器"""
66
+
67
+ def __init__(self, project_path: str):
68
+ self.project_path = Path(project_path)
69
+ self.project_name = self.project_path.name
70
+
71
+ def check_all(self) -> Dict[str, HealthScore]:
72
+ """執行完整健康檢查"""
73
+ scores = {
74
+ 'security': self._check_security(),
75
+ 'quality': self._check_quality(),
76
+ 'maintainability': self._check_maintainability(),
77
+ 'performance': self._check_performance(),
78
+ }
79
+
80
+ # 如果有 openspec 目錄,加入規格健康評分
81
+ openspec_dir = self.project_path / 'openspec'
82
+ if openspec_dir.exists():
83
+ scores['spec'] = self._check_spec()
84
+
85
+ return scores
86
+
87
+ def _check_security(self) -> HealthScore:
88
+ """安全性檢查"""
89
+ score = 100
90
+ issues = []
91
+ recommendations = []
92
+
93
+ # 檢查 .env 是否在 .gitignore
94
+ gitignore = self.project_path / '.gitignore'
95
+ if gitignore.exists():
96
+ content = gitignore.read_text()
97
+ if '.env' not in content:
98
+ score -= 20
99
+ issues.append('.env 未加入 .gitignore')
100
+ recommendations.append('將 .env 加入 .gitignore')
101
+ else:
102
+ score -= 10
103
+ issues.append('缺少 .gitignore 檔案')
104
+
105
+ # 檢查是否有 .env 檔案被追蹤
106
+ env_file = self.project_path / '.env'
107
+ if env_file.exists():
108
+ # 檢查是否在 git 中
109
+ git_dir = self.project_path / '.git'
110
+ if git_dir.exists():
111
+ import subprocess
112
+ result = subprocess.run(
113
+ ['git', 'ls-files', '.env'],
114
+ cwd=self.project_path,
115
+ capture_output=True,
116
+ text=True
117
+ )
118
+ if result.stdout.strip():
119
+ score -= 30
120
+ issues.append('.env 檔案被 git 追蹤')
121
+
122
+ # 檢查硬編碼的機敏資料
123
+ sensitive_patterns = [
124
+ ('password', -15, '發現硬編碼密碼'),
125
+ ('api_key', -15, '發現硬編碼 API Key'),
126
+ ('secret', -10, '發現硬編碼 Secret'),
127
+ ]
128
+
129
+ for ext in ['*.js', '*.ts', '*.py']:
130
+ for file_path in self.project_path.rglob(ext):
131
+ if 'node_modules' in str(file_path) or '.git' in str(file_path):
132
+ continue
133
+ try:
134
+ content = file_path.read_text().lower()
135
+ for pattern, penalty, message in sensitive_patterns:
136
+ if f'{pattern} = "' in content or f"{pattern} = '" in content:
137
+ score += penalty
138
+ if message not in issues:
139
+ issues.append(message)
140
+ except Exception:
141
+ pass
142
+
143
+ # 檢查依賴漏洞(簡化版)
144
+ package_lock = self.project_path / 'package-lock.json'
145
+ if package_lock.exists():
146
+ recommendations.append('建議定期執行 npm audit')
147
+
148
+ return HealthScore(
149
+ category='安全性',
150
+ score=max(0, score),
151
+ issues=issues,
152
+ recommendations=recommendations
153
+ )
154
+
155
+ def _check_quality(self) -> HealthScore:
156
+ """程式碼品質檢查"""
157
+ score = 100
158
+ issues = []
159
+ recommendations = []
160
+
161
+ # 檢查 ESLint/Prettier 設定
162
+ has_linter = any([
163
+ (self.project_path / '.eslintrc.js').exists(),
164
+ (self.project_path / '.eslintrc.json').exists(),
165
+ (self.project_path / 'eslint.config.js').exists(),
166
+ ])
167
+ if not has_linter:
168
+ score -= 10
169
+ recommendations.append('建議加入 ESLint 設定')
170
+
171
+ has_prettier = any([
172
+ (self.project_path / '.prettierrc').exists(),
173
+ (self.project_path / '.prettierrc.json').exists(),
174
+ ])
175
+ if not has_prettier:
176
+ score -= 5
177
+ recommendations.append('建議加入 Prettier 設定')
178
+
179
+ # 檢查 TypeScript
180
+ package_json = self.project_path / 'package.json'
181
+ if package_json.exists():
182
+ try:
183
+ pkg = json.loads(package_json.read_text())
184
+ deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
185
+ if 'typescript' not in deps:
186
+ score -= 5
187
+ recommendations.append('建議使用 TypeScript')
188
+ except Exception:
189
+ pass
190
+
191
+ # 檢查檔案行數
192
+ large_files = []
193
+ for ext in ['*.js', '*.ts', '*.py']:
194
+ for file_path in self.project_path.rglob(ext):
195
+ if 'node_modules' in str(file_path) or '.git' in str(file_path):
196
+ continue
197
+ try:
198
+ lines = len(file_path.read_text().splitlines())
199
+ if lines > 500:
200
+ large_files.append(f'{file_path.name} ({lines} 行)')
201
+ except Exception:
202
+ pass
203
+
204
+ if large_files:
205
+ score -= min(len(large_files) * 5, 20)
206
+ issues.append(f'{len(large_files)} 個檔案超過 500 行')
207
+
208
+ return HealthScore(
209
+ category='品質',
210
+ score=max(0, score),
211
+ issues=issues,
212
+ recommendations=recommendations
213
+ )
214
+
215
+ def _check_maintainability(self) -> HealthScore:
216
+ """維護性檢查"""
217
+ score = 100
218
+ issues = []
219
+ recommendations = []
220
+
221
+ # 檢查 README
222
+ readme = self.project_path / 'README.md'
223
+ if not readme.exists():
224
+ score -= 15
225
+ issues.append('缺少 README.md')
226
+ else:
227
+ content = readme.read_text()
228
+ if len(content) < 200:
229
+ score -= 5
230
+ recommendations.append('README 內容過短,建議補充')
231
+
232
+ # 檢查 CLAUDE.md
233
+ claude_md = self.project_path / 'CLAUDE.md'
234
+ if not claude_md.exists():
235
+ score -= 10
236
+ recommendations.append('建議加入 CLAUDE.md (dash docs claude)')
237
+
238
+ # 檢查 package.json 完整度
239
+ package_json = self.project_path / 'package.json'
240
+ if package_json.exists():
241
+ try:
242
+ pkg = json.loads(package_json.read_text())
243
+ if not pkg.get('description'):
244
+ score -= 5
245
+ recommendations.append('package.json 缺少 description')
246
+ if not pkg.get('scripts'):
247
+ score -= 5
248
+ issues.append('package.json 缺少 scripts')
249
+ except Exception:
250
+ pass
251
+
252
+ # 檢查測試設定
253
+ has_tests = any([
254
+ (self.project_path / 'tests').exists(),
255
+ (self.project_path / '__tests__').exists(),
256
+ (self.project_path / 'spec').exists(),
257
+ any(self.project_path.rglob('*.test.ts')),
258
+ any(self.project_path.rglob('*.spec.ts')),
259
+ ])
260
+ if not has_tests:
261
+ score -= 15
262
+ recommendations.append('建議加入測試')
263
+
264
+ # 檢查 Git hooks
265
+ husky = self.project_path / '.husky'
266
+ if not husky.exists():
267
+ score -= 5
268
+ recommendations.append('建議使用 Git hooks (dash hooks install)')
269
+
270
+ return HealthScore(
271
+ category='維護性',
272
+ score=max(0, score),
273
+ issues=issues,
274
+ recommendations=recommendations
275
+ )
276
+
277
+ def _check_performance(self) -> HealthScore:
278
+ """效能檢查"""
279
+ score = 100
280
+ issues = []
281
+ recommendations = []
282
+
283
+ # 檢查 node_modules 大小(簡化版)
284
+ node_modules = self.project_path / 'node_modules'
285
+ if node_modules.exists():
286
+ # 計算依賴數量
287
+ package_json = self.project_path / 'package.json'
288
+ if package_json.exists():
289
+ try:
290
+ pkg = json.loads(package_json.read_text())
291
+ deps_count = len(pkg.get('dependencies', {}))
292
+ if deps_count > 50:
293
+ score -= 10
294
+ issues.append(f'依賴數量過多 ({deps_count} 個)')
295
+ recommendations.append('審視並移除不必要的依賴')
296
+ except Exception:
297
+ pass
298
+
299
+ # 檢查是否有未使用的依賴(簡化版)
300
+ package_json = self.project_path / 'package.json'
301
+ if package_json.exists():
302
+ recommendations.append('建議定期執行 depcheck 檢查未使用依賴')
303
+
304
+ # 檢查圖片優化
305
+ large_images = []
306
+ for ext in ['*.png', '*.jpg', '*.jpeg']:
307
+ for file_path in self.project_path.rglob(ext):
308
+ if 'node_modules' in str(file_path):
309
+ continue
310
+ try:
311
+ size_kb = file_path.stat().st_size / 1024
312
+ if size_kb > 500:
313
+ large_images.append(f'{file_path.name} ({size_kb:.0f}KB)')
314
+ except Exception:
315
+ pass
316
+
317
+ if large_images:
318
+ score -= min(len(large_images) * 5, 15)
319
+ issues.append(f'{len(large_images)} 個圖片超過 500KB')
320
+ recommendations.append('建議壓縮大型圖片')
321
+
322
+ return HealthScore(
323
+ category='效能',
324
+ score=max(0, score),
325
+ issues=issues,
326
+ recommendations=recommendations
327
+ )
328
+
329
+ def _check_spec(self) -> HealthScore:
330
+ """OpenSpec 規格健康檢查"""
331
+ import time
332
+
333
+ score = 100
334
+ issues = []
335
+ recommendations = []
336
+
337
+ openspec_dir = self.project_path / 'openspec'
338
+ specs_dir = openspec_dir / 'specs'
339
+ changes_dir = openspec_dir / 'changes'
340
+
341
+ # 檢查目錄結構
342
+ if not specs_dir.exists():
343
+ score -= 10
344
+ recommendations.append('建議建立 openspec/specs/ 目錄')
345
+
346
+ if not changes_dir.exists():
347
+ score -= 10
348
+ recommendations.append('建議建立 openspec/changes/ 目錄')
349
+
350
+ # 統計規格數量
351
+ specs_count = 0
352
+ if specs_dir.exists():
353
+ specs_count = len(list(specs_dir.glob('*.md')))
354
+
355
+ if specs_count == 0:
356
+ score -= 15
357
+ recommendations.append('尚未建立任何功能規格')
358
+
359
+ # 統計活動變更
360
+ changes_count = 0
361
+ stale_changes = []
362
+ if changes_dir.exists():
363
+ now = time.time()
364
+ seven_days = 7 * 24 * 60 * 60
365
+
366
+ for change_file in changes_dir.glob('*.md'):
367
+ changes_count += 1
368
+ mtime = change_file.stat().st_mtime
369
+ if now - mtime > seven_days:
370
+ stale_changes.append(change_file.stem)
371
+
372
+ # 過期變更扣分
373
+ if stale_changes:
374
+ penalty = min(len(stale_changes) * 10, 30)
375
+ score -= penalty
376
+ issues.append(f'{len(stale_changes)} 個變更超過 7 天未處理')
377
+ for change in stale_changes[:3]:
378
+ recommendations.append(f'處理過期變更: {change}')
379
+
380
+ # 檢查規格格式 (簡化版)
381
+ invalid_specs = 0
382
+ if specs_dir.exists():
383
+ for spec_file in specs_dir.glob('*.md'):
384
+ try:
385
+ content = spec_file.read_text(encoding='utf-8')
386
+ if not content.startswith('---'):
387
+ invalid_specs += 1
388
+ except Exception:
389
+ invalid_specs += 1
390
+
391
+ if invalid_specs > 0:
392
+ score -= invalid_specs * 5
393
+ issues.append(f'{invalid_specs} 個規格檔案格式不正確')
394
+
395
+ return HealthScore(
396
+ category='規格',
397
+ score=max(0, score),
398
+ issues=issues,
399
+ recommendations=recommendations
400
+ )
401
+
402
+
403
+ def render_health_report(project_name: str, scores: Dict[str, HealthScore]):
404
+ """渲染健康報告"""
405
+
406
+ # 計算總分
407
+ total_score = sum(s.score for s in scores.values()) // len(scores)
408
+
409
+ # 標題
410
+ grade_colors = {'A': 'green', 'B': 'cyan', 'C': 'yellow', 'D': 'orange1', 'F': 'red'}
411
+ grade = 'A' if total_score >= 90 else 'B' if total_score >= 80 else 'C' if total_score >= 70 else 'D' if total_score >= 60 else 'F'
412
+
413
+ title = Text()
414
+ title.append(f"\n {project_name} ", style="bold white")
415
+ title.append("健康報告\n", style="dim")
416
+
417
+ console.print(Panel(title, border_style="cyan"))
418
+
419
+ # 總分顯示
420
+ score_display = Text()
421
+ score_display.append(" 總分: ", style="dim")
422
+ score_display.append(f"{total_score}", style=f"bold {grade_colors[grade]}")
423
+ score_display.append(f"/100 ", style="dim")
424
+ score_display.append(f"[{grade}]", style=f"bold {grade_colors[grade]}")
425
+ console.print(score_display)
426
+ console.print()
427
+
428
+ # 各項評分進度條
429
+ for key, score in scores.items():
430
+ bar_width = 30
431
+ filled = int((score.score / 100) * bar_width)
432
+
433
+ bar = Text()
434
+ bar.append(f" {score.category: <6} ", style="white")
435
+ bar.append("█" * filled, style=score.color)
436
+ bar.append("░" * (bar_width - filled), style="dim")
437
+ bar.append(f" {score.score:3d}%", style=score.color)
438
+
439
+ console.print(bar)
440
+
441
+ console.print()
442
+
443
+ # 問題和建議
444
+ all_issues = []
445
+ all_recommendations = []
446
+
447
+ for score in scores.values():
448
+ all_issues.extend(score.issues)
449
+ all_recommendations.extend(score.recommendations)
450
+
451
+ if all_issues:
452
+ console.print(" [red]問題[/red]")
453
+ for issue in all_issues[:5]:
454
+ console.print(f" [red]•[/red] {issue}")
455
+ console.print()
456
+
457
+ if all_recommendations:
458
+ console.print(" [yellow]建議[/yellow]")
459
+ for rec in all_recommendations[:5]:
460
+ console.print(f" [yellow]•[/yellow] {rec}")
461
+
462
+ console.print()
463
+
464
+ return {
465
+ 'project': project_name,
466
+ 'total_score': total_score,
467
+ 'grade': grade,
468
+ 'scores': {k: {'score': v.score, 'issues': v.issues} for k, v in scores.items()}
469
+ }
470
+
471
+
472
+ def run_health_check(project_path: str) -> dict:
473
+ """執行健康檢查並輸出報告"""
474
+ checker = HealthChecker(project_path)
475
+ scores = checker.check_all()
476
+ return render_health_report(checker.project_name, scores)