webcake-storefront-mcp 1.2.0 → 1.3.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/api.js +6 -0
- package/dist/builder/grid.js +68 -0
- package/dist/builder/guide.js +38 -15
- package/dist/builder/page.js +68 -8
- package/dist/changelog.json +7 -7
- package/dist/server.js +1 -1
- package/dist/smoke.js +12 -3
- package/dist/tools/builder.js +8 -2
- package/dist/tools/context.js +53 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -70,6 +70,12 @@ export class WebcakeCmsApi {
|
|
|
70
70
|
listMySites(query) {
|
|
71
71
|
return this.request("GET", `/api/v1/dashboard/site/all`, { query });
|
|
72
72
|
}
|
|
73
|
+
/** Create a brand-new personal site. The backend seeds sample categories/products/blog
|
|
74
|
+
* but NO pages. Returns { data: { site: { id, site_slug:{slug}, ... } } }.
|
|
75
|
+
* Fails with 403 when the account's site quota is reached (free plan: 4 sites). */
|
|
76
|
+
createSite(params) {
|
|
77
|
+
return this.request("POST", `/api/v1/dashboard/site/create`, { body: params, timeout: 60000 });
|
|
78
|
+
}
|
|
73
79
|
getSiteInfo() {
|
|
74
80
|
return this.request("GET", `/api/v1/site/${this.siteId}/`);
|
|
75
81
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Breakpoint + grid model, ported from builderx_spa (composable/grid.js + common/index.js).
|
|
2
|
+
//
|
|
3
|
+
// CRITICAL: BuilderX persists each node's style/layout under per-breakpoint keys
|
|
4
|
+
// `bp1`/`bp2`/`bp3`/`bp4` — each `{ style, config }` — NOT under `runtime`. The
|
|
5
|
+
// `runtime` key the factory emits is only a staging area inside the Vue editor; the
|
|
6
|
+
// storefront renderer reads `node[breakpointActive]` (see getStyle/getConfig in
|
|
7
|
+
// builderx_spa/src/composable/get.js) and does NOT fall back to `runtime`. A node that
|
|
8
|
+
// only has `runtime` renders with no styles/grid placement (a broken-looking page).
|
|
9
|
+
// page.ts:finalizeForRender() converts runtime -> bp1..bp4 before a page is saved.
|
|
10
|
+
/** Site default breakpoints, largest first. [minWidth, maxWidth]. bp1 is the base. */
|
|
11
|
+
export const BREAKPOINTS = {
|
|
12
|
+
bp1: [1320, 1e9], // desktop (base / largest, default active)
|
|
13
|
+
bp2: [993, 1319], // laptop
|
|
14
|
+
bp3: [641, 992], // tablet
|
|
15
|
+
bp4: [320, 640], // mobile
|
|
16
|
+
};
|
|
17
|
+
export const BREAKPOINT_KEYS = Object.keys(BREAKPOINTS); // ['bp1','bp2','bp3','bp4']
|
|
18
|
+
export const BASE_BP = "bp1";
|
|
19
|
+
// Position / layout keys that are breakpoint-specific. The builder does NOT copy these
|
|
20
|
+
// when syncing one breakpoint onto another (placement differs per device). We keep them
|
|
21
|
+
// in whichever breakpoint they were authored, and copy only the non-async keys across.
|
|
22
|
+
export const STYLE_ASYNC = ["top", "left", "right", "bottom", "width", "height", "zIndex", "position", "fontSize"];
|
|
23
|
+
export const CONFIG_ASYNC = [
|
|
24
|
+
"constraintX", "constraintY", "leftUnit", "rightUnit", "relLeft", "relRight", "absRight",
|
|
25
|
+
"relWidth", "widthUnit", "topUnit", "bottomUnit", "relTop", "relBottom", "absBottom", "absLeftCenterX",
|
|
26
|
+
"relLeftCenterX", "leftCenterXUnit", "absRightCenterX", "relRightCenterX", "rightCenterXUnit", "topCenterYUnit",
|
|
27
|
+
"absTopCenterY", "relTopCenterY", "bottomCenterYUnit", "absBottomCenterY", "relBottomCenterY", "heightUnit",
|
|
28
|
+
"relHeight", "vhHeight", "columnStart", "columnEnd", "rowStart", "rowEnd", "isHidden", "columns", "rows", "grid",
|
|
29
|
+
"is_use_width_outer_parent", "area", "lockCellGrid", "slideWidth", "slideWidthUnit", "relSlideWidth",
|
|
30
|
+
"posts_per_row", "is_pin_video", "sizeThumbnail", "layout", "scrollDirection",
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Section "centered content" grid for a given breakpoint width — verbatim port of
|
|
34
|
+
* builderx_spa composable/grid.js:genGridByBp. A section is a 3-column grid: a flexible
|
|
35
|
+
* margin on each side and the page content in the centre column (max 1300px on desktop).
|
|
36
|
+
* `rows` here is a single placeholder row; callers override `rows`/`grid` for the real
|
|
37
|
+
* number of stacked children.
|
|
38
|
+
*/
|
|
39
|
+
export function genGridByBp(bp) {
|
|
40
|
+
const rows = [{ unit: "min/max", min: { unit: "px", absValue: 600 }, max: { unit: "max-c" } }];
|
|
41
|
+
if (bp >= 1320) {
|
|
42
|
+
return {
|
|
43
|
+
grid: "3x1",
|
|
44
|
+
columns: [{ unit: "fr", value: 1 }, { unit: "px", absValue: 1300, value: 1 }, { unit: "fr", value: 1 }],
|
|
45
|
+
rows,
|
|
46
|
+
loaded: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
else if (bp >= 993) {
|
|
50
|
+
return {
|
|
51
|
+
grid: "3x1",
|
|
52
|
+
columns: [{ unit: "px", absValue: 10 }, { unit: "fr", value: 1 }, { unit: "px", absValue: 10 }],
|
|
53
|
+
rows,
|
|
54
|
+
loaded: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
return {
|
|
59
|
+
grid: "3x1",
|
|
60
|
+
columns: [{ unit: "px", absValue: 5 }, { unit: "fr", value: 1 }, { unit: "px", absValue: 5 }],
|
|
61
|
+
rows,
|
|
62
|
+
loaded: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** The centre (content) column index in a section's 3-column grid (1-based grid lines). */
|
|
67
|
+
export const SECTION_CONTENT_COL_START = 2;
|
|
68
|
+
export const SECTION_CONTENT_COL_END = 3;
|
package/dist/builder/guide.js
CHANGED
|
@@ -27,23 +27,40 @@ fills the correct defaults, then edit specials/style.
|
|
|
27
27
|
## Layout = CSS grid (NOT absolute top/left)
|
|
28
28
|
This is the key difference from landing-page builders. A section/container positions its
|
|
29
29
|
children with a grid:
|
|
30
|
-
-
|
|
31
|
-
\`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
- A SECTION uses a centred 3-column grid: \`grid: "3xN"\`, columns
|
|
31
|
+
\`[{unit:'fr',value:1}, {unit:'px',absValue:1300,value:1}, {unit:'fr',value:1}]\` —
|
|
32
|
+
flexible margin · 1300px content · flexible margin. Children sit in the CENTRE column
|
|
33
|
+
(\`columnStart:2, columnEnd:3\`). \`rows\` = one \`{unit:'min/max', min:{unit:'px',absValue:H}, max:{unit:'max-c'}}\` per child.
|
|
34
|
+
- A nested CONTAINER uses a simple \`grid: "1xN"\` with \`columns:[{unit:'fr',value:1}]\`;
|
|
35
|
+
its children sit in \`columnStart:1, columnEnd:2\`.
|
|
36
|
+
- each child config also has \`rowStart/rowEnd\` (1-based grid lines),
|
|
37
|
+
\`constraintX\` (['left'|'right'|'centerLeft']), \`constraintY\` (['top'|'bottom'|'centerTop']).
|
|
38
|
+
new_section does ALL of this for you: pass children and they are stacked one row each in
|
|
39
|
+
the centre column. To build multi-column layouts, nest a container child with its own grid.
|
|
40
|
+
|
|
41
|
+
## Where layout/style live: per-breakpoint keys (NOT \`runtime\`)
|
|
42
|
+
new_section / new_element emit a temporary \`runtime: { style, config }\`. That is a
|
|
43
|
+
STAGING shape — the storefront does NOT read \`runtime\`. On save, build_page / add_section
|
|
44
|
+
automatically expand \`runtime\` into the four breakpoint keys the renderer actually reads:
|
|
45
|
+
\`node.bp1\`, \`node.bp2\`, \`node.bp3\`, \`node.bp4\` (each \`{ style, config }\`). You normally
|
|
46
|
+
never write these by hand for new pages. When EDITING an existing page, elements are
|
|
47
|
+
already in this shape — see the \`responsive\` field on get_page_element/update_page_element.
|
|
36
48
|
|
|
37
49
|
## Styling
|
|
38
50
|
- \`runtime.style\` holds CSS-ish props: width/height (numbers = px), color, background,
|
|
39
51
|
fontSize ("16px"), fontWeight, textAlign, border*, boxShadow, etc.
|
|
40
52
|
- \`runtime.config.heightUnit\`: "auto" lets content set height (default for text/image).
|
|
41
|
-
- Colours
|
|
53
|
+
- Colours: prefer the site THEME variables \`var(--color_00)\`, \`var(--color_01)\`, …
|
|
54
|
+
(the published site themes them); plain hex or rgba() also work.
|
|
42
55
|
|
|
43
56
|
## Responsive breakpoints
|
|
44
|
-
|
|
45
|
-
\`
|
|
46
|
-
|
|
57
|
+
The four breakpoints (largest → smallest), keyed bp1..bp4, are:
|
|
58
|
+
- \`bp1\` ≥1320px (desktop, the base) · \`bp2\` 993–1319 (laptop) · \`bp3\` 641–992 (tablet) · \`bp4\` 320–640 (mobile).
|
|
59
|
+
For NEW pages you author once in \`runtime\` (desktop) and build_page copies it to all four
|
|
60
|
+
breakpoints automatically — the page renders identically across devices. To make a node
|
|
61
|
+
look DIFFERENT on a smaller screen, set that breakpoint's key explicitly, e.g.
|
|
62
|
+
\`node.bp4 = { style: { fontSize: "20px" }, config: {...} }\`. (There is no \`tablet\`/\`laptop\`
|
|
63
|
+
key — only bp1..bp4.)
|
|
47
64
|
|
|
48
65
|
## Content & data
|
|
49
66
|
- Text: \`specials.text\` (HTML allowed), \`specials.tag\` ("h1".."p").
|
|
@@ -51,8 +68,14 @@ Breakpoint widths: large_desktop 1920, desktop 1280, laptop 992, tablet 640.
|
|
|
51
68
|
- Form: wrap inputs in a \`form\`; set \`form.specials.type\`
|
|
52
69
|
(form_order | form_login | form_signup | form_discount | order_tracking). Each input
|
|
53
70
|
needs \`specials.field_name\`.
|
|
54
|
-
- Dataset elements (text-dataset, image-dataset,
|
|
55
|
-
|
|
71
|
+
- Dataset elements (text-dataset, image-dataset, rectangle-dataset...) pull live data via
|
|
72
|
+
a \`bindings\` array. Each binding is \`{ id:"BINDING"+random, name:<source>, target:"<source>::<field>" }\`.
|
|
73
|
+
Real target field names (use these EXACTLY — there is no \`product::price\`):
|
|
74
|
+
- product: \`product::product_image\`, \`product::product_name\`, \`product::product_price\`
|
|
75
|
+
- cart_item: \`cart_item::cart_item_image\`, \`cart_item::cart_item_name\`, \`cart_item::cart_item_price\`, \`cart_item::cart_item_total_price\`, \`cart_item::cart_item_prod_attr\`
|
|
76
|
+
- order_item: \`order_item::product_image\`, \`order_item::product_name\`, \`order_item::product_quantity\`, \`order_item::items_sum_up_price\`, \`order_item::product_attrs\`
|
|
77
|
+
- customer_address: \`customer_address::full_name\`, \`customer_address::phone_number\`, \`customer_address::address\`, \`customer_address::pdc\`
|
|
78
|
+
A target only resolves on a page of the matching \`type\` (see below).
|
|
56
79
|
|
|
57
80
|
## Page types & data sources (IMPORTANT for special pages)
|
|
58
81
|
A page's \`type\` decides which live data it can bind to. A SPECIAL page only works if the
|
|
@@ -61,15 +84,15 @@ but every product/customer/blog binding resolves to NULL (an empty, broken-looki
|
|
|
61
84
|
\`build_page\` enables the right flag for you when you pass \`type\`:
|
|
62
85
|
- \`main\` — homepage / normal content. No flag needed.
|
|
63
86
|
- \`store\` — product detail, category, cart, checkout, thank-you. Needs \`use_store\`.
|
|
64
|
-
Bindings: \`product
|
|
87
|
+
Bindings: \`product::product_*\`, \`cart_item::cart_item_*\`.
|
|
65
88
|
- \`member\` — login, register, profile, order history. Needs \`use_member\`.
|
|
66
89
|
Bindings: \`customer_address::*\`, \`order_item::*\`.
|
|
67
90
|
- \`blog\` — blog list, article/post. Needs \`use_blog\`.
|
|
68
91
|
- \`error\` / \`maintain\` — 404 / maintenance. Need \`use_error\` / \`use_maintain\`.
|
|
69
92
|
- \`custom\` — a free page with no special data. No flag needed.
|
|
70
93
|
Rule of thumb: if the page shows products, a cart, customer/order data, or blog posts,
|
|
71
|
-
set \`type\` accordingly so the binding source is turned on. A binding
|
|
72
|
-
\`product::
|
|
94
|
+
set \`type\` accordingly so the binding source is turned on. A binding target like
|
|
95
|
+
\`product::product_price\` REQUIRES its page to be the matching type.
|
|
73
96
|
|
|
74
97
|
## Workflow (do this every time)
|
|
75
98
|
1. Intake: confirm goal, brand, colours, sections wanted (ask 3-5 questions if unclear).
|
package/dist/builder/page.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// produce that structure the same way the builder does, so generated pages render.
|
|
7
7
|
import { buildElement, isKnownType, ELEMENT_TYPES } from "./catalog.js";
|
|
8
8
|
import { randomString } from "./factory.js";
|
|
9
|
+
import { BREAKPOINTS, genGridByBp, SECTION_CONTENT_COL_START, SECTION_CONTENT_COL_END, } from "./grid.js";
|
|
10
|
+
const clone = (o) => structuredClone(o);
|
|
9
11
|
/** Walk every node in a source tree (depth-first). Return false from fn to stop. */
|
|
10
12
|
export function walk(source, fn) {
|
|
11
13
|
const sections = source && Array.isArray(source.sections) ? source.sections : [];
|
|
@@ -42,10 +44,15 @@ export function reassignIds(node) {
|
|
|
42
44
|
return node;
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
45
|
-
* Lay children out vertically inside a section/container
|
|
46
|
-
* the same shape the builder emits.
|
|
47
|
+
* Lay children out vertically inside a section/container — one grid row per child,
|
|
48
|
+
* top-to-bottom — the same shape the builder emits. Values are written to `runtime`;
|
|
49
|
+
* finalizeForRender() later expands `runtime` into the per-breakpoint keys the
|
|
50
|
+
* storefront actually reads (bp1..bp4).
|
|
47
51
|
*/
|
|
48
|
-
export function stackChildren(container, children) {
|
|
52
|
+
export function stackChildren(container, children, opts = {}) {
|
|
53
|
+
const gridCols = opts.gridCols || 1;
|
|
54
|
+
const colStart = opts.contentColStart || 1;
|
|
55
|
+
const colEnd = opts.contentColEnd || 2;
|
|
49
56
|
const rows = children.map((child) => {
|
|
50
57
|
const h = (child.runtime && child.runtime.style && child.runtime.style.height) || 50;
|
|
51
58
|
return { unit: "min/max", min: { unit: "px", absValue: h }, max: { unit: "max-c" } };
|
|
@@ -53,8 +60,8 @@ export function stackChildren(container, children) {
|
|
|
53
60
|
container.runtime = container.runtime || {};
|
|
54
61
|
container.runtime.config = {
|
|
55
62
|
...(container.runtime.config || {}),
|
|
56
|
-
grid:
|
|
57
|
-
columns: [{ unit: "fr", value: 1 }],
|
|
63
|
+
grid: `${gridCols}x${children.length || 1}`,
|
|
64
|
+
columns: opts.columns || [{ unit: "fr", value: 1 }],
|
|
58
65
|
rows: rows.length ? rows : [{ unit: "min/max", min: { unit: "px", absValue: 50 }, max: { unit: "max-c" } }],
|
|
59
66
|
heightUnit: "auto",
|
|
60
67
|
};
|
|
@@ -62,8 +69,8 @@ export function stackChildren(container, children) {
|
|
|
62
69
|
child.runtime = child.runtime || {};
|
|
63
70
|
child.runtime.config = {
|
|
64
71
|
...(child.runtime.config || {}),
|
|
65
|
-
columnStart:
|
|
66
|
-
columnEnd:
|
|
72
|
+
columnStart: colStart,
|
|
73
|
+
columnEnd: colEnd,
|
|
67
74
|
rowStart: i + 1,
|
|
68
75
|
rowEnd: i + 2,
|
|
69
76
|
constraintX: (child.runtime.config && child.runtime.config.constraintX) || ["centerLeft"],
|
|
@@ -77,11 +84,19 @@ export function stackChildren(container, children) {
|
|
|
77
84
|
/**
|
|
78
85
|
* Build a ready-to-place section from a list of child specs.
|
|
79
86
|
* Each spec: { type, opts?, children? } where children is a nested array of specs.
|
|
87
|
+
* A section uses the builder's centred 3-column grid (margin · content · margin); the
|
|
88
|
+
* children live in the centre content column. finalizeForRender() sets the correct
|
|
89
|
+
* per-breakpoint column widths via genGridByBp.
|
|
80
90
|
*/
|
|
81
91
|
export function buildSection(childSpecs = [], sectionOpts = {}) {
|
|
82
92
|
const section = buildElement("section", sectionOpts);
|
|
83
93
|
const children = childSpecs.map((spec) => buildFromSpec(spec));
|
|
84
|
-
stackChildren(section, children
|
|
94
|
+
stackChildren(section, children, {
|
|
95
|
+
gridCols: 3,
|
|
96
|
+
columns: genGridByBp(BREAKPOINTS.bp1[0]).columns,
|
|
97
|
+
contentColStart: SECTION_CONTENT_COL_START,
|
|
98
|
+
contentColEnd: SECTION_CONTENT_COL_END,
|
|
99
|
+
});
|
|
85
100
|
return section;
|
|
86
101
|
}
|
|
87
102
|
function buildFromSpec(spec) {
|
|
@@ -146,4 +161,49 @@ export function validatePage(source) {
|
|
|
146
161
|
stats: { sections: source.sections.length, total_elements: total, element_types: typeCounts },
|
|
147
162
|
};
|
|
148
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Expand one node's `runtime.{style,config}` into the per-breakpoint keys the storefront
|
|
166
|
+
* renderer reads (bp1..bp4). Mirrors builderx_spa's syncBreakpoint: the authored
|
|
167
|
+
* (desktop) values are copied onto every breakpoint. Sections additionally get their
|
|
168
|
+
* centred 3-column grid recomputed per breakpoint via genGridByBp (the side-margin
|
|
169
|
+
* widths shrink on smaller screens). Nodes already in breakpoint shape are left as-is,
|
|
170
|
+
* so this is safe to run over a mixed source (e.g. add_section onto an existing page).
|
|
171
|
+
*/
|
|
172
|
+
function expandNodeToBreakpoints(node) {
|
|
173
|
+
const rt = node && node.runtime;
|
|
174
|
+
if (rt && (rt.style || rt.config)) {
|
|
175
|
+
const baseStyle = rt.style || {};
|
|
176
|
+
const baseConfig = { ...(rt.config || {}), loaded: true };
|
|
177
|
+
const isSection = node.type === "section";
|
|
178
|
+
for (const [bp, [minW]] of Object.entries(BREAKPOINTS)) {
|
|
179
|
+
const style = clone(baseStyle);
|
|
180
|
+
const config = clone(baseConfig);
|
|
181
|
+
if (isSection) {
|
|
182
|
+
const g = genGridByBp(minW);
|
|
183
|
+
const sectionRows = baseConfig.rows && baseConfig.rows.length ? clone(baseConfig.rows) : clone(g.rows);
|
|
184
|
+
config.columns = clone(g.columns);
|
|
185
|
+
config.rows = sectionRows;
|
|
186
|
+
config.grid = `3x${sectionRows.length}`;
|
|
187
|
+
config.heightUnit = config.heightUnit || "auto";
|
|
188
|
+
}
|
|
189
|
+
node[bp] = { style, config };
|
|
190
|
+
}
|
|
191
|
+
delete node.runtime;
|
|
192
|
+
}
|
|
193
|
+
for (const child of node.children || [])
|
|
194
|
+
expandNodeToBreakpoints(child);
|
|
195
|
+
return node;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Convert a freshly-built page source (whose nodes carry `runtime`) into the shape the
|
|
199
|
+
* storefront actually renders: every node gets bp1..bp4 `{style,config}` and `runtime`
|
|
200
|
+
* is removed. MUST be called before saving a page built with new_section/new_element —
|
|
201
|
+
* otherwise the page renders with no styling or grid placement.
|
|
202
|
+
*/
|
|
203
|
+
export function finalizeForRender(source) {
|
|
204
|
+
const sections = source && Array.isArray(source.sections) ? source.sections : [];
|
|
205
|
+
for (const s of sections)
|
|
206
|
+
expandNodeToBreakpoints(s);
|
|
207
|
+
return source;
|
|
208
|
+
}
|
|
149
209
|
export { ELEMENT_TYPES };
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.3.0",
|
|
4
|
+
"d": "23/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "New create_site tool creates a brand-new storefront site for the current account (seeded with sample products, categories, and a blog), optionally…",
|
|
7
|
+
"vi": "Tool mới create_site tạo một site storefront hoàn toàn mới cho tài khoản hiện tại (kèm sản phẩm, danh mục và blog mẫu), tự động chuyển sang site vừa…"
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"v": "1.2.0",
|
|
4
11
|
"d": "23/06/2026",
|
|
@@ -33,12 +40,5 @@
|
|
|
33
40
|
"type": "Added",
|
|
34
41
|
"en": "The serve command's OAuth token store now optionally uses Postgres (via DATABASE_URL) for durable persistence across restarts and shared state…",
|
|
35
42
|
"vi": "Kho lưu trữ token OAuth của lệnh serve nay hỗ trợ tùy chọn sử dụng Postgres (qua DATABASE_URL) để lưu token bền vững qua các lần khởi động lại và…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.1.0",
|
|
39
|
-
"d": "23/06/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "The serve (remote Streamable-HTTP) mode now embeds a full OAuth 2.1 Authorization Server at /authorize, /token, /revoke, /register, and…",
|
|
42
|
-
"vi": "Chế độ serve (remote Streamable-HTTP) nay tích hợp sẵn một Authorization Server OAuth 2.1 đầy đủ tại các endpoint /authorize, /token, /revoke,…"
|
|
43
43
|
}
|
|
44
44
|
]
|
package/dist/server.js
CHANGED
|
@@ -23,7 +23,7 @@ IMPORTANT: When the user asks ANY question about their website, store, products,
|
|
|
23
23
|
You can also BUILD pages: use get_build_guide, list_elements, get_element to learn the BuilderX component model, new_section/new_element to compose, validate_page to check, then build_page (dry_run first) to create. Publishing is site-level via publish_site.
|
|
24
24
|
|
|
25
25
|
Workflow:
|
|
26
|
-
1. On first interaction, call get_current_context. The site is NOT set from env — if no site is selected yet, call list_my_sites and ask the user which site to work on, then switch_site (the choice is saved and reused next session).
|
|
26
|
+
1. On first interaction, call get_current_context. The site is NOT set from env — if no site is selected yet, call list_my_sites and ask the user which site to work on, then switch_site (the choice is saved and reused next session). To start from scratch, create_site makes a new site and switches to it; then build a homepage with build_page (type:'main', is_homepage:true).
|
|
27
27
|
2. Before answering a site-specific question, query the relevant tool.
|
|
28
28
|
3. When building a page, read get_build_guide first and validate before saving.
|
|
29
29
|
4. Always reply in the user's language; keep Vietnamese with full diacritics.`;
|
package/dist/smoke.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* building blocks so a release can verify them without a client. Run: npm run smoke
|
|
4
4
|
*/
|
|
5
5
|
import { listElements, getElement, buildElement, ELEMENT_TYPES, isKnownType } from "./builder/catalog.js";
|
|
6
|
-
import { newPageSkeleton, buildSection, validatePage, reassignIds, walk, } from "./builder/page.js";
|
|
6
|
+
import { newPageSkeleton, buildSection, validatePage, finalizeForRender, reassignIds, walk, } from "./builder/page.js";
|
|
7
7
|
let failures = 0;
|
|
8
8
|
const check = (name, cond, extra) => {
|
|
9
9
|
if (cond) {
|
|
@@ -49,13 +49,22 @@ console.log("== page: grid composition + validation ==");
|
|
|
49
49
|
{ type: "text", opts: { text: "Welcome" } },
|
|
50
50
|
{ type: "button", opts: { text: "Buy" } },
|
|
51
51
|
]);
|
|
52
|
-
|
|
53
|
-
check("
|
|
52
|
+
// A section uses the builder's centred 3-column grid; children sit in the centre column.
|
|
53
|
+
check("section grid is 3xN", hero.runtime.config.grid === "3x2", hero.runtime.config.grid);
|
|
54
|
+
check("children placed in centre column", hero.children.every((c) => c.runtime.config.columnStart === 2));
|
|
54
55
|
const src = newPageSkeleton();
|
|
55
56
|
src.sections.push(hero);
|
|
56
57
|
const v = validatePage(src);
|
|
57
58
|
check("built page validates", v.valid === true, v.errors);
|
|
58
59
|
check("stats count elements", v.stats.total_elements === 3, v.stats);
|
|
60
|
+
// finalizeForRender must convert runtime -> bp1..bp4 (the shape the storefront renders).
|
|
61
|
+
finalizeForRender(src);
|
|
62
|
+
const sec0 = src.sections[0];
|
|
63
|
+
check("finalize removes runtime", !("runtime" in sec0), Object.keys(sec0));
|
|
64
|
+
check("finalize adds bp1..bp4", ["bp1", "bp2", "bp3", "bp4"].every((bp) => sec0[bp]?.config), Object.keys(sec0));
|
|
65
|
+
check("section bp4 is mobile grid", sec0.bp4.config.grid === "3x2" && sec0.bp4.config.columns[0].absValue === 5, sec0.bp4.config.columns?.[0]);
|
|
66
|
+
check("child bp1 keeps centre column", sec0.children[0].bp1.config.columnStart === 2, sec0.children[0].bp1?.config);
|
|
67
|
+
check("finalize is idempotent", (finalizeForRender(src), !("runtime" in sec0)));
|
|
59
68
|
// duplicate ids must fail validation
|
|
60
69
|
const dup = newPageSkeleton();
|
|
61
70
|
const a = buildElement("section");
|
package/dist/tools/builder.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { BUILD_GUIDE } from "../builder/guide.js";
|
|
3
3
|
import { listElements, getElement, buildElement } from "../builder/catalog.js";
|
|
4
|
-
import { buildSection, newPageSkeleton, validatePage, reassignIds, } from "../builder/page.js";
|
|
4
|
+
import { buildSection, newPageSkeleton, validatePage, finalizeForRender, reassignIds, } from "../builder/page.js";
|
|
5
5
|
// Recursive spec for new_section / build_page children.
|
|
6
6
|
const elementSpec = z.object({
|
|
7
7
|
type: z.string().describe("Element type (see list_elements)"),
|
|
@@ -76,8 +76,9 @@ The source must be { sections: [...] } — build sections with new_section. Vali
|
|
|
76
76
|
validation,
|
|
77
77
|
request: { name, slug, type: kind ?? null, page_type_num: typeNum ?? null, is_homepage, sections: (parsed && parsed.sections || []).length },
|
|
78
78
|
will_enable_feature: requiredFlag ?? null,
|
|
79
|
+
renders_at_breakpoints: ["bp1", "bp2", "bp3", "bp4"],
|
|
79
80
|
hint: validation.valid
|
|
80
|
-
? `Looks valid. Call again with dry_run=false to create and save the page.${requiredFlag ? ` Will also enable site.settings.${requiredFlag} so its data bindings resolve.` : ""}`
|
|
81
|
+
? `Looks valid. On save, every node's runtime is expanded into the bp1..bp4 keys the storefront renders. Call again with dry_run=false to create and save the page.${requiredFlag ? ` Will also enable site.settings.${requiredFlag} so its data bindings resolve.` : ""}`
|
|
81
82
|
: "Fix the errors above before saving.",
|
|
82
83
|
};
|
|
83
84
|
}
|
|
@@ -100,6 +101,8 @@ The source must be { sections: [...] } — build sections with new_section. Vali
|
|
|
100
101
|
if (!pageId) {
|
|
101
102
|
return { error: "Page created but no id was returned; cannot save source.", created };
|
|
102
103
|
}
|
|
104
|
+
// Expand runtime -> bp1..bp4 so the saved source actually renders on the storefront.
|
|
105
|
+
finalizeForRender(parsed);
|
|
103
106
|
const saved = await api.updatePageSource(pageId, { source: parsed });
|
|
104
107
|
return {
|
|
105
108
|
success: true,
|
|
@@ -145,6 +148,9 @@ Two-step safety: dry_run=true (default) previews; dry_run=false saves.`, {
|
|
|
145
148
|
}
|
|
146
149
|
if (!validation.valid)
|
|
147
150
|
return { error: "Validation failed — not saving.", validation };
|
|
151
|
+
// Expand the newly-added section's runtime -> bp1..bp4 (existing sections are
|
|
152
|
+
// already in breakpoint shape and are left untouched).
|
|
153
|
+
finalizeForRender(source);
|
|
148
154
|
const saved = await api.updatePageSource(page_id, { source });
|
|
149
155
|
return {
|
|
150
156
|
success: true,
|
package/dist/tools/context.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getConfig, setConfig } from "../db.js";
|
|
3
|
+
import { resolvePreviewUrl } from "../config.js";
|
|
3
4
|
/** Read all saved credentials from the local config file for startup */
|
|
4
5
|
export function getSavedConfig() {
|
|
5
6
|
return {
|
|
@@ -62,6 +63,58 @@ export function registerContextTools(server, api, handle) {
|
|
|
62
63
|
page,
|
|
63
64
|
};
|
|
64
65
|
}));
|
|
66
|
+
server.tool("create_site", `Create a brand-new storefront site for the current account, then (by default) switch to it.
|
|
67
|
+
The backend seeds sample categories, products and a blog, but creates NO pages — so after this,
|
|
68
|
+
build a homepage: get_build_guide → new_section → build_page (type:'main', is_homepage:true).
|
|
69
|
+
Note: free accounts are limited to 4 sites (creation fails with a quota error past that).`, {
|
|
70
|
+
name: z.string().describe("Display name of the new site, e.g. 'My Coffee Shop'"),
|
|
71
|
+
slug: z
|
|
72
|
+
.string()
|
|
73
|
+
.describe("URL-safe site slug (lowercase letters, digits, hyphens), e.g. 'my-coffee-shop'. Becomes the preview subdomain and must be unique."),
|
|
74
|
+
switch_to: z
|
|
75
|
+
.boolean()
|
|
76
|
+
.default(true)
|
|
77
|
+
.describe("Switch the session to the new site after creating it (saved for next session). Default true."),
|
|
78
|
+
}, ({ name, slug, switch_to }) => handle(async () => {
|
|
79
|
+
let res;
|
|
80
|
+
try {
|
|
81
|
+
res = await api.createSite({ name, slug });
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
85
|
+
if (msg.includes("403")) {
|
|
86
|
+
throw new Error("Cannot create site: your account's site quota is reached (free plan allows up to 4 sites). Delete an unused site or upgrade your plan, then retry.");
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Site creation failed: ${msg}. Check the slug is unique and URL-safe (lowercase, hyphens).`);
|
|
89
|
+
}
|
|
90
|
+
const site = res?.data?.site || res?.data || res?.site || res;
|
|
91
|
+
const newId = site?.id;
|
|
92
|
+
if (!newId) {
|
|
93
|
+
throw new Error("Site was not created (no id returned by the backend).");
|
|
94
|
+
}
|
|
95
|
+
const createdSlug = site?.site_slug?.slug || slug;
|
|
96
|
+
let switched = false;
|
|
97
|
+
let previewUrl = null;
|
|
98
|
+
const previousSiteId = api.siteId;
|
|
99
|
+
if (switch_to) {
|
|
100
|
+
api.switchSite(newId);
|
|
101
|
+
setConfig("site_id", newId);
|
|
102
|
+
setConfig("site_name", site?.name || name);
|
|
103
|
+
setConfig("site_domain", createdSlug || "");
|
|
104
|
+
switched = true;
|
|
105
|
+
previewUrl = await resolvePreviewUrl(api).catch(() => null);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
site_id: newId,
|
|
110
|
+
name: site?.name || name,
|
|
111
|
+
slug: createdSlug,
|
|
112
|
+
switched,
|
|
113
|
+
...(switched ? { current_site_id: api.siteId, previous_site_id: previousSiteId } : {}),
|
|
114
|
+
preview_url: previewUrl,
|
|
115
|
+
next_step: "New site has sample products/categories/blog but NO pages. Create a homepage with build_page (type:'main', is_homepage:true), then add store/member/blog pages as needed. Publish site-level with publish_site.",
|
|
116
|
+
};
|
|
117
|
+
}));
|
|
65
118
|
server.tool("switch_site", `Switch to a different site by site_id. All subsequent tool calls will target the new site.
|
|
66
119
|
The choice is saved to local database — next session will auto-connect to this site.
|
|
67
120
|
Use list_my_sites first to find the site_id`, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-storefront-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "MCP server for the WebCake/StoreCake storefront builder — page CRUD, page authoring, products, orders, and more",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/webcake-storefront-mcp",
|
|
6
6
|
"license": "MIT",
|