sbuilder-mcp 0.33.0 → 0.35.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 +16 -0
- package/CHANGELOG.vi.md +16 -0
- package/dist/catalog/api.generated.js +354 -3
- package/dist/catalog/shapes.generated.js +1 -1
- package/dist/domains/site/importmap.js +31 -1
- package/dist/server.js +1 -1
- package/dist/tools/chrome.js +93 -0
- package/dist/tools/store.js +52 -5
- package/dist/tools/undo.js +6 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ 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.35.0] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- The catalog now carries five page-history operations that reached the platform's own browser and swagger.json but had never carried an `@Router` annotation, so they were invisible to this server: `GET`/`POST .../pages/{pageId}/versions` (list and label a snapshot of the current draft), `POST .../pages/{pageId}/versions/{versionId}/restore`, `GET .../pages/{pageId}/history` (the autosave checkpoints written on every draft save), and `POST .../pages/{pageId}/history/{historyId}/restore`.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- sb_undo's description now points to this page history and restore surface as the better way to recover a page, since it survives process exit while sb_undo's own undo stack is in-process, capped, and gone on exit; it no longer claims the platform has no page history at all.
|
|
16
|
+
|
|
17
|
+
## [0.34.0] - 2026-09-11
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- sb_store's new `action: "chrome"` builds one shared header (or, with `footer: true`, a shared footer) from the pages a site already has, closing the `siteChrome` gap sb_review has reported since it learned to ask: the menu labels and links come from the site's own pages (home first), the look is read off the home page, and the master is given to every page as a reference instead of copied onto each one. It skips a site that already shares a header or footer, and a site with fewer than two pages. dry_run (the default) returns the planned menu and which pages it would touch without writing anything.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- The menu built for a shared header or footer (by sb_store's new `action: "chrome"` and by sb_import_site, which already built one) now packs its links against each other at their own width instead of splitting the row into equal-width columns, so three links no longer land spread across the thirds of a wide header with large gaps between them.
|
|
24
|
+
|
|
9
25
|
## [0.33.0] - 2026-09-11
|
|
10
26
|
|
|
11
27
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,22 @@ 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.35.0] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Catalog giờ mang theo năm operation lịch sử trang từng có mặt trên trình duyệt của nền tảng và trong swagger.json nhưng chưa từng được gắn annotation `@Router`, nên trước đây server này không thể thấy chúng: `GET`/`POST .../pages/{pageId}/versions` (liệt kê và gắn nhãn một bản chụp của bản nháp hiện tại), `POST .../pages/{pageId}/versions/{versionId}/restore`, `GET .../pages/{pageId}/history` (các checkpoint autosave được ghi ở mỗi lần lưu bản nháp), và `POST .../pages/{pageId}/history/{historyId}/restore`.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Mô tả của sb_undo giờ trỏ tới bề mặt lịch sử và khôi phục trang này như cách tốt hơn để phục hồi một trang, vì nó tồn tại qua cả khi tiến trình thoát, trong khi ngăn xếp undo của chính sb_undo chỉ nằm trong tiến trình, có giới hạn, và mất đi khi thoát; nó không còn khẳng định nền tảng hoàn toàn không có lịch sử trang.
|
|
16
|
+
|
|
17
|
+
## [0.34.0] - 2026-09-11
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Hành động mới `action: "chrome"` của sb_store dựng một header dùng chung (hoặc, với `footer: true`, một footer dùng chung) từ các trang site đã có sẵn, khép lại khoảng trống `siteChrome` mà sb_review đã báo cáo kể từ khi biết hỏi câu này: nhãn và liên kết của menu lấy từ chính các trang của site (trang chủ trước tiên), giao diện được đọc từ trang chủ, và master được gán cho mọi trang dưới dạng tham chiếu thay vì sao chép riêng vào từng trang. Lệnh này bỏ qua nếu site đã có sẵn header hoặc footer dùng chung, và nếu site có ít hơn hai trang. dry_run (mặc định bật) trả về menu dự kiến và các trang sẽ bị tác động mà không ghi gì cả.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Menu được dựng cho header hoặc footer dùng chung (bởi `action: "chrome"` mới của sb_store, và bởi sb_import_site — vốn đã dựng sẵn menu này) giờ xếp các liên kết sát nhau theo đúng chiều rộng nội dung của chúng thay vì chia hàng thành các cột bằng nhau, nên ba liên kết không còn bị dàn trải ra ba phần bằng nhau của một header rộng với khoảng trống lớn giữa chúng.
|
|
24
|
+
|
|
9
25
|
## [0.33.0] - 2026-09-11
|
|
10
26
|
|
|
11
27
|
### Added
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const SWAGGER_SOURCE = {
|
|
2
|
-
"operations":
|
|
2
|
+
"operations": 506,
|
|
3
3
|
"definitions": 105,
|
|
4
|
-
"bodyCarrying":
|
|
5
|
-
"bodyUndescribed":
|
|
4
|
+
"bodyCarrying": 182,
|
|
5
|
+
"bodyUndescribed": 67,
|
|
6
6
|
"generatedFrom": "server/docs/swagger.json"
|
|
7
7
|
};
|
|
8
8
|
export const API_OPERATIONS = [
|
|
@@ -9532,6 +9532,113 @@ export const API_OPERATIONS = [
|
|
|
9532
9532
|
"bodyRef": null,
|
|
9533
9533
|
"credential": "siteScoped"
|
|
9534
9534
|
},
|
|
9535
|
+
{
|
|
9536
|
+
"id": "get:/api/sites/{siteId}/pages/{pageId}/history",
|
|
9537
|
+
"method": "GET",
|
|
9538
|
+
"path": "/api/sites/{siteId}/pages/{pageId}/history",
|
|
9539
|
+
"tags": [
|
|
9540
|
+
"pages",
|
|
9541
|
+
"pages"
|
|
9542
|
+
],
|
|
9543
|
+
"summary": "Restore a page's draft from an autosave checkpoint",
|
|
9544
|
+
"params": [
|
|
9545
|
+
{
|
|
9546
|
+
"name": "siteId",
|
|
9547
|
+
"in": "path",
|
|
9548
|
+
"required": true,
|
|
9549
|
+
"type": "string",
|
|
9550
|
+
"description": "Site ID"
|
|
9551
|
+
},
|
|
9552
|
+
{
|
|
9553
|
+
"name": "pageId",
|
|
9554
|
+
"in": "path",
|
|
9555
|
+
"required": true,
|
|
9556
|
+
"type": "string",
|
|
9557
|
+
"description": "Page ID"
|
|
9558
|
+
},
|
|
9559
|
+
{
|
|
9560
|
+
"name": "limit",
|
|
9561
|
+
"in": "query",
|
|
9562
|
+
"required": false,
|
|
9563
|
+
"type": "integer",
|
|
9564
|
+
"description": "1-… , default 50"
|
|
9565
|
+
},
|
|
9566
|
+
{
|
|
9567
|
+
"name": "siteId",
|
|
9568
|
+
"in": "path",
|
|
9569
|
+
"required": true,
|
|
9570
|
+
"type": "string",
|
|
9571
|
+
"description": "Site ID"
|
|
9572
|
+
},
|
|
9573
|
+
{
|
|
9574
|
+
"name": "pageId",
|
|
9575
|
+
"in": "path",
|
|
9576
|
+
"required": true,
|
|
9577
|
+
"type": "string",
|
|
9578
|
+
"description": "Page ID"
|
|
9579
|
+
}
|
|
9580
|
+
],
|
|
9581
|
+
"bodyDescribed": false,
|
|
9582
|
+
"bodyRef": null,
|
|
9583
|
+
"credential": "siteScoped"
|
|
9584
|
+
},
|
|
9585
|
+
{
|
|
9586
|
+
"id": "post:/api/sites/{siteId}/pages/{pageId}/history/{historyId}/restore",
|
|
9587
|
+
"method": "POST",
|
|
9588
|
+
"path": "/api/sites/{siteId}/pages/{pageId}/history/{historyId}/restore",
|
|
9589
|
+
"tags": [
|
|
9590
|
+
"pages",
|
|
9591
|
+
"pages"
|
|
9592
|
+
],
|
|
9593
|
+
"summary": "Restore a page's draft from an autosave checkpoint",
|
|
9594
|
+
"params": [
|
|
9595
|
+
{
|
|
9596
|
+
"name": "siteId",
|
|
9597
|
+
"in": "path",
|
|
9598
|
+
"required": true,
|
|
9599
|
+
"type": "string",
|
|
9600
|
+
"description": "Site ID"
|
|
9601
|
+
},
|
|
9602
|
+
{
|
|
9603
|
+
"name": "pageId",
|
|
9604
|
+
"in": "path",
|
|
9605
|
+
"required": true,
|
|
9606
|
+
"type": "string",
|
|
9607
|
+
"description": "Page ID"
|
|
9608
|
+
},
|
|
9609
|
+
{
|
|
9610
|
+
"name": "limit",
|
|
9611
|
+
"in": "query",
|
|
9612
|
+
"required": false,
|
|
9613
|
+
"type": "integer",
|
|
9614
|
+
"description": "1-… , default 50"
|
|
9615
|
+
},
|
|
9616
|
+
{
|
|
9617
|
+
"name": "siteId",
|
|
9618
|
+
"in": "path",
|
|
9619
|
+
"required": true,
|
|
9620
|
+
"type": "string",
|
|
9621
|
+
"description": "Site ID"
|
|
9622
|
+
},
|
|
9623
|
+
{
|
|
9624
|
+
"name": "pageId",
|
|
9625
|
+
"in": "path",
|
|
9626
|
+
"required": true,
|
|
9627
|
+
"type": "string",
|
|
9628
|
+
"description": "Page ID"
|
|
9629
|
+
},
|
|
9630
|
+
{
|
|
9631
|
+
"name": "historyId",
|
|
9632
|
+
"in": "path",
|
|
9633
|
+
"required": true,
|
|
9634
|
+
"type": "string",
|
|
9635
|
+
"description": "History ID from the list"
|
|
9636
|
+
}
|
|
9637
|
+
],
|
|
9638
|
+
"bodyDescribed": false,
|
|
9639
|
+
"bodyRef": null,
|
|
9640
|
+
"credential": "siteScoped"
|
|
9641
|
+
},
|
|
9535
9642
|
{
|
|
9536
9643
|
"id": "get:/api/sites/{siteId}/pages/{pageId}/preview",
|
|
9537
9644
|
"method": "GET",
|
|
@@ -9616,6 +9723,250 @@ export const API_OPERATIONS = [
|
|
|
9616
9723
|
"bodyRef": null,
|
|
9617
9724
|
"credential": "siteScoped"
|
|
9618
9725
|
},
|
|
9726
|
+
{
|
|
9727
|
+
"id": "get:/api/sites/{siteId}/pages/{pageId}/versions",
|
|
9728
|
+
"method": "GET",
|
|
9729
|
+
"path": "/api/sites/{siteId}/pages/{pageId}/versions",
|
|
9730
|
+
"tags": [
|
|
9731
|
+
"pages",
|
|
9732
|
+
"pages",
|
|
9733
|
+
"pages"
|
|
9734
|
+
],
|
|
9735
|
+
"summary": "Restore a page's draft from a version snapshot",
|
|
9736
|
+
"params": [
|
|
9737
|
+
{
|
|
9738
|
+
"name": "siteId",
|
|
9739
|
+
"in": "path",
|
|
9740
|
+
"required": true,
|
|
9741
|
+
"type": "string",
|
|
9742
|
+
"description": "Site ID"
|
|
9743
|
+
},
|
|
9744
|
+
{
|
|
9745
|
+
"name": "pageId",
|
|
9746
|
+
"in": "path",
|
|
9747
|
+
"required": true,
|
|
9748
|
+
"type": "string",
|
|
9749
|
+
"description": "Page ID"
|
|
9750
|
+
},
|
|
9751
|
+
{
|
|
9752
|
+
"name": "limit",
|
|
9753
|
+
"in": "query",
|
|
9754
|
+
"required": false,
|
|
9755
|
+
"type": "integer",
|
|
9756
|
+
"description": "1-… , default 50"
|
|
9757
|
+
},
|
|
9758
|
+
{
|
|
9759
|
+
"name": "offset",
|
|
9760
|
+
"in": "query",
|
|
9761
|
+
"required": false,
|
|
9762
|
+
"type": "integer",
|
|
9763
|
+
"description": "default 0"
|
|
9764
|
+
},
|
|
9765
|
+
{
|
|
9766
|
+
"name": "siteId",
|
|
9767
|
+
"in": "path",
|
|
9768
|
+
"required": true,
|
|
9769
|
+
"type": "string",
|
|
9770
|
+
"description": "Site ID"
|
|
9771
|
+
},
|
|
9772
|
+
{
|
|
9773
|
+
"name": "pageId",
|
|
9774
|
+
"in": "path",
|
|
9775
|
+
"required": true,
|
|
9776
|
+
"type": "string",
|
|
9777
|
+
"description": "Page ID"
|
|
9778
|
+
},
|
|
9779
|
+
{
|
|
9780
|
+
"name": "body",
|
|
9781
|
+
"in": "body",
|
|
9782
|
+
"required": true,
|
|
9783
|
+
"type": "object",
|
|
9784
|
+
"description": "{label, document?}"
|
|
9785
|
+
},
|
|
9786
|
+
{
|
|
9787
|
+
"name": "siteId",
|
|
9788
|
+
"in": "path",
|
|
9789
|
+
"required": true,
|
|
9790
|
+
"type": "string",
|
|
9791
|
+
"description": "Site ID"
|
|
9792
|
+
},
|
|
9793
|
+
{
|
|
9794
|
+
"name": "pageId",
|
|
9795
|
+
"in": "path",
|
|
9796
|
+
"required": true,
|
|
9797
|
+
"type": "string",
|
|
9798
|
+
"description": "Page ID"
|
|
9799
|
+
}
|
|
9800
|
+
],
|
|
9801
|
+
"bodyDescribed": false,
|
|
9802
|
+
"bodyRef": null,
|
|
9803
|
+
"credential": "siteScoped"
|
|
9804
|
+
},
|
|
9805
|
+
{
|
|
9806
|
+
"id": "post:/api/sites/{siteId}/pages/{pageId}/versions",
|
|
9807
|
+
"method": "POST",
|
|
9808
|
+
"path": "/api/sites/{siteId}/pages/{pageId}/versions",
|
|
9809
|
+
"tags": [
|
|
9810
|
+
"pages",
|
|
9811
|
+
"pages",
|
|
9812
|
+
"pages"
|
|
9813
|
+
],
|
|
9814
|
+
"summary": "Restore a page's draft from a version snapshot",
|
|
9815
|
+
"params": [
|
|
9816
|
+
{
|
|
9817
|
+
"name": "siteId",
|
|
9818
|
+
"in": "path",
|
|
9819
|
+
"required": true,
|
|
9820
|
+
"type": "string",
|
|
9821
|
+
"description": "Site ID"
|
|
9822
|
+
},
|
|
9823
|
+
{
|
|
9824
|
+
"name": "pageId",
|
|
9825
|
+
"in": "path",
|
|
9826
|
+
"required": true,
|
|
9827
|
+
"type": "string",
|
|
9828
|
+
"description": "Page ID"
|
|
9829
|
+
},
|
|
9830
|
+
{
|
|
9831
|
+
"name": "limit",
|
|
9832
|
+
"in": "query",
|
|
9833
|
+
"required": false,
|
|
9834
|
+
"type": "integer",
|
|
9835
|
+
"description": "1-… , default 50"
|
|
9836
|
+
},
|
|
9837
|
+
{
|
|
9838
|
+
"name": "offset",
|
|
9839
|
+
"in": "query",
|
|
9840
|
+
"required": false,
|
|
9841
|
+
"type": "integer",
|
|
9842
|
+
"description": "default 0"
|
|
9843
|
+
},
|
|
9844
|
+
{
|
|
9845
|
+
"name": "siteId",
|
|
9846
|
+
"in": "path",
|
|
9847
|
+
"required": true,
|
|
9848
|
+
"type": "string",
|
|
9849
|
+
"description": "Site ID"
|
|
9850
|
+
},
|
|
9851
|
+
{
|
|
9852
|
+
"name": "pageId",
|
|
9853
|
+
"in": "path",
|
|
9854
|
+
"required": true,
|
|
9855
|
+
"type": "string",
|
|
9856
|
+
"description": "Page ID"
|
|
9857
|
+
},
|
|
9858
|
+
{
|
|
9859
|
+
"name": "body",
|
|
9860
|
+
"in": "body",
|
|
9861
|
+
"required": true,
|
|
9862
|
+
"type": "object",
|
|
9863
|
+
"description": "{label, document?}"
|
|
9864
|
+
},
|
|
9865
|
+
{
|
|
9866
|
+
"name": "siteId",
|
|
9867
|
+
"in": "path",
|
|
9868
|
+
"required": true,
|
|
9869
|
+
"type": "string",
|
|
9870
|
+
"description": "Site ID"
|
|
9871
|
+
},
|
|
9872
|
+
{
|
|
9873
|
+
"name": "pageId",
|
|
9874
|
+
"in": "path",
|
|
9875
|
+
"required": true,
|
|
9876
|
+
"type": "string",
|
|
9877
|
+
"description": "Page ID"
|
|
9878
|
+
}
|
|
9879
|
+
],
|
|
9880
|
+
"bodyDescribed": false,
|
|
9881
|
+
"bodyRef": null,
|
|
9882
|
+
"credential": "siteScoped"
|
|
9883
|
+
},
|
|
9884
|
+
{
|
|
9885
|
+
"id": "post:/api/sites/{siteId}/pages/{pageId}/versions/{versionId}/restore",
|
|
9886
|
+
"method": "POST",
|
|
9887
|
+
"path": "/api/sites/{siteId}/pages/{pageId}/versions/{versionId}/restore",
|
|
9888
|
+
"tags": [
|
|
9889
|
+
"pages",
|
|
9890
|
+
"pages",
|
|
9891
|
+
"pages"
|
|
9892
|
+
],
|
|
9893
|
+
"summary": "Restore a page's draft from a version snapshot",
|
|
9894
|
+
"params": [
|
|
9895
|
+
{
|
|
9896
|
+
"name": "siteId",
|
|
9897
|
+
"in": "path",
|
|
9898
|
+
"required": true,
|
|
9899
|
+
"type": "string",
|
|
9900
|
+
"description": "Site ID"
|
|
9901
|
+
},
|
|
9902
|
+
{
|
|
9903
|
+
"name": "pageId",
|
|
9904
|
+
"in": "path",
|
|
9905
|
+
"required": true,
|
|
9906
|
+
"type": "string",
|
|
9907
|
+
"description": "Page ID"
|
|
9908
|
+
},
|
|
9909
|
+
{
|
|
9910
|
+
"name": "limit",
|
|
9911
|
+
"in": "query",
|
|
9912
|
+
"required": false,
|
|
9913
|
+
"type": "integer",
|
|
9914
|
+
"description": "1-… , default 50"
|
|
9915
|
+
},
|
|
9916
|
+
{
|
|
9917
|
+
"name": "offset",
|
|
9918
|
+
"in": "query",
|
|
9919
|
+
"required": false,
|
|
9920
|
+
"type": "integer",
|
|
9921
|
+
"description": "default 0"
|
|
9922
|
+
},
|
|
9923
|
+
{
|
|
9924
|
+
"name": "siteId",
|
|
9925
|
+
"in": "path",
|
|
9926
|
+
"required": true,
|
|
9927
|
+
"type": "string",
|
|
9928
|
+
"description": "Site ID"
|
|
9929
|
+
},
|
|
9930
|
+
{
|
|
9931
|
+
"name": "pageId",
|
|
9932
|
+
"in": "path",
|
|
9933
|
+
"required": true,
|
|
9934
|
+
"type": "string",
|
|
9935
|
+
"description": "Page ID"
|
|
9936
|
+
},
|
|
9937
|
+
{
|
|
9938
|
+
"name": "body",
|
|
9939
|
+
"in": "body",
|
|
9940
|
+
"required": true,
|
|
9941
|
+
"type": "object",
|
|
9942
|
+
"description": "{label, document?}"
|
|
9943
|
+
},
|
|
9944
|
+
{
|
|
9945
|
+
"name": "siteId",
|
|
9946
|
+
"in": "path",
|
|
9947
|
+
"required": true,
|
|
9948
|
+
"type": "string",
|
|
9949
|
+
"description": "Site ID"
|
|
9950
|
+
},
|
|
9951
|
+
{
|
|
9952
|
+
"name": "pageId",
|
|
9953
|
+
"in": "path",
|
|
9954
|
+
"required": true,
|
|
9955
|
+
"type": "string",
|
|
9956
|
+
"description": "Page ID"
|
|
9957
|
+
},
|
|
9958
|
+
{
|
|
9959
|
+
"name": "versionId",
|
|
9960
|
+
"in": "path",
|
|
9961
|
+
"required": true,
|
|
9962
|
+
"type": "string",
|
|
9963
|
+
"description": "Version ID from the list"
|
|
9964
|
+
}
|
|
9965
|
+
],
|
|
9966
|
+
"bodyDescribed": false,
|
|
9967
|
+
"bodyRef": null,
|
|
9968
|
+
"credential": "siteScoped"
|
|
9969
|
+
},
|
|
9619
9970
|
{
|
|
9620
9971
|
"id": "get:/api/sites/{siteId}/payment-gateways",
|
|
9621
9972
|
"method": "GET",
|
|
@@ -423,6 +423,35 @@ function one(c, t) {
|
|
|
423
423
|
// Mobile gets one column by name. The flex path's `flexDirection: column`
|
|
424
424
|
// says nothing to a grid, and a mobile answer that silently does nothing
|
|
425
425
|
// is rule 3 failing with a value in the document to prove it tried.
|
|
426
|
+
// A PACKED ROW IS NOT A GRID AND NOT AN EQUAL-SHARE ROW. It wraps, so it
|
|
427
|
+
// needs no stack breakpoint of its own — a menu that runs out of width
|
|
428
|
+
// starts a second line, which is what a menu should do.
|
|
429
|
+
if (c.pack) {
|
|
430
|
+
return {
|
|
431
|
+
type: 'flex-block',
|
|
432
|
+
style: {
|
|
433
|
+
width: '100%',
|
|
434
|
+
display: 'flex',
|
|
435
|
+
flexDirection: 'row',
|
|
436
|
+
flexWrap: 'wrap',
|
|
437
|
+
alignItems: 'center',
|
|
438
|
+
gap: '24px',
|
|
439
|
+
...(c.align === 'center' ? { justifyContent: 'center' } : {}),
|
|
440
|
+
},
|
|
441
|
+
// `width: auto` IS THE LOAD-BEARING HALF, and leaving it out looked
|
|
442
|
+
// like the whole idea had failed. `flex-block` seeds `width: 100%`
|
|
443
|
+
// from its element defaults, and `flex: 0 0 auto` only says "do not
|
|
444
|
+
// grow or shrink from the BASIS" — the basis being `auto`, which
|
|
445
|
+
// reads the width. So every packed cell stayed 100% wide and the
|
|
446
|
+
// menu came out as a vertical list: measured, three links stacked in
|
|
447
|
+
// a 140px header where the equal-share version had been 52.
|
|
448
|
+
children: kids.map((k) => ({
|
|
449
|
+
type: 'flex-block',
|
|
450
|
+
style: { flex: '0 0 auto', width: 'auto', display: 'flex', flexDirection: 'column' },
|
|
451
|
+
children: [k],
|
|
452
|
+
})),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
426
455
|
if (c.wrap) {
|
|
427
456
|
return {
|
|
428
457
|
type: 'flex-block',
|
|
@@ -654,7 +683,8 @@ export function navSpec(links, t) {
|
|
|
654
683
|
{
|
|
655
684
|
kind: 'group',
|
|
656
685
|
direction: 'row',
|
|
657
|
-
|
|
686
|
+
// PACKED, not an equal share: a menu's links sit against each other.
|
|
687
|
+
pack: true,
|
|
658
688
|
children: links.map((l) => ({
|
|
659
689
|
kind: 'button',
|
|
660
690
|
variant: 'link',
|
package/dist/server.js
CHANGED
|
@@ -70,7 +70,7 @@ export function createServer(ctx = buildContext()) {
|
|
|
70
70
|
registerApiTools(server, ctx);
|
|
71
71
|
const pageSession = registerPageTools(server, ctx);
|
|
72
72
|
registerLiveTools(server, ctx, pageSession);
|
|
73
|
-
registerStoreTools(server, ctx);
|
|
73
|
+
registerStoreTools(server, ctx, pageSession);
|
|
74
74
|
registerThemeTools(server, ctx);
|
|
75
75
|
registerImportTools(server, ctx, pageSession);
|
|
76
76
|
registerUndoTools(server, ctx);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { request } from '../transport/http.js';
|
|
2
|
+
import { siteToken } from './credentialpick.js';
|
|
3
|
+
import { siteFor } from './context.js';
|
|
4
|
+
import { addSubtree } from '../domains/site/builder.js';
|
|
5
|
+
import { menuLabel, navSpec, tokensFromPage } from '../domains/site/importmap.js';
|
|
6
|
+
import { globalDocumentFrom } from './importpage.js';
|
|
7
|
+
/** Does this site already share a section of this kind? */
|
|
8
|
+
export async function hasGlobal(ctx, siteId, kind) {
|
|
9
|
+
const got = (await request({
|
|
10
|
+
base: ctx.base,
|
|
11
|
+
method: 'GET',
|
|
12
|
+
path: `/api/sites/${encodeURIComponent(siteId)}/global-sections`,
|
|
13
|
+
token: siteToken(ctx),
|
|
14
|
+
fetchImpl: ctx.fetchImpl,
|
|
15
|
+
}));
|
|
16
|
+
return (got.globalSections ?? []).some((g) => g.kind === kind);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create ONE global section from a link list and give every named page a
|
|
20
|
+
* reference to it.
|
|
21
|
+
*
|
|
22
|
+
* NOT ATOMIC, and it must not pretend to be: one page that will not take the
|
|
23
|
+
* header does not undo the header. The master exists, the others carry it, and
|
|
24
|
+
* the refusal is reported per page — the same shape `sb_import_site` uses for
|
|
25
|
+
* its own per-page report.
|
|
26
|
+
*/
|
|
27
|
+
export async function shareChrome(ctx, session, siteId, kind, links, pages, tokens) {
|
|
28
|
+
const out = { carried: [], failed: [] };
|
|
29
|
+
const spec = navSpec(links, tokens);
|
|
30
|
+
if (!spec) {
|
|
31
|
+
out.skipped = 'no links to put in it';
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
const made = (await request({
|
|
35
|
+
base: ctx.base,
|
|
36
|
+
method: 'POST',
|
|
37
|
+
path: `/api/sites/${encodeURIComponent(siteId)}/global-sections`,
|
|
38
|
+
token: siteToken(ctx),
|
|
39
|
+
body: {
|
|
40
|
+
name: kind === 'header' ? 'Header' : 'Footer',
|
|
41
|
+
kind,
|
|
42
|
+
document: globalDocumentFrom(spec),
|
|
43
|
+
},
|
|
44
|
+
fetchImpl: ctx.fetchImpl,
|
|
45
|
+
}));
|
|
46
|
+
const gid = made.globalSection?.id;
|
|
47
|
+
if (typeof gid !== 'string' || !gid) {
|
|
48
|
+
out.skipped = 'the platform created no global section';
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
out.created = gid;
|
|
52
|
+
for (const p of pages) {
|
|
53
|
+
try {
|
|
54
|
+
await session.open(siteId, p.id);
|
|
55
|
+
const doc = session.current();
|
|
56
|
+
// A HEADER GOES IN FIRST AND A FOOTER LAST. ROOT's children must read
|
|
57
|
+
// [header*][middle*][footer*] and the platform refuses EVERY save
|
|
58
|
+
// otherwise — compose turns the reference into a real band, so its index
|
|
59
|
+
// is the band it becomes.
|
|
60
|
+
const at = kind === 'header' ? 0 : doc.doc.nodes[doc.doc.root_node_id]?.data?.nodes?.length ?? 0;
|
|
61
|
+
const { patches } = addSubtree(doc, doc.doc.root_node_id, { type: 'flex-section', specials: { globalRef: gid, globalKind: kind } }, at);
|
|
62
|
+
await session.applyAndSave(patches);
|
|
63
|
+
out.carried.push(p.slug);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
out.failed.push({ page: p.slug, why: e.message.replace(/^sbuilder:\s*/, '').slice(0, 160) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** Every page this site has, as the menu would name them. */
|
|
72
|
+
export async function sitePages(ctx, siteId) {
|
|
73
|
+
const got = (await request({
|
|
74
|
+
base: ctx.base,
|
|
75
|
+
method: 'GET',
|
|
76
|
+
path: `/api/sites/${encodeURIComponent(siteId)}/pages`,
|
|
77
|
+
token: siteToken(ctx),
|
|
78
|
+
fetchImpl: ctx.fetchImpl,
|
|
79
|
+
}));
|
|
80
|
+
return (got.pages ?? []).map((p) => ({
|
|
81
|
+
id: String(p.id ?? ''),
|
|
82
|
+
slug: String(p.slug ?? ''),
|
|
83
|
+
name: String(p.name ?? p.title ?? p.slug ?? ''),
|
|
84
|
+
isHome: p.isHomepage === true || p.slug === '',
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
/** The link list a menu built from these pages would carry, home first. */
|
|
88
|
+
export function chromeLinks(pages) {
|
|
89
|
+
return [...pages]
|
|
90
|
+
.sort((a, b) => Number(b.isHome) - Number(a.isHome))
|
|
91
|
+
.map((p) => ({ text: menuLabel(p.name), href: p.isHome ? '/' : `/${p.slug}` }));
|
|
92
|
+
}
|
|
93
|
+
export { siteFor, tokensFromPage };
|
package/dist/tools/store.js
CHANGED
|
@@ -33,6 +33,8 @@ import { request, redact } from '../transport/http.js';
|
|
|
33
33
|
import { siteToken } from './credentialpick.js';
|
|
34
34
|
import { siteFor } from './context.js';
|
|
35
35
|
import { genId } from '../domains/site/ids.js';
|
|
36
|
+
import { chromeLinks, hasGlobal, shareChrome, sitePages } from './chrome.js';
|
|
37
|
+
import { tokensFromPage } from '../domains/site/importmap.js';
|
|
36
38
|
import { CHECKOUT_FORM, CHECKOUT_FORM_DOCUMENT, CHECKOUT_PAGE_DOCUMENT, CHECKOUT_TEXT, FORM_ID_SENTINEL, HEADLINE_SENTINEL, FORM_TEMPLATES, } from '../catalog/checkout.generated.js';
|
|
37
39
|
/**
|
|
38
40
|
* The store's live gateways and delivery options.
|
|
@@ -274,17 +276,18 @@ async function seedForm(ctx, siteId, key, formName, dryRun) {
|
|
|
274
276
|
'what makes a form-document edit visible.',
|
|
275
277
|
};
|
|
276
278
|
}
|
|
277
|
-
export function registerStoreTools(server, ctx) {
|
|
279
|
+
export function registerStoreTools(server, ctx, session) {
|
|
278
280
|
server.registerTool('sb_store', {
|
|
279
281
|
description: 'Run a store flow that must happen in a fixed order. action:"checkout" makes the order ' +
|
|
280
282
|
'form, configures it, saves its fields with this store\'s real payment and delivery ' +
|
|
281
283
|
'options, then creates and PUBLISHES the checkout page — /checkout 404s without all ' +
|
|
282
284
|
'four. action:"form" seeds any of the platform\'s other form templates (login, ' +
|
|
283
285
|
'register, forgot, reset, verify, contact, subscribe, booking, review and more) with ' +
|
|
284
|
-
'its own field document, which is the part that cannot be guessed.
|
|
285
|
-
'the
|
|
286
|
+
'its own field document, which is the part that cannot be guessed. action:"chrome" ' +
|
|
287
|
+
'gives every page ONE shared header, built from the pages this site already has — the ' +
|
|
288
|
+
'gap sb_review reports as siteChrome. Dry run returns the plan.',
|
|
286
289
|
inputSchema: {
|
|
287
|
-
action: z.enum(['checkout', 'form']),
|
|
290
|
+
action: z.enum(['checkout', 'form', 'chrome']),
|
|
288
291
|
site_id: z.string().optional(),
|
|
289
292
|
language: z.enum(['vi', 'en']).optional().describe('Copy language, default vi'),
|
|
290
293
|
page_name: z.string().optional(),
|
|
@@ -294,11 +297,55 @@ export function registerStoreTools(server, ctx) {
|
|
|
294
297
|
.optional()
|
|
295
298
|
.describe('action:"form" — which of the platform\'s own form templates to seed'),
|
|
296
299
|
name: z.string().optional().describe('action:"form" — the form\'s name in the merchant\'s list'),
|
|
300
|
+
footer: z
|
|
301
|
+
.boolean()
|
|
302
|
+
.optional()
|
|
303
|
+
.describe('action:"chrome" — build a shared FOOTER instead of a header'),
|
|
297
304
|
dry_run: z.boolean().optional(),
|
|
298
305
|
},
|
|
299
306
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
300
|
-
}, async ({ action, site_id: given, language, page_name, headline, template, name, dry_run }) => {
|
|
307
|
+
}, async ({ action, site_id: given, language, page_name, headline, template, name, footer, dry_run }) => {
|
|
301
308
|
const siteId = siteFor(ctx, given);
|
|
309
|
+
if (action === 'chrome') {
|
|
310
|
+
// SKIPPED WHEN THE SITE ALREADY SHARES ONE, because a second header is
|
|
311
|
+
// two headers rather than a menu — and below two pages, because a menu
|
|
312
|
+
// to one page is a link to itself. Both are `sb_import_site`'s own
|
|
313
|
+
// rules, kept because they were right there.
|
|
314
|
+
const kind = footer === true ? 'footer' : 'header';
|
|
315
|
+
if (await hasGlobal(ctx, siteId, kind)) {
|
|
316
|
+
return text({
|
|
317
|
+
skipped: `this site already shares a ${kind} — a second one is two of them, not a menu`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const pages = await sitePages(ctx, siteId);
|
|
321
|
+
if (pages.length < 2) {
|
|
322
|
+
return text({ skipped: 'a menu to one page is a link to itself' });
|
|
323
|
+
}
|
|
324
|
+
const links = chromeLinks(pages);
|
|
325
|
+
// RULE 0: the look comes off the HOME page, which is the one page whose
|
|
326
|
+
// pattern the rest of the site already follows. A site with nothing on
|
|
327
|
+
// its home page yields no tokens rather than an invented palette.
|
|
328
|
+
const home = pages.find((p) => p.isHome) ?? pages[0];
|
|
329
|
+
await session.open(siteId, home.id);
|
|
330
|
+
const tokens = tokensFromPage(session.current().doc);
|
|
331
|
+
if (dry_run !== false) {
|
|
332
|
+
return text({
|
|
333
|
+
dry_run: true,
|
|
334
|
+
would_create: kind,
|
|
335
|
+
menu: links,
|
|
336
|
+
onto: pages.map((p) => p.slug),
|
|
337
|
+
tokens_from: Object.keys(tokens).length ? 'the home page' : 'nothing — the home page is blank',
|
|
338
|
+
note: 'Nothing was sent. This creates ONE shared master and gives every page a reference ' +
|
|
339
|
+
'to it, so the menu becomes one edit instead of one per page.',
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const out = await shareChrome(ctx, session, siteId, kind, links, pages, tokens);
|
|
343
|
+
return text({
|
|
344
|
+
...out,
|
|
345
|
+
next: 'Publish the pages: a global section reaches a visitor through each page it is ' +
|
|
346
|
+
'composed onto, so a saved page keeps the old chrome until it is published again.',
|
|
347
|
+
});
|
|
348
|
+
}
|
|
302
349
|
if (action === 'form') {
|
|
303
350
|
if (!template) {
|
|
304
351
|
throw new Error(`sbuilder: action:"form" needs a template. One of: ${FORM_TEMPLATE_KEYS.join(', ')}.`);
|
package/dist/tools/undo.js
CHANGED
|
@@ -113,8 +113,12 @@ export class UndoLog {
|
|
|
113
113
|
*/
|
|
114
114
|
export function registerUndoTools(server, ctx) {
|
|
115
115
|
server.registerTool('sb_undo', {
|
|
116
|
-
description: 'Put back what a PUT through sb_api_call replaced
|
|
117
|
-
'
|
|
116
|
+
description: 'Put back what a PUT through sb_api_call replaced — settings, a product, a form, ' +
|
|
117
|
+
'anything with a shape. IN THIS PROCESS ONLY, capped, and gone when it exits. For a ' +
|
|
118
|
+
'PAGE the platform keeps its own: GET .../pages/{pageId}/history lists the autosave ' +
|
|
119
|
+
'checkpoint it writes on every draft save, versions lists the labelled snapshots, and ' +
|
|
120
|
+
'either restores. That one survives everything and is the better answer whenever the ' +
|
|
121
|
+
'thing to recover is a page. No argument lists what is undoable here.',
|
|
118
122
|
inputSchema: {
|
|
119
123
|
index: z.number().int().min(1).optional().describe('1 is the most recent write'),
|
|
120
124
|
dry_run: z.boolean().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.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",
|