claude-code-templates 1.22.0 → 1.22.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.
@@ -0,0 +1,376 @@
1
+ const BaseValidator = require('../BaseValidator');
2
+ const yaml = require('js-yaml');
3
+
4
+ /**
5
+ * StructuralValidator - Validates component structure and format
6
+ *
7
+ * Checks:
8
+ * - YAML frontmatter presence and validity
9
+ * - Required fields (name, description)
10
+ * - File size limits
11
+ * - UTF-8 encoding
12
+ * - Section count limits
13
+ * - Component type-specific requirements
14
+ */
15
+ class StructuralValidator extends BaseValidator {
16
+ constructor() {
17
+ super();
18
+
19
+ // Configuration limits
20
+ this.MAX_FILE_SIZE = 100 * 1024; // 100KB
21
+ this.MAX_SECTION_COUNT = 20; // Prevent context overflow
22
+ this.MIN_DESCRIPTION_LENGTH = 20;
23
+ this.MAX_DESCRIPTION_LENGTH = 500;
24
+
25
+ // Required fields by component type
26
+ this.REQUIRED_FIELDS = {
27
+ agent: ['name', 'description', 'tools'],
28
+ command: ['name', 'description'],
29
+ mcp: ['name', 'description', 'command'],
30
+ setting: ['name', 'description'],
31
+ hook: ['name', 'description', 'trigger']
32
+ };
33
+
34
+ // Optional but recommended fields
35
+ this.RECOMMENDED_FIELDS = {
36
+ agent: ['model'],
37
+ command: ['usage', 'examples'],
38
+ mcp: ['args'],
39
+ setting: ['type'],
40
+ hook: ['conditions']
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Validate component structure
46
+ * @param {object} component - Component data
47
+ * @param {string} component.content - Raw markdown content
48
+ * @param {string} component.path - File path
49
+ * @param {string} component.type - Component type (agent, command, mcp, etc.)
50
+ * @param {object} options - Validation options
51
+ * @returns {Promise<object>} Validation results
52
+ */
53
+ async validate(component, options = {}) {
54
+ this.reset();
55
+
56
+ const { content, path, type } = component;
57
+
58
+ if (!content) {
59
+ this.addError('STRUCT_E001', 'Component content is empty or missing', { path });
60
+ return this.getResults();
61
+ }
62
+
63
+ // 1. File size validation
64
+ this.validateFileSize(content, path);
65
+
66
+ // 2. UTF-8 encoding validation
67
+ this.validateEncoding(content, path);
68
+
69
+ // 3. Frontmatter validation
70
+ const frontmatter = this.validateFrontmatter(content, path);
71
+
72
+ if (frontmatter) {
73
+ // 4. Required fields validation
74
+ this.validateRequiredFields(frontmatter, type, path);
75
+
76
+ // 5. Description validation
77
+ this.validateDescription(frontmatter, path);
78
+
79
+ // 6. Tools validation (for agents)
80
+ if (type === 'agent') {
81
+ this.validateTools(frontmatter, path);
82
+ }
83
+
84
+ // 7. Model validation (for agents)
85
+ if (type === 'agent') {
86
+ this.validateModel(frontmatter, path);
87
+ }
88
+
89
+ // 8. Recommended fields check
90
+ this.checkRecommendedFields(frontmatter, type, path);
91
+ }
92
+
93
+ // 9. Content structure validation
94
+ this.validateContentStructure(content, path);
95
+
96
+ // 10. Section count validation
97
+ this.validateSectionCount(content, path);
98
+
99
+ return this.getResults();
100
+ }
101
+
102
+ /**
103
+ * Validate file size
104
+ */
105
+ validateFileSize(content, path) {
106
+ const size = Buffer.byteLength(content, 'utf8');
107
+
108
+ if (size > this.MAX_FILE_SIZE) {
109
+ this.addError(
110
+ 'STRUCT_E003',
111
+ `File size (${(size / 1024).toFixed(2)}KB) exceeds maximum allowed size (${this.MAX_FILE_SIZE / 1024}KB)`,
112
+ { path, size, limit: this.MAX_FILE_SIZE }
113
+ );
114
+ } else if (size > this.MAX_FILE_SIZE * 0.8) {
115
+ this.addWarning(
116
+ 'STRUCT_W002',
117
+ `File size (${(size / 1024).toFixed(2)}KB) is approaching the limit`,
118
+ { path, size, limit: this.MAX_FILE_SIZE }
119
+ );
120
+ }
121
+
122
+ this.addInfo('STRUCT_I001', `File size: ${(size / 1024).toFixed(2)}KB`, { path, size });
123
+ }
124
+
125
+ /**
126
+ * Validate UTF-8 encoding
127
+ */
128
+ validateEncoding(content, path) {
129
+ try {
130
+ // Try to detect non-UTF-8 characters
131
+ const buffer = Buffer.from(content, 'utf8');
132
+ const decoded = buffer.toString('utf8');
133
+
134
+ if (decoded !== content) {
135
+ this.addError(
136
+ 'STRUCT_E004',
137
+ 'File contains invalid UTF-8 encoding',
138
+ { path }
139
+ );
140
+ }
141
+
142
+ // Check for null bytes (potential binary content)
143
+ if (content.includes('\0')) {
144
+ this.addError(
145
+ 'STRUCT_E005',
146
+ 'File contains null bytes (possible binary content)',
147
+ { path }
148
+ );
149
+ }
150
+ } catch (error) {
151
+ this.addError(
152
+ 'STRUCT_E004',
153
+ 'Failed to validate encoding',
154
+ { path, error: error.message }
155
+ );
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Validate and parse YAML frontmatter
161
+ * @returns {object|null} Parsed frontmatter or null if invalid
162
+ */
163
+ validateFrontmatter(content, path) {
164
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
165
+
166
+ if (!frontmatterMatch) {
167
+ this.addError(
168
+ 'STRUCT_E001',
169
+ 'Missing YAML frontmatter (must start with --- and end with ---)',
170
+ { path }
171
+ );
172
+ return null;
173
+ }
174
+
175
+ try {
176
+ const frontmatter = yaml.load(frontmatterMatch[1]);
177
+
178
+ if (!frontmatter || typeof frontmatter !== 'object') {
179
+ this.addError(
180
+ 'STRUCT_E002',
181
+ 'Frontmatter is empty or not a valid object',
182
+ { path }
183
+ );
184
+ return null;
185
+ }
186
+
187
+ this.addInfo('STRUCT_I002', 'Valid YAML frontmatter found', { path });
188
+ return frontmatter;
189
+ } catch (error) {
190
+ this.addError(
191
+ 'STRUCT_E002',
192
+ `Invalid YAML syntax in frontmatter: ${error.message}`,
193
+ { path, error: error.message }
194
+ );
195
+ return null;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Validate required fields
201
+ */
202
+ validateRequiredFields(frontmatter, type, path) {
203
+ const requiredFields = this.REQUIRED_FIELDS[type] || ['name', 'description'];
204
+
205
+ for (const field of requiredFields) {
206
+ if (!frontmatter[field]) {
207
+ this.addError(
208
+ 'STRUCT_E006',
209
+ `Missing required field: ${field}`,
210
+ { path, field, type }
211
+ );
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Validate description field
218
+ */
219
+ validateDescription(frontmatter, path) {
220
+ const description = frontmatter.description;
221
+
222
+ if (!description) return; // Already caught by required fields
223
+
224
+ if (typeof description !== 'string') {
225
+ this.addError(
226
+ 'STRUCT_E007',
227
+ 'Description must be a string',
228
+ { path, type: typeof description }
229
+ );
230
+ return;
231
+ }
232
+
233
+ const length = description.trim().length;
234
+
235
+ if (length < this.MIN_DESCRIPTION_LENGTH) {
236
+ this.addWarning(
237
+ 'STRUCT_W003',
238
+ `Description is too short (${length} chars, minimum ${this.MIN_DESCRIPTION_LENGTH})`,
239
+ { path, length, min: this.MIN_DESCRIPTION_LENGTH }
240
+ );
241
+ }
242
+
243
+ if (length > this.MAX_DESCRIPTION_LENGTH) {
244
+ this.addWarning(
245
+ 'STRUCT_W004',
246
+ `Description is too long (${length} chars, maximum ${this.MAX_DESCRIPTION_LENGTH})`,
247
+ { path, length, max: this.MAX_DESCRIPTION_LENGTH }
248
+ );
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Validate tools field for agents
254
+ */
255
+ validateTools(frontmatter, path) {
256
+ const tools = frontmatter.tools;
257
+
258
+ if (!tools) return; // Already caught by required fields
259
+
260
+ // Tools can be a string (comma-separated) or array
261
+ if (typeof tools === 'string') {
262
+ const toolsList = tools.split(',').map(t => t.trim()).filter(t => t);
263
+
264
+ if (toolsList.length === 0) {
265
+ this.addWarning('STRUCT_W005', 'Tools field is empty', { path });
266
+ }
267
+
268
+ // Validate known tool names
269
+ const validTools = ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'WebSearch', 'WebFetch', '*'];
270
+ const invalidTools = toolsList.filter(t => !validTools.includes(t) && t !== '*');
271
+
272
+ if (invalidTools.length > 0) {
273
+ this.addWarning(
274
+ 'STRUCT_W006',
275
+ `Unknown tools specified: ${invalidTools.join(', ')}`,
276
+ { path, invalidTools }
277
+ );
278
+ }
279
+ } else if (Array.isArray(tools)) {
280
+ if (tools.length === 0) {
281
+ this.addWarning('STRUCT_W005', 'Tools array is empty', { path });
282
+ }
283
+ } else {
284
+ this.addError(
285
+ 'STRUCT_E008',
286
+ 'Tools field must be a string or array',
287
+ { path, type: typeof tools }
288
+ );
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Validate model field for agents
294
+ */
295
+ validateModel(frontmatter, path) {
296
+ const model = frontmatter.model;
297
+
298
+ if (!model) {
299
+ this.addWarning('STRUCT_W007', 'No model specified (recommended)', { path });
300
+ return;
301
+ }
302
+
303
+ const validModels = ['sonnet', 'opus', 'haiku', 'claude-3-5-sonnet', 'claude-3-opus', 'claude-3-haiku'];
304
+
305
+ if (!validModels.includes(model)) {
306
+ this.addWarning(
307
+ 'STRUCT_W008',
308
+ `Unknown model: ${model}. Valid models: ${validModels.join(', ')}`,
309
+ { path, model }
310
+ );
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Check for recommended fields
316
+ */
317
+ checkRecommendedFields(frontmatter, type, path) {
318
+ const recommendedFields = this.RECOMMENDED_FIELDS[type] || [];
319
+ const missingFields = recommendedFields.filter(field => !frontmatter[field]);
320
+
321
+ if (missingFields.length > 0) {
322
+ this.addInfo(
323
+ 'STRUCT_I003',
324
+ `Missing recommended fields: ${missingFields.join(', ')}`,
325
+ { path, missingFields }
326
+ );
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Validate content structure
332
+ */
333
+ validateContentStructure(content, path) {
334
+ // Remove frontmatter for content analysis
335
+ const contentWithoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n/, '');
336
+
337
+ if (contentWithoutFrontmatter.trim().length < 50) {
338
+ this.addWarning(
339
+ 'STRUCT_W009',
340
+ 'Component content is very short (less than 50 characters)',
341
+ { path, length: contentWithoutFrontmatter.trim().length }
342
+ );
343
+ }
344
+
345
+ // Check for basic markdown structure
346
+ const hasHeaders = /^#{1,6}\s+.+$/m.test(contentWithoutFrontmatter);
347
+
348
+ if (!hasHeaders) {
349
+ this.addWarning(
350
+ 'STRUCT_W010',
351
+ 'No markdown headers found in content (recommended for organization)',
352
+ { path }
353
+ );
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Validate section count (prevent context overflow)
359
+ */
360
+ validateSectionCount(content, path) {
361
+ const sections = content.match(/^#{1,6}\s+.+$/gm) || [];
362
+ const count = sections.length;
363
+
364
+ if (count > this.MAX_SECTION_COUNT) {
365
+ this.addWarning(
366
+ 'STRUCT_W011',
367
+ `Too many sections (${count}), may cause context overflow. Maximum recommended: ${this.MAX_SECTION_COUNT}`,
368
+ { path, count, max: this.MAX_SECTION_COUNT }
369
+ );
370
+ }
371
+
372
+ this.addInfo('STRUCT_I004', `Section count: ${count}`, { path, count });
373
+ }
374
+ }
375
+
376
+ module.exports = StructuralValidator;