collab-runtime 0.2.9__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.
- collab/__init__.py +77 -0
- collab/__main__.py +11 -0
- collab_runtime-0.2.9.dist-info/METADATA +218 -0
- collab_runtime-0.2.9.dist-info/RECORD +82 -0
- collab_runtime-0.2.9.dist-info/WHEEL +5 -0
- collab_runtime-0.2.9.dist-info/entry_points.txt +3 -0
- collab_runtime-0.2.9.dist-info/licenses/LICENSE +21 -0
- collab_runtime-0.2.9.dist-info/top_level.txt +10 -0
- scripts/cleanup.py +395 -0
- scripts/collab_git_hook.py +190 -0
- scripts/format_code.py +594 -0
- scripts/generate_tests.py +560 -0
- scripts/validate_code.py +1397 -0
- src/__init__.py +4 -0
- src/dashboard/index.html +1131 -0
- src/live_locks_watcher.py +1982 -0
- src/lock_client.py +4268 -0
- src/logging_config.py +259 -0
- src/main.py +436 -0
- tests/backend/__init__.py +0 -0
- tests/backend/functional/__init__.py +0 -0
- tests/backend/functional/test_package_imports.py +43 -0
- tests/backend/integration/__init__.py +0 -0
- tests/backend/integration/test_cli_contract_parity.py +220 -0
- tests/backend/performance/__init__.py +0 -0
- tests/backend/reliability/__init__.py +0 -0
- tests/backend/security/__init__.py +0 -0
- tests/backend/unit/live_locks_watcher/__init__.py +5 -0
- tests/backend/unit/live_locks_watcher/_helpers.py +123 -0
- tests/backend/unit/live_locks_watcher/conftest.py +18 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_dashboard.py +188 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_developer.py +56 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_graceful_shutdown.py +459 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_main.py +1925 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_module.py +187 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_multi_session.py +320 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_notify.py +67 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_parsing.py +155 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_process_helpers.py +684 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_processing.py +173 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_prompt_abort.py +71 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_reconcile.py +516 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_scan.py +296 -0
- tests/backend/unit/lock_client/__init__.py +1 -0
- tests/backend/unit/lock_client/_helpers.py +132 -0
- tests/backend/unit/lock_client/test_lock_client_acquire.py +214 -0
- tests/backend/unit/lock_client/test_lock_client_active.py +104 -0
- tests/backend/unit/lock_client/test_lock_client_api.py +63 -0
- tests/backend/unit/lock_client/test_lock_client_cli.py +682 -0
- tests/backend/unit/lock_client/test_lock_client_daemon.py +3730 -0
- tests/backend/unit/lock_client/test_lock_client_dashboard.py +438 -0
- tests/backend/unit/lock_client/test_lock_client_discover.py +241 -0
- tests/backend/unit/lock_client/test_lock_client_force_release.py +354 -0
- tests/backend/unit/lock_client/test_lock_client_helper_branches.py +1890 -0
- tests/backend/unit/lock_client/test_lock_client_history.py +301 -0
- tests/backend/unit/lock_client/test_lock_client_isolation.py +316 -0
- tests/backend/unit/lock_client/test_lock_client_pid.py +75 -0
- tests/backend/unit/lock_client/test_lock_client_reconcile.py +464 -0
- tests/backend/unit/lock_client/test_lock_client_release.py +77 -0
- tests/backend/unit/lock_client/test_lock_client_shutdown.py +1110 -0
- tests/backend/unit/lock_client/test_lock_client_utils.py +474 -0
- tests/backend/unit/lock_client/test_lock_client_watch.py +866 -0
- tests/backend/unit/scripts/__init__.py +1 -0
- tests/backend/unit/scripts/_helpers.py +42 -0
- tests/backend/unit/scripts/test_cleanup.py +285 -0
- tests/backend/unit/scripts/test_collab_git_hook.py +280 -0
- tests/backend/unit/scripts/test_collab_git_hook_ported.py +50 -0
- tests/backend/unit/scripts/test_format_code.py +368 -0
- tests/backend/unit/scripts/test_format_code_ported.py +177 -0
- tests/backend/unit/scripts/test_generate_tests.py +305 -0
- tests/backend/unit/scripts/test_hook_templates.py +357 -0
- tests/backend/unit/scripts/test_setup_hook_overlay.py +95 -0
- tests/backend/unit/scripts/test_validate_code.py +867 -0
- tests/backend/unit/scripts/test_validate_code_ported.py +237 -0
- tests/backend/unit/test_entrypoints_main_run.py +83 -0
- tests/backend/unit/test_logging_config.py +529 -0
- tests/backend/unit/test_main_watch_pid_file.py +278 -0
- tests/conftest.py +167 -0
- tests/frontend/__init__.py +0 -0
- tests/frontend/jest/__init__.py +0 -0
- tests/frontend/playwright/__init__.py +0 -0
- tests/packaging/test_smoke_install.py +76 -0
scripts/format_code.py
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Code Formatting Script - Single Source of Truth for ALL formatting.
|
|
3
|
+
|
|
4
|
+
REQUIREMENT: For YAML formatting, you must have both 'prettier' and
|
|
5
|
+
'prettier-plugin-yaml' installed as dev dependencies:
|
|
6
|
+
npm install --save-dev prettier prettier-plugin-yaml
|
|
7
|
+
|
|
8
|
+
Actively formats code using configured formatters:
|
|
9
|
+
- Whitespace: trailing whitespace removal, EOF newline normalization (ALL text files)
|
|
10
|
+
- Python: ruff (lint fixing + unsafe fixes), isort, black, docformatter
|
|
11
|
+
- JavaScript/CSS: prettier (quiet in format mode)
|
|
12
|
+
- Documentation: prettier (quiet in format mode, with prettier-plugin-yaml for YAML)
|
|
13
|
+
- Templates: djlint (Jinja2/HTML)
|
|
14
|
+
|
|
15
|
+
ARCHITECTURE:
|
|
16
|
+
format_code.py = the ONLY tool that MODIFIES files (formatter).
|
|
17
|
+
pre-commit hooks = CHECK-ONLY gate (--check mode, never modify files).
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python scripts/format_code.py # Format everything
|
|
21
|
+
python scripts/format_code.py --backend # Python only
|
|
22
|
+
python scripts/format_code.py --frontend # JS + templates only
|
|
23
|
+
python scripts/format_code.py --check # Check only (pre-commit)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import os
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
34
|
+
from cleanup import clean_caches # noqa: E402
|
|
35
|
+
|
|
36
|
+
# UTF-8 for Windows + ANSI colors (standardized - matches setup scripts)
|
|
37
|
+
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
|
|
38
|
+
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined,union-attr]
|
|
39
|
+
sys.stderr.reconfigure(encoding="utf-8") # type: ignore[attr-defined,union-attr]
|
|
40
|
+
|
|
41
|
+
GREEN = "\033[92m"
|
|
42
|
+
RED = "\033[91m"
|
|
43
|
+
CYAN = "\033[96m"
|
|
44
|
+
RESET = "\033[0m"
|
|
45
|
+
BOLD = "\033[1m"
|
|
46
|
+
MAGENTA = "\033[95m"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CodeFormatter:
|
|
50
|
+
"""Handles code formatting with colored, professional, numbered output."""
|
|
51
|
+
|
|
52
|
+
# NOTE: .html/.htm are intentionally excluded from whitespace pass because
|
|
53
|
+
# djlint can re-introduce trailing whitespace in template line wrapping.
|
|
54
|
+
TEXT_EXTENSIONS: frozenset[str] = frozenset(
|
|
55
|
+
{
|
|
56
|
+
".py",
|
|
57
|
+
".js",
|
|
58
|
+
".ts",
|
|
59
|
+
".jsx",
|
|
60
|
+
".tsx",
|
|
61
|
+
".css",
|
|
62
|
+
".json",
|
|
63
|
+
".yaml",
|
|
64
|
+
".yml",
|
|
65
|
+
".toml",
|
|
66
|
+
".ini",
|
|
67
|
+
".cfg",
|
|
68
|
+
".env",
|
|
69
|
+
".md",
|
|
70
|
+
".txt",
|
|
71
|
+
".rst",
|
|
72
|
+
".sh",
|
|
73
|
+
".bash",
|
|
74
|
+
".ps1",
|
|
75
|
+
".bat",
|
|
76
|
+
".cmd",
|
|
77
|
+
".sql",
|
|
78
|
+
".xml",
|
|
79
|
+
".csv",
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def __init__(self, check_only: bool = False, files: Optional[list[str]] = None):
|
|
84
|
+
self.check_only = check_only
|
|
85
|
+
self.files = files
|
|
86
|
+
self.root_dir = Path(__file__).parent.parent
|
|
87
|
+
self.failed_tools: list[tuple[str, str, bool]] = []
|
|
88
|
+
|
|
89
|
+
def _get_targets(
|
|
90
|
+
self, extensions: tuple[str, ...], default: list[str]
|
|
91
|
+
) -> list[str]:
|
|
92
|
+
if not self.files:
|
|
93
|
+
return default
|
|
94
|
+
return [f for f in self.files if f.lower().endswith(extensions)]
|
|
95
|
+
|
|
96
|
+
def _prepare_env(self) -> dict:
|
|
97
|
+
env = os.environ.copy()
|
|
98
|
+
scripts_dir = "Scripts" if sys.platform == "win32" else "bin"
|
|
99
|
+
venv_scripts = self.root_dir / ".venv" / scripts_dir
|
|
100
|
+
if venv_scripts.exists():
|
|
101
|
+
env["PATH"] = f"{venv_scripts}{os.pathsep}{env.get('PATH', '')}"
|
|
102
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
103
|
+
return env
|
|
104
|
+
|
|
105
|
+
def _get_python_executable(self) -> str:
|
|
106
|
+
"""Prefer repository .venv Python for module-based tool execution."""
|
|
107
|
+
scripts_dir = "Scripts" if sys.platform == "win32" else "bin"
|
|
108
|
+
python_name = "python.exe" if sys.platform == "win32" else "python"
|
|
109
|
+
venv_python = self.root_dir / ".venv" / scripts_dir / python_name
|
|
110
|
+
if venv_python.exists():
|
|
111
|
+
return str(venv_python)
|
|
112
|
+
return sys.executable
|
|
113
|
+
|
|
114
|
+
def _exec(
|
|
115
|
+
self, cmd: list[str], suppress_output: bool = False
|
|
116
|
+
) -> tuple[bool, Optional[subprocess.CompletedProcess]]:
|
|
117
|
+
try:
|
|
118
|
+
if sys.platform == "win32" and cmd[0] in ("npm", "npx"):
|
|
119
|
+
cmd = ["cmd", "/c"] + cmd
|
|
120
|
+
result = subprocess.run(
|
|
121
|
+
cmd,
|
|
122
|
+
cwd=self.root_dir,
|
|
123
|
+
capture_output=True,
|
|
124
|
+
text=True,
|
|
125
|
+
encoding="utf-8",
|
|
126
|
+
errors="replace",
|
|
127
|
+
check=False,
|
|
128
|
+
env=self._prepare_env(),
|
|
129
|
+
)
|
|
130
|
+
if not suppress_output:
|
|
131
|
+
printed = False
|
|
132
|
+
if result.stdout.strip():
|
|
133
|
+
for line in result.stdout.strip().splitlines():
|
|
134
|
+
print(f" {line}")
|
|
135
|
+
printed = True
|
|
136
|
+
if result.stderr.strip():
|
|
137
|
+
for line in result.stderr.strip().splitlines():
|
|
138
|
+
print(f" {line}", file=sys.stderr)
|
|
139
|
+
printed = True
|
|
140
|
+
if result.returncode == 0 and not printed:
|
|
141
|
+
print(" All checks passed!")
|
|
142
|
+
return result.returncode == 0, result
|
|
143
|
+
except FileNotFoundError:
|
|
144
|
+
if not suppress_output:
|
|
145
|
+
print(f" Tool not found: {cmd[0]}")
|
|
146
|
+
return False, None
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
if not suppress_output:
|
|
149
|
+
print(f" Error: {exc}")
|
|
150
|
+
return False, None
|
|
151
|
+
|
|
152
|
+
def _run_tool_step(
|
|
153
|
+
self,
|
|
154
|
+
description: str,
|
|
155
|
+
fix_cmd: Optional[list[str]],
|
|
156
|
+
check_cmd: Optional[list[str]],
|
|
157
|
+
section: str,
|
|
158
|
+
section_idx: int,
|
|
159
|
+
section_total: int,
|
|
160
|
+
) -> bool:
|
|
161
|
+
"""Run one formatting tool step with fully standardized output."""
|
|
162
|
+
step_header = f"[{section.upper()} {section_idx}/{section_total}] {description}"
|
|
163
|
+
print(f"\n{CYAN}{step_header}...{RESET}")
|
|
164
|
+
|
|
165
|
+
if self.check_only:
|
|
166
|
+
cmd = check_cmd or fix_cmd
|
|
167
|
+
assert cmd is not None, "At least one of fix_cmd or check_cmd required"
|
|
168
|
+
print(f" {MAGENTA}Command: {' '.join(cmd)}{RESET}")
|
|
169
|
+
success, _ = self._exec(cmd)
|
|
170
|
+
if success:
|
|
171
|
+
print(f" {GREEN}✅ {description} - SUCCESS{RESET}")
|
|
172
|
+
else:
|
|
173
|
+
print(f" {RED}❌ {description} - ISSUES FOUND{RESET}")
|
|
174
|
+
self.failed_tools.append((step_header, description, False))
|
|
175
|
+
return success
|
|
176
|
+
|
|
177
|
+
primary_cmd = fix_cmd if fix_cmd is not None else check_cmd
|
|
178
|
+
assert primary_cmd is not None, "At least one of fix_cmd or check_cmd required"
|
|
179
|
+
print(f" {MAGENTA}Command: {' '.join(primary_cmd)}{RESET}")
|
|
180
|
+
success, _ = self._exec(primary_cmd)
|
|
181
|
+
|
|
182
|
+
if success:
|
|
183
|
+
print(f" {GREEN}✅ {description} - SUCCESS{RESET}")
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
print(f" {RED}❌ {description} - ISSUES FOUND{RESET}")
|
|
187
|
+
|
|
188
|
+
if check_cmd:
|
|
189
|
+
print(f"\n {MAGENTA}Command: {' '.join(check_cmd)}{RESET}")
|
|
190
|
+
check_ok, _ = self._exec(check_cmd, suppress_output=True)
|
|
191
|
+
if check_ok:
|
|
192
|
+
print(
|
|
193
|
+
f" {GREEN}✅ {description} (check) - All issues fixed - "
|
|
194
|
+
f"no further action needed.{RESET}"
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
print(
|
|
198
|
+
f" {RED}❌ {description} (check) - Issues remain - "
|
|
199
|
+
f"manual fix required.{RESET}"
|
|
200
|
+
)
|
|
201
|
+
self.failed_tools.append((step_header, description, True))
|
|
202
|
+
return check_ok
|
|
203
|
+
|
|
204
|
+
self.failed_tools.append((step_header, description, False))
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
def normalize_whitespace(self) -> bool:
|
|
208
|
+
print("\n" + "=" * 80)
|
|
209
|
+
print(f"{BOLD}WHITESPACE & EOF NORMALIZATION{RESET}")
|
|
210
|
+
print("=" * 80)
|
|
211
|
+
|
|
212
|
+
if self.files:
|
|
213
|
+
tracked_files = self.files
|
|
214
|
+
else:
|
|
215
|
+
try:
|
|
216
|
+
proc = subprocess.run(
|
|
217
|
+
["git", "ls-files"],
|
|
218
|
+
cwd=self.root_dir,
|
|
219
|
+
capture_output=True,
|
|
220
|
+
text=True,
|
|
221
|
+
encoding="utf-8",
|
|
222
|
+
check=True,
|
|
223
|
+
)
|
|
224
|
+
tracked_files = [f for f in proc.stdout.strip().split("\n") if f]
|
|
225
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
226
|
+
print(" ⚠️ Could not list git files - skipping whitespace")
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
issues: list[str] = []
|
|
230
|
+
fix = not self.check_only
|
|
231
|
+
|
|
232
|
+
for rel_path in tracked_files:
|
|
233
|
+
filepath = self.root_dir / rel_path
|
|
234
|
+
if (
|
|
235
|
+
filepath.suffix.lower() not in self.TEXT_EXTENSIONS
|
|
236
|
+
or not filepath.is_file()
|
|
237
|
+
):
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
raw = filepath.read_bytes()
|
|
242
|
+
except OSError:
|
|
243
|
+
continue
|
|
244
|
+
if not raw or b"\x00" in raw:
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
fixed = self._normalize_whitespace(raw)
|
|
248
|
+
if fixed != raw:
|
|
249
|
+
issues.append(rel_path)
|
|
250
|
+
if fix:
|
|
251
|
+
filepath.write_bytes(fixed)
|
|
252
|
+
|
|
253
|
+
if not issues:
|
|
254
|
+
print(f" {GREEN}✅ Whitespace & EOF - all files clean{RESET}")
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
if fix:
|
|
258
|
+
print(f" {GREEN}✅ Fixed whitespace/EOF in {len(issues)} file(s){RESET}")
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
print(f" {RED}❌ {len(issues)} file(s) have whitespace/EOF issues{RESET}")
|
|
262
|
+
for rel_file in issues[:10]:
|
|
263
|
+
print(f" - {rel_file}")
|
|
264
|
+
if len(issues) > 10:
|
|
265
|
+
print(f" ... and {len(issues) - 10} more")
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def _normalize_whitespace(content: bytes) -> bytes:
|
|
270
|
+
uses_crlf = b"\r\n" in content
|
|
271
|
+
normalized = content.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
|
272
|
+
lines = normalized.split(b"\n")
|
|
273
|
+
stripped = [line.rstrip(b" \t") for line in lines]
|
|
274
|
+
result = b"\n".join(stripped).rstrip(b"\n") + b"\n"
|
|
275
|
+
if uses_crlf:
|
|
276
|
+
result = result.replace(b"\n", b"\r\n")
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
def format_python(self) -> bool:
|
|
280
|
+
targets = self._get_targets(
|
|
281
|
+
(".py",),
|
|
282
|
+
[
|
|
283
|
+
"src",
|
|
284
|
+
"tests",
|
|
285
|
+
"scripts",
|
|
286
|
+
],
|
|
287
|
+
)
|
|
288
|
+
if not targets:
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
print("\n" + "=" * 80)
|
|
292
|
+
print(f"{BOLD}BACKEND CODE FORMATTING{RESET}")
|
|
293
|
+
print("=" * 80)
|
|
294
|
+
|
|
295
|
+
flake8_exclude = (
|
|
296
|
+
"--exclude="
|
|
297
|
+
".venv,node_modules,__pycache__,.git,.pytest_cache,"
|
|
298
|
+
"htmlcov,playwright-report"
|
|
299
|
+
)
|
|
300
|
+
flake8_opts = [
|
|
301
|
+
flake8_exclude,
|
|
302
|
+
"--count",
|
|
303
|
+
"--show-source",
|
|
304
|
+
"--statistics",
|
|
305
|
+
"--max-line-length=88",
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
steps: list[tuple[str, Optional[list[str]], list[str]]] = [
|
|
309
|
+
(
|
|
310
|
+
"Import sorting (isort)",
|
|
311
|
+
["isort"] + targets,
|
|
312
|
+
["isort"] + targets + ["--check-only"],
|
|
313
|
+
),
|
|
314
|
+
(
|
|
315
|
+
"Code formatting (black)",
|
|
316
|
+
["black"] + targets,
|
|
317
|
+
["black", "--check"] + targets,
|
|
318
|
+
),
|
|
319
|
+
(
|
|
320
|
+
"Docstring formatting (docformatter)",
|
|
321
|
+
["docformatter", "--in-place", "-r"] + targets,
|
|
322
|
+
["docformatter", "--check", "-r"] + targets,
|
|
323
|
+
),
|
|
324
|
+
(
|
|
325
|
+
"Ruff linting & fixing",
|
|
326
|
+
["ruff", "check", "--no-cache"] + targets + ["--fix", "--unsafe-fixes"],
|
|
327
|
+
["ruff", "check", "--no-cache"] + targets,
|
|
328
|
+
),
|
|
329
|
+
(
|
|
330
|
+
"Final linting (flake8)",
|
|
331
|
+
None,
|
|
332
|
+
["flake8"] + targets + flake8_opts,
|
|
333
|
+
),
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
all_passed = True
|
|
337
|
+
for idx, (desc, fix_cmd, check_cmd) in enumerate(steps, 1):
|
|
338
|
+
all_passed &= self._run_tool_step(
|
|
339
|
+
desc, fix_cmd, check_cmd, "BACKEND", idx, len(steps)
|
|
340
|
+
)
|
|
341
|
+
return all_passed
|
|
342
|
+
|
|
343
|
+
def _check_prettier(self) -> bool:
|
|
344
|
+
npm_cmd = ["cmd", "/c", "npm"] if sys.platform == "win32" else ["npm"]
|
|
345
|
+
result = subprocess.run(
|
|
346
|
+
npm_cmd + ["list", "prettier"],
|
|
347
|
+
cwd=self.root_dir,
|
|
348
|
+
capture_output=True,
|
|
349
|
+
check=False,
|
|
350
|
+
)
|
|
351
|
+
if result.returncode != 0:
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
plugin_result = subprocess.run(
|
|
355
|
+
npm_cmd + ["list", "prettier-plugin-yaml"],
|
|
356
|
+
cwd=self.root_dir,
|
|
357
|
+
capture_output=True,
|
|
358
|
+
check=False,
|
|
359
|
+
)
|
|
360
|
+
if plugin_result.returncode != 0:
|
|
361
|
+
print(
|
|
362
|
+
" ⚠️ prettier-plugin-yaml not installed - YAML files will NOT be "
|
|
363
|
+
"formatted!\n"
|
|
364
|
+
" Run: npm install --save-dev prettier-plugin-yaml"
|
|
365
|
+
)
|
|
366
|
+
return False
|
|
367
|
+
return True
|
|
368
|
+
|
|
369
|
+
def _filter_glob_targets(self, patterns: list[str]) -> list[str]:
|
|
370
|
+
return [pattern for pattern in patterns if list(self.root_dir.glob(pattern))]
|
|
371
|
+
|
|
372
|
+
def format_frontend(self) -> bool:
|
|
373
|
+
base_targets = self._filter_glob_targets(
|
|
374
|
+
[
|
|
375
|
+
"src/**/*.js",
|
|
376
|
+
"src/**/*.css",
|
|
377
|
+
"tests/**/*.js",
|
|
378
|
+
]
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
targets = self._get_targets(
|
|
382
|
+
(".js", ".jsx", ".ts", ".tsx", ".css", ".scss"),
|
|
383
|
+
base_targets,
|
|
384
|
+
)
|
|
385
|
+
if not targets:
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
print("\n" + "=" * 80)
|
|
389
|
+
print(f"{BOLD}FRONTEND CODE FORMATTING{RESET}")
|
|
390
|
+
print("=" * 80)
|
|
391
|
+
|
|
392
|
+
if not self._check_prettier():
|
|
393
|
+
print(" ℹ️ Prettier not installed - skipping frontend")
|
|
394
|
+
return True
|
|
395
|
+
|
|
396
|
+
return self._run_tool_step(
|
|
397
|
+
"JavaScript/CSS (prettier)",
|
|
398
|
+
["npx", "prettier", "--write", "--log-level", "silent"] + targets,
|
|
399
|
+
["npx", "prettier", "--check"] + targets,
|
|
400
|
+
"FRONTEND",
|
|
401
|
+
1,
|
|
402
|
+
1,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def format_docs(self) -> bool:
|
|
406
|
+
doc_targets = self._get_targets(
|
|
407
|
+
(".md", ".json"),
|
|
408
|
+
self._filter_glob_targets(
|
|
409
|
+
[
|
|
410
|
+
"docs/**/*.md",
|
|
411
|
+
"*.md",
|
|
412
|
+
"*.json",
|
|
413
|
+
".github/**/*.md",
|
|
414
|
+
"tests/**/*.md",
|
|
415
|
+
".agents/**/*.md",
|
|
416
|
+
]
|
|
417
|
+
),
|
|
418
|
+
)
|
|
419
|
+
if not doc_targets:
|
|
420
|
+
return True
|
|
421
|
+
|
|
422
|
+
print("\n" + "=" * 80)
|
|
423
|
+
print(f"{BOLD}DOCUMENTATION FORMATTING{RESET}")
|
|
424
|
+
print("=" * 80)
|
|
425
|
+
|
|
426
|
+
return self._run_tool_step(
|
|
427
|
+
"Markdown/JSON (prettier)",
|
|
428
|
+
["npx", "prettier", "--write", "--log-level", "silent"] + doc_targets,
|
|
429
|
+
["npx", "prettier", "--check"] + doc_targets,
|
|
430
|
+
"DOCS",
|
|
431
|
+
1,
|
|
432
|
+
1,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
def format_yaml(self) -> bool:
|
|
436
|
+
exclude_dirs = {".venv", "node_modules", ".git", "__pycache__"}
|
|
437
|
+
yaml_files = []
|
|
438
|
+
for ext in ("*.yaml", "*.yml"):
|
|
439
|
+
for path in self.root_dir.rglob(ext):
|
|
440
|
+
if not any(part in exclude_dirs for part in path.parts):
|
|
441
|
+
yaml_files.append(str(path))
|
|
442
|
+
if not yaml_files:
|
|
443
|
+
return True
|
|
444
|
+
|
|
445
|
+
print("\n" + "=" * 80)
|
|
446
|
+
print(f"{BOLD}YAML FORMATTING & LINTING{RESET}")
|
|
447
|
+
print("=" * 80)
|
|
448
|
+
|
|
449
|
+
all_passed = True
|
|
450
|
+
all_passed &= self._run_tool_step(
|
|
451
|
+
"YAML (prettier)",
|
|
452
|
+
["npx", "prettier", "--write", "--log-level", "silent"] + yaml_files,
|
|
453
|
+
["npx", "prettier", "--check"] + yaml_files,
|
|
454
|
+
"YAML",
|
|
455
|
+
1,
|
|
456
|
+
2,
|
|
457
|
+
)
|
|
458
|
+
all_passed &= self._run_tool_step(
|
|
459
|
+
"YAML (yamllint)",
|
|
460
|
+
None,
|
|
461
|
+
["yamllint", "--strict"] + yaml_files,
|
|
462
|
+
"YAML",
|
|
463
|
+
2,
|
|
464
|
+
2,
|
|
465
|
+
)
|
|
466
|
+
return all_passed
|
|
467
|
+
|
|
468
|
+
def format_templates(self) -> bool:
|
|
469
|
+
template_dirs = ["src/dashboard"]
|
|
470
|
+
targets = self._get_targets((".html", ".htm"), template_dirs)
|
|
471
|
+
if not targets:
|
|
472
|
+
return True
|
|
473
|
+
|
|
474
|
+
python = self._get_python_executable()
|
|
475
|
+
djlint_check, _ = self._exec([python, "-m", "djlint", "--version"], True)
|
|
476
|
+
if not djlint_check:
|
|
477
|
+
print("\n" + "=" * 80)
|
|
478
|
+
print(f"{BOLD}JINJA2 TEMPLATE FORMATTING{RESET}")
|
|
479
|
+
print("=" * 80)
|
|
480
|
+
print(" ℹ️ djlint not installed - skipping template formatting")
|
|
481
|
+
return True
|
|
482
|
+
|
|
483
|
+
print("\n" + "=" * 80)
|
|
484
|
+
print(f"{BOLD}JINJA2 TEMPLATE FORMATTING{RESET}")
|
|
485
|
+
print("=" * 80)
|
|
486
|
+
|
|
487
|
+
description = "Jinja2 templates (djlint)"
|
|
488
|
+
step_header = "[TEMPLATES 1/1] Jinja2 templates (djlint)"
|
|
489
|
+
fix_cmd = [python, "-m", "djlint"] + targets + ["--reformat", "--quiet"]
|
|
490
|
+
check_cmd = [python, "-m", "djlint"] + targets + ["--check"]
|
|
491
|
+
|
|
492
|
+
print(f"\n{CYAN}{step_header}...{RESET}")
|
|
493
|
+
print(f" {MAGENTA}Command: {' '.join(fix_cmd)}{RESET}")
|
|
494
|
+
|
|
495
|
+
fix_ok, _ = self._exec(fix_cmd, suppress_output=True)
|
|
496
|
+
if fix_ok:
|
|
497
|
+
print(" All checks passed!")
|
|
498
|
+
print(f" {GREEN}✅ {description} - SUCCESS{RESET}")
|
|
499
|
+
return True
|
|
500
|
+
|
|
501
|
+
print(
|
|
502
|
+
f" {CYAN}ℹ️ {description} - changes applied; "
|
|
503
|
+
f"running verification check...{RESET}"
|
|
504
|
+
)
|
|
505
|
+
print(f"\n {MAGENTA}Command: {' '.join(check_cmd)}{RESET}")
|
|
506
|
+
check_ok, _ = self._exec(check_cmd, suppress_output=True)
|
|
507
|
+
|
|
508
|
+
if check_ok:
|
|
509
|
+
print(
|
|
510
|
+
f" {GREEN}✅ {description} - All issues fixed - "
|
|
511
|
+
f"no further action needed.{RESET}"
|
|
512
|
+
)
|
|
513
|
+
return True
|
|
514
|
+
|
|
515
|
+
print(
|
|
516
|
+
f" {RED}❌ {description} (check) - Issues remain - "
|
|
517
|
+
f"manual fix required.{RESET}"
|
|
518
|
+
)
|
|
519
|
+
self._exec(check_cmd, suppress_output=False)
|
|
520
|
+
self.failed_tools.append((step_header, description, True))
|
|
521
|
+
return False
|
|
522
|
+
|
|
523
|
+
def print_summary(self) -> None:
|
|
524
|
+
print("\n" + "=" * 80)
|
|
525
|
+
print(f"{BOLD}FORMATTING SUMMARY{RESET}")
|
|
526
|
+
print("=" * 80)
|
|
527
|
+
if not self.failed_tools:
|
|
528
|
+
mode = "check" if self.check_only else "formatting"
|
|
529
|
+
print(f" {GREEN}✅ All {mode} operations completed successfully!{RESET}")
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
print(f" {RED}❌ {len(self.failed_tools)} operation(s) failed{RESET}")
|
|
533
|
+
for step_header, _, _ in self.failed_tools:
|
|
534
|
+
print(f" - {step_header}")
|
|
535
|
+
print(f"\n {RED}⚠️ Review the errors above and fix manually.{RESET}")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def main() -> int:
|
|
539
|
+
parser = argparse.ArgumentParser(
|
|
540
|
+
description="Format code using configured formatters"
|
|
541
|
+
)
|
|
542
|
+
parser.add_argument("--backend", action="store_true", help="Python only")
|
|
543
|
+
parser.add_argument(
|
|
544
|
+
"--frontend", action="store_true", help="JS/CSS + templates only"
|
|
545
|
+
)
|
|
546
|
+
parser.add_argument("--docs", action="store_true", help="Markdown/JSON only")
|
|
547
|
+
parser.add_argument("--check", action="store_true", help="Check only (pre-commit)")
|
|
548
|
+
parser.add_argument(
|
|
549
|
+
"--update-hooks", action="store_true", help="pre-commit autoupdate"
|
|
550
|
+
)
|
|
551
|
+
parser.add_argument("files", nargs="*", help="Specific files to format")
|
|
552
|
+
|
|
553
|
+
args = parser.parse_args()
|
|
554
|
+
|
|
555
|
+
run_all = not (args.backend or args.frontend or args.docs)
|
|
556
|
+
format_backend = args.backend or run_all
|
|
557
|
+
format_frontend = args.frontend or run_all
|
|
558
|
+
format_docs = args.docs or run_all
|
|
559
|
+
|
|
560
|
+
formatter = CodeFormatter(
|
|
561
|
+
check_only=args.check, files=args.files if args.files else None
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
all_passed = True
|
|
565
|
+
all_passed &= formatter.normalize_whitespace()
|
|
566
|
+
|
|
567
|
+
if format_backend:
|
|
568
|
+
all_passed &= formatter.format_python()
|
|
569
|
+
|
|
570
|
+
if format_frontend:
|
|
571
|
+
all_passed &= formatter.format_frontend()
|
|
572
|
+
all_passed &= formatter.format_templates()
|
|
573
|
+
|
|
574
|
+
if format_docs:
|
|
575
|
+
all_passed &= formatter.format_docs()
|
|
576
|
+
|
|
577
|
+
all_passed &= formatter.format_yaml()
|
|
578
|
+
|
|
579
|
+
formatter.print_summary()
|
|
580
|
+
|
|
581
|
+
print("\n" + "=" * 80)
|
|
582
|
+
print(f"{BOLD}CLEANUP{RESET}")
|
|
583
|
+
print("=" * 80)
|
|
584
|
+
count = clean_caches(dry_run=False)
|
|
585
|
+
if count:
|
|
586
|
+
print(f" {GREEN}✅ Removed {count} cache artifact(s){RESET}")
|
|
587
|
+
else:
|
|
588
|
+
print(f" ✨ Repo already clean{RESET}")
|
|
589
|
+
|
|
590
|
+
return 0 if all_passed else 1
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
if __name__ == "__main__":
|
|
594
|
+
sys.exit(main())
|