@kiubi/mcp-designer 1.0.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.
Files changed (65) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +219 -0
  3. package/package.json +57 -0
  4. package/src/app.ts +39 -0
  5. package/src/index.ts +21 -0
  6. package/src/lib/stemmer.ts +38 -0
  7. package/src/lib/theme-checker/block-validator.ts +353 -0
  8. package/src/lib/theme-checker/checker.ts +897 -0
  9. package/src/lib/theme-checker/filter-validator.ts +163 -0
  10. package/src/lib/theme-checker/index.ts +6 -0
  11. package/src/lib/theme-checker/inspector.ts +193 -0
  12. package/src/lib/theme-checker/lang.ts +42 -0
  13. package/src/lib/theme-checker/parsers/component-xml.ts +120 -0
  14. package/src/lib/theme-checker/parsers/model-xml.ts +82 -0
  15. package/src/lib/theme-checker/parsers/post-type-xml.ts +92 -0
  16. package/src/lib/theme-checker/parsers/symbol-xml.ts +162 -0
  17. package/src/lib/theme-checker/parsers/theme-xml.ts +102 -0
  18. package/src/lib/theme-checker/types.ts +97 -0
  19. package/src/logger.ts +28 -0
  20. package/src/tool.ts +13 -0
  21. package/src/tools/create_type.ts +535 -0
  22. package/src/tools/detail_type.ts +208 -0
  23. package/src/tools/get_doc_section.ts +201 -0
  24. package/src/tools/list_types.ts +87 -0
  25. package/src/tools/list_widgets.ts +181 -0
  26. package/src/tools/ruleset.ts +25 -0
  27. package/src/tools/search_doc.ts +160 -0
  28. package/src/tools/validate_theme.ts +63 -0
  29. package/storage/definitions/blog.json +1444 -0
  30. package/storage/definitions/catalogue.json +2817 -0
  31. package/storage/definitions/cms.json +1002 -0
  32. package/storage/definitions/commandes.json +2409 -0
  33. package/storage/definitions/comptes.json +780 -0
  34. package/storage/definitions/modules.json +418 -0
  35. package/storage/definitions/recherche.json +804 -0
  36. package/storage/documents/CLAUDE.exemple.md +281 -0
  37. package/storage/documents/generated/exemples.md +8054 -0
  38. package/storage/documents/generated/integration/01_theme_perso.md +182 -0
  39. package/storage/documents/generated/integration/02_modeles.md +220 -0
  40. package/storage/documents/generated/integration/03_widgets.md +139 -0
  41. package/storage/documents/generated/integration/04_blocs.md +45 -0
  42. package/storage/documents/generated/integration/05_balises.md +43 -0
  43. package/storage/documents/generated/integration/06_balises_globales.md +201 -0
  44. package/storage/documents/generated/integration/07_balises_widget.md +257 -0
  45. package/storage/documents/generated/integration/08_types_billets.md +154 -0
  46. package/storage/documents/generated/integration/09_types_produits.md +77 -0
  47. package/storage/documents/generated/integration/10_types_composants.md +142 -0
  48. package/storage/documents/generated/integration/11_symboles.md +156 -0
  49. package/storage/documents/generated/integration/12_mediatheque.md +66 -0
  50. package/storage/documents/generated/integration/13_includes.md +25 -0
  51. package/storage/documents/generated/integration/14_404.md +32 -0
  52. package/storage/documents/generated/integration/15_arianne.md +62 -0
  53. package/storage/documents/generated/integration/16_fermeture.md +36 -0
  54. package/storage/documents/generated/integration/17_recherche.md +36 -0
  55. package/storage/documents/generated/integration/18_cms.md +582 -0
  56. package/storage/documents/generated/integration/19_modules.md +479 -0
  57. package/storage/documents/generated/integration/20_blog.md +946 -0
  58. package/storage/documents/generated/integration/21_catalogue.md +1834 -0
  59. package/storage/documents/generated/integration/22_commandes.md +2772 -0
  60. package/storage/documents/generated/integration/23_comptes.md +1195 -0
  61. package/storage/documents/generated/integration/24_recherche.md +542 -0
  62. package/storage/documents/hors_sujet_regles.md +43 -0
  63. package/storage/documents/kiubi_api.md +6690 -0
  64. package/storage/documents/kiubi_ruleset.md +419 -0
  65. package/storage/documents/kiubi_workflows.md +121 -0
