multisite-cms-mcp 1.2.4 → 1.4.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.
@@ -1,8 +1,14 @@
1
1
  /**
2
2
  * Fetches the complete schema for a specific project including:
3
- * - Built-in collections with their standard fields
4
- * - Custom fields added to built-in collections
5
- * - Custom collections with all their fields
3
+ * - All custom collections with their fields
4
+ * - Field tokens, types, and descriptions for AI context
5
+ * - Template patterns and example usage
6
+ *
7
+ * Each field includes:
8
+ * - Field Name (display name)
9
+ * - Token (the exact syntax to use in templates)
10
+ * - Type (text, richText, image, etc.)
11
+ * - Description (help text for context)
6
12
  *
7
13
  * Uses stored credentials or triggers device flow for authentication.
8
14
  *
@@ -1 +1 @@
1
- {"version":3,"file":"get-tenant-schema.d.ts","sourceRoot":"","sources":["../../src/tools/get-tenant-schema.ts"],"names":[],"mappings":"AA2EA;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmFxE"}
1
+ {"version":3,"file":"get-tenant-schema.d.ts","sourceRoot":"","sources":["../../src/tools/get-tenant-schema.ts"],"names":[],"mappings":"AA2EA;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmFxE"}
@@ -38,9 +38,15 @@ async function resolveProjectId(projectIdentifier) {
38
38
  }
39
39
  /**
40
40
  * Fetches the complete schema for a specific project including:
41
- * - Built-in collections with their standard fields
42
- * - Custom fields added to built-in collections
43
- * - Custom collections with all their fields
41
+ * - All custom collections with their fields
42
+ * - Field tokens, types, and descriptions for AI context
43
+ * - Template patterns and example usage
44
+ *
45
+ * Each field includes:
46
+ * - Field Name (display name)
47
+ * - Token (the exact syntax to use in templates)
48
+ * - Type (text, richText, image, etc.)
49
+ * - Description (help text for context)
44
50
  *
45
51
  * Uses stored credentials or triggers device flow for authentication.
46
52
  *
@@ -137,7 +143,13 @@ The API returned a successful response but the prompt data was missing.
137
143
 
138
144
  **Project ID:** \`${tenantId}\`
139
145
 
140
- The following schema is specific to this project and includes all custom collections and custom fields.
146
+ This schema is specific to this project and includes all collections with their fields.
147
+
148
+ **Field Table Format:**
149
+ - **Field Name** - Display name for the field
150
+ - **Token** - The exact token to use in templates (use triple braces for richText)
151
+ - **Type** - Field type (text, richText, image, url, boolean, number, date, select, relation)
152
+ - **Description** - Help text explaining what the field is for
141
153
 
142
154
  ---
143
155
 
@@ -23,7 +23,7 @@ interface CollectionToCreate {
23
23
  }
24
24
  interface FieldsToAdd {
25
25
  collectionSlug: string;
26
- isBuiltin: boolean;
26
+ isBuiltin?: boolean;
27
27
  fields: FieldToCreate[];
28
28
  }
29
29
  interface SyncSchemaInput {
@@ -1 +1 @@
1
- {"version":3,"file":"sync-schema.d.ts","sourceRoot":"","sources":["../../src/tools/sync-schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,UAAU,WAAW;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,UAAU,eAAe;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACnC,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;CAC7B;AAyKD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA8UxE"}
1
+ {"version":3,"file":"sync-schema.d.ts","sourceRoot":"","sources":["../../src/tools/sync-schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,UAAU,WAAW;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,UAAU,eAAe;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACnC,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;CAC7B;AAgJD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAmSxE"}
@@ -11,7 +11,6 @@ const api_client_1 = require("../lib/api-client");
11
11
  const device_flow_1 = require("../lib/device-flow");
12
12
  const get_field_types_1 = require("./get-field-types");
13
13
  // ============ Constants ============
14
- const BUILTIN_COLLECTION_TYPES = ['blogs', 'authors', 'team', 'downloads'];
15
14
  const VALID_FIELD_TYPES = get_field_types_1.AVAILABLE_FIELD_TYPES.map(ft => ft.value);
16
15
  const AUTH_REQUIRED_MESSAGE = `# Authentication Required
17
16
 
@@ -95,26 +94,14 @@ async function resolveProjectId(projectIdentifier) {
95
94
  };
96
95
  }
97
96
  /**
98
- * Fetch existing schema for a project
97
+ * Fetch existing collections for a project
99
98
  */
