uidetox 0.1.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.
- uidetox/__init__.py +3 -0
- uidetox/analyzer.py +227 -0
- uidetox/cli.py +221 -0
- uidetox/commands/__init__.py +1 -0
- uidetox/commands/add_issue.py +37 -0
- uidetox/commands/autofix.py +42 -0
- uidetox/commands/check.py +104 -0
- uidetox/commands/detect.py +58 -0
- uidetox/commands/exclude.py +17 -0
- uidetox/commands/format_cmd.py +60 -0
- uidetox/commands/history_cmd.py +49 -0
- uidetox/commands/lint.py +86 -0
- uidetox/commands/loop.py +174 -0
- uidetox/commands/memory_cmd.py +59 -0
- uidetox/commands/next.py +110 -0
- uidetox/commands/plan.py +37 -0
- uidetox/commands/rescan.py +73 -0
- uidetox/commands/resolve.py +47 -0
- uidetox/commands/review.py +20 -0
- uidetox/commands/scan.py +129 -0
- uidetox/commands/setup.py +40 -0
- uidetox/commands/show.py +40 -0
- uidetox/commands/skill_cmd.py +39 -0
- uidetox/commands/status.py +147 -0
- uidetox/commands/subagent_cmd.py +127 -0
- uidetox/commands/suppress.py +85 -0
- uidetox/commands/tsc.py +77 -0
- uidetox/commands/update_skill.py +44 -0
- uidetox/commands/viz.py +232 -0
- uidetox/commands/zone.py +128 -0
- uidetox/history.py +91 -0
- uidetox/memory.py +112 -0
- uidetox/state.py +148 -0
- uidetox/subagent.py +361 -0
- uidetox/tooling.py +237 -0
- uidetox/utils.py +8 -0
- uidetox-0.1.0.dist-info/METADATA +251 -0
- uidetox-0.1.0.dist-info/RECORD +42 -0
- uidetox-0.1.0.dist-info/WHEEL +5 -0
- uidetox-0.1.0.dist-info/entry_points.txt +2 -0
- uidetox-0.1.0.dist-info/licenses/LICENSE +21 -0
- uidetox-0.1.0.dist-info/top_level.txt +1 -0
uidetox/__init__.py
ADDED
uidetox/analyzer.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Static Slop Analyzer: Detects AI anti-patterns via regex/AST rules."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# Directories to always skip during traversal
|
|
8
|
+
IGNORE_DIRS = {
|
|
9
|
+
".git", "node_modules", "dist", "build", "out", ".next",
|
|
10
|
+
".nuxt", "coverage", ".uidetox", ".claude", ".cursor", "vendor"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
# The Anti-Pattern Rule Catalog
|
|
14
|
+
RULES = [
|
|
15
|
+
{
|
|
16
|
+
"id": "TYPOGRAPHY_SLOP",
|
|
17
|
+
"pattern": re.compile(r'\b(font-(inter|roboto|sans|arial|open-sans)|font-family:\s*(Inter|Roboto|Arial|System-ui))\b', re.IGNORECASE),
|
|
18
|
+
"tier": "T1",
|
|
19
|
+
"exts": {".css", ".scss", ".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
20
|
+
"description": "Generic AI Typography detected (Inter/Roboto/sans).",
|
|
21
|
+
"command": "Swap font family to a distinctive typeface (Geist, Outfit, Satoshi, etc.) and update scale."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "COLOR_GRADIENT_SLOP",
|
|
25
|
+
"pattern": re.compile(r'\b(from-(blue|purple|indigo)-[4-6]00.*?to-(purple|blue|indigo)-[4-6]00)\b', re.IGNORECASE),
|
|
26
|
+
"tier": "T1",
|
|
27
|
+
"exts": {".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
28
|
+
"description": "AI Pipeline Palette (Purple-Blue gradient) detected.",
|
|
29
|
+
"command": "Replace generic gradient with a high-contrast solid accent color on a neutral base."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "COLOR_BLACK_SLOP",
|
|
33
|
+
"pattern": re.compile(r'\b(#000000|bg-black|text-black)\b', re.IGNORECASE),
|
|
34
|
+
"tier": "T1",
|
|
35
|
+
"exts": {".css", ".scss", ".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
36
|
+
"description": "Pure black (#000000) detected. Pure black rarely exists in nature.",
|
|
37
|
+
"command": "Replace true black with tinted dark neutrals (e.g. zinc-950 or slate-900)."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "ICONOGRAPHY_SLOP",
|
|
41
|
+
"pattern": re.compile(r'\blucide-react\b', re.IGNORECASE),
|
|
42
|
+
"tier": "T2",
|
|
43
|
+
"exts": {".tsx", ".jsx", ".ts", ".js"},
|
|
44
|
+
"description": "Generic 'lucide-react' standard icons detected.",
|
|
45
|
+
"command": "Swap icon library for Phosphor Icons, Heroicons, or custom SVG to build unique identity."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "MATERIALITY_RADIUS_SLOP",
|
|
49
|
+
"pattern": re.compile(r'\b(rounded-2xl|rounded-3xl)\b', re.IGNORECASE),
|
|
50
|
+
"tier": "T1",
|
|
51
|
+
"exts": {".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
52
|
+
"description": "Oversized AI border radii (2xl/3xl) detected outside of modals/avatars.",
|
|
53
|
+
"command": "Reduce border-radius to tighter bounds (rounded-lg or rounded-xl) for precision."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "LAYOUT_MATH_SLOP",
|
|
57
|
+
"pattern": re.compile(r'\b(w-1/3|grid-cols-3)\b', re.IGNORECASE),
|
|
58
|
+
"tier": "T2",
|
|
59
|
+
"exts": {".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
60
|
+
"description": "Generic 3-Column feature card layout detected.",
|
|
61
|
+
"command": "Refactor into an asymmetrical grid, zig-zag layout, or masonry flow to break predictability."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "GLASSMORPHISM_SLOP",
|
|
65
|
+
"pattern": re.compile(r'\b(backdrop-blur|glass-?morphism|bg-white/\d|bg-opacity-)\b', re.IGNORECASE),
|
|
66
|
+
"tier": "T2",
|
|
67
|
+
"exts": {".css", ".scss", ".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
68
|
+
"description": "Glassmorphism pattern detected — strong AI fingerprint.",
|
|
69
|
+
"command": "Replace with solid surfaces, subtle borders, or elevation via shadow hierarchy."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "SHADOW_SLOP",
|
|
73
|
+
"pattern": re.compile(r'\b(shadow-2xl|shadow-3xl|shadow-\[0_\d+px)\b', re.IGNORECASE),
|
|
74
|
+
"tier": "T1",
|
|
75
|
+
"exts": {".css", ".scss", ".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
76
|
+
"description": "Oversized AI shadow detected (2xl/3xl).",
|
|
77
|
+
"command": "Use subtle shadows (shadow-sm, shadow-md) or border-based elevation instead."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "HERO_DASHBOARD_SLOP",
|
|
81
|
+
"pattern": re.compile(r'\b(stat-?card|metric-?card|dashboard-?hero|stats-?grid|kpi-?card)\b', re.IGNORECASE),
|
|
82
|
+
"tier": "T3",
|
|
83
|
+
"exts": {".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
84
|
+
"description": "Hero metric dashboard pattern detected — cliché AI layout.",
|
|
85
|
+
"command": "Replace with contextual data visualization or inline metrics woven into the narrative flow."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "BOUNCE_ANIMATION_SLOP",
|
|
89
|
+
"pattern": re.compile(r'\b(animate-bounce|animate-pulse|animate-spin)\b', re.IGNORECASE),
|
|
90
|
+
"tier": "T1",
|
|
91
|
+
"exts": {".css", ".scss", ".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
92
|
+
"description": "Generic Tailwind animation (bounce/pulse/spin) detected.",
|
|
93
|
+
"command": "Replace with intentional micro-interactions using CSS transitions or spring physics."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "GRAY_ON_COLOR_SLOP",
|
|
97
|
+
"pattern": re.compile(r'text-gray-[3-5]00.*?bg-(blue|purple|green|indigo|violet)-', re.IGNORECASE),
|
|
98
|
+
"tier": "T2",
|
|
99
|
+
"exts": {".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
100
|
+
"description": "Gray text on colored background detected — low contrast AI pattern.",
|
|
101
|
+
"command": "Use white or high-contrast text on colored backgrounds. Check WCAG AA contrast ratio."
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "MISSING_DARK_MODE",
|
|
105
|
+
"pattern": re.compile(r'(?:bg-(white|gray|slate|zinc)-[12]00|bg-white)\b(?!.*dark:)', re.IGNORECASE),
|
|
106
|
+
"tier": "T2",
|
|
107
|
+
"exts": {".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
108
|
+
"description": "Light background without dark: variant detected.",
|
|
109
|
+
"command": "Add dark mode variants (dark:bg-zinc-900) for every light surface color."
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "SPACING_REPETITION_SLOP",
|
|
113
|
+
"pattern": re.compile(r'(p-4|gap-4|space-y-4)(?:.*\1){4,}', re.DOTALL),
|
|
114
|
+
"tier": "T2",
|
|
115
|
+
"exts": {".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
116
|
+
"description": "Excessive identical spacing repetition detected (5+ p-4/gap-4).",
|
|
117
|
+
"command": "Introduce spacing scale variation (mix p-3, p-5, p-6) to create visual rhythm."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"id": "CSS_GRADIENT_SLOP",
|
|
121
|
+
"pattern": re.compile(r'linear-gradient\s*\([^)]*(?:purple|indigo|violet).*?(?:blue|cyan|sky)', re.IGNORECASE),
|
|
122
|
+
"tier": "T1",
|
|
123
|
+
"exts": {".css", ".scss", ".less", ".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
124
|
+
"description": "CSS linear-gradient with purple-to-blue spectrum detected.",
|
|
125
|
+
"command": "Replace with a single accent color or a more subtle, brand-aligned gradient."
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"id": "GENERIC_COPY_SLOP",
|
|
129
|
+
"pattern": re.compile(r'\b(Supercharge|Revolutionize|Unlock the power|Seamlessly|Take your .* to the next level|Effortlessly)\b', re.IGNORECASE),
|
|
130
|
+
"tier": "T2",
|
|
131
|
+
"exts": {".tsx", ".jsx", ".html", ".svelte", ".vue", ".md"},
|
|
132
|
+
"description": "Generic AI startup marketing copy detected.",
|
|
133
|
+
"command": "Rewrite copy to be specific, concrete, and human. Describe what the product does, not what it 'unlocks'."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "MISSING_HOVER_STATES",
|
|
137
|
+
"pattern": re.compile(r'<button[^>]*(?!hover:)[^>]*className', re.IGNORECASE),
|
|
138
|
+
"tier": "T2",
|
|
139
|
+
"exts": {".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
140
|
+
"description": "Button element without hover: state detected.",
|
|
141
|
+
"command": "Add hover:, focus:, and active: states to all interactive elements."
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"id": "EMOJI_HEAVY_SLOP",
|
|
145
|
+
"pattern": re.compile(r'[\U0001f300-\U0001f9ff](?:.*[\U0001f300-\U0001f9ff]){5,}', re.DOTALL),
|
|
146
|
+
"tier": "T2",
|
|
147
|
+
"exts": {".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
148
|
+
"description": "Emoji-heavy UI detected (6+ emoji in one file) — common AI pattern.",
|
|
149
|
+
"command": "Replace decorative emoji with proper iconography or remove entirely. Keep emoji only in user content."
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"id": "OPACITY_ABUSE_SLOP",
|
|
153
|
+
"pattern": re.compile(r'(?:opacity-\d{1,2}|bg-.*?/\d{1,2})(?:.*(?:opacity-\d{1,2}|bg-.*?/\d{1,2})){4,}', re.DOTALL),
|
|
154
|
+
"tier": "T2",
|
|
155
|
+
"exts": {".tsx", ".jsx", ".html", ".svelte", ".vue"},
|
|
156
|
+
"description": "Excessive opacity/transparency usage detected (glassmorphism cousin).",
|
|
157
|
+
"command": "Use solid colors. Reserve transparency for overlays and modals only."
|
|
158
|
+
},
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
def analyze_file(filepath: Path) -> list[dict]:
|
|
162
|
+
"""Scan a single file against all slop rules."""
|
|
163
|
+
issues = []
|
|
164
|
+
ext = filepath.suffix.lower()
|
|
165
|
+
|
|
166
|
+
# Filter rules that apply to this file extension
|
|
167
|
+
applicable_rules = [r for r in RULES if ext in r["exts"]]
|
|
168
|
+
if not applicable_rules:
|
|
169
|
+
return issues
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
content = filepath.read_text(encoding="utf-8")
|
|
173
|
+
except (UnicodeDecodeError, OSError):
|
|
174
|
+
return issues # Skip binary or unreadable files
|
|
175
|
+
|
|
176
|
+
for rule in applicable_rules:
|
|
177
|
+
# Prevent spam by only flagging a rule once per file
|
|
178
|
+
if rule["pattern"].search(content):
|
|
179
|
+
issues.append({
|
|
180
|
+
"file": str(filepath.resolve()),
|
|
181
|
+
"tier": rule["tier"],
|
|
182
|
+
"issue": rule["description"],
|
|
183
|
+
"command": rule["command"]
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
return issues
|
|
187
|
+
|
|
188
|
+
def analyze_directory(root_path: str = ".", exclude_paths: list[str] | None = None,
|
|
189
|
+
zone_overrides: dict[str, str] | None = None) -> list[dict]:
|
|
190
|
+
"""Walk directory and return a flat list of all detected slop issues.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
root_path: Directory to scan.
|
|
194
|
+
exclude_paths: Additional directory names/paths to skip (from ``uidetox exclude``).
|
|
195
|
+
zone_overrides: File-to-zone mapping; files in 'vendor' or 'generated' zones are skipped.
|
|
196
|
+
"""
|
|
197
|
+
all_issues = []
|
|
198
|
+
root = Path(root_path).resolve()
|
|
199
|
+
|
|
200
|
+
# Merge user excludes with built-in ignore list
|
|
201
|
+
skip_dirs = set(IGNORE_DIRS)
|
|
202
|
+
if exclude_paths:
|
|
203
|
+
for ep in exclude_paths:
|
|
204
|
+
skip_dirs.add(ep.strip("/").split("/")[-1] if "/" in ep else ep)
|
|
205
|
+
|
|
206
|
+
# Build a set of absolute paths to skip via zone overrides
|
|
207
|
+
zone_skip: set[str] = set()
|
|
208
|
+
if zone_overrides:
|
|
209
|
+
for fpath, zone in zone_overrides.items():
|
|
210
|
+
if zone in ("vendor", "generated"):
|
|
211
|
+
zone_skip.add(str(Path(fpath).resolve()))
|
|
212
|
+
|
|
213
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
214
|
+
# Mutate dirnames in-place to skip IGNORE_DIRS + user excludes
|
|
215
|
+
dirnames[:] = [d for d in dirnames if d not in skip_dirs and not d.startswith('.')]
|
|
216
|
+
|
|
217
|
+
for filename in filenames:
|
|
218
|
+
file_path = Path(dirpath) / filename
|
|
219
|
+
|
|
220
|
+
# Respect zone overrides
|
|
221
|
+
if zone_skip and str(file_path.resolve()) in zone_skip:
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
found_issues = analyze_file(file_path)
|
|
225
|
+
all_issues.extend(found_issues)
|
|
226
|
+
|
|
227
|
+
return all_issues
|
uidetox/cli.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""CLI entry point for UIdetox."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_version() -> str:
|
|
10
|
+
"""Return package version."""
|
|
11
|
+
try:
|
|
12
|
+
from uidetox import __version__
|
|
13
|
+
return __version__
|
|
14
|
+
except ImportError:
|
|
15
|
+
return "0.1.0"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_commands_dir() -> Path | None:
|
|
19
|
+
"""Find the commands/ directory next to the package or project root."""
|
|
20
|
+
try:
|
|
21
|
+
from uidetox.state import get_project_root
|
|
22
|
+
cmd_dir = get_project_root() / "commands"
|
|
23
|
+
if cmd_dir.exists():
|
|
24
|
+
return cmd_dir
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
# Fallback: installed package location
|
|
28
|
+
pkg_root = Path(__file__).resolve().parent.parent
|
|
29
|
+
cmd_dir = pkg_root / "commands"
|
|
30
|
+
if cmd_dir.exists():
|
|
31
|
+
return cmd_dir
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_args(args_list=None):
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
prog="uidetox",
|
|
38
|
+
description="UIdetox — agent harness to eliminate AI slop and enforce code quality across frontend, backend, and database layers."
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {_get_version()}")
|
|
41
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
42
|
+
|
|
43
|
+
# Command: setup
|
|
44
|
+
setup_parser = subparsers.add_parser("setup", help="Gather project design context and configure dials")
|
|
45
|
+
setup_parser.add_argument("--auto-commit", action="store_true", help="Enable automated git commits for resolved issues")
|
|
46
|
+
|
|
47
|
+
# Command: scan
|
|
48
|
+
scan_parser = subparsers.add_parser("scan", help="Full diagnostic audit of frontend interface quality")
|
|
49
|
+
scan_parser.add_argument("--path", default=".", help="Directory to scan")
|
|
50
|
+
|
|
51
|
+
# Command: add-issue (For agent use during scan)
|
|
52
|
+
add_parser = subparsers.add_parser("add-issue", help="Add an issue to the state queue (Agent use)")
|
|
53
|
+
add_parser.add_argument("--file", required=True, help="File path with the issue")
|
|
54
|
+
add_parser.add_argument("--tier", required=True, choices=["T1", "T2", "T3", "T4"], help="Severity tier")
|
|
55
|
+
add_parser.add_argument("--issue", required=True, help="Description of the issue")
|
|
56
|
+
add_parser.add_argument("--fix-command", required=True, help="Suggested command to fix")
|
|
57
|
+
|
|
58
|
+
# Command: next
|
|
59
|
+
next_parser = subparsers.add_parser("next", help="Picks the next highest-priority issue from the scan queue")
|
|
60
|
+
|
|
61
|
+
# Command: resolve
|
|
62
|
+
resolve_parser = subparsers.add_parser("resolve", help="Mark a specific issue as resolved")
|
|
63
|
+
resolve_parser.add_argument("issue_id", help="The ID of the issue to resolve (e.g. SCAN-001)")
|
|
64
|
+
resolve_parser.add_argument("--note", required=True, help="Mandatory explanation of the fix applied")
|
|
65
|
+
|
|
66
|
+
# Command: plan
|
|
67
|
+
plan_parser = subparsers.add_parser("plan", help="Reorder priorities or cluster related issues in the queue")
|
|
68
|
+
|
|
69
|
+
# Command: review
|
|
70
|
+
review_parser = subparsers.add_parser("review", help="Subjective UX review of the latest changes")
|
|
71
|
+
|
|
72
|
+
# Command: update-skill
|
|
73
|
+
update_parser = subparsers.add_parser("update-skill", help="Installs UIdetox rules into your agent's configuration")
|
|
74
|
+
update_parser.add_argument("agent", choices=["claude", "cursor", "gemini", "codex", "windsurf", "copilot"], help="Target AI agent")
|
|
75
|
+
|
|
76
|
+
# Command: status
|
|
77
|
+
status_parser = subparsers.add_parser("status", help="Show project health dashboard with design score")
|
|
78
|
+
status_parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
79
|
+
|
|
80
|
+
# Command: show
|
|
81
|
+
show_parser = subparsers.add_parser("show", help="Show details of issues, filter by file/tier/ID")
|
|
82
|
+
show_parser.add_argument("pattern", nargs="?", default=None, help="Filter by issue ID, file path, or tier (T1-T4)")
|
|
83
|
+
|
|
84
|
+
# Command: exclude
|
|
85
|
+
exclude_parser = subparsers.add_parser("exclude", help="Exclude a directory from scanning")
|
|
86
|
+
exclude_parser.add_argument("path", help="Path to exclude (e.g. node_modules, dist)")
|
|
87
|
+
|
|
88
|
+
# Command: autofix
|
|
89
|
+
autofix_parser = subparsers.add_parser("autofix", help="Batch-apply all safe T1 quick fixes")
|
|
90
|
+
autofix_parser.add_argument("--dry-run", action="store_true", help="Preview fixes without applying")
|
|
91
|
+
|
|
92
|
+
# Command: rescan
|
|
93
|
+
rescan_parser = subparsers.add_parser("rescan", help="Clear queue and re-scan the project fresh")
|
|
94
|
+
rescan_parser.add_argument("--path", default=".", help="Directory to rescan")
|
|
95
|
+
|
|
96
|
+
# Command: loop
|
|
97
|
+
loop_parser = subparsers.add_parser("loop", help="Instruct the AI agent to enter an autonomous fix loop")
|
|
98
|
+
loop_parser.add_argument("--target", type=int, default=95, help="Target design score to reach (default 95)")
|
|
99
|
+
loop_parser.add_argument("--orchestrator", action="store_true", help="Use sub-agent orchestrator mode (one agent per stage)")
|
|
100
|
+
|
|
101
|
+
# Command: subagent
|
|
102
|
+
sub_parser = subparsers.add_parser("subagent", help="Manage sub-agent sessions and generate stage prompts")
|
|
103
|
+
sub_parser.add_argument("--stage-prompt", type=str, help="Generate prompt for a stage (observe/diagnose/prioritize/fix/verify)")
|
|
104
|
+
sub_parser.add_argument("--parallel", type=int, default=1, help="Number of parallel sub-agents to shard the workload across")
|
|
105
|
+
sub_parser.add_argument("--list", action="store_true", help="List all sub-agent sessions")
|
|
106
|
+
sub_parser.add_argument("--show", type=str, help="Show details of a session by ID")
|
|
107
|
+
sub_parser.add_argument("--record", type=str, help="Mark a session as completed")
|
|
108
|
+
sub_parser.add_argument("--note", type=str, default="", help="Note for --record")
|
|
109
|
+
|
|
110
|
+
# Command: history
|
|
111
|
+
history_parser = subparsers.add_parser("history", help="View run history and score progression")
|
|
112
|
+
history_parser.add_argument("--full", action="store_true", help="Show full run details")
|
|
113
|
+
|
|
114
|
+
# Command: viz
|
|
115
|
+
viz_parser = subparsers.add_parser("viz", help="Generate an HTML treemap heatmap of codebase issues")
|
|
116
|
+
viz_parser.add_argument("--path", default=".", help="Directory to visualize")
|
|
117
|
+
viz_parser.set_defaults(viz_cmd="viz")
|
|
118
|
+
|
|
119
|
+
# Command: tree
|
|
120
|
+
tree_parser = subparsers.add_parser("tree", help="Print a terminal tree of codebase issues")
|
|
121
|
+
tree_parser.add_argument("--path", default=".", help="Directory to visualize")
|
|
122
|
+
tree_parser.add_argument("--depth", type=int, default=3, help="Max depth of the tree")
|
|
123
|
+
tree_parser.set_defaults(viz_cmd="tree")
|
|
124
|
+
|
|
125
|
+
# Command: zone
|
|
126
|
+
zone_parser = subparsers.add_parser("zone", help="Show, set, or clear file classifications (production, test, vendor, etc.)")
|
|
127
|
+
zone_parser.add_argument("zone_action", nargs="?", choices=["show", "set", "clear"], default="show", help="Action to perform")
|
|
128
|
+
zone_parser.add_argument("zone_path", nargs="?", help="File path to override")
|
|
129
|
+
zone_parser.add_argument("zone_value", nargs="?", help="Zone value (production, test, config, generated, script, vendor)")
|
|
130
|
+
|
|
131
|
+
# Command: suppress
|
|
132
|
+
suppress_parser = subparsers.add_parser("suppress", help="Permanently suppress issues matching a pattern")
|
|
133
|
+
suppress_parser.add_argument("pattern", nargs="?", help="Pattern to suppress (e.g., *vendor/* or 'Uses Inter font')")
|
|
134
|
+
suppress_parser.add_argument("--remove", action="store_true", help="Remove an existing suppression pattern")
|
|
135
|
+
|
|
136
|
+
# Command: memory
|
|
137
|
+
memory_parser = subparsers.add_parser("memory", help="Read or write to the persistent agent memory bank")
|
|
138
|
+
memory_parser.add_argument("memory_action", nargs="?", choices=["show", "pattern", "note", "clear"], default="show", help="Action to perform")
|
|
139
|
+
memory_parser.add_argument("value", nargs="?", help="The pattern or note string to save")
|
|
140
|
+
|
|
141
|
+
# Command: detect
|
|
142
|
+
detect_parser = subparsers.add_parser("detect", help="Auto-detect project tooling (linters, formatters, tsc, backend, db)")
|
|
143
|
+
detect_parser.add_argument("--path", default=".", help="Project root to scan")
|
|
144
|
+
|
|
145
|
+
# Command: check (master: tsc → lint → format)
|
|
146
|
+
check_parser = subparsers.add_parser("check", help="Run tsc → lint → format in sequence")
|
|
147
|
+
check_parser.add_argument("--fix", action="store_true", help="Auto-fix lint and format issues")
|
|
148
|
+
|
|
149
|
+
# Command: tsc
|
|
150
|
+
tsc_parser = subparsers.add_parser("tsc", help="Run TypeScript compiler and queue errors")
|
|
151
|
+
tsc_parser.add_argument("--fix", action="store_true", help="(reserved for future use)")
|
|
152
|
+
|
|
153
|
+
# Command: lint
|
|
154
|
+
lint_parser = subparsers.add_parser("lint", help="Run detected linter and queue errors")
|
|
155
|
+
lint_parser.add_argument("--fix", action="store_true", help="Auto-fix lint errors")
|
|
156
|
+
|
|
157
|
+
# Command: format
|
|
158
|
+
format_parser = subparsers.add_parser("format", help="Run detected formatter")
|
|
159
|
+
format_parser.add_argument("--fix", action="store_true", help="Auto-fix formatting")
|
|
160
|
+
|
|
161
|
+
# Dynamic Slash Commands (e.g., /audit, /polish) from the commands/ directory
|
|
162
|
+
cmd_dir = _get_commands_dir()
|
|
163
|
+
if cmd_dir:
|
|
164
|
+
for md_file in cmd_dir.glob("*.md"):
|
|
165
|
+
skill_name = md_file.stem
|
|
166
|
+
if skill_name not in ["scan", "setup", "fix"]:
|
|
167
|
+
skill_parser = subparsers.add_parser(skill_name, help=f"Execute the '{skill_name}' UX design skill")
|
|
168
|
+
skill_parser.add_argument("target", nargs="?", default=".", help="Target file, directory, or component pattern")
|
|
169
|
+
|
|
170
|
+
return parser.parse_args(args_list)
|
|
171
|
+
|
|
172
|
+
def main():
|
|
173
|
+
args = parse_args()
|
|
174
|
+
if not args.command:
|
|
175
|
+
# Just show help
|
|
176
|
+
parse_args(["--help"])
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Dispatch to the specific command module
|
|
180
|
+
try:
|
|
181
|
+
# Check dynamic skills
|
|
182
|
+
cmd_dir = _get_commands_dir()
|
|
183
|
+
dynamic_skills = []
|
|
184
|
+
if cmd_dir:
|
|
185
|
+
dynamic_skills = [f.stem for f in cmd_dir.glob("*.md") if f.stem not in ["scan", "setup", "fix"]]
|
|
186
|
+
|
|
187
|
+
if args.command in dynamic_skills:
|
|
188
|
+
command_name = "skill_cmd"
|
|
189
|
+
else:
|
|
190
|
+
command_name = args.command.replace("-", "_")
|
|
191
|
+
|
|
192
|
+
# Avoid collision with builtins and top-level modules
|
|
193
|
+
name_map = {
|
|
194
|
+
"format": "format_cmd",
|
|
195
|
+
"subagent": "subagent_cmd",
|
|
196
|
+
"history": "history_cmd",
|
|
197
|
+
"memory": "memory_cmd",
|
|
198
|
+
"tree": "viz" # route 'tree' to 'viz.py'
|
|
199
|
+
}
|
|
200
|
+
command_name = name_map.get(command_name, command_name)
|
|
201
|
+
module = import_module(f"uidetox.commands.{command_name}")
|
|
202
|
+
|
|
203
|
+
if hasattr(module, "run"):
|
|
204
|
+
# Pass args to the command runner
|
|
205
|
+
module.run(args)
|
|
206
|
+
else:
|
|
207
|
+
print(f"Error: Command module for '{args.command}' lacks a run() function.", file=sys.stderr)
|
|
208
|
+
sys.exit(1)
|
|
209
|
+
|
|
210
|
+
except KeyboardInterrupt:
|
|
211
|
+
print("\n\nInterrupted. Run 'uidetox status' to see where you left off.")
|
|
212
|
+
sys.exit(130)
|
|
213
|
+
except ImportError as e:
|
|
214
|
+
print(f"Error: Command '{args.command}' is not implemented yet. ({e})", file=sys.stderr)
|
|
215
|
+
sys.exit(1)
|
|
216
|
+
except Exception as e:
|
|
217
|
+
print(f"An error occurred: {e}", file=sys.stderr)
|
|
218
|
+
sys.exit(1)
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Commands package initialization."""
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Add issue command."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import fnmatch
|
|
5
|
+
import uuid
|
|
6
|
+
from uidetox.state import add_issue, load_config
|
|
7
|
+
|
|
8
|
+
def _is_suppressed(file_path: str, description: str, patterns: list[str]) -> bool:
|
|
9
|
+
"""Check if this issue matches any active suppress pattern."""
|
|
10
|
+
for pattern in patterns:
|
|
11
|
+
if fnmatch.fnmatch(file_path, pattern) or fnmatch.fnmatch(file_path, f"*{pattern}*"):
|
|
12
|
+
return True
|
|
13
|
+
if fnmatch.fnmatch(description, pattern) or fnmatch.fnmatch(description, f"*{pattern}*"):
|
|
14
|
+
return True
|
|
15
|
+
if pattern.lower() in file_path.lower() or pattern.lower() in description.lower():
|
|
16
|
+
return True
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
def run(args: argparse.Namespace):
|
|
20
|
+
config = load_config()
|
|
21
|
+
ignore_patterns = config.get("ignore_patterns", [])
|
|
22
|
+
|
|
23
|
+
if ignore_patterns and _is_suppressed(args.file, args.issue, ignore_patterns):
|
|
24
|
+
print(f"Suppressed: [{args.tier}] {args.issue} in {args.file} (matches active ignore pattern)")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
issue_id = f"SCAN-{str(uuid.uuid4())[:6].upper()}"
|
|
28
|
+
new_issue = {
|
|
29
|
+
"id": issue_id,
|
|
30
|
+
"file": args.file,
|
|
31
|
+
"tier": args.tier,
|
|
32
|
+
"issue": args.issue,
|
|
33
|
+
"command": args.fix_command
|
|
34
|
+
}
|
|
35
|
+
add_issue(new_issue)
|
|
36
|
+
print(f"Added issue {issue_id}: [{args.tier}] {args.issue} in {args.file}")
|
|
37
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Autofix command: automatically apply safe T1 fixes."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from uidetox.state import load_state, save_state, load_config
|
|
5
|
+
|
|
6
|
+
def run(args: argparse.Namespace):
|
|
7
|
+
state = load_state()
|
|
8
|
+
issues = state.get("issues", [])
|
|
9
|
+
|
|
10
|
+
t1_issues = [i for i in issues if i.get("tier") == "T1"]
|
|
11
|
+
|
|
12
|
+
if not t1_issues:
|
|
13
|
+
print("No T1 (quick fix) issues found. Nothing to autofix.")
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
dry_run = getattr(args, "dry_run", False)
|
|
17
|
+
|
|
18
|
+
print("==============================")
|
|
19
|
+
print(" UIdetox Autofix")
|
|
20
|
+
print("==============================")
|
|
21
|
+
print(f"Found {len(t1_issues)} T1 issue(s) eligible for autofix:\n")
|
|
22
|
+
|
|
23
|
+
for issue in t1_issues:
|
|
24
|
+
print(f" [{issue['id']}] {issue['file']}: {issue['issue']}")
|
|
25
|
+
print(f" → Fix: {issue.get('command', 'manual')}")
|
|
26
|
+
|
|
27
|
+
if dry_run:
|
|
28
|
+
print(f"\n[DRY RUN] No changes applied. Remove --dry-run to apply.")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
config = load_config()
|
|
32
|
+
auto_commit = config.get("auto_commit", False)
|
|
33
|
+
|
|
34
|
+
print(f"\n[AGENT INSTRUCTION]")
|
|
35
|
+
print(f"Apply all {len(t1_issues)} T1 fixes listed above. For each:")
|
|
36
|
+
print(f" 1. Open the file")
|
|
37
|
+
print(f" 2. Apply the fix described above")
|
|
38
|
+
print(f" 3. Run `uidetox resolve <issue_id> --note \"what you changed\"` when done")
|
|
39
|
+
if auto_commit:
|
|
40
|
+
print(f"\n 📦 AUTO-COMMIT is ON — each `resolve` will atomically commit the fix to git.")
|
|
41
|
+
print(f"\nThese are safe, mechanical changes (font swaps, color replacements, spacing).")
|
|
42
|
+
print(f"Apply them all before moving to T2+ issues.")
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Check command: runs tsc → lint → format in sequence."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import subprocess
|
|
5
|
+
from uidetox.tooling import detect_all
|
|
6
|
+
from uidetox.state import load_config, save_config
|
|
7
|
+
from uidetox.commands import tsc as tsc_cmd
|
|
8
|
+
from uidetox.commands import lint as lint_cmd
|
|
9
|
+
from uidetox.commands import format_cmd
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(args: argparse.Namespace):
|
|
13
|
+
# First, ensure tooling is detected
|
|
14
|
+
config = load_config()
|
|
15
|
+
if not config.get("tooling"):
|
|
16
|
+
profile = detect_all()
|
|
17
|
+
config["tooling"] = profile.to_dict()
|
|
18
|
+
save_config(config)
|
|
19
|
+
print("Auto-detected project tooling.\n")
|
|
20
|
+
|
|
21
|
+
tooling = config.get("tooling", {})
|
|
22
|
+
|
|
23
|
+
print("╔══════════════════════════════╗")
|
|
24
|
+
print("║ UIdetox Mechanical Check ║")
|
|
25
|
+
print("╚══════════════════════════════╝")
|
|
26
|
+
print()
|
|
27
|
+
|
|
28
|
+
fix = getattr(args, "fix", False)
|
|
29
|
+
steps_run = 0
|
|
30
|
+
|
|
31
|
+
if fix and (tooling.get("linter") or tooling.get("formatter")):
|
|
32
|
+
print("━━━ Phase 1: Iterative Auto-Fix ━━━")
|
|
33
|
+
for iteration in range(1, 4):
|
|
34
|
+
print(f"Iteration {iteration}...")
|
|
35
|
+
changed = False
|
|
36
|
+
|
|
37
|
+
if tooling.get("formatter"):
|
|
38
|
+
cmd = tooling["formatter"].get("fix_cmd")
|
|
39
|
+
if cmd:
|
|
40
|
+
try:
|
|
41
|
+
res = subprocess.run(cmd.split(), capture_output=True, text=True, cwd=".")
|
|
42
|
+
# If formatter changed files, it usually outputs file names or has exit code
|
|
43
|
+
if res.returncode != 0 or "fixed" in res.stdout.lower() or "formatted" in res.stdout.lower():
|
|
44
|
+
changed = True
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
print(f"Warning: Formatter command not found ({cmd})")
|
|
47
|
+
|
|
48
|
+
if tooling.get("linter"):
|
|
49
|
+
cmd = tooling["linter"].get("fix_cmd")
|
|
50
|
+
if cmd:
|
|
51
|
+
try:
|
|
52
|
+
res = subprocess.run(cmd.split(), capture_output=True, text=True, cwd=".")
|
|
53
|
+
# If linter fixed files, it might still have exit code 1 if some remain
|
|
54
|
+
# We assume it changed things if the output mentions fixes, or just run max 3 times anyway
|
|
55
|
+
if "fixed" in res.stdout.lower() or "fixed" in res.stderr.lower():
|
|
56
|
+
changed = True
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
print(f"Warning: Linter command not found ({cmd})")
|
|
59
|
+
|
|
60
|
+
if not changed:
|
|
61
|
+
print("Code is clean or no more auto-fixes available.\n")
|
|
62
|
+
break
|
|
63
|
+
print("Auto-fix phase complete.\n")
|
|
64
|
+
|
|
65
|
+
print("━━━ Phase 2: Diagnostic Checks ━━━")
|
|
66
|
+
|
|
67
|
+
# Step 1: TypeScript
|
|
68
|
+
if tooling.get("typescript"):
|
|
69
|
+
print(" Running TypeScript check...")
|
|
70
|
+
tsc_args = argparse.Namespace(fix=False)
|
|
71
|
+
tsc_cmd.run(tsc_args)
|
|
72
|
+
steps_run += 1
|
|
73
|
+
print()
|
|
74
|
+
else:
|
|
75
|
+
print(" TypeScript: skipped (not detected)\n")
|
|
76
|
+
|
|
77
|
+
# Step 2: Lint
|
|
78
|
+
if tooling.get("linter"):
|
|
79
|
+
print(" Running Linter check...")
|
|
80
|
+
lint_args = argparse.Namespace(fix=False)
|
|
81
|
+
lint_cmd.run(lint_args)
|
|
82
|
+
steps_run += 1
|
|
83
|
+
print()
|
|
84
|
+
else:
|
|
85
|
+
print(" Linter: skipped (not detected)\n")
|
|
86
|
+
|
|
87
|
+
# Step 3: Format
|
|
88
|
+
if tooling.get("formatter"):
|
|
89
|
+
print(" Running Formatter check...")
|
|
90
|
+
fmt_args = argparse.Namespace(fix=False)
|
|
91
|
+
format_cmd.run(fmt_args)
|
|
92
|
+
steps_run += 1
|
|
93
|
+
print()
|
|
94
|
+
else:
|
|
95
|
+
print(" Formatter: skipped (not detected)\n")
|
|
96
|
+
|
|
97
|
+
if steps_run == 0:
|
|
98
|
+
print("No tooling detected. Run 'uidetox detect' to configure tooling.")
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
102
|
+
print(f"Ran {steps_run} mechanical check(s).")
|
|
103
|
+
print("Run 'uidetox status' to see the updated health score.")
|
|
104
|
+
print("Run 'uidetox next' to start fixing any queued issues.")
|