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.
- package/dist/index.js +5 -6
- package/dist/tools/get-conversion-guide.d.ts.map +1 -1
- package/dist/tools/get-conversion-guide.js +134 -295
- package/dist/tools/get-example.d.ts.map +1 -1
- package/dist/tools/get-example.js +166 -409
- package/dist/tools/get-schema.d.ts +1 -1
- package/dist/tools/get-schema.d.ts.map +1 -1
- package/dist/tools/get-schema.js +130 -450
- package/dist/tools/get-tenant-schema.d.ts +9 -3
- package/dist/tools/get-tenant-schema.d.ts.map +1 -1
- package/dist/tools/get-tenant-schema.js +16 -4
- package/dist/tools/sync-schema.d.ts +1 -1
- package/dist/tools/sync-schema.d.ts.map +1 -1
- package/dist/tools/sync-schema.js +51 -97
- package/dist/tools/validate-manifest.d.ts.map +1 -1
- package/dist/tools/validate-manifest.js +41 -107
- package/dist/tools/validate-package.d.ts.map +1 -1
- package/dist/tools/validate-package.js +46 -41
- package/dist/tools/validate-template.d.ts +2 -2
- package/dist/tools/validate-template.d.ts.map +1 -1
- package/dist/tools/validate-template.js +67 -201
- package/package.json +1 -1
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fetches the complete schema for a specific project including:
|
|
3
|
-
* -
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
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
|
|
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
|
-
* -
|
|
42
|
-
* -
|
|
43
|
-
* -
|
|
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
|
-
|
|
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
|
|
|
@@ -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;
|
|
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
|
|
97
|
+
* Fetch existing collections for a project
|
|
99
98
|
*/
|
|
100
|
-
async function
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
|
208
|
-
const
|
|
209
|
-
if ('error' in
|
|
195
|
+
// Fetch existing collections
|
|
196
|
+
const existingResult = await fetchExistingCollections(tenantId);
|
|
197
|
+
if ('error' in existingResult) {
|
|
210
198
|
// Check if auth error
|
|
211
|
-
if (
|
|
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
|
|
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${
|
|
211
|
+
return `# Error\n\n${existingResult.error}`;
|
|
225
212
|
}
|
|
226
213
|
}
|
|
227
|
-
const
|
|
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(
|
|
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(`
|
|
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(`
|
|
251
|
+
results.push(` Failed to add field "${field.slug}": ${fieldRes.error}`);
|
|
265
252
|
}
|
|
266
253
|
else {
|
|
267
|
-
results.push(`
|
|
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(
|
|
275
|
+
results.push(`Failed to create collection "${col.slug}": ${createRes.error}`);
|
|
289
276
|
continue;
|
|
290
277
|
}
|
|
291
|
-
results.push(
|
|
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(`
|
|
298
|
+
results.push(` Failed to add field "${field.slug}": ${fieldRes.error}`);
|
|
312
299
|
}
|
|
313
300
|
else {
|
|
314
|
-
results.push(`
|
|
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(`\
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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":"
|
|
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
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
78
|
+
return `INVALID JSON
|
|
100
79
|
|
|
101
80
|
Error: ${errorMessage}
|
|
102
81
|
${diagnostics}
|
|
103
82
|
|
|
104
|
-
|
|
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
|
-
//
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
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 (!
|
|
153
|
-
|
|
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
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
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
|
|
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(`-
|
|
172
|
+
warnings.push(`- Collection "${slug}": has ${slug}Index but no ${slug}IndexPath`);
|
|
233
173
|
}
|
|
234
174
|
if (detailTemplate && !detailPath) {
|
|
235
|
-
warnings.push(`-
|
|
175
|
+
warnings.push(`- Collection "${slug}": has ${slug}Detail but no ${slug}DetailPath`);
|
|
236
176
|
}
|
|
237
177
|
if (indexPath && !indexTemplate) {
|
|
238
|
-
warnings.push(`-
|
|
178
|
+
warnings.push(`- Collection "${slug}": has ${slug}IndexPath but no ${slug}Index template`);
|
|
239
179
|
}
|
|
240
180
|
if (detailPath && !detailTemplate) {
|
|
241
|
-
warnings.push(`-
|
|
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
|
|
254
|
-
const
|
|
255
|
-
? `\n-
|
|
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 =
|
|
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)'}${
|
|
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 =
|
|
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
|
-
|
|
209
|
+
Tip: CMS templates can also be configured after upload via Dashboard → Settings → CMS Templates`;
|
|
276
210
|
}
|
|
277
211
|
else {
|
|
278
|
-
output =
|
|
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,
|
|
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"}
|