Skip to content

Commit e33f609

Browse files
authored
fix: plugin manifest did not adhere to plugin scheme (#23)
1 parent 33ac3bc commit e33f609

4 files changed

Lines changed: 219 additions & 44 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,48 @@
1-
{
2-
"name": "very_good_ai_flutter_plugin",
3-
"description": "Very Good AI Flutter Plugin - Flutter & Dart development enhancement",
4-
"version": "0.0.1",
5-
"homepage": "https://github.com/VeryGoodOpenSource/very_good_ai_flutter_plugin",
6-
"author": {
7-
"name": "Very Good Ventures",
8-
"url": "https://verygood.ventures"
9-
},
10-
"tags": [
11-
"a11y",
12-
"accessibility",
13-
"analyze",
14-
"architecture",
15-
"bloc",
16-
"bloc-test",
17-
"code-quality",
18-
"dart",
19-
"flutter",
20-
"format",
21-
"go-router",
22-
"golden-testing",
23-
"hooks",
24-
"internationalization",
25-
"layered-architecture",
26-
"lint",
27-
"localization",
28-
"mcp",
29-
"mobile-security",
30-
"navigation",
31-
"owasp",
32-
"screen-reader",
33-
"security",
34-
"semantics",
35-
"state-management",
36-
"testing",
37-
"theming",
38-
"very-good-cli",
39-
"wcag",
40-
"widget-testing"
41-
]
42-
}
1+
{
2+
"name": "very-good-ai-flutter-plugin",
3+
"version": "0.0.1",
4+
"description": "Very Good AI Flutter Plugin - Flutter & Dart development enhancement",
5+
"homepage": "https://github.com/VeryGoodOpenSource/very_good_ai_flutter_plugin",
6+
"repository": "https://github.com/VeryGoodOpenSource/very_good_ai_flutter_plugin",
7+
"license": "MIT",
8+
"author": {
9+
"name": "Very Good Ventures",
10+
"url": "https://verygood.ventures",
11+
"email": "hello@verygood.ventures"
12+
},
13+
"skills": "./skills/",
14+
"hooks": "./hooks/hooks.json",
15+
"mcpServers": "./.mcp.json",
16+
"keywords": [
17+
"a11y",
18+
"accessibility",
19+
"analyze",
20+
"architecture",
21+
"bloc",
22+
"bloc-test",
23+
"code-quality",
24+
"dart",
25+
"flutter",
26+
"format",
27+
"go-router",
28+
"golden-testing",
29+
"hooks",
30+
"internationalization",
31+
"layered-architecture",
32+
"lint",
33+
"localization",
34+
"mcp",
35+
"mobile-security",
36+
"navigation",
37+
"owasp",
38+
"screen-reader",
39+
"security",
40+
"semantics",
41+
"state-management",
42+
"testing",
43+
"theming",
44+
"very-good-cli",
45+
"wcag",
46+
"widget-testing"
47+
]
48+
}

.github/workflows/ci.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,11 @@ jobs:
2929
steps:
3030
- uses: actions/checkout@v4
3131
- name: Validate SKILL.md Files
32-
run: bash ./scripts/validate_skills.sh
32+
run: bash ./scripts/validate_skills.sh
33+
manifest-validation:
34+
name: 📦 Manifest Validation
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v4
38+
- name: Validate Plugin Manifest
39+
run: bash ./scripts/validate_plugin_manifest.sh

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ You can also invoke skills directly as slash commands:
6565

6666
Every skill includes:
6767

68-
- **Non-negotiable standards**enforced conventions (e.g., `mocktail` over `mockito`, sealed classes for Bloc events)
68+
- **Core Standards**recommended conventions (e.g., `mocktail` over `mockito`, sealed classes for Bloc events)
6969
- **Architecture patterns** — folder structures and layered architecture guidance
7070
- **Code examples** — ready-to-adapt snippets following best practices
7171
- **Testing strategies** — unit, widget, and integration testing patterns
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/bin/bash
2+
# Validates .claude-plugin/plugin.json:
3+
# 1. Valid JSON syntax
4+
# 2. Required field: name (non-empty, kebab-case)
5+
# 3. Metadata field types (version, description, author, homepage, etc.)
6+
# 4. Component path field types (skills, hooks, mcpServers, etc.)
7+
# 5. No unknown top-level keys
8+
# 6. Referenced paths exist on disk
9+
10+
MANIFEST=".claude-plugin/plugin.json"
11+
errors=0
12+
13+
# --- Prerequisite: jq must be available ---
14+
if ! command -v jq &>/dev/null; then
15+
echo "::error::jq is required but not installed"
16+
exit 1
17+
fi
18+
19+
# --- Check 1: JSON syntax ---
20+
if ! jq empty "$MANIFEST" 2>/dev/null; then
21+
echo "::error file=$MANIFEST::Invalid JSON syntax"
22+
exit 1
23+
fi
24+
25+
manifest=$(cat "$MANIFEST")
26+
27+
# --- Check 2: name field (required, non-empty, kebab-case) ---
28+
name=$(echo "$manifest" | jq -r '.name // empty')
29+
if [[ -z "$name" ]]; then
30+
echo "::error file=$MANIFEST::Missing or empty required field 'name'"
31+
errors=$((errors + 1))
32+
elif [[ ! "$name" =~ ^[a-z0-9-]+$ ]]; then
33+
echo "::error file=$MANIFEST::Invalid name '$name' — must match ^[a-z0-9-]+$"
34+
errors=$((errors + 1))
35+
fi
36+
37+
# --- Check 3: Metadata field types ---
38+
39+
# String fields (if present)
40+
for field in version description homepage repository license; do
41+
type=$(echo "$manifest" | jq -r "if has(\"$field\") then (.${field} | type) else \"absent\" end")
42+
if [[ "$type" != "absent" && "$type" != "string" ]]; then
43+
echo "::error file=$MANIFEST::'$field' must be a string, got $type"
44+
errors=$((errors + 1))
45+
fi
46+
done
47+
48+
# author (object with optional string fields, if present)
49+
author_type=$(echo "$manifest" | jq -r 'if has("author") then (.author | type) else "absent" end')
50+
if [[ "$author_type" != "absent" ]]; then
51+
if [[ "$author_type" != "object" ]]; then
52+
echo "::error file=$MANIFEST::'author' must be an object, got $author_type"
53+
errors=$((errors + 1))
54+
else
55+
for sub in name email url; do
56+
sub_type=$(echo "$manifest" | jq -r "if .author | has(\"$sub\") then (.author.${sub} | type) else \"absent\" end")
57+
if [[ "$sub_type" != "absent" && "$sub_type" != "string" ]]; then
58+
echo "::error file=$MANIFEST::'author.$sub' must be a string, got $sub_type"
59+
errors=$((errors + 1))
60+
fi
61+
done
62+
fi
63+
fi
64+
65+
# keywords (array of strings, if present)
66+
keywords_type=$(echo "$manifest" | jq -r 'if has("keywords") then (.keywords | type) else "absent" end')
67+
if [[ "$keywords_type" != "absent" ]]; then
68+
if [[ "$keywords_type" != "array" ]]; then
69+
echo "::error file=$MANIFEST::'keywords' must be an array, got $keywords_type"
70+
errors=$((errors + 1))
71+
else
72+
non_string_count=$(echo "$manifest" | jq '[.keywords[] | type != "string"] | map(select(.)) | length')
73+
if [[ "$non_string_count" -gt 0 ]]; then
74+
echo "::error file=$MANIFEST::'keywords' must contain only strings, found $non_string_count non-string element(s)"
75+
errors=$((errors + 1))
76+
fi
77+
fi
78+
fi
79+
80+
# --- Check 4: Component path field types ---
81+
82+
# Fields that accept string, array of strings, or object
83+
for field in skills hooks mcpServers lspServers outputStyles; do
84+
type=$(echo "$manifest" | jq -r "if has(\"$field\") then (.${field} | type) else \"absent\" end")
85+
if [[ "$type" == "absent" ]]; then
86+
continue
87+
fi
88+
if [[ "$type" != "string" && "$type" != "array" && "$type" != "object" ]]; then
89+
echo "::error file=$MANIFEST::'$field' must be a string, array of strings, or object — got $type"
90+
errors=$((errors + 1))
91+
elif [[ "$type" == "array" ]]; then
92+
non_string_count=$(echo "$manifest" | jq "[.${field}[] | type != \"string\"] | map(select(.)) | length")
93+
if [[ "$non_string_count" -gt 0 ]]; then
94+
echo "::error file=$MANIFEST::'$field' array must contain only strings, found $non_string_count non-string element(s)"
95+
errors=$((errors + 1))
96+
fi
97+
fi
98+
done
99+
100+
# Fields that accept string or array of strings only
101+
for field in commands agents; do
102+
type=$(echo "$manifest" | jq -r "if has(\"$field\") then (.${field} | type) else \"absent\" end")
103+
if [[ "$type" == "absent" ]]; then
104+
continue
105+
fi
106+
if [[ "$type" != "string" && "$type" != "array" ]]; then
107+
echo "::error file=$MANIFEST::'$field' must be a string or array of strings — got $type"
108+
errors=$((errors + 1))
109+
elif [[ "$type" == "array" ]]; then
110+
non_string_count=$(echo "$manifest" | jq "[.${field}[] | type != \"string\"] | map(select(.)) | length")
111+
if [[ "$non_string_count" -gt 0 ]]; then
112+
echo "::error file=$MANIFEST::'$field' array must contain only strings, found $non_string_count non-string element(s)"
113+
errors=$((errors + 1))
114+
fi
115+
fi
116+
done
117+
118+
# --- Check 5: No unknown top-level keys ---
119+
known_keys='["name","version","description","author","homepage","repository","license","keywords","commands","agents","skills","hooks","mcpServers","outputStyles","lspServers"]'
120+
unknown=$(echo "$manifest" | jq -r --argjson known "$known_keys" 'keys[] | select(. as $k | $known | index($k) | not)')
121+
if [[ -n "$unknown" ]]; then
122+
while IFS= read -r key; do
123+
echo "::warning file=$MANIFEST::Unknown top-level key '$key'"
124+
done <<< "$unknown"
125+
fi
126+
127+
# --- Check 6: Path validation ---
128+
# Helper: check a single path exists relative to project root
129+
check_path() {
130+
local field="$1"
131+
local path="$2"
132+
if [[ ! -e "$path" ]]; then
133+
echo "::error file=$MANIFEST::Path '$path' referenced by '$field' does not exist"
134+
errors=$((errors + 1))
135+
fi
136+
}
137+
138+
for field in skills hooks mcpServers lspServers outputStyles commands agents; do
139+
type=$(echo "$manifest" | jq -r "if has(\"$field\") then (.${field} | type) else \"absent\" end")
140+
if [[ "$type" == "absent" ]]; then
141+
continue
142+
fi
143+
144+
if [[ "$type" == "string" ]]; then
145+
path=$(echo "$manifest" | jq -r ".${field}")
146+
check_path "$field" "$path"
147+
elif [[ "$type" == "array" ]]; then
148+
while IFS= read -r path; do
149+
check_path "$field" "$path"
150+
done < <(echo "$manifest" | jq -r ".${field}[]")
151+
fi
152+
# Objects (inline configs) — skip path validation
153+
done
154+
155+
# --- Summary ---
156+
echo ""
157+
if [[ $errors -gt 0 ]]; then
158+
echo "❌ Manifest validation failed with $errors error(s)."
159+
exit 1
160+
else
161+
echo "✅ Plugin manifest is valid."
162+
fi

0 commit comments

Comments
 (0)