sbuilder-mcp 0.4.4 → 0.5.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/CHANGELOG.md +17 -0
- package/CHANGELOG.vi.md +17 -0
- package/README.md +5 -0
- package/README.vi.md +5 -0
- package/dist/catalog/checkout.generated.js +2 -1
- package/dist/catalog/elements.generated.js +2 -1
- package/dist/core/tree.js +46 -0
- package/dist/domains/site/builder.js +30 -10
- package/dist/domains/site/findings.js +15 -0
- package/dist/domains/site/review.js +112 -3
- package/dist/domains/site/traps.js +43 -5
- package/dist/install/index.js +29 -1
- package/dist/server.js +1 -0
- package/dist/tools/api.js +18 -0
- package/dist/tools/live.js +39 -5
- package/dist/tools/session.js +2 -1
- package/dist/vision/shoot.js +24 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,23 @@ All notable changes to this project are documented in this file.
|
|
|
6
6
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-09-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- sb_review reports default_seed_copy when a satellite's empty state still carries the platform's own English seed text, such as a repeater's empty state saying "No products yet", which previously matched no rule and reviewed clean.
|
|
13
|
+
- sb_review reports form_fields_flush when a form, form-segment or form-step-nav stacks its fields with no gap between them, so each label reads as belonging to the control above it.
|
|
14
|
+
- The install CLI accepts --site-name, matching the flag the platform's own Agent app install line already appends; it rides in as SB_SITE_NAME alongside the site id, and sb_connect reports it back so a session can display the store's name instead of its id.
|
|
15
|
+
- A dry run of the install CLI now reports its own preview outcome and exit code instead of reusing the failure marker and exit code of a real install.
|
|
16
|
+
- sb_api_call now falls back {siteId} and {siteID} path parameters to SB_SITE across all 289 operations that name the site, matching the fallback every other tool already applies through siteFor(); an explicit argument still wins.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- sb_set's state parameter now writes to the location the platform's cascade actually reads: node.states[state] at base, node.responsive[breakpoint].states[state] per breakpoint. Previously it wrote to a path nothing reads, and base:true combined with state:"hover" wrote the hover value straight into the plain style, leaving a node permanently styled as if hovered with no hover state at all.
|
|
20
|
+
- sb_set now refuses a state argument on specials instead of silently dropping it, since content and identity do not vary by interaction state.
|
|
21
|
+
- sb_review now walks into satellite nodes (a repeater's empty state, a variant option's skin, a quantity stepper's buttons, a menu or tab item's skin), so findings inside them are reported instead of being invisible to every check.
|
|
22
|
+
- sb_review and sb_set now report a site-wide edit as such: a change to a node inside a global section's interior, or inside a site overlay like the cart drawer, is flagged rather than reading as an ordinary page-local edit.
|
|
23
|
+
- sb_look now opens a closed overlay (such as the cart drawer) before measuring it, so a node_id resolving inside one can be photographed at all instead of failing with a clipping error that named neither the overlay nor the reason.
|
|
24
|
+
- sb_look's preview_note no longer claims the draft preview renders every repeater's empty state; the draft preview threads real store data just like a published page, and the note now describes the real caveat, which is that an entity template previews with nothing bound.
|
|
25
|
+
|
|
9
26
|
## [0.4.4] - 2026-09-08
|
|
10
27
|
|
|
11
28
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,23 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
|
|
|
6
6
|
Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-09-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- sb_review giờ báo cáo default_seed_copy khi một node vệ tinh ở trạng thái rỗng vẫn còn mang seed text tiếng Anh của nền tảng, chẳng hạn trạng thái rỗng của một repeater ghi "No products yet" — trước đây không khớp với rule nào nên review vẫn báo sạch.
|
|
13
|
+
- sb_review giờ báo cáo form_fields_flush khi một form, form-segment hoặc form-step-nav xếp các field mà không có gap, khiến mỗi label đọc như thể thuộc về control phía trên nó.
|
|
14
|
+
- CLI cài đặt giờ nhận --site-name, khớp với flag mà dòng lệnh cài đặt của chính Agent app trên nền tảng đã thêm vào; giá trị này đi kèm dưới dạng SB_SITE_NAME cùng với site id, và sb_connect báo cáo lại giá trị này để một session có thể hiển thị tên cửa hàng thay vì id.
|
|
15
|
+
- Một dry run của CLI cài đặt giờ báo cáo kết quả xem trước và exit code riêng, thay vì dùng chung dấu hiệu thất bại và exit code của một lần cài đặt thật.
|
|
16
|
+
- sb_api_call giờ fallback các path parameter {siteId} và {siteID} về SB_SITE trên toàn bộ 289 operation có tên site, khớp với cách fallback mà mọi tool khác đã áp dụng qua siteFor(); một tham số được truyền rõ ràng vẫn được ưu tiên.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Tham số state của sb_set giờ ghi đúng vào vị trí mà cascade của nền tảng thực sự đọc: node.states[state] ở base, node.responsive[breakpoint].states[state] theo từng breakpoint. Trước đây nó ghi vào một đường dẫn không ai đọc, và base:true kết hợp với state:"hover" ghi thẳng giá trị hover vào style thường, khiến node mang màu hover vĩnh viễn mà không hề có trạng thái hover.
|
|
20
|
+
- sb_set giờ từ chối tham số state trên specials thay vì âm thầm bỏ qua, vì nội dung và identity không thay đổi theo trạng thái tương tác.
|
|
21
|
+
- sb_review giờ đi vào cả các node vệ tinh (trạng thái rỗng của repeater, skin của một variant option, các nút của quantity stepper, skin của một mục menu hoặc tab), nên các finding bên trong chúng được báo cáo thay vì vô hình với mọi check.
|
|
22
|
+
- sb_review và sb_set giờ báo cáo một chỉnh sửa có phạm vi toàn site đúng như bản chất của nó: thay đổi một node bên trong phần thân của một global section, hoặc bên trong một overlay của site như cart drawer, giờ được gắn cờ thay vì đọc như một chỉnh sửa cục bộ trên trang.
|
|
23
|
+
- sb_look giờ mở một overlay đang đóng (như cart drawer) trước khi đo, nên một node_id nằm bên trong overlay đó giờ có thể được chụp ảnh, thay vì thất bại với lỗi clip không nêu rõ overlay hay lý do.
|
|
24
|
+
- preview_note của sb_look không còn khẳng định rằng bản xem trước bản nháp render trạng thái rỗng của mọi repeater; bản xem trước bản nháp truyền dữ liệu cửa hàng thật giống như một trang đã publish, và ghi chú giờ mô tả đúng lưu ý thực sự — đó là một entity template xem trước sẽ không có gì được bind.
|
|
25
|
+
|
|
9
26
|
## [0.4.4] - 2026-09-08
|
|
10
27
|
|
|
11
28
|
### Added
|
package/README.md
CHANGED
|
@@ -23,6 +23,11 @@ worse than one that does not exist.
|
|
|
23
23
|
as `SB_SITE` and every tool then defaults to it. Without it the model has to carry the id
|
|
24
24
|
through the session, which it can only get by listing pages and reading one back.
|
|
25
25
|
|
|
26
|
+
`--site-name "Your Store"` rides alongside it as `SB_SITE_NAME`. It is a label, never an
|
|
27
|
+
address — nothing resolves by it — but it lets the agent say the store's name back to you
|
|
28
|
+
instead of a 32-character id you did not choose. The **Apps → AI agent** screen appends it
|
|
29
|
+
whenever the store has a name.
|
|
30
|
+
|
|
26
31
|
It **merges**: the servers already in those files stay, whatever it replaces is copied to
|
|
27
32
|
`<file>.sbuilder-backup`, and a config it cannot parse is refused rather than overwritten —
|
|
28
33
|
a file with a trailing comma is far likelier than one worth discarding, and it is what you
|
package/README.vi.md
CHANGED
|
@@ -23,6 +23,11 @@ còn tệ hơn một cờ không tồn tại.
|
|
|
23
23
|
thành `SB_SITE` và mọi tool sau đó mặc định dùng site đó. Không có nó thì model phải mang
|
|
24
24
|
theo id suốt phiên, mà cách duy nhất để lấy là liệt kê trang rồi đọc ngược ra.
|
|
25
25
|
|
|
26
|
+
`--site-name "Tên Cửa Hàng"` đi kèm, được ghi thành `SB_SITE_NAME`. Nó là một nhãn, không phải
|
|
27
|
+
địa chỉ — không có gì phân giải theo nó — nhưng nhờ nó agent gọi được tên cửa hàng thay vì
|
|
28
|
+
đọc lại một id 32 ký tự bạn không đặt. Màn hình **Apps → AI agent** tự thêm nó khi cửa hàng
|
|
29
|
+
có tên.
|
|
30
|
+
|
|
26
31
|
Nó **gộp**: các server đã có trong file được giữ nguyên, thứ nó thay thế được chép sang
|
|
27
32
|
`<file>.sbuilder-backup`, và một config nó không đọc được thì bị từ chối chứ không ghi đè —
|
|
28
33
|
một file thừa dấu phẩy khả dĩ hơn nhiều một file đáng vứt, và đó chính là thứ bạn cần để sửa.
|
package/dist/core/tree.js
CHANGED
|
@@ -87,6 +87,32 @@ export function walk(doc, id, visit) {
|
|
|
87
87
|
};
|
|
88
88
|
go(id);
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* One level of `walk`: `data.nodes` followed by the satellites on `config[key]`.
|
|
92
|
+
*
|
|
93
|
+
* For a caller that needs its OWN recursion — carrying scope down as it goes,
|
|
94
|
+
* the way `reviewDesign` carries "am I inside a repeater / an overlay" — and so
|
|
95
|
+
* cannot hand the traversal to `walk`. Such a caller reaching for `childrenOf`
|
|
96
|
+
* is the mistake `walk`'s comment warns about, and `sb_review` made it: it never
|
|
97
|
+
* saw a satellite, so every empty state, every variant-option skin and every
|
|
98
|
+
* quantity stepper was outside the review entirely.
|
|
99
|
+
*
|
|
100
|
+
* Satellites come AFTER the real children and are not deduplicated against
|
|
101
|
+
* them — a `configKey` pointing at a node that is also a child would be a
|
|
102
|
+
* malformed document, and a walk is not a validator.
|
|
103
|
+
*/
|
|
104
|
+
export function childrenWithSatellites(doc, id) {
|
|
105
|
+
const n = doc.nodes[id];
|
|
106
|
+
if (!n)
|
|
107
|
+
return [];
|
|
108
|
+
const out = [...n.data.nodes];
|
|
109
|
+
for (const rule of SATELLITE_RULES[n.data.type] ?? []) {
|
|
110
|
+
const sat = n.config?.[rule.configKey];
|
|
111
|
+
if (typeof sat === 'string' && sat && doc.nodes[sat])
|
|
112
|
+
out.push(sat);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
90
116
|
export function subtreeIds(doc, id) {
|
|
91
117
|
const out = [];
|
|
92
118
|
walk(doc, id, (n) => out.push(n.id));
|
|
@@ -114,6 +140,26 @@ export function ancestors(doc, id) {
|
|
|
114
140
|
* therefore stored nowhere and reported nowhere — trap 5. Nearest stamp wins:
|
|
115
141
|
* the block root answers with itself.
|
|
116
142
|
*/
|
|
143
|
+
/**
|
|
144
|
+
* The site overlay this node sits in, or null. Nearest stamp wins, so the
|
|
145
|
+
* overlay root answers with itself.
|
|
146
|
+
*
|
|
147
|
+
* `isOverlay` answers only for the ROOT of one, which is right for the rules
|
|
148
|
+
* that decide whether a node may BE an overlay. It is the wrong question for a
|
|
149
|
+
* WRITE: an edit to a node inside the cart drawer is an edit to the drawer, and
|
|
150
|
+
* the drawer is one master shared by every page on the site. Without this, a
|
|
151
|
+
* `sb_set` on a drawer node reported a plain page-local success while changing
|
|
152
|
+
* ten pages — the same asymmetry `sb_review` closed when it started walking
|
|
153
|
+
* overlays and flagging their findings `overlay: true`.
|
|
154
|
+
*/
|
|
155
|
+
export function overlayRoot(doc, id) {
|
|
156
|
+
if (isOverlay(doc, id))
|
|
157
|
+
return id;
|
|
158
|
+
for (const a of ancestors(doc, id))
|
|
159
|
+
if (isOverlay(doc, a))
|
|
160
|
+
return a;
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
117
163
|
export function appBlockRoot(doc, id) {
|
|
118
164
|
const stamped = (n) => n !== undefined &&
|
|
119
165
|
(n.specials?.[SPEC_APP_BLOCK_ID] !== undefined || n.specials?.[SPEC_APP_BLOCK_REF] !== undefined);
|
|
@@ -182,12 +182,42 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
182
182
|
const rebind = namespace === 'config' ? rebindPatch(doc, id, keys) : null;
|
|
183
183
|
if (namespace === 'specials') {
|
|
184
184
|
refuseComposedStamp(keys);
|
|
185
|
+
// `specials` is content and identity, base-only by definition, and states
|
|
186
|
+
// carry style. Saying so is the point: this used to drop the state and write
|
|
187
|
+
// the value as if it had been asked for plainly.
|
|
188
|
+
if (opts.state) {
|
|
189
|
+
throw new Error(`sbuilder: specials takes no interaction state — "${opts.state}" would be dropped. ` +
|
|
190
|
+
'specials is content and identity (text, htmlTag, bound…), which do not vary by ' +
|
|
191
|
+
'state. Style is what has states.');
|
|
192
|
+
}
|
|
185
193
|
return Object.entries(keys).map(([k, v]) => ({
|
|
186
194
|
op: 'set',
|
|
187
195
|
path: ['nodes', id, 'specials', k],
|
|
188
196
|
value: v,
|
|
189
197
|
}));
|
|
190
198
|
}
|
|
199
|
+
// A STATE IS ANSWERED BEFORE `base`, because `base` used to be tested first
|
|
200
|
+
// and swallowed it: `base:true state:"hover"` wrote the hover value straight
|
|
201
|
+
// into the plain style, so the node wore its hover colour permanently and had
|
|
202
|
+
// no hover at all. The tool reported success, and this repo's own design skill
|
|
203
|
+
// documents that exact call — `sb_set pr_option style base:true state:"active"`
|
|
204
|
+
// — as the way to style a selected option.
|
|
205
|
+
if (opts.state) {
|
|
206
|
+
// Base state and per-breakpoint state are DIFFERENT PLACES in the document,
|
|
207
|
+
// and the old path (`states[state][bp][ns]`) was neither of them: it buried
|
|
208
|
+
// a breakpoint inside the base-state cluster, where nothing reads it.
|
|
209
|
+
const prefix = opts.base
|
|
210
|
+
? ['nodes', id, 'states', opts.state]
|
|
211
|
+
: ['nodes', id, 'responsive', opts.breakpoint ?? 'desktop', 'states', opts.state];
|
|
212
|
+
return [
|
|
213
|
+
...Object.entries(keys).map(([k, v]) => ({
|
|
214
|
+
op: 'set',
|
|
215
|
+
path: [...prefix, namespace, k],
|
|
216
|
+
value: v,
|
|
217
|
+
})),
|
|
218
|
+
...(rebind ? [rebind] : []),
|
|
219
|
+
];
|
|
220
|
+
}
|
|
191
221
|
if (opts.base) {
|
|
192
222
|
// Base IS legitimate, and this used to throw for anything that was not an
|
|
193
223
|
// identity key — a refusal built on a misread of the platform's responsive
|
|
@@ -217,16 +247,6 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
217
247
|
];
|
|
218
248
|
}
|
|
219
249
|
const bp = opts.breakpoint ?? 'desktop';
|
|
220
|
-
if (opts.state) {
|
|
221
|
-
return [
|
|
222
|
-
...Object.entries(keys).map(([k, v]) => ({
|
|
223
|
-
op: 'set',
|
|
224
|
-
path: ['nodes', id, 'states', opts.state, bp, namespace, k],
|
|
225
|
-
value: v,
|
|
226
|
-
})),
|
|
227
|
-
...(rebind ? [rebind] : []),
|
|
228
|
-
];
|
|
229
|
-
}
|
|
230
250
|
return [
|
|
231
251
|
...Object.entries(keys).map(([k, v]) => ({
|
|
232
252
|
op: 'set',
|
|
@@ -18,6 +18,15 @@ export const FIX = {
|
|
|
18
18
|
empty_text: 'Set it: sb_set id "<id>", namespace specials, keys { "<key>": … }.',
|
|
19
19
|
missing_media: 'Set it: sb_set id "<id>", namespace specials, keys { "<key>": … }.',
|
|
20
20
|
placeholder_content: 'Write the real copy: sb_set id "<id>", namespace specials, keys { "<key>": … }.',
|
|
21
|
+
// Named apart from placeholder_content because the reader needs telling that
|
|
22
|
+
// this surface EXISTS. An empty state is authored out of sight — it only shows
|
|
23
|
+
// when the list is empty — so an agent that never saw it does not know there is
|
|
24
|
+
// anything to write.
|
|
25
|
+
default_seed_copy: 'Write it in the shop\'s own language and ink: sb_set id "<id>", namespace specials, ' +
|
|
26
|
+
'keys { "<key>": … }. It is a seeded satellite (an empty state, an element\'s chrome), ' +
|
|
27
|
+
'so it only appears when the list is empty or the control is idle — sb_outline lists it ' +
|
|
28
|
+
'under its owner as satellite:"<config key>". Style it too: the seed ships #171717 ink ' +
|
|
29
|
+
'and #d4d4d4 icons, which are the platform\'s, not the site\'s.',
|
|
21
30
|
// The fix is a DIFFERENT ELEMENT, not a value: setting "<key>" here would put
|
|
22
31
|
// one authored value on every row of the repeater.
|
|
23
32
|
static_in_dataset: `Swap the element: sb_add one of ${RECORD_ELEMENTS} in the same parent, sb_bind it ` +
|
|
@@ -25,6 +34,12 @@ export const FIX = {
|
|
|
25
34
|
'sb_remove id "<id>". Setting "<key>" on this one would show the same value in every row.',
|
|
26
35
|
unbound_dataset_element: 'Bind it: sb_bind id "<id>", field "specials.<key>", and the source that names the record ' +
|
|
27
36
|
'field you want (sb_bind refuses an unknown source and lists every valid one).',
|
|
37
|
+
// The value the platform now seeds on these elements, so a repair matches what
|
|
38
|
+
// a freshly created form would have.
|
|
39
|
+
form_fields_flush: 'Give the stack room: sb_set id "<id>", namespace style, base true, keys { "gap": "12px" } ' +
|
|
40
|
+
'— the value the platform seeds on form / form-segment / form-step-nav. Raise it if the ' +
|
|
41
|
+
'design wants more air; it must stay clear of config.fieldStackGap, which is the smaller ' +
|
|
42
|
+
'label-to-control gap INSIDE one field.',
|
|
28
43
|
unlinked_form: 'Point it at a real form: sb_set id "<id>", namespace specials, keys ' +
|
|
29
44
|
'{ "formId": "<a form id from sb_api_find \'list forms\'>" }.',
|
|
30
45
|
dead_menu_link: 'Write the entries the renderer actually reads: sb_set id "<id>", namespace specials, keys ' +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { childrenOf, isOverlay, pageChildren, appBlockRoot } from '../../core/tree.js';
|
|
2
|
-
import { ELEMENTS, BINDING_SOURCES, BOUND_SPECIALS, FIRST_CHILD_ONLY } from '../../catalog/elements.generated.js';
|
|
1
|
+
import { childrenOf, childrenWithSatellites, isOverlay, pageChildren, appBlockRoot } from '../../core/tree.js';
|
|
2
|
+
import { ELEMENTS, BINDING_SOURCES, BOUND_SPECIALS, FIRST_CHILD_ONLY, SATELLITE_RULES, ELEMENT_SEEDS } from '../../catalog/elements.generated.js';
|
|
3
3
|
import { fill } from './findings.js';
|
|
4
4
|
/**
|
|
5
5
|
* Shipped with every non-empty finding list.
|
|
@@ -23,6 +23,57 @@ function contentKeys(type) {
|
|
|
23
23
|
function seededValue(type, key) {
|
|
24
24
|
return (ELEMENTS[type]?.defaults.specials ?? {})[key];
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Every line of copy the PLATFORM writes into a seeded subtree.
|
|
28
|
+
*
|
|
29
|
+
* `placeholder_content` above compares against `ELEMENTS[type].defaults.specials`
|
|
30
|
+
* — the element's OWN default — and that is exactly why it could never see an
|
|
31
|
+
* empty state. A heading's own default is `"Heading"`; the heading inside a
|
|
32
|
+
* repeater's empty state is minted from `SATELLITE_RULES`' seed tree and says
|
|
33
|
+
* `"No products yet"`. Different source, so the check walked straight past it.
|
|
34
|
+
*
|
|
35
|
+
* The consequence was general, not incidental: EVERY store built with these
|
|
36
|
+
* tools ships the platform's English empty states and reviews clean. Measured on
|
|
37
|
+
* a Vietnamese storefront — home, category, product and search all reported
|
|
38
|
+
* "nothing a visitor would notice" while four repeaters said "No products yet"
|
|
39
|
+
* and "New arrivals will show up here. Check back soon." in `#171717` on a page
|
|
40
|
+
* that is `#2E2A3B` throughout. It is the most-visited copy on a store: an empty
|
|
41
|
+
* cart is the empty state a shopper meets first.
|
|
42
|
+
*
|
|
43
|
+
* Collected from BOTH generated seed tables, so a new empty state the platform
|
|
44
|
+
* ships is covered by the next `npm run codegen` rather than by an edit here.
|
|
45
|
+
*/
|
|
46
|
+
const SEEDED_SUBTREE_TEXT = (() => {
|
|
47
|
+
const out = new Set();
|
|
48
|
+
const visit = (v) => {
|
|
49
|
+
if (Array.isArray(v)) {
|
|
50
|
+
for (const x of v)
|
|
51
|
+
visit(x);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (v === null || typeof v !== 'object')
|
|
55
|
+
return;
|
|
56
|
+
const o = v;
|
|
57
|
+
const t = o.specials?.text;
|
|
58
|
+
if (typeof t === 'string' && t.trim() !== '')
|
|
59
|
+
out.add(t);
|
|
60
|
+
for (const x of Object.values(v))
|
|
61
|
+
visit(x);
|
|
62
|
+
};
|
|
63
|
+
visit(SATELLITE_RULES);
|
|
64
|
+
visit(ELEMENT_SEEDS);
|
|
65
|
+
return out;
|
|
66
|
+
})();
|
|
67
|
+
/**
|
|
68
|
+
* The containers whose children are form FIELDS.
|
|
69
|
+
*
|
|
70
|
+
* Named rather than derived: "offers a Gap control" is true of 33 elements and
|
|
71
|
+
* four of them legitimately seed none (`flex-section` and `flex-block` are the
|
|
72
|
+
* layout primitives, where the author composes the spacing). What makes these
|
|
73
|
+
* three different is that their children are LABELLED CONTROLS, and a label with
|
|
74
|
+
* no space above it attaches itself to the wrong one.
|
|
75
|
+
*/
|
|
76
|
+
const FIELD_STACKS = new Set(['form', 'form-segment', 'form-step-nav']);
|
|
26
77
|
/**
|
|
27
78
|
* A container that renders its subtree ONCE PER RECORD.
|
|
28
79
|
*
|
|
@@ -111,7 +162,13 @@ export function reviewDesign(doc) {
|
|
|
111
162
|
if (appBlockRoot(d, id) === id)
|
|
112
163
|
return;
|
|
113
164
|
const inner = repeats(d.nodes[id]?.data.type ?? '') ? (repeater ?? id) : repeater;
|
|
114
|
-
|
|
165
|
+
// SATELLITES INCLUDED. `childrenOf` here meant the review never entered one,
|
|
166
|
+
// so an element's whole chrome — every repeater's empty state, the variant
|
|
167
|
+
// option skin, the quantity stepper, the menu and tab item skins — sat
|
|
168
|
+
// outside the check that exists to say what a visitor meets. `walk`'s own
|
|
169
|
+
// comment names this as the mistake a caller makes by reaching for the
|
|
170
|
+
// child-only list; this had made it.
|
|
171
|
+
for (const k of childrenWithSatellites(d, id))
|
|
115
172
|
go(k, inner, overlay || overlayIds.has(k));
|
|
116
173
|
};
|
|
117
174
|
go(d.root_node_id);
|
|
@@ -212,6 +269,22 @@ export function reviewDesign(doc) {
|
|
|
212
269
|
fix: fill('placeholder_content', { id, key }),
|
|
213
270
|
});
|
|
214
271
|
}
|
|
272
|
+
// The same defect, one seed table over — see SEEDED_SUBTREE_TEXT. Reported
|
|
273
|
+
// separately because the FIX reads differently: this copy is not a
|
|
274
|
+
// "<Heading>" nobody noticed, it is a real English sentence that renders
|
|
275
|
+
// as if somebody meant it.
|
|
276
|
+
else if (key === 'text' && typeof value === 'string' && SEEDED_SUBTREE_TEXT.has(value)) {
|
|
277
|
+
out.push({
|
|
278
|
+
code: 'default_seed_copy',
|
|
279
|
+
nodeId: id,
|
|
280
|
+
type,
|
|
281
|
+
problem: `Still the platform's own seed copy (${JSON.stringify(value)}), in English. This ` +
|
|
282
|
+
'surface was never authored — it is minted with the element and reads as if ' +
|
|
283
|
+
'somebody wrote it.',
|
|
284
|
+
key,
|
|
285
|
+
fix: fill('default_seed_copy', { id, key }),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
215
288
|
}
|
|
216
289
|
// A FORM NOBODY LINKED publishes as an empty box, and the platform stays
|
|
217
290
|
// deliberately quiet about it: form.go:221 skips an empty formId with the
|
|
@@ -233,6 +306,42 @@ export function reviewDesign(doc) {
|
|
|
233
306
|
});
|
|
234
307
|
}
|
|
235
308
|
}
|
|
309
|
+
// A FORM WHOSE FIELDS TOUCH. The three field stacks lay out `display:flex`
|
|
310
|
+
// + `flexDirection:column`, so with no `gap` every field sits flush against
|
|
311
|
+
// the one above it and each label ends up nearer the previous control than
|
|
312
|
+
// its own — the one thing a form's spacing has to get right.
|
|
313
|
+
//
|
|
314
|
+
// Measured on a published checkout at 1440px: six consecutive fields, every
|
|
315
|
+
// gap between them EXACTLY 0. The platform now seeds `gap: 12px` on these
|
|
316
|
+
// elements, but `defaults` seeds at CREATION, so every form authored before
|
|
317
|
+
// that keeps the spacing it was given and nothing says so.
|
|
318
|
+
//
|
|
319
|
+
// NOT a child check: a form's fields live in the FORM DOCUMENT and are
|
|
320
|
+
// composed on the render path, so the page's own node has `nodes: []` and
|
|
321
|
+
// counting children would report every form as empty.
|
|
322
|
+
if (FIELD_STACKS.has(type)) {
|
|
323
|
+
const slots = [
|
|
324
|
+
n.style,
|
|
325
|
+
...Object.values(n.responsive ?? {}).map((s) => s?.style),
|
|
326
|
+
];
|
|
327
|
+
const anyGap = slots.some((s) => {
|
|
328
|
+
const g = s?.gap;
|
|
329
|
+
return g !== undefined && g !== null && `${g}`.trim() !== '' && parseFloat(`${g}`) > 0;
|
|
330
|
+
});
|
|
331
|
+
if (!anyGap) {
|
|
332
|
+
out.push({
|
|
333
|
+
code: 'form_fields_flush',
|
|
334
|
+
nodeId: id,
|
|
335
|
+
type,
|
|
336
|
+
problem: 'This form stacks its fields with no gap, so each one touches the one above it and ' +
|
|
337
|
+
"every label reads as belonging to the control above rather than its own. The " +
|
|
338
|
+
'field\'s own label-to-control spacing (config.fieldStackGap) is a different, ' +
|
|
339
|
+
'smaller quantity and does not close this.',
|
|
340
|
+
key: 'gap',
|
|
341
|
+
fix: fill('form_fields_flush', { id, key: 'gap' }),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
236
345
|
// A MENU ENTRY WITH NO HREF is a link that goes nowhere. The Go renderer
|
|
237
346
|
// reads specials.menuItems and never menuId, so picking a menu by id
|
|
238
347
|
// publishes an empty nav, and the element's own seed ships one entry
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FIRST_CHILD_ONLY } from '../../catalog/elements.generated.js';
|
|
2
|
-
import { pageChildren, isOverlay, SPEC_GLOBAL_ID, SPEC_GLOBAL_KIND, SPEC_GLOBAL_REV, SPEC_OVERLAY_ID, SPEC_OVERLAY_REV, } from '../../core/tree.js';
|
|
2
|
+
import { pageChildren, isOverlay, overlayRoot, ancestors, SPEC_GLOBAL_ID, SPEC_GLOBAL_KIND, SPEC_GLOBAL_REV, SPEC_OVERLAY_ID, SPEC_OVERLAY_REV, } from '../../core/tree.js';
|
|
3
3
|
/**
|
|
4
4
|
* Which band a direct child of ROOT belongs to.
|
|
5
5
|
*
|
|
@@ -60,6 +60,22 @@ export function checkBandOrder(doc) {
|
|
|
60
60
|
export function isGlobal(doc, id) {
|
|
61
61
|
return doc.nodes[id]?.specials?.[SPEC_GLOBAL_ID] !== undefined;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The global section this node sits in, or null. Nearest stamp wins.
|
|
65
|
+
*
|
|
66
|
+
* `isGlobal` answers for the stamped ROOT alone, which is the right question for
|
|
67
|
+
* "may this node be moved out of the band it is in". It is the wrong one for a
|
|
68
|
+
* WRITE: nobody restyles the header section, they restyle a button inside it,
|
|
69
|
+
* and that edit is just as site-wide.
|
|
70
|
+
*/
|
|
71
|
+
export function globalRoot(doc, id) {
|
|
72
|
+
if (isGlobal(doc, id))
|
|
73
|
+
return id;
|
|
74
|
+
for (const a of ancestors(doc, id))
|
|
75
|
+
if (isGlobal(doc, a))
|
|
76
|
+
return a;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
63
79
|
/**
|
|
64
80
|
* The sentence to attach to any result that touched a global.
|
|
65
81
|
*
|
|
@@ -69,10 +85,32 @@ export function isGlobal(doc, id) {
|
|
|
69
85
|
* not know this reports "updated the header" having changed the whole site.
|
|
70
86
|
*/
|
|
71
87
|
export function globalWarning(doc, id) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
// THE STAMP IS ON THE SECTION ROOT, and asking only `isGlobal(id)` meant the
|
|
89
|
+
// warning fired for the one node nobody edits. The work happens INSIDE: a nav
|
|
90
|
+
// button in the global header, a line of the global footer. Restyling
|
|
91
|
+
// `bu_…` changed all ten pages of a site and the result said nothing, while
|
|
92
|
+
// the same edit one level up would have warned. Same asymmetry as the overlay
|
|
93
|
+
// one below, one construct over.
|
|
94
|
+
const g = globalRoot(doc, id);
|
|
95
|
+
if (g !== null) {
|
|
96
|
+
const gid = doc.nodes[g].specials?.[SPEC_GLOBAL_ID];
|
|
97
|
+
const kind = doc.nodes[g].specials?.[SPEC_GLOBAL_KIND];
|
|
98
|
+
const self = g === id ? '' : ` (inside global ${g})`;
|
|
99
|
+
return `Node ${id}${self} belongs to the shared global section ${JSON.stringify(gid)}${kind ? ` (${kind})` : ''}. Editing it changes EVERY page that carries it, and publishing cascades to all of them. Say so when reporting this change.`;
|
|
100
|
+
}
|
|
101
|
+
// AN OVERLAY IS SHARED TOO, and this used to answer only for globals. The cart
|
|
102
|
+
// drawer and the pop-ups are ONE master composed onto every page, so an edit
|
|
103
|
+
// to a node inside one is site-wide — measured: restyling the drawer's
|
|
104
|
+
// quantity stepper touched ten pages and the result read as a page-local
|
|
105
|
+
// success. It is the write-side of the reason `sb_review` flags overlay
|
|
106
|
+
// findings `overlay: true` instead of reporting them once per page.
|
|
107
|
+
const ov = overlayRoot(doc, id);
|
|
108
|
+
if (ov !== null) {
|
|
109
|
+
const oid = doc.nodes[ov].specials?.[SPEC_OVERLAY_ID];
|
|
110
|
+
const self = ov === id ? '' : ` (inside overlay ${ov})`;
|
|
111
|
+
return `Node ${id}${self} belongs to the site overlay ${JSON.stringify(oid)} — the cart drawer or a pop-up. It is ONE master composed onto every page, so this change is site-wide, not page-local. Say so when reporting it.`;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
76
114
|
}
|
|
77
115
|
/**
|
|
78
116
|
* Keys that are identity or content rather than a visual quantity.
|
package/dist/install/index.js
CHANGED
|
@@ -9,11 +9,21 @@ export const SERVER_NAME = 'sbuilder';
|
|
|
9
9
|
* at a real install, read by nothing, and reported as success — the caller then
|
|
10
10
|
* spent the session wondering why the site was not selected. A flag that is
|
|
11
11
|
* silently dropped is worse than one that does not exist.
|
|
12
|
+
*
|
|
13
|
+
* BUT REFUSING ONE THE PLATFORM ITSELF EMITS IS WORSE STILL, and that is what
|
|
14
|
+
* `--site-name` became. The editor's Agent app builds the whole install line for
|
|
15
|
+
* the merchant to paste — `AgentAppPanel.vue`, which appends
|
|
16
|
+
* `--site-name "<store>"` whenever it knows the name, with a test pinning it —
|
|
17
|
+
* so the ONE documented install path exited 1 and installed nothing. The flag
|
|
18
|
+
* was never noise; it was added on the platform side after this list was
|
|
19
|
+
* written. It is a label, not a credential: it rides in as SB_SITE_NAME so the
|
|
20
|
+
* agent can say "Áo Thun" instead of reading back site_14675b5a570b248d.
|
|
12
21
|
*/
|
|
13
22
|
const FLAGS = [
|
|
14
23
|
'--token',
|
|
15
24
|
'--api',
|
|
16
25
|
'--site',
|
|
26
|
+
'--site-name',
|
|
17
27
|
'--email',
|
|
18
28
|
'--password',
|
|
19
29
|
'--client',
|
|
@@ -30,6 +40,11 @@ export function buildEntry(opts, pkg = 'sbuilder-mcp') {
|
|
|
30
40
|
// model the guess it otherwise makes from a page list.
|
|
31
41
|
if (opts.site)
|
|
32
42
|
env.SB_SITE = opts.site;
|
|
43
|
+
// The name the merchant calls the store, so the agent can too. Carried only
|
|
44
|
+
// alongside an id — a name with nothing to resolve to is decoration, and a
|
|
45
|
+
// second install would file it under a site it does not name.
|
|
46
|
+
if (opts.site && opts.siteName)
|
|
47
|
+
env.SB_SITE_NAME = opts.siteName;
|
|
33
48
|
// Only when a key is absent: a key opens everything the agent does day to day,
|
|
34
49
|
// and writing an account password into six config files to buy the handful of
|
|
35
50
|
// account-level calls it adds is a bad trade the installer should not make for
|
|
@@ -109,6 +124,7 @@ export function runInstallCli(argv) {
|
|
|
109
124
|
token: get('--token') ?? process.env.SB_TOKEN,
|
|
110
125
|
api: get('--api') ?? process.env.SB_API,
|
|
111
126
|
site: get('--site') ?? process.env.SB_SITE,
|
|
127
|
+
siteName: get('--site-name') ?? process.env.SB_SITE_NAME,
|
|
112
128
|
email: get('--email') ?? process.env.SB_EMAIL,
|
|
113
129
|
password: get('--password') ?? process.env.SB_PASSWORD,
|
|
114
130
|
clients: get('--client')?.split(','),
|
|
@@ -134,7 +150,17 @@ export function runInstallCli(argv) {
|
|
|
134
150
|
return 1;
|
|
135
151
|
}
|
|
136
152
|
for (const r of results) {
|
|
137
|
-
|
|
153
|
+
// A DRY RUN THAT WROTE NOTHING DID ITS JOB. Every line used to be marked
|
|
154
|
+
// `✖` and the command exited 1, so the one way to inspect an install before
|
|
155
|
+
// making it reported total failure — and a CI step wrapping it would stop
|
|
156
|
+
// there. The preview gets its own mark and its own exit code.
|
|
157
|
+
const mark = opts.dryRun
|
|
158
|
+
? '→'
|
|
159
|
+
: r.status === 'installed'
|
|
160
|
+
? '✔'
|
|
161
|
+
: r.status === 'unchanged'
|
|
162
|
+
? '·'
|
|
163
|
+
: '✖';
|
|
138
164
|
console.error(`${mark} ${r.client} — ${r.status}${r.reason ? ` (${r.reason})` : ''}`);
|
|
139
165
|
console.error(` ${r.path}`);
|
|
140
166
|
// The undo, named. A config writer that changes a file without saying where
|
|
@@ -144,5 +170,7 @@ export function runInstallCli(argv) {
|
|
|
144
170
|
if (r.note)
|
|
145
171
|
console.error(` ${r.note}`);
|
|
146
172
|
}
|
|
173
|
+
if (opts.dryRun)
|
|
174
|
+
return 0;
|
|
147
175
|
return results.some((r) => r.status === 'installed' || r.status === 'unchanged') ? 0 : 1;
|
|
148
176
|
}
|
package/dist/server.js
CHANGED
package/dist/tools/api.js
CHANGED
|
@@ -163,6 +163,14 @@ export function tokenFor(ctx, credential) {
|
|
|
163
163
|
}
|
|
164
164
|
return undefined;
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* The path parameters that mean "THIS site" — lowercased, because the platform
|
|
168
|
+
* writes `{siteId}` in 281 operations and `{siteID}` in 8.
|
|
169
|
+
*
|
|
170
|
+
* Deliberately only the site. `{productId}`, `{id}` and the rest name a record
|
|
171
|
+
* the caller chose; the site is the one id an install already holds.
|
|
172
|
+
*/
|
|
173
|
+
const SITE_PARAMS = new Set(['siteid']);
|
|
166
174
|
export async function callOperation(ctx, args) {
|
|
167
175
|
const op = API_OPERATIONS.find((o) => o.id === args.id);
|
|
168
176
|
if (!op)
|
|
@@ -185,6 +193,16 @@ export async function callOperation(ctx, args) {
|
|
|
185
193
|
if (folded !== undefined)
|
|
186
194
|
value = given[folded];
|
|
187
195
|
}
|
|
196
|
+
// SB_SITE ANSWERS FOR {siteId}, exactly as it does for every tool's
|
|
197
|
+
// `site_id` argument through `siteFor()`. It did not here, and this is the
|
|
198
|
+
// surface where it costs most: `sb_api_call` reaches 484 operations and 289
|
|
199
|
+
// of them name the site, so a key-only install — where the id is a constant
|
|
200
|
+
// the environment already holds — made the model carry a 32-character
|
|
201
|
+
// string through every raw call it made. An explicit argument still wins,
|
|
202
|
+
// so a session spanning two sites works by naming each.
|
|
203
|
+
if (value === undefined && SITE_PARAMS.has(name.toLowerCase()) && ctx.siteId) {
|
|
204
|
+
value = ctx.siteId;
|
|
205
|
+
}
|
|
188
206
|
if (value === undefined) {
|
|
189
207
|
// NAME THE ARGUMENT, not just the parameter. The call sheet lists these
|
|
190
208
|
// under `params` while the call takes them in `path_params`, and a caller
|
package/dist/tools/live.js
CHANGED
|
@@ -24,7 +24,7 @@ const DATASET_TYPES = new Set([
|
|
|
24
24
|
import { RealtimeSocket } from '../transport/socket.js';
|
|
25
25
|
import { LiveSession } from '../live/session.js';
|
|
26
26
|
import { refuseAppBlockInterior } from '../domains/site/builder.js';
|
|
27
|
-
import { childrenOf, isOverlay, subtreeIds } from '../core/tree.js';
|
|
27
|
+
import { childrenOf, isOverlay, overlayRoot, subtreeIds } from '../core/tree.js';
|
|
28
28
|
import { siteToken } from './credentialpick.js';
|
|
29
29
|
import { siteFor } from './context.js';
|
|
30
30
|
import { projectList, MEDIA_FIELDS } from './project.js';
|
|
@@ -275,7 +275,21 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
275
275
|
// the process; `shoot` keeps them in `widths` order. The format changes
|
|
276
276
|
// bytes and latency only — the client prices an image by its pixel size,
|
|
277
277
|
// so jpeg and png cost the agent the same tokens.
|
|
278
|
-
|
|
278
|
+
// FRAMING A NODE IN THE CART DRAWER MEANS OPENING THE DRAWER. A closed
|
|
279
|
+
// overlay is translated off-screen, so the clip lands outside the image
|
|
280
|
+
// and the shot fails with a Playwright error naming neither the overlay
|
|
281
|
+
// nor the reason. `overlayRoot` answers for a node ANYWHERE inside one,
|
|
282
|
+
// which is the case that matters: the caller frames the stepper or the
|
|
283
|
+
// empty state, not the drawer root.
|
|
284
|
+
const openOverlay = node_id
|
|
285
|
+
? (overlayRoot(session.current().doc, node_id) ?? undefined)
|
|
286
|
+
: undefined;
|
|
287
|
+
const shots = await shoot(target, {
|
|
288
|
+
widths: widths ?? DEFAULT_WIDTHS,
|
|
289
|
+
node: node_id,
|
|
290
|
+
format,
|
|
291
|
+
...(openOverlay ? { open: openOverlay } : {}),
|
|
292
|
+
});
|
|
279
293
|
// The boxes feed the presence cursor as well as the agent's own reading.
|
|
280
294
|
session.noteBoxes(shots[0]?.boxes ?? []);
|
|
281
295
|
// The findings ride WITH the picture. Judging a page by eye and judging it
|
|
@@ -301,10 +315,30 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
301
315
|
// Say it ONCE, and only when it can actually mislead: a page with no
|
|
302
316
|
// store-driven element has nothing to be missing from the preview.
|
|
303
317
|
const dataDriven = Object.values(session.current().doc.nodes).some((n) => DATASET_TYPES.has(n.data.type));
|
|
318
|
+
// THIS NOTE USED TO SAY THE OPPOSITE, and it sent readers to fix a page
|
|
319
|
+
// that was right. It claimed the draft preview "threads no store data:
|
|
320
|
+
// every repeater renders its empty state there" — measured false: a home
|
|
321
|
+
// page previewed four real products at their real prices, matching the
|
|
322
|
+
// catalogue exactly. `ServePreview` runs RenderDraft → gather → assemble,
|
|
323
|
+
// the SAME path as a published page, and the platform's own comment on it
|
|
324
|
+
// says the result is "byte-identical to what publishing this source would
|
|
325
|
+
// serve" (storefront.go:1260). The shoot path's own comment had already
|
|
326
|
+
// recorded the observation — "identical content on screen (images, prices,
|
|
327
|
+
// no empty states)" — while this note contradicted it.
|
|
328
|
+
//
|
|
329
|
+
// What the preview genuinely cannot do is resolve ONE RECORD from the URL:
|
|
330
|
+
// ServePreview never runs entity routing (that lives in ServeHost), so an
|
|
331
|
+
// entity TEMPLATE previews with nothing bound. That is the real caveat,
|
|
332
|
+
// and it is the opposite population of pages from the one the old note
|
|
333
|
+
// warned about.
|
|
304
334
|
const previewNote = !url && dataDriven
|
|
305
|
-
? ctx.notices.once('preview-scope', 'This is the DRAFT PREVIEW
|
|
306
|
-
'
|
|
307
|
-
'
|
|
335
|
+
? ctx.notices.once('preview-scope', 'This is the DRAFT PREVIEW. It renders through the same path as a published page, ' +
|
|
336
|
+
'so repeaters DO show real store records — judge a list page from it. What it ' +
|
|
337
|
+
'cannot do is resolve a single record from the address: on an ENTITY TEMPLATE ' +
|
|
338
|
+
'(the product or category detail page) nothing is bound, so the title is blank, ' +
|
|
339
|
+
'the price reads zero and a variant picker shows the element\'s seed options ' +
|
|
340
|
+
'("Color / Size", "Red / S") rather than the product\'s own. That is the preview, ' +
|
|
341
|
+
'not the page. Pass a published storefront URL as `url` to judge a template.')
|
|
308
342
|
: undefined;
|
|
309
343
|
return images(shots.map((s) => ({ dataBase64: s.imageBase64, mimeType: s.mimeType })), {
|
|
310
344
|
widths: shots.map((s) => s.width),
|
package/dist/tools/session.js
CHANGED
|
@@ -24,9 +24,10 @@ export async function connect(ctx, args) {
|
|
|
24
24
|
sites: [],
|
|
25
25
|
api_key: 'present',
|
|
26
26
|
...(ctx.siteId ? { site: ctx.siteId } : {}),
|
|
27
|
+
...(ctx.siteId && ctx.siteName ? { site_name: ctx.siteName } : {}),
|
|
27
28
|
operations: API_OPERATIONS.length,
|
|
28
29
|
note: ctx.siteId
|
|
29
|
-
? `Connected with an API key alone, on site ${ctx.siteId} (SB_SITE). Every tool ` +
|
|
30
|
+
? `Connected with an API key alone, on site ${ctx.siteId} (SB_SITE)${ctx.siteName ? `, the store called ${JSON.stringify(ctx.siteName)}` : ''}. Every tool ` +
|
|
30
31
|
'defaults to it, so site_id is optional. Set SB_EMAIL and SB_PASSWORD as well if ' +
|
|
31
32
|
'you want account-level calls (listing sites, members, roles), which a key cannot make.'
|
|
32
33
|
: 'Connected with an API key alone. It is bound to one site, so there is no site list — ' +
|
package/dist/vision/shoot.js
CHANGED
|
@@ -243,6 +243,30 @@ async function shootOne(page, url, width, format, opts) {
|
|
|
243
243
|
// storefront. The settle is best-effort: if the page does go quiet, the shot
|
|
244
244
|
// waits for it; if it never does, the shot happens anyway.
|
|
245
245
|
await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
|
|
246
|
+
// OPEN THE OVERLAY BEFORE MEASURING, or it cannot be photographed at all.
|
|
247
|
+
//
|
|
248
|
+
// A closed drawer is `visibility:hidden` and translated 105% off-screen
|
|
249
|
+
// (`render/nodes/cart-drawer/css.go`), so it measures at x=1461 on a 1440
|
|
250
|
+
// viewport and `page.screenshot({clip})` fails outright: "Clipped area is
|
|
251
|
+
// either empty or outside the resulting image". That made the ONE surface this
|
|
252
|
+
// repo's own guidance insists you look at — "open the cart drawer and look,
|
|
253
|
+
// before calling a site done" — the one surface `sb_look` could not show.
|
|
254
|
+
//
|
|
255
|
+
// `is-open` is the platform's OWN mechanism, not a hack around it: the same
|
|
256
|
+
// class the storefront's cart button toggles, whose rule is `transform:none`
|
|
257
|
+
// plus `visibility:visible`. The scrim takes it too, so the shot matches what
|
|
258
|
+
// a shopper sees rather than a panel floating over bare page.
|
|
259
|
+
if (opts.open) {
|
|
260
|
+
await page.evaluate((id) => {
|
|
261
|
+
// The narrow `document` shim this file declares is for the MEASUREMENT
|
|
262
|
+
// pass; here the real DOM is what runs, so reach it through the cast
|
|
263
|
+
// rather than widening a shim that exists to keep that pass honest.
|
|
264
|
+
const d = document;
|
|
265
|
+
d.getElementById(id)?.classList.add('is-open');
|
|
266
|
+
for (const s of d.querySelectorAll('.wb-cart-scrim'))
|
|
267
|
+
s.classList.add('is-open');
|
|
268
|
+
}, opts.open);
|
|
269
|
+
}
|
|
246
270
|
await settleDom(page);
|
|
247
271
|
await settleLazyImages(page);
|
|
248
272
|
// A RENDERED page carries its node ids as the HTML `id` attribute — not as
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/sbuilder-mcp",
|
|
6
6
|
"type": "module",
|