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,270 @@
1
+ """
2
+ 安全性驗證器(通用)
3
+
4
+ 檢查項目:
5
+ 1. API Key / Token 外洩
6
+ 2. 密碼硬編碼
7
+ 3. .env 檔案提交
8
+ 4. 敏感資料暴露
9
+ """
10
+
11
+ import re
12
+ from fnmatch import fnmatch
13
+ from pathlib import Path
14
+
15
+
16
+ class SecurityValidator:
17
+ """安全性驗證器"""
18
+
19
+ name = 'security'
20
+
21
+ # 敏感資料正則表達式
22
+ SENSITIVE_PATTERNS = [
23
+ (r'(?i)(api[_-]?key|apikey)\s*[=:]\s*["\']?[a-zA-Z0-9_-]{20,}', 'API Key'),
24
+ (r'(?i)(secret|token)\s*[=:]\s*["\']?[a-zA-Z0-9_-]{20,}', 'Secret/Token'),
25
+ (r'(?i)password\s*[=:]\s*["\'][^"\']+["\']', '密碼'),
26
+ (r'sk-[a-zA-Z0-9]{48}', 'OpenAI API Key'),
27
+ (r'sk_live_[a-zA-Z0-9]{24,}', 'Stripe Live Key'),
28
+ (r'ghp_[a-zA-Z0-9]{36}', 'GitHub Token'),
29
+ # Clerk 只檢查 secret key (sk_),publishable key (pk_) 是公開的
30
+ (r'CLERK_SECRET_KEY\s*=\s*["\']?sk_[a-zA-Z0-9_-]{20,}', 'Clerk Secret Key'),
31
+ # Neon Database API Key (napi_開頭,64字元)
32
+ (r'napi_[a-zA-Z0-9]{60,}', 'Neon API Key'),
33
+ # PostgreSQL 連線字串 (含密碼)
34
+ (r'postgres(?:ql)?://[^:<]+:[^@<]+@[^\s"\']+', 'PostgreSQL 連線字串'),
35
+ # Neon PostgreSQL 專用格式
36
+ (r'npg_[a-zA-Z0-9]{10,}', 'Neon PostgreSQL 密碼'),
37
+ ]
38
+
39
+ # 敏感檔案
40
+ SENSITIVE_FILES = [
41
+ '.env',
42
+ '.env.local',
43
+ '.env.production',
44
+ 'credentials.json',
45
+ 'service-account.json',
46
+ 'private.key',
47
+ '*.pem',
48
+ ]
49
+
50
+ # 忽略目錄
51
+ IGNORE_DIRS = [
52
+ 'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
53
+ '.angular', 'venv', '.venv', '.cache', 'coverage'
54
+ ]
55
+
56
+ def __init__(self, project_path):
57
+ self.project_path = Path(project_path)
58
+ self.project_name = self.project_path.name
59
+ self.scanignore = self._parse_scanignore()
60
+ self.result = {
61
+ 'name': self.name,
62
+ 'passed': True,
63
+ 'errors': [],
64
+ 'warnings': [],
65
+ 'checks': {}
66
+ }
67
+
68
+ def run(self):
69
+ """執行所有驗證"""
70
+ if not self.project_path.exists():
71
+ self.result['passed'] = False
72
+ self.result['errors'].append(f'專案路徑不存在: {self.project_path}')
73
+ return self.result
74
+
75
+ self.check_sensitive_files()
76
+ self.check_hardcoded_secrets()
77
+ self.check_gitignore()
78
+
79
+ return self.result
80
+
81
+ def check_sensitive_files(self):
82
+ """檢查敏感檔案是否被追蹤"""
83
+ issues = []
84
+
85
+ # 找出所有巢狀的 git repo (要跳過)
86
+ nested_repos = self._get_nested_repos()
87
+
88
+ for pattern in self.SENSITIVE_FILES:
89
+ if '*' in pattern:
90
+ files = list(self.project_path.rglob(pattern))
91
+ else:
92
+ files = [self.project_path / pattern]
93
+
94
+ for f in files:
95
+ if f.exists() and f.is_file():
96
+ if self._should_skip(f, nested_repos):
97
+ continue
98
+ if not self._is_gitignored(f):
99
+ issues.append(str(f.relative_to(self.project_path)))
100
+
101
+ self.result['checks']['sensitive_files'] = {
102
+ 'count': len(issues),
103
+ 'files': issues
104
+ }
105
+
106
+ if issues:
107
+ self.result['passed'] = False
108
+ for f in issues:
109
+ self.result['errors'].append(f'敏感檔案未忽略: {f}')
110
+
111
+ def check_hardcoded_secrets(self):
112
+ """檢查硬編碼的敏感資料"""
113
+ issues = []
114
+
115
+ for file_path in self._get_source_files():
116
+ try:
117
+ content = file_path.read_text(encoding='utf-8')
118
+ rel_path = str(file_path.relative_to(self.project_path))
119
+
120
+ # 全排除檢查
121
+ if self._is_scanignored(rel_path, None):
122
+ continue
123
+
124
+ for pattern, desc in self.SENSITIVE_PATTERNS:
125
+ # 特定 pattern 排除檢查
126
+ if self._is_scanignored(rel_path, desc):
127
+ continue
128
+ matches = re.findall(pattern, content)
129
+ if matches:
130
+ issues.append({
131
+ 'file': rel_path,
132
+ 'type': desc,
133
+ 'count': len(matches)
134
+ })
135
+ except Exception:
136
+ pass
137
+
138
+ self.result['checks']['hardcoded_secrets'] = {
139
+ 'count': len(issues),
140
+ 'issues': issues
141
+ }
142
+
143
+ if issues:
144
+ self.result['passed'] = False
145
+ for issue in issues:
146
+ self.result['errors'].append(
147
+ f"發現 {issue['type']} 在 {issue['file']}"
148
+ )
149
+
150
+ def check_gitignore(self):
151
+ """檢查 .gitignore 設定"""
152
+ gitignore = self.project_path / '.gitignore'
153
+ required_patterns = ['.env', 'node_modules', '*.log']
154
+ missing = []
155
+
156
+ if gitignore.exists():
157
+ content = gitignore.read_text(encoding='utf-8')
158
+ for pattern in required_patterns:
159
+ if pattern not in content:
160
+ missing.append(pattern)
161
+ else:
162
+ missing = required_patterns
163
+
164
+ self.result['checks']['gitignore'] = {
165
+ 'exists': gitignore.exists(),
166
+ 'missing_patterns': missing
167
+ }
168
+
169
+ if missing:
170
+ for pattern in missing:
171
+ self.result['warnings'].append(f'.gitignore 缺少: {pattern}')
172
+
173
+ def _get_nested_repos(self):
174
+ """取得巢狀 git repo 路徑"""
175
+ nested_repos = []
176
+ for git_dir in self.project_path.rglob('.git'):
177
+ if git_dir.parent != self.project_path:
178
+ nested_repos.append(str(git_dir.parent))
179
+ return nested_repos
180
+
181
+ def _should_skip(self, file_path, nested_repos):
182
+ """檢查是否應該跳過該檔案"""
183
+ file_str = str(file_path)
184
+ # 跳過巢狀 git repo
185
+ if any(file_str.startswith(repo) for repo in nested_repos):
186
+ return True
187
+ # 跳過忽略目錄
188
+ if any(ignore in file_str for ignore in self.IGNORE_DIRS):
189
+ return True
190
+ return False
191
+
192
+ def _get_source_files(self):
193
+ """取得所有原始碼檔案"""
194
+ extensions = ['*.js', '*.ts', '*.jsx', '*.tsx', '*.py', '*.json', '*.yaml', '*.yml']
195
+ files = []
196
+ nested_repos = self._get_nested_repos()
197
+
198
+ for ext in extensions:
199
+ for f in self.project_path.rglob(ext):
200
+ if not self._should_skip(f, nested_repos):
201
+ files.append(f)
202
+
203
+ return files
204
+
205
+ def _parse_scanignore(self):
206
+ """解析 .scanignore 檔案"""
207
+ scanignore_file = self.project_path / '.scanignore'
208
+ result = {'paths': [], 'pattern_paths': []}
209
+
210
+ if not scanignore_file.exists():
211
+ return result
212
+
213
+ try:
214
+ for line in scanignore_file.read_text(encoding='utf-8').splitlines():
215
+ line = line.strip()
216
+ if not line or line.startswith('#'):
217
+ continue
218
+
219
+ pattern_match = re.match(r'^\[pattern:(.+?)\]\s+(.+)$', line)
220
+ if pattern_match:
221
+ pattern_name = pattern_match.group(1).strip()
222
+ path_glob = pattern_match.group(2).strip()
223
+ result['pattern_paths'].append((pattern_name, path_glob))
224
+ else:
225
+ result['paths'].append(line)
226
+ except Exception:
227
+ pass
228
+
229
+ return result
230
+
231
+ def _is_scanignored(self, rel_path, pattern_name):
232
+ """檢查檔案是否在 .scanignore 中"""
233
+ # 全排除路徑
234
+ for glob_pattern in self.scanignore['paths']:
235
+ if glob_pattern.endswith('/'):
236
+ if rel_path.startswith(glob_pattern) or rel_path.startswith(glob_pattern.rstrip('/')):
237
+ return True
238
+ if fnmatch(rel_path, glob_pattern):
239
+ return True
240
+ if rel_path.startswith(glob_pattern.rstrip('/') + '/'):
241
+ return True
242
+
243
+ # 特定 pattern 排除
244
+ if pattern_name:
245
+ for p_name, p_glob in self.scanignore['pattern_paths']:
246
+ if p_name == pattern_name:
247
+ if fnmatch(rel_path, p_glob):
248
+ return True
249
+ if rel_path == p_glob:
250
+ return True
251
+
252
+ return False
253
+
254
+ def _is_gitignored(self, file_path):
255
+ """檢查檔案是否在 .gitignore 中"""
256
+ gitignore = self.project_path / '.gitignore'
257
+ if not gitignore.exists():
258
+ return False
259
+
260
+ content = gitignore.read_text(encoding='utf-8')
261
+ rel_path = str(file_path.relative_to(self.project_path))
262
+
263
+ for line in content.splitlines():
264
+ line = line.strip()
265
+ if not line or line.startswith('#'):
266
+ continue
267
+ if line in rel_path or rel_path.startswith(line):
268
+ return True
269
+
270
+ return False
@@ -0,0 +1,273 @@
1
+ """
2
+ OpenSpec 規格驗證器
3
+
4
+ 檢查項目:
5
+ 1. openspec/ 目錄存在性
6
+ 2. 規格檔案格式正確 (YAML frontmatter)
7
+ 3. 無孤立的提案(超過 7 天未處理)
8
+ 4. specs/ 與 changes/ 一致性
9
+ """
10
+
11
+ import re
12
+ import time
13
+ from pathlib import Path
14
+
15
+
16
+ class SpecValidator:
17
+ """OpenSpec 規格驗證器"""
18
+
19
+ name = 'spec'
20
+
21
+ # 忽略目錄
22
+ IGNORE_DIRS = [
23
+ 'node_modules', '.git', 'dist', 'build', '__pycache__', 'venv'
24
+ ]
25
+
26
+ # 規格檔案必要欄位
27
+ REQUIRED_FRONTMATTER = ['title', 'status']
28
+
29
+ # 變更檔案必要欄位
30
+ REQUIRED_CHANGE_FIELDS = ['title', 'type']
31
+
32
+ def __init__(self, project_path):
33
+ self.project_path = Path(project_path)
34
+ self.project_name = self.project_path.name
35
+ self.openspec_dir = self.project_path / 'openspec'
36
+ self.result = {
37
+ 'name': self.name,
38
+ 'passed': True,
39
+ 'errors': [],
40
+ 'warnings': [],
41
+ 'checks': {}
42
+ }
43
+
44
+ def run(self):
45
+ """執行所有驗證"""
46
+ if not self.project_path.exists():
47
+ self.result['passed'] = False
48
+ self.result['errors'].append(f'專案路徑不存在: {self.project_path}')
49
+ return self.result
50
+
51
+ # 如果沒有 openspec 目錄,跳過驗證
52
+ if not self.openspec_dir.exists():
53
+ self.result['checks']['initialized'] = {'exists': False}
54
+ return self.result
55
+
56
+ self.check_directory_structure()
57
+ self.check_specs_format()
58
+ self.check_changes_format()
59
+ self.check_stale_changes()
60
+ self.check_consistency()
61
+
62
+ return self.result
63
+
64
+ def check_directory_structure(self):
65
+ """檢查目錄結構"""
66
+ specs_dir = self.openspec_dir / 'specs'
67
+ changes_dir = self.openspec_dir / 'changes'
68
+
69
+ structure = {
70
+ 'openspec_exists': self.openspec_dir.exists(),
71
+ 'specs_exists': specs_dir.exists(),
72
+ 'changes_exists': changes_dir.exists(),
73
+ }
74
+
75
+ self.result['checks']['directory_structure'] = structure
76
+
77
+ # 目錄存在但子目錄缺失是警告
78
+ if self.openspec_dir.exists():
79
+ if not specs_dir.exists():
80
+ self.result['warnings'].append('openspec/specs/ 目錄不存在')
81
+ if not changes_dir.exists():
82
+ self.result['warnings'].append('openspec/changes/ 目錄不存在')
83
+
84
+ def check_specs_format(self):
85
+ """檢查規格檔案格式"""
86
+ specs_dir = self.openspec_dir / 'specs'
87
+ if not specs_dir.exists():
88
+ return
89
+
90
+ issues = []
91
+ valid_count = 0
92
+
93
+ for spec_file in specs_dir.glob('*.md'):
94
+ try:
95
+ content = spec_file.read_text(encoding='utf-8')
96
+ frontmatter = self._parse_frontmatter(content)
97
+
98
+ if frontmatter is None:
99
+ issues.append({
100
+ 'file': spec_file.name,
101
+ 'error': '缺少 YAML frontmatter'
102
+ })
103
+ continue
104
+
105
+ # 檢查必要欄位
106
+ missing = [f for f in self.REQUIRED_FRONTMATTER if f not in frontmatter]
107
+ if missing:
108
+ issues.append({
109
+ 'file': spec_file.name,
110
+ 'error': f'缺少欄位: {", ".join(missing)}'
111
+ })
112
+ else:
113
+ valid_count += 1
114
+
115
+ except Exception as e:
116
+ issues.append({
117
+ 'file': spec_file.name,
118
+ 'error': str(e)
119
+ })
120
+
121
+ self.result['checks']['specs_format'] = {
122
+ 'valid_count': valid_count,
123
+ 'issues': issues
124
+ }
125
+
126
+ for issue in issues:
127
+ self.result['warnings'].append(
128
+ f"規格格式問題: {issue['file']} - {issue['error']}"
129
+ )
130
+
131
+ def check_changes_format(self):
132
+ """檢查變更檔案格式"""
133
+ changes_dir = self.openspec_dir / 'changes'
134
+ if not changes_dir.exists():
135
+ return
136
+
137
+ issues = []
138
+ valid_count = 0
139
+
140
+ for change_file in changes_dir.glob('*.md'):
141
+ try:
142
+ content = change_file.read_text(encoding='utf-8')
143
+ frontmatter = self._parse_frontmatter(content)
144
+
145
+ if frontmatter is None:
146
+ issues.append({
147
+ 'file': change_file.name,
148
+ 'error': '缺少 YAML frontmatter'
149
+ })
150
+ continue
151
+
152
+ # 檢查必要欄位
153
+ missing = [f for f in self.REQUIRED_CHANGE_FIELDS if f not in frontmatter]
154
+ if missing:
155
+ issues.append({
156
+ 'file': change_file.name,
157
+ 'error': f'缺少欄位: {", ".join(missing)}'
158
+ })
159
+ else:
160
+ valid_count += 1
161
+
162
+ except Exception as e:
163
+ issues.append({
164
+ 'file': change_file.name,
165
+ 'error': str(e)
166
+ })
167
+
168
+ self.result['checks']['changes_format'] = {
169
+ 'valid_count': valid_count,
170
+ 'issues': issues
171
+ }
172
+
173
+ for issue in issues:
174
+ self.result['warnings'].append(
175
+ f"變更格式問題: {issue['file']} - {issue['error']}"
176
+ )
177
+
178
+ def check_stale_changes(self):
179
+ """檢查過期的變更提案(超過 7 天未處理)"""
180
+ changes_dir = self.openspec_dir / 'changes'
181
+ if not changes_dir.exists():
182
+ return
183
+
184
+ stale = []
185
+ now = time.time()
186
+ seven_days = 7 * 24 * 60 * 60
187
+
188
+ for change_file in changes_dir.glob('*.md'):
189
+ mtime = change_file.stat().st_mtime
190
+ age_days = (now - mtime) / (24 * 60 * 60)
191
+
192
+ if now - mtime > seven_days:
193
+ stale.append({
194
+ 'file': change_file.stem,
195
+ 'age_days': int(age_days)
196
+ })
197
+
198
+ self.result['checks']['stale_changes'] = {
199
+ 'count': len(stale),
200
+ 'changes': stale
201
+ }
202
+
203
+ for change in stale:
204
+ self.result['warnings'].append(
205
+ f"過期變更: {change['file']} (已 {change['age_days']} 天未處理)"
206
+ )
207
+
208
+ def check_consistency(self):
209
+ """檢查 specs 與 changes 的一致性"""
210
+ specs_dir = self.openspec_dir / 'specs'
211
+ changes_dir = self.openspec_dir / 'changes'
212
+
213
+ if not specs_dir.exists() or not changes_dir.exists():
214
+ return
215
+
216
+ # 收集 specs 中參照的變更
217
+ referenced_changes = set()
218
+ for spec_file in specs_dir.glob('*.md'):
219
+ try:
220
+ content = spec_file.read_text(encoding='utf-8')
221
+ # 簡單解析 changes 參照 (格式: [[change-name]])
222
+ refs = re.findall(r'\[\[([^\]]+)\]\]', content)
223
+ referenced_changes.update(refs)
224
+ except Exception:
225
+ pass
226
+
227
+ # 檢查 changes 目錄中的檔案
228
+ existing_changes = {f.stem for f in changes_dir.glob('*.md')}
229
+
230
+ # 找出孤立的變更(在 changes/ 但沒被任何 spec 參照)
231
+ orphan_changes = existing_changes - referenced_changes
232
+
233
+ self.result['checks']['consistency'] = {
234
+ 'specs_count': len(list(specs_dir.glob('*.md'))),
235
+ 'changes_count': len(existing_changes),
236
+ 'orphan_changes': list(orphan_changes)
237
+ }
238
+
239
+ # 孤立變更只是警告,不是錯誤
240
+ if orphan_changes:
241
+ for change in orphan_changes:
242
+ self.result['warnings'].append(
243
+ f"孤立變更: {change} (未被任何規格參照)"
244
+ )
245
+
246
+ def _parse_frontmatter(self, content: str) -> dict | None:
247
+ """解析 YAML frontmatter
248
+
249
+ Args:
250
+ content: Markdown 檔案內容
251
+
252
+ Returns:
253
+ dict: frontmatter 資料,如果沒有則返回 None
254
+ """
255
+ # 檢查是否以 --- 開頭
256
+ if not content.startswith('---'):
257
+ return None
258
+
259
+ # 找到結束的 ---
260
+ end_match = re.search(r'\n---\n', content)
261
+ if not end_match:
262
+ return None
263
+
264
+ frontmatter_text = content[3:end_match.start()]
265
+
266
+ # 簡單解析 YAML (只處理 key: value 格式)
267
+ result = {}
268
+ for line in frontmatter_text.strip().split('\n'):
269
+ if ':' in line:
270
+ key, value = line.split(':', 1)
271
+ result[key.strip()] = value.strip().strip('"\'')
272
+
273
+ return result if result else None