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.
Files changed (156) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/LICENSE +21 -0
  3. package/README.md +369 -0
  4. package/README_ko-KR.md +374 -0
  5. package/RELEASE_CHECKLIST.md +165 -0
  6. package/bin/litclaude-ai.js +643 -0
  7. package/cover.png +0 -0
  8. package/docs/agents.md +67 -0
  9. package/docs/hooks.md +134 -0
  10. package/docs/lsp.md +40 -0
  11. package/docs/migration.md +209 -0
  12. package/docs/workflow-compatibility-audit.md +119 -0
  13. package/generate_cover.py +123 -0
  14. package/package.json +48 -0
  15. package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
  16. package/plugins/litclaude/.lsp.json +13 -0
  17. package/plugins/litclaude/.mcp.json +9 -0
  18. package/plugins/litclaude/agents/boulder-executor.md +12 -0
  19. package/plugins/litclaude/agents/librarian-researcher.md +15 -0
  20. package/plugins/litclaude/agents/oracle-verifier.md +16 -0
  21. package/plugins/litclaude/agents/prometheus-planner.md +13 -0
  22. package/plugins/litclaude/agents/qa-runner.md +16 -0
  23. package/plugins/litclaude/agents/quality-reviewer.md +17 -0
  24. package/plugins/litclaude/bin/litclaude-hook.js +110 -0
  25. package/plugins/litclaude/bin/litclaude-hud.js +271 -0
  26. package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
  27. package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
  28. package/plugins/litclaude/commands/deep-interview.md +21 -0
  29. package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
  30. package/plugins/litclaude/commands/lit-loop.md +40 -0
  31. package/plugins/litclaude/commands/lit-plan.md +35 -0
  32. package/plugins/litclaude/commands/litgoal.md +30 -0
  33. package/plugins/litclaude/commands/review-work.md +35 -0
  34. package/plugins/litclaude/commands/start-work.md +36 -0
  35. package/plugins/litclaude/hooks/hooks.json +54 -0
  36. package/plugins/litclaude/lib/context-pressure.mjs +25 -0
  37. package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
  38. package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
  39. package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
  40. package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
  41. package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
  42. package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
  43. package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
  44. package/plugins/litclaude/lib/workflow-check.mjs +83 -0
  45. package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
  46. package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
  47. package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
  48. package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
  49. package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
  50. package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
  51. package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
  52. package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
  53. package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
  54. package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
  55. package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
  56. package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
  57. package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
  58. package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
  59. package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
  60. package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
  61. package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
  62. package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
  63. package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
  64. package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
  65. package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
  66. package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
  67. package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
  68. package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
  69. package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
  70. package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
  71. package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
  72. package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
  73. package/plugins/litclaude/skills/programming/SKILL.md +106 -0
  74. package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
  75. package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
  76. package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
  77. package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
  78. package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
  79. package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
  80. package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
  81. package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
  82. package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
  83. package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
  84. package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
  85. package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
  86. package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
  87. package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
  88. package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
  89. package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
  90. package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
  91. package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
  92. package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
  93. package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
  94. package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
  95. package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
  96. package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
  97. package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
  98. package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
  99. package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
  100. package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
  101. package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
  102. package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
  103. package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
  104. package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
  105. package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
  106. package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
  107. package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
  108. package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
  109. package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
  110. package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
  111. package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
  112. package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
  113. package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
  114. package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
  115. package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
  116. package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
  117. package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
  118. package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
  119. package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
  120. package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
  121. package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
  122. package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
  123. package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
  124. package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
  125. package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
  126. package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
  127. package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
  128. package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
  129. package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
  130. package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
  131. package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
  132. package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
  133. package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
  134. package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
  135. package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
  136. package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
  137. package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
  138. package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
  139. package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
  140. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
  141. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
  142. package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
  143. package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
  144. package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
  145. package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
  146. package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
  147. package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
  148. package/plugins/litclaude/skills/rules/SKILL.md +66 -0
  149. package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
  150. package/scripts/audit-plan-checkboxes.mjs +37 -0
  151. package/scripts/doctor.mjs +41 -0
  152. package/scripts/inspect-agent-tools.mjs +27 -0
  153. package/scripts/postinstall.mjs +50 -0
  154. package/scripts/qa-claude-plugin-smoke.sh +60 -0
  155. package/scripts/qa-portable-install.sh +136 -0
  156. package/scripts/validate-plugin.mjs +72 -0
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env bash
2
+ # No-excuse rule checker for Rust files.
3
+ # Mirrors the philosophy of python-programmer / typescript-programmer scripts:
4
+ # only rules that can be enforced via pure text matching live here.
5
+ # Everything semantic is on clippy + miri + nextest.
6
+
7
+ set -euo pipefail
8
+
9
+ if [ $# -eq 0 ]; then
10
+ echo "Usage: $0 <file.rs> [file.rs ...]" >&2
11
+ exit 2
12
+ fi
13
+
14
+ violations=0
15
+ report() {
16
+ local file="$1"
17
+ local line="$2"
18
+ local rule="$3"
19
+ local detail="$4"
20
+ echo "::error file=${file},line=${line}::[${rule}] ${detail}" >&2
21
+ violations=$((violations + 1))
22
+ }
23
+
24
+ is_test_path() {
25
+ local path="$1"
26
+ case "$path" in
27
+ */tests/*|*/benches/*|*/examples/*|*/build.rs|*_test.rs|tests/*|benches/*|examples/*) return 0 ;;
28
+ esac
29
+ # In-file #[cfg(test)] modules are handled per-line below.
30
+ return 1
31
+ }
32
+
33
+ for file in "$@"; do
34
+ [ -f "$file" ] || continue
35
+ case "$file" in
36
+ *.rs) ;;
37
+ *) continue ;;
38
+ esac
39
+
40
+ if is_test_path "$file"; then
41
+ # Test files are exempt from unwrap/expect/todo rules.
42
+ # Still enforce unsafe-comment, allow-comment, panic-in-lib rules below
43
+ # by setting a marker - keeping the loop unified.
44
+ in_test_file=1
45
+ else
46
+ in_test_file=0
47
+ fi
48
+
49
+ # Track #[cfg(test)] regions for per-line exemptions.
50
+ in_cfg_test=0
51
+ cfg_test_brace_depth=0
52
+ line_no=0
53
+
54
+ while IFS= read -r raw_line || [ -n "$raw_line" ]; do
55
+ line_no=$((line_no + 1))
56
+ line="$raw_line"
57
+
58
+ # Crude #[cfg(test)] region tracker: when we see #[cfg(test)] on a
59
+ # line followed by a mod with `{`, count braces until depth returns
60
+ # to zero. This is approximate but matches typical formatting.
61
+ if [[ "$line" =~ \#\[cfg\(test\)\] ]]; then
62
+ in_cfg_test=1
63
+ cfg_test_brace_depth=0
64
+ fi
65
+ if [ "$in_cfg_test" -eq 1 ]; then
66
+ opens=$(printf '%s' "$line" | tr -cd '{' | wc -c)
67
+ closes=$(printf '%s' "$line" | tr -cd '}' | wc -c)
68
+ cfg_test_brace_depth=$((cfg_test_brace_depth + opens - closes))
69
+ if [ "$cfg_test_brace_depth" -le 0 ] && [[ ! "$line" =~ \#\[cfg\(test\)\] ]]; then
70
+ in_cfg_test=0
71
+ fi
72
+ fi
73
+
74
+ exempt=0
75
+ [ "$in_test_file" -eq 1 ] && exempt=1
76
+ [ "$in_cfg_test" -eq 1 ] && exempt=1
77
+
78
+ # Strip line comments before pattern checks - so doc comments and
79
+ # explanatory prose do not trip the regexes.
80
+ code_only="${line%%//*}"
81
+
82
+ if [ "$exempt" -eq 0 ]; then
83
+ # .unwrap()
84
+ if [[ "$code_only" =~ \.unwrap\(\) ]]; then
85
+ # Allow if previous line had // SAFE-UNWRAP: comment
86
+ prev_line=$(sed -n "$((line_no - 1))p" "$file" 2>/dev/null || true)
87
+ if [[ ! "$prev_line" =~ //[[:space:]]*SAFE-UNWRAP: ]]; then
88
+ report "$file" "$line_no" "unwrap" ".unwrap() outside tests - use ? / ok_or / pattern match or annotate previous line with // SAFE-UNWRAP: <reason>"
89
+ fi
90
+ fi
91
+
92
+ # .expect("...")
93
+ if [[ "$code_only" =~ \.expect\( ]]; then
94
+ prev_line=$(sed -n "$((line_no - 1))p" "$file" 2>/dev/null || true)
95
+ if [[ ! "$prev_line" =~ //[[:space:]]*SAFE-EXPECT: ]]; then
96
+ report "$file" "$line_no" "expect" ".expect() outside tests - use ? or annotate previous line with // SAFE-EXPECT: <reason>"
97
+ fi
98
+ fi
99
+
100
+ # todo!() / unimplemented!() / unreachable!()
101
+ if [[ "$code_only" =~ (todo!|unimplemented!|unreachable!|unreachable_unchecked!) ]]; then
102
+ report "$file" "$line_no" "placeholder-macro" "todo!/unimplemented!/unreachable! in committed code"
103
+ fi
104
+
105
+ # Box<dyn Error
106
+ if [[ "$code_only" =~ Box\<dyn[[:space:]]+Error ]]; then
107
+ report "$file" "$line_no" "box-dyn-error" "Box<dyn Error> in non-test code - use anyhow::Error (apps) or thiserror enum (libs)"
108
+ fi
109
+
110
+ # panic!( in lib
111
+ if [[ "$file" == */src/lib.rs || "$file" == */src/*/mod.rs || ( "$file" == */src/*.rs && "$file" != */src/main.rs && "$file" != */src/bin/* ) ]]; then
112
+ if [[ "$code_only" =~ panic!\( ]]; then
113
+ report "$file" "$line_no" "lib-panic" "panic!() in library code - return Result"
114
+ fi
115
+ fi
116
+ fi
117
+
118
+ # unsafe { without preceding // SAFETY: in the last 5 lines (always enforced)
119
+ if [[ "$code_only" =~ unsafe[[:space:]]*\{ ]]; then
120
+ start=$((line_no > 5 ? line_no - 5 : 1))
121
+ window=$(sed -n "${start},${line_no}p" "$file" 2>/dev/null || true)
122
+ if [[ ! "$window" =~ //[[:space:]]*SAFETY: ]]; then
123
+ report "$file" "$line_no" "unsafe-no-safety-comment" "unsafe block without // SAFETY: comment in preceding 5 lines"
124
+ fi
125
+ fi
126
+
127
+ # #[allow(clippy::...)] without preceding // CLIPPY-ALLOW: justification
128
+ if [[ "$code_only" =~ \#\[allow\(clippy:: ]]; then
129
+ prev_line=$(sed -n "$((line_no - 1))p" "$file" 2>/dev/null || true)
130
+ if [[ ! "$prev_line" =~ //[[:space:]]*CLIPPY-ALLOW: ]]; then
131
+ report "$file" "$line_no" "unjustified-clippy-allow" "#[allow(clippy::...)] without // CLIPPY-ALLOW: <reason> on previous line"
132
+ fi
133
+ fi
134
+
135
+ # Narrowing numeric `as` casts - heuristic flag for human review.
136
+ # Catches the common shapes; precise type analysis belongs to clippy::cast_possible_truncation.
137
+ if [[ "$code_only" =~ as[[:space:]]+(u8|u16|u32|i8|i16|i32) ]] && \
138
+ [[ "$code_only" =~ (u16|u32|u64|u128|usize|i16|i32|i64|i128|isize)[[:space:]]+as[[:space:]]+(u8|u16|u32|i8|i16|i32) ]]; then
139
+ report "$file" "$line_no" "narrowing-as-cast" "possible narrowing 'as' cast - use TryFrom / try_into() for fallible conversion"
140
+ fi
141
+ done < "$file"
142
+ done
143
+
144
+ if [ "$violations" -gt 0 ]; then
145
+ echo "" >&2
146
+ echo "rust-programmer: ${violations} violation(s). Fix before declaring work done." >&2
147
+ echo "" >&2
148
+ echo "Then run the full toolchain gate:" >&2
149
+ echo " cargo +stable fmt --all -- --check" >&2
150
+ echo " cargo +stable clippy --all-targets --all-features -- -D warnings" >&2
151
+ echo " cargo nextest run --all-targets --all-features" >&2
152
+ echo " cargo +nightly miri nextest run --all-features # if unsafe touched" >&2
153
+ echo " cargo machete" >&2
154
+ echo " cargo deny check" >&2
155
+ exit 1
156
+ fi
157
+
158
+ echo "rust-programmer: no-excuse rules passed for $# file(s)."
@@ -0,0 +1,175 @@
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
+ # ──────────────────
17
+ #
18
+ # Creates a new Rust project with strict lints, deny.toml, rustfmt.toml,
19
+ # rust-toolchain.toml, and .cargo/config.toml pre-configured.
20
+
21
+ from __future__ import annotations
22
+
23
+ import subprocess
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ import typer
28
+ from rich.console import Console
29
+
30
+ console = Console(stderr=True)
31
+
32
+ # ── Embedded config contents ─────────────────────────────────────────────
33
+
34
+ RUST_TOOLCHAIN_TOML = """\
35
+ [toolchain]
36
+ channel = "stable"
37
+ components = ["rustfmt", "clippy", "rust-src"]
38
+ profile = "default"
39
+ """
40
+
41
+ CARGO_TOML_LINTS = """
42
+ [lints.rust]
43
+ unsafe_op_in_unsafe_fn = "deny"
44
+ missing_docs = "warn"
45
+ missing_debug_implementations = "warn"
46
+ unreachable_pub = "warn"
47
+ unused_must_use = "deny"
48
+ elided_lifetimes_in_paths = "warn"
49
+ non_ascii_idents = "deny"
50
+ trivial_numeric_casts = "warn"
51
+ unused_lifetimes = "warn"
52
+ single_use_lifetimes = "warn"
53
+
54
+ [lints.clippy]
55
+ all = { level = "deny", priority = -1 }
56
+ pedantic = { level = "warn", priority = -1 }
57
+ nursery = { level = "warn", priority = -1 }
58
+ cargo = { level = "warn", priority = -1 }
59
+ undocumented_unsafe_blocks = "deny"
60
+ multiple_unsafe_ops_per_block = "deny"
61
+ unwrap_used = "deny"
62
+ expect_used = "deny"
63
+ panic = "deny"
64
+ todo = "deny"
65
+ unimplemented = "deny"
66
+ dbg_macro = "deny"
67
+ print_stdout = "warn"
68
+ print_stderr = "warn"
69
+ module_name_repetitions = { level = "allow" }
70
+ must_use_candidate = { level = "allow" }
71
+ missing_errors_doc = { level = "allow" }
72
+ missing_panics_doc = { level = "allow" }
73
+ """
74
+
75
+ CARGO_CONFIG_TOML = """\
76
+ [build]
77
+ rustflags = ["-C", "link-arg=-fuse-ld=lld"]
78
+
79
+ [target.x86_64-unknown-linux-gnu]
80
+ linker = "clang"
81
+ rustflags = ["-C", "link-arg=-fuse-ld=lld"]
82
+
83
+ [target.aarch64-apple-darwin]
84
+ rustflags = []
85
+ """
86
+
87
+ DENY_TOML = """\
88
+ [advisories]
89
+ vulnerability = "deny"
90
+ unmaintained = "warn"
91
+ yanked = "deny"
92
+
93
+ [licenses]
94
+ unlicensed = "deny"
95
+ allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-3.0", "Zlib"]
96
+
97
+ [bans]
98
+ multiple-versions = "warn"
99
+ wildcards = "deny"
100
+
101
+ [sources]
102
+ unknown-registry = "deny"
103
+ unknown-git = "deny"
104
+ """
105
+
106
+ RUSTFMT_TOML = """\
107
+ edition = "2024"
108
+ max_width = 100
109
+ use_field_init_shorthand = true
110
+ use_try_shorthand = true
111
+ """
112
+
113
+ # ── Main ─────────────────────────────────────────────────────────────────
114
+
115
+ app = typer.Typer(add_completion=False)
116
+
117
+
118
+ @app.command()
119
+ def main(
120
+ name: str = typer.Argument(help="Name of the new Rust project"),
121
+ path: Path = typer.Option(
122
+ Path.cwd(),
123
+ "--path",
124
+ "-p",
125
+ help="Parent directory where the project folder is created",
126
+ ),
127
+ ) -> None:
128
+ """Scaffold a new Rust project with strict lints and tooling configs."""
129
+ project_dir = path / name
130
+
131
+ # ── cargo init ───────────────────────────────────────────────────
132
+ console.print(f"[bold green]Creating[/] project [cyan]{name}[/] at [dim]{project_dir}[/]")
133
+ try:
134
+ subprocess.run(
135
+ ["cargo", "init", str(project_dir), "--name", name],
136
+ check=True,
137
+ capture_output=True,
138
+ text=True,
139
+ )
140
+ except FileNotFoundError:
141
+ console.print("[bold red]Error:[/] cargo not found. Install Rust via https://rustup.rs")
142
+ sys.exit(1)
143
+ except subprocess.CalledProcessError as exc:
144
+ console.print(f"[bold red]cargo init failed:[/]\n{exc.stderr}")
145
+ sys.exit(1)
146
+
147
+ # ── rust-toolchain.toml ──────────────────────────────────────────
148
+ (project_dir / "rust-toolchain.toml").write_text(RUST_TOOLCHAIN_TOML)
149
+ console.print(" [dim]wrote[/] rust-toolchain.toml")
150
+
151
+ # ── Append [lints] to Cargo.toml ─────────────────────────────────
152
+ cargo_toml = project_dir / "Cargo.toml"
153
+ with cargo_toml.open("a") as f:
154
+ f.write(CARGO_TOML_LINTS)
155
+ console.print(" [dim]appended[/] [lints] to Cargo.toml")
156
+
157
+ # ── .cargo/config.toml ───────────────────────────────────────────
158
+ cargo_config_dir = project_dir / ".cargo"
159
+ cargo_config_dir.mkdir(parents=True, exist_ok=True)
160
+ (cargo_config_dir / "config.toml").write_text(CARGO_CONFIG_TOML)
161
+ console.print(" [dim]wrote[/] .cargo/config.toml")
162
+
163
+ # ── deny.toml ────────────────────────────────────────────────────
164
+ (project_dir / "deny.toml").write_text(DENY_TOML)
165
+ console.print(" [dim]wrote[/] deny.toml")
166
+
167
+ # ── rustfmt.toml ─────────────────────────────────────────────────
168
+ (project_dir / "rustfmt.toml").write_text(RUSTFMT_TOML)
169
+ console.print(" [dim]wrote[/] rustfmt.toml")
170
+
171
+ console.print(f"\n[bold green]Done![/] cd {project_dir} && cargo check")
172
+
173
+
174
+ if __name__ == "__main__":
175
+ app()
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Check TypeScript files for no-excuse violations.
4
+ *
5
+ * Rules:
6
+ * no-any-assertion - `as any`
7
+ * no-unknown-assertion - `as unknown`
8
+ * no-ts-ignore - `@ts-ignore` comments
9
+ * no-ts-expect-error - `@ts-expect-error` comments
10
+ * no-enum - `enum` declarations
11
+ * no-non-null-assertion - `x!` postfix operator
12
+ * no-throw-literal - `throw "string"` / `throw 123`
13
+ * no-mutable-export - `export let` / `export var`
14
+ * no-any-annotation - `: any` in annotations (opt out: `// no-excuse-ok: any`)
15
+ * no-explicit-any-return - `(): any` return types (opt out: `// no-excuse-ok: any`)
16
+ * empty-catch - `catch { }` or `catch (e) { }` with empty body
17
+ * catch-without-narrowing - catch block that uses error without instanceof narrowing
18
+ *
19
+ * Usage:
20
+ * bun run scripts/check-no-excuse-rules.ts <file-or-dir>...
21
+ *
22
+ * Exit codes:
23
+ * 0 - no violations
24
+ * 1 - violations found
25
+ * 2 - input error
26
+ */
27
+
28
+ import fs from "node:fs"
29
+ import path from "node:path"
30
+ import process from "node:process"
31
+ import ts from "typescript"
32
+
33
+ type RuleId =
34
+ | "no-any-assertion"
35
+ | "no-unknown-assertion"
36
+ | "no-ts-ignore"
37
+ | "no-ts-expect-error"
38
+ | "no-enum"
39
+ | "no-non-null-assertion"
40
+ | "no-throw-literal"
41
+ | "no-mutable-export"
42
+ | "no-any-annotation"
43
+ | "no-explicit-any-return"
44
+ | "empty-catch"
45
+ | "catch-without-narrowing"
46
+
47
+ type Violation = {
48
+ readonly ruleId: RuleId
49
+ readonly filePath: string
50
+ readonly line: number
51
+ readonly column: number
52
+ readonly message: string
53
+ }
54
+
55
+ const INCLUDED_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"])
56
+ const IGNORED_DIRECTORIES = new Set([
57
+ ".git", ".next", ".nuxt", ".turbo", ".yarn",
58
+ "coverage", "dist", "build", "node_modules",
59
+ ])
60
+
61
+ const OPT_OUT_RE = /\/\/\s*no-excuse-ok:\s*any/
62
+ const CATCH_OK_RE = /\/\/\s*no-excuse-ok:\s*catch/
63
+
64
+ function isIncludedFile(filePath: string): boolean {
65
+ return INCLUDED_EXTENSIONS.has(path.extname(filePath).toLowerCase())
66
+ }
67
+
68
+ function isDeclarationFile(filePath: string): boolean {
69
+ return filePath.endsWith(".d.ts") || filePath.endsWith(".d.mts") || filePath.endsWith(".d.cts")
70
+ }
71
+
72
+ function discoverFiles(inputs: string[]): string[] {
73
+ const files: string[] = []
74
+ for (const input of inputs) {
75
+ const resolved = path.resolve(input)
76
+ if (!fs.existsSync(resolved)) {
77
+ console.error(`Path does not exist: ${resolved}`)
78
+ process.exit(2)
79
+ }
80
+ if (fs.statSync(resolved).isFile()) {
81
+ if (isIncludedFile(resolved) && !isDeclarationFile(resolved)) files.push(resolved)
82
+ continue
83
+ }
84
+ const walk = (dir: string): void => {
85
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
86
+ if (entry.isDirectory()) {
87
+ if (!IGNORED_DIRECTORIES.has(entry.name)) walk(path.join(dir, entry.name))
88
+ } else if (isIncludedFile(entry.name) && !isDeclarationFile(entry.name)) {
89
+ files.push(path.join(dir, entry.name))
90
+ }
91
+ }
92
+ }
93
+ walk(resolved)
94
+ }
95
+ return files
96
+ }
97
+
98
+ function getLineText(sourceFile: ts.SourceFile, line: number): string {
99
+ const lineStarts = sourceFile.getLineStarts()
100
+ const start = lineStarts[line]
101
+ const end = line + 1 < lineStarts.length ? lineStarts[line + 1] : sourceFile.getEnd()
102
+ return sourceFile.text.slice(start, end)
103
+ }
104
+
105
+ function analyzeFile(filePath: string): Violation[] {
106
+ const source = fs.readFileSync(filePath, "utf-8")
107
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true)
108
+ const violations: Violation[] = []
109
+
110
+ function pos(node: ts.Node): { line: number; column: number } {
111
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
112
+ return { line: line + 1, column: character + 1 }
113
+ }
114
+
115
+ function lineHasOptOut(node: ts.Node): boolean {
116
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
117
+ return OPT_OUT_RE.test(getLineText(sourceFile, line))
118
+ }
119
+
120
+ function visit(node: ts.Node): void {
121
+ // ── as any / as unknown ──
122
+ if (ts.isAsExpression(node)) {
123
+ const typeText = node.type.getText(sourceFile)
124
+ if (typeText === "any") {
125
+ const p = pos(node)
126
+ violations.push({ ruleId: "no-any-assertion", filePath, ...p, message: "`as any` — narrow with type guards or redesign the types" })
127
+ }
128
+ if (typeText === "unknown") {
129
+ const p = pos(node)
130
+ violations.push({ ruleId: "no-unknown-assertion", filePath, ...p, message: "`as unknown` — redesign the types" })
131
+ }
132
+ }
133
+
134
+ // ── enum ──
135
+ if (ts.isEnumDeclaration(node)) {
136
+ const p = pos(node)
137
+ violations.push({ ruleId: "no-enum", filePath, ...p, message: "`enum` — use `as const` object + literal union type" })
138
+ }
139
+
140
+ // ── x! non-null assertion ──
141
+ if (ts.isNonNullExpression(node)) {
142
+ const p = pos(node)
143
+ violations.push({ ruleId: "no-non-null-assertion", filePath, ...p, message: "`x!` — use narrowing or optional chaining" })
144
+ }
145
+
146
+ // ── throw "literal" ──
147
+ if (ts.isThrowStatement(node) && node.expression) {
148
+ const expr = node.expression
149
+ if (ts.isStringLiteral(expr) || ts.isNumericLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
150
+ const p = pos(node)
151
+ violations.push({ ruleId: "no-throw-literal", filePath, ...p, message: "`throw literal` — throw an Error subclass" })
152
+ }
153
+ if (ts.isTemplateExpression(expr)) {
154
+ const p = pos(node)
155
+ violations.push({ ruleId: "no-throw-literal", filePath, ...p, message: "`throw template` — throw an Error subclass" })
156
+ }
157
+ }
158
+
159
+ // ── export let / export var ──
160
+ if (ts.isVariableStatement(node)) {
161
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
162
+ if (hasExport) {
163
+ const flags = node.declarationList.flags
164
+ if (!(flags & ts.NodeFlags.Const)) {
165
+ const p = pos(node)
166
+ violations.push({ ruleId: "no-mutable-export", filePath, ...p, message: "`export let/var` — use `export const`" })
167
+ }
168
+ }
169
+ }
170
+
171
+ // ── : any in annotations ──
172
+ if (ts.isTypeReferenceNode(node) || node.kind === ts.SyntaxKind.AnyKeyword) {
173
+ if (node.kind === ts.SyntaxKind.AnyKeyword && !lineHasOptOut(node)) {
174
+ const parent = node.parent
175
+ // Skip `as any` — already caught by no-any-assertion
176
+ if (parent && ts.isAsExpression(parent)) {
177
+ // already handled
178
+ } else if (parent && (
179
+ ts.isParameter(parent) ||
180
+ ts.isVariableDeclaration(parent) ||
181
+ ts.isPropertyDeclaration(parent) ||
182
+ ts.isPropertySignature(parent)
183
+ )) {
184
+ const p = pos(node)
185
+ violations.push({ ruleId: "no-any-annotation", filePath, ...p, message: "`: any` annotation — use `unknown` and narrow" })
186
+ } else if (parent && (
187
+ ts.isFunctionDeclaration(parent) ||
188
+ ts.isMethodDeclaration(parent) ||
189
+ ts.isArrowFunction(parent) ||
190
+ ts.isFunctionExpression(parent)
191
+ )) {
192
+ const p = pos(node)
193
+ violations.push({ ruleId: "no-explicit-any-return", filePath, ...p, message: "`(): any` return — use a specific type" })
194
+ }
195
+ }
196
+ }
197
+
198
+ // ── empty catch / catch without narrowing ──
199
+ if (ts.isCatchClause(node)) {
200
+ const catchLine = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line
201
+ const catchLineText = getLineText(sourceFile, catchLine)
202
+ if (!CATCH_OK_RE.test(catchLineText)) {
203
+ const body = node.block
204
+ const stmts = body.statements
205
+
206
+ if (stmts.length === 0) {
207
+ // Empty catch — swallows everything silently
208
+ const p = pos(node)
209
+ violations.push({ ruleId: "empty-catch", filePath, ...p, message: "empty `catch` block — handle, re-throw, or remove the try/catch" })
210
+ } else if (node.variableDeclaration) {
211
+ // Has a bound variable — check if it's narrowed with instanceof
212
+ const varName = node.variableDeclaration.name.getText(sourceFile)
213
+ const blockText = body.getText(sourceFile)
214
+ const hasInstanceof = blockText.includes(`instanceof`)
215
+ const hasRethrow = blockText.includes(`throw ${varName}`) || blockText.includes(`throw new`)
216
+ if (!hasInstanceof && !hasRethrow) {
217
+ const p = pos(node)
218
+ violations.push({ ruleId: "catch-without-narrowing", filePath, ...p, message: "`catch` without `instanceof` narrowing or re-throw — narrow the error type or re-throw" })
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ ts.forEachChild(node, visit)
225
+ }
226
+
227
+ visit(sourceFile)
228
+
229
+ // ── @ts-ignore / @ts-expect-error in comments ──
230
+ const commentRanges = [
231
+ ...(ts.getLeadingCommentRanges(source, 0) ?? []),
232
+ ]
233
+ // Scan all comments via regex for reliability
234
+ const commentRegex = /\/\/\s*@ts-(ignore|expect-error)/g
235
+ let match: RegExpExecArray | null
236
+ while ((match = commentRegex.exec(source)) !== null) {
237
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(match.index)
238
+ const kind = match[1]
239
+ violations.push({
240
+ ruleId: kind === "ignore" ? "no-ts-ignore" : "no-ts-expect-error",
241
+ filePath,
242
+ line: line + 1,
243
+ column: character + 1,
244
+ message: `\`@ts-${kind}\` — fix the underlying type`,
245
+ })
246
+ }
247
+
248
+ return violations
249
+ }
250
+
251
+ function formatViolation(v: Violation): string {
252
+ return `${v.filePath}:${v.line}:${v.column}: [${v.ruleId}] ${v.message}`
253
+ }
254
+
255
+ function main(): void {
256
+ const args = process.argv.slice(2)
257
+ if (args.length === 0) {
258
+ console.error("usage: check-no-excuse-rules.ts <file-or-dir>...")
259
+ process.exit(2)
260
+ }
261
+
262
+ const files = discoverFiles(args)
263
+ if (files.length === 0) {
264
+ console.error("No TypeScript files found.")
265
+ process.exit(2)
266
+ }
267
+
268
+ const violations = files.flatMap((f) => analyzeFile(f))
269
+
270
+ if (violations.length === 0) {
271
+ console.log(`No violations in ${files.length} file(s).`)
272
+ return
273
+ }
274
+
275
+ for (const v of violations) {
276
+ console.error(formatViolation(v))
277
+ }
278
+ console.error(`\n${violations.length} violation(s) in ${files.length} file(s).`)
279
+ process.exit(1)
280
+ }
281
+
282
+ main()