@bettercms-ai/mcp 0.66.2 → 0.66.4
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 +76 -32
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +3 -1
- package/dist/skills/bettercms/references/bindings.md +3 -3
- package/dist/skills/bettercms/references/content-modeling.md +2 -1
- package/dist/skills/bettercms/references/dock-input.md +115 -0
- package/dist/skills/bettercms/references/getting-started.md +3 -1
- package/dist/skills/bettercms/references/localization.md +7 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2277,7 +2277,7 @@ var STRUCTURE_EXAMPLE_PAYLOAD = {
|
|
|
2277
2277
|
doc: {
|
|
2278
2278
|
schema: 1,
|
|
2279
2279
|
folders: [
|
|
2280
|
-
{ id: "home:pages", parentId: null, name: "
|
|
2280
|
+
{ id: "home:pages", parentId: null, name: "Pages", icon: "documents", sort: 1e3 },
|
|
2281
2281
|
{ id: "home:collections", parentId: null, name: "Shared", icon: null, sort: 1001 },
|
|
2282
2282
|
{ id: "fld_std_blog", parentId: null, name: "Blog posts", icon: "document-text", sort: 0 },
|
|
2283
2283
|
{ id: "fld_std_products", parentId: null, name: "Products", icon: "basket", sort: 1 }
|
|
@@ -2327,18 +2327,20 @@ ever changes.
|
|
|
2327
2327
|
The family's main collection comes first, then the others in the order its fields reference
|
|
2328
2328
|
them.
|
|
2329
2329
|
4. **Family order:** the site's navigation order; failing that, entry count, then name.
|
|
2330
|
-
5. **
|
|
2331
|
-
("
|
|
2330
|
+
5. **Pages.** EVERY remaining standalone page goes in the system folder \`home:pages\`
|
|
2331
|
+
("Pages") \u2014 no standalone page ever gets a top-level folder. Code-only routes (a 404, a
|
|
2332
2332
|
page built by code with nothing to edit) stay there too, marked with the \`file-code\` icon.
|
|
2333
2333
|
Pages that share a route (/legal/terms, /legal/privacy, /docs/...) get a subfolder of it.
|
|
2334
|
+
A site in more than one language files every page, home included, under Pages \u203A <language>
|
|
2335
|
+
(named in itself: English, Fran\xE7ais, Deutsch), default language first.
|
|
2334
2336
|
6. **Shared taxonomies.** A vocabulary several families reference (Tags used by posts AND
|
|
2335
2337
|
products) goes in the system folder \`home:collections\`, named "Shared". So does supporting
|
|
2336
2338
|
content no family owns (testimonials, FAQs with no list page).
|
|
2337
2339
|
7. **Settings closes the sidebar.** \`home:globals\` ("Settings") holds NO pages or collections \u2014
|
|
2338
2340
|
navigation, header, footer, site settings and SEO defaults open from it in the dashboard. The
|
|
2339
|
-
three system folders always sort BELOW the folders you make: \u2026
|
|
2341
|
+
three system folders always sort BELOW the folders you make: \u2026 Pages, Shared, Settings.
|
|
2340
2342
|
8. **Icons** (kit names, bettercms://icons/dock): \`document-text\` for posts, \`basket\` for products,
|
|
2341
|
-
\`book\` for docs, \`documents\` for
|
|
2343
|
+
\`book\` for docs, \`documents\` for Pages, \`folder\` by default. Lucide names already stored
|
|
2342
2344
|
still draw.
|
|
2343
2345
|
9. **Depth:** nest at most 2 levels by default. The hard limit is 4; do not use it without a
|
|
2344
2346
|
reason the user gave you.
|
|
@@ -2354,15 +2356,15 @@ ever changes.
|
|
|
2354
2356
|
|
|
2355
2357
|
What the structure writes refuse, and the code each refusal carries:
|
|
2356
2358
|
|
|
2357
|
-
- **System folders.** \`home:pages\` (
|
|
2359
|
+
- **System folders.** \`home:pages\` (Pages), \`home:collections\` (Shared) and
|
|
2358
2360
|
\`home:globals\` (Settings) always exist and can never be moved or deleted (400
|
|
2359
2361
|
\`NAV_SYSTEM_FOLDER\`); \`rename_folder\` and \`set_icon\` work on them.
|
|
2360
|
-
- **Type purity** (400 \`NAV_TYPE_MISMATCH\`):
|
|
2362
|
+
- **Type purity** (400 \`NAV_TYPE_MISMATCH\`): Pages holds only pages at any depth, Shared
|
|
2361
2363
|
only collections, Settings neither \u2014 globals are not sidebar items.
|
|
2362
2364
|
- **Pins** sit above the folders in their own order, pages or collections. In a document a pin is
|
|
2363
2365
|
\`folderId: "home:root"\`, a virtual id with no folder record; a folder record with that id is
|
|
2364
2366
|
400 \`NAV_SYSTEM_FOLDER\`. In \`move_to_folder\`, \`folderId: 'root'\` pins and \`null\` returns
|
|
2365
|
-
the item to its home (
|
|
2367
|
+
the item to its home (Pages / Shared).
|
|
2366
2368
|
- **Kind.** \`move_to_folder\` and \`set_icon\` VERIFY what an id is: a page id sent as a
|
|
2367
2369
|
collection is 400 \`NAV_KIND_MISMATCH\` naming the kind to use. \`set_content_structure\` never
|
|
2368
2370
|
refuses a kind mismatch: it returns \`warnings\` and \`kindMismatches\` instead.
|
|
@@ -2407,7 +2409,7 @@ collection (\`cm_posts\`), a /shop list page (\`pg_shop\`, dynamic) for Products
|
|
|
2407
2409
|
Blog
|
|
2408
2410
|
[folder] Products icon=shopping-bag
|
|
2409
2411
|
Products, Product categories, Colours (referenced only by Products)
|
|
2410
|
-
[system folder]
|
|
2412
|
+
[system folder] Pages icon=files
|
|
2411
2413
|
About, Contact, Not found (icon=file-code)
|
|
2412
2414
|
[system folder] Shared
|
|
2413
2415
|
Tags (referenced by Blog AND Products: shared)
|
|
@@ -2518,6 +2520,14 @@ function mergeConfig(current, patch) {
|
|
|
2518
2520
|
}
|
|
2519
2521
|
return out;
|
|
2520
2522
|
}
|
|
2523
|
+
var wordsOf = (v) => (typeof v === "string" ? v : typeof v?.label === "string" ? v.label : "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
2524
|
+
function helpTextEcho(helpText, field) {
|
|
2525
|
+
if (typeof helpText !== "string" || !helpText.trim()) return null;
|
|
2526
|
+
const said = wordsOf(helpText.replace(/^\s*e\.?g\.?[:,]?\s*/i, "")).replace(/^["'“”‘’]+|["'“”‘’.…]+$/g, "");
|
|
2527
|
+
const value = wordsOf(field.defaultValue);
|
|
2528
|
+
const echoes = said === wordsOf(field.label) || value !== "" && (said === value || /…["”]?$/.test(helpText.trim()) && value.startsWith(said));
|
|
2529
|
+
return echoes ? `helpText repeats the field's label or value, which the editor already shows. Say what belongs here or its format in under 60 characters ("Whole number, no currency symbol."), or send null for none.` : null;
|
|
2530
|
+
}
|
|
2521
2531
|
function planFieldEdit(model, edit, callerIfMatch) {
|
|
2522
2532
|
const fields = model.fields ?? [];
|
|
2523
2533
|
const keys = typeof edit.path === "string" && edit.path ? edit.path.split(".") : [];
|
|
@@ -2540,6 +2550,8 @@ function planFieldEdit(model, edit, callerIfMatch) {
|
|
|
2540
2550
|
if (!retype && rename === void 0 && edit.config === void 0 && changes.length === 0) {
|
|
2541
2551
|
return { ok: false, error: "Nothing to change: pass label, helpText, required, config, showIf, ui, placement, type or key." };
|
|
2542
2552
|
}
|
|
2553
|
+
const echo = helpTextEcho(edit.helpText, { label: edit.label ?? current.label, defaultValue: current.defaultValue });
|
|
2554
|
+
if (echo) return { ok: false, error: echo };
|
|
2543
2555
|
const next = patchAt(fields, keys, (f) => {
|
|
2544
2556
|
let out = { ...f };
|
|
2545
2557
|
for (const k of changes) {
|
|
@@ -2698,8 +2710,10 @@ var PARAM_TEXT = {
|
|
|
2698
2710
|
sourceUrl: "instead of data: a public http(s) URL the server fetches and stores (private or internal addresses are refused)",
|
|
2699
2711
|
focalX: "focal point, 0-1 across from the left edge; null resets to the centre",
|
|
2700
2712
|
focalY: "focal point, 0-1 down from the top edge; null resets to the centre",
|
|
2701
|
-
mergeHref: "pageId only: the link field paired with `path` (the label). Both become ONE `button` field keyed `key` (default: their stem, primaryCtaHref \u2192 primaryCta). Each entry's DRAFT gets {label, href} and keeps the old values; the live copy changes on publish. ifMatch is the page's version"
|
|
2713
|
+
mergeHref: "pageId only: the link field paired with `path` (the label). Both become ONE `button` field keyed `key` (default: their stem, primaryCtaHref \u2192 primaryCta). Each entry's DRAFT gets {label, href} and keeps the old values; the live copy changes on publish. ifMatch is the page's version",
|
|
2714
|
+
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"
|
|
2702
2715
|
};
|
|
2716
|
+
var INPUT_CONFIRMED_HEADER = "x-bcms-confirmed-input";
|
|
2703
2717
|
var BUTTON_FIELD_NOTE = "'button' is ONE call to action, value {label, href, variant?, size?, arrow?, icon?, newTab?}: never a text field beside a link field.";
|
|
2704
2718
|
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 or placement (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`.";
|
|
2705
2719
|
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.";
|
|
@@ -2902,7 +2916,13 @@ var uiObject = z.strictObject({
|
|
|
2902
2916
|
// A refine, not an enum: 236 names inlined into every schema that carries `ui` would be paid on
|
|
2903
2917
|
// every tools/list. The layoutIcon precedent; the server's z.enum is the guard.
|
|
2904
2918
|
icon: z.string().min(1).max(40).refine((v) => DOCK_ICON_SET.has(v), `not a dock icon, see ${DOCK_ICONS_URI}`).optional().describe(`the branch row's icon, a name from ${DOCK_ICONS_URI}. Nesting fields only.`),
|
|
2905
|
-
itemLabel: z.string().min(1).max(24).optional().describe("what ONE item is called ('Slide'): the add button, crumbs and empty state. Nesting fields only.")
|
|
2919
|
+
itemLabel: z.string().min(1).max(24).optional().describe("what ONE item is called ('Slide'): the add button, crumbs and empty state. Nesting fields only."),
|
|
2920
|
+
input: z.object({
|
|
2921
|
+
name: z.string().regex(/^[a-z][a-z0-9-]{0,47}$/),
|
|
2922
|
+
writes: z.array(z.string().min(1).max(64)).max(16).optional()
|
|
2923
|
+
}).optional().describe(
|
|
2924
|
+
"a custom dock input by NAME (the project's bettercms/inputs/<name>.tsx), a last resort after `control`, `preview` and groups. `writes`: sibling field keys it may also write, nothing else. Setting or changing it needs `confirmed: true`, sent only after the user approved it."
|
|
2925
|
+
)
|
|
2906
2926
|
});
|
|
2907
2927
|
var propUiObject = uiObject.extend({
|
|
2908
2928
|
group: z.strictObject({
|
|
@@ -3054,6 +3074,8 @@ function proseType(f) {
|
|
|
3054
3074
|
if (f.type !== "text") return f.type;
|
|
3055
3075
|
return f.richText === false ? "text" : "richtext";
|
|
3056
3076
|
}
|
|
3077
|
+
var confirmedHeaders = (confirmed) => confirmed === true ? { [INPUT_CONFIRMED_HEADER]: "true" } : {};
|
|
3078
|
+
var withHeaders = (headers) => Object.keys(headers).length > 0 ? { headers } : {};
|
|
3057
3079
|
function toField(f) {
|
|
3058
3080
|
const base = {
|
|
3059
3081
|
key: f.key,
|
|
@@ -3238,7 +3260,7 @@ function authPrompt(err) {
|
|
|
3238
3260
|
var currentProject = new AsyncLocalStorage();
|
|
3239
3261
|
var currentBranch = new AsyncLocalStorage();
|
|
3240
3262
|
var projectIdArg = z.string().min(1).optional().describe(
|
|
3241
|
-
"
|
|
3263
|
+
"list_projects id; workspace-wide grants only"
|
|
3242
3264
|
);
|
|
3243
3265
|
var branchIdArg = z.string().min(1).optional().describe("content branch id (from list_branches); omit to work on main");
|
|
3244
3266
|
var withProjectId = (def) => ({
|
|
@@ -3373,7 +3395,8 @@ function buildToolDefs(deps) {
|
|
|
3373
3395
|
modelId: z.string().min(1).optional().describe("the content model to extend (from list_content_models); or send pageId"),
|
|
3374
3396
|
pageId: z.string().min(1).optional().describe("instead of modelId: the page whose schema to extend (from list_pages / create_page)"),
|
|
3375
3397
|
...fieldShape,
|
|
3376
|
-
after: afterArg
|
|
3398
|
+
after: afterArg,
|
|
3399
|
+
confirmed: z.boolean().optional().describe(PARAM_TEXT.confirmedInput)
|
|
3377
3400
|
});
|
|
3378
3401
|
const uploadAssetInput = z.object({
|
|
3379
3402
|
localPath: z.string().min(1).optional().describe("absolute path to a local file (e.g. a repo image); provide this OR url"),
|
|
@@ -3663,7 +3686,8 @@ function buildToolDefs(deps) {
|
|
|
3663
3686
|
propLabel: z.record(z.string(), z.string().min(1).max(255)).optional().describe("prop key \u2192 label"),
|
|
3664
3687
|
propHelpText: z.record(z.string(), z.string().max(200).nullable()).optional().describe("prop key \u2192 help text, null clears it"),
|
|
3665
3688
|
override: z.boolean().optional().describe(OVERRIDE_NOTE),
|
|
3666
|
-
ifMatch: z.number().int().min(0).optional().describe(COMPONENT_IF_MATCH_NOTE)
|
|
3689
|
+
ifMatch: z.number().int().min(0).optional().describe(COMPONENT_IF_MATCH_NOTE),
|
|
3690
|
+
confirmed: z.boolean().optional().describe(PARAM_TEXT.confirmedInput)
|
|
3667
3691
|
});
|
|
3668
3692
|
const setComponentSourceInput = z.object({
|
|
3669
3693
|
componentId: z.string().min(1).describe("component id (from list_components)"),
|
|
@@ -3833,7 +3857,7 @@ ${d.summary.outline}` : "Content structure.", d);
|
|
|
3833
3857
|
def(
|
|
3834
3858
|
"suggest_content_structure",
|
|
3835
3859
|
"Propose the standard Content structure",
|
|
3836
|
-
`READ-ONLY: propose the standard Content sidebar structure for the connected project (${STRUCTURE_PLAYBOOK_URI}) and write NOTHING. Runs the standard over the project's pages, collections, references and built routes: the home page and each collection's list page pinned, one folder per content family named for its detail type (Blog posts, Case studies, Products with its taxonomies and an Attributes subfolder), ordered by entry count (or the caller's nav order), shared taxonomies in Shared, every remaining page in
|
|
3860
|
+
`READ-ONLY: propose the standard Content sidebar structure for the connected project (${STRUCTURE_PLAYBOOK_URI}) and write NOTHING. Runs the standard over the project's pages, collections, references and built routes: the home page and each collection's list page pinned, one folder per content family named for its detail type (Blog posts, Case studies, Products with its taxonomies and an Attributes subfolder), ordered by entry count (or the caller's nav order), shared taxonomies in Shared, every remaining page in Pages, at most 2 levels deep. Page-bound models are never filed. Returns { doc, version, hasDocument, outline, rationale }. To apply it: show the user the outline, then call set_content_structure with this doc and this version (the If-Match; a 412 means someone changed the sidebar, so suggest again). When hasDocument is true the project already has a structure somebody made: never replace it without the user's yes; file only what is new with move_to_folder instead.`,
|
|
3837
3861
|
z.object({
|
|
3838
3862
|
maxDepth: z.number().int().min(1).max(4).optional().describe("folder nesting to propose; default 2 (the standard), 4 is the hard limit")
|
|
3839
3863
|
}).shape,
|
|
@@ -3846,7 +3870,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3846
3870
|
def(
|
|
3847
3871
|
"set_content_structure",
|
|
3848
3872
|
"Replace the Content sidebar structure",
|
|
3849
|
-
`Replace the whole Content sidebar document in one write. Follow the structure standard (read ${STRUCTURE_PLAYBOOK_URI} first; suggest_content_structure returns a ready doc). Send the complete \`doc\` and the \`version\` get_content_structure returned (0 when the project has none yet). Validated exactly like the dashboard: unique folder ids, every parentId/folderId a folder in the doc, no cycles, depth \u22644, names 1-80 chars,
|
|
3873
|
+
`Replace the whole Content sidebar document in one write. Follow the structure standard (read ${STRUCTURE_PLAYBOOK_URI} first; suggest_content_structure returns a ready doc). Send the complete \`doc\` and the \`version\` get_content_structure returned (0 when the project has none yet). Validated exactly like the dashboard: unique folder ids, every parentId/folderId a folder in the doc, no cycles, depth \u22644, names 1-80 chars, kit icons (bettercms://icons/dock). A 412 VERSION_CONFLICT means someone changed it since you read it: read again and re-apply. Returns the new version and a diff. ${STRUCTURE_FENCE}`,
|
|
3850
3874
|
z.object({
|
|
3851
3875
|
doc: z.object({
|
|
3852
3876
|
schema: z.literal(1),
|
|
@@ -3860,7 +3884,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3860
3884
|
items: z.array(z.object({
|
|
3861
3885
|
kind: z.enum(["page", "collection"]),
|
|
3862
3886
|
id: z.string().min(1).describe("page id or collection (content model) id"),
|
|
3863
|
-
folderId: z.string().nullable().describe('folder id; "home:root" = pinned at the top level; null = its home (
|
|
3887
|
+
folderId: z.string().nullable().describe('folder id; "home:root" = pinned at the top level; null = its home (Pages for a page, the Shared list for a collection)'),
|
|
3864
3888
|
sort: z.number(),
|
|
3865
3889
|
icon: z.string().nullable()
|
|
3866
3890
|
}))
|
|
@@ -3893,13 +3917,13 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3893
3917
|
def(
|
|
3894
3918
|
"move_to_folder",
|
|
3895
3919
|
"Move into a Content sidebar folder",
|
|
3896
|
-
`Move a page, a collection or a folder into a folder. folderId null moves it back to its home:
|
|
3920
|
+
`Move a page, a collection or a folder into a folder. folderId null moves it back to its home: Pages for a page, the Shared list for a collection, the top level for a folder. It lands after what is already there. Moving a folder takes its contents with it and is refused if the result would nest deeper than 4 levels or put a folder inside itself. ${STRUCTURE_POINTER}`,
|
|
3897
3921
|
z.object({
|
|
3898
3922
|
kind: z.enum(["page", "collection", "folder"]).describe(
|
|
3899
3923
|
"what the id IS on this branch \u2014 VERIFIED, not assumed: an id is either a page or a collection, never both, and a mismatch is refused with 400 NAV_KIND_MISMATCH naming the kind to use. Call it again with that kind, which also clears the wrong-kind entry."
|
|
3900
3924
|
),
|
|
3901
3925
|
id: z.string().min(1).describe("the page id, collection (content model) id, or folder id"),
|
|
3902
|
-
folderId: z.string().min(1).nullable().describe("target folder id; 'root' pins a page or collection at the top level (for a folder, 'root' is the top level); null moves it back to its home (
|
|
3926
|
+
folderId: z.string().min(1).nullable().describe("target folder id; 'root' pins a page or collection at the top level (for a folder, 'root' is the top level); null moves it back to its home (Pages, the Shared list, or the top level for a folder)")
|
|
3903
3927
|
}).shape,
|
|
3904
3928
|
async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/move`, { kind: a.kind, id: a.id, folderId: a.folderId }))
|
|
3905
3929
|
),
|
|
@@ -4244,7 +4268,8 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4244
4268
|
override: z.boolean().optional().describe(OVERRIDE_NOTE),
|
|
4245
4269
|
ifMatch: z.number().int().min(0).optional().describe(MODEL_IF_MATCH_NOTE),
|
|
4246
4270
|
removeFields: z.array(z.string().min(1)).min(1).max(100).optional().describe(FIELD_REMOVAL_NOTE),
|
|
4247
|
-
allowFieldRemoval: z.boolean().optional().describe(ALLOW_FIELD_REMOVAL_NOTE)
|
|
4271
|
+
allowFieldRemoval: z.boolean().optional().describe(ALLOW_FIELD_REMOVAL_NOTE),
|
|
4272
|
+
confirmed: z.boolean().optional().describe(PARAM_TEXT.confirmedInput)
|
|
4248
4273
|
}).shape,
|
|
4249
4274
|
// `fields` is sent only by removeFields, rebuilt from the model just read: the PATCH
|
|
4250
4275
|
// replaces `fields` wholesale when present.
|
|
@@ -4269,7 +4294,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4269
4294
|
(await c.fetchJSON(c.url(path), {
|
|
4270
4295
|
method: "PATCH",
|
|
4271
4296
|
body: JSON.stringify(body),
|
|
4272
|
-
...ifMatch !== void 0 ? {
|
|
4297
|
+
...withHeaders({ ...ifMatch !== void 0 ? { "if-match": `W/"${ifMatch}"` } : {}, ...confirmedHeaders(a.confirmed) })
|
|
4273
4298
|
})).data
|
|
4274
4299
|
);
|
|
4275
4300
|
}
|
|
@@ -4468,7 +4493,8 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4468
4493
|
allowRetype: z.boolean().optional().describe("confirm a type change, once the user has agreed"),
|
|
4469
4494
|
allowFieldRemoval: z.boolean().optional().describe("with a rename: discard the old key's stored values"),
|
|
4470
4495
|
mergeHref: z.string().min(1).optional().describe(PARAM_TEXT.mergeHref),
|
|
4471
|
-
ifMatch: z.number().int().min(0).optional().describe("the model's optimisticVersion; the version read when omitted")
|
|
4496
|
+
ifMatch: z.number().int().min(0).optional().describe("the model's optimisticVersion; the version read when omitted"),
|
|
4497
|
+
confirmed: z.boolean().optional().describe(PARAM_TEXT.confirmedInput)
|
|
4472
4498
|
}).shape,
|
|
4473
4499
|
// Read the model (a page's through its backingModel), patch ONE field with the helper the
|
|
4474
4500
|
// hosted catalogue uses, and PATCH the whole list at the version read. `mergeHref` is the
|
|
@@ -4496,7 +4522,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4496
4522
|
const updated = (await c.fetchJSON(c.url(path), {
|
|
4497
4523
|
method: "PATCH",
|
|
4498
4524
|
body: JSON.stringify({ fields: plan.fields, ...a.allowFieldRemoval === true ? { allowFieldRemoval: true } : {} }),
|
|
4499
|
-
...plan.ifMatch !== void 0 ? {
|
|
4525
|
+
...withHeaders({ ...plan.ifMatch !== void 0 ? { "If-Match": `W/"${plan.ifMatch}"` } : {}, ...confirmedHeaders(a.confirmed) })
|
|
4500
4526
|
})).data;
|
|
4501
4527
|
return ok(plan.renamed ? `Renamed '${plan.renamed.from}' to '${plan.renamed.to}'.` : `Updated field '${s(a.path)}'.`, updated);
|
|
4502
4528
|
}
|
|
@@ -5132,12 +5158,17 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5132
5158
|
async (args) => withClient(async (client) => {
|
|
5133
5159
|
if (!args.modelId === !args.pageId) return fail("Pass exactly one of modelId (a content model) or pageId (a page).");
|
|
5134
5160
|
if (args.pageId) {
|
|
5135
|
-
const
|
|
5161
|
+
const input2 = {
|
|
5136
5162
|
// Cast: the SDK types `options` as string[], narrower than the API, which also takes
|
|
5137
5163
|
// {label, value} (backend selectFieldSchema).
|
|
5138
5164
|
addFields: [toField(args)],
|
|
5139
5165
|
...args.after !== void 0 ? { after: args.after } : {}
|
|
5140
|
-
}
|
|
5166
|
+
};
|
|
5167
|
+
const page = args.confirmed === true ? (await client.fetchJSON(client.url(`/management/pages/${args.pageId}`), {
|
|
5168
|
+
method: "PATCH",
|
|
5169
|
+
body: JSON.stringify(input2),
|
|
5170
|
+
headers: confirmedHeaders(true)
|
|
5171
|
+
})).data : await client.addPageFields(args.pageId, input2);
|
|
5141
5172
|
return ok(`Added field '${args.key}' to page '${page.title}'. Page now has ${page.fields.length} field(s).`, page);
|
|
5142
5173
|
}
|
|
5143
5174
|
const modelId = args.modelId;
|
|
@@ -5160,7 +5191,12 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5160
5191
|
const nextFields = [...existingFields];
|
|
5161
5192
|
nextFields.splice(at, 0, toField(args));
|
|
5162
5193
|
const input = { fields: nextFields };
|
|
5163
|
-
const
|
|
5194
|
+
const opts = model.optimisticVersion !== void 0 ? { ifMatch: model.optimisticVersion } : void 0;
|
|
5195
|
+
const updated = args.confirmed === true ? (await client.fetchJSON(client.url(`/management/content/models/${modelId}`), {
|
|
5196
|
+
method: "PATCH",
|
|
5197
|
+
body: JSON.stringify(input),
|
|
5198
|
+
headers: { ...opts ? { "If-Match": `W/"${opts.ifMatch}"` } : {}, ...confirmedHeaders(true) }
|
|
5199
|
+
})).data : opts ? await client.updateModel(modelId, input, opts) : await client.updateModel(modelId, input);
|
|
5164
5200
|
return ok(
|
|
5165
5201
|
`Added field '${args.key}' to '${updated.name}'${args.after ? ` after '${args.after}'` : ""}. Model now has ${updated.fields.length} field(s).`,
|
|
5166
5202
|
updated
|
|
@@ -5683,8 +5719,12 @@ ${lines.join("\n")}`, found);
|
|
|
5683
5719
|
},
|
|
5684
5720
|
handler: guard(
|
|
5685
5721
|
async (args) => withClient(async (client) => {
|
|
5686
|
-
const { componentId, ifMatch, ...input } = args;
|
|
5687
|
-
const cmp = await client.
|
|
5722
|
+
const { componentId, ifMatch, confirmed, ...input } = args;
|
|
5723
|
+
const cmp = confirmed === true ? (await client.fetchJSON(client.url(`/management/components/${componentId}`), {
|
|
5724
|
+
method: "PATCH",
|
|
5725
|
+
body: JSON.stringify(input),
|
|
5726
|
+
headers: { ...ifMatch !== void 0 ? { "If-Match": `W/"${ifMatch}"` } : {}, ...confirmedHeaders(true) }
|
|
5727
|
+
})).data : await client.updateComponent(componentId, input, { ifMatch });
|
|
5688
5728
|
return ok(`Updated component '${cmp.name}' (id ${cmp.id}).`, cmp);
|
|
5689
5729
|
})
|
|
5690
5730
|
)
|
|
@@ -6124,7 +6164,9 @@ components \u2014 is \`editor_setup\`, run on demand against the live build. The
|
|
|
6124
6164
|
-> submit_conversion_receipt / submit_componentize_receipt -> deploy
|
|
6125
6165
|
|
|
6126
6166
|
\`get_deploy_status\` \`editorSetup\` (and get_next_steps) say when it has not run for the live
|
|
6127
|
-
build: \`never\` or \`stale\` \u2192 run it before anything below.
|
|
6167
|
+
build: \`never\` or \`stale\` \u2192 run it before anything below. It also fills missing meta from the
|
|
6168
|
+
pages' own copy, merges components that are identical, and switches to declared bindings when the
|
|
6169
|
+
build proves every binding declared; get_next_steps lists only what it could not do.
|
|
6128
6170
|
|
|
6129
6171
|
**Builds run \`build:bcms\` instead of \`build\`** when package.json declares one: the build alone,
|
|
6130
6172
|
without CI checks (\`"build:bcms": "astro build"\` beside \`"build": "astro check && astro build"\`),
|
|
@@ -6602,7 +6644,8 @@ hand. THE ORDER, and every step of it matters:
|
|
|
6602
6644
|
\`unmatched\` 0 and \`unaddressable\` under 2% of each route's visible text (\xA711). Fix what it
|
|
6603
6645
|
names IN THE SOURCE and release again until \`coverage.pending\` is empty.
|
|
6604
6646
|
7. \`set_binding_mode { declaredBindings: true }\` and release ONE MORE TIME \u2014 the mode applies to
|
|
6605
|
-
the next release, not to the one already out.
|
|
6647
|
+
the next release, not to the one already out. (editor_setup sets it itself when the build
|
|
6648
|
+
already declares every binding; skip this step when get_binding_report reads \`declared\`.)
|
|
6606
6649
|
|
|
6607
6650
|
**Or let BetterCMS propose the edit.** \`get_conversion_plan\` returns an APPROVED conversion \u2014 the
|
|
6608
6651
|
exact new contents of each template file, reviewed by a human in the dashboard and already checked
|
|
@@ -6852,7 +6895,8 @@ Importance orders a screen: required fields first, then array order.
|
|
|
6852
6895
|
schema: the same call with \`pageId\` instead of \`modelId\`, and the version from \`get_page\`'s
|
|
6853
6896
|
\`backingModel\`.
|
|
6854
6897
|
- A component's props: \`update_component\` with \`propUi\`, \`propGroup\`, \`propLabel\`, \`propHelpText\` (keyed by prop key).
|
|
6855
|
-
Name props from the code (\`plan.cents\` \u2192 Cents)
|
|
6898
|
+
Name props from the code (\`plan.cents\` \u2192 Cents); help text guides ("Whole number, no currency symbol."), never repeats
|
|
6899
|
+
the value; repeated cards are one \`table\` prop. An icon (\`config.media: "icon"\`) is content: group it, never prune it.
|
|
6856
6900
|
They write the DRAFT props; no publish is needed for the editor to show them.
|
|
6857
6901
|
- Always send \`ifMatch\`, the \`optimisticVersion\` you read.
|
|
6858
6902
|
- A NEW field carries its chrome in the same \`add_field\` call.
|
|
@@ -7647,7 +7691,7 @@ function buildServer(deps) {
|
|
|
7647
7691
|
STRUCTURE_PLAYBOOK_URI,
|
|
7648
7692
|
{
|
|
7649
7693
|
title: "BetterCMS Content structure playbook",
|
|
7650
|
-
description: "How to organise a project's Content sidebar: one place per document, the home page and list pages pinned, one folder per content family named for the detail type its route holds and ordered by entry count, shared taxonomies in Shared,
|
|
7694
|
+
description: "How to organise a project's Content sidebar: one place per document, the home page and list pages pinned, one folder per content family named for the detail type its route holds and ordered by entry count, shared taxonomies in Shared, Pages for the rest, Settings last, at most 2 levels. With a worked set_content_structure payload.",
|
|
7651
7695
|
mimeType: "text/markdown"
|
|
7652
7696
|
},
|
|
7653
7697
|
() => ({
|