@bettercms-ai/mcp 0.70.0 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +504 -76
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +16 -7
- package/dist/skills/bettercms/references/animation.md +251 -0
- package/dist/skills/bettercms/references/astro.md +47 -2
- package/dist/skills/bettercms/references/bindings.md +47 -4
- package/dist/skills/bettercms/references/componentize.md +65 -6
- package/dist/skills/bettercms/references/components.md +50 -11
- package/dist/skills/bettercms/references/content-modeling.md +14 -4
- package/dist/skills/bettercms/references/detection.md +2 -2
- package/dist/skills/bettercms/references/finish-site.md +188 -3
- package/dist/skills/bettercms/references/forms-and-interactions.md +7 -0
- package/dist/skills/bettercms/references/getting-started.md +2 -0
- package/dist/skills/bettercms/references/media-and-brand.md +39 -1
- package/dist/skills/bettercms/references/next.md +63 -3
- package/dist/skills/bettercms/references/publishing.md +100 -1
- package/dist/skills/bettercms/references/seo.md +2 -1
- package/dist/skills/bettercms/references/worked-examples.md +4 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
|
|
17
17
|
// ../types/src/component.ts
|
|
18
|
-
var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: inside a component, ONLY the leaves a declared prop TARGETS are click-to-edit on the canvas \u2014 each declared prop's `target` is re-keyed to that PLACEMENT's own override address, so editing it changes this page and not the shared definition. A prop with no `target` still SHOWS as a dock control, but its override reaches no rendered slot, so editing it changes nothing on the page. Copy that no prop points at is worse still: no binding and no control, unreachable from click-to-edit AND from the dock, changeable only by editing the component definition, which rewrites every page that places it. A `section` block's children stay click-to-edit unconditionally. So when you choose a component, DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. An IMPORTED site's components are its own (read the repo); its nav, footer and modals are the Layout. On a blank site, do not hand-write a band's JSON: start from a built-in section blueprint (list_components returns locked `builtin:*` blueprints with no projectId \u2014 hero-centered, hero-split, feature-grid-three, cta-banner and
|
|
18
|
+
var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: inside a component, ONLY the leaves a declared prop TARGETS are click-to-edit on the canvas \u2014 each declared prop's `target` is re-keyed to that PLACEMENT's own override address, so editing it changes this page and not the shared definition. A prop with no `target` still SHOWS as a dock control, but its override reaches no rendered slot, so editing it changes nothing on the page. Copy that no prop points at is worse still: no binding and no control, unreachable from click-to-edit AND from the dock, changeable only by editing the component definition, which rewrites every page that places it. A `section` block's children stay click-to-edit unconditionally. So when you choose a component, DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. An IMPORTED site's components are its own (read the repo); its nav, footer and modals are the Layout. On a blank site, do not hand-write a band's JSON: start from a built-in section blueprint (list_components returns locked `builtin:*` blueprints with no projectId \u2014 hero-centered, hero-split, feature-grid-three, cta-banner and more), each already rooted in a `section` block with its editable leaves declared as props. INLINE its blockJson as a `section` block for a one-off band; for a recurring band, materialize it with create_component `from` so it becomes project-scoped before implementation validation, Output or publication. Direct `builtin:*` component references exist only for legacy delivery compatibility. Two consecutive call-to-action buttons are two sibling `button` blocks inside the same section \u2014 never a `columns` block, which is a `repeat(N,1fr)` grid and would stretch each CTA to half the container. Buttons are inline-level and flow side by side on their own.";
|
|
19
19
|
var COMPONENT_PROP_TYPES = [
|
|
20
20
|
"text",
|
|
21
21
|
"richtext",
|
|
@@ -36,7 +36,32 @@ var COMPONENT_PROP_TYPES = [
|
|
|
36
36
|
"color",
|
|
37
37
|
"file",
|
|
38
38
|
"icon",
|
|
39
|
-
"multiselect"
|
|
39
|
+
"multiselect",
|
|
40
|
+
// V2 schema language (F1.x). `object` is ONE nested value of named `config.fields`, stored as
|
|
41
|
+
// `{ [childKey]: value }` like the model field `object`; the six after it store what the
|
|
42
|
+
// same-named content-model FIELD stores (`location` is the V2 geopoint) and are judged by
|
|
43
|
+
// that field's validator.
|
|
44
|
+
"object",
|
|
45
|
+
"datetime",
|
|
46
|
+
"slug",
|
|
47
|
+
"email",
|
|
48
|
+
"phone",
|
|
49
|
+
"json",
|
|
50
|
+
"location",
|
|
51
|
+
// V2 leaves, each storing what the same-named content-model FIELD stores: a taxonomy string[],
|
|
52
|
+
// a rating number, a code value (`{ language?, code }` or a string).
|
|
53
|
+
"tags",
|
|
54
|
+
"rating",
|
|
55
|
+
"code",
|
|
56
|
+
// CMS items, as the same-named content-model FIELDS store them: an entry id (or a polymorphic
|
|
57
|
+
// `{ entryId, modelSlug }`), and a list of them. Delivery hydrates them like an entry's.
|
|
58
|
+
"reference",
|
|
59
|
+
"multi-reference",
|
|
60
|
+
// V2 chrome types, each storing what the same-named content-model FIELD stores: a video value,
|
|
61
|
+
// a primitive list (`config.itemType`), and multi-line text (the field lane's V2 `text`).
|
|
62
|
+
"video",
|
|
63
|
+
"array",
|
|
64
|
+
"longtext"
|
|
40
65
|
];
|
|
41
66
|
|
|
42
67
|
// ../types/src/layout-lucide-icons.ts
|
|
@@ -2003,6 +2028,7 @@ var LAYOUT_SECTION_ICON_SET = new Set(LAYOUT_SECTION_ICONS);
|
|
|
2003
2028
|
|
|
2004
2029
|
// ../types/src/dock-icon-names.ts
|
|
2005
2030
|
var DOCK_ICON_NAMES = [
|
|
2031
|
+
"abc",
|
|
2006
2032
|
"access-denied",
|
|
2007
2033
|
"activity",
|
|
2008
2034
|
"add",
|
|
@@ -2010,14 +2036,40 @@ var DOCK_ICON_NAMES = [
|
|
|
2010
2036
|
"add-comment",
|
|
2011
2037
|
"add-document",
|
|
2012
2038
|
"add-user",
|
|
2039
|
+
"address-book",
|
|
2040
|
+
"adjustments",
|
|
2041
|
+
"adjustments-horizontal",
|
|
2042
|
+
"alarm",
|
|
2043
|
+
"alert-triangle",
|
|
2044
|
+
"align-left",
|
|
2013
2045
|
"api",
|
|
2046
|
+
"app-window",
|
|
2047
|
+
"apple",
|
|
2048
|
+
"apps",
|
|
2014
2049
|
"archive",
|
|
2050
|
+
"arrow-back-up",
|
|
2015
2051
|
"arrow-down",
|
|
2052
|
+
"arrow-guide",
|
|
2016
2053
|
"arrow-left",
|
|
2017
2054
|
"arrow-right",
|
|
2018
2055
|
"arrow-top-right",
|
|
2019
2056
|
"arrow-up",
|
|
2057
|
+
"arrows-diagonal",
|
|
2058
|
+
"arrows-down",
|
|
2059
|
+
"arrows-exchange",
|
|
2060
|
+
"arrows-horizontal",
|
|
2061
|
+
"arrows-left-right",
|
|
2062
|
+
"arrows-shuffle",
|
|
2063
|
+
"arrows-sort",
|
|
2064
|
+
"article",
|
|
2065
|
+
"aspect-ratio",
|
|
2020
2066
|
"asterisk",
|
|
2067
|
+
"award",
|
|
2068
|
+
"axis-x",
|
|
2069
|
+
"backpack",
|
|
2070
|
+
"badge",
|
|
2071
|
+
"badge-cc",
|
|
2072
|
+
"badges",
|
|
2021
2073
|
"bar-chart",
|
|
2022
2074
|
"basket",
|
|
2023
2075
|
"bell",
|
|
@@ -2029,25 +2081,76 @@ var DOCK_ICON_NAMES = [
|
|
|
2029
2081
|
"bold",
|
|
2030
2082
|
"bolt",
|
|
2031
2083
|
"book",
|
|
2084
|
+
"book-2",
|
|
2032
2085
|
"bookmark",
|
|
2033
2086
|
"bookmark-filled",
|
|
2087
|
+
"books",
|
|
2034
2088
|
"bottle",
|
|
2089
|
+
"box",
|
|
2090
|
+
"brand-dribbble",
|
|
2091
|
+
"brand-facebook",
|
|
2092
|
+
"brand-instagram",
|
|
2093
|
+
"brand-linkedin",
|
|
2094
|
+
"brand-pinterest",
|
|
2095
|
+
"brand-tiktok",
|
|
2096
|
+
"brand-x",
|
|
2097
|
+
"brand-youtube",
|
|
2098
|
+
"briefcase",
|
|
2099
|
+
"broadcast",
|
|
2100
|
+
"browser",
|
|
2035
2101
|
"bug",
|
|
2102
|
+
"building",
|
|
2103
|
+
"building-skyscraper",
|
|
2104
|
+
"building-store",
|
|
2036
2105
|
"bulb-filled",
|
|
2037
2106
|
"bulb-outline",
|
|
2107
|
+
"calculator",
|
|
2038
2108
|
"calendar",
|
|
2109
|
+
"calendar-dollar",
|
|
2110
|
+
"calendar-event",
|
|
2111
|
+
"calendar-exclamation",
|
|
2112
|
+
"calendar-off",
|
|
2113
|
+
"calendar-plus",
|
|
2114
|
+
"calendar-star",
|
|
2115
|
+
"calendar-time",
|
|
2116
|
+
"camera",
|
|
2117
|
+
"campfire",
|
|
2118
|
+
"carousel-horizontal",
|
|
2039
2119
|
"case",
|
|
2120
|
+
"cash",
|
|
2121
|
+
"category",
|
|
2122
|
+
"category-2",
|
|
2123
|
+
"chart-area",
|
|
2124
|
+
"chart-arrows",
|
|
2125
|
+
"chart-bar",
|
|
2126
|
+
"chart-bar-popular",
|
|
2127
|
+
"chart-donut",
|
|
2128
|
+
"chart-dots",
|
|
2129
|
+
"chart-line",
|
|
2040
2130
|
"chart-upward",
|
|
2131
|
+
"check",
|
|
2132
|
+
"checkbox",
|
|
2133
|
+
"checklist",
|
|
2041
2134
|
"checkmark",
|
|
2042
2135
|
"checkmark-circle",
|
|
2136
|
+
"chef-hat",
|
|
2043
2137
|
"chevron-down",
|
|
2044
2138
|
"chevron-left",
|
|
2045
2139
|
"chevron-right",
|
|
2046
2140
|
"chevron-up",
|
|
2141
|
+
"chevrons-right",
|
|
2047
2142
|
"circle",
|
|
2143
|
+
"circle-check",
|
|
2144
|
+
"circle-dot",
|
|
2145
|
+
"circle-number-1",
|
|
2146
|
+
"circle-plus",
|
|
2147
|
+
"click",
|
|
2048
2148
|
"clipboard",
|
|
2049
2149
|
"clipboard-image",
|
|
2150
|
+
"clipboard-text",
|
|
2050
2151
|
"clock",
|
|
2152
|
+
"clock-check",
|
|
2153
|
+
"clock-hour-4",
|
|
2051
2154
|
"close",
|
|
2052
2155
|
"close-circle",
|
|
2053
2156
|
"code",
|
|
@@ -2055,20 +2158,31 @@ var DOCK_ICON_NAMES = [
|
|
|
2055
2158
|
"cog",
|
|
2056
2159
|
"collapse",
|
|
2057
2160
|
"color-wheel",
|
|
2161
|
+
"columns",
|
|
2162
|
+
"columns-2",
|
|
2163
|
+
"columns-3",
|
|
2058
2164
|
"comment",
|
|
2059
2165
|
"component",
|
|
2060
2166
|
"compose",
|
|
2061
2167
|
"compose-sparkles",
|
|
2062
2168
|
"confetti",
|
|
2063
2169
|
"controls",
|
|
2170
|
+
"cookie",
|
|
2064
2171
|
"copy",
|
|
2065
2172
|
"credit-card",
|
|
2066
2173
|
"crop",
|
|
2067
2174
|
"cube",
|
|
2175
|
+
"currency-dollar",
|
|
2068
2176
|
"dashboard",
|
|
2069
2177
|
"database",
|
|
2178
|
+
"database-search",
|
|
2070
2179
|
"desktop",
|
|
2180
|
+
"device-desktop",
|
|
2181
|
+
"device-mobile",
|
|
2182
|
+
"device-tablet",
|
|
2183
|
+
"devices",
|
|
2071
2184
|
"diamond",
|
|
2185
|
+
"discount",
|
|
2072
2186
|
"document",
|
|
2073
2187
|
"document-pdf",
|
|
2074
2188
|
"document-remove",
|
|
@@ -2078,6 +2192,7 @@ var DOCK_ICON_NAMES = [
|
|
|
2078
2192
|
"document-word",
|
|
2079
2193
|
"document-zip",
|
|
2080
2194
|
"documents",
|
|
2195
|
+
"door-exit",
|
|
2081
2196
|
"dot",
|
|
2082
2197
|
"double-chevron-down",
|
|
2083
2198
|
"double-chevron-left",
|
|
@@ -2087,9 +2202,12 @@ var DOCK_ICON_NAMES = [
|
|
|
2087
2202
|
"download",
|
|
2088
2203
|
"drag-handle",
|
|
2089
2204
|
"drop",
|
|
2205
|
+
"droplet",
|
|
2206
|
+
"droplet-half",
|
|
2090
2207
|
"earth-americas",
|
|
2091
2208
|
"earth-globe",
|
|
2092
2209
|
"edit",
|
|
2210
|
+
"egg",
|
|
2093
2211
|
"ellipsis-horizontal",
|
|
2094
2212
|
"ellipsis-vertical",
|
|
2095
2213
|
"empty",
|
|
@@ -2102,77 +2220,169 @@ var DOCK_ICON_NAMES = [
|
|
|
2102
2220
|
"error-screen",
|
|
2103
2221
|
"expand",
|
|
2104
2222
|
"eye-closed",
|
|
2223
|
+
"eye-off",
|
|
2105
2224
|
"eye-open",
|
|
2106
2225
|
"face-happy",
|
|
2107
2226
|
"face-indifferent",
|
|
2108
2227
|
"face-sad",
|
|
2228
|
+
"feather",
|
|
2109
2229
|
"feedback",
|
|
2230
|
+
"file",
|
|
2231
|
+
"file-certificate",
|
|
2232
|
+
"file-code",
|
|
2233
|
+
"file-download",
|
|
2234
|
+
"file-text",
|
|
2110
2235
|
"filter",
|
|
2236
|
+
"flag",
|
|
2237
|
+
"flame",
|
|
2238
|
+
"flask",
|
|
2239
|
+
"focus-2",
|
|
2111
2240
|
"folder",
|
|
2241
|
+
"folders",
|
|
2242
|
+
"forms",
|
|
2243
|
+
"frame",
|
|
2112
2244
|
"generate",
|
|
2245
|
+
"gift",
|
|
2246
|
+
"git-branch",
|
|
2113
2247
|
"github",
|
|
2248
|
+
"grid-dots",
|
|
2114
2249
|
"groq",
|
|
2250
|
+
"hammer",
|
|
2251
|
+
"hand-finger",
|
|
2252
|
+
"hanger",
|
|
2115
2253
|
"hash",
|
|
2254
|
+
"headphones",
|
|
2255
|
+
"headset",
|
|
2116
2256
|
"heart",
|
|
2117
2257
|
"heart-filled",
|
|
2258
|
+
"heart-handshake",
|
|
2118
2259
|
"help-circle",
|
|
2119
2260
|
"highlight",
|
|
2261
|
+
"history",
|
|
2120
2262
|
"home",
|
|
2263
|
+
"hourglass",
|
|
2121
2264
|
"ice-cream",
|
|
2265
|
+
"id-badge",
|
|
2266
|
+
"id-badge-2",
|
|
2122
2267
|
"image",
|
|
2123
2268
|
"image-remove",
|
|
2124
2269
|
"images",
|
|
2125
2270
|
"inbox",
|
|
2271
|
+
"infinity",
|
|
2272
|
+
"info-circle",
|
|
2126
2273
|
"info-filled",
|
|
2127
2274
|
"info-outline",
|
|
2275
|
+
"info-square-rounded",
|
|
2128
2276
|
"inline",
|
|
2129
2277
|
"inline-element",
|
|
2278
|
+
"input-search",
|
|
2130
2279
|
"insert-above",
|
|
2131
2280
|
"insert-below",
|
|
2132
2281
|
"italic",
|
|
2133
2282
|
"joystick",
|
|
2134
2283
|
"json",
|
|
2284
|
+
"language",
|
|
2135
2285
|
"launch",
|
|
2286
|
+
"layout",
|
|
2287
|
+
"layout-align-center",
|
|
2288
|
+
"layout-board",
|
|
2289
|
+
"layout-board-split",
|
|
2290
|
+
"layout-bottombar",
|
|
2291
|
+
"layout-bottombar-expand",
|
|
2292
|
+
"layout-cards",
|
|
2293
|
+
"layout-columns",
|
|
2294
|
+
"layout-dashboard",
|
|
2295
|
+
"layout-grid",
|
|
2296
|
+
"layout-list",
|
|
2297
|
+
"layout-navbar",
|
|
2298
|
+
"layout-rows",
|
|
2299
|
+
"layout-sidebar",
|
|
2300
|
+
"layout-sidebar-right",
|
|
2301
|
+
"leaf",
|
|
2136
2302
|
"leave",
|
|
2137
2303
|
"lemon",
|
|
2304
|
+
"library-photo",
|
|
2305
|
+
"lifebuoy",
|
|
2138
2306
|
"link",
|
|
2139
2307
|
"link-removed",
|
|
2140
2308
|
"linkedin",
|
|
2141
2309
|
"list",
|
|
2310
|
+
"list-check",
|
|
2311
|
+
"list-details",
|
|
2312
|
+
"list-numbers",
|
|
2313
|
+
"list-tree",
|
|
2142
2314
|
"lock",
|
|
2143
2315
|
"logo-js",
|
|
2144
2316
|
"logo-ts",
|
|
2317
|
+
"mail",
|
|
2318
|
+
"map-2",
|
|
2319
|
+
"map-pin",
|
|
2320
|
+
"map-pins",
|
|
2145
2321
|
"marker",
|
|
2146
2322
|
"marker-removed",
|
|
2147
2323
|
"master-detail",
|
|
2324
|
+
"math-function",
|
|
2325
|
+
"meat",
|
|
2148
2326
|
"menu",
|
|
2327
|
+
"message",
|
|
2328
|
+
"message-2",
|
|
2329
|
+
"message-check",
|
|
2330
|
+
"message-circle",
|
|
2331
|
+
"message-circle-2",
|
|
2332
|
+
"message-plus",
|
|
2333
|
+
"message-star",
|
|
2149
2334
|
"microphone",
|
|
2150
2335
|
"microphone-slash",
|
|
2151
2336
|
"mobile-device",
|
|
2337
|
+
"mood-empty",
|
|
2152
2338
|
"moon",
|
|
2339
|
+
"movie",
|
|
2340
|
+
"news",
|
|
2341
|
+
"notes",
|
|
2153
2342
|
"number",
|
|
2154
2343
|
"ok-hand",
|
|
2155
2344
|
"olist",
|
|
2156
2345
|
"overage",
|
|
2157
2346
|
"package",
|
|
2347
|
+
"packages",
|
|
2348
|
+
"palette",
|
|
2158
2349
|
"panel-left",
|
|
2159
2350
|
"panel-right",
|
|
2351
|
+
"paperclip",
|
|
2160
2352
|
"pause",
|
|
2353
|
+
"pencil",
|
|
2354
|
+
"phone",
|
|
2355
|
+
"photo",
|
|
2356
|
+
"photo-scan",
|
|
2357
|
+
"photo-search",
|
|
2358
|
+
"pig-money",
|
|
2161
2359
|
"pin",
|
|
2162
2360
|
"pin-filled",
|
|
2163
2361
|
"pin-removed",
|
|
2164
2362
|
"play",
|
|
2363
|
+
"player-play",
|
|
2364
|
+
"player-track-next",
|
|
2365
|
+
"playlist-add",
|
|
2165
2366
|
"plug",
|
|
2367
|
+
"plug-connected",
|
|
2368
|
+
"plus-minus",
|
|
2369
|
+
"point",
|
|
2166
2370
|
"presentation",
|
|
2371
|
+
"printer",
|
|
2167
2372
|
"progress-50",
|
|
2168
2373
|
"progress-75",
|
|
2169
2374
|
"projects",
|
|
2170
2375
|
"publish",
|
|
2376
|
+
"qrcode",
|
|
2377
|
+
"quote",
|
|
2171
2378
|
"read-only",
|
|
2379
|
+
"receipt",
|
|
2380
|
+
"recycle",
|
|
2172
2381
|
"redo",
|
|
2173
2382
|
"refresh",
|
|
2174
2383
|
"remove",
|
|
2175
2384
|
"remove-circle",
|
|
2385
|
+
"repeat",
|
|
2176
2386
|
"reset",
|
|
2177
2387
|
"restore",
|
|
2178
2388
|
"retrieve",
|
|
@@ -2180,49 +2390,90 @@ var DOCK_ICON_NAMES = [
|
|
|
2180
2390
|
"revert",
|
|
2181
2391
|
"robot",
|
|
2182
2392
|
"rocket",
|
|
2393
|
+
"rosette",
|
|
2394
|
+
"route",
|
|
2395
|
+
"ruler",
|
|
2396
|
+
"ruler-measure",
|
|
2397
|
+
"salad",
|
|
2398
|
+
"scale",
|
|
2183
2399
|
"schema",
|
|
2400
|
+
"school",
|
|
2184
2401
|
"search",
|
|
2402
|
+
"section",
|
|
2185
2403
|
"select",
|
|
2404
|
+
"send",
|
|
2405
|
+
"server",
|
|
2406
|
+
"settings",
|
|
2186
2407
|
"share",
|
|
2408
|
+
"shield-check",
|
|
2409
|
+
"shield-lock",
|
|
2410
|
+
"shopping-bag",
|
|
2411
|
+
"shopping-cart",
|
|
2412
|
+
"shopping-cart-plus",
|
|
2413
|
+
"signature",
|
|
2414
|
+
"sitemap",
|
|
2415
|
+
"slideshow",
|
|
2187
2416
|
"sort",
|
|
2417
|
+
"sort-descending",
|
|
2418
|
+
"spacing-vertical",
|
|
2188
2419
|
"sparkle",
|
|
2189
2420
|
"sparkles",
|
|
2421
|
+
"speakerphone",
|
|
2190
2422
|
"spinner",
|
|
2191
2423
|
"split-horizontal",
|
|
2192
2424
|
"split-vertical",
|
|
2193
2425
|
"square",
|
|
2194
2426
|
"stack",
|
|
2427
|
+
"stack-2",
|
|
2195
2428
|
"stack-compact",
|
|
2429
|
+
"stairs",
|
|
2430
|
+
"stairs-up",
|
|
2196
2431
|
"star",
|
|
2197
2432
|
"star-filled",
|
|
2433
|
+
"stars",
|
|
2198
2434
|
"stop",
|
|
2199
2435
|
"strikethrough",
|
|
2200
2436
|
"string",
|
|
2437
|
+
"subtask",
|
|
2201
2438
|
"sun",
|
|
2439
|
+
"superscript",
|
|
2202
2440
|
"sync",
|
|
2441
|
+
"table",
|
|
2442
|
+
"table-options",
|
|
2203
2443
|
"tablet-device",
|
|
2204
2444
|
"tag",
|
|
2205
2445
|
"tags",
|
|
2206
2446
|
"target",
|
|
2207
2447
|
"task",
|
|
2448
|
+
"template",
|
|
2449
|
+
"tent",
|
|
2208
2450
|
"terminal",
|
|
2451
|
+
"terminal-2",
|
|
2209
2452
|
"text",
|
|
2210
2453
|
"th-large",
|
|
2211
2454
|
"th-list",
|
|
2455
|
+
"thumb-up",
|
|
2212
2456
|
"thumbs-down",
|
|
2213
2457
|
"thumbs-up",
|
|
2458
|
+
"ticket",
|
|
2214
2459
|
"tiers",
|
|
2215
2460
|
"timeline",
|
|
2216
2461
|
"toggle-arrow-right",
|
|
2462
|
+
"toggle-right",
|
|
2217
2463
|
"token",
|
|
2464
|
+
"tool",
|
|
2465
|
+
"tools-kitchen-2",
|
|
2218
2466
|
"transfer",
|
|
2219
2467
|
"translate",
|
|
2220
2468
|
"trash",
|
|
2221
2469
|
"trend-upward",
|
|
2222
2470
|
"triangle-outline",
|
|
2223
2471
|
"trolley",
|
|
2472
|
+
"truck",
|
|
2473
|
+
"truck-delivery",
|
|
2224
2474
|
"truncate",
|
|
2225
2475
|
"twitter",
|
|
2476
|
+
"typography",
|
|
2226
2477
|
"ulist",
|
|
2227
2478
|
"unarchive",
|
|
2228
2479
|
"underline",
|
|
@@ -2233,12 +2484,22 @@ var DOCK_ICON_NAMES = [
|
|
|
2233
2484
|
"unpublish",
|
|
2234
2485
|
"upload",
|
|
2235
2486
|
"user",
|
|
2487
|
+
"user-circle",
|
|
2488
|
+
"user-plus",
|
|
2489
|
+
"user-search",
|
|
2490
|
+
"user-star",
|
|
2236
2491
|
"users",
|
|
2492
|
+
"users-group",
|
|
2237
2493
|
"versions",
|
|
2238
2494
|
"video",
|
|
2495
|
+
"vocabulary",
|
|
2496
|
+
"vs",
|
|
2239
2497
|
"warning-filled",
|
|
2240
2498
|
"warning-outline",
|
|
2241
|
-
"
|
|
2499
|
+
"weight",
|
|
2500
|
+
"world",
|
|
2501
|
+
"wrench",
|
|
2502
|
+
"zoom-question"
|
|
2242
2503
|
];
|
|
2243
2504
|
var DOCK_ICON_NAME_SET = new Set(DOCK_ICON_NAMES);
|
|
2244
2505
|
|
|
@@ -2264,6 +2525,8 @@ never carries an icon.
|
|
|
2264
2525
|
Two vocabularies, on purpose: Layout section icons (update_layout \`add-section\`) stay Lucide names.
|
|
2265
2526
|
A name from one list is not valid in the other.
|
|
2266
2527
|
|
|
2528
|
+
The list includes the V2 kit's line-icon names (\`building\`, \`settings\`, \`layout-grid\`, \u2026) as themselves.
|
|
2529
|
+
|
|
2267
2530
|
${DOCK_ICON_NAMES.length} names:
|
|
2268
2531
|
|
|
2269
2532
|
${DOCK_ICON_NAMES.join(", ")}
|
|
@@ -2277,8 +2540,8 @@ var STRUCTURE_PLAYBOOK_URI = "bettercms://playbook/structure";
|
|
|
2277
2540
|
var STRUCTURE_DEFAULT_INSTRUCTION = `After you create collections or pages, organise them per the structure playbook: read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure (read-only), show the user its outline, then apply it with set_content_structure and the version it returned (the If-Match). A project that already has a structure keeps it: file only what you created, with move_to_folder, unless the user asks for a full re-organisation.`;
|
|
2278
2541
|
var SKILLS_ROUTING_INSTRUCTION = "Before writing code in a repo that uses BetterCMS, explore the repo, then read the installed `bettercms` skill (`npx @bettercms-ai/install --project -y` adds it if missing): its table turns what you found into the one reference the task needs \u2014 read only that. Without the skill installed, read the same files as resources: bettercms://skill/bettercms, then bettercms://skill/<reference>.";
|
|
2279
2542
|
var DOCK_UI_ROUTING_INSTRUCTION = "How fields and props look in the editor (groups, tabs, rows, icons, item names) is playbook \xA714 or the `field-ui` prompt: write it with update_content_model field* or update_component prop* plus ifMatch, read it back, and finish when get_next_steps reports no dock-* step.";
|
|
2280
|
-
var RECEIPT_FIRST_INSTRUCTION = "Start every project with get_next_steps: its `receipt` lists what is already done (`done`), what is still missing (`missing`, each item citing the count it reacted to) and the one `nextAction`. Work from that receipt instead of re-reading the site, call get_next_steps again after each batch of edits, and before you tell the user you are finished.
|
|
2281
|
-
var NEXT_STEPS_DESCRIPTION = `Call this FIRST on a project. Returns \`receipt\` \u2014 \`done\` (live release, bound elements, conversion coverage, composed pages, published components and Layout), \`missing\`, \`nextAction\` and \`notes\` (unjudged: unknown, not done) \u2014 plus \`data\`, the unfinished items
|
|
2543
|
+
var RECEIPT_FIRST_INSTRUCTION = "Start every project with get_next_steps: its `receipt` lists what is already done (`done`), what is still missing (`missing`, each item citing the count it reacted to) and the one `nextAction`. Work from that receipt instead of re-reading the site, call get_next_steps again after each batch of edits, and before you tell the user you are finished. On an imported site the gate is the definition of done: run `receipt.gate.nextAction`, read again, and repeat until `receipt.complete` is true. Record a gap you cannot close with editor_setup `exempt` only for an allowed reason that is true, with a note, never in prose.";
|
|
2544
|
+
var NEXT_STEPS_DESCRIPTION = `Call this FIRST on a project. Returns \`receipt\` \u2014 \`done\` (live release, bound elements, conversion coverage, composed pages, published components and Layout), \`missing\`, \`nextAction\` and \`notes\` (unjudged: unknown, not done) \u2014 plus \`data\`, the unfinished items by \`ruleId\` (skill finish-site lists them). Each item cites its count and may carry \`payload\` {rows, total}, at most 20 rows. \`receipt.complete\` is the setup gate's verdict and \`receipt.gate\` its numbers per built route. Run \`receipt.gate.nextAction\`, call it again after each batch; you are finished only when \`complete\` is true. \`organise-content\`: pages and collections are in no folder \u2014 read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure and, once the user agrees, set_content_structure.`;
|
|
2282
2545
|
var LAYOUT_CHROME_INSTRUCTION = 'When the user asks to make the navigation, footer or other site chrome editable, or get_next_steps lists a `layout-*` or `chrome-duplicated` item: this is playbook \xA711 item 3. Chrome is the project Layout, never page fields: in the source, wrap each Section in `data-bcms-layout-section` (the slot, its base slug) and mark each value `data-bcms-layout-field="layout:<sections[slug].id>:<fieldId>"`, never `data-bcms-field` inside it, then push; the strip removes the page copies, never delete them by hand. On a site BetterCMS serves, publish_layout writes Layout VALUES into the live pages; a structure change waits for a Rebuild. Never place nav, footer or modal blocks or fields on a page: the Layout (or the site\'s own layout) draws chrome, so a page copy either duplicates it or renders nowhere (get_componentize_plan reports one as LAYOUT_CHROME).';
|
|
2283
2546
|
var CTA_BUTTON_INSTRUCTION = "A call to action is ONE `button` field (value {label, href, variant?, size?, arrow?, icon?, newTab?}), rendered with BcmsButton from @bettercms-ai/astro or @bettercms-ai/next: never a text field beside a link field. A page that already holds such a pair (primaryCtaText + primaryCtaHref) becomes one button with update_field `mergeHref`. Inside a component a CTA is a `button` block whose text and href two props target.";
|
|
2284
2547
|
var STRUCTURE_EXAMPLE_PAYLOAD = {
|
|
@@ -2519,7 +2782,7 @@ function allKeys(fields) {
|
|
|
2519
2782
|
})
|
|
2520
2783
|
]);
|
|
2521
2784
|
}
|
|
2522
|
-
var CLEARABLE = ["helpText", "showIf", "ui", "placement", "readOnly", "lockReason"];
|
|
2785
|
+
var CLEARABLE = ["helpText", "showIf", "ui", "placement", "readOnly", "lockReason", "helpMore", "advanced", "recommendedMax", "openMode", "localized"];
|
|
2523
2786
|
var SETTABLE = ["label", "required", ...CLEARABLE];
|
|
2524
2787
|
function mergeConfig(current, patch) {
|
|
2525
2788
|
const out = { ...current ?? {} };
|
|
@@ -2557,7 +2820,7 @@ function planFieldEdit(model, edit, callerIfMatch) {
|
|
|
2557
2820
|
}
|
|
2558
2821
|
const changes = SETTABLE.filter((k) => edit[k] !== void 0);
|
|
2559
2822
|
if (!retype && rename === void 0 && edit.config === void 0 && changes.length === 0) {
|
|
2560
|
-
return { ok: false, error: "Nothing to change: pass label, helpText, required, config, showIf, ui, placement, readOnly, lockReason, type or key." };
|
|
2823
|
+
return { ok: false, error: "Nothing to change: pass label, helpText, required, config, showIf, ui, placement, readOnly, lockReason, helpMore, advanced, recommendedMax, openMode, localized, type or key." };
|
|
2561
2824
|
}
|
|
2562
2825
|
const echo = helpTextEcho(edit.helpText, { label: edit.label ?? current.label, defaultValue: current.defaultValue });
|
|
2563
2826
|
if (echo) return { ok: false, error: echo };
|
|
@@ -2673,7 +2936,8 @@ function listsTool(name, sets) {
|
|
|
2673
2936
|
}
|
|
2674
2937
|
|
|
2675
2938
|
// src/tool-text.ts
|
|
2676
|
-
var ADD_FIELD_DESCRIPTION = "Append ONE field to a content model (`modelId`) or to a page's schema (`pageId`) \u2014 additive, never overwrites; a key already in use is refused. For a section/zone add ONE 'group' (fixed block) or 'repeater' (repeating list) carrying its child `fields`, not the zone's inner fields as separate top-level fields. Pass `after` to insert it after an existing top-level field instead of at the end; `ui`, `helpText`, `showIf`, `placement` and `fieldsetId` declare how it presents in the editor \u2014 read them back with get_content_model or get_page. On a page a 'group' is stored as type 'array' with `config.zones.nonRepeatable`: entry and delivery data carry `<key>.nonRepeatable.<sub>`, while data-bcms-field paths are `<key>.<sub>`. Schema only \u2014 never places a section; use componentize_sections to turn a group into a section. `multiselect` stores a string[] of `config.options
|
|
2939
|
+
var ADD_FIELD_DESCRIPTION = "Append ONE field to a content model (`modelId`) or to a page's schema (`pageId`) \u2014 additive, never overwrites; a key already in use is refused. For a section/zone add ONE 'group' (fixed block) or 'repeater' (repeating list) carrying its child `fields`, not the zone's inner fields as separate top-level fields. Pass `after` to insert it after an existing top-level field instead of at the end; `ui`, `helpText`, `showIf`, `placement` and `fieldsetId` declare how it presents in the editor \u2014 read them back with get_content_model or get_page. On a page a 'group' is stored as type 'array' with `config.zones.nonRepeatable`: entry and delivery data carry `<key>.nonRepeatable.<sub>`, while data-bcms-field paths are `<key>.<sub>`. Schema only \u2014 never places a section; use componentize_sections to turn a group into a section. `multiselect` stores a string[] of `config.options`; `icon` is a BetterCMS icon name or an uploaded icon. To change a field that exists, use update_field. V2 keys (object, openMode, advanced, helpMore, recommendedMax, config.scale): \xA714 of bettercms://playbook/schema.";
|
|
2940
|
+
var V2_LEAF_TYPES_NOTE = "`tags`: string[] taxonomy (config.options suggestions, max, allowCustom). `rating`: 0..config.max (5), step 1|0.5. `code`: {language?, code} or a string (config.languages).";
|
|
2677
2941
|
var MOVE_STAGE_DESCRIPTION = "Move a content entry (`entryId`) or a page (`pageId`) to another editorial stage \u2014 this is how you take it OUT of review so it can be published. `workflowStage` is a stage key from get_workflow_board; pass `fromStage` to 409 if someone moved it first. Moving into the publish-gate stage is an approval and needs a publish-enabled connection. Publish afterwards with update_content_entry or update_page status:'published' \u2014 approving and publishing are deliberately two separate calls.";
|
|
2678
2942
|
var WINDOW_NOTE = "{from?, until?}, ISO 8601 with offset: shown only between the two instants; null clears (skill publishing).";
|
|
2679
2943
|
var WEBHOOK_EVENTS = [
|
|
@@ -2716,22 +2980,28 @@ var PARAM_TEXT = {
|
|
|
2716
2980
|
lockReason: "which system owns the value (\u2264 200 chars)",
|
|
2717
2981
|
lockClears: "as on add_field; null clears it",
|
|
2718
2982
|
helpText: "shown under the field: its purpose, where it shows, any constraint; never its label or value. Omitted: written for you.",
|
|
2983
|
+
helpMore: "longer help behind the \u24D8 next to the label, when one helpText line cannot carry it",
|
|
2984
|
+
advanced: "true = a rare or developer setting: it collects in a closed 'Advanced' section",
|
|
2985
|
+
recommendedMax: "a SOFT limit (characters, items or value): the editor warns past it, never refuses",
|
|
2986
|
+
openMode: "object/array/group/repeater: how the value (or each row) opens: 'inline', 'drill', 'popover' or 'dialog'",
|
|
2719
2987
|
fromBuiltin: "a builtin:* id from list_components: copies it into the project; fields you send win",
|
|
2720
2988
|
describes: "for editors: what it is and holds; omitted: written for you",
|
|
2721
2989
|
runtimeBundle: "instead of pageId: the server-app bundle cleanup",
|
|
2722
2990
|
pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
|
|
2723
2991
|
editorSetupForce: "re-run a setup already ready for the live build",
|
|
2724
2992
|
editorSetupCheck: "true = only report the state; starts nothing",
|
|
2993
|
+
editorSetupExempt: "instead of running: record setup-gate exceptions [{ route, check, reason, note }]; reason null removes one. Reasons: get_next_steps `receipt.gate`",
|
|
2994
|
+
pageBlockJson: "an ARRAY REPLACES the page's block composition (a `component` block per section). An OBJECT edits ONE section of the stored page instead: `insertSection` adds a component (its id, or a builtin:* id: copied into the project once) at index `at`, or `before` / `after` a section's block id (none = last), with its `style` and `overrides`; `sectionStyle` sets a section's block style (a key replaces the stored one, null unsets it). `section` in the answer names the block (skill publishing)",
|
|
2725
2995
|
pageDynamic: "a dynamic (collection template) page only",
|
|
2726
2996
|
previewPath: "its route pattern, e.g. /blog/{slug}; null clears",
|
|
2727
2997
|
pageModelId: "an existing collection this page lists (when it has no model yet)",
|
|
2728
2998
|
formIgnored: "true = not one of the site's forms (a search box, a widget); false restores it",
|
|
2729
|
-
componentizeCodemod: "true = componentize nothing; run the section-order codemod
|
|
2999
|
+
componentizeCodemod: "true = componentize nothing; run the section-order codemod in the connected repo on its own Actions, which commits for the next build. After the components and pages are published. `skipped` = no repo to commit to: run `convert --sections` in a checkout (skill componentize)",
|
|
2730
3000
|
where: "field filters: {field: value} is equality, {field: {op: value}} with op eq|ne|gt|gte|lt|lte|contains|in, and an array means `in`. System fields slug, status, publishedAt, locale work anywhere; a data field needs modelId. Compares the served copy (live for published, working for draft)",
|
|
2731
3001
|
sort: "comma list, a leading - sorts descending, e.g. '-publishedAt,title'",
|
|
2732
3002
|
page: "1-based page of `limit` rows; the response's meta says totalPages",
|
|
2733
3003
|
locale: "language, a BCP-47 tag (zh-CN \u2192 zh-cn), one of the project's locales; null = the default",
|
|
2734
|
-
translationOf: "the SOURCE
|
|
3004
|
+
translationOf: "the SOURCE id this translates (never a translation); null unlinks. One per locale: the translation sets locale and translationOf",
|
|
2735
3005
|
pageScheduledAt: "ISO datetime to publish this page's draft at (needs publish permission); null cancels. An edit that omits scheduledAt CANCELS a pending schedule",
|
|
2736
3006
|
pageUnpublishAt: "ISO datetime at which the published page returns to draft; null cancels; omitting it never cancels it",
|
|
2737
3007
|
sourceUrl: "instead of data: a public http(s) URL the server fetches and stores (private or internal addresses are refused)",
|
|
@@ -2741,8 +3011,9 @@ var PARAM_TEXT = {
|
|
|
2741
3011
|
confirmedInput: "true only after the user explicitly approved adding this custom input (`ui.input`); else setting or changing one is refused input_needs_confirmation. Removing one needs no approval"
|
|
2742
3012
|
};
|
|
2743
3013
|
var INPUT_CONFIRMED_HEADER = "x-bcms-confirmed-input";
|
|
2744
|
-
var BUTTON_FIELD_NOTE = "'button' is ONE call to action, value {label, href, variant
|
|
2745
|
-
var
|
|
3014
|
+
var BUTTON_FIELD_NOTE = "'button' is ONE call to action, value {label, href, variant? (a Button family look), size?, arrow?, icon?, newTab?, rel?} (arrow:false, icon:null off): never a text field beside a link field.";
|
|
3015
|
+
var LINK_VALUE_NOTE = "A link or button href: a URL string or {type: url|page|entry|email|phone|anchor|file, href, title?, newTab?, rel?, section?, ref: page/entry id}.";
|
|
3016
|
+
var UPDATE_FIELD_DESCRIPTION = "Change ONE existing field of a content model (`modelId`) or of a page's schema (`pageId`), addressed by `path`: its key, or dot-separated keys through groups and repeaters ('hero.title'). Set its label, helpText, required, validation `config` (merged by key; null deletes one), showIf, ui, placement, readOnly, lockReason, or the V2 helpMore, advanced, recommendedMax and openMode (null clears those). The rest of the schema is written back unchanged at the version read; a 409 means it changed since, so read again. `type` changes how every stored value is read and needs allowRetype: true. `key` renames it: stored values stay under the old key, so the route answers 409 with the entry count unless allowFieldRemoval: true discards them \u2014 migrate the values first. Ask the user before a retype or a rename. A page's label + link pair (primaryCtaText + primaryCtaHref) becomes one button with `mergeHref`.";
|
|
2746
3017
|
var GET_SEO_DESCRIPTION = "Get the connected project's site-wide SEO settings \u2014 every key update_seo writes: seoDefaults (metaTitle, metaDescription, ogImage, twitterHandle, og, twitter, collectionSchemaTypes), siteSchema (the site's JSON-LD), robotsConfig, sitemapConfig, rssConfig, indexnowConfig and searchConfig.";
|
|
2747
3018
|
var UPDATE_SEO_DESCRIPTION = "Set the connected project's site-wide SEO defaults and config. Each key you send is REPLACED whole (not deep-merged), so read get_seo and send the complete object. `seoDefaults` applies to every page that does not override it, and its `collectionSchemaTypes` maps a collection's model id to the schema.org type of its entries. `siteSchema` is the site's own JSON-LD (Organization/WebSite, emitted on the home page). `robotsConfig`, `sitemapConfig` and `rssConfig` shape robots.txt, sitemap.xml and the feed; `indexnowConfig` pings search engines on publish; `searchConfig` is site search. A key the plan does not include answers 403 FEATURE_LOCKED. Rebuilds the site.";
|
|
2748
3019
|
var SEO_KEY_TEXT = {
|
|
@@ -2757,7 +3028,8 @@ var SEO_KEY_TEXT = {
|
|
|
2757
3028
|
var VARIANT_GROUP_NOTE = "When the component belongs to a family of looks, `variantGroup` names the family and lists its looks.";
|
|
2758
3029
|
var UNPROPPED_COPY_NOTE = "A `warnings` entry UNPROPPED_COPY names copy no prop targets, which nobody can edit: add a prop with a `target` for each leaf it lists.";
|
|
2759
3030
|
var UPDATE_PAGE_IF_MATCH_NOTE = "get_page's `optimisticVersion`: the write applies only at that version, and a 409 means the page changed since, so read it again. Send it whenever you pass blockJson, which REPLACES the whole composition.";
|
|
2760
|
-
var SLOT_MULTIPLE_NOTE = "'slot' with config.multiple holds a list [{componentId, overrides?}] (config.maxItems, default 12, at most 50): target.path 'props.children' on a section.";
|
|
3031
|
+
var SLOT_MULTIPLE_NOTE = "'slot' with config.multiple holds a list [{componentId, overrides?}] (config.maxItems, default 12, at most 50): target.path 'props.children' on a section. V2 slots, 'table' mode 'types': playbook \xA75.";
|
|
3032
|
+
var PROP_V2_NOTE = "Props take the fields' V2 keys (\xA714): 'object' = ONE {child: value} from config.fields, \u226410 levels (field_depth_exceeded); datetime|slug|email|phone|json|location|tags|rating|code|reference|multi-reference as that field (a reference names contentModelId, or polymorphic + allowedModelSlugs; page reads hydrate its entries); helpMore, advanced, recommendedMax, openMode, config.scale, ui.layout rows|chips|gallery, ui.preview.fallback, showIf notEmpty|empty.";
|
|
2761
3033
|
var LAYOUT_LOCK_NOTE = "A Global field with `overridable:false` + `lockReason` (add-field `field`, update-field `patch`) refuses page overrides (LAYOUT_FIELD_NOT_OVERRIDABLE); get_layout scope page lists stored ones as `shadowed`.";
|
|
2762
3034
|
|
|
2763
3035
|
// src/entry-query.ts
|
|
@@ -2922,32 +3194,53 @@ var fieldType = z.enum([
|
|
|
2922
3194
|
"file",
|
|
2923
3195
|
"sections",
|
|
2924
3196
|
"multiselect",
|
|
2925
|
-
"icon"
|
|
3197
|
+
"icon",
|
|
3198
|
+
// V2: ONE nested object of `fields`, kept as `object` (not normalized to zones). `toField` recurses into it.
|
|
3199
|
+
"object",
|
|
3200
|
+
// V2 leaves: a taxonomy string[], a rating number, a code value. The pass-through branch carries their config.
|
|
3201
|
+
"tags",
|
|
3202
|
+
"rating",
|
|
3203
|
+
"code",
|
|
3204
|
+
"video",
|
|
3205
|
+
"table",
|
|
3206
|
+
"slot"
|
|
2926
3207
|
]);
|
|
3208
|
+
var OPEN_MODE_INVALID = "openMode_invalid: openMode must be one of 'inline', 'drill', 'popover', 'dialog'";
|
|
2927
3209
|
var slug = z.string().regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only");
|
|
2928
3210
|
var fieldKey = z.string().regex(/^[a-zA-Z0-9_-]+$/, "letters, numbers, underscores, and hyphens only");
|
|
3211
|
+
var dockIconArg = z.string().min(1).max(40).refine((v) => DOCK_ICON_SET.has(v), `not a dock icon, see ${DOCK_ICONS_URI}`);
|
|
2929
3212
|
var uiObject = z.strictObject({
|
|
2930
3213
|
collapsed: z.boolean().optional().describe("start this panel collapsed. Use it for long optional sections."),
|
|
2931
3214
|
preview: z.strictObject({
|
|
2932
3215
|
title: z.string().min(1).optional().describe("CHILD KEY whose value titles a collapsed row"),
|
|
2933
3216
|
subtitle: z.string().min(1).optional().describe("CHILD KEY whose value is the row's second line (e.g. 'level' \u2192 'Heading \xB7 h2')"),
|
|
2934
|
-
media: z.string().min(1).optional().describe("CHILD KEY whose value is the row's thumbnail (image/file/url all work)")
|
|
3217
|
+
media: z.string().min(1).optional().describe("CHILD KEY whose value is the row's thumbnail (image/file/url all work)"),
|
|
3218
|
+
fallback: z.string().min(1).max(60).optional().describe("a literal row label while the title child is empty ('Untitled question'), not a key"),
|
|
3219
|
+
icon: dockIconArg.optional().describe("the row's icon while it has no media, a dock icon name")
|
|
2935
3220
|
}).optional().describe(
|
|
2936
|
-
"how ONE ITEM summarises itself when collapsed. Every slot names a CHILD KEY of THIS field/prop
|
|
3221
|
+
"how ONE ITEM summarises itself when collapsed. Every slot names a CHILD KEY of THIS field/prop (a dotted path reads into it: 'author.name'). Without it a repeater of testimonials reads 'Item 1, Item 2, Item 3'; with {title:'author', media:'avatar'} it reads the names with their faces."
|
|
2937
3222
|
),
|
|
2938
|
-
layout: z.enum(["list", "grid", "table"]).optional().describe(
|
|
2939
|
-
"how the ITEMS are arranged. Omit (\u2261 'list') for rows of text; 'grid' for items whose thumbnail is the thing you scan \u2014 a gallery, a logo wall, a team; 'table' for up to 3 scalar columns. Same placement rule as `preview`."
|
|
3223
|
+
layout: z.enum(["list", "grid", "table", "rows", "chips", "gallery"]).optional().describe(
|
|
3224
|
+
"how the ITEMS are arranged. Omit (\u2261 'list') for rows of text; 'grid' for items whose thumbnail is the thing you scan \u2014 a gallery, a logo wall, a team; 'table' for up to 3 scalar columns. Same placement rule as `preview`. V2 'rows' | 'chips' | 'gallery' are also valid on a primitive array."
|
|
2940
3225
|
),
|
|
2941
3226
|
reorderable: z.boolean().optional().describe(
|
|
2942
3227
|
"omit (\u2261 true) unless the order is PART OF THE MEANING. `false` LOCKS the list \u2014 a 'three steps' band that must stay three steps in that order, a nav whose order is semantic, a timeline. Valid only where there is a list to lock."
|
|
2943
3228
|
),
|
|
2944
|
-
control: z.enum(["segmented", "slider", "rating"]).optional().describe(
|
|
2945
|
-
"editor control for a LEAF. Omit for the default. 'segmented' on a select with a few short options (alignment, theme, size); 'slider' on a number that has config.min < config.max (opacity, columns); 'rating' on a number with config.min, max and step. Anything else is refused with the rule named."
|
|
3229
|
+
control: z.enum(["segmented", "slider", "rating", "dropdown", "radio", "cards", "grid", "switch", "checkbox"]).optional().describe(
|
|
3230
|
+
"editor control for a LEAF. Omit for the default. 'segmented' on a select with a few short options (alignment, theme, size); 'slider' on a number that has config.min < config.max (opacity, columns); 'rating' on a number with config.min, max and step; 'dropdown'/'radio'/'grid'/'cards' on a select ('cards' also a multiselect); 'switch'/'checkbox' on a boolean ('checkbox' also a multiselect). Anything else is refused with the rule named."
|
|
2946
3231
|
),
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
3232
|
+
icon: dockIconArg.optional().describe(`the branch row's icon, a name from ${DOCK_ICONS_URI}. Nesting fields only.`),
|
|
3233
|
+
itemLabel: z.string().min(1).max(24).optional().describe("what ONE item is called ('Slide'): the add button, crumbs and empty state. Nesting fields and lists. V2 itemNoun goes here."),
|
|
3234
|
+
// V2 chrome keys (the server's fieldUiSchema): editor-only, each refused with a named code where it means nothing.
|
|
3235
|
+
itemName: z.string().max(64).optional().describe("a list's item type name (an identifier). Lists only."),
|
|
3236
|
+
itemTitle: z.string().min(1).max(60).optional().describe("a list's item type title. Lists only."),
|
|
3237
|
+
rows: z.number().int().min(1).max(40).optional().describe("the lines a text box opens with. text / longtext only."),
|
|
3238
|
+
placeholder: z.string().min(1).max(120).optional().describe("ghost text, a field of any type. A prop uses its own `placeholder`."),
|
|
3239
|
+
synced: z.strictObject({ from: z.string().min(1).max(64) }).optional().describe("the value comes from this global; edits apply everywhere."),
|
|
3240
|
+
valueShape: z.enum(["string", "object"]).optional().describe("a link / image / button writes a plain string or the object."),
|
|
3241
|
+
access: z.strictObject({ view: z.literal("schema.view").optional(), edit: z.literal("schema.manage").optional() }).optional().describe("who sees (view: developer-only) and who changes (edit) it, gated in the editor. The enforced lock is readOnly:'developer'."),
|
|
3242
|
+
hidden: z.boolean().optional().describe("true: never shown in the form (a system value). A conditional hide is showIf."),
|
|
3243
|
+
blocks: z.record(z.string(), z.strictObject({ icon: dockIconArg.optional(), preview: z.record(z.string(), z.string()).optional() })).optional().describe("modular only: per block slug, {icon?, preview?} for its rows."),
|
|
2951
3244
|
input: z.object({
|
|
2952
3245
|
name: z.string().regex(/^[a-z][a-z0-9-]{0,47}$/),
|
|
2953
3246
|
writes: z.array(z.string().min(1).max(64)).max(16).optional()
|
|
@@ -2961,26 +3254,32 @@ var propUiObject = uiObject.extend({
|
|
|
2961
3254
|
label: z.string().min(1).max(32).optional(),
|
|
2962
3255
|
kind: z.enum(["group", "row"]).optional(),
|
|
2963
3256
|
icon: uiObject.shape.icon,
|
|
2964
|
-
collapsed: z.boolean().optional()
|
|
2965
|
-
|
|
3257
|
+
collapsed: z.boolean().optional(),
|
|
3258
|
+
description: z.string().min(1).max(200).optional(),
|
|
3259
|
+
columns: z.union([z.literal(1), z.literal(2)]).optional(),
|
|
3260
|
+
collapsible: z.boolean().optional(),
|
|
3261
|
+
layout: z.enum(["stack", "breakpoints"]).optional(),
|
|
3262
|
+
order: z.number().int().min(0).max(99).optional()
|
|
3263
|
+
}).optional().describe("the group this prop (or sub-field) sits in. The FIRST member defines {id, label, kind?, icon?, collapsed?, description?, columns?, collapsible?, layout?, order?}; later ones send only {id}.")
|
|
2966
3264
|
});
|
|
3265
|
+
var choiceOption = z.union([z.string(), z.object({ label: z.string(), value: z.string(), icon: z.string().optional(), description: z.string().optional() })]);
|
|
2967
3266
|
var fieldShape = {
|
|
2968
3267
|
key: fieldKey.describe(
|
|
2969
3268
|
"machine field key. \u{1F534} UNIQUE ACROSS THE WHOLE MODEL \u2014 API IDs share ONE FLAT NAMESPACE, so a field nested inside a group must NOT reuse a key used by another field or group. Every section's heading cannot be 'title'. Prefix with the section: 'hero_title', 'pricing_title', 'faq_title'. Reusing a key is refused, and if it slips through it leaves permanent errors in the editor and breaks conditional-visibility rules, which reference fields by bare key. Keys derived from an imported site may contain hyphens ('hero-h1-welcome'): address them exactly as returned."
|
|
2970
3269
|
),
|
|
2971
3270
|
label: z.string().min(1).describe("human label shown in the editor"),
|
|
2972
|
-
type: fieldType.describe(`field type. ${BUTTON_FIELD_NOTE}`),
|
|
3271
|
+
type: fieldType.describe(`field type. ${BUTTON_FIELD_NOTE} ${LINK_VALUE_NOTE}`),
|
|
2973
3272
|
required: z.boolean().optional().describe(
|
|
2974
3273
|
"an entry cannot be saved without a value \u2014 set it only where the site genuinely cannot render without one"
|
|
2975
3274
|
),
|
|
2976
3275
|
richText: z.boolean().optional().describe(
|
|
2977
3276
|
"prose formatting. DEFAULTS TO TRUE for 'text': the field is created as rich text so editors can bold, link and format it on the canvas, and the API returns rich text (render with rich() from @bettercms-ai/sdk; plain() for titles and meta). Pass false for a value that is NOT prose and must stay a bare string \u2014 a URL/href, a slug, an id, an email, a phone number, a CSS class, an icon name. A link stored as rich text will not work as an href."
|
|
2978
3277
|
),
|
|
2979
|
-
options: z.array(
|
|
2980
|
-
"choices when type is 'select'. A plain string, or {label, value} when the stored value differs from what the editor shows (e.g. {label:'Sold out', value:'soldOut'})."
|
|
3278
|
+
options: z.array(choiceOption).optional().describe(
|
|
3279
|
+
"choices when type is 'select'. A plain string, or {label, value, icon?, description?} when the stored value differs from what the editor shows (e.g. {label:'Sold out', value:'soldOut'}); icon a dock name, description one line \u2264140."
|
|
2981
3280
|
),
|
|
2982
3281
|
config: z.record(z.string(), z.unknown()).optional().describe(
|
|
2983
|
-
"per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, array {itemType:
|
|
3282
|
+
"per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, array {itemType: text|number|date|image|link|richtext}, date {includeTime, timezone}, modular|slot {blockSlugs: ['quote','gallery'], minItems?, maxItems?} (slot: playbook \xA75) \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model; richtext {inline:true, hostTag:'h2', accept:['h2','p','b','i','u','link'], features:['bold','italic','underline','link','headings'], allowMultipleParagraphs:false} \u2014 for a field bound to ONE element on the page: `hostTag` is that element's tag and `accept` must name it, so the editor's block-style control opens already showing 'Heading 2' instead of 'Paragraph'. Name exactly ONE heading level: publish refuses an <h3> value bound to an <h2> rather than nesting one heading inside the other. Keep 'p' in `accept` or the sanitiser strips a block demoted to Paragraph; limits: text/longtext {minLength,maxLength,pattern,patternMessage}, richtext {minLength,maxLength}, number {validation:{min,max}}, array {minItems,maxItems}, tags {min,max}, rating {min,max,step}"
|
|
2984
3283
|
),
|
|
2985
3284
|
fields: z.array(z.lazy(() => fieldObject)).optional().describe(
|
|
2986
3285
|
"NESTED child fields \u2014 REQUIRED for type 'group' (one nested object, a Non-Repeatable Zone like blog_hero \u2192 heading, description, hero_image) and type 'repeater' (a repeatable array of such objects, a Repeatable Zone / section-list like testimonials \u2192 quote, author). A section with repeating items is a 'repeater'; a fixed grouped block is a 'group'. Recurse to any depth \u2014 do NOT flatten zones into separate top-level fields."
|
|
@@ -2996,8 +3295,8 @@ var fieldShape = {
|
|
|
2996
3295
|
rules: z.array(
|
|
2997
3296
|
z.object({
|
|
2998
3297
|
field: z.string().describe("the BARE KEY of another field on this same model"),
|
|
2999
|
-
op: z.enum(["eq", "neq", "in", "isSet"]),
|
|
3000
|
-
value: z.union([z.string(), z.array(z.string())]).optional().describe("omit for 'isSet' (the source holds any value); required for every other op")
|
|
3298
|
+
op: z.enum(["eq", "neq", "in", "isSet", "notEmpty", "empty"]),
|
|
3299
|
+
value: z.union([z.string(), z.array(z.string())]).optional().describe("omit for 'isSet'/'notEmpty' (the source holds any value) and 'empty' (it holds none); required for every other op")
|
|
3001
3300
|
})
|
|
3002
3301
|
)
|
|
3003
3302
|
}).optional().describe(
|
|
@@ -3016,10 +3315,16 @@ var fieldShape = {
|
|
|
3016
3315
|
defaultValue: z.unknown().optional().describe("the value a new entry starts with"),
|
|
3017
3316
|
searchable: z.boolean().optional().describe("omit (\u2261 true) to index this field for site search; false excludes it"),
|
|
3018
3317
|
unique: z.boolean().optional().describe(
|
|
3019
|
-
"true: no two entries of this model may hold the same value (compared trimmed and case-insensitively). On a repeater child such as a variant SKU it also covers every row of every entry. Only on text, longtext, slug, email, phone, link and
|
|
3318
|
+
"true: no two entries of this model may hold the same value (compared trimmed and case-insensitively). On a repeater child such as a variant SKU it also covers every row of every entry. Only on text, longtext, slug, email, phone, link, number, select, reference and date; on a list it means no item repeats. A clash is refused with 409 `unique_conflict`."
|
|
3020
3319
|
),
|
|
3021
3320
|
readOnly: z.union([z.literal(true), z.literal("developer")]).optional().describe(PARAM_TEXT.readOnly),
|
|
3022
|
-
lockReason: z.string().max(200).optional().describe(PARAM_TEXT.lockReason)
|
|
3321
|
+
lockReason: z.string().max(200).optional().describe(PARAM_TEXT.lockReason),
|
|
3322
|
+
// V2 schema language. Editor-only; the playbook's §14 table maps each to what the editor draws.
|
|
3323
|
+
helpMore: z.string().max(2e3).optional().describe(PARAM_TEXT.helpMore),
|
|
3324
|
+
advanced: z.boolean().optional().describe(PARAM_TEXT.advanced),
|
|
3325
|
+
recommendedMax: z.number().int().positive().optional().describe(PARAM_TEXT.recommendedMax),
|
|
3326
|
+
openMode: z.enum(["inline", "drill", "popover", "dialog"], { error: OPEN_MODE_INVALID }).optional().describe(PARAM_TEXT.openMode),
|
|
3327
|
+
localized: z.boolean().optional()
|
|
3023
3328
|
};
|
|
3024
3329
|
var fieldObject = z.object(fieldShape);
|
|
3025
3330
|
var urlPatternArg = z.string().nullable().optional().describe(
|
|
@@ -3034,13 +3339,16 @@ var fieldsetsArg = z.array(
|
|
|
3034
3339
|
tab: z.string().min(1).optional(),
|
|
3035
3340
|
icon: uiObject.shape.icon,
|
|
3036
3341
|
collapsed: z.boolean().optional(),
|
|
3342
|
+
collapsible: z.boolean().optional(),
|
|
3343
|
+
columns: z.union([z.literal(1), z.literal(2)]).optional(),
|
|
3344
|
+
layout: z.enum(["stack", "breakpoints"]).optional(),
|
|
3037
3345
|
// Server-owned provenance, as get_content_model returns it: an echo parses and is dropped
|
|
3038
3346
|
// before the write (dropProvenance), never refused.
|
|
3039
3347
|
uiOrigin: z.unknown().optional(),
|
|
3040
3348
|
uiBefore: z.unknown().optional()
|
|
3041
3349
|
})
|
|
3042
3350
|
).max(30).optional().describe(
|
|
3043
|
-
"the model's editor cards: [{id, name, description?, kind?, tab?, icon?, collapsed
|
|
3351
|
+
"the model's editor cards: [{id, name, description?, kind?, tab?, icon?, collapsed?, collapsible?, columns?:1|2, layout?:'breakpoints'}], at most 30, ids unique, order = array index. kind 'group' is a disclosure, 'row' puts up to 3 short scalars on one line, 'tab' is a tab (at most 3, top-level fields only; untabbed fields stay pinned above the tabs; a group's `tab` names its tab). A field joins one with `fieldsetId`. Editor-only: never changes what is stored or delivered."
|
|
3044
3352
|
);
|
|
3045
3353
|
var dropProvenance = (sets) => sets?.map(({ uiOrigin: _origin, uiBefore: _before, ...set }) => set) ?? sets;
|
|
3046
3354
|
var RICH_TEXT_NOTE = "\u{1F534} A field of type 'text' is created as RICH TEXT unless you send `richText: false` on it. Send `richText: false` for every value that is not prose: a name used as a title, a URL, slug, id, SKU, email, phone, CSS class or icon name.";
|
|
@@ -3053,8 +3361,9 @@ var seoPatchShape = {
|
|
|
3053
3361
|
metaDescription: z.string().max(500).optional(),
|
|
3054
3362
|
ogImage: z.string().url().max(2e3).nullable().optional(),
|
|
3055
3363
|
twitterHandle: z.string().max(255).nullable().optional(),
|
|
3056
|
-
og: z.object({ image: z.string().max(2048).optional(), type: z.string().max(50).optional() }).optional(),
|
|
3364
|
+
og: z.object({ image: z.string().max(2048).optional(), type: z.string().max(50).optional(), siteName: z.string().max(255).optional(), locale: z.string().max(20).optional() }).optional(),
|
|
3057
3365
|
twitter: z.object({ card: z.string().max(50).optional() }).optional(),
|
|
3366
|
+
feeds: z.array(z.object({ type: z.string().max(100), href: z.string().max(2048), title: z.string().max(255).optional() })).max(10).optional(),
|
|
3058
3367
|
collectionSchemaTypes: z.record(z.string().min(1).max(64), z.enum(SCHEMA_ORG_TYPES)).optional()
|
|
3059
3368
|
}).optional().describe(SEO_KEY_TEXT.seoDefaults),
|
|
3060
3369
|
siteSchema: z.union([jsonObject, z.array(jsonObject)]).optional().describe(SEO_KEY_TEXT.siteSchema),
|
|
@@ -3126,8 +3435,17 @@ function toField(f) {
|
|
|
3126
3435
|
...f.unique !== void 0 ? { unique: f.unique } : {},
|
|
3127
3436
|
// The developer lock: stripped here, an agent writing a model back would unlock every field.
|
|
3128
3437
|
...f.readOnly !== void 0 ? { readOnly: f.readOnly } : {},
|
|
3129
|
-
...f.lockReason !== void 0 ? { lockReason: f.lockReason } : {}
|
|
3438
|
+
...f.lockReason !== void 0 ? { lockReason: f.lockReason } : {},
|
|
3439
|
+
...f.localized !== void 0 ? { localized: f.localized } : {},
|
|
3440
|
+
// V2 editor keys: on `base` so the group/repeater rebuild cannot drop them.
|
|
3441
|
+
...f.helpMore !== void 0 ? { helpMore: f.helpMore } : {},
|
|
3442
|
+
...f.advanced !== void 0 ? { advanced: f.advanced } : {},
|
|
3443
|
+
...f.recommendedMax !== void 0 ? { recommendedMax: f.recommendedMax } : {},
|
|
3444
|
+
...f.openMode !== void 0 ? { openMode: f.openMode } : {}
|
|
3130
3445
|
};
|
|
3446
|
+
if (f.type === "object") {
|
|
3447
|
+
return { ...base, type: "object", fields: toFields(f.fields) };
|
|
3448
|
+
}
|
|
3131
3449
|
if (f.type === "group") {
|
|
3132
3450
|
return { ...base, type: "array", config: { zones: { nonRepeatable: toFields(f.fields) } } };
|
|
3133
3451
|
}
|
|
@@ -3210,9 +3528,29 @@ var GIT_TEXT = {
|
|
|
3210
3528
|
var HOSTING_TEXT = {
|
|
3211
3529
|
list_env_vars: "The project's environment variables by NAME and scope (never a value), and `missing`: the ones the repository's .env.example declares that the project does not set. A site missing them builds and serves, then renders empty or crashes in the browser. Resolve `missing` by running `npx @bettercms-ai/cli@latest env push` in the repository: it uploads those values from the user's own .env files straight to BetterCMS, so they never pass through this conversation. Never read, print or ask the user to paste a value. When the values do not exist locally because the site reads its content from another CMS, move that content into BetterCMS instead (the bettercms skill's external-content reference). BetterCMS's own BCMS_* identifiers are supplied to every build and never need setting.",
|
|
3212
3530
|
list_hosting_connections: "The Vercel, Netlify and Cloudflare accounts connected to this workspace (id, provider, accountLabel, status; never a token), plus `connectUrl`, the dashboard page where a person connects one. Call it before deploy_to_host. If the provider the user wants has no connection with status `ok`, give the user `connectUrl` to connect it in their browser and stop until they have: you never handle OAuth or a host token yourself.",
|
|
3213
|
-
deploy_to_host: "Deploy this project to the user's own Vercel or Netlify account through their connection. BetterCMS builds the site and the host serves that build
|
|
3531
|
+
deploy_to_host: "Deploy this project to the user's own Vercel or Netlify account through their connection. BetterCMS builds the site and the host serves that build (skill publishing). The first call creates the site on the host and makes it the project's host; a later call for the same provider redeploys that site (`redeployed: true`). `connectionId` (from list_hosting_connections) defaults to the newest working one, `siteName` to the project's handle. A refusal carries a `code` and a `fixUrl` (not_connected, reconnect, pick_existing_site, grant_required, ADMIN_REQUIRED): each is fixed by the user at `fixUrl`, so relay it and stop. `deploymentQueued: false` means the project has no live build yet: deploy_project first, and that release reaches the host on its own. Never run the Vercel or Netlify CLI while a connection exists. Then poll get_host_deploy_status. Needs artifact:write, like deploy_project.",
|
|
3214
3532
|
get_host_deploy_status: "The connected project's recent deployments to Vercel, Netlify or Cloudflare, newest first: state (queued|deploying|live|failed), providerUrl, error, sha and targetName. Poll it after deploy_to_host until the newest one is `live`, whose `providerUrl` is the live URL to hand the user, or `failed`, whose `error` you read back to them."
|
|
3215
3533
|
};
|
|
3534
|
+
async function builtinCopyId(client, builtinId) {
|
|
3535
|
+
const listed = await client.fetchJSON(client.url(`/management/components?select=summary`));
|
|
3536
|
+
const copy = (listed.data ?? []).find((row) => row.sourceKey === builtinId && !row.builtin);
|
|
3537
|
+
if (copy) return copy.id;
|
|
3538
|
+
const made = await client.fetchJSON(client.url(`/management/components`), { method: "POST", body: JSON.stringify({ from: builtinId }) });
|
|
3539
|
+
return made.data.id;
|
|
3540
|
+
}
|
|
3541
|
+
function publishedLines(outcome, summary) {
|
|
3542
|
+
if (!outcome) return [summary];
|
|
3543
|
+
if (outcome.notLive.length > 0) {
|
|
3544
|
+
const values = outcome.notLive.map((v) => `${v.route} ${v.address}: ${v.reason ?? "not live"} (${v.action?.label ?? "no action"})`).join("; ");
|
|
3545
|
+
if (outcome.next && outcome.notLive.every((v) => v.reason === "NOT_CHECKED")) {
|
|
3546
|
+
return [`PENDING: ${values}. Read ${outcome.next} \`publishes\` until each reads live or held; do not report it as live before.`, summary];
|
|
3547
|
+
}
|
|
3548
|
+
return [`NOT LIVE: ${outcome.summary ?? outcome.status}. ${values}. Take each action, then read get_deploy_status \`publishes\`.`, summary];
|
|
3549
|
+
}
|
|
3550
|
+
if (outcome.next) return [`${summary} Published, and not judged yet: nothing has looked at the served page. Read ${outcome.next} \`publishes\` before saying it is live.`];
|
|
3551
|
+
if (outcome.status !== "live") return [`${summary} Publish status: ${outcome.status}.`];
|
|
3552
|
+
return [`${summary} Published: ${outcome.live > 0 ? `${outcome.live} ${outcome.live === 1 ? "value" : "values"} live on the served page` : "live"}.`];
|
|
3553
|
+
}
|
|
3216
3554
|
function ok(summary, data) {
|
|
3217
3555
|
return {
|
|
3218
3556
|
content: [
|
|
@@ -3372,7 +3710,7 @@ function buildToolDefs(deps) {
|
|
|
3372
3710
|
]).describe("block type; section/slider/tabs/columns nest child blocks"),
|
|
3373
3711
|
id: z.string().min(1).describe("stable unique block id"),
|
|
3374
3712
|
props: z.record(z.string(), z.unknown()).describe(
|
|
3375
|
-
"per-type props: heading {text, level}; text/richtext {html} (NOT {text}), and both also take level 1-6, rendering the html AS that <hN> with inline marks kept (one line of inline copy only) \u2014 use it for a title that carries a styled span; image {src, alt}; button {text, href, variant?
|
|
3713
|
+
"per-type props: heading {text, level}; text/richtext {html} (NOT {text}), and both also take level 1-6, rendering the html AS that <hN> with inline marks kept (one line of inline copy only) \u2014 use it for a title that carries a styled span; image {src, alt}; button {text, href, variant? (hosted pages; else a Button component), size? sm|md|lg, fullWidth?, arrow? boolean, icon? ui.icon name, newTab? boolean, rel? nofollow|sponsored|ugc}; spacer {height}; video {url}; form {formId}; component {componentId, overrides?}; navbar {links:[{label,href}], logo?, cta?}; footer {columns, copyright?}; section {children: block[]}; columns {columns: block[][], gap} \u2014 a column may hold ANY block; slider {slides:[{id,children}]}; tabs {tabs:[{id,label,children}]}; collection {cardComponentId?, source? {modelId}, limit?, filters?, sort?, pinnedIds?, excludeCurrent?}: a collection's published entries as cards (every prop: skill content-modeling)"
|
|
3376
3714
|
),
|
|
3377
3715
|
style: z.record(z.string(), z.unknown()).optional().describe(
|
|
3378
3716
|
"design tokens: theme, bg (none|surface|muted|accent|dark|custom), bgCustom hex, paddingTop/paddingBottom/paddingSides px, contentWidth (narrow|default|wide|full), align, corner, shadow, borderTop/borderBottom, border (all four sides, in the text colour), hideOn [mobile|tablet|desktop], never all three. A real marketing band is a `section` block carrying bg + padding + contentWidth."
|
|
@@ -3617,10 +3955,10 @@ function buildToolDefs(deps) {
|
|
|
3617
3955
|
config: z.object({
|
|
3618
3956
|
componentIds: z.array(z.string()).optional(),
|
|
3619
3957
|
// 'slot' allowlist
|
|
3620
|
-
options: z.array(
|
|
3958
|
+
options: z.array(choiceOption).optional(),
|
|
3621
3959
|
// 'select' — required, choices
|
|
3622
|
-
// 'group'/'table' sub-shape. Typed loosely here (the server validates the full
|
|
3623
|
-
// recursive shape) so a
|
|
3960
|
+
// 'group'/'table'/'object' sub-shape. Typed loosely here (the server validates the full
|
|
3961
|
+
// recursive shape, 10 levels at most) so a deep tree needs no Zod mirror in the client.
|
|
3624
3962
|
fields: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
3625
3963
|
min: z.number().optional(),
|
|
3626
3964
|
// 'number'
|
|
@@ -3637,19 +3975,25 @@ function buildToolDefs(deps) {
|
|
|
3637
3975
|
// Stored markers on a prop: nothing refuses a prop value for them. Declared so an echo keeps them.
|
|
3638
3976
|
readOnly: z.union([z.literal(true), z.literal("developer")]).optional(),
|
|
3639
3977
|
lockReason: z.string().max(200).optional(),
|
|
3978
|
+
localized: z.boolean().optional(),
|
|
3640
3979
|
showIf: z.object({
|
|
3641
3980
|
match: z.enum(["all", "any"]),
|
|
3642
3981
|
rules: z.array(z.object({
|
|
3643
3982
|
field: z.string().min(1),
|
|
3644
|
-
op: z.enum(["eq", "neq", "in", "isSet"]),
|
|
3983
|
+
op: z.enum(["eq", "neq", "in", "isSet", "notEmpty", "empty"]),
|
|
3645
3984
|
value: z.union([z.string(), z.array(z.string())]).optional()
|
|
3646
3985
|
})).min(1).max(20)
|
|
3647
|
-
}).optional().describe("show this prop only while SIBLING props (by key) match: {match:'all'|'any', rules:[{field, op:'eq'|'neq'|'in'|'isSet', value?}]}. 'isSet'
|
|
3986
|
+
}).optional().describe("show this prop only while SIBLING props (by key) match: {match:'all'|'any', rules:[{field, op:'eq'|'neq'|'in'|'isSet'|'notEmpty'|'empty', value?}]}. 'isSet', 'notEmpty' and 'empty' take no value. Editor-only."),
|
|
3987
|
+
// V2 editor-only keys, the model field's (playbook §14). Declared: this is a strip site.
|
|
3988
|
+
helpMore: z.string().max(2e3).optional().describe(PARAM_TEXT.helpMore),
|
|
3989
|
+
advanced: z.boolean().optional().describe(PARAM_TEXT.advanced),
|
|
3990
|
+
recommendedMax: z.number().int().positive().optional().describe(PARAM_TEXT.recommendedMax),
|
|
3991
|
+
openMode: z.enum(["inline", "drill", "popover", "dialog"], { error: OPEN_MODE_INVALID }).optional().describe("object/group/table props: how the value (or each row) opens"),
|
|
3648
3992
|
// ⚠ A STRIP SITE, exactly like OutField above: this is a `z.object`, so a key the model
|
|
3649
3993
|
// sends and this shape does not declare is dropped BEFORE the request leaves — a 200 with
|
|
3650
3994
|
// the declaration gone. `ui` rode through the backend's looseObject `config` and died here.
|
|
3651
3995
|
ui: propUiObject.optional().describe(
|
|
3652
|
-
"AUTHORING CHROME for this prop's control in the inspector \u2014 never a delivery effect, and the same object a FIELD's `ui` takes. `preview` and `layout` are valid on 'group', 'table' and 'slot'
|
|
3996
|
+
"AUTHORING CHROME for this prop's control in the inspector \u2014 never a delivery effect, and the same object a FIELD's `ui` takes. `preview` and `layout` are valid on 'group', 'table', 'object' and 'slot' (`layout` 'rows'|'chips'|'gallery' also on 'multiselect', 'tags'); `reorderable` on 'table' ALONE, the one prop type whose value is a LIST (a 'group' is one object, a 'slot' one component instance). preview keys name a SUB-FIELD of this prop's config.fields and are checked against them (on a 'slot' the shape is checked but the keys cannot be \u2014 they belong to whichever component fills it). An unknown key inside `ui` is a 400, not a silent strip. Read it back with get_component: a prop whose `ui` is absent there was not stored."
|
|
3653
3997
|
)
|
|
3654
3998
|
});
|
|
3655
3999
|
const getLayoutInput = z.object({
|
|
@@ -3728,7 +4072,7 @@ function buildToolDefs(deps) {
|
|
|
3728
4072
|
description: z.string().optional().describe(PARAM_TEXT.describes),
|
|
3729
4073
|
blockJson: z.array(blockObject).default([]).describe("the component's block tree"),
|
|
3730
4074
|
props: z.array(componentPropObject).default([]).describe(
|
|
3731
|
-
"overridable fields. DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch: only the leaves a prop targets are click-to-edit. Each one may carry `ui` to say how its control presents in the inspector \u2014 a collapsed group, what titles and thumbnails a table's rows, whether that table's rows can be dragged at all. 'button' is ONE unit {label, href, variant?, size?, arrow?, icon?, newTab?} on a button block's 'props', never a text prop plus a url prop. 'link' an href, 'date' YYYY-MM-DD, 'color' a hex, 'file' {url, name?}, 'icon' an icon name or an image, 'multiselect' a string[] from config.options (required). " + SLOT_MULTIPLE_NOTE
|
|
4075
|
+
"overridable fields. DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch: only the leaves a prop targets are click-to-edit. Each one may carry `ui` to say how its control presents in the inspector \u2014 a collapsed group, what titles and thumbnails a table's rows, whether that table's rows can be dragged at all. 'button' is ONE unit {label, href, variant?, size?, arrow?, icon?, newTab?} on a button block's 'props', never a text prop plus a url prop. 'link' an href, 'date' YYYY-MM-DD, 'color' a hex, 'file' {url, name?}, 'icon' an icon name or an image, 'multiselect' a string[] from config.options (required). " + SLOT_MULTIPLE_NOTE + " " + PROP_V2_NOTE
|
|
3732
4076
|
)
|
|
3733
4077
|
});
|
|
3734
4078
|
const updateComponentInput = z.object({
|
|
@@ -4273,7 +4617,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4273
4617
|
def(
|
|
4274
4618
|
"set_binding_mode",
|
|
4275
4619
|
"Set how the site's bindings are resolved",
|
|
4276
|
-
"Switch this project between the two binding resolvers, from the NEXT release on. `declaredBindings: true` makes the annotator trust the template's own data-bcms-field / data-bcms-props and never guess from rendered text \u2014 the durable state; `false` returns to text-matching, which
|
|
4620
|
+
"Switch this project between the two binding resolvers, from the NEXT release on. `declaredBindings: true` makes the annotator trust the template's own data-bcms-field / data-bcms-props and never guess from rendered text \u2014 the durable state; `false` returns to text-matching, which holds only while the CMS values equal the built copy. Call it ONLY after every page's copy is declared in the template: undeclared fields stop being editable. The order around the flip (push, release, clean get_binding_report, flip, release, confirm): skill bindings. Flipping back is the same call. REQUIRES artifact:write (the authority that deploys the site): this decides what every future release does to every page. On a workspace-wide connection pass `projectId`: the scope is there, but the human who authorized the connection must be able to publish THAT project or it is refused with 403 PROJECT_PUBLISH_DENIED. Playbook section 13.",
|
|
4277
4621
|
z.object({ declaredBindings: z.boolean().describe("true = trust the template's declared bindings; false = text-match (the default)") }).shape,
|
|
4278
4622
|
async (c, a) => ok("Recorded the binding mode.", await data(c, "PATCH", `/management/projects/current/binding-mode`, { declaredBindings: a.declaredBindings }))
|
|
4279
4623
|
),
|
|
@@ -4408,21 +4752,27 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4408
4752
|
def(
|
|
4409
4753
|
"update_page",
|
|
4410
4754
|
"Edit a page",
|
|
4411
|
-
"Edit a page: title, slug, SEO metaTitle/metaDescription, structured data (`schemaType`, `schema`), publish status (draft|published), and `blockJson` (its block composition \u2014 passing
|
|
4412
|
-
z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1).describe(PARAM_TEXT.pageRef), title: z.string().optional(), slug: z.string().optional(), blockJson: z.array(blockObject).optional().
|
|
4755
|
+
"Edit a page: title, slug, SEO metaTitle/metaDescription, structured data (`schemaType`, `schema`), publish status (draft|published), and `blockJson` (its block composition \u2014 passing an array REPLACES the whole array, so read get_page first and send its `optimisticVersion` as `ifMatch`; an object adds or styles ONE section). An answer that starts with `held` is a draft the live page will NOT show until each of `holds` has its `action` taken, and a publish answers with `outcome`: `partial` lists the values not live. Never report either as done (skill publishing). `warnings`: COMPONENT_SWAP_DROPS_OVERRIDES (skill components), ENTRY_STILL_DRAFT and COMPONENT_DRAFT (skill publishing). It does NOT change the field SCHEMA \u2014 use add_field (pageId) / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call, and a singleton's content entry with them. STRUCTURED DATA is native SEO, never a field: Automatic JSON-LD from the content, unless `schemaType` picks a schema.org type or pasted `schema` (wins over both, null clears it) is set (skill seo). " + PAGE_HEAD_NOTE + " " + SECTION_DOCTRINE,
|
|
4756
|
+
z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1).describe(PARAM_TEXT.pageRef), title: z.string().optional(), slug: z.string().optional(), blockJson: z.union([z.array(blockObject), z.object({ insertSection: z.object({ componentId: z.string().min(1), at: z.number().int().min(0).optional(), before: z.string().optional(), after: z.string().optional(), style: z.record(z.string(), z.unknown()).optional(), overrides: z.record(z.string(), z.unknown()).optional() }).strict().optional(), sectionStyle: z.object({ blockId: z.string().min(1), style: z.record(z.string(), z.unknown()) }).strict().optional() }).strict()]).optional().describe(PARAM_TEXT.pageBlockJson), metaTitle: z.string().optional(), metaDescription: z.string().optional(), schemaType: z.enum(["auto", ...SCHEMA_ORG_TYPES]).optional().describe("structured data type; 'auto' (the default) derives it from the content"), schema: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).nullable().optional().describe("custom JSON-LD; wins over schemaType; null clears it"), status: z.enum(["draft", "published"]).optional(), scheduledAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageScheduledAt), unpublishAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageUnpublishAt), locale: z.string().nullable().optional().describe(PARAM_TEXT.locale), translationOf: z.string().nullable().optional().describe(PARAM_TEXT.translationOf), ifMatch: z.number().int().min(0).optional().describe(UPDATE_PAGE_IF_MATCH_NOTE), dynamic: z.object({ previewPath: z.string().nullable().optional().describe(PARAM_TEXT.previewPath), modelId: z.string().min(1).optional().describe(PARAM_TEXT.pageModelId) }).optional().describe(PARAM_TEXT.pageDynamic) }).shape,
|
|
4413
4757
|
async (c, a) => {
|
|
4758
|
+
const edit = Array.isArray(a.blockJson) ? void 0 : a.blockJson;
|
|
4759
|
+
const insert = edit?.insertSection;
|
|
4760
|
+
const insertSection = insert?.componentId.startsWith("builtin:") ? { ...insert, componentId: await builtinCopyId(c, insert.componentId) } : insert;
|
|
4414
4761
|
const res = await c.fetchJSON(
|
|
4415
4762
|
c.url(`/management/pages/${s(a.pageId)}/meta`),
|
|
4416
4763
|
{
|
|
4417
4764
|
method: "PATCH",
|
|
4418
4765
|
...a.ifMatch !== void 0 ? { headers: { "If-Match": `W/"${a.ifMatch}"` } } : {},
|
|
4419
|
-
body: JSON.stringify({ title: a.title, slug: a.slug, blockJson: a.blockJson, metaTitle: a.metaTitle, metaDescription: a.metaDescription, schemaType: a.schemaType, schema: a.schema, status: a.status, noindex: a.noindex, canonical: a.canonical, og: a.og, twitter: a.twitter, scheduledAt: a.scheduledAt, unpublishAt: a.unpublishAt, locale: a.locale, translationOf: a.translationOf, ...a.dynamic })
|
|
4766
|
+
body: JSON.stringify({ title: a.title, slug: a.slug, blockJson: Array.isArray(a.blockJson) ? a.blockJson : void 0, insertSection, sectionStyle: edit?.sectionStyle, metaTitle: a.metaTitle, metaDescription: a.metaDescription, schemaType: a.schemaType, schema: a.schema, status: a.status, noindex: a.noindex, canonical: a.canonical, og: a.og, twitter: a.twitter, scheduledAt: a.scheduledAt, unpublishAt: a.unpublishAt, locale: a.locale, translationOf: a.translationOf, ...a.dynamic })
|
|
4420
4767
|
}
|
|
4421
4768
|
);
|
|
4422
4769
|
const note = res.redirectNote;
|
|
4423
|
-
const
|
|
4424
|
-
|
|
4425
|
-
${
|
|
4770
|
+
const edited = res.section ? insertSection ? `Added section ${res.section.id} at position ${res.section.index + 1} of the draft.` : `Set the style of section ${res.section.id} in the draft.` : null;
|
|
4771
|
+
const saved = res.held ? `${edited ? `${edited} ` : ""}The draft is saved; the live page is unchanged.` : edited ?? "Updated page.";
|
|
4772
|
+
const summary = note ? `${saved} An existing redirect from ${note.source} already points to ${note.existingDestination}; it wasn't changed, so the old address does not redirect to the new one.` : saved;
|
|
4773
|
+
const lines = [...res.held ? [res.held] : [], ...publishedLines(res.outcome, summary), ...res.warnings ?? []].join("\n");
|
|
4774
|
+
const extra = { ...res.holds?.length ? { holds: res.holds } : {}, ...res.outcome ? { outcome: res.outcome } : {}, ...res.section ? { section: res.section } : {} };
|
|
4775
|
+
return ok(lines, Object.keys(extra).length ? { ...res.data, ...extra } : res.data);
|
|
4426
4776
|
}
|
|
4427
4777
|
),
|
|
4428
4778
|
// ── Code + deploy (parity with remote /mcp; needs artifact:write) ──
|
|
@@ -4569,6 +4919,11 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4569
4919
|
placement: z.enum(["cover", "title", "excerpt", "body", "inline", "panel"]).nullable().optional(),
|
|
4570
4920
|
readOnly: z.union([z.literal(true), z.literal("developer")]).nullable().optional().describe(PARAM_TEXT.lockClears),
|
|
4571
4921
|
lockReason: z.string().max(200).nullable().optional().describe(PARAM_TEXT.lockClears),
|
|
4922
|
+
helpMore: z.string().max(2e3).nullable().optional().describe(PARAM_TEXT.helpMore),
|
|
4923
|
+
advanced: z.boolean().nullable().optional().describe(PARAM_TEXT.advanced),
|
|
4924
|
+
recommendedMax: z.number().int().positive().nullable().optional().describe(PARAM_TEXT.recommendedMax),
|
|
4925
|
+
openMode: z.enum(["inline", "drill", "popover", "dialog"], { error: OPEN_MODE_INVALID }).nullable().optional().describe(PARAM_TEXT.openMode),
|
|
4926
|
+
localized: z.boolean().nullable().optional(),
|
|
4572
4927
|
type: fieldType.optional().describe("a new type; needs allowRetype"),
|
|
4573
4928
|
key: fieldKey.optional().describe("a new key (flat namespace, unique in the schema)"),
|
|
4574
4929
|
allowRetype: z.boolean().optional().describe("confirm a type change, once the user has agreed"),
|
|
@@ -4671,7 +5026,11 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4671
5026
|
entryIds: z.array(z.string().min(1)).max(100).optional().describe("entry ids (from list_content_entries)"),
|
|
4672
5027
|
pageIds: z.array(z.string().min(1)).max(100).optional().describe("page ids (from list_pages); publish only")
|
|
4673
5028
|
}).shape,
|
|
4674
|
-
async (c, a) =>
|
|
5029
|
+
async (c, a) => {
|
|
5030
|
+
const receipt = await data(c, "POST", `/management/publish`, { action: a.action, entryIds: a.entryIds, pageIds: a.pageIds });
|
|
5031
|
+
const outcome = receipt?.outcome;
|
|
5032
|
+
return ok(a.action === "publish" ? publishedLines(outcome, "Publish receipt.").join("\n") : "Unpublish receipt.", receipt);
|
|
5033
|
+
}
|
|
4675
5034
|
),
|
|
4676
5035
|
def(
|
|
4677
5036
|
"create_content_entries",
|
|
@@ -4835,7 +5194,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4835
5194
|
"Get the plan for turning this site's sections into components",
|
|
4836
5195
|
"What this site's SECTIONS would become as components \u2014 a proposal that creates nothing and is computed live. For a site whose pages were DERIVED at import, each top-level field GROUP is one section: `hero-*` and `faq-*` keys, and the repeaters the import folded (`group-*`). Per page: each section's `groupKey`, `sectionType` family, leaf `fields` with their CMS values, `shapeHash`, and either the component already rendering it (`reuse.componentId`) or the proposed one (`reuse.proposedSlug`). Under `components`, a NEW one carries the exact `props` and `blockJson` create_component would take; an EXISTING one only `componentId`, `slug` and `name` (nothing is written for it). `pending` says why a group is not offered (its codes: see `summary`). Per-page components, collection templates and chrome (never a section): skill componentize. READ `summary: true` FIRST \u2014 the full plan runs to hundreds of KB; `paged` pages it (409 PLAN_CHANGED = start again); the unpaged full read is what `--plan plan.json` takes. Keep the `digest` \u2014 componentize_sections refuses any other.",
|
|
4837
5196
|
z.object({
|
|
4838
|
-
summary: z.boolean().optional().describe("true = no values: each section's groupKey, family, pending and component slug. Read this first. `pending` codes
|
|
5197
|
+
summary: z.boolean().optional().describe("true = no values: each section's groupKey, family, pending and component slug. Read this first. `pending` codes (each explained in skill componentize): `NO_GROUP_ROOT`, `NOT_A_SECTION` (`detail`: `ENTRY_METADATA`, `DECLINED`), `IMAGE_AS_TEXT`, `ALREADY_COMPONENTIZED`, `EMPTY_GROUP`, `NESTED_REPEATER`, `PAGE_NOT_EMPTY`."),
|
|
4839
5198
|
paged: z.boolean().optional().describe("true = 10 pages at a time; the response carries `page`, `pageCount` and a `cursor` for the next."),
|
|
4840
5199
|
cursor: z.string().optional().describe("The `cursor` from the previous page. Implies paged. A 409 `PLAN_CHANGED` means start again with no cursor.")
|
|
4841
5200
|
}).shape,
|
|
@@ -4844,15 +5203,15 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4844
5203
|
def(
|
|
4845
5204
|
"componentize_sections",
|
|
4846
5205
|
"Turn this site's derived sections into components",
|
|
4847
|
-
"Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections, how many components it will create and which pages it will rewrite; `dryRun` first. It creates each proposed component as a DRAFT and replaces each page's DRAFT blocks with ordered `component` instances, one per group
|
|
5206
|
+
"Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections, how many components it will create and which pages it will rewrite; `dryRun` first. It creates each proposed component as a DRAFT and replaces each page's DRAFT blocks with ordered `component` instances, one per group. The page keeps its `fields`, values and bindings (see `copy`). Pass the plan's `digest`; a 409 `stale-plan` means the site changed: read the plan again, show the user what changed and confirm again. Read `components.wouldDuplicate` first (skill components). Re-running is safe: a placed group returns as ALREADY_COMPONENTIZED. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages, then call it again with `codemod: true` for the repository half (skill componentize). A group the user says is NOT a section: `decline: true` with those `sections` (skill componentize).",
|
|
4848
5207
|
z.object({
|
|
4849
5208
|
digest: z.string().min(1).describe("The `digest` get_componentize_plan returned. A different one is refused with 409 stale-plan."),
|
|
4850
5209
|
pageIds: z.array(z.string().min(1)).optional().describe("Componentize only these pages (ids from the plan). Omit for every page the plan lists."),
|
|
4851
5210
|
pages: z.array(z.string().min(1)).optional().describe("The same page filter under the name the dashboard uses. Unioned with pageIds; an EMPTY list is refused rather than treated as every page."),
|
|
4852
5211
|
sections: z.array(z.string().min(1)).optional().describe("Act only on these section groupKeys (from the plan); every other section keeps its placement."),
|
|
4853
|
-
copy: z.enum(["bind", "instance"]).optional().describe("Who owns each section's copy. 'bind' (default) leaves it in the page fields. 'instance'
|
|
5212
|
+
copy: z.enum(["bind", "instance"]).optional().describe("Who owns each section's copy. 'bind' (default) leaves it in the page fields. 'instance' moves it onto each placement; the page's fields are KEPT and marked `origin: \"componentized\"` (skill componentize)."),
|
|
4854
5213
|
dryRun: z.boolean().optional().describe("true = return the receipt without writing anything. Do this first."),
|
|
4855
|
-
group: z.string().min(1).max(100).optional().describe("File the components this run creates into this Component Group (
|
|
5214
|
+
group: z.string().min(1).max(100).optional().describe("File the components this run creates into this Component Group (by NAME, created when missing), else its section family (skill componentize). A reused component keeps its Group; the receipt's `components.groups` lists the Groups filed into."),
|
|
4856
5215
|
decline: z.boolean().optional().describe("true = record `sections` as NOT sections (nothing is componentized; the plan stops proposing them). false = offer them again. Needs `sections`."),
|
|
4857
5216
|
joinTwins: z.boolean().optional().describe("true = move each locale copy (`/fr`) still on its own component onto its twin's (`bindPaths` keeps its fields). A dry run lists each in `joins`. Whole plan only."),
|
|
4858
5217
|
codemod: z.boolean().optional().describe(PARAM_TEXT.componentizeCodemod)
|
|
@@ -5073,7 +5432,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
5073
5432
|
def(
|
|
5074
5433
|
"get_deploy_status",
|
|
5075
5434
|
"Get deploy/build status",
|
|
5076
|
-
"Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `editorSetup`: whether editing is set up for the live build (never or stale = call editor_setup). Poll this after deploy_project until state is idle with your sha live. `release` and `
|
|
5435
|
+
"Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `editorSetup`: whether editing is set up for the live build (never or stale = call editor_setup). Poll this after deploy_project until state is idle with your sha live. `release`, `held` and `publishes` (each `live` or `partial`): skill publishing. `runtime` ({state: ok|crash-loop, restarts, oomKilled}) appears for a server app only: crash-loop means its container keeps restarting (often out of memory) though the build went live fine. Also returns `canvas.lane` (skill bindings) and `editorUrl`, the visual editor to hand the user.",
|
|
5077
5436
|
z.object({}).shape,
|
|
5078
5437
|
// Flat body, no `data` envelope: `data()` read it as undefined.
|
|
5079
5438
|
async (c) => ok("Deploy status.", await c.fetchJSON(c.url(`/management/projects/deploy-status`), { method: "GET" }))
|
|
@@ -5084,10 +5443,12 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
5084
5443
|
NEW_TOOL_TEXT.editor_setup,
|
|
5085
5444
|
z.object({
|
|
5086
5445
|
force: z.boolean().optional().describe(PARAM_TEXT.editorSetupForce),
|
|
5087
|
-
check: z.boolean().optional().describe(PARAM_TEXT.editorSetupCheck)
|
|
5446
|
+
check: z.boolean().optional().describe(PARAM_TEXT.editorSetupCheck),
|
|
5447
|
+
exempt: z.array(z.record(z.string(), z.unknown())).optional().describe(PARAM_TEXT.editorSetupExempt)
|
|
5088
5448
|
}).shape,
|
|
5089
5449
|
// The route answers `{ state, … }` with no `data` envelope, so the whole body is the result.
|
|
5090
|
-
|
|
5450
|
+
// `exempt` records setup-gate exceptions instead of running anything (the gate's one write).
|
|
5451
|
+
async (c, a) => a.exempt ? ok("Setup gate exceptions.", await c.fetchJSON(c.url(`/management/projects/setup-gate/exceptions`), { method: "POST", body: JSON.stringify({ exempt: a.exempt }) })) : ok("Editor setup.", await c.fetchJSON(c.url(`/management/projects/editor-setup`), a.check === true ? { method: "GET" } : { method: "POST", body: JSON.stringify({ force: a.force }) }))
|
|
5091
5452
|
),
|
|
5092
5453
|
// ── Deploy to the user's own host (parity with remote /mcp) ──
|
|
5093
5454
|
// A person connects Vercel or Netlify in the dashboard; nothing here touches OAuth or a host
|
|
@@ -5240,7 +5601,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5240
5601
|
name: "add_field",
|
|
5241
5602
|
config: {
|
|
5242
5603
|
title: "Add a field to a content model or a page",
|
|
5243
|
-
description: ADD_FIELD_DESCRIPTION + " " + RICH_TEXT_NOTE,
|
|
5604
|
+
description: ADD_FIELD_DESCRIPTION + " " + V2_LEAF_TYPES_NOTE + " " + RICH_TEXT_NOTE,
|
|
5244
5605
|
inputSchema: addFieldInput.shape
|
|
5245
5606
|
},
|
|
5246
5607
|
handler: guard(
|
|
@@ -5771,7 +6132,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5771
6132
|
client.url(`/management/pages/${pageId}/meta`),
|
|
5772
6133
|
{ method: "PATCH", body: JSON.stringify({ blockJson: page.blockJson, ...args.dryRun ? { dryRun: true } : {} }) }
|
|
5773
6134
|
);
|
|
5774
|
-
results.push({ ok: true, ...args.dryRun ? { status: "would-update" } : {}, pageId, slug: written.data?.slug ?? page.slug, path: written.data?.path, blocks: page.blockJson.length });
|
|
6135
|
+
results.push({ ok: true, ...args.dryRun ? { status: "would-update" } : {}, pageId, slug: written.data?.slug ?? page.slug, path: written.data?.path, blocks: page.blockJson.length, ...written.held ? { held: written.held } : {}, ...written.holds?.length ? { holds: written.holds } : {} });
|
|
5775
6136
|
} catch (err) {
|
|
5776
6137
|
results.push({ ok: false, pageId, slug: page.slug, error: errorText(err) });
|
|
5777
6138
|
}
|
|
@@ -5780,8 +6141,11 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5780
6141
|
if (args.dryRun) {
|
|
5781
6142
|
return ok(DRY_RUN_SUMMARY, { dryRun: true, results, wouldCompose: results.length - failed, failed });
|
|
5782
6143
|
}
|
|
6144
|
+
const held = results.filter((r) => typeof r.held === "string");
|
|
6145
|
+
const lead = held.length ? `HELD on ${held.length} of ${args.pages.length} page(s). ${held.map((r) => `${String(r.slug ?? r.pageId)}: ${String(r.held)}`).join(" ")}
|
|
6146
|
+
` : "";
|
|
5783
6147
|
return ok(
|
|
5784
|
-
|
|
6148
|
+
`${lead}Composed ${results.length - failed} of ${args.pages.length} page(s)${failed ? `; ${failed} failed \u2014 see the per-item errors` : ""}. These are page DRAFTS: publish each with update_page status:'published'. Then call get_site_composition for the receipt.`,
|
|
5785
6149
|
results
|
|
5786
6150
|
);
|
|
5787
6151
|
})
|
|
@@ -6089,6 +6453,9 @@ DERIVED by \`editor_setup\`, where the componentize lane in \xA710 reuses every
|
|
|
6089
6453
|
A call to action (words + link)? -> ONE \`button\` field {label, href, variant?, size?,
|
|
6090
6454
|
arrow?, icon?, newTab?} \u2014 never text + link fields
|
|
6091
6455
|
(in a component: a \`button\` block, \xA73)
|
|
6456
|
+
A link to a page or CMS item? -> a \`link\` value {type:'page'|'entry', href, ref:<id>}:
|
|
6457
|
+
the write checks ref, delivery resolves it;
|
|
6458
|
+
a plain href string stays valid
|
|
6092
6459
|
Site-wide nav/footer/modal? -> the LAYOUT (get_layout/update_layout), never a
|
|
6093
6460
|
page section and never one component per page
|
|
6094
6461
|
|
|
@@ -6173,6 +6540,15 @@ model's \`modular\` field. Create the blocks FIRST, then name their slugs (not i
|
|
|
6173
6540
|
\`config.blockSlugs\`. This is the page-builder shape: one \`sections\` field holding an
|
|
6174
6541
|
ordered list of typed blocks.
|
|
6175
6542
|
|
|
6543
|
+
A \`slot\` field (V2 blocks inside a block) takes the same \`config.blockSlugs\` (plus
|
|
6544
|
+
\`minItems\`, \`maxItems\` and \`slotSettings\`, leaf fields each child's \`_settings\` holds),
|
|
6545
|
+
but stores each child INLINE: \`[{ _key, _type: '<block slug>', _variant?, _settings?, ...values }]\`,
|
|
6546
|
+
its values judged by its block's fields. A component's \`slot\` prop with \`config.multiple\`
|
|
6547
|
+
takes the same child shape with \`_type\` the COMPONENT id (no \`_variant\`: a variant is its own
|
|
6548
|
+
component). A list of several item shapes on a component is a \`table\` prop with
|
|
6549
|
+
\`config.mode: 'types'\` and \`config.itemTypes: [{ name, label?, fields }]\`, rows
|
|
6550
|
+
\`[{ _key?, _type: '<name>', ...values }]\`.
|
|
6551
|
+
|
|
6176
6552
|
## 5b. NEVER MODEL SEO. It is native.
|
|
6177
6553
|
|
|
6178
6554
|
Do not create \`seoTitle\`, \`metaTitle\`, \`metaDescription\`, \`ogImage\`, \`canonicalUrl\`,
|
|
@@ -6239,7 +6615,7 @@ is interpolated per entry with \`{{fieldKey}}\` placeholders \u2014 \`{{title}}\
|
|
|
6239
6615
|
6. content create_content_entry / set_page_content
|
|
6240
6616
|
7. publish update_page status:'published'
|
|
6241
6617
|
7b. layout publish_layout <- if you authored the Global Layout (nav/footer); page overrides publish with the page
|
|
6242
|
-
8. check get_next_steps, and fix what it lists
|
|
6618
|
+
8. check get_next_steps, and fix what it lists; on an imported site, until receipt.complete is true
|
|
6243
6619
|
|
|
6244
6620
|
## 8. Two ways to ship a blank site
|
|
6245
6621
|
|
|
@@ -6415,7 +6791,8 @@ the release's characters. Ornaments, skip links and the platform badge are exclu
|
|
|
6415
6791
|
4. deploy again and re-read. Repeat until every route is under 2%.
|
|
6416
6792
|
|
|
6417
6793
|
\`get_next_steps\` runs the same test and hands you the worst route with its buckets
|
|
6418
|
-
(\`unaddressable-copy\`).
|
|
6794
|
+
(\`unaddressable-copy\`). Its \`receipt.gate\` holds EVERY built route to that bar, beside its page,
|
|
6795
|
+
its images and its links, and \`receipt.complete\` stays false until each route passes. A \`coverage\` carrying \`error: "nothing-bound"\` means NOT ONE element
|
|
6419
6796
|
of the build is bound \u2014 usually the singleton lane declined the project (\`skipReasons\`
|
|
6420
6797
|
\`authored-content\`: its content models were authored by the repo), so the percentage beside it
|
|
6421
6798
|
describes a site this build is not.
|
|
@@ -6499,7 +6876,8 @@ not carry falls back always (a static build has no file to frame).
|
|
|
6499
6876
|
but a code-built site ships only what its own templates draw:
|
|
6500
6877
|
- An added or moved section reaches the live site only when the site renders the page's block list
|
|
6501
6878
|
itself, in order, each section root stamped \`data-bcms-block="<blockId>"\`. \`npx @bettercms-ai/convert
|
|
6502
|
-
--
|
|
6879
|
+
--sections\` wraps a page's sections in \`<BcmsSections>\`, which is that loop and the stamps
|
|
6880
|
+
(\`--componentize\` lifts them into components first, where a plan exists). Until it ships, the canvas
|
|
6503
6881
|
only ARRANGES the static build to match the draft (a section the build never rendered is stood in for
|
|
6504
6882
|
by a server render); the editor still saves added, moved and swapped sections but says the live site
|
|
6505
6883
|
keeps its own order, and offers "Let BetterCMS control section order" (that same codemod, committed
|
|
@@ -6507,6 +6885,15 @@ but a code-built site ships only what its own templates draw:
|
|
|
6507
6885
|
with a \`SECTIONS_UNSTAMPED\` row, so \`complete\` stays false. Never hard-code a page's bands, or
|
|
6508
6886
|
literal \`data-bcms-block\` ids, in a template whose order the CMS owns: a release measures whether
|
|
6509
6887
|
the stamped roots come out in the CMS order (\`ordered\` on the binding report's structure row).
|
|
6888
|
+
- To add ONE section or set ONE section's spacing, send \`update_page\` an OBJECT \`blockJson\`:
|
|
6889
|
+
\`{ insertSection: { componentId, at | before | after, style?, overrides? } }\` or
|
|
6890
|
+
\`{ sectionStyle: { blockId, style } }\`. The server edits the stored page, so nothing else is
|
|
6891
|
+
resent. An answer that begins with \`held\` is a saved draft this site's code will not show:
|
|
6892
|
+
take the hold's action first. A publish answers with \`outcome\`; \`partial\` names each value
|
|
6893
|
+
that is not live and its action. Neither is ever "done". \`get_next_steps\` keeps them listed
|
|
6894
|
+
(\`publish-held\`), and its gate fails a route on \`sections\` or \`style\` until the build draws
|
|
6895
|
+
the page's sections from the CMS and marks every root for style, or the release writes them in
|
|
6896
|
+
place. Each failing row's \`fixCode\` says which: the project's switch, the wrap, or a rebuild.
|
|
6510
6897
|
- A placement the site never renders (a leftover nav, footer or modal its layout draws) moves
|
|
6511
6898
|
nothing: the editor counts section moves in RENDERED sections only and locks that row. Remove
|
|
6512
6899
|
such placements; do not reorder around them.
|
|
@@ -6550,9 +6937,12 @@ below is one heading of that ask.
|
|
|
6550
6937
|
the \`add-component\` command. CTA buttons are \`button\` BLOCKS inside the section that uses them,
|
|
6551
6938
|
exposed as \`text\` + \`url\` props that target that ONE block \u2014 never the words as a separate text
|
|
6552
6939
|
block beside it, or the Button panel edits nothing \u2014 variant, size and fullWidth are that block's own props and
|
|
6553
|
-
\`style\`. Do not hand-build a Button component
|
|
6554
|
-
family (Globals/Buttons) on its own, and a new look is \`create_component\` with
|
|
6555
|
-
Button. On
|
|
6940
|
+
\`style\`. Do not hand-build a Button component. On a site built here every button look you write joins
|
|
6941
|
+
the project's Button family (Globals/Buttons) on its own, and a new look is \`create_component\` with
|
|
6942
|
+
\`variantOf\` naming a Button. On an IMPORTED site the family is the looks measured in the site and
|
|
6943
|
+
nothing you write adds to it: read it with \`list_components\`, place a look as a \`component\` block,
|
|
6944
|
+
and never add CSS for a look the site does not have. On a page's FIELDS a CTA is one \`button\` field
|
|
6945
|
+
(\xA72), whose \`variant\` picks a look of that family. Rich text is a \`richtext\` prop over a
|
|
6556
6946
|
\`richtext\`/\`text\` block. Forms are \`create_form\` plus a \`form\` block, and form FIELDS are the
|
|
6557
6947
|
one place the platform models \`required\`, \`placeholder\`, \`helpText\` and real \`validation\`
|
|
6558
6948
|
(emailPolicy, min/max, phoneFormat, pattern) \u2014 put a site's validated inputs there. Cards,
|
|
@@ -6880,6 +7270,16 @@ conversion is yours to write.
|
|
|
6880
7270
|
fetch the live URL cache-busted (RECEIPTS in \xA711: publish and deploy are separate claims).
|
|
6881
7271
|
\`get_next_steps\` keeps reporting the gap until every one of these holds.
|
|
6882
7272
|
|
|
7273
|
+
**The finish line is \`receipt.complete\`.** \`get_next_steps\` answers it from the setup gate
|
|
7274
|
+
(\`receipt.gate\`): every route of the live build has a published page (or entry), one page only,
|
|
7275
|
+
at most 2% of its visible text unowned, every image a field's and 95% of its links. \`blocking\`
|
|
7276
|
+
lists what fails, worst route first, each with the \`fix\` call to run, and \`nextAction\` is the
|
|
7277
|
+
one to run now (the fix that closes the most rows): run it, read again after each batch, repeat
|
|
7278
|
+
until \`complete\`. A null \`fix\` is the user's to decide. A gap left on purpose is RECORDED, \`editor_setup { exempt: [{ route, check,
|
|
7279
|
+
reason, note }] }\`, with a reason \`allowedExceptions\` lists for that check. Running out of time,
|
|
7280
|
+
context or patience is in \`neverExceptions\`: it is refused, and telling the user why instead
|
|
7281
|
+
leaves the gate incomplete. \`bettercms init\` reads the same gate when you exit and prints it.
|
|
7282
|
+
|
|
6883
7283
|
**Done means.** Whichever recipe you ran: \`get_binding_report\` reads \`mode "declared"\`,
|
|
6884
7284
|
\`unmatched\` empty, \`bound\` above 0, \`coverage.pending\` empty, and \`canvas.lane\` is \`bridge\` or
|
|
6885
7285
|
\`draft-route\` when the site should show structural drafts.
|
|
@@ -6969,7 +7369,7 @@ FIELD never takes \`ui.group\`; it joins a fieldset with \`fieldsetId\`.
|
|
|
6969
7369
|
- **leaf** \u2014 one field, one control. Nothing to declare; \`ui.control\` only when the default is wrong.
|
|
6970
7370
|
- **group** \u2014 a \`kind: 'group'\` fieldset (a prop: \`ui.group\`): a disclosure that shows two values
|
|
6971
7371
|
when closed. \`collapsed: true\` starts it closed.
|
|
6972
|
-
- **row** \u2014 a \`kind: 'row'\` fieldset: at most 3 short scalars (number, color, boolean, date,
|
|
7372
|
+
- **row** \u2014 a \`kind: 'row'\` fieldset: at most 3 short scalars (number, rating, color, boolean, date,
|
|
6973
7373
|
datetime, select) on one line. No \`ui.control\` on a row member.
|
|
6974
7374
|
- **branch** \u2014 a nesting field drawn as a drill-in row with its \`ui.icon\` and a summary.
|
|
6975
7375
|
- **list** \u2014 repeatable items named by \`ui.itemLabel\` and \`ui.preview\`.
|
|
@@ -7002,6 +7402,32 @@ Importance orders a screen: required fields first, then array order.
|
|
|
7002
7402
|
| CTA | one \`kind:'group'\` fieldset holding the heading, text and ONE \`button\` field, \`icon:'launch'\` |
|
|
7003
7403
|
| Dimensions | one \`kind:'row'\` fieldset of up to 3 numbers (width, height, gap) |
|
|
7004
7404
|
|
|
7405
|
+
**The V2 schema language \u2014 to get X in the editor, write Y.** Every key below is editor-only and stored as sent, EXCEPT \`type: 'object'\`: it changes the stored and delivered value to \`{ [childKey]: value }\` (a \`group\` carries \`<key>.nonRepeatable.<sub>\` instead), and codegen types it as a nested object, so swapping group \u2194 object moves values and templates.
|
|
7406
|
+
|
|
7407
|
+
| To get this in the editor | Write this |
|
|
7408
|
+
|---|---|
|
|
7409
|
+
| A section that starts closed / a static heading | fieldset \`collapsed: true\` / \`collapsible: false\` |
|
|
7410
|
+
| Two short fields side by side | fieldset \`columns: 2\` |
|
|
7411
|
+
| One Desktop \xB7 Tablet \xB7 Phone switcher | three \`type: 'object'\` fields named \`desktop\`, \`tablet\`, \`phone\`, each with \`fieldsetId\` pointing at ONE fieldset with \`layout: 'breakpoints'\` |
|
|
7412
|
+
| A small inset of 2\u20133 fields, or a row that opens its own screen | \`type: 'object'\` with \`fields\`; \`openMode: 'inline', 'drill', 'popover', 'dialog'\` |
|
|
7413
|
+
| List rows that drill in instead of expanding | \`openMode: 'drill'\` on the array (or repeater) |
|
|
7414
|
+
| Tiles instead of rows | \`ui.layout: 'grid'\` on a list of objects (V2 \`arrayLayout: 'grid'\`); a primitive image array takes \`'gallery'\` |
|
|
7415
|
+
| BetterCMS-only list layouts | \`ui.layout: 'rows'\` (\u2261 'list'), \`'chips'\`, \`'gallery'\`, also on a primitive array. Never chips for a feature list: chips cut long lines |
|
|
7416
|
+
| A size capsule with numeric steps | \`type: 'number'\`, \`config.scale: { steps: {min, max, step} or [{value, label}], units?, presets? }\`. Token (string) steps and an \`allowCustom\` object are refused (\`scale_invalid\`): use numeric steps and \`allowCustom: true\` |
|
|
7417
|
+
| Help under the label / longer help behind \u24D8 | \`helpText\` / \`helpMore\` |
|
|
7418
|
+
| Calm defaults for rare settings | \`advanced: true\` |
|
|
7419
|
+
| A soft length warning (amber, never a refusal) | \`recommendedMax: 60\` |
|
|
7420
|
+
| A marketer cannot change it | \`readOnly: 'developer'\` plus \`lockReason\` |
|
|
7421
|
+
| A field that shows only sometimes | \`showIf\` (it SHOWS the field): ops \`eq\`, \`neq\`, \`in\`, \`isSet\`, \`notEmpty\`, \`empty\` |
|
|
7422
|
+
| A good row title and thumbnail | \`ui.preview: { title, subtitle, media, fallback }\` (\`fallback\` is a literal label) |
|
|
7423
|
+
| Labels that classify or filter (topics, keywords), never a feature list | \`type: 'tags'\`: a string[], trimmed, de-duplicated, each \u226464 characters; \`config.options\` are suggestions, \`config.max\`, \`config.allowCustom: false\` closes the list (\`tags_value_invalid\`) |
|
|
7424
|
+
| A star rating | \`type: 'rating'\`: a number from 0 to \`config.max\` (5), \`config.step\` 1 or 0.5 (\`rating_value_invalid\`) |
|
|
7425
|
+
| A code snippet | \`type: 'code'\`: \`{ language?, code }\` or a string; \`config.language\` default, \`config.languages\` allowlist (\`code_value_invalid\`) |
|
|
7426
|
+
| "Add question" instead of "Add item" | \`ui.itemLabel: 'Question'\` (V2 \`itemNoun\`; there is no itemNoun key) |
|
|
7427
|
+
| The same section as a component (the dock edits a page section through its props) | the same keys on its \`props\`: \`type: 'object'\` with \`config.fields\` (\`group\` and \`table\` too, 10 levels), \`datetime\`, \`slug\`, \`email\`, \`phone\`, \`json\`, \`location\`, \`tags\`, \`rating\`, \`code\`, plus the two CMS-item types (\`reference\`, \`multi-reference\`: \`config.contentModelId\` or \`polymorphic\` + \`allowedModelSlugs\`; page reads hydrate them, a text slot shows the title), and every key above except fieldsets (props group with \`ui.group\`) |
|
|
7428
|
+
|
|
7429
|
+
An unknown \`openMode\` is refused with \`openMode_invalid\`, one on a leaf \`openMode_leaf\`; a bad \`config.scale\` with \`scale_invalid\`; more than 10 nested field levels with \`field_depth_exceeded\`. update_field sets and clears (null) helpMore, advanced, recommendedMax and openMode on an existing field.
|
|
7430
|
+
|
|
7005
7431
|
**Tabs.** A \`kind: 'tab'\` fieldset is a tab:
|
|
7006
7432
|
- at most **3** tabs, names at most 20 characters and unique;
|
|
7007
7433
|
- top-level fields only: tabs never nest and props never take them;
|
|
@@ -7137,7 +7563,7 @@ Do not stop at the plan or at a partial receipt \u2014 finish the whole site, ne
|
|
|
7137
7563
|
\`data-bcms-style=""\` plus \`style={...}\` from \`block.style\`. For \`upgrade-element-styles\` (field width, spacing, text handles)
|
|
7138
7564
|
do the same with \`npx @bettercms-ai/convert@latest --upgrade-element-styles --root . --dry-run\`. Run \`--verify-styles\` again
|
|
7139
7565
|
and restore what it lists (the marker, and the inline style from \`block.style\` or \`fieldAttrs\`, are allowed; changing a class
|
|
7140
|
-
or an existing style is not). Deploy, poll, call \`get_next_steps\` again
|
|
7566
|
+
or an existing style is not). Deploy, poll, call \`get_next_steps\` again and run \`receipt.gate.nextAction\` until \`receipt.complete\` is true; what else it lists, tell me by name. Not listed: say so; invent no work. Its \`receipt.gate\` fails a route on \`sections\` or \`style\` while I could not add a section to that page, reorder it or space it: run each such row's \`fix\` too. A write that answers \`held\`, or a publish whose \`outcome\` is \`partial\`, is not done until its action is taken.
|
|
7141
7567
|
11. Ask me whether to commit and push to my repository, commit only, or leave it uncommitted (unpushed, the
|
|
7142
7568
|
next rebuild from my repo drops it). Touch git only as I answer.`;
|
|
7143
7569
|
var FIELD_UI_PROMPT = {
|
|
@@ -7250,8 +7676,8 @@ error-prone, so go slow and confirm. Never guess the layout.
|
|
|
7250
7676
|
the same prop keys across a family or a swap drops content.
|
|
7251
7677
|
4. **Props** \u2014 declare only what should really be editable:
|
|
7252
7678
|
{ key, label, target: { blockId, path },
|
|
7253
|
-
type: text|richtext|image|url|boolean|number|select|group|table|slot|form|link|button|date|color|file|icon|multiselect }.
|
|
7254
|
-
'select'/'multiselect' need config.options; 'group'/'table' need config.fields; a 'button'
|
|
7679
|
+
type: text|richtext|image|url|boolean|number|select|group|table|slot|form|link|button|date|color|file|icon|multiselect|object|datetime|slug|email|phone|json|location|tags|rating|code|reference|multi-reference|video|array|longtext }.
|
|
7680
|
+
'select'/'multiselect' need config.options; 'group'/'table'/'object' need config.fields; a 'button'
|
|
7255
7681
|
is ONE prop on a button block's 'props', never a label prop plus a url prop; 'slot' holds ONE nested component (a list with config.multiple);
|
|
7256
7682
|
'form' holds ONE Form document \u2014 point target.path at 'props' on an empty form block and set
|
|
7257
7683
|
defaultValue to {formId}. Never re-declare a form's FIELDS as props: the Form document owns
|
|
@@ -7710,8 +8136,10 @@ Read bettercms://playbook/schema section 11 first, then work this order:
|
|
|
7710
8136
|
scale, nav position/background, footer surface as DTCG {"$type","$value"} entries) and
|
|
7711
8137
|
redeploy, or structural draft previews render in a generic theme, not this site's design.
|
|
7712
8138
|
5. VERIFY \u2014 call get_next_steps and fix what it lists (it knows about missing manifests,
|
|
7713
|
-
a setup that has not run, and placed-but-unpublished components)
|
|
7714
|
-
|
|
8139
|
+
a setup that has not run, and placed-but-unpublished components). You are finished only
|
|
8140
|
+
when its receipt.complete is true: run receipt.gate.nextAction, read it again after each
|
|
8141
|
+
batch, and record an item you cannot close with editor_setup { exempt } only under an allowed
|
|
8142
|
+
reason that is true. Telling me why is not an exception.
|
|
7715
8143
|
|
|
7716
8144
|
On 401/403, the MCP key needs (re)authorizing.`
|
|
7717
8145
|
}
|