@@ -0,0 +1,535 @@
1
+ import { z } from "zod";
2
+ import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import * as nodePath from 'node:path';
4
+ import { XMLParser } from 'fast-xml-parser';
5
+ import { defineTool } from "../tool.ts";
6
+ import { ThemeInspector, resolveLangs } from "#lib/theme-checker/index";
7
+
8
+ // ─── Internal types ───────────────────────────────────────────────────────────
9
+
10
+ type TypeKind = "billet_page" | "billet_blog" | "produit" | "composant" | "symbole" | "template";
11
+
12
+ const CHAMP_TYPES = ["text", "textarea", "wysiwyg", "image", "fichier", "select", "color", "date", "datetime", "checkbox"] as const;
13
+ const COLMAX_VALUES = ["1", "2", "2d", "2g", "3"] as const;
14
+
15
+ export type ChampInput = {
16
+ name: string;
17
+ type: typeof CHAMP_TYPES[number];
18
+ label: string;
19
+ hint?: string;
20
+ default?: string;
21
+ options?: { value: string; label: string }[];
22
+ };
23
+
24
+ export type ZoneInput = {
25
+ id: string;
26
+ label: string;
27
+ maxColumns: typeof COLMAX_VALUES[number];
28
+ defaultColumns: typeof COLMAX_VALUES[number];
29
+ allowContent: boolean;
30
+ };
31
+
32
+ // ─── Constants ────────────────────────────────────────────────────────────────
33
+
34
+ const TYPE_SUBDIR: Record<TypeKind, string> = {
35
+ billet_page: "billets",
36
+ billet_blog: "billets_blog",
37
+ produit: "produits",
38
+ composant: "composants",
39
+ symbole: "symboles",
40
+ template: "templates",
41
+ };
42
+
43
+ const FIELD_VALIDATORS: Record<TypeKind, RegExp | null> = {
44
+ billet_page: /^(titre|sstitre|texte([1-9]|1[0-5]))$/,
45
+ billet_blog: /^texte([1-9]|1[0-5])$/,
46
+ produit: /^texte([1-9]|1[0-5])$/,
47
+ composant: /^[a-zA-Z0-9_]{1,40}$/,
48
+ symbole: /^[a-zA-Z0-9_]{1,40}$/,
49
+ template: null,
50
+ };
51
+
52
+ // ─── Validation ───────────────────────────────────────────────────────────────
53
+
54
+ const VALID_REPERTOIRE = /^[a-zA-Z0-9_]{1,40}$/;
55
+
56
+ export function validateRepertoire(name: string): string | null {
57
+ if (!VALID_REPERTOIRE.test(name)) {
58
+ return `Répertoire invalide "${name}" : seuls les caractères alphanumériques ASCII et _ sont autorisés (1-40 caractères, sans / ni ..).`;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ // ─── XML helpers ──────────────────────────────────────────────────────────────
64
+
65
+ function escapeAttr(s: string): string {
66
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
67
+ }
68
+
69
+ function escapeText(s: string): string {
70
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
71
+ }
72
+
73
+ function buildChampXml(c: ChampInput, indent: string): string {
74
+ let attrs = `champ="${escapeAttr(c.name)}" type="${c.type}" intitule="${escapeAttr(c.label)}"`;
75
+ if (c.default !== undefined && c.default !== '') attrs += ` defaut="${escapeAttr(c.default)}"`;
76
+ if (c.hint !== undefined && c.hint !== '') attrs += ` aide="${escapeAttr(c.hint)}"`;
77
+
78
+ if ((c.type === 'select' || c.type === 'checkbox') && c.options && c.options.length > 0) {
79
+ const lines = [`${indent}<champ ${attrs}>`];
80
+ for (const o of c.options) {
81
+ lines.push(`${indent} <option value="${escapeAttr(o.value)}">${escapeText(o.label)}</option>`);
82
+ }
83
+ lines.push(`${indent}</champ>`);
84
+ return lines.join('\n');
85
+ }
86
+
87
+ return `${indent}<champ ${attrs} />`;
88
+ }
89
+
90
+ function buildListeChamps(fields: ChampInput[], indent: string): string[] {
91
+ const lines: string[] = [];
92
+ if (fields.length === 0) {
93
+ lines.push(`${indent}<listechamps />`);
94
+ } else {
95
+ lines.push(`${indent}<listechamps>`);
96
+ for (const c of fields) {
97
+ lines.push(buildChampXml(c, indent + ' '));
98
+ }
99
+ lines.push(`${indent}</listechamps>`);
100
+ }
101
+ return lines;
102
+ }
103
+
104
+ // ─── XML builders ─────────────────────────────────────────────────────────────
105
+
106
+ export type ConfigXmlParams = {
107
+ kind: "billet_page" | "billet_blog" | "produit" | "composant";
108
+ label: string;
109
+ tri: number;
110
+ fields: ChampInput[];
111
+ collectionFields?: ChampInput[];
112
+ };
113
+
114
+ export function buildConfigXml(params: ConfigXmlParams): string {
115
+ const { kind, label, tri, fields, collectionFields } = params;
116
+ const header = '<?xml version="1.0" encoding="iso-8859-1"?>';
117
+
118
+ if (kind === 'composant') {
119
+ const dtd = '<!DOCTYPE composant SYSTEM "http://www.kiubi-admin.com/DTD/composants.dtd">';
120
+ const lines = [
121
+ header,
122
+ dtd,
123
+ `<composant intitule="${escapeAttr(label)}" tri="${tri}">`,
124
+ ...buildListeChamps(fields, ' '),
125
+ ];
126
+ if (collectionFields && collectionFields.length > 0) {
127
+ lines.push(' <collection>');
128
+ for (const c of collectionFields) {
129
+ lines.push(buildChampXml(c, ' '));
130
+ }
131
+ lines.push(' </collection>');
132
+ }
133
+ lines.push('</composant>');
134
+ return lines.join('\n') + '\n';
135
+ }
136
+
137
+ const dtdMap: Record<string, string> = {
138
+ billet_page: '<!DOCTYPE type SYSTEM "http://www.kiubi-admin.com/DTD/typesbillets.dtd">',
139
+ billet_blog: '<!DOCTYPE type SYSTEM "http://www.kiubi-admin.com/DTD/typesbillets.dtd">',
140
+ produit: '<!DOCTYPE type SYSTEM "http://www.kiubi-admin.com/DTD/typesproduits.dtd">',
141
+ };
142
+ const lines = [
143
+ header,
144
+ dtdMap[kind],
145
+ `<type tri="${tri}">`,
146
+ ` <desc>${escapeText(label)}</desc>`,
147
+ ...buildListeChamps(fields, ' '),
148
+ '</type>',
149
+ ];
150
+ return lines.join('\n') + '\n';
151
+ }
152
+
153
+ export type DescXmlParams = {
154
+ kind: "symbole" | "template";
155
+ label: string;
156
+ tri?: number;
157
+ fields?: ChampInput[];
158
+ zones: ZoneInput[];
159
+ };
160
+
161
+ export function buildDescXml(params: DescXmlParams): string {
162
+ const { kind, label, tri, fields = [], zones } = params;
163
+ const header = '<?xml version="1.0" encoding="iso-8859-1"?>';
164
+
165
+ const defautZone = zones.find(z => z.allowContent) ?? zones[0];
166
+ const defautId = defautZone?.id ?? '';
167
+
168
+ const buildZones = (indent: string): string[] => {
169
+ const defautAttr = defautId ? ` defaut="${escapeAttr(defautId)}"` : '';
170
+ const lines = [`${indent}<zones${defautAttr}>`];
171
+ for (const z of zones) {
172
+ let attrs = `id="${escapeAttr(z.id)}" colmax="${z.maxColumns}" intitule="${escapeAttr(z.label)}" defaut="${z.defaultColumns}"`;
173
+ if (z.allowContent) attrs += ` type="contenu"`;
174
+ lines.push(`${indent} <zone ${attrs} />`);
175
+ }
176
+ lines.push(`${indent}</zones>`);
177
+ return lines;
178
+ };
179
+
180
+ if (kind === 'template') {
181
+ const lines = [
182
+ header,
183
+ `<modele intitule="${escapeAttr(label)}">`,
184
+ ...buildZones(' '),
185
+ '</modele>',
186
+ ];
187
+ return lines.join('\n') + '\n';
188
+ }
189
+
190
+ // symbole
191
+ const dtd = '<!DOCTYPE symbole SYSTEM "http://www.kiubi-admin.com/DTD/symboles.dtd">';
192
+ const lines = [
193
+ header,
194
+ dtd,
195
+ `<symbole intitule="${escapeAttr(label)}" tri="${tri ?? 1}">`,
196
+ ...buildListeChamps(fields, ' '),
197
+ ...buildZones(' '),
198
+ '</symbole>',
199
+ ];
200
+ return lines.join('\n') + '\n';
201
+ }
202
+
203
+ // ─── HTML builders ────────────────────────────────────────────────────────────
204
+
205
+ export type IndexHtmlParams = {
206
+ kind: TypeKind;
207
+ label: string;
208
+ fields: ChampInput[];
209
+ zones: ZoneInput[];
210
+ collectionFields?: ChampInput[];
211
+ };
212
+
213
+ function renderChamp(c: ChampInput, indent: string): string {
214
+ if (c.type === 'image' || c.type === 'fichier') {
215
+ return `${indent}<!-- BEGIN:${c.name} --><img src="{${c.name}}" alt="" /><!-- END:${c.name} -->`;
216
+ }
217
+ if (c.type === 'wysiwyg' || c.type === 'textarea') {
218
+ return `${indent}<div>{${c.name}}</div>`;
219
+ }
220
+ return `${indent}<span>{${c.name}}</span>`;
221
+ }
222
+
223
+ export function buildIndexHtml(params: IndexHtmlParams): string {
224
+ const { kind, label, fields, zones, collectionFields } = params;
225
+ const comment = `<!-- Template généré automatiquement pour "${label}" — à adapter -->`;
226
+
227
+ if (kind === 'template') {
228
+ const zoneLines = zones.map(z => `<div>{ZONE.${z.id}}</div>`).join('\n');
229
+ return [
230
+ '<!-- BEGIN:main -->',
231
+ comment,
232
+ '<!DOCTYPE html>',
233
+ '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">',
234
+ '<head>',
235
+ ' <title>{title}</title>',
236
+ ' <meta name="description" content="{metaDescription}" />',
237
+ ' {metaRobots}',
238
+ ' {metaCanonical}',
239
+ ' {js_head}',
240
+ '</head>',
241
+ '<body>',
242
+ zoneLines || '<!-- zones du template -->',
243
+ '{js_body}',
244
+ '</body>',
245
+ '</html>',
246
+ '<!-- END:main -->',
247
+ ].join('\n') + '\n';
248
+ }
249
+
250
+ if (kind === 'symbole') {
251
+ const fieldLines = fields.map(c => renderChamp(c, '')).join('\n');
252
+ const zoneLines = zones.map(z => `<div>{ZONE.${z.id}}</div>`).join('\n');
253
+ return [
254
+ '<!-- BEGIN:main -->',
255
+ comment,
256
+ '<div>',
257
+ fieldLines || '<!-- champs du symbole -->',
258
+ zoneLines || '<!-- zones du symbole -->',
259
+ '</div>',
260
+ '<!-- END:main -->',
261
+ ].join('\n') + '\n';
262
+ }
263
+
264
+ if (kind === 'composant') {
265
+ const fieldLines = fields.map(c => renderChamp(c, ' ')).join('\n');
266
+ const lines = [
267
+ '<!-- BEGIN:main -->',
268
+ comment,
269
+ '<div>',
270
+ ];
271
+ lines.push(fieldLines || ' <!-- champs du composant -->');
272
+ if (collectionFields && collectionFields.length > 0) {
273
+ lines.push(' <!-- BEGIN:collection -->');
274
+ lines.push(' <!-- BEGIN:item -->');
275
+ for (const c of collectionFields) {
276
+ lines.push(renderChamp(c, ' '));
277
+ }
278
+ lines.push(' <!-- END:item -->');
279
+ lines.push(' <!-- BEGIN:empty -->');
280
+ lines.push(' <!-- END:empty -->');
281
+ lines.push(' <!-- END:collection -->');
282
+ }
283
+ lines.push('</div>');
284
+ lines.push('<!-- END:main -->');
285
+ return lines.join('\n') + '\n';
286
+ }
287
+
288
+ // billet_page, produit
289
+ const fieldLines = fields.map(c => renderChamp(c, '')).join('\n');
290
+ return [
291
+ '<!-- BEGIN:main -->',
292
+ comment,
293
+ fieldLines || '<!-- champs du billet -->',
294
+ '<!-- END:main -->',
295
+ ].join('\n') + '\n';
296
+ }
297
+
298
+ export function buildStructureXhtml(zones: ZoneInput[]): string {
299
+ const rows = zones.map(z => `\t<tr>\n\t\t<td>{${z.id}}</td>\n\t</tr>`).join('\n');
300
+ return `<table>\n${rows}\n</table>\n`;
301
+ }
302
+
303
+ // ─── Tri computation ──────────────────────────────────────────────────────────
304
+
305
+ const triParser = new XMLParser({
306
+ ignoreAttributes: false,
307
+ attributeNamePrefix: '',
308
+ transformTagName: (n: string) => n.toLowerCase(),
309
+ transformAttributeName: (n: string) => n.toUpperCase(),
310
+ });
311
+
312
+ export function readExistingTri(filePath: string): number | null {
313
+ try {
314
+ const content = readFileSync(filePath, 'latin1');
315
+ const doc = triParser.parse(content);
316
+ const root = doc?.type ?? doc?.composant ?? doc?.symbole;
317
+ if (root && root.TRI !== undefined) {
318
+ const n = parseInt(String(root.TRI), 10);
319
+ return isNaN(n) ? null : n;
320
+ }
321
+ return null;
322
+ } catch {
323
+ return null;
324
+ }
325
+ }
326
+
327
+ export function computeTri(themePath: string, kind: TypeKind, lang: string = 'fr'): number {
328
+ if (kind === 'template') return 1;
329
+ const inspector = new ThemeInspector(themePath, lang);
330
+ let items: { position?: number }[];
331
+ switch (kind) {
332
+ case 'billet_page': items = inspector.listCms(); break;
333
+ case 'billet_blog': items = inspector.listBlogs(); break;
334
+ case 'produit': items = inspector.listProducts(); break;
335
+ case 'composant': items = inspector.listComponents(); break;
336
+ case 'symbole': items = inspector.listSymbols(); break;
337
+ }
338
+ const max = items.reduce((acc, i) => Math.max(acc, i.position ?? 0), 0);
339
+ return max + 1;
340
+ }
341
+
342
+ // ─── Main function ────────────────────────────────────────────────────────────
343
+
344
+ export type CreateTypeParams = {
345
+ themePath: string;
346
+ type: TypeKind;
347
+ name: string;
348
+ label: string;
349
+ fields: ChampInput[];
350
+ collectionFields?: ChampInput[];
351
+ zones: ZoneInput[];
352
+ lang?: string[] | null;
353
+ };
354
+
355
+ // Crée/met à jour le type dans un seul dossier de langue, retourne les messages associés.
356
+ function createTypeForLang(params: {
357
+ themePath: string;
358
+ lang: string;
359
+ type: TypeKind;
360
+ name: string;
361
+ label: string;
362
+ fields: ChampInput[];
363
+ collectionFields?: ChampInput[];
364
+ zones: ZoneInput[];
365
+ }): string[] {
366
+ const { themePath, lang, type, name, label, fields, collectionFields, zones } = params;
367
+
368
+ const subdir = nodePath.join(lang, TYPE_SUBDIR[type]);
369
+ const typeDir = nodePath.join(themePath, subdir, name);
370
+ const xmlFile = (type === 'symbole' || type === 'template') ? 'desc.xml' : 'config.xml';
371
+ const xmlPath = nodePath.join(typeDir, xmlFile);
372
+ const htmlPath = nodePath.join(typeDir, 'index.html');
373
+ const structurePath = nodePath.join(typeDir, 'structure.xhtml');
374
+
375
+ // Compute tri (preserve existing if file already exists)
376
+ let tri: number;
377
+ if (existsSync(xmlPath)) {
378
+ tri = readExistingTri(xmlPath) ?? 1;
379
+ } else {
380
+ tri = computeTri(themePath, type, lang);
381
+ }
382
+
383
+ // Create directory
384
+ mkdirSync(typeDir, { recursive: true });
385
+
386
+ // Write XML
387
+ let xmlContent: string;
388
+ if (type === 'symbole' || type === 'template') {
389
+ xmlContent = buildDescXml({ kind: type, label, tri, fields, zones });
390
+ } else {
391
+ xmlContent = buildConfigXml({ kind: type as ConfigXmlParams['kind'], label, tri, fields, collectionFields });
392
+ }
393
+ writeFileSync(xmlPath, xmlContent, 'latin1');
394
+
395
+ const relPath = `${subdir}/${name}`;
396
+ const messages: string[] = [`${xmlFile} créé/mis à jour : ${relPath}/${xmlFile}`];
397
+
398
+ // No index.html for billet_blog
399
+ if (type !== 'billet_blog') {
400
+ if (existsSync(htmlPath)) {
401
+ messages.push(`index.html non modifié (fichier existant conservé)`);
402
+ } else {
403
+ const html = buildIndexHtml({ kind: type, label, fields, zones, collectionFields });
404
+ writeFileSync(htmlPath, html, 'latin1');
405
+ messages.push(`index.html créé — à adapter : ${relPath}/index.html`);
406
+ }
407
+ }
408
+
409
+ // structure.xhtml for templates and symboles
410
+ if (type === 'template' || type === 'symbole') {
411
+ if (existsSync(structurePath)) {
412
+ messages.push(`structure.xhtml non modifié (fichier existant conservé)`);
413
+ } else {
414
+ writeFileSync(structurePath, buildStructureXhtml(zones), 'latin1');
415
+ messages.push(`structure.xhtml créé : ${relPath}/structure.xhtml`);
416
+ }
417
+ }
418
+
419
+ return messages;
420
+ }
421
+
422
+ export function createType(params: CreateTypeParams): string {
423
+ const { themePath, type, name, label, fields, collectionFields, zones, lang } = params;
424
+
425
+ // Validate name
426
+ const err = validateRepertoire(name);
427
+ if (err) return `Erreur : ${err}`;
428
+
429
+ // Validate field names
430
+ const validator = FIELD_VALIDATORS[type];
431
+ if (type === 'template' && fields.length > 0) {
432
+ return `Erreur : les templates ne supportent pas de champs (champs ignorés).`;
433
+ }
434
+ if (validator) {
435
+ const invalid = fields.filter(c => !validator.test(c.name)).map(c => c.name);
436
+ if (invalid.length > 0) {
437
+ return `Erreur : nom(s) de champ invalide(s) pour le type "${type}" : ${invalid.join(', ')}.`;
438
+ }
439
+ }
440
+ if (collectionFields !== undefined) {
441
+ if (type !== 'composant') {
442
+ return `Erreur : la collection n'est disponible que pour les composants.`;
443
+ }
444
+ const compValidator = FIELD_VALIDATORS.composant!;
445
+ const invalid = collectionFields.filter(c => !compValidator.test(c.name)).map(c => c.name);
446
+ if (invalid.length > 0) {
447
+ return `Erreur : nom(s) de champ de collection invalide(s) : ${invalid.join(', ')}.`;
448
+ }
449
+ }
450
+
451
+ // Resolve target languages
452
+ const resolved = resolveLangs(themePath, lang);
453
+ if ('error' in resolved) return `Erreur : ${resolved.error}`;
454
+
455
+ const blocks = resolved.map(l => {
456
+ const messages = createTypeForLang({ themePath, lang: l, type, name, label, fields, collectionFields, zones });
457
+ return `[${l}]\n${messages.join('\n')}`;
458
+ });
459
+
460
+ return blocks.join('\n\n');
461
+ }
462
+
463
+ // ─── Tool definition ──────────────────────────────────────────────────────────
464
+
465
+ // Factory : need to create un distinct Zod instance
466
+ // Avoid zod-to-json-schema optimization that would create a $ref type
467
+ // that som MCP clients would not resolve
468
+ function createFieldSchema() {
469
+ return z.object({
470
+ name: z.string().describe("Nom du champ"),
471
+ type: z.enum(CHAMP_TYPES).describe("Type de saisie"),
472
+ label: z.string().describe("Label en français affiché dans le back-office"),
473
+ hint: z.string().optional().describe("Texte d'aide (optionnel)"),
474
+ default: z.string().optional().describe("Valeur par défaut (optionnel)"),
475
+ options: z.array(z.object({
476
+ value: z.string().describe("Valeur de l'option"),
477
+ label: z.string().describe("Label de l'option"),
478
+ })).optional().describe("Options pour les champs select/checkbox"),
479
+ });
480
+ }
481
+
482
+ export const createTypeTool = defineTool({
483
+ name: "create_type",
484
+ description:
485
+ "Crée ou met à jour un type d'élément dans un thème graphique Kiubi : " +
486
+ "billet de contenu (billet_page), billet de blog (billet_blog), produit, composant, symbole ou template principal. " +
487
+ "Génère les fichiers XML (config.xml ou desc.xml) et, si absent, un template HTML de base à adapter. " +
488
+ "Idempotent : si les fichiers existent déjà, le XML est mis à jour (tri conservé) sans toucher au HTML existant. " +
489
+ "Le tri est calculé automatiquement (max existant + 1). " +
490
+ "Noms de champs : billet_page (titre, sstitre, texte1-15), billet_blog/produit (texte1-15), " +
491
+ "composant/symbole (alphanumérique ASCII + _, max 40 car.). " +
492
+ "billet_blog : pas de index.html (template dans widgets/blog/). " +
493
+ "template/symbole : nécessitent des zones. " +
494
+ "lang : si omis (ou vide), crée l'élément dans tous les dossiers de langue (2 lettres) trouvés à la racine du " +
495
+ "thème ; si précisé, uniquement dans les langues listées (erreur si un dossier de langue demandé n'existe pas). " +
496
+ "Le tri est calculé indépendamment pour chaque langue. Le résultat liste les fichiers créés/mis à jour par langue.",
497
+ inputSchema: {
498
+ themePath: z.string().min(1).describe("Chemin absolu vers le dossier racine du thème graphique"),
499
+ type: z.enum(["billet_page", "billet_blog", "produit", "composant", "symbole", "template"])
500
+ .describe("Type d'élément à créer ou mettre à jour"),
501
+ name: z.string().describe("Nom du dossier (ASCII alphanumérique + _, 1-40 car., sans / ni ..)"),
502
+ label: z.string().describe("Intitulé du type en français affiché dans le back-office"),
503
+ lang: z.array(z.string().regex(/^[a-z]{2}$/, "code langue sur 2 lettres minuscules attendu (ex. \"fr\")"))
504
+ .optional()
505
+ .describe("Codes langue (2 lettres minuscules, ex. [\"fr\",\"de\"]) où créer l'élément. Omis ou vide : tous les dossiers de langue du thème."),
506
+ fields: z.array(createFieldSchema())
507
+ .optional().default([])
508
+ .describe("Champs de saisie. Noms autorisés selon le type (voir description)."),
509
+ collectionFields: z.array(createFieldSchema())
510
+ .optional()
511
+ .describe("Champs des items de collection (composants uniquement)"),
512
+ zones: z.array(z.object({
513
+ id: z.string().describe("Identifiant de la zone (utilisé dans index.html avec {ZONE.id})"),
514
+ label: z.string().describe("Label de la zone dans le back-office"),
515
+ maxColumns: z.enum(COLMAX_VALUES).default("1").describe("Nombre max de colonnes (1, 2, 2d, 2g, 3)"),
516
+ defaultColumns: z.enum(COLMAX_VALUES).default("1").describe("Nombre de colonnes par défaut"),
517
+ allowContent: z.boolean().default(false).describe("Si true, la zone accepte les billets et composants"),
518
+ }))
519
+ .optional().default([])
520
+ .describe("Zones de dépôt de widgets (templates et symboles uniquement)"),
521
+ },
522
+ handler: async ({ themePath, type, name, label, fields, collectionFields, zones, lang }) => {
523
+ const result = createType({
524
+ themePath,
525
+ type,
526
+ name,
527
+ label,
528
+ fields: fields ?? [],
529
+ collectionFields,
530
+ zones: zones ?? [],
531
+ lang,
532
+ });
533
+ return { content: [{ type: "text" as const, text: result }] };
534
+ },
535
+ });