story-theme-mcp 1.7.1 → 1.7.3
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/README.md +2 -2
- package/package.json +1 -1
- package/src/index.js +2 -1
- package/src/knowledge/guides.js +2 -2
- package/src/migrate/extract.js +16 -4
- package/src/migrate/source.js +77 -11
- package/src/migrate/v3.js +812 -125
- package/src/server.js +35 -16
- package/src/shops/app-tools.js +3 -3
- package/src/shops/client.js +2 -2
- package/src/shops/direct.js +1 -1
- package/src/shops/format.js +2 -2
- package/src/shops/tools.js +10 -8
- package/src/theme/source.js +6 -1
- package/src/validate/design.js +22 -2
- package/src/validate/values.js +7 -3
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Le MCP ne contient pas le thème : au démarrage, il demande à l'espace membre
|
|
|
63
63
|
| Livrer | `export_theme` (zip à importer + check-list), `import_theme` (auditer un thème téléchargé d'une boutique) |
|
|
64
64
|
| Passer en 4.x | `migrate_v3` : une boutique en Story Thème 3.7 ou avant, reconstruite en 4.x avec son contenu (textes, photos, avis, FAQ, produits, couleurs, polices, logo, menus), section par section — ce que la mise à jour de l'espace membre ne sait pas faire |
|
|
65
65
|
| Vos boutiques (Theme Access) | `list_shops` (et la marche à suivre pour brancher une boutique), `pull_from_shop` (relire le thème de la boutique), `push_to_shop` (écrire le projet dans un thème non publié, sans zip), `update_theme` (mettre à jour le Story Thème dans un thème neuf non publié) — la boutique se branche une fois depuis l'espace membre avec un mot de passe Theme Access, qui n'ouvre que ses thèmes |
|
|
66
|
-
| L'appli Story Thème (Story Apps, gratuite) | `app_status`, `app_reviews` (import CSV, modération, réponses), `app_ab_tests`, `app_offers` (la remise des offres du thème), `app_discount_codes`, `shops_overview`, `shop_graphql` — sur les boutiques où le marchand a installé l'appli (depuis son espace membre) |
|
|
66
|
+
| L'appli Story Thème (Story Theme Apps, gratuite) | `app_status`, `app_reviews` (import CSV, modération, réponses), `app_ab_tests`, `app_offers` (la remise des offres du thème), `app_discount_codes`, `shops_overview`, `shop_graphql` — sur les boutiques où le marchand a installé l'appli (depuis son espace membre) |
|
|
67
67
|
|
|
68
68
|
Prompts prêts à l'emploi : `creer-boutique`, `refaire-une-page`, `auditer-un-theme`. Les guides sont aussi exposés en ressources (`storytheme://guides/<sujet>`).
|
|
69
69
|
|
|
@@ -106,7 +106,7 @@ Prompts prêts à l'emploi : `creer-boutique`, `refaire-une-page`, `auditer-un-t
|
|
|
106
106
|
|
|
107
107
|
## Limites
|
|
108
108
|
|
|
109
|
-
Le MCP ne publie jamais un thème : il écrit dans un thème non publié, le marchand vérifie et publie lui-même. Sans l'appli Story Apps installée sur la boutique, il fabrique seulement un zip à importer. Il ne crée ni produits, ni collections, ni menus, ni images, ni réductions par lui-même : `shop_graphql` le permet quand une boutique est connectée, sinon la check-list les liste. Et il ne remplace pas une vérification visuelle dans l'éditeur Shopify.
|
|
109
|
+
Le MCP ne publie jamais un thème : il écrit dans un thème non publié, le marchand vérifie et publie lui-même. Sans l'appli Story Theme Apps installée sur la boutique, il fabrique seulement un zip à importer. Il ne crée ni produits, ni collections, ni menus, ni images, ni réductions par lui-même : `shop_graphql` le permet quand une boutique est connectée, sinon la check-list les liste. Et il ne remplace pas une vérification visuelle dans l'éditeur Shopify.
|
|
110
110
|
|
|
111
111
|
## Développement (New Story)
|
|
112
112
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "story-theme-mcp",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"description": "Serveur MCP du Story Thème : votre assistant IA connaît le thème Shopify Story par cœur et construit des boutiques propres et professionnelles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/index.js
CHANGED
|
@@ -57,6 +57,7 @@ try {
|
|
|
57
57
|
// l'interface de Claude.
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// L'appli Story Theme Apps n'est proposée que si l'espace membre la dit disponible pour ce compte.
|
|
61
|
+
const server = createServer({ ...config, source, appli: source.appli });
|
|
61
62
|
await server.connect(new StdioServerTransport());
|
|
62
63
|
journal(`prêt (projets : ${config.homeDir})`);
|
package/src/knowledge/guides.js
CHANGED
|
@@ -217,8 +217,8 @@ Formats conseillés :
|
|
|
217
217
|
limites: {
|
|
218
218
|
title: 'Ce que le MCP (V1) ne fait pas',
|
|
219
219
|
body: `- Il ne publie jamais un thème : il écrit dans un thème NON publié (push_to_shop) ou fabrique un zip à importer (export_theme) ; le marchand vérifie et publie lui-même.
|
|
220
|
-
-
|
|
221
|
-
- Il ne crée pas de lui-même produits, collections, menus, pages, réductions ni images :
|
|
220
|
+
- Il n'ouvre que les THÈMES d'une boutique branchée par Theme Access (list_shops le dit) : ni ses produits, ni ses commandes, ni ses clients.
|
|
221
|
+
- Il ne crée pas de lui-même produits, collections, menus, pages, réductions ni images : le connecteur Shopify officiel du marchand le permet ; sinon la check-list les liste.
|
|
222
222
|
- Il ne modifie jamais le code du thème (Liquid, CSS, JS) : seulement les modèles, les groupes de sections et les réglages, comme l'éditeur de thème.
|
|
223
223
|
- Il ne garantit pas le rendu visuel : prévisualisez toujours dans l'éditeur Shopify avant de publier.`,
|
|
224
224
|
},
|
package/src/migrate/extract.js
CHANGED
|
@@ -53,7 +53,9 @@ const ROLES = [
|
|
|
53
53
|
['title', /^(heading|title|image_heading|review_title|tab_label|headline|heading_text|question)$/],
|
|
54
54
|
['eyebrow', /^(subheading|image_subheading|subtitle|label|tagline|eyebrow|caption|announcment)$/],
|
|
55
55
|
['text', /^(description|text|row_content|content|text_footer|caption_text|form_text|text_above|message|body|paragraph|info|tooltip|answer|text_before|text_after|helper_text)$/],
|
|
56
|
-
|
|
56
|
+
// L'image qui remplace un pictogramme (3.x : image_custom, icon_N_custom_icon) reste un pictogramme.
|
|
57
|
+
['icon_image', /^(image_custom|custom_icon|icon_custom|icon_image)$/],
|
|
58
|
+
['image', /^(image|avatar|cover_image|background_image|image_before|image_after|logo|photo)$/],
|
|
57
59
|
['image_mobile', /^(image_mobile|background_image_mobile)$/],
|
|
58
60
|
['video', /^(video|video_url|video_link|video_mobile|video_link_mobile)$/],
|
|
59
61
|
];
|
|
@@ -92,9 +94,14 @@ const texteVide = (valeur) => !plainText(isHtml(valeur) ? valeur.html : valeur).
|
|
|
92
94
|
// (« Écrire un avis », « Voir plus », jours/heures du compte à rebours…), que la 4.0 traduit elle-même.
|
|
93
95
|
const INTERFACE = /^(section_id|custom_class|anchor|write_review_\w+|success_message|plus|less|button_show_all_text|placeholder|error_message|default_value|days|hours|mins|secs|free_label|reviews_based_on_text|reviews_suffix_text|reviews_count|cart_\w+|sale_badge|share_label|size_guide_trigger|product_trigger|trigger|final_message|track)$/;
|
|
94
96
|
|
|
97
|
+
// Le pictogramme choisi dans la bibliothèque du thème (« truck », « leaf ») : la 4.0 a la même.
|
|
98
|
+
const PICTO = /(^|_)icon(_type)?$/;
|
|
99
|
+
|
|
95
100
|
/** Un réglage lu : sa clé, son rôle (titre, texte, lien…), son genre et sa valeur. */
|
|
96
101
|
function lireReglage(cle, valeur, type) {
|
|
97
102
|
if (INTERFACE.test(cle)) return null;
|
|
103
|
+
// « none » compte aussi : la colonne n'avait pas de pictogramme, la 4.0 n'en devine pas un.
|
|
104
|
+
if (PICTO.test(cle) && typeof valeur === 'string' && /^[a-z_]+$/.test(valeur)) return { cle, genre: 'icon', valeur };
|
|
98
105
|
const genre = type ? SCHEMA_GENRE[type] ?? null : genreDeviné(cle, valeur);
|
|
99
106
|
if (!genre || valeur === '' || valeur == null) return null;
|
|
100
107
|
if (genre === 'html' || genre === 'text') {
|
|
@@ -119,11 +126,13 @@ function ranger(contenu, reglages) {
|
|
|
119
126
|
const { genre, valeur } = reglage;
|
|
120
127
|
|
|
121
128
|
if (genre === 'palette') contenu.palette ??= valeur;
|
|
129
|
+
else if (genre === 'icon') contenu.icon ??= valeur;
|
|
122
130
|
else if (genre === 'code') contenu.code.push(valeur);
|
|
123
131
|
else if (['product', 'collection', 'blog', 'page', 'menu', 'article'].includes(genre)) contenu[genre] ??= valeur;
|
|
124
132
|
else if (genre === 'products' || genre === 'collections') contenu[genre] = [...(contenu[genre] || []), ...(Array.isArray(valeur) ? valeur : [valeur])];
|
|
125
133
|
else if (genre === 'image') {
|
|
126
|
-
if (role === '
|
|
134
|
+
if (role === 'icon_image') contenu.icon_image ??= valeur;
|
|
135
|
+
else if (role === 'image_mobile') contenu.image_mobile ??= valeur;
|
|
127
136
|
else if (/after/.test(base)) contenu.image_after ??= valeur;
|
|
128
137
|
else if (/before/.test(base)) contenu.image_before ??= valeur;
|
|
129
138
|
else if (!contenu.image) contenu.image = valeur;
|
|
@@ -191,7 +200,7 @@ function element(reglages, genre = null) {
|
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
const utile = (item) =>
|
|
194
|
-
item.title !== undefined || item.text !== undefined || item.name !== undefined || item.image || item.video || item.product || item.collection || item.buttons.length || item.eyebrow !== undefined;
|
|
203
|
+
item.title !== undefined || item.text !== undefined || item.name !== undefined || item.image || item.icon_image || item.video || item.product || item.collection || item.buttons.length || item.eyebrow !== undefined;
|
|
195
204
|
|
|
196
205
|
/**
|
|
197
206
|
* Le contenu d'une section 3.x.
|
|
@@ -244,13 +253,16 @@ export function extraireSection(section, schema, { elements: typesElements = nul
|
|
|
244
253
|
}
|
|
245
254
|
if (lu.elements.length && !lu.simples.some((r) => ['text', 'html'].includes(r.genre) && /^(heading|title|name|description|text)$/.test(r.cle))) {
|
|
246
255
|
// Un bloc qui porte plusieurs éléments numérotés (icônes, questions, avis).
|
|
247
|
-
contenu.items.push(...lu.elements.map((el) => ({ ...el, kind: bloc.type })));
|
|
256
|
+
contenu.items.push(...lu.elements.map((el) => ({ ...el, kind: bloc.type, bloc: id })));
|
|
248
257
|
continue;
|
|
249
258
|
}
|
|
250
259
|
const item = element(lu.simples, bloc.type);
|
|
260
|
+
item.bloc = id;
|
|
251
261
|
if (lu.elements.length) item.sous = lu.elements;
|
|
252
262
|
if (bloc.blocks) item.enfants = bloc;
|
|
253
263
|
if (utile(item) || item.enfants) contenu.items.push(item);
|
|
264
|
+
// Un bloc qui ne porte que du code (Liquid, HTML) : le code de la section, dans l'ordre.
|
|
265
|
+
else if (item.code.length) contenu.code.push(...item.code);
|
|
254
266
|
}
|
|
255
267
|
return contenu;
|
|
256
268
|
}
|
package/src/migrate/source.js
CHANGED
|
@@ -15,26 +15,80 @@
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { unzipSync, strFromU8 } from 'fflate';
|
|
18
|
-
import {
|
|
18
|
+
import { stripJsonComment } from '../theme/loader.js';
|
|
19
19
|
import { fileKind } from '../validate/template.js';
|
|
20
20
|
import { devinerLangue, textesDesModeles } from '../validate/design.js';
|
|
21
21
|
|
|
22
22
|
const SCHEMA_RE = /\{%-?\s*schema\s*-?%\}([\s\S]*?)\{%-?\s*endschema\s*-?%\}/;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Le JSON tel que Shopify l'accepte : commentaires et virgule finale permis. Le thème 3.x d'origine
|
|
26
|
+
* en a (schéma de main-product, cart-drawer…), et un settings_schema.json retouché à la main aussi :
|
|
27
|
+
* un JSON.parse strict perdait la version du thème et le type de chaque réglage.
|
|
28
|
+
*/
|
|
29
|
+
export function parseJsonSouple(texte) {
|
|
30
|
+
const source = stripJsonComment(String(texte));
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(source);
|
|
33
|
+
} catch (erreur) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(sansVirguleFinale(sansCommentaires(source)));
|
|
36
|
+
} catch {
|
|
37
|
+
throw erreur;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parcourt le texte hors des chaînes : `quoi(c, i)` rend ce qu'il faut écrire et où reprendre.
|
|
43
|
+
function horsChaines(source, quoi) {
|
|
44
|
+
let sortie = '';
|
|
45
|
+
for (let i = 0; i < source.length; i++) {
|
|
46
|
+
if (source[i] === '"') {
|
|
47
|
+
let j = i + 1;
|
|
48
|
+
while (j < source.length && source[j] !== '"') j += source[j] === '\\' ? 2 : 1;
|
|
49
|
+
sortie += source.slice(i, j + 1);
|
|
50
|
+
i = j;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const [texte, suite] = quoi(source, i);
|
|
54
|
+
sortie += texte;
|
|
55
|
+
i = suite;
|
|
56
|
+
}
|
|
57
|
+
return sortie;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sansCommentaires = (source) =>
|
|
61
|
+
horsChaines(source, (t, i) => {
|
|
62
|
+
if (t[i] === '/' && t[i + 1] === '/') {
|
|
63
|
+
const fin = t.indexOf('\n', i);
|
|
64
|
+
return ['\n', fin === -1 ? t.length : fin];
|
|
65
|
+
}
|
|
66
|
+
if (t[i] === '/' && t[i + 1] === '*') {
|
|
67
|
+
const fin = t.indexOf('*/', i + 2);
|
|
68
|
+
return [' ', fin === -1 ? t.length : fin + 1];
|
|
69
|
+
}
|
|
70
|
+
return [t[i], i];
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const sansVirguleFinale = (source) => horsChaines(source, (t, i) => (t[i] === ',' && /^\s*[}\]]/.test(t.slice(i + 1, i + 200)) ? ['', i] : [t[i], i]));
|
|
74
|
+
|
|
24
75
|
function lireSchema(liquid) {
|
|
25
76
|
const match = SCHEMA_RE.exec(liquid);
|
|
26
77
|
if (!match) return null;
|
|
27
78
|
try {
|
|
28
|
-
return
|
|
79
|
+
return parseJsonSouple(match[1]);
|
|
29
80
|
} catch {
|
|
30
81
|
return null;
|
|
31
82
|
}
|
|
32
83
|
}
|
|
33
84
|
|
|
85
|
+
// La langue par défaut du thème 3.x : locales/da.default.json → « da ». Seul le nom compte.
|
|
86
|
+
const LOCALE_DEFAUT = /^locales\/([a-z]{2}(?:-[A-Za-z]{2,4})?)\.default\.json$/;
|
|
87
|
+
|
|
34
88
|
/** Les fichiers utiles d'un thème : chemin relatif → texte. */
|
|
35
89
|
function lireFichiers(source) {
|
|
36
90
|
const fichiers = new Map();
|
|
37
|
-
const garder = (rel) => fileKind(rel) || rel === 'config/settings_schema.json' || /^(sections|blocks)\/[^/]+\.liquid$/.test(rel);
|
|
91
|
+
const garder = (rel) => fileKind(rel) || rel === 'config/settings_schema.json' || /^(sections|blocks)\/[^/]+\.liquid$/.test(rel) || LOCALE_DEFAUT.test(rel);
|
|
38
92
|
|
|
39
93
|
if (fs.statSync(source).isDirectory()) {
|
|
40
94
|
const parcourir = (dossier) => {
|
|
@@ -46,7 +100,7 @@ function lireFichiers(source) {
|
|
|
46
100
|
else if (garder(rel)) fichiers.set(rel, fs.readFileSync(path.join(source, rel), 'utf8'));
|
|
47
101
|
}
|
|
48
102
|
};
|
|
49
|
-
for (const dossier of ['templates', 'sections', 'blocks', 'config']) parcourir(dossier);
|
|
103
|
+
for (const dossier of ['templates', 'sections', 'blocks', 'config', 'locales']) parcourir(dossier);
|
|
50
104
|
return fichiers;
|
|
51
105
|
}
|
|
52
106
|
|
|
@@ -81,7 +135,14 @@ export function lireThemeV3DepuisFichiers(fichiers, origine = 'boutique') {
|
|
|
81
135
|
const schemas = { sections: new Map(), blocks: new Map() };
|
|
82
136
|
let version = null;
|
|
83
137
|
let nom = null;
|
|
138
|
+
let langueParDefaut = null;
|
|
139
|
+
let reglagesSchema = [];
|
|
84
140
|
for (const [rel, texte] of fichiers) {
|
|
141
|
+
const locale = LOCALE_DEFAUT.exec(rel);
|
|
142
|
+
if (locale) {
|
|
143
|
+
langueParDefaut = locale[1];
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
85
146
|
const liquid = /^(sections|blocks)\/([^/]+)\.liquid$/.exec(rel);
|
|
86
147
|
if (liquid) {
|
|
87
148
|
const schema = lireSchema(texte);
|
|
@@ -90,28 +151,33 @@ export function lireThemeV3DepuisFichiers(fichiers, origine = 'boutique') {
|
|
|
90
151
|
}
|
|
91
152
|
if (rel === 'config/settings_schema.json') {
|
|
92
153
|
try {
|
|
93
|
-
const
|
|
154
|
+
const groupes = parseJsonSouple(texte);
|
|
155
|
+
const info = groupes.find((groupe) => groupe.name === 'theme_info');
|
|
94
156
|
version = info?.theme_version || null;
|
|
95
157
|
nom = info?.theme_name || null;
|
|
158
|
+
reglagesSchema = groupes.flatMap((groupe) => groupe.settings || []).filter((reglage) => reglage?.id);
|
|
96
159
|
} catch {
|
|
97
160
|
/* version inconnue */
|
|
98
161
|
}
|
|
99
162
|
continue;
|
|
100
163
|
}
|
|
101
164
|
try {
|
|
102
|
-
fichiersJson.set(rel,
|
|
165
|
+
fichiersJson.set(rel, parseJsonSouple(texte));
|
|
103
166
|
} catch (error) {
|
|
104
167
|
erreurs.push(`${rel} : JSON illisible (${error.message})`);
|
|
105
168
|
}
|
|
106
169
|
}
|
|
107
170
|
|
|
108
|
-
// La langue de la boutique se lit dans SES textes : le fichier de traductions par défaut du
|
|
109
|
-
// thème (en.default.json) est l'anglais pour toutes les boutiques Story Thème.
|
|
110
|
-
const langue = devinerLangue(textesDesModeles([...fichiersJson].filter(([rel]) => rel.startsWith('templates/') || rel.startsWith('sections/')).map(([, data]) => data)));
|
|
111
|
-
|
|
112
171
|
const reglages = fichiersJson.get('config/settings_data.json');
|
|
113
172
|
const courant = typeof reglages?.current === 'string' ? reglages.presets?.[reglages.current] : reglages?.current;
|
|
114
|
-
|
|
173
|
+
|
|
174
|
+
// La langue de la boutique se lit dans SES textes (modèles, groupes, et sections fixes comme le
|
|
175
|
+
// panier). Le fichier de traductions par défaut du thème (en.default.json) est l'anglais pour
|
|
176
|
+
// toutes les boutiques Story Thème : il ne départage que s'il a été changé (da.default.json…).
|
|
177
|
+
const modeles = [...fichiersJson].filter(([rel]) => rel.startsWith('templates/') || rel.startsWith('sections/')).map(([, data]) => data);
|
|
178
|
+
const textes = textesDesModeles([...modeles, { sections: courant?.sections || {} }]);
|
|
179
|
+
const langue = devinerLangue(textes) || (langueParDefaut && langueParDefaut !== 'en' ? langueParDefaut : null);
|
|
180
|
+
return { origine, version, nom, langue, langueParDefaut, fichiers: fichiersJson, schemas, reglages: courant || {}, reglagesSchema, erreurs };
|
|
115
181
|
}
|
|
116
182
|
|
|
117
183
|
/** La version majeure d'un numéro « 3.7.0 », ou null. */
|