codeforge-dev 1.7.0 → 1.8.0
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/.devcontainer/.env +4 -6
- package/.devcontainer/.env.example +29 -0
- package/.devcontainer/.gitignore +8 -0
- package/.devcontainer/.secrets.example +12 -0
- package/.devcontainer/CHANGELOG.md +130 -0
- package/.devcontainer/CLAUDE.md +56 -19
- package/.devcontainer/README.md +111 -56
- package/.devcontainer/config/{main-system-prompt.md → defaults/main-system-prompt.md} +72 -0
- package/.devcontainer/config/file-manifest.json +20 -0
- package/.devcontainer/devcontainer.json +20 -0
- package/.devcontainer/docs/configuration-reference.md +90 -0
- package/.devcontainer/docs/keybindings.md +100 -0
- package/.devcontainer/docs/optional-features.md +129 -0
- package/.devcontainer/docs/plugins.md +154 -0
- package/.devcontainer/docs/troubleshooting.md +128 -0
- package/.devcontainer/features/agent-browser/install.sh +6 -0
- package/.devcontainer/features/ast-grep/install.sh +6 -0
- package/.devcontainer/features/biome/README.md +27 -0
- package/.devcontainer/features/biome/install.sh +6 -0
- package/.devcontainer/features/ccburn/install.sh +6 -0
- package/.devcontainer/features/ccstatusline/devcontainer-feature.json +5 -0
- package/.devcontainer/features/ccstatusline/install.sh +7 -0
- package/.devcontainer/features/ccusage/install.sh +6 -0
- package/.devcontainer/features/claude-monitor/install.sh +6 -0
- package/.devcontainer/features/dprint/README.md +30 -0
- package/.devcontainer/features/dprint/devcontainer-feature.json +18 -0
- package/.devcontainer/features/dprint/install.sh +131 -0
- package/.devcontainer/features/hadolint/README.md +35 -0
- package/.devcontainer/features/hadolint/devcontainer-feature.json +13 -0
- package/.devcontainer/features/hadolint/install.sh +86 -0
- package/.devcontainer/features/lsp-servers/devcontainer-feature.json +5 -0
- package/.devcontainer/features/lsp-servers/install.sh +7 -0
- package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +5 -0
- package/.devcontainer/features/mcp-qdrant/install.sh +13 -6
- package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +5 -0
- package/.devcontainer/features/mcp-reasoner/install.sh +8 -1
- package/.devcontainer/features/notify-hook/devcontainer-feature.json +5 -0
- package/.devcontainer/features/notify-hook/install.sh +7 -0
- package/.devcontainer/features/ruff/README.md +26 -0
- package/.devcontainer/features/ruff/devcontainer-feature.json +21 -0
- package/.devcontainer/features/ruff/install.sh +74 -0
- package/.devcontainer/features/shellcheck/README.md +38 -0
- package/.devcontainer/features/shellcheck/devcontainer-feature.json +13 -0
- package/.devcontainer/features/shellcheck/install.sh +24 -0
- package/.devcontainer/features/shfmt/README.md +37 -0
- package/.devcontainer/features/shfmt/devcontainer-feature.json +13 -0
- package/.devcontainer/features/shfmt/install.sh +85 -0
- package/.devcontainer/features/splitrail/devcontainer-feature.json +5 -0
- package/.devcontainer/features/splitrail/install.sh +7 -0
- package/.devcontainer/features/tmux/install.sh +8 -0
- package/.devcontainer/features/tree-sitter/install.sh +6 -0
- package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +3 -10
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/__pycache__/format-on-stop.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-on-stop.py +114 -9
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +4 -5
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/__pycache__/lint-file.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +478 -76
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/.claude-plugin/plugin.json +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/AGENT-REDIRECTION.md +226 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/architect.md +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/bash-exec.md +4 -4
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/claude-guide.md +14 -23
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/debug-logs.md +2 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/dependency-analyst.md +2 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/doc-writer.md +13 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/explorer.md +2 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/generalist.md +10 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/migrator.md +6 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/refactorer.md +4 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/spec-writer.md +36 -23
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/statusline-config.md +3 -3
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/test-writer.md +3 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/hooks/hooks.json +39 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/advisory-test-runner.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/collect-edited-files.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/commit-reminder.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/git-state-injector.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/redirect-builtin-agents.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/ticket-linker.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/todo-harvester.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/advisory-test-runner.py +174 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/collect-edited-files.py +8 -6
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/commit-reminder.py +90 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/git-state-injector.py +114 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/skill-suggester.py +61 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/ticket-linker.py +137 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/todo-harvester.py +130 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/api-design/SKILL.md +224 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/api-design/references/error-handling.md +166 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/api-design/references/rest-conventions.md +215 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/ast-grep-patterns/SKILL.md +211 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/ast-grep-patterns/references/language-patterns.md +327 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/dependency-management/SKILL.md +134 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/dependency-management/references/ecosystem-commands.md +264 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/dependency-management/references/license-compliance.md +80 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/SKILL.md +153 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/references/api-doc-templates.md +221 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/references/docstring-formats.md +296 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/migration-patterns/SKILL.md +150 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/migration-patterns/references/javascript-migrations.md +179 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/migration-patterns/references/python-migrations.md +141 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/SKILL.md +32 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/__pycache__/block-dangerous.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/__pycache__/guard-protected.cpython-314.pyc +0 -0
- package/.devcontainer/scripts/check-setup.sh +72 -0
- package/.devcontainer/scripts/setup-aliases.sh +43 -3
- package/.devcontainer/scripts/setup-auth.sh +74 -0
- package/.devcontainer/scripts/setup-config.sh +112 -22
- package/.devcontainer/scripts/setup-update-claude.sh +8 -0
- package/.devcontainer/scripts/setup.sh +46 -13
- package/README.md +23 -190
- package/package.json +1 -1
- package/setup.js +245 -71
- package/.devcontainer/features/claude-code/README.md +0 -498
- package/.devcontainer/features/claude-code/config/settings.json +0 -72
- package/.devcontainer/features/claude-code/config/system-prompt.md +0 -118
- package/.devcontainer/features/claude-code/config/world-building-sp.md +0 -1432
- package/.devcontainer/features/claude-code/devcontainer-feature.json +0 -42
- package/.devcontainer/features/claude-code/install.sh +0 -466
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +0 -7
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +0 -17
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +0 -6
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +0 -14
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +0 -989
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +0 -33
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +0 -71
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +0 -68
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +0 -120
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +0 -133
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +0 -253
- /package/.devcontainer/config/{keybindings.json → defaults/keybindings.json} +0 -0
- /package/.devcontainer/config/{settings.json → defaults/settings.json} +0 -0
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
Batch linter — runs as a Stop hook.
|
|
4
|
+
|
|
5
|
+
Reads file paths collected by collect-edited-files.py during the
|
|
6
|
+
conversation turn, deduplicates them, and lints each based on
|
|
7
|
+
extension:
|
|
8
|
+
.py / .pyi → Pyright (type checking) + Ruff check (style/correctness)
|
|
9
|
+
.js/.jsx/.ts/… → Biome lint
|
|
10
|
+
.css/.graphql/… → Biome lint
|
|
11
|
+
.sh/.bash/.zsh/… → ShellCheck
|
|
12
|
+
.go → go vet
|
|
13
|
+
Dockerfile → hadolint
|
|
14
|
+
.rs → clippy (conditional)
|
|
4
15
|
|
|
5
|
-
Reads tool input from stdin, detects file type by extension,
|
|
6
|
-
runs appropriate linter if available.
|
|
7
16
|
Outputs JSON with additionalContext containing lint warnings.
|
|
8
|
-
|
|
17
|
+
Always cleans up the temp file. Always exits 0.
|
|
9
18
|
"""
|
|
10
19
|
|
|
11
20
|
import json
|
|
@@ -14,120 +23,513 @@ import subprocess
|
|
|
14
23
|
import sys
|
|
15
24
|
from pathlib import Path
|
|
16
25
|
|
|
17
|
-
#
|
|
18
|
-
PYTHON_EXTENSIONS = {".py", ".pyi"}
|
|
26
|
+
# ── Extension sets ──────────────────────────────────────────────────
|
|
19
27
|
|
|
28
|
+
PYTHON_EXTS = {".py", ".pyi"}
|
|
29
|
+
BIOME_EXTS = {
|
|
30
|
+
".js",
|
|
31
|
+
".jsx",
|
|
32
|
+
".ts",
|
|
33
|
+
".tsx",
|
|
34
|
+
".mjs",
|
|
35
|
+
".cjs",
|
|
36
|
+
".mts",
|
|
37
|
+
".cts",
|
|
38
|
+
".css",
|
|
39
|
+
".graphql",
|
|
40
|
+
".gql",
|
|
41
|
+
}
|
|
42
|
+
SHELL_EXTS = {".sh", ".bash", ".zsh", ".mksh", ".bats"}
|
|
43
|
+
GO_EXTS = {".go"}
|
|
44
|
+
RUST_EXTS = {".rs"}
|
|
20
45
|
|
|
21
|
-
|
|
22
|
-
"""Run pyright on a Python file.
|
|
46
|
+
SUBPROCESS_TIMEOUT = 10
|
|
23
47
|
|
|
24
|
-
Returns:
|
|
25
|
-
(success, message)
|
|
26
|
-
"""
|
|
27
|
-
pyright_cmd = "pyright"
|
|
28
48
|
|
|
29
|
-
|
|
49
|
+
# ── Tool resolution ─────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _which(name: str) -> str | None:
|
|
53
|
+
"""Check if a tool is available in PATH."""
|
|
30
54
|
try:
|
|
31
|
-
subprocess.run(["which",
|
|
32
|
-
|
|
33
|
-
|
|
55
|
+
result = subprocess.run(["which", name], capture_output=True, text=True)
|
|
56
|
+
if result.returncode == 0:
|
|
57
|
+
return result.stdout.strip()
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _find_tool_upward(file_path: str, tool_name: str) -> str | None:
|
|
64
|
+
"""Walk up from file directory looking for node_modules/.bin/<tool>."""
|
|
65
|
+
current = Path(file_path).parent
|
|
66
|
+
for _ in range(20):
|
|
67
|
+
candidate = current / "node_modules" / ".bin" / tool_name
|
|
68
|
+
if candidate.is_file():
|
|
69
|
+
return str(candidate)
|
|
70
|
+
parent = current.parent
|
|
71
|
+
if parent == current:
|
|
72
|
+
break
|
|
73
|
+
current = parent
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _find_biome(file_path: str) -> str | None:
|
|
78
|
+
"""Find biome binary: project-local first, then global."""
|
|
79
|
+
local = _find_tool_upward(file_path, "biome")
|
|
80
|
+
if local:
|
|
81
|
+
return local
|
|
82
|
+
return _which("biome")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── Diagnostic formatting ──────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _format_issues(filename: str, diagnostics: list[dict]) -> str:
|
|
89
|
+
"""Format a list of {severity, line, message} dicts into display text."""
|
|
90
|
+
if not diagnostics:
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
issues = []
|
|
94
|
+
for diag in diagnostics[:5]:
|
|
95
|
+
severity = diag.get("severity", "info")
|
|
96
|
+
message = diag.get("message", "")
|
|
97
|
+
line = diag.get("line", 0)
|
|
98
|
+
|
|
99
|
+
if severity == "error":
|
|
100
|
+
icon = "\u2717"
|
|
101
|
+
elif severity == "warning":
|
|
102
|
+
icon = "!"
|
|
103
|
+
else:
|
|
104
|
+
icon = "\u2022"
|
|
105
|
+
|
|
106
|
+
issues.append(f" {icon} Line {line}: {message}")
|
|
107
|
+
|
|
108
|
+
total = len(diagnostics)
|
|
109
|
+
shown = min(5, total)
|
|
110
|
+
header = f" {filename}: {total} issue(s)"
|
|
111
|
+
if total > shown:
|
|
112
|
+
header += f" (showing first {shown})"
|
|
113
|
+
|
|
114
|
+
return header + "\n" + "\n".join(issues)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ── Linters ─────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def lint_python_pyright(file_path: str) -> str:
|
|
121
|
+
"""Run Pyright type checker on a Python file."""
|
|
122
|
+
pyright = _which("pyright")
|
|
123
|
+
if not pyright:
|
|
124
|
+
return ""
|
|
34
125
|
|
|
35
126
|
try:
|
|
36
127
|
result = subprocess.run(
|
|
37
|
-
[
|
|
128
|
+
[pyright, "--outputjson", file_path],
|
|
38
129
|
capture_output=True,
|
|
39
130
|
text=True,
|
|
40
|
-
timeout=
|
|
131
|
+
timeout=SUBPROCESS_TIMEOUT,
|
|
41
132
|
)
|
|
42
133
|
|
|
43
|
-
# Parse pyright JSON output
|
|
44
134
|
try:
|
|
45
135
|
output = json.loads(result.stdout)
|
|
46
|
-
|
|
136
|
+
except json.JSONDecodeError:
|
|
137
|
+
return ""
|
|
138
|
+
|
|
139
|
+
diagnostics = output.get("generalDiagnostics", [])
|
|
140
|
+
if not diagnostics:
|
|
141
|
+
return ""
|
|
142
|
+
|
|
143
|
+
parsed = [
|
|
144
|
+
{
|
|
145
|
+
"severity": d.get("severity", "info"),
|
|
146
|
+
"line": d.get("range", {}).get("start", {}).get("line", 0) + 1,
|
|
147
|
+
"message": d.get("message", ""),
|
|
148
|
+
}
|
|
149
|
+
for d in diagnostics
|
|
150
|
+
]
|
|
151
|
+
return _format_issues(Path(file_path).name, parsed)
|
|
47
152
|
|
|
48
|
-
|
|
49
|
-
|
|
153
|
+
except subprocess.TimeoutExpired:
|
|
154
|
+
return f" {Path(file_path).name}: pyright timed out"
|
|
155
|
+
except Exception:
|
|
156
|
+
return ""
|
|
50
157
|
|
|
51
|
-
# Format diagnostics
|
|
52
|
-
issues = []
|
|
53
|
-
for diag in diagnostics[:5]: # Limit to first 5 issues
|
|
54
|
-
severity = diag.get("severity", "info")
|
|
55
|
-
message = diag.get("message", "")
|
|
56
|
-
line = diag.get("range", {}).get("start", {}).get("line", 0) + 1
|
|
57
158
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
159
|
+
def lint_python_ruff(file_path: str) -> str:
|
|
160
|
+
"""Run Ruff linter on a Python file."""
|
|
161
|
+
ruff = _which("ruff")
|
|
162
|
+
if not ruff:
|
|
163
|
+
return ""
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
result = subprocess.run(
|
|
167
|
+
[ruff, "check", "--output-format=json", "--no-fix", file_path],
|
|
168
|
+
capture_output=True,
|
|
169
|
+
text=True,
|
|
170
|
+
timeout=SUBPROCESS_TIMEOUT,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
issues = json.loads(result.stdout)
|
|
175
|
+
except json.JSONDecodeError:
|
|
176
|
+
return ""
|
|
177
|
+
|
|
178
|
+
if not issues:
|
|
179
|
+
return ""
|
|
180
|
+
|
|
181
|
+
parsed = [
|
|
182
|
+
{
|
|
183
|
+
"severity": "warning",
|
|
184
|
+
"line": issue.get("location", {}).get("row", 0),
|
|
185
|
+
"message": f"[{issue.get('code', '?')}] {issue.get('message', '')}",
|
|
186
|
+
}
|
|
187
|
+
for issue in issues
|
|
188
|
+
]
|
|
189
|
+
return _format_issues(Path(file_path).name, parsed)
|
|
190
|
+
|
|
191
|
+
except subprocess.TimeoutExpired:
|
|
192
|
+
return f" {Path(file_path).name}: ruff timed out"
|
|
193
|
+
except Exception:
|
|
194
|
+
return ""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def lint_biome(file_path: str) -> str:
|
|
198
|
+
"""Run Biome linter for JS/TS/CSS/GraphQL files."""
|
|
199
|
+
biome = _find_biome(file_path)
|
|
200
|
+
if not biome:
|
|
201
|
+
return ""
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
result = subprocess.run(
|
|
205
|
+
[biome, "lint", "--reporter=json", file_path],
|
|
206
|
+
capture_output=True,
|
|
207
|
+
text=True,
|
|
208
|
+
timeout=SUBPROCESS_TIMEOUT,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
output = json.loads(result.stdout)
|
|
213
|
+
except json.JSONDecodeError:
|
|
214
|
+
return ""
|
|
215
|
+
|
|
216
|
+
diagnostics = output.get("diagnostics", [])
|
|
217
|
+
if not diagnostics:
|
|
218
|
+
return ""
|
|
219
|
+
|
|
220
|
+
parsed = [
|
|
221
|
+
{
|
|
222
|
+
"severity": d.get("severity", "warning"),
|
|
223
|
+
"line": (
|
|
224
|
+
d.get("location", {}).get("span", {}).get("start", {})
|
|
225
|
+
if isinstance(
|
|
226
|
+
d.get("location", {}).get("span", {}).get("start"), int
|
|
227
|
+
)
|
|
228
|
+
else 0
|
|
229
|
+
),
|
|
230
|
+
"message": d.get("description", d.get("message", "")),
|
|
231
|
+
}
|
|
232
|
+
for d in diagnostics
|
|
233
|
+
]
|
|
234
|
+
return _format_issues(Path(file_path).name, parsed)
|
|
235
|
+
|
|
236
|
+
except subprocess.TimeoutExpired:
|
|
237
|
+
return f" {Path(file_path).name}: biome lint timed out"
|
|
238
|
+
except Exception:
|
|
239
|
+
return ""
|
|
64
240
|
|
|
65
|
-
issues.append(f" {icon} Line {line}: {message}")
|
|
66
241
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
242
|
+
def lint_shellcheck(file_path: str) -> str:
|
|
243
|
+
"""Run ShellCheck on a shell script."""
|
|
244
|
+
shellcheck = _which("shellcheck")
|
|
245
|
+
if not shellcheck:
|
|
246
|
+
return ""
|
|
72
247
|
|
|
73
|
-
|
|
248
|
+
try:
|
|
249
|
+
result = subprocess.run(
|
|
250
|
+
[shellcheck, "--format=json", file_path],
|
|
251
|
+
capture_output=True,
|
|
252
|
+
text=True,
|
|
253
|
+
timeout=SUBPROCESS_TIMEOUT,
|
|
254
|
+
)
|
|
74
255
|
|
|
256
|
+
try:
|
|
257
|
+
issues = json.loads(result.stdout)
|
|
75
258
|
except json.JSONDecodeError:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
259
|
+
return ""
|
|
260
|
+
|
|
261
|
+
if not issues:
|
|
262
|
+
return ""
|
|
263
|
+
|
|
264
|
+
severity_map = {
|
|
265
|
+
"error": "error",
|
|
266
|
+
"warning": "warning",
|
|
267
|
+
"info": "info",
|
|
268
|
+
"style": "info",
|
|
269
|
+
}
|
|
270
|
+
parsed = [
|
|
271
|
+
{
|
|
272
|
+
"severity": severity_map.get(issue.get("level", "info"), "info"),
|
|
273
|
+
"line": issue.get("line", 0),
|
|
274
|
+
"message": f"[SC{issue.get('code', '?')}] {issue.get('message', '')}",
|
|
275
|
+
}
|
|
276
|
+
for issue in issues
|
|
277
|
+
]
|
|
278
|
+
return _format_issues(Path(file_path).name, parsed)
|
|
279
|
+
|
|
280
|
+
except subprocess.TimeoutExpired:
|
|
281
|
+
return f" {Path(file_path).name}: shellcheck timed out"
|
|
282
|
+
except Exception:
|
|
283
|
+
return ""
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def lint_go_vet(file_path: str) -> str:
|
|
287
|
+
"""Run go vet on a Go file."""
|
|
288
|
+
go = _which("go")
|
|
289
|
+
if not go:
|
|
290
|
+
return ""
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
result = subprocess.run(
|
|
294
|
+
[go, "vet", file_path],
|
|
295
|
+
capture_output=True,
|
|
296
|
+
text=True,
|
|
297
|
+
timeout=SUBPROCESS_TIMEOUT,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# go vet outputs to stderr
|
|
301
|
+
output = result.stderr.strip()
|
|
302
|
+
if not output:
|
|
303
|
+
return ""
|
|
304
|
+
|
|
305
|
+
lines = output.splitlines()
|
|
306
|
+
parsed = []
|
|
307
|
+
for line in lines:
|
|
308
|
+
# Format: file.go:LINE:COL: message
|
|
309
|
+
parts = line.split(":", 3)
|
|
310
|
+
if len(parts) >= 4:
|
|
311
|
+
try:
|
|
312
|
+
line_num = int(parts[1])
|
|
313
|
+
except ValueError:
|
|
314
|
+
line_num = 0
|
|
315
|
+
parsed.append(
|
|
316
|
+
{
|
|
317
|
+
"severity": "warning",
|
|
318
|
+
"line": line_num,
|
|
319
|
+
"message": parts[3].strip(),
|
|
320
|
+
}
|
|
81
321
|
)
|
|
82
|
-
|
|
322
|
+
elif line.strip():
|
|
323
|
+
parsed.append(
|
|
324
|
+
{
|
|
325
|
+
"severity": "warning",
|
|
326
|
+
"line": 0,
|
|
327
|
+
"message": line.strip(),
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return _format_issues(Path(file_path).name, parsed)
|
|
83
332
|
|
|
84
333
|
except subprocess.TimeoutExpired:
|
|
85
|
-
return
|
|
86
|
-
except Exception
|
|
87
|
-
return
|
|
334
|
+
return f" {Path(file_path).name}: go vet timed out"
|
|
335
|
+
except Exception:
|
|
336
|
+
return ""
|
|
88
337
|
|
|
89
338
|
|
|
90
|
-
def
|
|
91
|
-
"""Run
|
|
339
|
+
def lint_hadolint(file_path: str) -> str:
|
|
340
|
+
"""Run hadolint on a Dockerfile."""
|
|
341
|
+
hadolint = _which("hadolint")
|
|
342
|
+
if not hadolint:
|
|
343
|
+
return ""
|
|
92
344
|
|
|
93
|
-
|
|
94
|
-
(
|
|
95
|
-
|
|
96
|
-
|
|
345
|
+
try:
|
|
346
|
+
result = subprocess.run(
|
|
347
|
+
[hadolint, "--format", "json", file_path],
|
|
348
|
+
capture_output=True,
|
|
349
|
+
text=True,
|
|
350
|
+
timeout=SUBPROCESS_TIMEOUT,
|
|
351
|
+
)
|
|
97
352
|
|
|
98
|
-
|
|
99
|
-
|
|
353
|
+
try:
|
|
354
|
+
issues = json.loads(result.stdout)
|
|
355
|
+
except json.JSONDecodeError:
|
|
356
|
+
return ""
|
|
357
|
+
|
|
358
|
+
if not issues:
|
|
359
|
+
return ""
|
|
360
|
+
|
|
361
|
+
severity_map = {
|
|
362
|
+
"error": "error",
|
|
363
|
+
"warning": "warning",
|
|
364
|
+
"info": "info",
|
|
365
|
+
"style": "info",
|
|
366
|
+
}
|
|
367
|
+
parsed = [
|
|
368
|
+
{
|
|
369
|
+
"severity": severity_map.get(issue.get("level", "info"), "info"),
|
|
370
|
+
"line": issue.get("line", 0),
|
|
371
|
+
"message": f"[{issue.get('code', '?')}] {issue.get('message', '')}",
|
|
372
|
+
}
|
|
373
|
+
for issue in issues
|
|
374
|
+
]
|
|
375
|
+
return _format_issues(Path(file_path).name, parsed)
|
|
100
376
|
|
|
101
|
-
|
|
102
|
-
|
|
377
|
+
except subprocess.TimeoutExpired:
|
|
378
|
+
return f" {Path(file_path).name}: hadolint timed out"
|
|
379
|
+
except Exception:
|
|
380
|
+
return ""
|
|
103
381
|
|
|
104
382
|
|
|
105
|
-
def
|
|
383
|
+
def lint_clippy(file_path: str) -> str:
|
|
384
|
+
"""Run clippy on a Rust file (conditional — only if cargo is in PATH)."""
|
|
385
|
+
cargo = _which("cargo")
|
|
386
|
+
if not cargo:
|
|
387
|
+
return ""
|
|
388
|
+
|
|
106
389
|
try:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
390
|
+
result = subprocess.run(
|
|
391
|
+
[cargo, "clippy", "--message-format=json", "--", "-W", "clippy::all"],
|
|
392
|
+
capture_output=True,
|
|
393
|
+
text=True,
|
|
394
|
+
timeout=SUBPROCESS_TIMEOUT,
|
|
395
|
+
cwd=str(Path(file_path).parent),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
lines = result.stdout.strip().splitlines()
|
|
399
|
+
parsed = []
|
|
400
|
+
target_name = Path(file_path).name
|
|
401
|
+
|
|
402
|
+
for line in lines:
|
|
403
|
+
try:
|
|
404
|
+
msg = json.loads(line)
|
|
405
|
+
except json.JSONDecodeError:
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
if msg.get("reason") != "compiler-message":
|
|
409
|
+
continue
|
|
410
|
+
inner = msg.get("message", {})
|
|
411
|
+
level = inner.get("level", "")
|
|
412
|
+
if level not in ("warning", "error"):
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
# Match diagnostics to the target file
|
|
416
|
+
spans = inner.get("spans", [])
|
|
417
|
+
line_num = 0
|
|
418
|
+
for span in spans:
|
|
419
|
+
if span.get("is_primary") and target_name in span.get("file_name", ""):
|
|
420
|
+
line_num = span.get("line_start", 0)
|
|
421
|
+
break
|
|
422
|
+
|
|
423
|
+
if line_num or not spans:
|
|
424
|
+
parsed.append(
|
|
425
|
+
{
|
|
426
|
+
"severity": level,
|
|
427
|
+
"line": line_num,
|
|
428
|
+
"message": inner.get("message", ""),
|
|
429
|
+
}
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
return _format_issues(Path(file_path).name, parsed)
|
|
433
|
+
|
|
434
|
+
except subprocess.TimeoutExpired:
|
|
435
|
+
return f" {Path(file_path).name}: clippy timed out"
|
|
436
|
+
except Exception:
|
|
437
|
+
return ""
|
|
438
|
+
|
|
110
439
|
|
|
111
|
-
|
|
112
|
-
sys.exit(0)
|
|
440
|
+
# ── Main ────────────────────────────────────────────────────────────
|
|
113
441
|
|
|
114
|
-
# Check if file exists
|
|
115
|
-
if not os.path.exists(file_path):
|
|
116
|
-
sys.exit(0)
|
|
117
442
|
|
|
118
|
-
|
|
443
|
+
def main():
|
|
444
|
+
try:
|
|
445
|
+
input_data = json.load(sys.stdin)
|
|
446
|
+
except (json.JSONDecodeError, ValueError):
|
|
447
|
+
sys.exit(0)
|
|
119
448
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
print(json.dumps({"additionalContext": message}))
|
|
449
|
+
if input_data.get("stop_hook_active"):
|
|
450
|
+
sys.exit(0)
|
|
123
451
|
|
|
452
|
+
session_id = input_data.get("session_id", "")
|
|
453
|
+
if not session_id:
|
|
124
454
|
sys.exit(0)
|
|
125
455
|
|
|
126
|
-
|
|
456
|
+
tmp_path = f"/tmp/claude-lint-files-{session_id}"
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
with open(tmp_path) as f:
|
|
460
|
+
raw_paths = f.read().splitlines()
|
|
461
|
+
except FileNotFoundError:
|
|
127
462
|
sys.exit(0)
|
|
128
|
-
except
|
|
129
|
-
print(f"Hook error: {e}", file=sys.stderr)
|
|
463
|
+
except OSError:
|
|
130
464
|
sys.exit(0)
|
|
465
|
+
finally:
|
|
466
|
+
try:
|
|
467
|
+
os.unlink(tmp_path)
|
|
468
|
+
except OSError:
|
|
469
|
+
pass
|
|
470
|
+
|
|
471
|
+
# Deduplicate, filter to existing files (no longer Python-only)
|
|
472
|
+
seen: set[str] = set()
|
|
473
|
+
paths: list[str] = []
|
|
474
|
+
for p in raw_paths:
|
|
475
|
+
p = p.strip()
|
|
476
|
+
if p and p not in seen and os.path.isfile(p):
|
|
477
|
+
seen.add(p)
|
|
478
|
+
paths.append(p)
|
|
479
|
+
|
|
480
|
+
if not paths:
|
|
481
|
+
sys.exit(0)
|
|
482
|
+
|
|
483
|
+
# Collect results grouped by linter
|
|
484
|
+
all_results: dict[str, list[str]] = {}
|
|
485
|
+
|
|
486
|
+
for path in paths:
|
|
487
|
+
ext = Path(path).suffix.lower()
|
|
488
|
+
name = Path(path).name
|
|
489
|
+
|
|
490
|
+
if ext in PYTHON_EXTS:
|
|
491
|
+
msg = lint_python_pyright(path)
|
|
492
|
+
if msg:
|
|
493
|
+
all_results.setdefault("Pyright", []).append(msg)
|
|
494
|
+
msg = lint_python_ruff(path)
|
|
495
|
+
if msg:
|
|
496
|
+
all_results.setdefault("Ruff", []).append(msg)
|
|
497
|
+
|
|
498
|
+
elif ext in BIOME_EXTS:
|
|
499
|
+
msg = lint_biome(path)
|
|
500
|
+
if msg:
|
|
501
|
+
all_results.setdefault("Biome", []).append(msg)
|
|
502
|
+
|
|
503
|
+
elif ext in SHELL_EXTS:
|
|
504
|
+
msg = lint_shellcheck(path)
|
|
505
|
+
if msg:
|
|
506
|
+
all_results.setdefault("ShellCheck", []).append(msg)
|
|
507
|
+
|
|
508
|
+
elif ext in GO_EXTS:
|
|
509
|
+
msg = lint_go_vet(path)
|
|
510
|
+
if msg:
|
|
511
|
+
all_results.setdefault("go vet", []).append(msg)
|
|
512
|
+
|
|
513
|
+
elif name == "Dockerfile" or ext == ".dockerfile":
|
|
514
|
+
msg = lint_hadolint(path)
|
|
515
|
+
if msg:
|
|
516
|
+
all_results.setdefault("hadolint", []).append(msg)
|
|
517
|
+
|
|
518
|
+
elif ext in RUST_EXTS:
|
|
519
|
+
msg = lint_clippy(path)
|
|
520
|
+
if msg:
|
|
521
|
+
all_results.setdefault("clippy", []).append(msg)
|
|
522
|
+
|
|
523
|
+
if all_results:
|
|
524
|
+
sections = []
|
|
525
|
+
for linter_name, results in all_results.items():
|
|
526
|
+
sections.append(
|
|
527
|
+
f"[Auto-linter] {linter_name} results:\n" + "\n".join(results)
|
|
528
|
+
)
|
|
529
|
+
output = "\n\n".join(sections)
|
|
530
|
+
print(json.dumps({"additionalContext": output}))
|
|
531
|
+
|
|
532
|
+
sys.exit(0)
|
|
131
533
|
|
|
132
534
|
|
|
133
535
|
if __name__ == "__main__":
|
package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/.claude-plugin/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-directive",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "17 custom agents, 16 coding skills, agent redirection, syntax validation, and skill auto-suggestion",
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "AnExiledDev"
|