Redesign around web-based Claude Code workflow
Instead of a CLI the user runs separately, this is now two files you drop into any repo: - CLAUDE.md: read automatically by Claude Code, teaches it to scan first, commit often, and leave handoff notes when usage runs low - scan.py: standalone script Claude runs itself to find errors via linters/tests/type-checkers, using zero AI tokens No workflow change for the user — they type in Claude Code as before. https://claude.ai/code/session_01Fzv8baXnEVVhnrffAb3Ucc
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fast codebase scanner — finds errors using linters, tests, and type checkers.
|
||||
|
||||
Claude Code runs this script itself via bash. It uses zero AI tokens because
|
||||
it's just running your project's existing tools and collecting the output.
|
||||
|
||||
Usage (run by Claude Code, not by you):
|
||||
python scan.py [project_root]
|
||||
|
||||
Outputs a structured report to stdout that Claude can read and act on.
|
||||
Auto-detects project type: Python, Node/TypeScript, Rust, Go.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run(cmd, cwd, timeout=120):
|
||||
"""Run a command, return (exit_code, output)."""
|
||||
try:
|
||||
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
|
||||
out = r.stdout
|
||||
if r.stderr:
|
||||
out += "\n" + r.stderr
|
||||
return r.returncode, out.strip()
|
||||
except subprocess.TimeoutExpired:
|
||||
return -1, f"TIMEOUT after {timeout}s: {' '.join(cmd)}"
|
||||
except FileNotFoundError:
|
||||
return -1, ""
|
||||
|
||||
|
||||
def extract_files(output, root):
|
||||
"""Pull file paths from linter output like 'path/file.py:10: error'."""
|
||||
files = set()
|
||||
for line in output.splitlines():
|
||||
if ":" in line:
|
||||
candidate = line.split(":")[0].strip().lstrip("./")
|
||||
if candidate and (root / candidate).exists():
|
||||
files.add(candidate)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def scan_python(root):
|
||||
"""Run Python checks: pytest, ruff, mypy, syntax."""
|
||||
sections = []
|
||||
|
||||
if not any((root / f).exists() for f in ["pyproject.toml", "setup.py", "requirements.txt"]):
|
||||
return sections
|
||||
|
||||
# pytest
|
||||
for cmd in [["python", "-m", "pytest", "--tb=short", "-q"], ["pytest", "--tb=short", "-q"]]:
|
||||
code, out = run(cmd, root)
|
||||
if code == -1 and not out:
|
||||
continue
|
||||
if code != 0 and out:
|
||||
sections.append(("Test Failures", out))
|
||||
break
|
||||
|
||||
# ruff
|
||||
if shutil.which("ruff"):
|
||||
code, out = run(["ruff", "check", "."], root)
|
||||
if code != 0 and out:
|
||||
sections.append(("Lint Errors (ruff)", out))
|
||||
|
||||
# mypy
|
||||
if shutil.which("mypy"):
|
||||
code, out = run(["mypy", ".", "--no-error-summary"], root, timeout=180)
|
||||
if code != 0 and out:
|
||||
sections.append(("Type Errors (mypy)", out))
|
||||
|
||||
# Syntax errors
|
||||
syntax_errors = []
|
||||
for py_file in list(root.rglob("*.py"))[:500]:
|
||||
rel = py_file.relative_to(root)
|
||||
if any(p in {".venv", "venv", "node_modules", "__pycache__", ".git", ".tox"} for p in rel.parts):
|
||||
continue
|
||||
code, out = run(["python", "-c", f"import ast; ast.parse(open('{py_file}').read())"], root)
|
||||
if code != 0:
|
||||
syntax_errors.append(f"{rel}: SyntaxError - {out.splitlines()[-1] if out else 'unknown'}")
|
||||
if syntax_errors:
|
||||
sections.append(("Syntax Errors", "\n".join(syntax_errors)))
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
def scan_node(root):
|
||||
"""Run Node/TypeScript checks: test, eslint, tsc."""
|
||||
sections = []
|
||||
|
||||
if not (root / "package.json").exists():
|
||||
return sections
|
||||
|
||||
pkg = "yarn" if (root / "yarn.lock").exists() else "npm"
|
||||
|
||||
code, out = run([pkg, "test", "--", "--reporter=min"], root, timeout=180)
|
||||
if code != 0 and out:
|
||||
sections.append(("Test Failures", out))
|
||||
|
||||
if shutil.which("npx"):
|
||||
code, out = run(["npx", "eslint", ".", "--format=compact", "--no-warn-ignored"], root, timeout=120)
|
||||
if code != 0 and out:
|
||||
sections.append(("Lint Errors (eslint)", out))
|
||||
|
||||
if (root / "tsconfig.json").exists():
|
||||
code, out = run(["npx", "tsc", "--noEmit"], root, timeout=180)
|
||||
if code != 0 and out:
|
||||
sections.append(("TypeScript Errors", out))
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
def scan_rust(root):
|
||||
"""Run Rust checks: cargo check, cargo test --no-run."""
|
||||
sections = []
|
||||
|
||||
if not (root / "Cargo.toml").exists():
|
||||
return sections
|
||||
|
||||
code, out = run(["cargo", "check", "--message-format=short"], root, timeout=300)
|
||||
if code != 0 and out:
|
||||
sections.append(("Build Errors (cargo)", out))
|
||||
|
||||
code, out = run(["cargo", "test", "--no-run"], root, timeout=300)
|
||||
if code != 0 and out:
|
||||
sections.append(("Test Compilation Errors", out))
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
def scan_go(root):
|
||||
"""Run Go checks: build, vet, test."""
|
||||
sections = []
|
||||
|
||||
if not (root / "go.mod").exists():
|
||||
return sections
|
||||
|
||||
code, out = run(["go", "build", "./..."], root, timeout=180)
|
||||
if code != 0 and out:
|
||||
sections.append(("Build Errors (go)", out))
|
||||
|
||||
code, out = run(["go", "vet", "./..."], root, timeout=120)
|
||||
if code != 0 and out:
|
||||
sections.append(("Vet Warnings (go)", out))
|
||||
|
||||
code, out = run(["go", "test", "-short", "-count=1", "./..."], root, timeout=180)
|
||||
if code != 0 and out:
|
||||
sections.append(("Test Failures (go)", out))
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
def main():
|
||||
root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
|
||||
|
||||
if not root.is_dir():
|
||||
print(f"Error: {root} is not a directory", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
all_sections = []
|
||||
all_sections.extend(scan_python(root))
|
||||
all_sections.extend(scan_node(root))
|
||||
all_sections.extend(scan_rust(root))
|
||||
all_sections.extend(scan_go(root))
|
||||
|
||||
if not all_sections:
|
||||
print("SCAN COMPLETE: No errors found.")
|
||||
sys.exit(0)
|
||||
|
||||
# Collect all files with issues
|
||||
all_files = set()
|
||||
for title, output in all_sections:
|
||||
all_files.update(extract_files(output, root))
|
||||
|
||||
# Print structured report
|
||||
print(f"SCAN COMPLETE: Found issues in {len(all_sections)} categories across {len(all_files)} files.\n")
|
||||
|
||||
for title, output in all_sections:
|
||||
print(f"=== {title} ===")
|
||||
lines = output.splitlines()
|
||||
if len(lines) > 80:
|
||||
print("(truncated to last 80 lines)")
|
||||
print("\n".join(lines[-80:]))
|
||||
else:
|
||||
print(output)
|
||||
print()
|
||||
|
||||
if all_files:
|
||||
print("=== Files with Issues ===")
|
||||
for f in sorted(all_files):
|
||||
print(f" {f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user