100
- async function fetchExistingSchema(tenantId) {
101
- // Fetch custom collections
99
+ async function fetchExistingCollections(tenantId) {
102
100
  const collectionsRes = await (0, api_client_1.apiRequest)('/api/collections', { tenantId });
103
101
  if ((0, api_client_1.isApiError)(collectionsRes)) {
104
102
  return { error: `Failed to fetch collections: ${collectionsRes.error}` };
105
103
  }
106
- // Fetch builtin collection fields
107
- const builtinRes = await (0, api_client_1.apiRequest)('/api/cms/builtin-collections', { tenantId });
108
- const builtinFields = {};
109
- if (!(0, api_client_1.isApiError)(builtinRes)) {
110
- for (const bc of builtinRes.data) {
111
- builtinFields[bc.type] = bc.fields.map(f => ({ slug: f.slug, name: f.name, type: f.type }));
112
- }
113
- }
114
- return {
115
- collections: collectionsRes.data,
116
- builtinFields,
117
- };
104
+ return collectionsRes.data;
118
105
  }
119
106
  // ============ Main Function ============
120
107
  /**
@@ -172,8 +159,9 @@ Use \`get_field_types\` to see available field types.
172
159
  allValidationErrors.push(`fieldsToAdd entry missing collectionSlug`);
173
160
  continue;
174
161
  }
175
- if (group.isBuiltin && !BUILTIN_COLLECTION_TYPES.includes(group.collectionSlug)) {
176
- allValidationErrors.push(`"${group.collectionSlug}" is not a valid builtin collection. Valid: ${BUILTIN_COLLECTION_TYPES.join(', ')}`);
162
+ // Note: isBuiltin is deprecated and ignored - all collections are now custom
163
+ if (group.isBuiltin) {
164
+ allValidationErrors.push(`isBuiltin is no longer supported. All collections are custom collections. Use the collection slug directly.`);
177
165
  }
178
166
  if (!group.fields || group.fields.length === 0) {
179
167
  allValidationErrors.push(`fieldsToAdd for "${group.collectionSlug}" has no fields`);
@@ -204,27 +192,26 @@ ${resolved.error}
204
192
  `;
205
193
  }
206
194
  const { tenantId } = resolved;
207
- // Fetch existing schema
208
- const existingSchema = await fetchExistingSchema(tenantId);
209
- if ('error' in existingSchema) {
195
+ // Fetch existing collections
196
+ const existingResult = await fetchExistingCollections(tenantId);
197
+ if ('error' in existingResult) {
210
198
  // Check if auth error
211
- if (existingSchema.error.includes('401') || existingSchema.error.includes('auth')) {
199
+ if (existingResult.error.includes('401') || existingResult.error.includes('auth')) {
212
200
  const authResult = await (0, device_flow_1.ensureAuthenticated)();
213
201
  if (!authResult.authenticated) {
214
202
  return AUTH_REQUIRED_MESSAGE;
215
203
  }
216
204
  // Retry
217
- const retry = await fetchExistingSchema(tenantId);
205
+ const retry = await fetchExistingCollections(tenantId);
218
206
  if ('error' in retry) {
219
207
  return `# Error\n\n${retry.error}`;
220
208
  }
221
- Object.assign(existingSchema, retry);
222
209
  }
223
210
  else {
224
- return `# Error\n\n${existingSchema.error}`;
211
+ return `# Error\n\n${existingResult.error}`;
225
212
  }
226
213
  }
227
- const { collections: existingCollections, builtinFields } = existingSchema;
214
+ const existingCollections = Array.isArray(existingResult) ? existingResult : [];
228
215
  // Track results
229
216
  const results = [];
230
217
  const created = { collections: 0, fields: 0 };
@@ -235,14 +222,14 @@ ${resolved.error}
235
222
  // Check if collection already exists
236
223
  const existing = existingCollections.find(c => c.slug.toLowerCase() === col.slug.toLowerCase());
237
224
  if (existing) {
238
- results.push(`⏭️ Skipped collection "${col.slug}" (already exists)`);
225
+ results.push(`Skipped collection "${col.slug}" (already exists)`);
239
226
  skipped.collections++;
240
227
  // But still try to add any new fields
241
228
  if (col.fields && col.fields.length > 0) {
242
229
  for (const field of col.fields) {
243
230
  const fieldExists = existing.fields.some(f => f.slug.toLowerCase() === field.slug.toLowerCase());
244
231
  if (fieldExists) {
245
- results.push(` ⏭️ Skipped field "${field.slug}" (already exists)`);
232
+ results.push(` Skipped field "${field.slug}" (already exists)`);
246
233
  skipped.fields++;
247
234
  }
248
235
  else {
@@ -261,10 +248,10 @@ ${resolved.error}
261
248
  },
262
249
  });
263
250
  if ((0, api_client_1.isApiError)(fieldRes)) {
264
- results.push(` Failed to add field "${field.slug}": ${fieldRes.error}`);
251
+ results.push(` Failed to add field "${field.slug}": ${fieldRes.error}`);
265
252
  }
266
253
  else {
267
- results.push(` Added field "${field.slug}" (${field.type})`);
254
+ results.push(` Added field "${field.slug}" (${field.type})`);
268
255
  created.fields++;
269
256
  }
270
257
  }
@@ -285,10 +272,10 @@ ${resolved.error}
285
272
  },
286
273
  });
287
274
  if ((0, api_client_1.isApiError)(createRes)) {
288
- results.push(`❌ Failed to create collection "${col.slug}": ${createRes.error}`);
275
+ results.push(`Failed to create collection "${col.slug}": ${createRes.error}`);
289
276
  continue;
290
277
  }
291
- results.push(`✅ Created collection "${col.name}" (${col.slug})`);
278
+ results.push(`Created collection "${col.name}" (${col.slug})`);
292
279
  created.collections++;
293
280
  // Add fields to the new collection
294
281
  if (col.fields && col.fields.length > 0) {
@@ -308,10 +295,10 @@ ${resolved.error}
308
295
  },
309
296
  });
310
297
  if ((0, api_client_1.isApiError)(fieldRes)) {
311
- results.push(` Failed to add field "${field.slug}": ${fieldRes.error}`);
298
+ results.push(` Failed to add field "${field.slug}": ${fieldRes.error}`);
312
299
  }
313
300
  else {
314
- results.push(` Added field "${field.slug}" (${field.type})`);
301
+ results.push(` Added field "${field.slug}" (${field.type})`);
315
302
  created.fields++;
316
303
  }
317
304
  }
@@ -321,72 +308,39 @@ ${resolved.error}
321
308
  // Add fields to existing collections
322
309
  if (fieldsToAdd && fieldsToAdd.length > 0) {
323
310
  for (const group of fieldsToAdd) {
324
- results.push(`\n📁 ${group.collectionSlug} (${group.isBuiltin ? 'builtin' : 'custom'}):`);
325
- if (group.isBuiltin) {
326
- // Add to builtin collection
327
- const existingBuiltinFields = builtinFields[group.collectionSlug] || [];
328
- for (const field of group.fields) {
329
- const fieldExists = existingBuiltinFields.some(f => f.slug.toLowerCase() === field.slug.toLowerCase());
330
- if (fieldExists) {
331
- results.push(` ⏭️ Skipped field "${field.slug}" (already exists)`);
332
- skipped.fields++;
333
- continue;
334
- }
335
- const fieldRes = await (0, api_client_1.apiRequest)(`/api/cms/builtin-collections/${group.collectionSlug}/fields`, {
336
- tenantId,
337
- method: 'POST',
338
- body: {
339
- slug: field.slug,
340
- name: field.name,
341
- type: field.type,
342
- description: field.description,
343
- isRequired: field.isRequired,
344
- options: field.options,
345
- },
346
- });
347
- if ((0, api_client_1.isApiError)(fieldRes)) {
348
- results.push(` ❌ Failed to add field "${field.slug}": ${fieldRes.error}`);
349
- }
350
- else {
351
- results.push(` ✅ Added field "${field.slug}" (${field.type})`);
352
- created.fields++;
353
- }
354
- }
311
+ results.push(`\nCollection ${group.collectionSlug}:`);
312
+ // All collections are custom collections now
313
+ const customCollection = existingCollections.find(c => c.slug.toLowerCase() === group.collectionSlug.toLowerCase());
314
+ if (!customCollection) {
315
+ results.push(` Collection "${group.collectionSlug}" not found. Create it first with the collections parameter.`);
316
+ continue;
355
317
  }
356
- else {
357
- // Add to custom collection
358
- const customCollection = existingCollections.find(c => c.slug.toLowerCase() === group.collectionSlug.toLowerCase());
359
- if (!customCollection) {
360
- results.push(` ❌ Collection "${group.collectionSlug}" not found. Create it first with the collections parameter.`);
318
+ for (const field of group.fields) {
319
+ const fieldExists = customCollection.fields.some(f => f.slug.toLowerCase() === field.slug.toLowerCase());
320
+ if (fieldExists) {
321
+ results.push(` Skipped field "${field.slug}" (already exists)`);
322
+ skipped.fields++;
361
323
  continue;
362
324
  }
363
- for (const field of group.fields) {
364
- const fieldExists = customCollection.fields.some(f => f.slug.toLowerCase() === field.slug.toLowerCase());
365
- if (fieldExists) {
366
- results.push(` ⏭️ Skipped field "${field.slug}" (already exists)`);
367
- skipped.fields++;
368
- continue;
369
- }
370
- const fieldRes = await (0, api_client_1.apiRequest)(`/api/collections/${customCollection.id}/fields`, {
371
- tenantId,
372
- method: 'POST',
373
- body: {
374
- slug: field.slug,
375
- name: field.name,
376
- type: field.type,
377
- description: field.description,
378
- isRequired: field.isRequired,
379
- options: field.options,
380
- referenceCollection: field.referenceCollection,
381
- },
382
- });
383
- if ((0, api_client_1.isApiError)(fieldRes)) {
384
- results.push(` ❌ Failed to add field "${field.slug}": ${fieldRes.error}`);
385
- }
386
- else {
387
- results.push(` ✅ Added field "${field.slug}" (${field.type})`);
388
- created.fields++;
389
- }
325
+ const fieldRes = await (0, api_client_1.apiRequest)(`/api/collections/${customCollection.id}/fields`, {
326
+ tenantId,
327
+ method: 'POST',
328
+ body: {
329
+ slug: field.slug,
330
+ name: field.name,
331
+ type: field.type,
332
+ description: field.description,
333
+ isRequired: field.isRequired,
334
+ options: field.options,
335
+ referenceCollection: field.referenceCollection,
336
+ },
337
+ });
338
+ if ((0, api_client_1.isApiError)(fieldRes)) {
339
+ results.push(` Failed to add field "${field.slug}": ${fieldRes.error}`);
340
+ }
341
+ else {
342
+ results.push(` Added field "${field.slug}" (${field.type})`);
343
+ created.fields++;
390
344
  }
391
345
  }
392
346
  }
@@ -1 +1 @@
1
- {"version":3,"file":"validate-manifest.d.ts","sourceRoot":"","sources":["../../src/tools/validate-manifest.ts"],"names":[],"mappings":"AAyCA;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0Q5E"}
1
+ {"version":3,"file":"validate-manifest.d.ts","sourceRoot":"","sources":["../../src/tools/validate-manifest.ts"],"names":[],"mappings":"AAmBA;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2N5E"}
@@ -8,30 +8,9 @@ const pageSchema = zod_1.z.object({
8
8
  title: zod_1.z.string(),
9
9
  editable: zod_1.z.boolean().optional(),
10
10
  });
11
- // Built-in collection templates
12
- const builtInTemplatesSchema = zod_1.z.object({
13
- blogIndex: zod_1.z.string().optional(),
14
- blogIndexPath: zod_1.z.string().startsWith('/').optional(),
15
- blogPost: zod_1.z.string().optional(),
16
- blogPostPath: zod_1.z.string().startsWith('/').optional(),
17
- team: zod_1.z.string().optional(),
18
- teamPath: zod_1.z.string().startsWith('/').optional(),
19
- downloads: zod_1.z.string().optional(),
20
- downloadsPath: zod_1.z.string().startsWith('/').optional(),
21
- authorsIndex: zod_1.z.string().optional(),
22
- authorDetail: zod_1.z.string().optional(),
23
- authorsIndexPath: zod_1.z.string().startsWith('/').optional(),
24
- // Legacy format - deprecated
25
- collectionPaths: zod_1.z.record(zod_1.z.string().startsWith('/')).optional(),
26
- collectionTemplates: zod_1.z.record(zod_1.z.object({
27
- path: zod_1.z.string().startsWith('/').optional(),
28
- indexTemplate: zod_1.z.string().optional(),
29
- detailTemplate: zod_1.z.string().optional(),
30
- })).optional(),
31
- });
32
- // Custom collection templates use flat format: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath
33
- // This schema allows any additional string keys for custom collections
34
- const cmsTemplatesSchema = builtInTemplatesSchema.catchall(zod_1.z.string()).optional();
11
+ // CMS templates use flat format: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath
12
+ // This schema allows any string keys for collection templates
13
+ const cmsTemplatesSchema = zod_1.z.record(zod_1.z.string()).optional();
35
14
  const manifestSchema = zod_1.z.object({
36
15
  pages: zod_1.z.array(pageSchema),
37
16
  cmsTemplates: cmsTemplatesSchema,
@@ -56,7 +35,7 @@ async function validateManifest(manifestJson) {
56
35
  // Check for missing closing brace
57
36
  if (!trimmed.endsWith('}')) {
58
37
  diagnostics = `
59
- 🔍 Detected Issue: File does not end with a closing brace '}'
38
+ Detected Issue: File does not end with a closing brace '}'
60
39
  The manifest.json appears to be truncated or missing the final '}'.
61
40
 
62
41
  Fix: Add a closing '}' at the end of the file.`;
@@ -64,7 +43,7 @@ async function validateManifest(manifestJson) {
64
43
  // Check for missing opening brace
65
44
  else if (!trimmed.startsWith('{')) {
66
45
  diagnostics = `
67
- 🔍 Detected Issue: File does not start with an opening brace '{'
46
+ Detected Issue: File does not start with an opening brace '{'
68
47
  The manifest.json must be a JSON object starting with '{'.`;
69
48
  }
70
49
  // Check bracket balance
@@ -75,20 +54,20 @@ async function validateManifest(manifestJson) {
75
54
  const closeBrackets = (manifestJson.match(/]/g) || []).length;
76
55
  if (openBraces !== closeBraces) {
77
56
  diagnostics = `
78
- 🔍 Detected Issue: Mismatched braces
57
+ Detected Issue: Mismatched braces
79
58
  Found ${openBraces} opening '{' but ${closeBraces} closing '}'
80
59
  ${openBraces > closeBraces ? `Missing ${openBraces - closeBraces} closing brace(s) '}'` : `Extra ${closeBraces - openBraces} closing brace(s) '}'`}`;
81
60
  }
82
61
  else if (openBrackets !== closeBrackets) {
83
62
  diagnostics = `
84
- 🔍 Detected Issue: Mismatched brackets
63
+ Detected Issue: Mismatched brackets
85
64
  Found ${openBrackets} opening '[' but ${closeBrackets} closing ']'
86
65
  ${openBrackets > closeBrackets ? `Missing ${openBrackets - closeBrackets} closing bracket(s) ']'` : `Extra ${closeBrackets - openBrackets} closing bracket(s) ']'`}`;
87
66
  }
88
67
  else {
89
68
  // Generic guidance
90
69
  diagnostics = `
91
- 🔍 Common JSON issues to check:
70
+ Common JSON issues to check:
92
71
  - Missing or extra commas between items
93
72
  - Trailing comma after last item in arrays/objects
94
73
  - Unquoted property names (must use "key" not key)
@@ -96,12 +75,12 @@ async function validateManifest(manifestJson) {
96
75
  - Unescaped special characters in strings`;
97
76
  }
98
77
  }
99
- return `❌ INVALID JSON
78
+ return `INVALID JSON
100
79
 
101
80
  Error: ${errorMessage}
102
81
  ${diagnostics}
103
82
 
104
- 💡 Tip: Use a JSON validator (like jsonlint.com) to find the exact error location.`;
83
+ Tip: Use a JSON validator (like jsonlint.com) to find the exact error location.`;
105
84
  }
106
85
  // Validate against schema
107
86
  const result = manifestSchema.safeParse(manifest);
@@ -137,73 +116,34 @@ ${diagnostics}
137
116
  }
138
117
  }
139
118
  }
140
- // Check cmsTemplates
119
+ // Check cmsTemplates - enforce strict pattern-based validation
141
120
  const templates = m.cmsTemplates;
142
121
  const customCollections = new Set();
143
122
  if (templates) {
144
- // Check template file paths
145
- const templateFields = ['blogIndex', 'blogPost', 'team', 'downloads', 'authorsIndex', 'authorDetail'];
146
- for (const field of templateFields) {
147
- const value = templates[field];
148
- if (typeof value === 'string') {
149
- if (!value.startsWith('templates/')) {
150
- warnings.push(`- cmsTemplates.${field}: should be in templates/ folder (got: ${value})`);
123
+ // All keys must match the unified format: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath
124
+ const VALID_KEY_PATTERN = /^[a-z][a-zA-Z0-9]*(Index|Detail|IndexPath|DetailPath)$/;
125
+ const KEY_PARTS_PATTERN = /^([a-z][a-zA-Z0-9]*)(Index|Detail|IndexPath|DetailPath)$/;
126
+ for (const key of Object.keys(templates)) {
127
+ if (!VALID_KEY_PATTERN.test(key)) {
128
+ // Provide helpful error message with suggestion
129
+ let suggestion = '';
130
+ if (key.toLowerCase().includes('post')) {
131
+ suggestion = ` Did you mean "${key.replace(/[Pp]ost/g, 'Detail')}"?`;
151
132
  }
152
- if (!value.endsWith('.html')) {
153
- warnings.push(`- cmsTemplates.${field}: should end with .html (got: ${value})`);
133
+ else if (!key.includes('Index') && !key.includes('Detail')) {
134
+ suggestion = ` Did you mean "${key}Index" or "${key}Detail"?`;
154
135
  }
155
- }
156
- }
157
- // Check path consistency
158
- if (templates.blogIndex && !templates.blogIndexPath) {
159
- warnings.push('- blogIndex template defined but no blogIndexPath - will default to /blog');
160
- }
161
- if (templates.blogPost && !templates.blogPostPath) {
162
- warnings.push('- blogPost template defined but no blogPostPath - will default to /blog');
163
- }
164
- if (templates.blogIndexPath !== templates.blogPostPath && templates.blogIndexPath && templates.blogPostPath) {
165
- warnings.push(`- blogIndexPath (${templates.blogIndexPath}) differs from blogPostPath (${templates.blogPostPath}) - usually these should match`);
166
- }
167
- // Check for common author path mistake
168
- if (templates.authorsPath && !templates.authorsIndexPath) {
169
- warnings.push('- Found "authorsPath" but expected "authorsIndexPath" - rename to authorsIndexPath');
170
- }
171
- // Check author template consistency
172
- if (templates.authorsIndex && !templates.authorsIndexPath) {
173
- warnings.push('- authorsIndex template defined but no authorsIndexPath - will default to /authors');
174
- }
175
- if (templates.authorDetail && !templates.authorsIndex) {
176
- warnings.push('- authorDetail template defined but no authorsIndex - authors need both templates');
177
- }
178
- // Detect and validate custom collection templates (flat format)
179
- // Pattern: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath
180
- const allKeys = Object.keys(templates);
181
- for (const key of allKeys) {
182
- // Skip built-in keys
183
- if (['blogIndex', 'blogIndexPath', 'blogPost', 'blogPostPath', 'team', 'teamPath',
184
- 'downloads', 'downloadsPath', 'authorsIndex', 'authorDetail', 'authorsIndexPath',
185
- 'collectionPaths', 'collectionTemplates'].includes(key)) {
136
+ errors.push(`- Invalid cmsTemplates key "${key}". ` +
137
+ `All keys must use format: {slug}Index, {slug}Detail, {slug}IndexPath, or {slug}DetailPath.${suggestion}`);
186
138
  continue;
187
139
  }
188
- // Extract collection slug from key pattern
189
- let slug = null;
190
- if (key.endsWith('Index') && !key.endsWith('IndexPath')) {
191
- slug = key.slice(0, -5); // Remove 'Index'
192
- }
193
- else if (key.endsWith('Detail') && !key.endsWith('DetailPath')) {
194
- slug = key.slice(0, -6); // Remove 'Detail'
195
- }
196
- else if (key.endsWith('IndexPath')) {
197
- slug = key.slice(0, -9); // Remove 'IndexPath'
198
- }
199
- else if (key.endsWith('DetailPath')) {
200
- slug = key.slice(0, -10); // Remove 'DetailPath'
201
- }
202
- if (slug && slug.length > 0) {
203
- customCollections.add(slug);
140
+ // Extract collection slug from valid keys
141
+ const match = key.match(KEY_PARTS_PATTERN);
142
+ if (match) {
143
+ customCollections.add(match[1]);
204
144
  }
205
145
  }
206
- // Validate each custom collection
146
+ // Validate each detected collection
207
147
  for (const slug of customCollections) {
208
148
  const indexTemplate = templates[`${slug}Index`];
209
149
  const detailTemplate = templates[`${slug}Detail`];
@@ -229,53 +169,47 @@ ${diagnostics}
229
169
  }
230
170
  // Check consistency - if you have one, you should have the others
231
171
  if (indexTemplate && !indexPath) {
232
- warnings.push(`- Custom collection "${slug}": has ${slug}Index but no ${slug}IndexPath`);
172
+ warnings.push(`- Collection "${slug}": has ${slug}Index but no ${slug}IndexPath`);
233
173
  }
234
174
  if (detailTemplate && !detailPath) {
235
- warnings.push(`- Custom collection "${slug}": has ${slug}Detail but no ${slug}DetailPath`);
175
+ warnings.push(`- Collection "${slug}": has ${slug}Detail but no ${slug}DetailPath`);
236
176
  }
237
177
  if (indexPath && !indexTemplate) {
238
- warnings.push(`- Custom collection "${slug}": has ${slug}IndexPath but no ${slug}Index template`);
178
+ warnings.push(`- Collection "${slug}": has ${slug}IndexPath but no ${slug}Index template`);
239
179
  }
240
180
  if (detailPath && !detailTemplate) {
241
- warnings.push(`- Custom collection "${slug}": has ${slug}DetailPath but no ${slug}Detail template`);
181
+ warnings.push(`- Collection "${slug}": has ${slug}DetailPath but no ${slug}Detail template`);
242
182
  }
243
- // Note: indexPath and detailPath can be the same (e.g., both "/videos")
244
- // The system distinguishes by whether a slug segment exists after the path
245
- }
246
- // Warn about deprecated collectionTemplates format
247
- if (templates.collectionTemplates) {
248
- warnings.push('- cmsTemplates.collectionTemplates: This nested format is deprecated. Use flat format instead: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath');
249
183
  }
250
184
  }
251
185
  // Build result
252
186
  let output = '';
253
- // Build custom collections summary
254
- const customCollectionsSummary = customCollections.size > 0
255
- ? `\n- Custom collections: ${Array.from(customCollections).join(', ')} (using flat format: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath)`
187
+ // Build collections summary
188
+ const collectionsSummary = customCollections.size > 0
189
+ ? `\n- Collections: ${Array.from(customCollections).join(', ')}`
256
190
  : '';
257
191
  if (errors.length === 0 && warnings.length === 0) {
258
- output = `✅ MANIFEST VALID
192
+ output = `MANIFEST VALID
259
193
 
260
194
  The manifest.json structure is correct.
261
195
 
262
196
  Summary:
263
197
  - ${m.pages?.length || 0} static page(s) defined
264
- - CMS templates: ${templates ? 'configured in manifest' : 'not configured (can be set via Settings → CMS Templates after upload)'}${customCollectionsSummary}
198
+ - CMS templates: ${templates ? 'configured in manifest' : 'not configured (can be set via Settings → CMS Templates after upload)'}${collectionsSummary}
265
199
  - Head HTML: ${m.defaultHeadHtml ? 'configured' : 'not configured'}`;
266
200
  }
267
201
  else if (errors.length === 0) {
268
- output = `⚠️ MANIFEST VALID WITH WARNINGS
202
+ output = `MANIFEST VALID WITH WARNINGS
269
203
 
270
204
  The manifest structure is valid but has potential issues:
271
205
 
272
206
  Warnings:
273
207
  ${warnings.join('\n')}
274
208
 
275
- 💡 Tip: CMS templates can also be configured after upload via Dashboard → Settings → CMS Templates`;
209
+ Tip: CMS templates can also be configured after upload via Dashboard → Settings → CMS Templates`;
276
210
  }
277
211
  else {
278
- output = `❌ MANIFEST INVALID
212
+ output = `MANIFEST INVALID
279
213
 
280
214
  Errors (must fix):
281
215
  ${errors.join('\n')}`;
@@ -1 +1 @@
1
- {"version":3,"file":"validate-package.d.ts","sourceRoot":"","sources":["../../src/tools/validate-package.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAAE,EAClB,eAAe,EAAE,MAAM,EACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACxC,OAAO,CAAC,MAAM,CAAC,CA6LjB"}
1
+ {"version":3,"file":"validate-package.d.ts","sourceRoot":"","sources":["../../src/tools/validate-package.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAAE,EAClB,eAAe,EAAE,MAAM,EACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACxC,OAAO,CAAC,MAAM,CAAC,CAwMjB"}