sbuilder-mcp 0.81.0 → 0.82.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 +19 -0
- package/CHANGELOG.vi.md +19 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/dist/catalog/checkout.generated.js +116 -0
- package/dist/catalog/deadkeys.generated.js +3 -3
- package/dist/catalog/elements.generated.js +4 -0
- package/dist/catalog/source.generated.js +2 -2
- package/dist/catalog/translations.generated.js +3 -1
- package/dist/domains/site/builder.js +3 -1
- package/dist/domains/site/icons.js +59 -5
- package/dist/domains/site/readiness.js +4 -1
- package/dist/tools/api.js +236 -30
- package/dist/tools/live.js +8 -3
- package/dist/transport/http.js +7 -2
- package/dist/transport/media.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ 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.82.0] - 2026-10-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sb_api_call` accepts `merge` on a PUT to deep-merge `body` over the current record before sending, so a PUT that replaces a whole record (settings, orders, payment gateways) keeps the fields the caller omits.
|
|
13
|
+
- `sb_api_call` accepts `file` to send a local file as multipart (for example a product import spreadsheet), and a GET with `file` saves the response bytes to a new path without overwriting an existing file.
|
|
14
|
+
- `sb_api_call` accepts `if_match` and reads the version itself on routes that require `If-Match`, such as the shipping-config PATCH, and a `shipping_config_moved` conflict names the route to re-read.
|
|
15
|
+
- Form fields in the element catalog gain `urlParam` and `hideOnPage` settings.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `sb_api_call` on a PUT without `merge` returns a `replace_warning` listing the fields the write would erase, and its `dry_run` preview shows only the caller's body, never data read from a GET.
|
|
19
|
+
- The shipping readiness check points at the shipping-config route, because the v1 route answers 409 on a site that has saved rules.
|
|
20
|
+
|
|
21
|
+
## [0.81.1] - 2026-10-10
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- `sb_set` and `sb_add` suggest the right icon for a misspelled name, ranking names that contain the typed word first, then close typos by edit distance, and honoring a `Fill` or `Line` suffix the caller wrote.
|
|
25
|
+
- `sb_duplicate` with `to_page_id` reports that `parent_id` is not on the target page and says to omit it or read ids with `sb_outline`.
|
|
26
|
+
- `sb_media_upload` of a font file tells the caller how to create a font family instead of suggesting `sb_set` with `src`.
|
|
27
|
+
|
|
9
28
|
## [0.81.0] - 2026-10-10
|
|
10
29
|
|
|
11
30
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,25 @@ 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.82.0] - 2026-10-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sb_api_call` nhận `merge` trên PUT để trộn sâu `body` lên bản ghi hiện tại trước khi gửi, nên PUT thay cả bản ghi (settings, đơn hàng, cổng thanh toán) giữ lại các field người gọi bỏ qua.
|
|
13
|
+
- `sb_api_call` nhận `file` để gửi file cục bộ dạng multipart (ví dụ bảng tính import sản phẩm), và GET với `file` lưu bytes phản hồi ra đường dẫn mới mà không ghi đè file đã có.
|
|
14
|
+
- `sb_api_call` nhận `if_match` và tự đọc version trên các route yêu cầu `If-Match`, như PATCH shipping-config, và xung đột `shipping_config_moved` chỉ ra route cần đọc lại.
|
|
15
|
+
- Form field trong catalog element có thêm thiết lập `urlParam` và `hideOnPage`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `sb_api_call` trên PUT không có `merge` trả về `replace_warning` liệt kê các field lần ghi sẽ xoá, và bản xem trước `dry_run` chỉ in body của người gọi, không in dữ liệu đọc từ GET.
|
|
19
|
+
- Kiểm tra readiness của shipping trỏ sang route shipping-config, vì route v1 trả 409 trên site đã lưu rules.
|
|
20
|
+
|
|
21
|
+
## [0.81.1] - 2026-10-10
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- `sb_set` và `sb_add` gợi ý đúng icon cho tên gõ sai, ưu tiên tên chứa nguyên từ đã gõ, rồi đến lỗi gõ gần theo khoảng cách sửa, và tôn trọng đuôi `Fill` hoặc `Line` do người gọi viết.
|
|
25
|
+
- `sb_duplicate` với `to_page_id` báo `parent_id` không có trên trang đích và hướng dẫn bỏ qua nó hoặc đọc id bằng `sb_outline`.
|
|
26
|
+
- `sb_media_upload` với file font hướng dẫn cách tạo họ font thay vì gợi ý `sb_set` với `src`.
|
|
27
|
+
|
|
9
28
|
## [0.81.0] - 2026-10-10
|
|
10
29
|
|
|
11
30
|
### Added
|
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ in this client: one would put the very key the platform exists to hold back into
|
|
|
83
83
|
| `sb_connect` | Log in, list the sites this account can operate, report which credentials are present |
|
|
84
84
|
| `sb_site_list` | List the sites this account can operate |
|
|
85
85
|
| `sb_api_find` | Find API operations by intent — one line per match — then read one operation's call sheet by id: real parameter schemas, the credential it needs, and the body's fields read off the handler that decodes them, each carrying the trap its own doc comment records |
|
|
86
|
-
| `sb_api_call` | Execute an operation, or any route by method+path; dry run by default. Field selection and local result pagination |
|
|
86
|
+
| `sb_api_call` | Execute an operation, or any route by method+path; dry run by default. Field selection and local result pagination; `merge` for whole-record PUTs, `If-Match`, multipart `file` |
|
|
87
87
|
| `sb_page_open` | Open a page for editing and return its outline — or, with `form_id`, a form's field document (fields, defaults, dependent-field rules) |
|
|
88
88
|
| `sb_outline` | The open page as a compressed tree — never a raw document dump |
|
|
89
89
|
| `sb_node_read` | One node in full, with a warning if it is a shared global |
|
package/README.vi.md
CHANGED
|
@@ -80,7 +80,7 @@ nhét ngược lại vào mọi bản cài.
|
|
|
80
80
|
| `sb_connect` | Đăng nhập, liệt kê site tài khoản vận hành được, báo đang có credential nào |
|
|
81
81
|
| `sb_site_list` | Liệt kê site tài khoản vận hành được |
|
|
82
82
|
| `sb_api_find` | Tìm operation theo ý định — mỗi kết quả một dòng — rồi đọc call sheet của một operation theo id: schema tham số thật, credential cần dùng, và các trường body đọc thẳng từ handler decode chúng, mỗi trường mang theo cái bẫy doc comment của chính nó ghi lại |
|
|
83
|
-
| `sb_api_call` | Chạy operation, hoặc bất kỳ route nào bằng method+path; mặc định chạy khô. Chọn field và phân trang kết quả phía MCP |
|
|
83
|
+
| `sb_api_call` | Chạy operation, hoặc bất kỳ route nào bằng method+path; mặc định chạy khô. Chọn field và phân trang kết quả phía MCP; `merge` cho PUT thay cả bản ghi, `If-Match`, `file` multipart |
|
|
84
84
|
| `sb_page_open` | Mở một trang để sửa và trả về outline — hoặc, với `form_id`, tài liệu trường của biểu mẫu (trường, giá trị mặc định, quy tắc trường phụ thuộc) |
|
|
85
85
|
| `sb_outline` | Trang đang mở dạng cây nén — không bao giờ dump tài liệu thô |
|
|
86
86
|
| `sb_node_read` | Một node đầy đủ, kèm cảnh báo nếu nó là global dùng chung |
|
|
@@ -114,6 +114,8 @@ export const CHECKOUT_FORM_DOCUMENT = {
|
|
|
114
114
|
"showLabel": true,
|
|
115
115
|
"mapTo": "customer.fullName",
|
|
116
116
|
"prefillValue": "",
|
|
117
|
+
"urlParam": "",
|
|
118
|
+
"hideOnPage": false,
|
|
117
119
|
"multiline": false,
|
|
118
120
|
"inputType": "text",
|
|
119
121
|
"limitChars": false,
|
|
@@ -154,6 +156,8 @@ export const CHECKOUT_FORM_DOCUMENT = {
|
|
|
154
156
|
"showLabel": true,
|
|
155
157
|
"mapTo": "customer.phone",
|
|
156
158
|
"prefillValue": "",
|
|
159
|
+
"urlParam": "",
|
|
160
|
+
"hideOnPage": false,
|
|
157
161
|
"multiline": false,
|
|
158
162
|
"inputType": "tel",
|
|
159
163
|
"limitChars": false,
|
|
@@ -194,6 +198,8 @@ export const CHECKOUT_FORM_DOCUMENT = {
|
|
|
194
198
|
"showLabel": true,
|
|
195
199
|
"mapTo": "customer.email",
|
|
196
200
|
"prefillValue": "",
|
|
201
|
+
"urlParam": "",
|
|
202
|
+
"hideOnPage": false,
|
|
197
203
|
"multiline": false,
|
|
198
204
|
"inputType": "email",
|
|
199
205
|
"limitChars": false,
|
|
@@ -394,6 +400,8 @@ export const CHECKOUT_FORM_DOCUMENT = {
|
|
|
394
400
|
"showLabel": true,
|
|
395
401
|
"mapTo": "order.discountCode",
|
|
396
402
|
"prefillValue": "",
|
|
403
|
+
"urlParam": "",
|
|
404
|
+
"hideOnPage": false,
|
|
397
405
|
"multiline": false,
|
|
398
406
|
"inputType": "text",
|
|
399
407
|
"limitChars": false,
|
|
@@ -465,6 +473,8 @@ export const CHECKOUT_FORM_DOCUMENT = {
|
|
|
465
473
|
"showLabel": true,
|
|
466
474
|
"mapTo": "order.note",
|
|
467
475
|
"prefillValue": "",
|
|
476
|
+
"urlParam": "",
|
|
477
|
+
"hideOnPage": false,
|
|
468
478
|
"multiline": true,
|
|
469
479
|
"inputType": "text",
|
|
470
480
|
"limitChars": false,
|
|
@@ -946,6 +956,8 @@ export const FORM_TEMPLATES = {
|
|
|
946
956
|
"showLabel": true,
|
|
947
957
|
"mapTo": "customer.fullName",
|
|
948
958
|
"prefillValue": "",
|
|
959
|
+
"urlParam": "",
|
|
960
|
+
"hideOnPage": false,
|
|
949
961
|
"multiline": false,
|
|
950
962
|
"inputType": "text",
|
|
951
963
|
"limitChars": false,
|
|
@@ -986,6 +998,8 @@ export const FORM_TEMPLATES = {
|
|
|
986
998
|
"showLabel": true,
|
|
987
999
|
"mapTo": "customer.email",
|
|
988
1000
|
"prefillValue": "",
|
|
1001
|
+
"urlParam": "",
|
|
1002
|
+
"hideOnPage": false,
|
|
989
1003
|
"multiline": false,
|
|
990
1004
|
"inputType": "email",
|
|
991
1005
|
"limitChars": false,
|
|
@@ -1026,6 +1040,8 @@ export const FORM_TEMPLATES = {
|
|
|
1026
1040
|
"showLabel": true,
|
|
1027
1041
|
"mapTo": "customer.phone",
|
|
1028
1042
|
"prefillValue": "",
|
|
1043
|
+
"urlParam": "",
|
|
1044
|
+
"hideOnPage": false,
|
|
1029
1045
|
"multiline": false,
|
|
1030
1046
|
"inputType": "tel",
|
|
1031
1047
|
"limitChars": false,
|
|
@@ -1066,6 +1082,8 @@ export const FORM_TEMPLATES = {
|
|
|
1066
1082
|
"showLabel": true,
|
|
1067
1083
|
"mapTo": "customer.notes",
|
|
1068
1084
|
"prefillValue": "",
|
|
1085
|
+
"urlParam": "",
|
|
1086
|
+
"hideOnPage": false,
|
|
1069
1087
|
"multiline": true,
|
|
1070
1088
|
"inputType": "text",
|
|
1071
1089
|
"limitChars": false,
|
|
@@ -1238,6 +1256,8 @@ export const FORM_TEMPLATES = {
|
|
|
1238
1256
|
"showLabel": false,
|
|
1239
1257
|
"mapTo": "customer.email",
|
|
1240
1258
|
"prefillValue": "",
|
|
1259
|
+
"urlParam": "",
|
|
1260
|
+
"hideOnPage": false,
|
|
1241
1261
|
"multiline": false,
|
|
1242
1262
|
"inputType": "email",
|
|
1243
1263
|
"limitChars": false,
|
|
@@ -1389,6 +1409,8 @@ export const FORM_TEMPLATES = {
|
|
|
1389
1409
|
"showLabel": true,
|
|
1390
1410
|
"mapTo": "customer.fullName",
|
|
1391
1411
|
"prefillValue": "",
|
|
1412
|
+
"urlParam": "",
|
|
1413
|
+
"hideOnPage": false,
|
|
1392
1414
|
"multiline": false,
|
|
1393
1415
|
"inputType": "text",
|
|
1394
1416
|
"limitChars": false,
|
|
@@ -1429,6 +1451,8 @@ export const FORM_TEMPLATES = {
|
|
|
1429
1451
|
"showLabel": true,
|
|
1430
1452
|
"mapTo": "customer.phone",
|
|
1431
1453
|
"prefillValue": "",
|
|
1454
|
+
"urlParam": "",
|
|
1455
|
+
"hideOnPage": false,
|
|
1432
1456
|
"multiline": false,
|
|
1433
1457
|
"inputType": "tel",
|
|
1434
1458
|
"limitChars": false,
|
|
@@ -1598,6 +1622,8 @@ export const FORM_TEMPLATES = {
|
|
|
1598
1622
|
"showLabel": true,
|
|
1599
1623
|
"mapTo": "order.quantity",
|
|
1600
1624
|
"prefillValue": "",
|
|
1625
|
+
"urlParam": "",
|
|
1626
|
+
"hideOnPage": false,
|
|
1601
1627
|
"setRange": true,
|
|
1602
1628
|
"minValue": 1,
|
|
1603
1629
|
"maxValue": 500,
|
|
@@ -1634,6 +1660,8 @@ export const FORM_TEMPLATES = {
|
|
|
1634
1660
|
"showLabel": true,
|
|
1635
1661
|
"mapTo": "order.note",
|
|
1636
1662
|
"prefillValue": "",
|
|
1663
|
+
"urlParam": "",
|
|
1664
|
+
"hideOnPage": false,
|
|
1637
1665
|
"multiline": true,
|
|
1638
1666
|
"inputType": "text",
|
|
1639
1667
|
"limitChars": false,
|
|
@@ -1790,6 +1818,8 @@ export const FORM_TEMPLATES = {
|
|
|
1790
1818
|
"showLabel": true,
|
|
1791
1819
|
"mapTo": "customer.fullName",
|
|
1792
1820
|
"prefillValue": "",
|
|
1821
|
+
"urlParam": "",
|
|
1822
|
+
"hideOnPage": false,
|
|
1793
1823
|
"multiline": false,
|
|
1794
1824
|
"inputType": "text",
|
|
1795
1825
|
"limitChars": false,
|
|
@@ -1830,6 +1860,8 @@ export const FORM_TEMPLATES = {
|
|
|
1830
1860
|
"showLabel": true,
|
|
1831
1861
|
"mapTo": "customer.phone",
|
|
1832
1862
|
"prefillValue": "",
|
|
1863
|
+
"urlParam": "",
|
|
1864
|
+
"hideOnPage": false,
|
|
1833
1865
|
"multiline": false,
|
|
1834
1866
|
"inputType": "tel",
|
|
1835
1867
|
"limitChars": false,
|
|
@@ -1870,6 +1902,8 @@ export const FORM_TEMPLATES = {
|
|
|
1870
1902
|
"showLabel": true,
|
|
1871
1903
|
"mapTo": "customer.email",
|
|
1872
1904
|
"prefillValue": "",
|
|
1905
|
+
"urlParam": "",
|
|
1906
|
+
"hideOnPage": false,
|
|
1873
1907
|
"multiline": false,
|
|
1874
1908
|
"inputType": "email",
|
|
1875
1909
|
"limitChars": false,
|
|
@@ -2070,6 +2104,8 @@ export const FORM_TEMPLATES = {
|
|
|
2070
2104
|
"showLabel": true,
|
|
2071
2105
|
"mapTo": "order.discountCode",
|
|
2072
2106
|
"prefillValue": "",
|
|
2107
|
+
"urlParam": "",
|
|
2108
|
+
"hideOnPage": false,
|
|
2073
2109
|
"multiline": false,
|
|
2074
2110
|
"inputType": "text",
|
|
2075
2111
|
"limitChars": false,
|
|
@@ -2141,6 +2177,8 @@ export const FORM_TEMPLATES = {
|
|
|
2141
2177
|
"showLabel": true,
|
|
2142
2178
|
"mapTo": "order.note",
|
|
2143
2179
|
"prefillValue": "",
|
|
2180
|
+
"urlParam": "",
|
|
2181
|
+
"hideOnPage": false,
|
|
2144
2182
|
"multiline": true,
|
|
2145
2183
|
"inputType": "text",
|
|
2146
2184
|
"limitChars": false,
|
|
@@ -2300,6 +2338,8 @@ export const FORM_TEMPLATES = {
|
|
|
2300
2338
|
"showLabel": true,
|
|
2301
2339
|
"mapTo": "customer.fullName",
|
|
2302
2340
|
"prefillValue": "",
|
|
2341
|
+
"urlParam": "",
|
|
2342
|
+
"hideOnPage": false,
|
|
2303
2343
|
"multiline": false,
|
|
2304
2344
|
"inputType": "text",
|
|
2305
2345
|
"limitChars": false,
|
|
@@ -2340,6 +2380,8 @@ export const FORM_TEMPLATES = {
|
|
|
2340
2380
|
"showLabel": true,
|
|
2341
2381
|
"mapTo": "customer.phone",
|
|
2342
2382
|
"prefillValue": "",
|
|
2383
|
+
"urlParam": "",
|
|
2384
|
+
"hideOnPage": false,
|
|
2343
2385
|
"multiline": false,
|
|
2344
2386
|
"inputType": "tel",
|
|
2345
2387
|
"limitChars": false,
|
|
@@ -3064,6 +3106,8 @@ export const FORM_TEMPLATES = {
|
|
|
3064
3106
|
"showLabel": true,
|
|
3065
3107
|
"mapTo": "customer.fullName",
|
|
3066
3108
|
"prefillValue": "",
|
|
3109
|
+
"urlParam": "",
|
|
3110
|
+
"hideOnPage": false,
|
|
3067
3111
|
"multiline": false,
|
|
3068
3112
|
"inputType": "text",
|
|
3069
3113
|
"limitChars": false,
|
|
@@ -3104,6 +3148,8 @@ export const FORM_TEMPLATES = {
|
|
|
3104
3148
|
"showLabel": true,
|
|
3105
3149
|
"mapTo": "customer.phone",
|
|
3106
3150
|
"prefillValue": "",
|
|
3151
|
+
"urlParam": "",
|
|
3152
|
+
"hideOnPage": false,
|
|
3107
3153
|
"multiline": false,
|
|
3108
3154
|
"inputType": "tel",
|
|
3109
3155
|
"limitChars": false,
|
|
@@ -3144,6 +3190,8 @@ export const FORM_TEMPLATES = {
|
|
|
3144
3190
|
"showLabel": true,
|
|
3145
3191
|
"mapTo": "customer.notes",
|
|
3146
3192
|
"prefillValue": "",
|
|
3193
|
+
"urlParam": "",
|
|
3194
|
+
"hideOnPage": false,
|
|
3147
3195
|
"multiline": true,
|
|
3148
3196
|
"inputType": "text",
|
|
3149
3197
|
"limitChars": false,
|
|
@@ -3293,6 +3341,8 @@ export const FORM_TEMPLATES = {
|
|
|
3293
3341
|
"showLabel": true,
|
|
3294
3342
|
"mapTo": "customer.fullName",
|
|
3295
3343
|
"prefillValue": "",
|
|
3344
|
+
"urlParam": "",
|
|
3345
|
+
"hideOnPage": false,
|
|
3296
3346
|
"multiline": false,
|
|
3297
3347
|
"inputType": "text",
|
|
3298
3348
|
"limitChars": false,
|
|
@@ -3333,6 +3383,8 @@ export const FORM_TEMPLATES = {
|
|
|
3333
3383
|
"showLabel": true,
|
|
3334
3384
|
"mapTo": "customer.phone",
|
|
3335
3385
|
"prefillValue": "",
|
|
3386
|
+
"urlParam": "",
|
|
3387
|
+
"hideOnPage": false,
|
|
3336
3388
|
"multiline": false,
|
|
3337
3389
|
"inputType": "tel",
|
|
3338
3390
|
"limitChars": false,
|
|
@@ -3441,6 +3493,8 @@ export const FORM_TEMPLATES = {
|
|
|
3441
3493
|
"showLabel": true,
|
|
3442
3494
|
"mapTo": "customer.notes",
|
|
3443
3495
|
"prefillValue": "",
|
|
3496
|
+
"urlParam": "",
|
|
3497
|
+
"hideOnPage": false,
|
|
3444
3498
|
"multiline": true,
|
|
3445
3499
|
"inputType": "text",
|
|
3446
3500
|
"limitChars": false,
|
|
@@ -3592,6 +3646,8 @@ export const FORM_TEMPLATES = {
|
|
|
3592
3646
|
"showLabel": true,
|
|
3593
3647
|
"mapTo": "customer.fullName",
|
|
3594
3648
|
"prefillValue": "",
|
|
3649
|
+
"urlParam": "",
|
|
3650
|
+
"hideOnPage": false,
|
|
3595
3651
|
"multiline": false,
|
|
3596
3652
|
"inputType": "text",
|
|
3597
3653
|
"limitChars": false,
|
|
@@ -3632,6 +3688,8 @@ export const FORM_TEMPLATES = {
|
|
|
3632
3688
|
"showLabel": true,
|
|
3633
3689
|
"mapTo": "customer.phone",
|
|
3634
3690
|
"prefillValue": "",
|
|
3691
|
+
"urlParam": "",
|
|
3692
|
+
"hideOnPage": false,
|
|
3635
3693
|
"multiline": false,
|
|
3636
3694
|
"inputType": "tel",
|
|
3637
3695
|
"limitChars": false,
|
|
@@ -3777,6 +3835,8 @@ export const FORM_TEMPLATES = {
|
|
|
3777
3835
|
"showLabel": true,
|
|
3778
3836
|
"mapTo": "",
|
|
3779
3837
|
"prefillValue": "",
|
|
3838
|
+
"urlParam": "",
|
|
3839
|
+
"hideOnPage": false,
|
|
3780
3840
|
"setRange": true,
|
|
3781
3841
|
"minValue": 1,
|
|
3782
3842
|
"maxValue": 10,
|
|
@@ -3813,6 +3873,8 @@ export const FORM_TEMPLATES = {
|
|
|
3813
3873
|
"showLabel": true,
|
|
3814
3874
|
"mapTo": "customer.notes",
|
|
3815
3875
|
"prefillValue": "",
|
|
3876
|
+
"urlParam": "",
|
|
3877
|
+
"hideOnPage": false,
|
|
3816
3878
|
"multiline": true,
|
|
3817
3879
|
"inputType": "text",
|
|
3818
3880
|
"limitChars": false,
|
|
@@ -3996,6 +4058,8 @@ export const FORM_TEMPLATES = {
|
|
|
3996
4058
|
"showLabel": true,
|
|
3997
4059
|
"mapTo": "customer.notes",
|
|
3998
4060
|
"prefillValue": "",
|
|
4061
|
+
"urlParam": "",
|
|
4062
|
+
"hideOnPage": false,
|
|
3999
4063
|
"multiline": true,
|
|
4000
4064
|
"inputType": "text",
|
|
4001
4065
|
"limitChars": false,
|
|
@@ -4036,6 +4100,8 @@ export const FORM_TEMPLATES = {
|
|
|
4036
4100
|
"showLabel": true,
|
|
4037
4101
|
"mapTo": "customer.email",
|
|
4038
4102
|
"prefillValue": "",
|
|
4103
|
+
"urlParam": "",
|
|
4104
|
+
"hideOnPage": false,
|
|
4039
4105
|
"multiline": false,
|
|
4040
4106
|
"inputType": "email",
|
|
4041
4107
|
"limitChars": false,
|
|
@@ -4184,6 +4250,8 @@ export const FORM_TEMPLATES = {
|
|
|
4184
4250
|
"showLabel": true,
|
|
4185
4251
|
"mapTo": "customer.fullName",
|
|
4186
4252
|
"prefillValue": "",
|
|
4253
|
+
"urlParam": "",
|
|
4254
|
+
"hideOnPage": false,
|
|
4187
4255
|
"multiline": false,
|
|
4188
4256
|
"inputType": "text",
|
|
4189
4257
|
"limitChars": false,
|
|
@@ -4224,6 +4292,8 @@ export const FORM_TEMPLATES = {
|
|
|
4224
4292
|
"showLabel": true,
|
|
4225
4293
|
"mapTo": "customer.phone",
|
|
4226
4294
|
"prefillValue": "",
|
|
4295
|
+
"urlParam": "",
|
|
4296
|
+
"hideOnPage": false,
|
|
4227
4297
|
"multiline": false,
|
|
4228
4298
|
"inputType": "tel",
|
|
4229
4299
|
"limitChars": false,
|
|
@@ -4264,6 +4334,8 @@ export const FORM_TEMPLATES = {
|
|
|
4264
4334
|
"showLabel": true,
|
|
4265
4335
|
"mapTo": "customer.email",
|
|
4266
4336
|
"prefillValue": "",
|
|
4337
|
+
"urlParam": "",
|
|
4338
|
+
"hideOnPage": false,
|
|
4267
4339
|
"multiline": false,
|
|
4268
4340
|
"inputType": "email",
|
|
4269
4341
|
"limitChars": false,
|
|
@@ -4304,6 +4376,8 @@ export const FORM_TEMPLATES = {
|
|
|
4304
4376
|
"showLabel": true,
|
|
4305
4377
|
"mapTo": "",
|
|
4306
4378
|
"prefillValue": "",
|
|
4379
|
+
"urlParam": "",
|
|
4380
|
+
"hideOnPage": false,
|
|
4307
4381
|
"setRange": true,
|
|
4308
4382
|
"minValue": 1,
|
|
4309
4383
|
"maxValue": 20,
|
|
@@ -4448,6 +4522,8 @@ export const FORM_TEMPLATES = {
|
|
|
4448
4522
|
"showLabel": true,
|
|
4449
4523
|
"mapTo": "customer.fullName",
|
|
4450
4524
|
"prefillValue": "",
|
|
4525
|
+
"urlParam": "",
|
|
4526
|
+
"hideOnPage": false,
|
|
4451
4527
|
"multiline": false,
|
|
4452
4528
|
"inputType": "text",
|
|
4453
4529
|
"limitChars": false,
|
|
@@ -4488,6 +4564,8 @@ export const FORM_TEMPLATES = {
|
|
|
4488
4564
|
"showLabel": true,
|
|
4489
4565
|
"mapTo": "customer.phone",
|
|
4490
4566
|
"prefillValue": "",
|
|
4567
|
+
"urlParam": "",
|
|
4568
|
+
"hideOnPage": false,
|
|
4491
4569
|
"multiline": false,
|
|
4492
4570
|
"inputType": "tel",
|
|
4493
4571
|
"limitChars": false,
|
|
@@ -4528,6 +4606,8 @@ export const FORM_TEMPLATES = {
|
|
|
4528
4606
|
"showLabel": true,
|
|
4529
4607
|
"mapTo": "customer.email",
|
|
4530
4608
|
"prefillValue": "",
|
|
4609
|
+
"urlParam": "",
|
|
4610
|
+
"hideOnPage": false,
|
|
4531
4611
|
"multiline": false,
|
|
4532
4612
|
"inputType": "email",
|
|
4533
4613
|
"limitChars": false,
|
|
@@ -4568,6 +4648,8 @@ export const FORM_TEMPLATES = {
|
|
|
4568
4648
|
"showLabel": true,
|
|
4569
4649
|
"mapTo": "customer.notes",
|
|
4570
4650
|
"prefillValue": "",
|
|
4651
|
+
"urlParam": "",
|
|
4652
|
+
"hideOnPage": false,
|
|
4571
4653
|
"multiline": true,
|
|
4572
4654
|
"inputType": "text",
|
|
4573
4655
|
"limitChars": false,
|
|
@@ -4717,6 +4799,8 @@ export const FORM_TEMPLATES = {
|
|
|
4717
4799
|
"showLabel": true,
|
|
4718
4800
|
"mapTo": "",
|
|
4719
4801
|
"prefillValue": "",
|
|
4802
|
+
"urlParam": "",
|
|
4803
|
+
"hideOnPage": false,
|
|
4720
4804
|
"multiline": false,
|
|
4721
4805
|
"inputType": "text",
|
|
4722
4806
|
"limitChars": false,
|
|
@@ -4757,6 +4841,8 @@ export const FORM_TEMPLATES = {
|
|
|
4757
4841
|
"showLabel": true,
|
|
4758
4842
|
"mapTo": "",
|
|
4759
4843
|
"prefillValue": "",
|
|
4844
|
+
"urlParam": "",
|
|
4845
|
+
"hideOnPage": false,
|
|
4760
4846
|
"multiline": false,
|
|
4761
4847
|
"inputType": "tel",
|
|
4762
4848
|
"limitChars": false,
|
|
@@ -4797,6 +4883,8 @@ export const FORM_TEMPLATES = {
|
|
|
4797
4883
|
"showLabel": true,
|
|
4798
4884
|
"mapTo": "",
|
|
4799
4885
|
"prefillValue": "",
|
|
4886
|
+
"urlParam": "",
|
|
4887
|
+
"hideOnPage": false,
|
|
4800
4888
|
"multiline": false,
|
|
4801
4889
|
"inputType": "email",
|
|
4802
4890
|
"limitChars": false,
|
|
@@ -4837,6 +4925,8 @@ export const FORM_TEMPLATES = {
|
|
|
4837
4925
|
"showLabel": true,
|
|
4838
4926
|
"mapTo": "",
|
|
4839
4927
|
"prefillValue": "",
|
|
4928
|
+
"urlParam": "",
|
|
4929
|
+
"hideOnPage": false,
|
|
4840
4930
|
"multiline": true,
|
|
4841
4931
|
"inputType": "text",
|
|
4842
4932
|
"limitChars": false,
|
|
@@ -5016,6 +5106,8 @@ export const FORM_TEMPLATES = {
|
|
|
5016
5106
|
"showLabel": true,
|
|
5017
5107
|
"mapTo": "customer.email",
|
|
5018
5108
|
"prefillValue": "",
|
|
5109
|
+
"urlParam": "",
|
|
5110
|
+
"hideOnPage": false,
|
|
5019
5111
|
"multiline": false,
|
|
5020
5112
|
"inputType": "email",
|
|
5021
5113
|
"limitChars": false,
|
|
@@ -5056,6 +5148,8 @@ export const FORM_TEMPLATES = {
|
|
|
5056
5148
|
"showLabel": true,
|
|
5057
5149
|
"mapTo": "",
|
|
5058
5150
|
"prefillValue": "",
|
|
5151
|
+
"urlParam": "",
|
|
5152
|
+
"hideOnPage": false,
|
|
5059
5153
|
"multiline": false,
|
|
5060
5154
|
"inputType": "password",
|
|
5061
5155
|
"limitChars": false,
|
|
@@ -5263,6 +5357,8 @@ export const FORM_TEMPLATES = {
|
|
|
5263
5357
|
"showLabel": true,
|
|
5264
5358
|
"mapTo": "customer.fullName",
|
|
5265
5359
|
"prefillValue": "",
|
|
5360
|
+
"urlParam": "",
|
|
5361
|
+
"hideOnPage": false,
|
|
5266
5362
|
"multiline": false,
|
|
5267
5363
|
"inputType": "text",
|
|
5268
5364
|
"limitChars": false,
|
|
@@ -5303,6 +5399,8 @@ export const FORM_TEMPLATES = {
|
|
|
5303
5399
|
"showLabel": true,
|
|
5304
5400
|
"mapTo": "customer.email",
|
|
5305
5401
|
"prefillValue": "",
|
|
5402
|
+
"urlParam": "",
|
|
5403
|
+
"hideOnPage": false,
|
|
5306
5404
|
"multiline": false,
|
|
5307
5405
|
"inputType": "email",
|
|
5308
5406
|
"limitChars": false,
|
|
@@ -5343,6 +5441,8 @@ export const FORM_TEMPLATES = {
|
|
|
5343
5441
|
"showLabel": true,
|
|
5344
5442
|
"mapTo": "",
|
|
5345
5443
|
"prefillValue": "",
|
|
5444
|
+
"urlParam": "",
|
|
5445
|
+
"hideOnPage": false,
|
|
5346
5446
|
"multiline": false,
|
|
5347
5447
|
"inputType": "password",
|
|
5348
5448
|
"limitChars": false,
|
|
@@ -5383,6 +5483,8 @@ export const FORM_TEMPLATES = {
|
|
|
5383
5483
|
"showLabel": true,
|
|
5384
5484
|
"mapTo": "",
|
|
5385
5485
|
"prefillValue": "",
|
|
5486
|
+
"urlParam": "",
|
|
5487
|
+
"hideOnPage": false,
|
|
5386
5488
|
"multiline": false,
|
|
5387
5489
|
"inputType": "password",
|
|
5388
5490
|
"limitChars": false,
|
|
@@ -5558,6 +5660,8 @@ export const FORM_TEMPLATES = {
|
|
|
5558
5660
|
"showLabel": true,
|
|
5559
5661
|
"mapTo": "customer.email",
|
|
5560
5662
|
"prefillValue": "",
|
|
5663
|
+
"urlParam": "",
|
|
5664
|
+
"hideOnPage": false,
|
|
5561
5665
|
"multiline": false,
|
|
5562
5666
|
"inputType": "email",
|
|
5563
5667
|
"limitChars": false,
|
|
@@ -5733,6 +5837,8 @@ export const FORM_TEMPLATES = {
|
|
|
5733
5837
|
"showLabel": true,
|
|
5734
5838
|
"mapTo": "customer.email",
|
|
5735
5839
|
"prefillValue": "",
|
|
5840
|
+
"urlParam": "",
|
|
5841
|
+
"hideOnPage": false,
|
|
5736
5842
|
"multiline": false,
|
|
5737
5843
|
"inputType": "email",
|
|
5738
5844
|
"limitChars": false,
|
|
@@ -5773,6 +5879,8 @@ export const FORM_TEMPLATES = {
|
|
|
5773
5879
|
"showLabel": true,
|
|
5774
5880
|
"mapTo": "",
|
|
5775
5881
|
"prefillValue": "",
|
|
5882
|
+
"urlParam": "",
|
|
5883
|
+
"hideOnPage": false,
|
|
5776
5884
|
"multiline": false,
|
|
5777
5885
|
"inputType": "text",
|
|
5778
5886
|
"limitChars": false,
|
|
@@ -5921,6 +6029,8 @@ export const FORM_TEMPLATES = {
|
|
|
5921
6029
|
"showLabel": true,
|
|
5922
6030
|
"mapTo": "customer.email",
|
|
5923
6031
|
"prefillValue": "",
|
|
6032
|
+
"urlParam": "",
|
|
6033
|
+
"hideOnPage": false,
|
|
5924
6034
|
"multiline": false,
|
|
5925
6035
|
"inputType": "email",
|
|
5926
6036
|
"limitChars": false,
|
|
@@ -5961,6 +6071,8 @@ export const FORM_TEMPLATES = {
|
|
|
5961
6071
|
"showLabel": true,
|
|
5962
6072
|
"mapTo": "",
|
|
5963
6073
|
"prefillValue": "",
|
|
6074
|
+
"urlParam": "",
|
|
6075
|
+
"hideOnPage": false,
|
|
5964
6076
|
"multiline": false,
|
|
5965
6077
|
"inputType": "text",
|
|
5966
6078
|
"limitChars": false,
|
|
@@ -6001,6 +6113,8 @@ export const FORM_TEMPLATES = {
|
|
|
6001
6113
|
"showLabel": true,
|
|
6002
6114
|
"mapTo": "",
|
|
6003
6115
|
"prefillValue": "",
|
|
6116
|
+
"urlParam": "",
|
|
6117
|
+
"hideOnPage": false,
|
|
6004
6118
|
"multiline": false,
|
|
6005
6119
|
"inputType": "password",
|
|
6006
6120
|
"limitChars": false,
|
|
@@ -6041,6 +6155,8 @@ export const FORM_TEMPLATES = {
|
|
|
6041
6155
|
"showLabel": true,
|
|
6042
6156
|
"mapTo": "",
|
|
6043
6157
|
"prefillValue": "",
|
|
6158
|
+
"urlParam": "",
|
|
6159
|
+
"hideOnPage": false,
|
|
6044
6160
|
"multiline": false,
|
|
6045
6161
|
"inputType": "password",
|
|
6046
6162
|
"limitChars": false,
|
|
@@ -7123,6 +7123,8 @@ export const ELEMENTS = {
|
|
|
7123
7123
|
"showLabel": true,
|
|
7124
7124
|
"mapTo": "",
|
|
7125
7125
|
"prefillValue": "",
|
|
7126
|
+
"urlParam": "",
|
|
7127
|
+
"hideOnPage": false,
|
|
7126
7128
|
"multiline": false,
|
|
7127
7129
|
"inputType": "text",
|
|
7128
7130
|
"limitChars": false,
|
|
@@ -7668,6 +7670,8 @@ export const ELEMENTS = {
|
|
|
7668
7670
|
"showLabel": true,
|
|
7669
7671
|
"mapTo": "",
|
|
7670
7672
|
"prefillValue": "",
|
|
7673
|
+
"urlParam": "",
|
|
7674
|
+
"hideOnPage": false,
|
|
7671
7675
|
"setRange": false,
|
|
7672
7676
|
"minValue": 1,
|
|
7673
7677
|
"maxValue": 500,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-catalog.ts — do not edit by hand.
|
|
2
2
|
// The platform commit every other generated file here describes.
|
|
3
3
|
export const PLATFORM_SOURCE = {
|
|
4
|
-
"commit": "
|
|
5
|
-
"committedAt": "2026-10-
|
|
4
|
+
"commit": "045c6f78414bfeec8924263a391a6aa8f989e565",
|
|
5
|
+
"committedAt": "2026-10-10T18:23:19+07:00",
|
|
6
6
|
"dirty": false
|
|
7
7
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export const TRANSLATION_SOURCE = {
|
|
4
4
|
"elements": 67,
|
|
5
5
|
"pairs": 187,
|
|
6
|
-
"neverKeys":
|
|
6
|
+
"neverKeys": 180,
|
|
7
7
|
"entityTypes": 11
|
|
8
8
|
};
|
|
9
9
|
/** Every entity type a translation record can name, including "node". */
|
|
@@ -429,6 +429,7 @@ export const NEVER_TRANSLATED = [
|
|
|
429
429
|
"formId",
|
|
430
430
|
"formRules",
|
|
431
431
|
"format",
|
|
432
|
+
"hideOnPage",
|
|
432
433
|
"hideWhenEmpty",
|
|
433
434
|
"hoverHostDepth",
|
|
434
435
|
"href",
|
|
@@ -533,6 +534,7 @@ export const NEVER_TRANSLATED = [
|
|
|
533
534
|
"target",
|
|
534
535
|
"thresholdCents",
|
|
535
536
|
"trackUrl",
|
|
537
|
+
"urlParam",
|
|
536
538
|
"variant",
|
|
537
539
|
"videoId",
|
|
538
540
|
"videoSrc"
|
|
@@ -564,7 +564,9 @@ export function copyNodeInto(src, id, target, parentId, index, guard) {
|
|
|
564
564
|
'again through its app.');
|
|
565
565
|
}
|
|
566
566
|
if (!parentId || !target.has(parentId)) {
|
|
567
|
-
throw new Error(
|
|
567
|
+
throw new Error(src === target
|
|
568
|
+
? `sbuilder: ${id} has no parent to be duplicated beside`
|
|
569
|
+
: `sbuilder: parent "${parentId}" is not on the target page — omit parent_id to paste at the page's end, or read its ids with sb_outline.`);
|
|
568
570
|
}
|
|
569
571
|
if (src !== target) {
|
|
570
572
|
// Onto ANOTHER document the parent is the caller's choice, so it gets the
|
|
@@ -27,10 +27,10 @@ let index = null;
|
|
|
27
27
|
* Nothing found → retried on the query's longest word, so "ShoppingBasketIcon"
|
|
28
28
|
* still lands near the basket.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Substring ranking only — a SEARCH is a word someone chose. A misspelled NAME is
|
|
31
|
+
* `nearestIcons`' job, which tries edit distance first.
|
|
32
32
|
*/
|
|
33
|
-
export function searchIcons(query, cap = 30) {
|
|
33
|
+
export function searchIcons(query, cap = 30, retryOnWord = true) {
|
|
34
34
|
index ??= [...ICON_NAMES].map((n) => [n, stemOf(n)]);
|
|
35
35
|
const rank = (q) => !q
|
|
36
36
|
? []
|
|
@@ -40,7 +40,7 @@ export function searchIcons(query, cap = 30) {
|
|
|
40
40
|
.sort((a, b) => b.r - a.r || a.s.length - b.s.length || a.n.localeCompare(b.n))
|
|
41
41
|
.map((x) => x.n);
|
|
42
42
|
let hits = rank(stemOf(query));
|
|
43
|
-
if (!hits.length) {
|
|
43
|
+
if (!hits.length && retryOnWord) {
|
|
44
44
|
const words = query.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9]+/);
|
|
45
45
|
const longest = words.filter((w) => w.length > 2).sort((a, b) => b.length - a.length)[0];
|
|
46
46
|
if (longest)
|
|
@@ -48,8 +48,62 @@ export function searchIcons(query, cap = 30) {
|
|
|
48
48
|
}
|
|
49
49
|
return hits.slice(0, cap);
|
|
50
50
|
}
|
|
51
|
+
/** Edit distance, capped: stops early past `max` (a name's stem is short). */
|
|
52
|
+
function distance(a, b, max) {
|
|
53
|
+
if (Math.abs(a.length - b.length) > max)
|
|
54
|
+
return max + 1;
|
|
55
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
56
|
+
for (let i = 1; i <= a.length; i++) {
|
|
57
|
+
const cur = [i];
|
|
58
|
+
let best = i;
|
|
59
|
+
for (let j = 1; j <= b.length; j++) {
|
|
60
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
61
|
+
best = Math.min(best, cur[j]);
|
|
62
|
+
}
|
|
63
|
+
if (best > max)
|
|
64
|
+
return max + 1;
|
|
65
|
+
prev = cur;
|
|
66
|
+
}
|
|
67
|
+
return prev[b.length];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The names a WRONG value most likely meant. A typo first ("ShoppingCartt" is
|
|
71
|
+
* one edit from ShoppingCart, while a word search on "shopping" ranks the shorter
|
|
72
|
+
* ShoppingBag ahead of it — measured on a live platform), then the search.
|
|
73
|
+
* Fill before Line on a tie: the platform's own default (StarFill) is a Fill.
|
|
74
|
+
*/
|
|
51
75
|
export function nearestIcons(query) {
|
|
52
|
-
|
|
76
|
+
index ??= [...ICON_NAMES].map((n) => [n, stemOf(n)]);
|
|
77
|
+
const q = stemOf(query);
|
|
78
|
+
const max = Math.max(1, Math.floor(q.length / 4));
|
|
79
|
+
// The suffix the caller wrote wins a tie; otherwise Fill, the default's own.
|
|
80
|
+
const want = /line$/i.test(norm(query)) ? 'Line' : 'Fill';
|
|
81
|
+
const typos = q
|
|
82
|
+
? index
|
|
83
|
+
.map(([n, s]) => ({ n, s, d: distance(q, s, max) }))
|
|
84
|
+
.filter((x) => x.d <= max)
|
|
85
|
+
.sort((a, b) => a.d - b.d ||
|
|
86
|
+
// ShoppingCart over ShoppingCart2 for "ShoppingCartt": a stem the query
|
|
87
|
+
// extends is a closer guess than a sibling the same distance away.
|
|
88
|
+
Number(q.startsWith(b.s) || b.s.startsWith(q)) - Number(q.startsWith(a.s) || a.s.startsWith(q)) ||
|
|
89
|
+
Number(b.n.endsWith(want)) - Number(a.n.endsWith(want)) ||
|
|
90
|
+
a.s.length - b.s.length ||
|
|
91
|
+
a.n.localeCompare(b.n))
|
|
92
|
+
.map((x) => x.n)
|
|
93
|
+
: [];
|
|
94
|
+
// A value that is a WHOLE WORD of real names ("cart" in Shopping·Cart, "user")
|
|
95
|
+
// means those names, not Car/Cast one edit away; a fragment inside another word
|
|
96
|
+
// ("hart" in c·hart) is more likely a typo (Heart). So: word hits, then typos,
|
|
97
|
+
// then whatever else the search finds.
|
|
98
|
+
const wordsOf = (n) => n.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(' ');
|
|
99
|
+
const words = q
|
|
100
|
+
? index
|
|
101
|
+
.filter(([n, st]) => st.startsWith(q) || wordsOf(n).includes(q))
|
|
102
|
+
.sort(([an, as], [bn, bs]) => as.length - bs.length || Number(bn.endsWith(want)) - Number(an.endsWith(want)) || an.localeCompare(bn))
|
|
103
|
+
.map(([n]) => n)
|
|
104
|
+
: [];
|
|
105
|
+
const order = [...words, ...typos, ...searchIcons(query, 5)];
|
|
106
|
+
return [...new Set(order)].slice(0, 5);
|
|
53
107
|
}
|
|
54
108
|
/**
|
|
55
109
|
* The problem sentence for `value` written to `<ns>.<key>` on `type`, or null
|
|
@@ -608,7 +608,10 @@ export function readinessGaps(input) {
|
|
|
608
608
|
problem: 'No delivery options. The checkout page seeds a shipping select whose options ARE the ' +
|
|
609
609
|
"site's own methods, so the shopper meets a required-looking field with nothing in it " +
|
|
610
610
|
'and every order ships free.',
|
|
611
|
-
|
|
611
|
+
// Through shipping-config, not the v1 methods route: once a site has saved
|
|
612
|
+
// rules, every v1 writer answers 409 shipping_config_moved.
|
|
613
|
+
fix: 'Add at least one shipping method: GET /api/sites/{siteId}/shipping-config, then PATCH it ' +
|
|
614
|
+
'(sb_api_call reads the If-Match version itself) and POST …/shipping-config/preview to check the price.',
|
|
612
615
|
});
|
|
613
616
|
}
|
|
614
617
|
// THE OTHER FIXED PATHS.
|
package/dist/tools/api.js
CHANGED
|
@@ -2,9 +2,79 @@ import { z } from 'zod';
|
|
|
2
2
|
import { API_OPERATIONS, SWAGGER_SOURCE } from '../catalog/api.generated.js';
|
|
3
3
|
import { REQUEST_SHAPES } from '../catalog/shapes.generated.js';
|
|
4
4
|
import { searchOperations, describeOperation, summarizeOperation, findOperation, } from '../catalog/search.js';
|
|
5
|
-
import {
|
|
5
|
+
import { readFile, stat, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { basename } from 'node:path';
|
|
7
|
+
import { request, redact, ApiError } from '../transport/http.js';
|
|
8
|
+
import { typeForName } from '../transport/media.js';
|
|
9
|
+
import { restoreBodyFrom } from './undo.js';
|
|
6
10
|
import { text } from '../mcp/response.js';
|
|
7
11
|
import { credentialFor } from '../transport/credential.js';
|
|
12
|
+
/**
|
|
13
|
+
* ROUTES THAT REFUSE A WRITE WITHOUT `If-Match: "<version>"` (428
|
|
14
|
+
* if_match_required) — the shipping rules engine's one versioned document
|
|
15
|
+
* (`server/internal/shipping/rest/config.go`). The version is the GET's own
|
|
16
|
+
* `config.version`, which is what the editor sends (`features/shipping/config.ts`).
|
|
17
|
+
*/
|
|
18
|
+
const IF_MATCH_ROUTES = new Set([
|
|
19
|
+
'patch:/api/sites/{siteId}/shipping-config',
|
|
20
|
+
'post:/api/sites/{siteId}/shipping-config/versions/{id}/restore',
|
|
21
|
+
]);
|
|
22
|
+
const quoteVersion = (v) => {
|
|
23
|
+
if (typeof v === 'number' || /^\d+$/.test(v))
|
|
24
|
+
return `"${v}"`;
|
|
25
|
+
// The server reads W/"7", "7" and 7 (shipping/rest/config.go ifMatch); anything
|
|
26
|
+
// else is a 400 if_match_invalid that reads like a server fault.
|
|
27
|
+
if (/^(W\/)?"\d+"$/.test(v))
|
|
28
|
+
return v;
|
|
29
|
+
throw new Error(`sbuilder: if_match must be a version number (7 or "7"), not ${JSON.stringify(v)}.`);
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Once a site's shipping lives in the rules engine, every v1 method/zone
|
|
33
|
+
* writer answers 409 `shipping_config_moved` (`shipping/store.go movedLocked`).
|
|
34
|
+
* The platform's sentence does not say where the prices went.
|
|
35
|
+
*/
|
|
36
|
+
const SHIPPING_MOVED_FIX = ' — this site\'s shipping lives in the rules engine now: edit it with PATCH ' +
|
|
37
|
+
'/api/sites/{siteId}/shipping-config (sb_api_call reads If-Match for you), then verify with ' +
|
|
38
|
+
'POST /api/sites/{siteId}/shipping-config/preview.';
|
|
39
|
+
const isPlain = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
40
|
+
/** `over` wins; objects merge key by key; arrays and scalars replace. */
|
|
41
|
+
export function deepMerge(base, over) {
|
|
42
|
+
if (!isPlain(base) || !isPlain(over))
|
|
43
|
+
return over;
|
|
44
|
+
const out = { ...base };
|
|
45
|
+
for (const [k, v] of Object.entries(over))
|
|
46
|
+
out[k] = deepMerge(base[k], v);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** Leaf paths where `b` differs from `a` (arrays compared whole). */
|
|
50
|
+
function changedPaths(a, b, at = '') {
|
|
51
|
+
if (isPlain(a) && isPlain(b)) {
|
|
52
|
+
return [...new Set([...Object.keys(a), ...Object.keys(b)])].flatMap((k) => changedPaths(a[k], b[k], at ? `${at}.${k}` : k));
|
|
53
|
+
}
|
|
54
|
+
return JSON.stringify(a) === JSON.stringify(b) ? [] : [at];
|
|
55
|
+
}
|
|
56
|
+
const isEmpty = (v) => v === null || v === undefined || v === '' || v === false || v === 0 ||
|
|
57
|
+
(Array.isArray(v) && v.length === 0) || (isPlain(v) && Object.keys(v).length === 0);
|
|
58
|
+
/**
|
|
59
|
+
* What a whole-document PUT of `body` would ERASE: every path holding a value
|
|
60
|
+
* now that the body leaves out. Go decodes a PUT into a fresh struct, so an
|
|
61
|
+
* omitted `enabled: true` arrives as false, and an omitted settings key is gone.
|
|
62
|
+
*/
|
|
63
|
+
/** Fields the server owns on every record: a PUT never sets them, so never "erases" them. */
|
|
64
|
+
const SERVER_OWNED = new Set(['id', 'siteId', 'createdAt', 'updatedAt', 'deletedAt', 'createdBy', 'updatedBy', 'rev', 'version', 'number']);
|
|
65
|
+
function erasedPaths(current, body, at = '', owned = SERVER_OWNED) {
|
|
66
|
+
if (!isPlain(current))
|
|
67
|
+
return [];
|
|
68
|
+
return Object.entries(current).flatMap(([k, v]) => {
|
|
69
|
+
const p = at ? `${at}.${k}` : k;
|
|
70
|
+
if (isEmpty(v) || owned.has(k))
|
|
71
|
+
return [];
|
|
72
|
+
if (!isPlain(body) || !(k in body))
|
|
73
|
+
return [p];
|
|
74
|
+
return isPlain(v) && isPlain(body[k]) ? erasedPaths(v, body[k], p, owned) : [];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const listPaths = (ps) => (ps.length > 12 ? `${ps.slice(0, 12).join(', ')}, …` : ps.join(', '));
|
|
8
78
|
/** Past this many characters a list response is cut to fit and says so. */
|
|
9
79
|
export const RESULT_CAP = 60_000;
|
|
10
80
|
/**
|
|
@@ -466,11 +536,109 @@ export async function callOperation(ctx, args) {
|
|
|
466
536
|
}
|
|
467
537
|
const token = tokenFor(ctx, op.credential);
|
|
468
538
|
const dryRun = args.dry_run !== false;
|
|
539
|
+
if (args.merge && op.method !== 'PUT') {
|
|
540
|
+
throw new Error('sbuilder: merge is for a PUT — it merges body over the record the PUT replaces.');
|
|
541
|
+
}
|
|
542
|
+
if (args.merge && args.file)
|
|
543
|
+
throw new Error('sbuilder: merge and file do not go together.');
|
|
544
|
+
// A PUT IS A REPLACE, AND THE PLATFORM HAS NO HISTORY. Read what is about to
|
|
545
|
+
// be destroyed first: `sb_undo` puts it back, `merge` builds on it, and a
|
|
546
|
+
// dry run warns what the replace would erase. Reads only, never a write.
|
|
547
|
+
const canRead = op.method === 'PUT' && !args.file && !!REQUEST_SHAPES[op.id] &&
|
|
548
|
+
API_OPERATIONS.some((o) => o.id === `get:${op.path}`);
|
|
549
|
+
if (args.merge && !canRead) {
|
|
550
|
+
throw new Error(`sbuilder: merge needs a body shape and a matching GET; ${op.id} has none. Send the whole record.`);
|
|
551
|
+
}
|
|
552
|
+
let before;
|
|
553
|
+
if (canRead) {
|
|
554
|
+
try {
|
|
555
|
+
before = await request({
|
|
556
|
+
base: ctx.base,
|
|
557
|
+
method: 'GET',
|
|
558
|
+
path,
|
|
559
|
+
token: tokenFor(ctx, findOperation(`get:${op.path}`)?.credential ?? op.credential),
|
|
560
|
+
fetchImpl: ctx.fetchImpl,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
catch (e) {
|
|
564
|
+
// The row may not exist yet — the common case for a PUT that creates.
|
|
565
|
+
// Nothing to undo or warn about; but nothing to merge onto either.
|
|
566
|
+
if (args.merge)
|
|
567
|
+
throw new Error(`sbuilder: merge could not read the current record: ${e.message}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const current = before === undefined ? null : restoreBodyFrom(op.id, before);
|
|
571
|
+
if (args.merge && !current) {
|
|
572
|
+
throw new Error(`sbuilder: merge found no record in the GET answer for ${op.id}. Send the whole record.`);
|
|
573
|
+
}
|
|
574
|
+
if (args.merge && isPlain(args.body) && isPlain(current)) {
|
|
575
|
+
// A key the record does not have at the top is a body in the wrong envelope
|
|
576
|
+
// ({locale} for {settings:{locale}}): merged, it would report a change the
|
|
577
|
+
// server ignores.
|
|
578
|
+
const stray = Object.keys(args.body).filter((k) => !(k in current));
|
|
579
|
+
if (stray.length && Object.keys(current).length === 1) {
|
|
580
|
+
throw new Error(`sbuilder: this PUT takes { ${Object.keys(current)[0]}: … } — put ${stray.join(', ')} inside it.`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const body = args.merge ? deepMerge(current, args.body) : args.body;
|
|
584
|
+
const merged = args.merge ? { changed: changedPaths(current, body) } : undefined;
|
|
585
|
+
// The settings PUT MERGES a body that is exactly {settings:{locale}}
|
|
586
|
+
// (sitesettings/rest/rest.go LocaleOnly), so nothing is erased there.
|
|
587
|
+
const localeOnly = op.id.endsWith('/settings') && isPlain(args.body) && isPlain(args.body.settings) &&
|
|
588
|
+
Object.keys(args.body).length === 1 && Object.keys(args.body.settings).join() === 'locale';
|
|
589
|
+
const owned = new Set([...SERVER_OWNED, ...(REQUEST_SHAPES[op.id]?.readOnly ?? [])]);
|
|
590
|
+
const erased = !args.merge && current && !localeOnly ? erasedPaths(current, args.body, '', owned) : [];
|
|
591
|
+
const replaceWarning = erased.length
|
|
592
|
+
? `This PUT replaces the whole record; ${erased.length} field(s) it holds now ` +
|
|
593
|
+
`${dryRun ? 'would be' : 'were'} erased: ${listPaths(erased)}. ` +
|
|
594
|
+
(dryRun ? 'Pass merge:true to keep them.' : 'sb_undo puts them back.')
|
|
595
|
+
: undefined;
|
|
596
|
+
// If-Match: given, or — on a route that refuses a write without one — the
|
|
597
|
+
// version the GET answers right now.
|
|
598
|
+
const headers = {};
|
|
599
|
+
let ifMatchRead;
|
|
600
|
+
if (args.if_match !== undefined)
|
|
601
|
+
headers['If-Match'] = quoteVersion(args.if_match);
|
|
602
|
+
else if (IF_MATCH_ROUTES.has(op.id)) {
|
|
603
|
+
ifMatchRead = path.replace(/(\/shipping-config).*$/, '$1');
|
|
604
|
+
if (dryRun) {
|
|
605
|
+
headers['If-Match'] = `(read from GET ${ifMatchRead} at send)`;
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
const got = (await request({ base: ctx.base, method: 'GET', path: ifMatchRead, token, fetchImpl: ctx.fetchImpl }));
|
|
609
|
+
if (typeof got?.config?.version !== 'number') {
|
|
610
|
+
throw new Error(`sbuilder: GET ${ifMatchRead} answered no config.version; pass if_match.`);
|
|
611
|
+
}
|
|
612
|
+
headers['If-Match'] = quoteVersion(got.config.version);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// MULTIPART: a non-GET sends a local file under `field` (default "file") —
|
|
616
|
+
// the product import takes `file:formData/file` and cannot read JSON. A GET
|
|
617
|
+
// with `file` saves the answer's bytes (an xlsx template) rather than
|
|
618
|
+
// pasting binary into the result.
|
|
619
|
+
const download = !!args.file && (op.method === 'GET' || op.method === 'HEAD');
|
|
620
|
+
if (download && !dryRun && (await stat(args.file.path).then(() => true, () => false))) {
|
|
621
|
+
throw new Error(`sbuilder: ${args.file.path} already exists — nothing was overwritten; name a new path.`);
|
|
622
|
+
}
|
|
623
|
+
const field = args.file?.field ?? 'file';
|
|
624
|
+
const scalars = isPlain(args.body)
|
|
625
|
+
? Object.entries(args.body).filter(([, v]) => ['string', 'number', 'boolean'].includes(typeof v))
|
|
626
|
+
: [];
|
|
469
627
|
if (dryRun) {
|
|
470
628
|
// THE DRY RUN IS THE DEFAULT, so a directive said only after a real send
|
|
471
629
|
// reaches nobody who looks before they leap. Once per process either way:
|
|
472
630
|
// a dry run that said it leaves the send that follows silent.
|
|
473
631
|
const directive = op.raw ? ctx.notices.once('raw_call', RAW_CALL_NOTICE) : undefined;
|
|
632
|
+
const upload = args.file && !download
|
|
633
|
+
? {
|
|
634
|
+
multipart: {
|
|
635
|
+
field,
|
|
636
|
+
path: args.file.path,
|
|
637
|
+
bytes: (await stat(args.file.path)).size,
|
|
638
|
+
...(scalars.length ? { fields: Object.fromEntries(scalars) } : {}),
|
|
639
|
+
},
|
|
640
|
+
}
|
|
641
|
+
: undefined;
|
|
474
642
|
return {
|
|
475
643
|
dry_run: true,
|
|
476
644
|
...(op.raw ? { uncatalogued: true } : {}),
|
|
@@ -480,45 +648,71 @@ export async function callOperation(ctx, args) {
|
|
|
480
648
|
url: ctx.base.replace(/\/$/, '') + path,
|
|
481
649
|
query: args.query,
|
|
482
650
|
Authorization: token ? `Bearer ${token}` : undefined,
|
|
483
|
-
|
|
651
|
+
...(Object.keys(headers).length ? { headers } : {}),
|
|
652
|
+
// The CALLER's body, never the merged one: a merge folds in what the GET
|
|
653
|
+
// answered, and redact() hides only secret-looking KEY NAMES — a webhook
|
|
654
|
+
// URL or an account number under an ordinary key would be printed.
|
|
655
|
+
body: upload ?? args.body,
|
|
484
656
|
}),
|
|
657
|
+
...(download ? { would_save: args.file.path } : {}),
|
|
658
|
+
...(merged ? { merge: merged } : {}),
|
|
659
|
+
...(replaceWarning ? { replace_warning: replaceWarning } : {}),
|
|
485
660
|
// Shaping is part of what the call WOULD do, so the preview says it;
|
|
486
661
|
// otherwise a dry run reads identically whether or not it was asked for.
|
|
487
662
|
...(args.pick !== undefined || args.max_items !== undefined || args.item_offset !== undefined
|
|
488
663
|
? { shaping: { pick: args.pick, max_items: args.max_items, item_offset: args.item_offset } }
|
|
489
664
|
: {}),
|
|
490
|
-
note:
|
|
665
|
+
note: before !== undefined
|
|
666
|
+
? 'Nothing was written — only the current record was read, to ' +
|
|
667
|
+
(merged ? 'merge onto' : 'check what the replace would erase') +
|
|
668
|
+
'. Re-call with dry_run:false to execute.'
|
|
669
|
+
: 'Nothing was sent. Re-call with dry_run:false to execute.',
|
|
491
670
|
};
|
|
492
671
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
672
|
+
if (before !== undefined)
|
|
673
|
+
ctx.undo.record(op.id, path, before);
|
|
674
|
+
let sendBody = body;
|
|
675
|
+
if (args.file && !download) {
|
|
676
|
+
const form = new FormData();
|
|
677
|
+
const name = basename(args.file.path);
|
|
678
|
+
const type = typeForName(name);
|
|
679
|
+
form.set(field, new Blob([await readFile(args.file.path)], type ? { type } : undefined), name);
|
|
680
|
+
for (const [k, v] of scalars)
|
|
681
|
+
form.set(k, String(v));
|
|
682
|
+
sendBody = form;
|
|
683
|
+
}
|
|
684
|
+
let raw;
|
|
685
|
+
try {
|
|
686
|
+
raw = await request({
|
|
687
|
+
base: ctx.base,
|
|
688
|
+
method: op.method,
|
|
689
|
+
path,
|
|
690
|
+
token,
|
|
691
|
+
query: args.query,
|
|
692
|
+
body: sendBody,
|
|
693
|
+
...(Object.keys(headers).length ? { headers } : {}),
|
|
694
|
+
...(download ? { binary: true } : {}),
|
|
695
|
+
fetchImpl: ctx.fetchImpl,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
catch (e) {
|
|
699
|
+
if (e instanceof ApiError && e.code === 'shipping_config_moved')
|
|
700
|
+
e.message += SHIPPING_MOVED_FIX;
|
|
701
|
+
throw e;
|
|
702
|
+
}
|
|
703
|
+
if (download && raw instanceof Uint8Array) {
|
|
704
|
+
// 'wx': a download never replaces a file that is already there.
|
|
498
705
|
try {
|
|
499
|
-
|
|
500
|
-
base: ctx.base,
|
|
501
|
-
method: 'GET',
|
|
502
|
-
path,
|
|
503
|
-
token: tokenFor(ctx, findOperation(`get:${op.path}`)?.credential ?? op.credential),
|
|
504
|
-
fetchImpl: ctx.fetchImpl,
|
|
505
|
-
});
|
|
506
|
-
ctx.undo.record(op.id, path, before);
|
|
706
|
+
await writeFile(args.file.path, raw, { flag: 'wx' });
|
|
507
707
|
}
|
|
508
|
-
catch {
|
|
509
|
-
|
|
510
|
-
|
|
708
|
+
catch (e) {
|
|
709
|
+
if (e.code === 'EEXIST') {
|
|
710
|
+
throw new Error(`sbuilder: ${args.file.path} already exists — nothing was overwritten; name a new path.`);
|
|
711
|
+
}
|
|
712
|
+
throw e;
|
|
511
713
|
}
|
|
714
|
+
return { ok: true, saved: args.file.path, bytes: raw.byteLength };
|
|
512
715
|
}
|
|
513
|
-
const raw = await request({
|
|
514
|
-
base: ctx.base,
|
|
515
|
-
method: op.method,
|
|
516
|
-
path,
|
|
517
|
-
token,
|
|
518
|
-
query: args.query,
|
|
519
|
-
body: args.body,
|
|
520
|
-
fetchImpl: ctx.fetchImpl,
|
|
521
|
-
});
|
|
522
716
|
// A 204 HAS NO BODY, and `null` is not an answer a caller can read: a DELETE
|
|
523
717
|
// that worked and a DELETE that returned nothing looked identical, so sixteen
|
|
524
718
|
// page deletes in a row reported `null` sixteen times and the only way to know
|
|
@@ -532,10 +726,16 @@ export async function callOperation(ctx, args) {
|
|
|
532
726
|
max_items: args.max_items,
|
|
533
727
|
item_offset: args.item_offset,
|
|
534
728
|
});
|
|
729
|
+
const extra = {
|
|
730
|
+
...(ifMatchRead ? { if_match: { sent: headers['If-Match'], read_from: `GET ${ifMatchRead}` } } : {}),
|
|
731
|
+
...(merged ? { merge: merged } : {}),
|
|
732
|
+
...(replaceWarning ? { replace_warning: replaceWarning } : {}),
|
|
733
|
+
};
|
|
734
|
+
const told = Object.keys(extra).length && isPlain(answer) ? { ...answer, ...extra } : answer;
|
|
535
735
|
if (!op.raw)
|
|
536
|
-
return
|
|
736
|
+
return told;
|
|
537
737
|
const note = ctx.notices.once('raw_call', RAW_CALL_NOTICE);
|
|
538
|
-
return { uncatalogued: true, ...(note ? { note } : {}), data:
|
|
738
|
+
return { uncatalogued: true, ...(note ? { note } : {}), data: told };
|
|
539
739
|
}
|
|
540
740
|
/**
|
|
541
741
|
* WHY A SUMMARY CAN BE ABOUT A DIFFERENT ROUTE.
|
|
@@ -615,6 +815,12 @@ export function registerApiTools(server, ctx) {
|
|
|
615
815
|
pick: z.array(z.string()).optional(),
|
|
616
816
|
max_items: z.number().int().min(1).optional(),
|
|
617
817
|
item_offset: z.number().int().min(0).optional().describe('Offset within this response, after API paging. Reads only.'),
|
|
818
|
+
if_match: z.union([z.string(), z.number()]).optional().describe('If-Match version; shipping-config reads it itself'),
|
|
819
|
+
merge: z.boolean().optional().describe('PUT: deep-merge body over the current record (arrays replace)'),
|
|
820
|
+
file: z
|
|
821
|
+
.object({ path: z.string(), field: z.string().optional() })
|
|
822
|
+
.optional()
|
|
823
|
+
.describe('Local file: multipart upload (field "file"); on a GET, saves the answer there'),
|
|
618
824
|
},
|
|
619
825
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
620
826
|
}, async (args) => text(await callOperation(ctx, args)));
|
package/dist/tools/live.js
CHANGED
|
@@ -652,11 +652,16 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
652
652
|
});
|
|
653
653
|
}
|
|
654
654
|
const asset = await uploadMedia(ctx, site_id, { path, url, name, folderId: folder_id });
|
|
655
|
+
const font = asset.mediaType === 'font';
|
|
655
656
|
return text({
|
|
656
657
|
asset,
|
|
657
|
-
next: asset.url
|
|
658
|
-
?
|
|
659
|
-
:
|
|
658
|
+
next: !asset.url
|
|
659
|
+
? 'Uploaded, but the server returned no url — read it back with sb_media_list.'
|
|
660
|
+
: font
|
|
661
|
+
? `A font: give it a family — POST /api/sites/{siteId}/fonts {name, source:"upload"}, then PUT ` +
|
|
662
|
+
`…/fonts/{groupId}/files {weight, url: ${JSON.stringify(asset.url)}, format, sizeBytes}, ` +
|
|
663
|
+
'and use the family by NAME (settings fontFamily or a theme text style).'
|
|
664
|
+
: `Use it: sb_set id "<node>", namespace specials, keys { "src": ${JSON.stringify(asset.url)} }`,
|
|
660
665
|
});
|
|
661
666
|
});
|
|
662
667
|
server.registerTool('sb_event', {
|
package/dist/transport/http.js
CHANGED
|
@@ -232,14 +232,19 @@ async function send(opts) {
|
|
|
232
232
|
const headers = { Accept: 'application/json', ...identityHeaders(), ...opts.headers };
|
|
233
233
|
if (opts.token)
|
|
234
234
|
headers.Authorization = `Bearer ${opts.token}`;
|
|
235
|
-
|
|
235
|
+
// A FormData body is multipart, and fetch must write its Content-Type itself
|
|
236
|
+
// so the boundary matches the body it builds.
|
|
237
|
+
const multipart = opts.body instanceof FormData;
|
|
238
|
+
if (opts.body !== undefined && !multipart)
|
|
236
239
|
headers['Content-Type'] = 'application/json';
|
|
237
240
|
const body = withPageRoot(opts);
|
|
238
241
|
const res = await doFetch(buildUrl(opts.base, opts.path, opts.query), {
|
|
239
242
|
method: opts.method,
|
|
240
243
|
headers,
|
|
241
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
244
|
+
body: body === undefined ? undefined : multipart ? body : JSON.stringify(body),
|
|
242
245
|
});
|
|
246
|
+
if (opts.binary && res.ok)
|
|
247
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
243
248
|
const raw = await res.text();
|
|
244
249
|
let parsed = undefined;
|
|
245
250
|
if (raw) {
|
package/dist/transport/media.js
CHANGED
|
@@ -53,8 +53,11 @@ const TYPE_BY_EXT = {
|
|
|
53
53
|
'.woff': 'font/woff',
|
|
54
54
|
'.ttf': 'font/ttf',
|
|
55
55
|
'.otf': 'font/otf',
|
|
56
|
+
// `sb_api_call file` — the product import takes a workbook.
|
|
57
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
58
|
+
'.csv': 'text/csv',
|
|
56
59
|
};
|
|
57
|
-
function typeForName(name) {
|
|
60
|
+
export function typeForName(name) {
|
|
58
61
|
const dot = name.lastIndexOf('.');
|
|
59
62
|
return dot < 0 ? '' : (TYPE_BY_EXT[name.slice(dot).toLowerCase()] ?? '');
|
|
60
63
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.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",
|