-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathci
More file actions
executable file
·172 lines (151 loc) · 6.78 KB
/
Copy pathci
File metadata and controls
executable file
·172 lines (151 loc) · 6.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
#!/usr/bin/env bash
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
git diff --check
node --input-type=module <<'NODE'
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
function walk(dir) {
const out = [];
for (const entry of readdirSync(dir)) {
if (entry === '.git') continue;
const path = join(dir, entry);
const stat = statSync(path);
if (stat.isDirectory()) out.push(...walk(path));
else out.push(path.replace(/^\.\//, ''));
}
return out;
}
for (const file of walk('.').filter((path) => path.endsWith('plugin.json') || path.endsWith('marketplace.json'))) {
JSON.parse(readFileSync(file, 'utf8'));
}
const files = new Set(walk('.'));
for (const file of [...files]) {
const parts = file.split('/');
for (let i = 1; i < parts.length; i++) {
files.add(`${parts.slice(0, i).join('/')}/`);
}
}
const readme = readFileSync('README.md', 'utf8');
const missingLinks = [];
for (const match of readme.matchAll(/\[[^\]]+\]\((\.\/?[^)#?]+)(?:#[^)]*)?\)/g)) {
let path = match[1].replace(/^\.\//, '');
if (match[1].endsWith('/') && !path.endsWith('/')) path += '/';
if (!files.has(path)) {
const line = readme.slice(0, match.index).split('\n').length;
missingLinks.push(`${line}: ${match[1]}`);
}
}
if (missingLinks.length) {
throw new Error(`Broken README relative links:\n${missingLinks.join('\n')}`);
}
const plugins = ['compose-agent', 'jetpack-compose-audit'];
const marketplace = JSON.parse(readFileSync('.claude-plugin/marketplace.json', 'utf8'));
const marketplacePaths = new Map(marketplace.plugins.map((plugin) => [plugin.name, plugin.source.path]));
for (const plugin of plugins) {
const claudeManifest = JSON.parse(readFileSync(`skills/${plugin}/.claude-plugin/plugin.json`, 'utf8'));
const cursorManifest = JSON.parse(readFileSync(`skills/${plugin}/.cursor-plugin/plugin.json`, 'utf8'));
if (!existsSync(`skills/${plugin}/SKILL.md`)) {
throw new Error(`skills/${plugin} is missing SKILL.md`);
}
if (existsSync(`skills/${plugin}/skills`)) {
throw new Error(`skills/${plugin}/skills must not exist; use direct skills/<name>/SKILL.md layout`);
}
if (marketplacePaths.get(plugin) !== `skills/${plugin}`) {
throw new Error(`marketplace path for ${plugin} must be skills/${plugin}`);
}
if ('skills' in claudeManifest) {
throw new Error(`skills/${plugin}/.claude-plugin/plugin.json must use default skills/ discovery`);
}
if (claudeManifest.version !== cursorManifest.version) {
throw new Error(`skills/${plugin} manifest versions differ`);
}
if (!readFileSync(`skills/${plugin}/SKILL.md`, 'utf8').includes(`name: ${plugin}`)) {
throw new Error(`skills/${plugin}/SKILL.md frontmatter name must be ${plugin}`);
}
}
const composeSkill = readFileSync('skills/compose-agent/SKILL.md', 'utf8');
const composeVersion = JSON.parse(readFileSync('skills/compose-agent/.claude-plugin/plugin.json', 'utf8')).version;
if (!composeSkill.includes(`version: "${composeVersion}"`)) {
throw new Error('compose-agent SKILL.md metadata version does not match manifest version');
}
const composeAgentDocs = [...files]
.filter((file) => file.startsWith('skills/compose-agent/') && file.endsWith('.md'))
.map((file) => readFileSync(file, 'utf8'))
.join('\n');
if (
!composeAgentDocs.includes('windowSplashScreenAnimatedIcon') ||
!/animated[- ]vector/i.test(composeAgentDocs) ||
!/108\s*dp/i.test(composeAgentDocs) ||
!/160\s*dp/i.test(composeAgentDocs) ||
!/blurr/i.test(composeAgentDocs)
) {
throw new Error('compose-agent docs must cover the Android 12+ static splash icon blur workaround');
}
const auditDocs = [...files]
.filter((file) => file.startsWith('skills/jetpack-compose-audit/') && file.endsWith('.md'))
.map((file) => readFileSync(file, 'utf8'))
.join('\n');
if (
!auditDocs.includes('Android Launch UX') ||
!auditDocs.includes('windowSplashScreenAnimatedIcon') ||
!auditDocs.includes('drawable-v31') ||
!/static splash icon may render blurry/i.test(auditDocs) ||
!/non-scored|not score/i.test(auditDocs) ||
!auditDocs.includes('https://issuetracker.google.com/issues/520672537')
) {
throw new Error('jetpack-compose-audit docs must detect and report the Android 12+ static splash icon blur risk');
}
if (
!composeAgentDocs.includes('references/animation.md') ||
!composeAgentDocs.includes('updateTransition') ||
!composeAgentDocs.includes('snapTo') ||
!composeAgentDocs.includes('MotionDurationScale') ||
!composeAgentDocs.includes('animateEnterExit')
) {
throw new Error('compose-agent animation reference must cover updateTransition, gesture snapTo, reduced motion/MotionDurationScale, and animateEnterExit');
}
if (
!auditDocs.includes('Animation performance signals') ||
!auditDocs.includes('Animation side-effect signals') ||
!/Animation phase correctness/i.test(auditDocs)
) {
throw new Error('jetpack-compose-audit docs must surface animation findings in Performance and Side Effects report sections');
}
if (!readFileSync('skills/compose-agent/references/performance.md', 'utf8').includes('references/animation.md')) {
throw new Error('compose-agent performance.md must cross-link references/animation.md');
}
if (
!composeAgentDocs.includes('references/paging.md') ||
!composeAgentDocs.includes('collectAsLazyPagingItems') ||
!composeAgentDocs.includes('itemKey') ||
!composeAgentDocs.includes('LoadState') ||
!composeAgentDocs.includes('Golden Path') ||
!composeAgentDocs.includes('PagingData.from')
) {
throw new Error('compose-agent paging reference must cover collectAsLazyPagingItems, itemKey, LoadState, golden path, and PagingData.from preview pattern');
}
if (!readFileSync('skills/compose-agent/references/performance.md', 'utf8').includes('references/paging.md')) {
throw new Error('compose-agent performance.md must cross-link references/paging.md');
}
if (
!auditDocs.includes('Paging list correctness') ||
!auditDocs.includes('Paging load-state handling') ||
!auditDocs.includes('Paging list signals') ||
!auditDocs.includes('Paging load-state signals') ||
!auditDocs.includes('Paging side-effect signals')
) {
throw new Error('jetpack-compose-audit docs must surface paging findings in Performance, State, and Side Effects report sections');
}
const auditSkill = readFileSync('skills/jetpack-compose-audit/SKILL.md', 'utf8');
const auditVersion = JSON.parse(readFileSync('skills/jetpack-compose-audit/.claude-plugin/plugin.json', 'utf8')).version;
if (!auditSkill.includes(`**Skill version:** ${auditVersion}`)) {
throw new Error('jetpack-compose-audit SKILL.md version does not match manifest version');
}
NODE
if command -v claude >/dev/null 2>&1; then
claude plugin validate ./skills/compose-agent
claude plugin validate ./skills/jetpack-compose-audit
else
echo "warning: claude CLI not found; skipped Claude plugin validation" >&2
fi