litclaude-ai 0.2.2
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.
- package/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/README_ko-KR.md +374 -0
- package/RELEASE_CHECKLIST.md +165 -0
- package/bin/litclaude-ai.js +643 -0
- package/cover.png +0 -0
- package/docs/agents.md +67 -0
- package/docs/hooks.md +134 -0
- package/docs/lsp.md +40 -0
- package/docs/migration.md +209 -0
- package/docs/workflow-compatibility-audit.md +119 -0
- package/generate_cover.py +123 -0
- package/package.json +48 -0
- package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
- package/plugins/litclaude/.lsp.json +13 -0
- package/plugins/litclaude/.mcp.json +9 -0
- package/plugins/litclaude/agents/boulder-executor.md +12 -0
- package/plugins/litclaude/agents/librarian-researcher.md +15 -0
- package/plugins/litclaude/agents/oracle-verifier.md +16 -0
- package/plugins/litclaude/agents/prometheus-planner.md +13 -0
- package/plugins/litclaude/agents/qa-runner.md +16 -0
- package/plugins/litclaude/agents/quality-reviewer.md +17 -0
- package/plugins/litclaude/bin/litclaude-hook.js +110 -0
- package/plugins/litclaude/bin/litclaude-hud.js +271 -0
- package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
- package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
- package/plugins/litclaude/commands/deep-interview.md +21 -0
- package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
- package/plugins/litclaude/commands/lit-loop.md +40 -0
- package/plugins/litclaude/commands/lit-plan.md +35 -0
- package/plugins/litclaude/commands/litgoal.md +30 -0
- package/plugins/litclaude/commands/review-work.md +35 -0
- package/plugins/litclaude/commands/start-work.md +36 -0
- package/plugins/litclaude/hooks/hooks.json +54 -0
- package/plugins/litclaude/lib/context-pressure.mjs +25 -0
- package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
- package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
- package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
- package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
- package/plugins/litclaude/lib/workflow-check.mjs +83 -0
- package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
- package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
- package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
- package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
- package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
- package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
- package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
- package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
- package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
- package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
- package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
- package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
- package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
- package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
- package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
- package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
- package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
- package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
- package/plugins/litclaude/skills/programming/SKILL.md +106 -0
- package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
- package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
- package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
- package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
- package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
- package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
- package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
- package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
- package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
- package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
- package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
- package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
- package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
- package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
- package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
- package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
- package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
- package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
- package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
- package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
- package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
- package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
- package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
- package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
- package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
- package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
- package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
- package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
- package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
- package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
- package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
- package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
- package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
- package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
- package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
- package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
- package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
- package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
- package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
- package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
- package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
- package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
- package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
- package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
- package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
- package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
- package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
- package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
- package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
- package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
- package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
- package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
- package/plugins/litclaude/skills/rules/SKILL.md +66 -0
- package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
- package/scripts/audit-plan-checkboxes.mjs +37 -0
- package/scripts/doctor.mjs +41 -0
- package/scripts/inspect-agent-tools.mjs +27 -0
- package/scripts/postinstall.mjs +50 -0
- package/scripts/qa-claude-plugin-smoke.sh +60 -0
- package/scripts/qa-portable-install.sh +136 -0
- package/scripts/validate-plugin.mjs +72 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "typer",
|
|
6
|
+
# "rich",
|
|
7
|
+
# ]
|
|
8
|
+
# ///
|
|
9
|
+
|
|
10
|
+
# ─── How to run ───
|
|
11
|
+
# 1. Install uv (if not installed):
|
|
12
|
+
# curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
13
|
+
# 2. Run:
|
|
14
|
+
# uv run new-project.py myproject
|
|
15
|
+
# uv run new-project.py myproject --path ./workspace
|
|
16
|
+
# uv run new-project.py myproject --lib # library (publishable)
|
|
17
|
+
# ──────────────────
|
|
18
|
+
|
|
19
|
+
"""Scaffold a new Python project with ultra-strict config from pyproject-strict.md.
|
|
20
|
+
|
|
21
|
+
Creates via `uv init`, then injects basedpyright + ruff ALL + pytest config.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
import typer
|
|
31
|
+
from rich import print as rprint
|
|
32
|
+
|
|
33
|
+
# ── Strict tool config (from pyproject-strict.md) ──
|
|
34
|
+
|
|
35
|
+
TOOL_CONFIG = '''
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"basedpyright>=1.21",
|
|
39
|
+
"ruff>=0.8",
|
|
40
|
+
"pytest>=8",
|
|
41
|
+
"pytest-cov>=5",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.basedpyright]
|
|
45
|
+
typeCheckingMode = "all"
|
|
46
|
+
pythonVersion = "3.13"
|
|
47
|
+
reportMissingTypeStubs = false
|
|
48
|
+
reportUnknownMemberType = false
|
|
49
|
+
reportUnknownArgumentType = false
|
|
50
|
+
reportUnknownVariableType = false
|
|
51
|
+
reportUnknownLambdaType = false
|
|
52
|
+
reportUnknownParameterType = false
|
|
53
|
+
reportMissingParameterType = false
|
|
54
|
+
reportUnnecessaryIsInstance = false
|
|
55
|
+
reportUnusedCallResult = false
|
|
56
|
+
reportImplicitOverride = false
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
target-version = "py313"
|
|
60
|
+
line-length = 120
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = ["ALL"]
|
|
64
|
+
ignore = [
|
|
65
|
+
"COM812", # trailing comma (conflicts with formatter)
|
|
66
|
+
"ISC001", # single-line string concat (conflicts with formatter)
|
|
67
|
+
"D1", # undocumented-public-* (too noisy early on)
|
|
68
|
+
"ANN101", # deprecated: self annotation
|
|
69
|
+
"ANN102", # deprecated: cls annotation
|
|
70
|
+
"S101", # assert used (pytest needs it)
|
|
71
|
+
"PLR2004", # magic-value-comparison (test data)
|
|
72
|
+
"FBT", # boolean-trap (too strict for CLIs)
|
|
73
|
+
"TD", # flake8-todos (noisy)
|
|
74
|
+
"FIX", # fixme (noisy)
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint.per-file-ignores]
|
|
78
|
+
"tests/**/*.py" = ["S101", "PLR2004", "SLF001", "D", "ARG", "ANN"]
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint.pydocstyle]
|
|
81
|
+
convention = "google"
|
|
82
|
+
|
|
83
|
+
[tool.pytest.ini_options]
|
|
84
|
+
testpaths = ["tests"]
|
|
85
|
+
addopts = "-ra --strict-markers --strict-config"
|
|
86
|
+
'''
|
|
87
|
+
|
|
88
|
+
GITIGNORE = """\
|
|
89
|
+
__pycache__/
|
|
90
|
+
*.py[cod]
|
|
91
|
+
*.so
|
|
92
|
+
.venv/
|
|
93
|
+
dist/
|
|
94
|
+
*.egg-info/
|
|
95
|
+
.coverage
|
|
96
|
+
htmlcov/
|
|
97
|
+
.basedpyright/
|
|
98
|
+
.ruff_cache/
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(
|
|
103
|
+
name: str = typer.Argument(help="Project name"),
|
|
104
|
+
path: Path = typer.Option(Path("."), "--path", "-p", help="Parent directory"),
|
|
105
|
+
lib: bool = typer.Option(False, "--lib", help="Create as publishable library (uv init --lib)"),
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Create a new Python project with ultra-strict config."""
|
|
108
|
+
project_dir = path / name
|
|
109
|
+
|
|
110
|
+
if project_dir.exists():
|
|
111
|
+
rprint(f"[red]Error:[/red] {project_dir} already exists")
|
|
112
|
+
raise SystemExit(1)
|
|
113
|
+
|
|
114
|
+
# Run uv init
|
|
115
|
+
cmd = ["uv", "init", "--lib" if lib else "--app", str(project_dir)]
|
|
116
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
117
|
+
if result.returncode != 0:
|
|
118
|
+
rprint(f"[red]uv init failed:[/red] {result.stderr}")
|
|
119
|
+
raise SystemExit(1)
|
|
120
|
+
|
|
121
|
+
# Read existing pyproject.toml
|
|
122
|
+
pyproject_path = project_dir / "pyproject.toml"
|
|
123
|
+
content = pyproject_path.read_text()
|
|
124
|
+
|
|
125
|
+
# Remove the default [dependency-groups] if uv init created one
|
|
126
|
+
# (we'll replace it with our strict version)
|
|
127
|
+
lines = content.splitlines(keepends=True)
|
|
128
|
+
filtered: list[str] = []
|
|
129
|
+
skip = False
|
|
130
|
+
for line in lines:
|
|
131
|
+
if line.strip().startswith("[dependency-groups]"):
|
|
132
|
+
skip = True
|
|
133
|
+
continue
|
|
134
|
+
if skip and line.strip().startswith("["):
|
|
135
|
+
skip = False
|
|
136
|
+
if not skip:
|
|
137
|
+
filtered.append(line)
|
|
138
|
+
|
|
139
|
+
content = "".join(filtered).rstrip("\n") + "\n"
|
|
140
|
+
|
|
141
|
+
# Append strict tool config
|
|
142
|
+
content += TOOL_CONFIG
|
|
143
|
+
|
|
144
|
+
pyproject_path.write_text(content)
|
|
145
|
+
|
|
146
|
+
# Add dev dependencies
|
|
147
|
+
subprocess.run(
|
|
148
|
+
["uv", "add", "--dev", "basedpyright", "ruff", "pytest", "pytest-cov"],
|
|
149
|
+
cwd=project_dir,
|
|
150
|
+
capture_output=True,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Create tests directory
|
|
154
|
+
tests_dir = project_dir / "tests"
|
|
155
|
+
tests_dir.mkdir(exist_ok=True)
|
|
156
|
+
(tests_dir / "__init__.py").touch()
|
|
157
|
+
|
|
158
|
+
# Overwrite .gitignore
|
|
159
|
+
(project_dir / ".gitignore").write_text(GITIGNORE)
|
|
160
|
+
|
|
161
|
+
# Create py.typed marker for libraries
|
|
162
|
+
if lib:
|
|
163
|
+
src_dir = project_dir / "src" / name.replace("-", "_")
|
|
164
|
+
if src_dir.exists():
|
|
165
|
+
(src_dir / "py.typed").touch()
|
|
166
|
+
|
|
167
|
+
rprint(f"[green]✓[/green] Created: [bold]{project_dir}[/bold]")
|
|
168
|
+
rprint(f" cd {name} && uv sync && uv run basedpyright . && uv run ruff check .")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
typer.run(main)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "typer",
|
|
6
|
+
# "rich",
|
|
7
|
+
# ]
|
|
8
|
+
# ///
|
|
9
|
+
|
|
10
|
+
# ─── How to run ───
|
|
11
|
+
# 1. Install uv (if not installed):
|
|
12
|
+
# curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
13
|
+
# 2. Run:
|
|
14
|
+
# uv run new-script.py my_tool
|
|
15
|
+
# uv run new-script.py my_tool --output ./scripts/my_tool.py
|
|
16
|
+
# uv run new-script.py my_tool --deps 'httpx2[http2,brotli,zstd]' --deps rich --deps polars
|
|
17
|
+
# uv run new-script.py my_tool --py 3.13
|
|
18
|
+
# ──────────────────
|
|
19
|
+
|
|
20
|
+
"""Generate a PEP 723 Python script with all boilerplate pre-filled.
|
|
21
|
+
|
|
22
|
+
Creates a new .py file with:
|
|
23
|
+
- uv shebang
|
|
24
|
+
- PEP 723 inline metadata (requires-python + dependencies)
|
|
25
|
+
- Mandatory "How to run" comment block
|
|
26
|
+
- from __future__ import annotations
|
|
27
|
+
- main() + if __name__ guard
|
|
28
|
+
|
|
29
|
+
By default writes to a temp directory and prints the path.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
import stat
|
|
36
|
+
import sys
|
|
37
|
+
import tempfile
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
import typer
|
|
41
|
+
from rich import print as rprint
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
TEMPLATE = '''\
|
|
45
|
+
#!/usr/bin/env -S uv run --script
|
|
46
|
+
# /// script
|
|
47
|
+
# requires-python = ">={python_version}"
|
|
48
|
+
# dependencies = [
|
|
49
|
+
{deps_block}# ]
|
|
50
|
+
# ///
|
|
51
|
+
|
|
52
|
+
# ─── How to run ───
|
|
53
|
+
# 1. Install uv (if not installed):
|
|
54
|
+
# curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
55
|
+
# 2. Run directly (no venv, no pip install needed):
|
|
56
|
+
# uv run {filename} {args_hint}
|
|
57
|
+
# 3. Or make executable and run:
|
|
58
|
+
# chmod +x {filename} && ./{filename}
|
|
59
|
+
# ──────────────────
|
|
60
|
+
|
|
61
|
+
from __future__ import annotations
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> None:
|
|
65
|
+
"""TODO: implement."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
|
70
|
+
'''
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main(
|
|
74
|
+
name: str = typer.Argument(help="Script name (without .py extension)"),
|
|
75
|
+
output: Path | None = typer.Option(None, "--output", "-o", help="Output path. Default: OS temp directory."),
|
|
76
|
+
deps: list[str] = typer.Option([], "--deps", "-d", help="Dependencies to include (repeat --deps for each)."),
|
|
77
|
+
py: str = typer.Option("3.13", "--py", help="Minimum Python version."),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Generate a new PEP 723 script with all boilerplate pre-filled."""
|
|
80
|
+
filename = f"{name}.py" if not name.endswith(".py") else name
|
|
81
|
+
stem = filename.removesuffix(".py")
|
|
82
|
+
|
|
83
|
+
if output is not None:
|
|
84
|
+
dest = Path(output)
|
|
85
|
+
else:
|
|
86
|
+
tmp_dir = Path(tempfile.gettempdir()) / "uv-scripts"
|
|
87
|
+
tmp_dir.mkdir(exist_ok=True)
|
|
88
|
+
dest = tmp_dir / filename
|
|
89
|
+
|
|
90
|
+
dep_list = deps or []
|
|
91
|
+
if dep_list:
|
|
92
|
+
deps_block = "".join(f'# "{d}",\n' for d in dep_list)
|
|
93
|
+
else:
|
|
94
|
+
deps_block = '# # add deps here, e.g.: "httpx2[http2,brotli,zstd]"\n'
|
|
95
|
+
|
|
96
|
+
content = TEMPLATE.format(
|
|
97
|
+
python_version=py,
|
|
98
|
+
deps_block=deps_block,
|
|
99
|
+
filename=filename,
|
|
100
|
+
args_hint="",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
dest.write_text(content)
|
|
105
|
+
|
|
106
|
+
# Make executable on Unix
|
|
107
|
+
if sys.platform != "win32":
|
|
108
|
+
st = dest.stat()
|
|
109
|
+
dest.chmod(st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
110
|
+
|
|
111
|
+
rprint(f"[green]✓[/green] Created: [bold]{dest}[/bold]")
|
|
112
|
+
rprint(f" Run: [cyan]uv run {dest}[/cyan]")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
typer.run(main)
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
#
|
|
7
|
+
# How to run:
|
|
8
|
+
# uv run --script check-no-excuse-rules.py src/lib.rs src/main.rs
|
|
9
|
+
# uv run --script check-no-excuse-rules.py src/ # recursively finds .rs files
|
|
10
|
+
# uv run --script check-no-excuse-rules.py . # entire tree
|
|
11
|
+
#
|
|
12
|
+
# No-excuse rule checker for Rust files — Python rewrite of check-no-excuse-rules.sh.
|
|
13
|
+
# Only rules enforceable via pure text matching live here.
|
|
14
|
+
# Everything semantic is on clippy + miri + nextest.
|
|
15
|
+
#
|
|
16
|
+
# Rules:
|
|
17
|
+
# unwrap .unwrap() outside tests without // SAFE-UNWRAP:
|
|
18
|
+
# expect .expect() outside tests without // SAFE-EXPECT:
|
|
19
|
+
# placeholder-macro todo!/unimplemented!/unreachable!/unreachable_unchecked! in committed code
|
|
20
|
+
# box-dyn-error Box<dyn Error> in non-test code
|
|
21
|
+
# lib-panic panic!() in library code
|
|
22
|
+
# unsafe-no-safety unsafe { without // SAFETY: in preceding 5 lines
|
|
23
|
+
# unjustified-clippy-allow #[allow(clippy::...)] without // CLIPPY-ALLOW:
|
|
24
|
+
# narrowing-as-cast possible narrowing 'as' cast
|
|
25
|
+
#
|
|
26
|
+
# Opt-out: place the appropriate comment on the previous line:
|
|
27
|
+
# // SAFE-UNWRAP: <reason>
|
|
28
|
+
# // SAFE-EXPECT: <reason>
|
|
29
|
+
# // SAFETY: <reason> (for unsafe blocks, within 5 lines above)
|
|
30
|
+
# // CLIPPY-ALLOW: <reason>
|
|
31
|
+
#
|
|
32
|
+
# Test paths (exempt from unwrap/expect/placeholder/box-dyn-error/lib-panic):
|
|
33
|
+
# tests/, benches/, examples/, build.rs, *_test.rs, #[cfg(test)] regions
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import re
|
|
38
|
+
import sys
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Patterns (compiled once)
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
RE_UNWRAP = re.compile(r"\.unwrap\(\)")
|
|
46
|
+
RE_EXPECT = re.compile(r"\.expect\(")
|
|
47
|
+
RE_PLACEHOLDER = re.compile(r"\b(todo!|unimplemented!|unreachable!|unreachable_unchecked!)")
|
|
48
|
+
RE_BOX_DYN_ERROR = re.compile(r"Box<dyn\s+Error")
|
|
49
|
+
RE_PANIC = re.compile(r"\bpanic!\(")
|
|
50
|
+
RE_UNSAFE_BLOCK = re.compile(r"\bunsafe\s*\{")
|
|
51
|
+
RE_CLIPPY_ALLOW = re.compile(r"#\[allow\(clippy::")
|
|
52
|
+
RE_CFG_TEST = re.compile(r"#\[cfg\(test\)\]")
|
|
53
|
+
RE_SAFE_UNWRAP = re.compile(r"//\s*SAFE-UNWRAP:")
|
|
54
|
+
RE_SAFE_EXPECT = re.compile(r"//\s*SAFE-EXPECT:")
|
|
55
|
+
RE_SAFETY = re.compile(r"//\s*SAFETY:")
|
|
56
|
+
RE_CLIPPY_ALLOW_JUST = re.compile(r"//\s*CLIPPY-ALLOW:")
|
|
57
|
+
|
|
58
|
+
# Narrowing cast: (wider) as (narrower)
|
|
59
|
+
# Wider types that lose bits when cast to narrower targets.
|
|
60
|
+
# No leading \b — must match e.g. `999u64 as u32` where a digit precedes the type.
|
|
61
|
+
_WIDER = r"(?:u16|u32|u64|u128|usize|i16|i32|i64|i128|isize)"
|
|
62
|
+
_NARROWER = r"(?:u8|u16|u32|i8|i16|i32)"
|
|
63
|
+
RE_NARROWING_CAST = re.compile(
|
|
64
|
+
rf"{_WIDER}\s+as\s+{_NARROWER}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Test-path fragments.
|
|
68
|
+
_TEST_PATH_PARTS = {"tests", "benches", "examples"}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Helpers
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
violations = 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def report(file: str, line: int, rule: str, detail: str) -> None:
|
|
79
|
+
"""Emit a GitHub-Actions-compatible error annotation to stderr."""
|
|
80
|
+
global violations
|
|
81
|
+
print(f"::error file={file},line={line}::[{rule}] {detail}", file=sys.stderr)
|
|
82
|
+
violations += 1
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_test_path(path: Path) -> bool:
|
|
86
|
+
"""Return True if *path* is in a test/bench/example directory or is a test file."""
|
|
87
|
+
parts = path.parts
|
|
88
|
+
for part in parts:
|
|
89
|
+
if part in _TEST_PATH_PARTS:
|
|
90
|
+
return True
|
|
91
|
+
if path.name == "build.rs":
|
|
92
|
+
return True
|
|
93
|
+
if path.name.endswith("_test.rs"):
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def is_lib_path(file: Path) -> bool:
|
|
99
|
+
"""Heuristic: is this file library code (not main.rs, not src/bin/*)."""
|
|
100
|
+
parts = path_parts_str(file)
|
|
101
|
+
# Must live under src/
|
|
102
|
+
if "src" not in parts:
|
|
103
|
+
return False
|
|
104
|
+
if file.name == "main.rs":
|
|
105
|
+
return False
|
|
106
|
+
# src/bin/* is binary code
|
|
107
|
+
try:
|
|
108
|
+
src_idx = parts.index("src")
|
|
109
|
+
if src_idx + 1 < len(parts) and parts[src_idx + 1] == "bin":
|
|
110
|
+
return False
|
|
111
|
+
except ValueError:
|
|
112
|
+
return False
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def path_parts_str(p: Path) -> list[str]:
|
|
117
|
+
return list(p.parts)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def strip_line_comment(line: str) -> str:
|
|
121
|
+
"""Return the portion of *line* before any ``//`` line comment.
|
|
122
|
+
|
|
123
|
+
This is a crude heuristic — it does not handle ``//`` inside string
|
|
124
|
+
literals, but matches the behaviour of the bash version.
|
|
125
|
+
"""
|
|
126
|
+
idx = line.find("//")
|
|
127
|
+
if idx == -1:
|
|
128
|
+
return line
|
|
129
|
+
return line[:idx]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def collect_rs_files(args: list[str]) -> list[Path]:
|
|
133
|
+
"""Expand CLI arguments: files are kept as-is, directories are walked."""
|
|
134
|
+
result: list[Path] = []
|
|
135
|
+
for arg in args:
|
|
136
|
+
p = Path(arg)
|
|
137
|
+
if p.is_file():
|
|
138
|
+
if p.suffix == ".rs":
|
|
139
|
+
result.append(p)
|
|
140
|
+
elif p.is_dir():
|
|
141
|
+
result.extend(sorted(p.rglob("*.rs")))
|
|
142
|
+
# Ignore non-existent / non-.rs
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Main checker
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def check_file(file: Path) -> None:
|
|
151
|
+
in_test_file = is_test_path(file)
|
|
152
|
+
in_cfg_test = False
|
|
153
|
+
cfg_test_brace_depth = 0
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
lines = file.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
157
|
+
except OSError as exc:
|
|
158
|
+
print(f"warning: cannot read {file}: {exc}", file=sys.stderr)
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
for line_no_0, raw_line in enumerate(lines):
|
|
162
|
+
line_no = line_no_0 + 1 # 1-indexed
|
|
163
|
+
|
|
164
|
+
# --- #[cfg(test)] region tracker ---
|
|
165
|
+
if RE_CFG_TEST.search(raw_line):
|
|
166
|
+
in_cfg_test = True
|
|
167
|
+
cfg_test_brace_depth = 0
|
|
168
|
+
|
|
169
|
+
if in_cfg_test:
|
|
170
|
+
opens = raw_line.count("{")
|
|
171
|
+
closes = raw_line.count("}")
|
|
172
|
+
cfg_test_brace_depth += opens - closes
|
|
173
|
+
if cfg_test_brace_depth <= 0 and not RE_CFG_TEST.search(raw_line):
|
|
174
|
+
in_cfg_test = False
|
|
175
|
+
|
|
176
|
+
exempt = in_test_file or in_cfg_test
|
|
177
|
+
code_only = strip_line_comment(raw_line)
|
|
178
|
+
|
|
179
|
+
if not exempt:
|
|
180
|
+
# .unwrap()
|
|
181
|
+
if RE_UNWRAP.search(code_only):
|
|
182
|
+
prev = lines[line_no_0 - 1] if line_no_0 > 0 else ""
|
|
183
|
+
if not RE_SAFE_UNWRAP.search(prev):
|
|
184
|
+
report(
|
|
185
|
+
str(file), line_no, "unwrap",
|
|
186
|
+
".unwrap() outside tests - use ? / ok_or / pattern match "
|
|
187
|
+
"or annotate previous line with // SAFE-UNWRAP: <reason>",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# .expect(...)
|
|
191
|
+
if RE_EXPECT.search(code_only):
|
|
192
|
+
prev = lines[line_no_0 - 1] if line_no_0 > 0 else ""
|
|
193
|
+
if not RE_SAFE_EXPECT.search(prev):
|
|
194
|
+
report(
|
|
195
|
+
str(file), line_no, "expect",
|
|
196
|
+
".expect() outside tests - use ? or annotate previous "
|
|
197
|
+
"line with // SAFE-EXPECT: <reason>",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# todo!/unimplemented!/unreachable!/unreachable_unchecked!
|
|
201
|
+
if RE_PLACEHOLDER.search(code_only):
|
|
202
|
+
report(
|
|
203
|
+
str(file), line_no, "placeholder-macro",
|
|
204
|
+
"todo!/unimplemented!/unreachable! in committed code",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Box<dyn Error>
|
|
208
|
+
if RE_BOX_DYN_ERROR.search(code_only):
|
|
209
|
+
report(
|
|
210
|
+
str(file), line_no, "box-dyn-error",
|
|
211
|
+
"Box<dyn Error> in non-test code - use anyhow::Error (apps) "
|
|
212
|
+
"or thiserror enum (libs)",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# panic!() in library code
|
|
216
|
+
if is_lib_path(file) and RE_PANIC.search(code_only):
|
|
217
|
+
report(
|
|
218
|
+
str(file), line_no, "lib-panic",
|
|
219
|
+
"panic!() in library code - return Result",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# unsafe { without // SAFETY: — always enforced, even in tests
|
|
223
|
+
if RE_UNSAFE_BLOCK.search(code_only):
|
|
224
|
+
start = max(0, line_no_0 - 5)
|
|
225
|
+
window = "\n".join(lines[start : line_no_0 + 1])
|
|
226
|
+
if not RE_SAFETY.search(window):
|
|
227
|
+
report(
|
|
228
|
+
str(file), line_no, "unsafe-no-safety-comment",
|
|
229
|
+
"unsafe block without // SAFETY: comment in preceding 5 lines",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# #[allow(clippy::...)] without // CLIPPY-ALLOW: — always enforced
|
|
233
|
+
if RE_CLIPPY_ALLOW.search(code_only):
|
|
234
|
+
prev = lines[line_no_0 - 1] if line_no_0 > 0 else ""
|
|
235
|
+
if not RE_CLIPPY_ALLOW_JUST.search(prev):
|
|
236
|
+
report(
|
|
237
|
+
str(file), line_no, "unjustified-clippy-allow",
|
|
238
|
+
"#[allow(clippy::...)] without // CLIPPY-ALLOW: <reason> on "
|
|
239
|
+
"previous line",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Narrowing numeric `as` casts
|
|
243
|
+
if RE_NARROWING_CAST.search(code_only):
|
|
244
|
+
report(
|
|
245
|
+
str(file), line_no, "narrowing-as-cast",
|
|
246
|
+
"possible narrowing 'as' cast - use TryFrom / try_into() for "
|
|
247
|
+
"fallible conversion",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
# Entry point
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def main() -> None:
|
|
256
|
+
global violations
|
|
257
|
+
|
|
258
|
+
if len(sys.argv) < 2:
|
|
259
|
+
print(f"Usage: {sys.argv[0]} <file.rs|dir> [file.rs|dir ...]", file=sys.stderr)
|
|
260
|
+
sys.exit(2)
|
|
261
|
+
|
|
262
|
+
files = collect_rs_files(sys.argv[1:])
|
|
263
|
+
if not files:
|
|
264
|
+
print("warning: no .rs files found in the given arguments", file=sys.stderr)
|
|
265
|
+
sys.exit(0)
|
|
266
|
+
|
|
267
|
+
for f in files:
|
|
268
|
+
check_file(f)
|
|
269
|
+
|
|
270
|
+
if violations > 0:
|
|
271
|
+
print("", file=sys.stderr)
|
|
272
|
+
print(
|
|
273
|
+
f"rust-programmer: {violations} violation(s). Fix before declaring work done.",
|
|
274
|
+
file=sys.stderr,
|
|
275
|
+
)
|
|
276
|
+
print("", file=sys.stderr)
|
|
277
|
+
print("Then run the full toolchain gate:", file=sys.stderr)
|
|
278
|
+
print(" cargo +stable fmt --all -- --check", file=sys.stderr)
|
|
279
|
+
print(
|
|
280
|
+
" cargo +stable clippy --all-targets --all-features -- -D warnings",
|
|
281
|
+
file=sys.stderr,
|
|
282
|
+
)
|
|
283
|
+
print(" cargo nextest run --all-targets --all-features", file=sys.stderr)
|
|
284
|
+
print(
|
|
285
|
+
" cargo +nightly miri nextest run --all-features # if unsafe touched",
|
|
286
|
+
file=sys.stderr,
|
|
287
|
+
)
|
|
288
|
+
print(" cargo machete", file=sys.stderr)
|
|
289
|
+
print(" cargo deny check", file=sys.stderr)
|
|
290
|
+
sys.exit(1)
|
|
291
|
+
|
|
292
|
+
print(f"rust-programmer: no-excuse rules passed for {len(files)} file(s).")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
main()
|