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 ADDED
@@ -0,0 +1,3 @@
1
+ """UIdetox Python Package"""
2
+
3
+ __version__ = "0.1.0"
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.")