@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.
- package/LICENSE +15 -0
- package/README.md +219 -0
- package/package.json +57 -0
- package/src/app.ts +39 -0
- package/src/index.ts +21 -0
- package/src/lib/stemmer.ts +38 -0
- package/src/lib/theme-checker/block-validator.ts +353 -0
- package/src/lib/theme-checker/checker.ts +897 -0
- package/src/lib/theme-checker/filter-validator.ts +163 -0
- package/src/lib/theme-checker/index.ts +6 -0
- package/src/lib/theme-checker/inspector.ts +193 -0
- package/src/lib/theme-checker/lang.ts +42 -0
- package/src/lib/theme-checker/parsers/component-xml.ts +120 -0
- package/src/lib/theme-checker/parsers/model-xml.ts +82 -0
- package/src/lib/theme-checker/parsers/post-type-xml.ts +92 -0
- package/src/lib/theme-checker/parsers/symbol-xml.ts +162 -0
- package/src/lib/theme-checker/parsers/theme-xml.ts +102 -0
- package/src/lib/theme-checker/types.ts +97 -0
- package/src/logger.ts +28 -0
- package/src/tool.ts +13 -0
- package/src/tools/create_type.ts +535 -0
- package/src/tools/detail_type.ts +208 -0
- package/src/tools/get_doc_section.ts +201 -0
- package/src/tools/list_types.ts +87 -0
- package/src/tools/list_widgets.ts +181 -0
- package/src/tools/ruleset.ts +25 -0
- package/src/tools/search_doc.ts +160 -0
- package/src/tools/validate_theme.ts +63 -0
- package/storage/definitions/blog.json +1444 -0
- package/storage/definitions/catalogue.json +2817 -0
- package/storage/definitions/cms.json +1002 -0
- package/storage/definitions/commandes.json +2409 -0
- package/storage/definitions/comptes.json +780 -0
- package/storage/definitions/modules.json +418 -0
- package/storage/definitions/recherche.json +804 -0
- package/storage/documents/CLAUDE.exemple.md +281 -0
- package/storage/documents/generated/exemples.md +8054 -0
- package/storage/documents/generated/integration/01_theme_perso.md +182 -0
- package/storage/documents/generated/integration/02_modeles.md +220 -0
- package/storage/documents/generated/integration/03_widgets.md +139 -0
- package/storage/documents/generated/integration/04_blocs.md +45 -0
- package/storage/documents/generated/integration/05_balises.md +43 -0
- package/storage/documents/generated/integration/06_balises_globales.md +201 -0
- package/storage/documents/generated/integration/07_balises_widget.md +257 -0
- package/storage/documents/generated/integration/08_types_billets.md +154 -0
- package/storage/documents/generated/integration/09_types_produits.md +77 -0
- package/storage/documents/generated/integration/10_types_composants.md +142 -0
- package/storage/documents/generated/integration/11_symboles.md +156 -0
- package/storage/documents/generated/integration/12_mediatheque.md +66 -0
- package/storage/documents/generated/integration/13_includes.md +25 -0
- package/storage/documents/generated/integration/14_404.md +32 -0
- package/storage/documents/generated/integration/15_arianne.md +62 -0
- package/storage/documents/generated/integration/16_fermeture.md +36 -0
- package/storage/documents/generated/integration/17_recherche.md +36 -0
- package/storage/documents/generated/integration/18_cms.md +582 -0
- package/storage/documents/generated/integration/19_modules.md +479 -0
- package/storage/documents/generated/integration/20_blog.md +946 -0
- package/storage/documents/generated/integration/21_catalogue.md +1834 -0
- package/storage/documents/generated/integration/22_commandes.md +2772 -0
- package/storage/documents/generated/integration/23_comptes.md +1195 -0
- package/storage/documents/generated/integration/24_recherche.md +542 -0
- package/storage/documents/hors_sujet_regles.md +43 -0
- package/storage/documents/kiubi_api.md +6690 -0
- package/storage/documents/kiubi_ruleset.md +419 -0
- package/storage/documents/kiubi_workflows.md +121 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
4
|
+
import type { PostTypeDef, FieldDef, ParseResult } from '../types.ts';
|
|
5
|
+
|
|
6
|
+
const parser = new XMLParser({
|
|
7
|
+
ignoreAttributes: false,
|
|
8
|
+
attributeNamePrefix: '',
|
|
9
|
+
textNodeName: '#text',
|
|
10
|
+
isArray: (name: string) => ['champ', 'option'].includes(name),
|
|
11
|
+
transformTagName: (name: string) => name.toLowerCase(),
|
|
12
|
+
transformAttributeName: (name: string) => name.toUpperCase(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const s = (v: unknown): string => v != null ? String(v) : '';
|
|
16
|
+
|
|
17
|
+
// Field name validators by post kind
|
|
18
|
+
const FIELD_NAME_VALIDATORS: Record<string, RegExp> = {
|
|
19
|
+
cms: /^(titre|sstitre|texte[0-9]+)$/,
|
|
20
|
+
blog: /^(texte[a-z0-9]*)$/,
|
|
21
|
+
product: /^(texte[a-z0-9]*)$/,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const ALLOWED_TYPES = new Set([
|
|
25
|
+
'text', 'textarea', 'image', 'fichier', 'wysiwyg', 'select', 'checkbox', 'color', 'date', 'datetime',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export function parsePostTypeXml(
|
|
29
|
+
dir: string,
|
|
30
|
+
id: string,
|
|
31
|
+
kind: 'cms' | 'blog' | 'product',
|
|
32
|
+
): ParseResult<PostTypeDef> {
|
|
33
|
+
try {
|
|
34
|
+
const filePath = nodePath.join(dir, 'config.xml');
|
|
35
|
+
const stat = statSync(filePath);
|
|
36
|
+
if (stat.size > 10 * 1024) return { definition: null, error: 'Fichier trop volumineux (> 10 Ko)' };
|
|
37
|
+
const content = readFileSync(filePath, 'latin1');
|
|
38
|
+
const doc = parser.parse(content);
|
|
39
|
+
const type = doc?.type;
|
|
40
|
+
if (!type) return { definition: null, error: 'Balise <type> manquante' };
|
|
41
|
+
|
|
42
|
+
const validator = FIELD_NAME_VALIDATORS[kind];
|
|
43
|
+
const fields: FieldDef[] = [];
|
|
44
|
+
const champSource = (type.listechamps as Record<string, unknown> | undefined) ?? type;
|
|
45
|
+
const rawChamps: Record<string, unknown>[] = Array.isArray(champSource.champ)
|
|
46
|
+
? champSource.champ as Record<string, unknown>[]
|
|
47
|
+
: (champSource.champ ? [champSource.champ as Record<string, unknown>] : []);
|
|
48
|
+
|
|
49
|
+
for (const c of rawChamps) {
|
|
50
|
+
const champName = s(c.CHAMP);
|
|
51
|
+
if (!validator.test(champName)) return { definition: null, error: `Nom de champ invalide pour le type ${kind} : '${champName}'` };
|
|
52
|
+
const fieldType = ALLOWED_TYPES.has(s(c.TYPE)) ? s(c.TYPE) : 'text';
|
|
53
|
+
const help = s(c.AIDE).slice(0, 255);
|
|
54
|
+
const name = s(c.INTITULE).slice(0, 255) || champName;
|
|
55
|
+
|
|
56
|
+
const rawOptions: unknown[] = Array.isArray(c.option)
|
|
57
|
+
? c.option as unknown[]
|
|
58
|
+
: (c.option ? [c.option] : []);
|
|
59
|
+
|
|
60
|
+
const field: FieldDef = { field: champName, name, type: fieldType, help };
|
|
61
|
+
if ((fieldType === 'select' || fieldType === 'checkbox') && rawOptions.length > 0) {
|
|
62
|
+
const options: Array<{ value: string; label: string }> = [];
|
|
63
|
+
for (const o of rawOptions.slice(0, 100)) {
|
|
64
|
+
const isObj = o !== null && typeof o === 'object';
|
|
65
|
+
const label = isObj ? s((o as Record<string, unknown>)['#text']) : s(o);
|
|
66
|
+
const value = isObj ? (s((o as Record<string, unknown>).VALUE) || label) : label;
|
|
67
|
+
if (!options.some(existing => existing.value === value)) {
|
|
68
|
+
options.push({ value, label: label || value });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
field.options = options;
|
|
72
|
+
}
|
|
73
|
+
fields.push(field);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const def: PostTypeDef = {
|
|
77
|
+
id,
|
|
78
|
+
fields,
|
|
79
|
+
name: s(type.desc),
|
|
80
|
+
position: parseInt(s(type.TRI), 10) || 0,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Only cms and product have a template
|
|
84
|
+
if (kind !== 'blog') {
|
|
85
|
+
def.tpl = nodePath.join(dir, 'index.html');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { definition: def, error: null };
|
|
89
|
+
} catch {
|
|
90
|
+
return { definition: null, error: 'Impossible de lire ou parser le fichier' };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
4
|
+
import type { SymbolDef, ZoneDef, FieldDef, OptionDef, ParseResult } from '../types.ts';
|
|
5
|
+
|
|
6
|
+
const parser = new XMLParser({
|
|
7
|
+
ignoreAttributes: false,
|
|
8
|
+
attributeNamePrefix: '',
|
|
9
|
+
textNodeName: '#text',
|
|
10
|
+
isArray: (name: string) => ['zone', 'champ', 'option'].includes(name),
|
|
11
|
+
transformTagName: (name: string) => name.toLowerCase(),
|
|
12
|
+
transformAttributeName: (name: string) => name.toUpperCase(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const s = (v: unknown): string => v != null ? String(v) : '';
|
|
16
|
+
|
|
17
|
+
const MAX_FIELDS = 20;
|
|
18
|
+
const ALLOWED_COLMAX = ['1', '2', '2d', '2g', '3'];
|
|
19
|
+
const VALID_FIELD_NAME = /^([a-zA-Z0-9_]{1,40})$/;
|
|
20
|
+
const ALLOWED_TYPES = new Set([
|
|
21
|
+
'text', 'textarea', 'image', 'wysiwyg', 'select', 'checkbox', 'color', 'date', 'datetime', 'file',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function parseFieldType(raw: string): string {
|
|
25
|
+
if (raw === 'fichier') return 'file';
|
|
26
|
+
return ALLOWED_TYPES.has(raw) ? raw : 'text';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toArray(v: unknown): Record<string, unknown>[] {
|
|
30
|
+
if (Array.isArray(v)) return v as Record<string, unknown>[];
|
|
31
|
+
if (v) return [v as Record<string, unknown>];
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseZones(zonesEl: Record<string, unknown>): { zones: ZoneDef[]; defaultContentZone: string | null } {
|
|
36
|
+
const rawZones: Record<string, unknown>[] = Array.isArray(zonesEl.zone)
|
|
37
|
+
? zonesEl.zone as Record<string, unknown>[]
|
|
38
|
+
: (zonesEl.zone ? [zonesEl.zone as Record<string, unknown>] : []);
|
|
39
|
+
|
|
40
|
+
const zones: ZoneDef[] = [];
|
|
41
|
+
const seenIds = new Set<string>();
|
|
42
|
+
|
|
43
|
+
for (const z of rawZones) {
|
|
44
|
+
const id = s(z.ID).trim();
|
|
45
|
+
if (!id || seenIds.has(id)) continue;
|
|
46
|
+
seenIds.add(id);
|
|
47
|
+
const colmax = ALLOWED_COLMAX.includes(s(z.COLMAX)) ? s(z.COLMAX) : '1';
|
|
48
|
+
const defaut = ALLOWED_COLMAX.includes(s(z.DEFAUT)) ? s(z.DEFAUT) : '1';
|
|
49
|
+
zones.push({
|
|
50
|
+
id,
|
|
51
|
+
isContent: s(z.TYPE).toUpperCase() === 'CONTENU',
|
|
52
|
+
colmax,
|
|
53
|
+
name: s(z.INTITULE),
|
|
54
|
+
defaut,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Resolve default content zone
|
|
59
|
+
const defautId = s(zonesEl.DEFAUT);
|
|
60
|
+
let defaultContentZone: string | null = null;
|
|
61
|
+
let firstContent: string | null = null;
|
|
62
|
+
|
|
63
|
+
for (const z of zones) {
|
|
64
|
+
if (z.id === defautId) {
|
|
65
|
+
z.isContent = true;
|
|
66
|
+
defaultContentZone = z.id;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (z.isContent && firstContent === null) {
|
|
70
|
+
firstContent = z.id;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (defaultContentZone === null && firstContent !== null) {
|
|
75
|
+
defaultContentZone = firstContent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { zones, defaultContentZone };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseFields(rawChamps: Record<string, unknown>[]): FieldDef[] {
|
|
82
|
+
const fields: FieldDef[] = [];
|
|
83
|
+
const seenNames = new Set<string>();
|
|
84
|
+
|
|
85
|
+
for (const c of rawChamps) {
|
|
86
|
+
const name = s(c.CHAMP);
|
|
87
|
+
if (!VALID_FIELD_NAME.test(name)) continue;
|
|
88
|
+
if (seenNames.has(name)) continue;
|
|
89
|
+
if (fields.length >= MAX_FIELDS) break;
|
|
90
|
+
|
|
91
|
+
const type = parseFieldType(s(c.TYPE));
|
|
92
|
+
seenNames.add(name);
|
|
93
|
+
|
|
94
|
+
const rawOptions: Record<string, unknown>[] = Array.isArray(c.option)
|
|
95
|
+
? c.option as Record<string, unknown>[]
|
|
96
|
+
: (c.option ? [c.option as Record<string, unknown>] : []);
|
|
97
|
+
|
|
98
|
+
const options: OptionDef[] = [];
|
|
99
|
+
for (const o of (rawOptions as unknown[]).slice(0, 100)) {
|
|
100
|
+
const isObj = o !== null && typeof o === 'object';
|
|
101
|
+
const label = isObj ? s((o as Record<string, unknown>)['#text']) : s(o);
|
|
102
|
+
const value = isObj ? (s((o as Record<string, unknown>).VALUE) || label) : label;
|
|
103
|
+
if (!options.some(existing => existing.value === value)) {
|
|
104
|
+
options.push({ value, label: label || value });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let defaultVal: string | null = s(c.DEFAUT) || null;
|
|
109
|
+
if (type === 'image' || type === 'file') defaultVal = null;
|
|
110
|
+
if (type === 'select' && defaultVal !== null && !options.some(o => o.value === defaultVal)) defaultVal = null;
|
|
111
|
+
|
|
112
|
+
const field: FieldDef = {
|
|
113
|
+
field: name,
|
|
114
|
+
name: s(c.INTITULE).slice(0, 80) || name,
|
|
115
|
+
type,
|
|
116
|
+
default: defaultVal,
|
|
117
|
+
help: s(c.AIDE).slice(0, 255),
|
|
118
|
+
};
|
|
119
|
+
if (type === 'select' || type === 'checkbox') field.options = options;
|
|
120
|
+
fields.push(field);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return fields;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function parseSymbolXml(dir: string, id: string): ParseResult<SymbolDef> {
|
|
127
|
+
try {
|
|
128
|
+
const filePath = nodePath.join(dir, 'desc.xml');
|
|
129
|
+
const stat = statSync(filePath);
|
|
130
|
+
if (stat.size > 10 * 1024) return { definition: null, error: 'Fichier trop volumineux (> 10 Ko)' };
|
|
131
|
+
const content = readFileSync(filePath, 'latin1');
|
|
132
|
+
const doc = parser.parse(content);
|
|
133
|
+
const symbole = doc?.symbole;
|
|
134
|
+
if (!symbole) return { definition: null, error: 'Balise <symbole> manquante' };
|
|
135
|
+
|
|
136
|
+
const zonesEl = symbole.zones as Record<string, unknown> | undefined;
|
|
137
|
+
const { zones } = zonesEl ? parseZones(zonesEl) : { zones: [] };
|
|
138
|
+
|
|
139
|
+
const listechampsEl = symbole.listechamps as Record<string, unknown> | undefined;
|
|
140
|
+
const fields = listechampsEl ? parseFields(toArray(listechampsEl.champ)) : [];
|
|
141
|
+
|
|
142
|
+
// ensure title field
|
|
143
|
+
if (!fields.some(f => f.field === 'title')) {
|
|
144
|
+
fields.push({ field: 'title', name: 'Titre', type: 'text', default: null, help: '' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
definition: {
|
|
149
|
+
id,
|
|
150
|
+
tpl: nodePath.join(dir, 'index.html'),
|
|
151
|
+
structure: nodePath.join(dir, 'structure.xhtml'),
|
|
152
|
+
zones,
|
|
153
|
+
fields,
|
|
154
|
+
name: s(symbole.INTITULE),
|
|
155
|
+
position: parseInt(s(symbole.TRI), 10) || 0,
|
|
156
|
+
},
|
|
157
|
+
error: null,
|
|
158
|
+
};
|
|
159
|
+
} catch {
|
|
160
|
+
return { definition: null, error: 'Impossible de lire ou parser le fichier' };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
3
|
+
import type { ThemeDesc, ThemeVariant, ParseResult } from '../types.ts';
|
|
4
|
+
|
|
5
|
+
const parser = new XMLParser({
|
|
6
|
+
ignoreAttributes: false,
|
|
7
|
+
attributeNamePrefix: '',
|
|
8
|
+
textNodeName: '#text',
|
|
9
|
+
isArray: (name: string) => name === 'variante',
|
|
10
|
+
transformTagName: (name: string) => name.toLowerCase(),
|
|
11
|
+
transformAttributeName: (name: string) => name.toUpperCase(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const s = (v: unknown): string => v != null ? String(v) : '';
|
|
15
|
+
|
|
16
|
+
const VALID_ETATS = ['production', 'incomplete', 'testing', 'deprecated'];
|
|
17
|
+
|
|
18
|
+
export function parseThemeXml(filePath: string): ParseResult<ThemeDesc> {
|
|
19
|
+
try {
|
|
20
|
+
const stat = statSync(filePath);
|
|
21
|
+
if (stat.size > 5 * 1024) return { definition: null, error: 'Fichier trop volumineux (> 5 Ko)' };
|
|
22
|
+
const content = readFileSync(filePath, 'latin1');
|
|
23
|
+
const doc = parser.parse(content);
|
|
24
|
+
const theme = doc?.theme;
|
|
25
|
+
if (!theme) return { definition: null, error: 'Balise <theme> manquante' };
|
|
26
|
+
|
|
27
|
+
// Validate etat
|
|
28
|
+
const etatRaw = s(theme.ETAT).toLowerCase();
|
|
29
|
+
let etat: string;
|
|
30
|
+
if (!etatRaw) {
|
|
31
|
+
etat = 'production';
|
|
32
|
+
} else if (!VALID_ETATS.includes(etatRaw)) {
|
|
33
|
+
return { definition: null, error: `Valeur ETAT invalide : '${etatRaw}'` };
|
|
34
|
+
} else {
|
|
35
|
+
etat = etatRaw;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Parse img (reject invalid chars)
|
|
39
|
+
const img = /[\\\/&#%]/.test(s(theme.IMG)) ? '' : s(theme.IMG);
|
|
40
|
+
|
|
41
|
+
// Parse auteur
|
|
42
|
+
const auteurRaw = theme.auteur;
|
|
43
|
+
const auteur = auteurRaw ? {
|
|
44
|
+
nom: s(auteurRaw.NOM),
|
|
45
|
+
contact: s(auteurRaw.CONTACT),
|
|
46
|
+
site: s(auteurRaw.SITE),
|
|
47
|
+
} : undefined;
|
|
48
|
+
|
|
49
|
+
// Parse apercu
|
|
50
|
+
const apercuRaw = s(theme.apercu ?? theme['#text'] ?? '');
|
|
51
|
+
const apercu = apercuRaw && !/^https?:\/\//.test(apercuRaw)
|
|
52
|
+
? 'http://' + apercuRaw
|
|
53
|
+
: apercuRaw || undefined;
|
|
54
|
+
|
|
55
|
+
// Parse variantes
|
|
56
|
+
const variantes: ThemeVariant[] = [];
|
|
57
|
+
const rawVariantes: unknown[] = Array.isArray(theme.variante)
|
|
58
|
+
? theme.variante
|
|
59
|
+
: (theme.variante ? [theme.variante] : []);
|
|
60
|
+
const seenIds = new Set<string>();
|
|
61
|
+
|
|
62
|
+
for (const v of rawVariantes as Record<string, unknown>[]) {
|
|
63
|
+
const css = s(v.CSS).trim();
|
|
64
|
+
const id = s(v.ID).trim();
|
|
65
|
+
if (!css || !id) continue;
|
|
66
|
+
if (id.length > 30 || seenIds.has(id)) continue;
|
|
67
|
+
if (/[\\]/.test(css)) continue;
|
|
68
|
+
const colorKeys = ['COLOR1', 'COLOR2', 'COLOR3', 'COLOR4'] as const;
|
|
69
|
+
if (!colorKeys.every(c => s(v[c]) && /^#[0-9a-f]+$/i.test(s(v[c])))) continue;
|
|
70
|
+
if (variantes.length >= 30) break;
|
|
71
|
+
seenIds.add(id);
|
|
72
|
+
variantes.push({
|
|
73
|
+
id,
|
|
74
|
+
css,
|
|
75
|
+
colors: {
|
|
76
|
+
color1: s(v.COLOR1),
|
|
77
|
+
color2: s(v.COLOR2),
|
|
78
|
+
color3: s(v.COLOR3),
|
|
79
|
+
color4: s(v.COLOR4),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
definition: {
|
|
86
|
+
name: s(theme.INTITULE),
|
|
87
|
+
version: s(theme.VERSION) || undefined,
|
|
88
|
+
date: s(theme.DATE) || undefined,
|
|
89
|
+
etat,
|
|
90
|
+
javascripts: s(theme.JAVASCRIPTS) === 'none' ? 'none' : 'kiubi',
|
|
91
|
+
style: s(theme.STYLE) || undefined,
|
|
92
|
+
img,
|
|
93
|
+
auteur,
|
|
94
|
+
apercu,
|
|
95
|
+
variantes,
|
|
96
|
+
},
|
|
97
|
+
error: null,
|
|
98
|
+
};
|
|
99
|
+
} catch {
|
|
100
|
+
return { definition: null, error: 'Impossible de lire ou parser le fichier' };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export type IssueLevel = 'error' | 'warning';
|
|
2
|
+
|
|
3
|
+
export interface Issue {
|
|
4
|
+
level: IssueLevel;
|
|
5
|
+
category: string;
|
|
6
|
+
msg: string;
|
|
7
|
+
file?: string;
|
|
8
|
+
line?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ThemeReport {
|
|
12
|
+
issues: Issue[];
|
|
13
|
+
hasErrors: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ZoneDef {
|
|
17
|
+
id: string;
|
|
18
|
+
isContent: boolean;
|
|
19
|
+
colmax: string;
|
|
20
|
+
name: string;
|
|
21
|
+
defaut?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OptionDef {
|
|
25
|
+
value: string;
|
|
26
|
+
label: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FieldDef {
|
|
30
|
+
field: string;
|
|
31
|
+
name: string;
|
|
32
|
+
type: string;
|
|
33
|
+
default?: string | null;
|
|
34
|
+
help?: string;
|
|
35
|
+
options?: OptionDef[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ModelDef {
|
|
39
|
+
id: string;
|
|
40
|
+
tpl: string;
|
|
41
|
+
structure: string;
|
|
42
|
+
zones: ZoneDef[];
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SymbolDef {
|
|
47
|
+
id: string;
|
|
48
|
+
tpl: string;
|
|
49
|
+
structure: string;
|
|
50
|
+
zones: ZoneDef[];
|
|
51
|
+
fields: FieldDef[];
|
|
52
|
+
name: string;
|
|
53
|
+
position: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ComponentDef {
|
|
57
|
+
id: string;
|
|
58
|
+
tpl: string;
|
|
59
|
+
name: string;
|
|
60
|
+
position: number;
|
|
61
|
+
fields: FieldDef[];
|
|
62
|
+
collection: FieldDef[];
|
|
63
|
+
hasCollection: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PostTypeDef {
|
|
67
|
+
id: string;
|
|
68
|
+
tpl?: string; // Only cms and product have a template
|
|
69
|
+
fields: FieldDef[];
|
|
70
|
+
name: string;
|
|
71
|
+
position: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ThemeVariant {
|
|
75
|
+
id: string;
|
|
76
|
+
css: string;
|
|
77
|
+
colors: { color1: string; color2: string; color3: string; color4: string };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ThemeDesc {
|
|
81
|
+
name: string;
|
|
82
|
+
version?: string;
|
|
83
|
+
date?: string;
|
|
84
|
+
etat: string;
|
|
85
|
+
javascripts?: string;
|
|
86
|
+
style?: string;
|
|
87
|
+
img?: string;
|
|
88
|
+
auteur?: { nom: string; contact?: string; site?: string };
|
|
89
|
+
apercu?: string;
|
|
90
|
+
variantes: ThemeVariant[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type ThemeType = 'model' | 'symbol' | 'component' | 'cms' | 'blog' | 'product';
|
|
94
|
+
|
|
95
|
+
export type TypeDefinition = ModelDef | SymbolDef | ComponentDef | PostTypeDef;
|
|
96
|
+
|
|
97
|
+
export type ParseResult<T> = { definition: T; error: null } | { definition: null; error: string };
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const logDir = join(dirname(fileURLToPath(import.meta.url)), "../storage/logs");
|
|
6
|
+
mkdirSync(logDir, { recursive: true });
|
|
7
|
+
const logFile = join(logDir, "server.log");
|
|
8
|
+
|
|
9
|
+
function truncate(value: unknown): unknown {
|
|
10
|
+
if (typeof value === "string") return value.length > 50 ? value.slice(0, 50) + "…" : value;
|
|
11
|
+
if (Array.isArray(value)) return value.map(truncate);
|
|
12
|
+
if (value !== null && typeof value === "object") {
|
|
13
|
+
return Object.fromEntries(Object.entries(value as object).map(([k, v]) => [k, truncate(v)]));
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function write(level: "info" | "error", message: string, data?: unknown): void {
|
|
19
|
+
const entry: Record<string, unknown> = { ts: new Date().toISOString(), level, message };
|
|
20
|
+
if (data !== undefined) entry["data"] = data;
|
|
21
|
+
appendFileSync(logFile, JSON.stringify(entry) + "\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const logger = {
|
|
25
|
+
info: (message: string, data?: unknown) => write("info", message, data),
|
|
26
|
+
error: (message: string, data?: unknown) => write("error", message, data),
|
|
27
|
+
tool: (name: string, args: unknown) => write("info", `tool:${name}`, truncate(args)),
|
|
28
|
+
};
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ZodRawShape } from "zod";
|
|
2
|
+
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
|
|
4
|
+
export type ToolDefinition<T extends ZodRawShape> = {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema: T;
|
|
8
|
+
handler: ToolCallback<T>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function defineTool<T extends ZodRawShape>(def: ToolDefinition<T>) {
|
|
12
|
+
return def;
|
|
13
|
+
}
|