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/spec.py ADDED
@@ -0,0 +1,230 @@
1
+ """
2
+ OpenSpec CLI 包裝模組
3
+
4
+ Spec-Driven Development (SDD) 工作流程整合。
5
+ 使用 @fission-ai/openspec npm 套件。
6
+
7
+ 安裝:npm install -g @fission-ai/openspec@latest
8
+ """
9
+
10
+ import subprocess
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Optional
14
+ from dataclasses import dataclass, field
15
+
16
+
17
+ @dataclass
18
+ class SpecResult:
19
+ """OpenSpec 操作結果"""
20
+ success: bool
21
+ message: str = ""
22
+ data: dict = field(default_factory=dict)
23
+ error: str = ""
24
+
25
+
26
+ class OpenSpecWrapper:
27
+ """OpenSpec CLI 包裝器"""
28
+
29
+ @staticmethod
30
+ def _check_installed() -> bool:
31
+ """檢查 openspec 是否安裝"""
32
+ try:
33
+ result = subprocess.run(
34
+ ['which', 'openspec'],
35
+ capture_output=True,
36
+ text=True
37
+ )
38
+ return result.returncode == 0
39
+ except Exception:
40
+ return False
41
+
42
+ @staticmethod
43
+ def _run_command(args: list, cwd: Optional[str] = None) -> SpecResult:
44
+ """執行 openspec 指令"""
45
+ if not OpenSpecWrapper._check_installed():
46
+ return SpecResult(
47
+ success=False,
48
+ error="openspec 未安裝。請執行: npm install -g @fission-ai/openspec@latest"
49
+ )
50
+
51
+ try:
52
+ cmd = ['openspec'] + args
53
+ result = subprocess.run(
54
+ cmd,
55
+ cwd=cwd,
56
+ capture_output=True,
57
+ text=True
58
+ )
59
+
60
+ if result.returncode == 0:
61
+ return SpecResult(
62
+ success=True,
63
+ message=result.stdout.strip(),
64
+ data={}
65
+ )
66
+ else:
67
+ return SpecResult(
68
+ success=False,
69
+ error=result.stderr.strip() or result.stdout.strip()
70
+ )
71
+ except Exception as e:
72
+ return SpecResult(
73
+ success=False,
74
+ error=str(e)
75
+ )
76
+
77
+ @staticmethod
78
+ def init(project_path: str) -> SpecResult:
79
+ """初始化 OpenSpec
80
+
81
+ Args:
82
+ project_path: 專案路徑
83
+
84
+ Returns:
85
+ SpecResult: 操作結果
86
+ """
87
+ result = OpenSpecWrapper._run_command(['init'], cwd=project_path)
88
+ if result.success:
89
+ result.message = "OpenSpec 初始化完成"
90
+ result.data = {
91
+ 'openspec_dir': str(Path(project_path) / 'openspec'),
92
+ 'specs_dir': str(Path(project_path) / 'openspec' / 'specs'),
93
+ 'changes_dir': str(Path(project_path) / 'openspec' / 'changes')
94
+ }
95
+ return result
96
+
97
+ @staticmethod
98
+ def list_changes(project_path: str) -> SpecResult:
99
+ """列出活動變更
100
+
101
+ Args:
102
+ project_path: 專案路徑
103
+
104
+ Returns:
105
+ SpecResult: 包含變更清單
106
+ """
107
+ result = OpenSpecWrapper._run_command(['list'], cwd=project_path)
108
+ if result.success:
109
+ # 解析輸出為結構化資料
110
+ changes = []
111
+ for line in result.message.split('\n'):
112
+ line = line.strip()
113
+ if line and not line.startswith('─') and not line.startswith('Active'):
114
+ changes.append(line)
115
+ result.data = {'changes': changes, 'count': len(changes)}
116
+ return result
117
+
118
+ @staticmethod
119
+ def view(project_path: str) -> None:
120
+ """開啟互動式儀表板 (需要 TTY)
121
+
122
+ Args:
123
+ project_path: 專案路徑
124
+ """
125
+ subprocess.run(['openspec', 'view'], cwd=project_path)
126
+
127
+ @staticmethod
128
+ def show(project_path: str, change_name: str) -> SpecResult:
129
+ """顯示變更詳情
130
+
131
+ Args:
132
+ project_path: 專案路徑
133
+ change_name: 變更名稱
134
+
135
+ Returns:
136
+ SpecResult: 包含變更詳情
137
+ """
138
+ result = OpenSpecWrapper._run_command(['show', change_name], cwd=project_path)
139
+ if result.success:
140
+ result.data = {'change_name': change_name, 'content': result.message}
141
+ return result
142
+
143
+ @staticmethod
144
+ def validate(project_path: str, change_name: str) -> SpecResult:
145
+ """驗證規格格式
146
+
147
+ Args:
148
+ project_path: 專案路徑
149
+ change_name: 變更名稱
150
+
151
+ Returns:
152
+ SpecResult: 驗證結果
153
+ """
154
+ result = OpenSpecWrapper._run_command(['validate', change_name], cwd=project_path)
155
+ if result.success:
156
+ result.data = {'change_name': change_name, 'valid': True}
157
+ else:
158
+ result.data = {'change_name': change_name, 'valid': False}
159
+ return result
160
+
161
+ @staticmethod
162
+ def archive(project_path: str, change_name: str, yes: bool = False) -> SpecResult:
163
+ """歸檔完成的變更
164
+
165
+ Args:
166
+ project_path: 專案路徑
167
+ change_name: 變更名稱
168
+ yes: 自動確認
169
+
170
+ Returns:
171
+ SpecResult: 歸檔結果
172
+ """
173
+ args = ['archive', change_name]
174
+ if yes:
175
+ args.append('-y')
176
+ result = OpenSpecWrapper._run_command(args, cwd=project_path)
177
+ if result.success:
178
+ result.message = f"變更 '{change_name}' 已歸檔"
179
+ result.data = {'change_name': change_name, 'archived': True}
180
+ return result
181
+
182
+
183
+ def get_spec_status(project_path: str) -> dict:
184
+ """取得專案的 OpenSpec 狀態摘要
185
+
186
+ Args:
187
+ project_path: 專案路徑
188
+
189
+ Returns:
190
+ dict: 狀態摘要
191
+ """
192
+ path = Path(project_path)
193
+ openspec_dir = path / 'openspec'
194
+
195
+ status = {
196
+ 'initialized': openspec_dir.exists(),
197
+ 'specs_count': 0,
198
+ 'changes_count': 0,
199
+ 'archive_count': 0,
200
+ 'stale_changes': [], # 超過 7 天未處理的提案
201
+ }
202
+
203
+ if not openspec_dir.exists():
204
+ return status
205
+
206
+ # 統計 specs
207
+ specs_dir = openspec_dir / 'specs'
208
+ if specs_dir.exists():
209
+ status['specs_count'] = len(list(specs_dir.glob('*.md')))
210
+
211
+ # 統計 changes
212
+ changes_dir = openspec_dir / 'changes'
213
+ if changes_dir.exists():
214
+ import time
215
+ now = time.time()
216
+ seven_days = 7 * 24 * 60 * 60
217
+
218
+ for change_file in changes_dir.glob('*.md'):
219
+ status['changes_count'] += 1
220
+ # 檢查是否過期
221
+ mtime = change_file.stat().st_mtime
222
+ if now - mtime > seven_days:
223
+ status['stale_changes'].append(change_file.stem)
224
+
225
+ # 統計 archive
226
+ archive_dir = openspec_dir / 'archive'
227
+ if archive_dir.exists():
228
+ status['archive_count'] = len(list(archive_dir.glob('*.md')))
229
+
230
+ return status
dash_devtools/stats.py ADDED
@@ -0,0 +1,355 @@
1
+ """
2
+ 程式碼統計儀表板
3
+
4
+ 視覺化專案統計資訊:
5
+ - 語言分佈
6
+ - 檔案數量與行數
7
+ - 複雜度指標
8
+ - 技術債務追蹤
9
+ """
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from dataclasses import dataclass, field
14
+ from typing import Dict, List, Tuple
15
+ from collections import defaultdict
16
+ from rich.console import Console
17
+ from rich.panel import Panel
18
+ from rich.table import Table
19
+ from rich.text import Text
20
+
21
+ console = Console()
22
+
23
+
24
+ # 語言設定
25
+ LANGUAGE_CONFIG = {
26
+ '.py': {'name': 'Python', 'color': 'yellow', 'icon': ''},
27
+ '.js': {'name': 'JavaScript', 'color': 'yellow', 'icon': ''},
28
+ '.ts': {'name': 'TypeScript', 'color': 'blue', 'icon': ''},
29
+ '.tsx': {'name': 'TSX', 'color': 'cyan', 'icon': ''},
30
+ '.jsx': {'name': 'JSX', 'color': 'cyan', 'icon': ''},
31
+ '.html': {'name': 'HTML', 'color': 'orange1', 'icon': ''},
32
+ '.css': {'name': 'CSS', 'color': 'magenta', 'icon': ''},
33
+ '.scss': {'name': 'SCSS', 'color': 'magenta', 'icon': ''},
34
+ '.json': {'name': 'JSON', 'color': 'green', 'icon': ''},
35
+ '.md': {'name': 'Markdown', 'color': 'white', 'icon': ''},
36
+ '.vue': {'name': 'Vue', 'color': 'green', 'icon': ''},
37
+ '.svelte': {'name': 'Svelte', 'color': 'orange1', 'icon': ''},
38
+ }
39
+
40
+ # 忽略的目錄
41
+ IGNORE_DIRS = {
42
+ 'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
43
+ 'venv', '.venv', '.angular', '.cache', 'coverage', '.nuxt',
44
+ '.output', '.turbo', 'vendor'
45
+ }
46
+
47
+
48
+ @dataclass
49
+ class FileStats:
50
+ """單一檔案統計"""
51
+ path: str
52
+ extension: str
53
+ lines: int
54
+ code_lines: int
55
+ comment_lines: int
56
+ blank_lines: int
57
+ size_bytes: int
58
+
59
+
60
+ @dataclass
61
+ class LanguageStats:
62
+ """語言統計"""
63
+ name: str
64
+ files: int = 0
65
+ lines: int = 0
66
+ code_lines: int = 0
67
+ size_bytes: int = 0
68
+
69
+
70
+ @dataclass
71
+ class ProjectStats:
72
+ """專案統計總覽"""
73
+ name: str
74
+ total_files: int = 0
75
+ total_lines: int = 0
76
+ total_code_lines: int = 0
77
+ total_size_bytes: int = 0
78
+ languages: Dict[str, LanguageStats] = field(default_factory=dict)
79
+ largest_files: List[Tuple[str, int]] = field(default_factory=list)
80
+ complexity_issues: List[str] = field(default_factory=list)
81
+
82
+
83
+ class StatsCollector:
84
+ """程式碼統計收集器"""
85
+
86
+ def __init__(self, project_path: str):
87
+ self.project_path = Path(project_path)
88
+ self.project_name = self.project_path.name
89
+
90
+ def collect(self) -> ProjectStats:
91
+ """收集專案統計"""
92
+ stats = ProjectStats(name=self.project_name)
93
+ file_stats_list = []
94
+
95
+ for file_path in self.project_path.rglob('*'):
96
+ # 跳過目錄
97
+ if file_path.is_dir():
98
+ continue
99
+
100
+ # 跳過忽略的目錄
101
+ if any(ignore in file_path.parts for ignore in IGNORE_DIRS):
102
+ continue
103
+
104
+ # 只處理已知的語言
105
+ ext = file_path.suffix.lower()
106
+ if ext not in LANGUAGE_CONFIG:
107
+ continue
108
+
109
+ try:
110
+ file_stat = self._analyze_file(file_path)
111
+ file_stats_list.append(file_stat)
112
+
113
+ # 更新語言統計
114
+ lang_name = LANGUAGE_CONFIG[ext]['name']
115
+ if lang_name not in stats.languages:
116
+ stats.languages[lang_name] = LanguageStats(name=lang_name)
117
+
118
+ lang_stats = stats.languages[lang_name]
119
+ lang_stats.files += 1
120
+ lang_stats.lines += file_stat.lines
121
+ lang_stats.code_lines += file_stat.code_lines
122
+ lang_stats.size_bytes += file_stat.size_bytes
123
+
124
+ # 更新總計
125
+ stats.total_files += 1
126
+ stats.total_lines += file_stat.lines
127
+ stats.total_code_lines += file_stat.code_lines
128
+ stats.total_size_bytes += file_stat.size_bytes
129
+
130
+ # 檢查複雜度問題
131
+ if file_stat.lines > 500:
132
+ stats.complexity_issues.append(
133
+ f'{file_path.name}: {file_stat.lines} 行 (建議拆分)'
134
+ )
135
+
136
+ except Exception:
137
+ pass
138
+
139
+ # 找出最大的檔案
140
+ file_stats_list.sort(key=lambda x: x.lines, reverse=True)
141
+ stats.largest_files = [
142
+ (fs.path, fs.lines) for fs in file_stats_list[:10]
143
+ ]
144
+
145
+ return stats
146
+
147
+ def _analyze_file(self, file_path: Path) -> FileStats:
148
+ """分析單一檔案"""
149
+ content = file_path.read_text(encoding='utf-8', errors='ignore')
150
+ lines = content.splitlines()
151
+
152
+ code_lines = 0
153
+ comment_lines = 0
154
+ blank_lines = 0
155
+
156
+ ext = file_path.suffix.lower()
157
+ in_block_comment = False
158
+
159
+ for line in lines:
160
+ stripped = line.strip()
161
+
162
+ if not stripped:
163
+ blank_lines += 1
164
+ continue
165
+
166
+ # Python 風格註解
167
+ if ext == '.py':
168
+ if stripped.startswith('#'):
169
+ comment_lines += 1
170
+ elif stripped.startswith('"""') or stripped.startswith("'''"):
171
+ in_block_comment = not in_block_comment
172
+ comment_lines += 1
173
+ elif in_block_comment:
174
+ comment_lines += 1
175
+ else:
176
+ code_lines += 1
177
+
178
+ # JS/TS 風格註解
179
+ elif ext in ['.js', '.ts', '.jsx', '.tsx', '.css', '.scss']:
180
+ if stripped.startswith('//'):
181
+ comment_lines += 1
182
+ elif stripped.startswith('/*'):
183
+ in_block_comment = True
184
+ comment_lines += 1
185
+ elif stripped.endswith('*/'):
186
+ in_block_comment = False
187
+ comment_lines += 1
188
+ elif in_block_comment:
189
+ comment_lines += 1
190
+ else:
191
+ code_lines += 1
192
+
193
+ # HTML 風格
194
+ elif ext == '.html':
195
+ if '<!--' in stripped:
196
+ comment_lines += 1
197
+ else:
198
+ code_lines += 1
199
+
200
+ else:
201
+ code_lines += 1
202
+
203
+ return FileStats(
204
+ path=str(file_path.relative_to(self.project_path)),
205
+ extension=ext,
206
+ lines=len(lines),
207
+ code_lines=code_lines,
208
+ comment_lines=comment_lines,
209
+ blank_lines=blank_lines,
210
+ size_bytes=file_path.stat().st_size
211
+ )
212
+
213
+
214
+ def render_stats_report(stats: ProjectStats):
215
+ """渲染統計報告"""
216
+
217
+ # 標題
218
+ title = Text()
219
+ title.append(f"\n {stats.name} ", style="bold white")
220
+ title.append("程式碼統計\n", style="dim")
221
+ console.print(Panel(title, border_style="cyan"))
222
+
223
+ # 總覽
224
+ size_kb = stats.total_size_bytes / 1024
225
+ size_display = f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb/1024:.1f} MB"
226
+
227
+ console.print(f" [dim]檔案數:[/dim] [cyan]{stats.total_files:,}[/cyan]")
228
+ console.print(f" [dim]總行數:[/dim] [cyan]{stats.total_lines:,}[/cyan]")
229
+ console.print(f" [dim]程式碼行數:[/dim] [cyan]{stats.total_code_lines:,}[/cyan]")
230
+ console.print(f" [dim]大小:[/dim] [cyan]{size_display}[/cyan]")
231
+ console.print()
232
+
233
+ # 語言分佈 - 水平條狀圖
234
+ console.print(" [bold]語言分佈[/bold]")
235
+ console.print()
236
+
237
+ if stats.languages:
238
+ max_lines = max(lang.lines for lang in stats.languages.values())
239
+ bar_width = 35
240
+
241
+ # 按行數排序
242
+ sorted_langs = sorted(
243
+ stats.languages.values(),
244
+ key=lambda x: x.lines,
245
+ reverse=True
246
+ )
247
+
248
+ for lang in sorted_langs[:8]: # 最多顯示 8 種語言
249
+ # 找出對應的顏色
250
+ color = 'white'
251
+ for ext, config in LANGUAGE_CONFIG.items():
252
+ if config['name'] == lang.name:
253
+ color = config['color']
254
+ break
255
+
256
+ filled = int((lang.lines / max_lines) * bar_width) if max_lines > 0 else 0
257
+ percentage = (lang.lines / stats.total_lines * 100) if stats.total_lines > 0 else 0
258
+
259
+ bar = Text()
260
+ bar.append(f" {lang.name:12} ", style="white")
261
+ bar.append("█" * filled, style=color)
262
+ bar.append("░" * (bar_width - filled), style="dim")
263
+ bar.append(f" {percentage:5.1f}%", style=color)
264
+ bar.append(f" ({lang.lines:,} 行)", style="dim")
265
+
266
+ console.print(bar)
267
+
268
+ console.print()
269
+
270
+ # 最大的檔案
271
+ if stats.largest_files:
272
+ console.print(" [bold]最大檔案 Top 5[/bold]")
273
+ console.print()
274
+
275
+ for i, (path, lines) in enumerate(stats.largest_files[:5], 1):
276
+ color = 'red' if lines > 500 else 'yellow' if lines > 300 else 'green'
277
+ console.print(f" {i}. [{color}]{lines:4} 行[/{color}] {path}")
278
+
279
+ console.print()
280
+
281
+ # 複雜度警告
282
+ if stats.complexity_issues:
283
+ console.print(" [bold yellow]複雜度警告[/bold yellow]")
284
+ console.print()
285
+ for issue in stats.complexity_issues[:5]:
286
+ console.print(f" [yellow]![/yellow] {issue}")
287
+ console.print()
288
+
289
+ return {
290
+ 'project': stats.name,
291
+ 'total_files': stats.total_files,
292
+ 'total_lines': stats.total_lines,
293
+ 'languages': {k: {'files': v.files, 'lines': v.lines} for k, v in stats.languages.items()},
294
+ 'complexity_issues': len(stats.complexity_issues)
295
+ }
296
+
297
+
298
+ def run_stats(project_path: str) -> dict:
299
+ """執行程式碼統計"""
300
+ collector = StatsCollector(project_path)
301
+ stats = collector.collect()
302
+ return render_stats_report(stats)
303
+
304
+
305
+ def run_stats_all(projects: List[str]) -> dict:
306
+ """多專案統計比較"""
307
+ results = []
308
+
309
+ # 標題
310
+ console.print(Panel(
311
+ Text("\n 專案統計比較\n", style="bold white"),
312
+ border_style="cyan"
313
+ ))
314
+
315
+ # 建立比較表格
316
+ table = Table(show_header=True, header_style="bold cyan")
317
+ table.add_column("專案", style="white")
318
+ table.add_column("檔案", justify="right")
319
+ table.add_column("行數", justify="right")
320
+ table.add_column("主要語言", justify="center")
321
+ table.add_column("大小", justify="right")
322
+
323
+ for project in projects:
324
+ try:
325
+ collector = StatsCollector(project)
326
+ stats = collector.collect()
327
+
328
+ # 找出主要語言
329
+ main_lang = '-'
330
+ if stats.languages:
331
+ main_lang = max(stats.languages.values(), key=lambda x: x.lines).name
332
+
333
+ size_kb = stats.total_size_bytes / 1024
334
+ size_display = f"{size_kb:.0f} KB" if size_kb < 1024 else f"{size_kb/1024:.1f} MB"
335
+
336
+ table.add_row(
337
+ stats.name,
338
+ f"{stats.total_files:,}",
339
+ f"{stats.total_lines:,}",
340
+ main_lang,
341
+ size_display
342
+ )
343
+
344
+ results.append({
345
+ 'project': stats.name,
346
+ 'files': stats.total_files,
347
+ 'lines': stats.total_lines
348
+ })
349
+
350
+ except Exception as e:
351
+ table.add_row(project.split('/')[-1], "-", "-", "-", f"[red]錯誤[/red]")
352
+
353
+ console.print(table)
354
+
355
+ return {'projects': results}