frontmatter-guard 0.1.0__tar.gz
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.
- frontmatter_guard-0.1.0/LICENSE +21 -0
- frontmatter_guard-0.1.0/PKG-INFO +133 -0
- frontmatter_guard-0.1.0/README.md +120 -0
- frontmatter_guard-0.1.0/pyproject.toml +22 -0
- frontmatter_guard-0.1.0/setup.cfg +4 -0
- frontmatter_guard-0.1.0/src/frontmatter_guard/__init__.py +5 -0
- frontmatter_guard-0.1.0/src/frontmatter_guard/__main__.py +5 -0
- frontmatter_guard-0.1.0/src/frontmatter_guard/cli.py +201 -0
- frontmatter_guard-0.1.0/src/frontmatter_guard/schema.py +491 -0
- frontmatter_guard-0.1.0/src/frontmatter_guard.egg-info/PKG-INFO +133 -0
- frontmatter_guard-0.1.0/src/frontmatter_guard.egg-info/SOURCES.txt +14 -0
- frontmatter_guard-0.1.0/src/frontmatter_guard.egg-info/dependency_links.txt +1 -0
- frontmatter_guard-0.1.0/src/frontmatter_guard.egg-info/entry_points.txt +2 -0
- frontmatter_guard-0.1.0/src/frontmatter_guard.egg-info/top_level.txt +1 -0
- frontmatter_guard-0.1.0/tests/test_cli.py +156 -0
- frontmatter_guard-0.1.0/tests/test_frontmatter.py +174 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hao li
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: frontmatter-guard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Semantic linter for Claude Code skill/plugin frontmatter — catches silent key typos official validators miss
|
|
5
|
+
Author: hao li
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/hahahahahahahahah6/frontmatter-guard
|
|
8
|
+
Keywords: claude-code,frontmatter,linter,skills,plugins,developer-tools
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# frontmatter-guard
|
|
15
|
+
|
|
16
|
+
A semantic linter for Claude Code skill/plugin frontmatter. Catches the silent
|
|
17
|
+
mistakes the official validator waves through.
|
|
18
|
+
|
|
19
|
+
## The problem
|
|
20
|
+
|
|
21
|
+
`claude plugin validate` checks that your plugin is structurally compatible.
|
|
22
|
+
It does **not** check that your frontmatter keys actually mean anything. I
|
|
23
|
+
found this out the embarrassing way: I had written `effort: high` in a command
|
|
24
|
+
frontmatter, expecting it to constrain the model. `claude plugin validate`
|
|
25
|
+
passed it without a word. In reality `effort` is not a Claude Code frontmatter
|
|
26
|
+
key at all — it is a Codex concept — so my setting silently fell back to the
|
|
27
|
+
session default and I had no idea.
|
|
28
|
+
|
|
29
|
+
Misspelled keys and misspelled hook events fail the same silent way:
|
|
30
|
+
`PreToolUs` never fires, `licence` never licenses anything, and nothing ever
|
|
31
|
+
tells you.
|
|
32
|
+
|
|
33
|
+
frontmatter-guard closes that gap: it knows the real key vocabulary and the
|
|
34
|
+
real hook event list, and it tells you — with `file:line`, a rule name, and a
|
|
35
|
+
fix suggestion — when something you wrote does nothing.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install frontmatter-guard
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Zero dependencies. Python 3.9+. Works anywhere, including pre-commit and CI.
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# lint one file or a whole directory
|
|
49
|
+
frontmatter-guard check SKILL.md
|
|
50
|
+
frontmatter-guard check .claude/ --strict
|
|
51
|
+
|
|
52
|
+
# machine-readable output for CI
|
|
53
|
+
frontmatter-guard check . --format json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Example output:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
commands/deploy.md:4: error [unknown-key] Unknown key 'effort'.
|
|
60
|
+
fix: Remove the key -- unknown keys are silently ignored, so it currently does nothing.
|
|
61
|
+
commands/deploy.md:5: error [unknown-key] Unknown key 'licence'. Did you mean 'license'?
|
|
62
|
+
fix: Rename 'licence' to 'license'. Unknown keys are silently ignored, so fix the typo or remove the key.
|
|
63
|
+
commands/deploy.md:7: error [unknown-hook-event] Unknown hook event 'PreToolUs'. Did you mean 'PreToolUse'?
|
|
64
|
+
fix: Rename 'PreToolUs' to 'PreToolUse'. Unknown hook events never fire.
|
|
65
|
+
frontmatter-guard: 3 error(s), 0 warning(s) in 1 file(s)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Exit codes are CI-ready: exit `1` when any error-level finding exists (or any
|
|
69
|
+
warning under `--strict`); exit `0` when clean; exit `2` on usage errors.
|
|
70
|
+
|
|
71
|
+
## Rules
|
|
72
|
+
|
|
73
|
+
| Rule | Severity | What it catches |
|
|
74
|
+
|------|----------|-----------------|
|
|
75
|
+
| `unknown-key` | error | Top-level key not in the known skill/plugin vocabulary. Suggests the closest real key via difflib ("Did you mean 'license'?"). |
|
|
76
|
+
| `unknown-hook-event` | error | Hook event name not in Claude Code's event list (`PreToolUse`, `PostToolUse`, `SessionStart`, …). Unknown events never fire. |
|
|
77
|
+
| `missing-required` | error | `name` or `description` absent or empty. |
|
|
78
|
+
| `bad-version` | warning | `version` that isn't semver-ish (`1.2.3`, `v0.1`, `2.0.0-beta`). |
|
|
79
|
+
| `bad-type` | warning | Value of the wrong type (`hooks:` as a string, `name:` as a number, …). |
|
|
80
|
+
| `parse-warning` | warning | Frontmatter uses YAML beyond the supported subset — the file is still linted, not crashed. |
|
|
81
|
+
|
|
82
|
+
Scans `*.md` frontmatter (`---` fences) and `plugin.json` files. The built-in
|
|
83
|
+
YAML parser is stdlib-only and covers the shapes frontmatter actually uses:
|
|
84
|
+
scalars, block maps/sequences, inline flow collections, literal blocks.
|
|
85
|
+
Anything fancier gets a warning, never an exception.
|
|
86
|
+
|
|
87
|
+
## How it differs
|
|
88
|
+
|
|
89
|
+
- **vs `claude plugin validate` (official):** the official validator checks
|
|
90
|
+
compatibility and structure — "will this plugin load?" frontmatter-guard
|
|
91
|
+
checks semantic strictness — "does everything you wrote actually do
|
|
92
|
+
something?" Unknown keys sail through the official check silently; they
|
|
93
|
+
fail here.
|
|
94
|
+
- **vs damson/skill-lint:** that project is a CI action doing structural
|
|
95
|
+
checks on skills. frontmatter-guard is a local, stdlib-only CLI doing
|
|
96
|
+
*semantic* checks (typo suggestions, event-name validation, type/format
|
|
97
|
+
checks). Use it in pre-commit for instant feedback *and* in CI for
|
|
98
|
+
enforcement — same command, both places.
|
|
99
|
+
|
|
100
|
+
## CI integration
|
|
101
|
+
|
|
102
|
+
GitHub Actions:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
- name: Lint skill/plugin frontmatter
|
|
106
|
+
run: |
|
|
107
|
+
pip install frontmatter-guard
|
|
108
|
+
frontmatter-guard check . --strict
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
pre-commit:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
repos:
|
|
115
|
+
- repo: local
|
|
116
|
+
hooks:
|
|
117
|
+
- id: frontmatter-guard
|
|
118
|
+
name: frontmatter-guard
|
|
119
|
+
entry: frontmatter-guard check
|
|
120
|
+
language: system
|
|
121
|
+
types: [markdown]
|
|
122
|
+
args: [--strict]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
python3 -m unittest discover -s tests # 34 tests, stdlib only
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frontmatter-guard
|
|
2
|
+
|
|
3
|
+
A semantic linter for Claude Code skill/plugin frontmatter. Catches the silent
|
|
4
|
+
mistakes the official validator waves through.
|
|
5
|
+
|
|
6
|
+
## The problem
|
|
7
|
+
|
|
8
|
+
`claude plugin validate` checks that your plugin is structurally compatible.
|
|
9
|
+
It does **not** check that your frontmatter keys actually mean anything. I
|
|
10
|
+
found this out the embarrassing way: I had written `effort: high` in a command
|
|
11
|
+
frontmatter, expecting it to constrain the model. `claude plugin validate`
|
|
12
|
+
passed it without a word. In reality `effort` is not a Claude Code frontmatter
|
|
13
|
+
key at all — it is a Codex concept — so my setting silently fell back to the
|
|
14
|
+
session default and I had no idea.
|
|
15
|
+
|
|
16
|
+
Misspelled keys and misspelled hook events fail the same silent way:
|
|
17
|
+
`PreToolUs` never fires, `licence` never licenses anything, and nothing ever
|
|
18
|
+
tells you.
|
|
19
|
+
|
|
20
|
+
frontmatter-guard closes that gap: it knows the real key vocabulary and the
|
|
21
|
+
real hook event list, and it tells you — with `file:line`, a rule name, and a
|
|
22
|
+
fix suggestion — when something you wrote does nothing.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install frontmatter-guard
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Zero dependencies. Python 3.9+. Works anywhere, including pre-commit and CI.
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# lint one file or a whole directory
|
|
36
|
+
frontmatter-guard check SKILL.md
|
|
37
|
+
frontmatter-guard check .claude/ --strict
|
|
38
|
+
|
|
39
|
+
# machine-readable output for CI
|
|
40
|
+
frontmatter-guard check . --format json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Example output:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
commands/deploy.md:4: error [unknown-key] Unknown key 'effort'.
|
|
47
|
+
fix: Remove the key -- unknown keys are silently ignored, so it currently does nothing.
|
|
48
|
+
commands/deploy.md:5: error [unknown-key] Unknown key 'licence'. Did you mean 'license'?
|
|
49
|
+
fix: Rename 'licence' to 'license'. Unknown keys are silently ignored, so fix the typo or remove the key.
|
|
50
|
+
commands/deploy.md:7: error [unknown-hook-event] Unknown hook event 'PreToolUs'. Did you mean 'PreToolUse'?
|
|
51
|
+
fix: Rename 'PreToolUs' to 'PreToolUse'. Unknown hook events never fire.
|
|
52
|
+
frontmatter-guard: 3 error(s), 0 warning(s) in 1 file(s)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Exit codes are CI-ready: exit `1` when any error-level finding exists (or any
|
|
56
|
+
warning under `--strict`); exit `0` when clean; exit `2` on usage errors.
|
|
57
|
+
|
|
58
|
+
## Rules
|
|
59
|
+
|
|
60
|
+
| Rule | Severity | What it catches |
|
|
61
|
+
|------|----------|-----------------|
|
|
62
|
+
| `unknown-key` | error | Top-level key not in the known skill/plugin vocabulary. Suggests the closest real key via difflib ("Did you mean 'license'?"). |
|
|
63
|
+
| `unknown-hook-event` | error | Hook event name not in Claude Code's event list (`PreToolUse`, `PostToolUse`, `SessionStart`, …). Unknown events never fire. |
|
|
64
|
+
| `missing-required` | error | `name` or `description` absent or empty. |
|
|
65
|
+
| `bad-version` | warning | `version` that isn't semver-ish (`1.2.3`, `v0.1`, `2.0.0-beta`). |
|
|
66
|
+
| `bad-type` | warning | Value of the wrong type (`hooks:` as a string, `name:` as a number, …). |
|
|
67
|
+
| `parse-warning` | warning | Frontmatter uses YAML beyond the supported subset — the file is still linted, not crashed. |
|
|
68
|
+
|
|
69
|
+
Scans `*.md` frontmatter (`---` fences) and `plugin.json` files. The built-in
|
|
70
|
+
YAML parser is stdlib-only and covers the shapes frontmatter actually uses:
|
|
71
|
+
scalars, block maps/sequences, inline flow collections, literal blocks.
|
|
72
|
+
Anything fancier gets a warning, never an exception.
|
|
73
|
+
|
|
74
|
+
## How it differs
|
|
75
|
+
|
|
76
|
+
- **vs `claude plugin validate` (official):** the official validator checks
|
|
77
|
+
compatibility and structure — "will this plugin load?" frontmatter-guard
|
|
78
|
+
checks semantic strictness — "does everything you wrote actually do
|
|
79
|
+
something?" Unknown keys sail through the official check silently; they
|
|
80
|
+
fail here.
|
|
81
|
+
- **vs damson/skill-lint:** that project is a CI action doing structural
|
|
82
|
+
checks on skills. frontmatter-guard is a local, stdlib-only CLI doing
|
|
83
|
+
*semantic* checks (typo suggestions, event-name validation, type/format
|
|
84
|
+
checks). Use it in pre-commit for instant feedback *and* in CI for
|
|
85
|
+
enforcement — same command, both places.
|
|
86
|
+
|
|
87
|
+
## CI integration
|
|
88
|
+
|
|
89
|
+
GitHub Actions:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
- name: Lint skill/plugin frontmatter
|
|
93
|
+
run: |
|
|
94
|
+
pip install frontmatter-guard
|
|
95
|
+
frontmatter-guard check . --strict
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
pre-commit:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
repos:
|
|
102
|
+
- repo: local
|
|
103
|
+
hooks:
|
|
104
|
+
- id: frontmatter-guard
|
|
105
|
+
name: frontmatter-guard
|
|
106
|
+
entry: frontmatter-guard check
|
|
107
|
+
language: system
|
|
108
|
+
types: [markdown]
|
|
109
|
+
args: [--strict]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
python3 -m unittest discover -s tests # 34 tests, stdlib only
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "frontmatter-guard"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Semantic linter for Claude Code skill/plugin frontmatter — catches silent key typos official validators miss"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "hao li" }]
|
|
13
|
+
keywords = ["claude-code", "frontmatter", "linter", "skills", "plugins", "developer-tools"]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
frontmatter-guard = "frontmatter_guard.cli:main"
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/hahahahahahahahah6/frontmatter-guard"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Command-line interface for frontmatter-guard."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .schema import (
|
|
10
|
+
SEVERITY_ERROR,
|
|
11
|
+
SEVERITY_WARNING,
|
|
12
|
+
extract_md_frontmatter,
|
|
13
|
+
lint_frontmatter,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
SCAN_EXTENSIONS = (".md",)
|
|
17
|
+
SCAN_FILENAMES = ("plugin.json",)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def iter_target_files(path):
|
|
21
|
+
"""Yield (path) for every scannable file under path."""
|
|
22
|
+
if os.path.isfile(path):
|
|
23
|
+
yield path
|
|
24
|
+
return
|
|
25
|
+
for root, dirs, files in os.walk(path):
|
|
26
|
+
dirs[:] = [d for d in dirs
|
|
27
|
+
if d not in (".git", "__pycache__", "node_modules",
|
|
28
|
+
".venv", "venv", "dist", "build")]
|
|
29
|
+
for name in sorted(files):
|
|
30
|
+
full = os.path.join(root, name)
|
|
31
|
+
if name.endswith(SCAN_EXTENSIONS) or name in SCAN_FILENAMES:
|
|
32
|
+
yield full
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def lint_file(path):
|
|
36
|
+
"""Lint one file. Returns a list of finding dicts."""
|
|
37
|
+
try:
|
|
38
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
39
|
+
text = handle.read()
|
|
40
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
41
|
+
return [{
|
|
42
|
+
"file": path,
|
|
43
|
+
"line": 1,
|
|
44
|
+
"rule": "unreadable",
|
|
45
|
+
"severity": SEVERITY_ERROR,
|
|
46
|
+
"message": "Could not read file: %s" % exc,
|
|
47
|
+
"fix": "Check file permissions and encoding (UTF-8 expected).",
|
|
48
|
+
}]
|
|
49
|
+
|
|
50
|
+
findings = []
|
|
51
|
+
name = os.path.basename(path)
|
|
52
|
+
|
|
53
|
+
if name.endswith(".md"):
|
|
54
|
+
extracted = extract_md_frontmatter(text)
|
|
55
|
+
if extracted is None:
|
|
56
|
+
if name == "SKILL.md":
|
|
57
|
+
findings.append({
|
|
58
|
+
"file": path,
|
|
59
|
+
"line": 1,
|
|
60
|
+
"rule": "no-frontmatter",
|
|
61
|
+
"severity": SEVERITY_WARNING,
|
|
62
|
+
"message": "SKILL.md has no frontmatter block.",
|
|
63
|
+
"fix": "Add a --- fenced frontmatter block with at "
|
|
64
|
+
"least name and description.",
|
|
65
|
+
})
|
|
66
|
+
return findings
|
|
67
|
+
data, key_lines, start_line, parse_warnings = extracted
|
|
68
|
+
for warning in parse_warnings:
|
|
69
|
+
findings.append({
|
|
70
|
+
"file": path,
|
|
71
|
+
"line": start_line,
|
|
72
|
+
"rule": "parse-warning",
|
|
73
|
+
"severity": SEVERITY_WARNING,
|
|
74
|
+
"message": "Frontmatter YAML: %s." % warning,
|
|
75
|
+
"fix": "Simplify the frontmatter to plain key: value pairs, "
|
|
76
|
+
"lists and nested maps.",
|
|
77
|
+
})
|
|
78
|
+
findings.extend(lint_frontmatter(data, key_lines, path, start_line))
|
|
79
|
+
return findings
|
|
80
|
+
|
|
81
|
+
# plugin.json and friends: JSON object, same key vocabulary
|
|
82
|
+
try:
|
|
83
|
+
data = json.loads(text)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
return [{
|
|
86
|
+
"file": path,
|
|
87
|
+
"line": 1,
|
|
88
|
+
"rule": "parse-warning",
|
|
89
|
+
"severity": SEVERITY_ERROR,
|
|
90
|
+
"message": "Invalid JSON: %s" % exc,
|
|
91
|
+
"fix": "Fix the JSON syntax.",
|
|
92
|
+
}]
|
|
93
|
+
if not isinstance(data, dict):
|
|
94
|
+
return [{
|
|
95
|
+
"file": path,
|
|
96
|
+
"line": 1,
|
|
97
|
+
"rule": "bad-type",
|
|
98
|
+
"severity": SEVERITY_ERROR,
|
|
99
|
+
"message": "Top-level JSON value must be an object.",
|
|
100
|
+
"fix": "Make the top level a JSON object with plugin keys.",
|
|
101
|
+
}]
|
|
102
|
+
key_lines = {key: 1 for key in data}
|
|
103
|
+
return lint_frontmatter(data, key_lines, path, start_line=1)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def check_targets(targets):
|
|
107
|
+
findings = []
|
|
108
|
+
files = []
|
|
109
|
+
for target in targets:
|
|
110
|
+
if not os.path.exists(target):
|
|
111
|
+
return None, ["no such file or directory: %s" % target]
|
|
112
|
+
for path in iter_target_files(target):
|
|
113
|
+
files.append(path)
|
|
114
|
+
findings.extend(lint_file(path))
|
|
115
|
+
return (files, findings), []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def format_text(files, findings):
|
|
119
|
+
out = []
|
|
120
|
+
for finding in findings:
|
|
121
|
+
out.append("%s:%s: %s [%s] %s"
|
|
122
|
+
% (finding["file"], finding["line"],
|
|
123
|
+
finding["severity"], finding["rule"],
|
|
124
|
+
finding["message"]))
|
|
125
|
+
if finding.get("fix"):
|
|
126
|
+
out.append(" fix: %s" % finding["fix"])
|
|
127
|
+
errors = sum(1 for f in findings if f["severity"] == SEVERITY_ERROR)
|
|
128
|
+
warnings = sum(1 for f in findings if f["severity"] == SEVERITY_WARNING)
|
|
129
|
+
out.append("frontmatter-guard: %d error(s), %d warning(s) in %d file(s)"
|
|
130
|
+
% (errors, warnings, len(files)))
|
|
131
|
+
return "\n".join(out)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def format_json(files, findings):
|
|
135
|
+
errors = sum(1 for f in findings if f["severity"] == SEVERITY_ERROR)
|
|
136
|
+
warnings = sum(1 for f in findings if f["severity"] == SEVERITY_WARNING)
|
|
137
|
+
return json.dumps({
|
|
138
|
+
"tool": "frontmatter-guard",
|
|
139
|
+
"version": __version__,
|
|
140
|
+
"files": files,
|
|
141
|
+
"findings": findings,
|
|
142
|
+
"summary": {
|
|
143
|
+
"files": len(files),
|
|
144
|
+
"errors": errors,
|
|
145
|
+
"warnings": warnings,
|
|
146
|
+
},
|
|
147
|
+
}, indent=2)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build_parser():
|
|
151
|
+
parser = argparse.ArgumentParser(
|
|
152
|
+
prog="frontmatter-guard",
|
|
153
|
+
description="Semantic linter for Claude Code skill/plugin "
|
|
154
|
+
"frontmatter.")
|
|
155
|
+
parser.add_argument("--version", action="version",
|
|
156
|
+
version="frontmatter-guard %s" % __version__)
|
|
157
|
+
sub = parser.add_subparsers(dest="command")
|
|
158
|
+
check = sub.add_parser("check", help="Lint frontmatter in a file or "
|
|
159
|
+
"directory.")
|
|
160
|
+
check.add_argument("targets", nargs="+",
|
|
161
|
+
help="File(s) or directorie(s) to scan.")
|
|
162
|
+
check.add_argument("--format", choices=("text", "json"), default="text",
|
|
163
|
+
help="Output format (default: text).")
|
|
164
|
+
check.add_argument("--strict", action="store_true",
|
|
165
|
+
help="Treat warnings as failures (exit 1).")
|
|
166
|
+
return parser
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main(argv=None):
|
|
170
|
+
parser = build_parser()
|
|
171
|
+
args = parser.parse_args(argv)
|
|
172
|
+
|
|
173
|
+
if args.command != "check":
|
|
174
|
+
parser.print_help()
|
|
175
|
+
return 2
|
|
176
|
+
|
|
177
|
+
result, errors = check_targets(args.targets)
|
|
178
|
+
if result is None:
|
|
179
|
+
for error in errors:
|
|
180
|
+
print("frontmatter-guard: error: %s" % error, file=sys.stderr)
|
|
181
|
+
return 2
|
|
182
|
+
files, findings = result
|
|
183
|
+
|
|
184
|
+
if args.format == "json":
|
|
185
|
+
print(format_json(files, findings))
|
|
186
|
+
else:
|
|
187
|
+
print(format_text(files, findings))
|
|
188
|
+
|
|
189
|
+
error_count = sum(1 for f in findings
|
|
190
|
+
if f["severity"] == SEVERITY_ERROR)
|
|
191
|
+
warning_count = sum(1 for f in findings
|
|
192
|
+
if f["severity"] == SEVERITY_WARNING)
|
|
193
|
+
if error_count > 0:
|
|
194
|
+
return 1
|
|
195
|
+
if args.strict and warning_count > 0:
|
|
196
|
+
return 1
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
sys.exit(main())
|