@cmo-so/cli 0.12.4 → 0.12.6
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/README.md +1 -0
- package/dist/cmo.mjs +199 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ cmo wp status | map [--sections] | pages [--search …] [--missing-seo] | sectio
|
|
|
36
36
|
cmo wp page <page> [--section s2] a page's sections (ids: m… bands, s… reading column, a… sidebar)
|
|
37
37
|
cmo wp page create --title … --from sections.json [--parent …] [--publish] [--menu-parent …]
|
|
38
38
|
cmo wp page update <page> title=… slug=… status=… | delete <page> [--redirect-to …] | undo <page>
|
|
39
|
+
cmo wp page publish <page> [--homepage] | cmo wp home <page> [--posts-page …] [--latest-posts]
|
|
39
40
|
cmo wp page duplicate <page> --title … --replace Fairfax=Vienna
|
|
40
41
|
cmo wp section add <page> <type> --from slots.json [--after s1] [--preview]
|
|
41
42
|
cmo wp section update <page> <section> --from change.json [--preview] (edits | slots | faq)
|
package/dist/cmo.mjs
CHANGED
|
@@ -14160,7 +14160,7 @@ var SECTIONS2 = [
|
|
|
14160
14160
|
[
|
|
14161
14161
|
S(s.heading) || S(s.intro) ? prose([...headed(s), para(S(s.intro))]) : "",
|
|
14162
14162
|
group(boxes, {
|
|
14163
|
-
cls: `maggie-stagger grid gap-5 ${boxes.length
|
|
14163
|
+
cls: `maggie-stagger grid gap-5 ${boxes.length === 3 ? "sm:grid-cols-3" : "sm:grid-cols-2"}`
|
|
14164
14164
|
})
|
|
14165
14165
|
],
|
|
14166
14166
|
"flex flex-col gap-8"
|
|
@@ -14383,6 +14383,7 @@ var SECTIONS2 = [
|
|
|
14383
14383
|
slots: {
|
|
14384
14384
|
title: str('e.g. "Book your HydraFacial".'),
|
|
14385
14385
|
text: str("One sentence."),
|
|
14386
|
+
button_label: str('Default "Book Now".'),
|
|
14386
14387
|
button_url: str("Default: the booking link.")
|
|
14387
14388
|
},
|
|
14388
14389
|
required: ["title"],
|
|
@@ -14393,7 +14394,7 @@ var SECTIONS2 = [
|
|
|
14393
14394
|
buttons(
|
|
14394
14395
|
[
|
|
14395
14396
|
button(
|
|
14396
|
-
"Book Now",
|
|
14397
|
+
S(s.button_label) || "Book Now",
|
|
14397
14398
|
S(s.button_url) || kit.bookUrl || "/contact-us/",
|
|
14398
14399
|
"w-full [&_a]:w-full [&_a]:bg-white [&_a]:text-ink hover:[&_a]:bg-panel"
|
|
14399
14400
|
)
|
|
@@ -15041,7 +15042,7 @@ async function menuSet(site, ops2, id) {
|
|
|
15041
15042
|
const blocks = parse(nav.content.raw);
|
|
15042
15043
|
const before = toItems(blocks);
|
|
15043
15044
|
const after = applyMenuOps(before, ops2);
|
|
15044
|
-
const rest = blocks.filter((b) => b.name && !["core/navigation-link", "core/navigation-submenu"].includes(b.name));
|
|
15045
|
+
const rest = ops2.items ? [] : blocks.filter((b) => b.name && !["core/navigation-link", "core/navigation-submenu"].includes(b.name));
|
|
15045
15046
|
const content = [fromItems(after), serialize(rest)].filter(Boolean).join("\n");
|
|
15046
15047
|
await call(site, {
|
|
15047
15048
|
method: "POST",
|
|
@@ -15612,7 +15613,11 @@ function composePage(sections, kit, aside = {}) {
|
|
|
15612
15613
|
const asideParts = [
|
|
15613
15614
|
kit.synced?.share ? group([syncedRef(kit.synced.share)], { cls: "px-2" }) : "",
|
|
15614
15615
|
serialize([
|
|
15615
|
-
buildSection(
|
|
15616
|
+
buildSection(
|
|
15617
|
+
"cta-card",
|
|
15618
|
+
{ title: aside.ctaTitle || "Book your visit", text: aside.ctaText ?? "", button_label: aside.ctaButton },
|
|
15619
|
+
kit
|
|
15620
|
+
)
|
|
15616
15621
|
])
|
|
15617
15622
|
];
|
|
15618
15623
|
const body = band(
|
|
@@ -16176,6 +16181,27 @@ async function changePage(ctx, a, fn) {
|
|
|
16176
16181
|
undo: "wp_page_undo goes back to the version before this change."
|
|
16177
16182
|
};
|
|
16178
16183
|
}
|
|
16184
|
+
var findAllBlocks = (list3) => list3.flatMap((b) => [b, ...findAllBlocks(b.inner)]);
|
|
16185
|
+
async function setHome(site, id, postsPage) {
|
|
16186
|
+
const before = await call(site, {
|
|
16187
|
+
path: "/wp/v2/settings",
|
|
16188
|
+
query: { _fields: "show_on_front,page_on_front,page_for_posts" }
|
|
16189
|
+
});
|
|
16190
|
+
const body = { show_on_front: "page", page_on_front: id };
|
|
16191
|
+
if (postsPage !== void 0) body.page_for_posts = postsPage;
|
|
16192
|
+
if (postsPage === id) throw new Error("The homepage and the posts page can't be the same page.");
|
|
16193
|
+
const after = await call(site, {
|
|
16194
|
+
method: "POST",
|
|
16195
|
+
path: "/wp/v2/settings",
|
|
16196
|
+
query: { _fields: "show_on_front,page_on_front,page_for_posts" },
|
|
16197
|
+
body
|
|
16198
|
+
});
|
|
16199
|
+
return {
|
|
16200
|
+
before,
|
|
16201
|
+
after,
|
|
16202
|
+
undo: before.show_on_front === "page" && before.page_on_front ? `wp_site_home_set page ${before.page_on_front}` : "wp_site_home_set latest_posts true"
|
|
16203
|
+
};
|
|
16204
|
+
}
|
|
16179
16205
|
function position(a) {
|
|
16180
16206
|
return {
|
|
16181
16207
|
after: a.after || void 0,
|
|
@@ -16399,7 +16425,11 @@ var pageTools = [
|
|
|
16399
16425
|
description: str2(""),
|
|
16400
16426
|
keywords: str2("")
|
|
16401
16427
|
}),
|
|
16402
|
-
cta: obj("Sidebar booking card: title, text
|
|
16428
|
+
cta: obj("Sidebar booking card: title, text, button label (default Book Now).", {
|
|
16429
|
+
title: str2(""),
|
|
16430
|
+
text: str2(""),
|
|
16431
|
+
button: str2("")
|
|
16432
|
+
}),
|
|
16403
16433
|
related: bool(
|
|
16404
16434
|
"Add a related-services band from sibling pages (default true when the page has a reading column)."
|
|
16405
16435
|
),
|
|
@@ -16444,7 +16474,8 @@ var pageTools = [
|
|
|
16444
16474
|
const cta = a.cta ?? {};
|
|
16445
16475
|
const blocks = composePage(sections, kit, {
|
|
16446
16476
|
ctaTitle: cta.title || `Book ${String(a.title)}`,
|
|
16447
|
-
ctaText: cta.text
|
|
16477
|
+
ctaText: cta.text,
|
|
16478
|
+
ctaButton: cta.button
|
|
16448
16479
|
});
|
|
16449
16480
|
const outline = brief2(blocks, kit.syncedTitles);
|
|
16450
16481
|
if (a.preview) return { preview: true, title: a.title, sections: outline };
|
|
@@ -16543,6 +16574,103 @@ var pageTools = [
|
|
|
16543
16574
|
};
|
|
16544
16575
|
}
|
|
16545
16576
|
}),
|
|
16577
|
+
tool({
|
|
16578
|
+
name: "wp_page_publish",
|
|
16579
|
+
description: "Publish a page (a draft or private page) so visitors and search engines see it. Checks it first: one H1, an SEO title and description (warns when missing). homepage: true also makes it the site's homepage. Afterwards the live page is opened to confirm it works. To take it down again, wp_page_update status draft.",
|
|
16580
|
+
parameters: {
|
|
16581
|
+
type: "object",
|
|
16582
|
+
properties: {
|
|
16583
|
+
page: PAGE,
|
|
16584
|
+
homepage: bool("Also make it the site's homepage."),
|
|
16585
|
+
expect_modified: EXPECT
|
|
16586
|
+
},
|
|
16587
|
+
required: ["page"]
|
|
16588
|
+
},
|
|
16589
|
+
group: "site",
|
|
16590
|
+
need: WRITE,
|
|
16591
|
+
risk: "W3",
|
|
16592
|
+
gates: [...GATES],
|
|
16593
|
+
confirm: (a, lang) => say(
|
|
16594
|
+
lang,
|
|
16595
|
+
`Publish page ${q2(a.page, "")} on your website${a.homepage ? " and make it the homepage" : ""}.`,
|
|
16596
|
+
`\u767C\u4F48\u7DB2\u7AD9\u4E0A\u7684\u9801\u9762 ${q2(a.page, "")}${a.homepage ? "\uFF0C\u4E26\u8A2D\u70BA\u9996\u9801" : ""}\u3002`
|
|
16597
|
+
),
|
|
16598
|
+
run: async (ctx, a) => {
|
|
16599
|
+
const site = siteOf(ctx);
|
|
16600
|
+
const p = await findPage(site, a.page, true);
|
|
16601
|
+
checkFresh(p, a.expect_modified);
|
|
16602
|
+
const m = p.meta ?? {};
|
|
16603
|
+
const blocks = parse(p.content?.raw ?? "");
|
|
16604
|
+
const h1 = findAllBlocks(blocks).filter((b) => b.name === "core/heading" && Number(b.attrs.level) === 1).length;
|
|
16605
|
+
const warnings = [
|
|
16606
|
+
h1 === 1 ? "" : h1 === 0 ? "The page has no H1 in its content (a template may add one)." : `The page has ${h1} H1 headings.`,
|
|
16607
|
+
m.maggie_seo_title ? "" : "No SEO title: the page title is used.",
|
|
16608
|
+
m.maggie_seo_description ? "" : "No meta description: set one with wp_seo_set."
|
|
16609
|
+
].filter(Boolean);
|
|
16610
|
+
const wasStatus = p.status;
|
|
16611
|
+
const saved = p.status === "publish" ? p : await savePage(site, p.id, null, { status: "publish" });
|
|
16612
|
+
let home = null;
|
|
16613
|
+
if (a.homepage) home = await setHome(site, p.id);
|
|
16614
|
+
const live = await check(site, saved.link, "publish");
|
|
16615
|
+
return {
|
|
16616
|
+
published: true,
|
|
16617
|
+
id: p.id,
|
|
16618
|
+
path: path(saved.link),
|
|
16619
|
+
before: wasStatus,
|
|
16620
|
+
homepage: home,
|
|
16621
|
+
check: live,
|
|
16622
|
+
warnings
|
|
16623
|
+
};
|
|
16624
|
+
}
|
|
16625
|
+
}),
|
|
16626
|
+
tool({
|
|
16627
|
+
name: "wp_site_home_set",
|
|
16628
|
+
description: "Choose the site's homepage (a published page) and, optionally, the page that lists the blog posts. Returns the previous setting so it can be put back. latest_posts: true shows the newest posts as the homepage instead.",
|
|
16629
|
+
parameters: {
|
|
16630
|
+
type: "object",
|
|
16631
|
+
properties: {
|
|
16632
|
+
page: str2("The page to show as the homepage (path, slug or id)."),
|
|
16633
|
+
posts_page: str2('The page that lists the blog posts (optional; "" to clear).'),
|
|
16634
|
+
latest_posts: bool("Show the newest posts as the homepage instead of a page.")
|
|
16635
|
+
}
|
|
16636
|
+
},
|
|
16637
|
+
group: "site",
|
|
16638
|
+
need: WRITE,
|
|
16639
|
+
risk: "W3",
|
|
16640
|
+
gates: [...GATES],
|
|
16641
|
+
confirm: (a, lang) => a.latest_posts ? say(lang, "Show your newest posts as your website's homepage.", "\u628A\u7DB2\u7AD9\u9996\u9801\u6539\u70BA\u986F\u793A\u6700\u65B0\u6587\u7AE0\u3002") : say(
|
|
16642
|
+
lang,
|
|
16643
|
+
`Make ${q2(a.page, "that page")} your website's homepage.`,
|
|
16644
|
+
`\u628A ${q2(a.page, "\u8A72\u9801\u9762")} \u8A2D\u70BA\u7DB2\u7AD9\u9996\u9801\u3002`
|
|
16645
|
+
),
|
|
16646
|
+
run: async (ctx, a) => {
|
|
16647
|
+
const site = siteOf(ctx);
|
|
16648
|
+
if (a.latest_posts) {
|
|
16649
|
+
const before = await call(site, {
|
|
16650
|
+
path: "/wp/v2/settings",
|
|
16651
|
+
query: { _fields: "show_on_front,page_on_front,page_for_posts" }
|
|
16652
|
+
});
|
|
16653
|
+
await call(site, {
|
|
16654
|
+
method: "POST",
|
|
16655
|
+
path: "/wp/v2/settings",
|
|
16656
|
+
query: { _fields: "show_on_front" },
|
|
16657
|
+
body: { show_on_front: "posts" }
|
|
16658
|
+
});
|
|
16659
|
+
return { before, after: { show_on_front: "posts" } };
|
|
16660
|
+
}
|
|
16661
|
+
if (!a.page) throw new Error("Which page should be the homepage?");
|
|
16662
|
+
const p = await findPage(site, a.page);
|
|
16663
|
+
if (p.status !== "publish")
|
|
16664
|
+
throw new Error(
|
|
16665
|
+
`${path(p.link)} isn't published yet: publish it first (wp_page_publish homepage: true does both).`
|
|
16666
|
+
);
|
|
16667
|
+
let postsPage;
|
|
16668
|
+
if (typeof a.posts_page === "string")
|
|
16669
|
+
postsPage = a.posts_page === "" ? 0 : (await findPage(site, a.posts_page)).id;
|
|
16670
|
+
const r = await setHome(site, p.id, postsPage);
|
|
16671
|
+
return { ...r, check: await check(site, `${site.url ?? ""}/`, "publish") };
|
|
16672
|
+
}
|
|
16673
|
+
}),
|
|
16546
16674
|
tool({
|
|
16547
16675
|
name: "wp_page_delete",
|
|
16548
16676
|
description: "Move a page to the WordPress trash (restorable for 30 days) and redirect its address (301) to another page, by default its parent or the homepage.",
|
|
@@ -17108,6 +17236,17 @@ var childTools = [
|
|
|
17108
17236
|
slug: str2("The child theme (default the active one)."),
|
|
17109
17237
|
colors: obj('e.g. { "olive": "#4F5B48" }.'),
|
|
17110
17238
|
fonts: obj('Font families by slug: { "body": "Jost, sans-serif" } (files must already be in the child).'),
|
|
17239
|
+
font_files: arr(
|
|
17240
|
+
"Install fonts in the child from https links to font files (woff2/woff/ttf), e.g. Google Fonts' fonts.gstatic.com files: each becomes a fontFace of its family (slug display / body / any).",
|
|
17241
|
+
obj("", {
|
|
17242
|
+
slug: str2("Family slug in theme.json: display, body, \u2026"),
|
|
17243
|
+
family: str2('Family name, e.g. "Roboto Slab".'),
|
|
17244
|
+
fallback: str2('Fallback stack, e.g. "Georgia, serif".'),
|
|
17245
|
+
url: str2("https link to the font file."),
|
|
17246
|
+
weight: str2('e.g. "400", "800" or "100 900".'),
|
|
17247
|
+
style: str2('"normal" or "italic".')
|
|
17248
|
+
})
|
|
17249
|
+
),
|
|
17111
17250
|
styles: obj('theme.json styles to merge in (e.g. { "elements": { "button": { \u2026 } } }).'),
|
|
17112
17251
|
css: str2("Replace the child's CSS (plain CSS)."),
|
|
17113
17252
|
css_append: str2("Add CSS at the end."),
|
|
@@ -17134,9 +17273,54 @@ var childTools = [
|
|
|
17134
17273
|
const cur = info.files ?? {};
|
|
17135
17274
|
const files = {};
|
|
17136
17275
|
const notes = [];
|
|
17276
|
+
if (Array.isArray(a.font_files) && a.font_files.length) {
|
|
17277
|
+
const tj0 = JSON.parse(files["theme.json"] ?? cur["theme.json"] ?? '{"version":3,"settings":{}}');
|
|
17278
|
+
if (!tj0.settings || Array.isArray(tj0.settings)) tj0.settings = {};
|
|
17279
|
+
const typo = tj0.settings.typography ?? {};
|
|
17280
|
+
const fams = typo.fontFamilies ?? [];
|
|
17281
|
+
for (const f of a.font_files) {
|
|
17282
|
+
if (!/^[a-z0-9-]{2,30}$/.test(f.slug ?? ""))
|
|
17283
|
+
throw new Error(`Font slug "${f.slug}": lowercase letters, digits and dashes.`);
|
|
17284
|
+
if (!/^https:\/\//.test(f.url ?? "")) throw new Error(`${f.family}: the font's link must be https.`);
|
|
17285
|
+
const r = await fetch(f.url);
|
|
17286
|
+
if (!r.ok) throw new Error(`${f.family}: the font's link answered HTTP ${r.status}.`);
|
|
17287
|
+
const bytes = new Uint8Array(await r.arrayBuffer());
|
|
17288
|
+
const head = String.fromCharCode(...bytes.subarray(0, 4));
|
|
17289
|
+
const ext = head === "wOF2" ? "woff2" : head === "wOFF" ? "woff" : head === "OTTO" ? "otf" : head === "\0\0\0" || head === "true" ? "ttf" : "";
|
|
17290
|
+
if (!ext) throw new Error(`${f.family}: that link isn't a font file.`);
|
|
17291
|
+
if (bytes.length > 4 * 1024 * 1024) throw new Error(`${f.family}: the font file is larger than 4 MB.`);
|
|
17292
|
+
const weight = f.weight || "400";
|
|
17293
|
+
const style = f.style || "normal";
|
|
17294
|
+
const name = `${f.family.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${weight.replace(/\s+/g, "-")}${style === "italic" ? "-italic" : ""}.${ext}`;
|
|
17295
|
+
let bin = "";
|
|
17296
|
+
for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
17297
|
+
files[`assets/fonts/${name}`] = btoa(bin);
|
|
17298
|
+
let fam = fams.find((x) => x.slug === f.slug);
|
|
17299
|
+
const stack = `"${f.family}", ${f.fallback || "sans-serif"}`;
|
|
17300
|
+
if (!fam || !fam.fontFamily.includes(`"${f.family}"`)) {
|
|
17301
|
+
const keep = fam ? fams.indexOf(fam) : -1;
|
|
17302
|
+
fam = { slug: f.slug, name: f.family, fontFamily: stack, fontFace: [] };
|
|
17303
|
+
if (keep >= 0) fams[keep] = fam;
|
|
17304
|
+
else fams.push(fam);
|
|
17305
|
+
}
|
|
17306
|
+
fam.fontFace = [
|
|
17307
|
+
...(fam.fontFace ?? []).filter((x) => !(x.fontWeight === weight && x.fontStyle === style)),
|
|
17308
|
+
{
|
|
17309
|
+
fontFamily: f.family,
|
|
17310
|
+
fontWeight: weight,
|
|
17311
|
+
fontStyle: style,
|
|
17312
|
+
fontDisplay: "swap",
|
|
17313
|
+
src: [`file:./assets/fonts/${name}`]
|
|
17314
|
+
}
|
|
17315
|
+
];
|
|
17316
|
+
}
|
|
17317
|
+
tj0.settings.typography = { ...typo, fontFamilies: fams };
|
|
17318
|
+
files["theme.json"] = JSON.stringify(tj0, null, " ");
|
|
17319
|
+
}
|
|
17137
17320
|
if (a.colors || a.fonts || a.styles) {
|
|
17138
|
-
const tj = JSON.parse(cur["theme.json"] ?? '{"version":3,"settings":{}}');
|
|
17139
|
-
tj.settings
|
|
17321
|
+
const tj = JSON.parse(files["theme.json"] ?? cur["theme.json"] ?? '{"version":3,"settings":{}}');
|
|
17322
|
+
if (!tj.settings || Array.isArray(tj.settings)) tj.settings = {};
|
|
17323
|
+
if (Array.isArray(tj.styles)) tj.styles = {};
|
|
17140
17324
|
if (a.colors) {
|
|
17141
17325
|
let palette = tj.settings.color?.palette ?? [];
|
|
17142
17326
|
if (!palette.length) palette = ((await styleGet(site))._palette ?? []).map((c) => ({ ...c }));
|
|
@@ -21022,7 +21206,7 @@ var program = new Command();
|
|
|
21022
21206
|
// package.json
|
|
21023
21207
|
var package_default = {
|
|
21024
21208
|
name: "@cmo-so/cli",
|
|
21025
|
-
version: "0.12.
|
|
21209
|
+
version: "0.12.6",
|
|
21026
21210
|
description: "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
|
|
21027
21211
|
type: "module",
|
|
21028
21212
|
license: "MIT",
|
|
@@ -31993,6 +32177,9 @@ function registerWp(program3) {
|
|
|
31993
32177
|
wp.command("pages").description("find pages").option("--search <words>", "words in the title").option("--missing-seo", "only pages without their own SEO title or description").option("--template <slug>", "only this template").action(
|
|
31994
32178
|
(o, c) => run(g(c), "wp_pages_list", { search: o.search, missing_seo: !!o.missingSeo, template: o.template })
|
|
31995
32179
|
);
|
|
32180
|
+
wp.command("home [page]").description("choose the homepage (a published page), the posts page, or the latest posts").option("--posts-page <page>", 'the page that lists the blog posts ("" to clear)').option("--latest-posts", "show the newest posts as the homepage").action(
|
|
32181
|
+
(p, o, c) => run(g(c), "wp_site_home_set", { page: p, posts_page: o.postsPage, latest_posts: !!o.latestPosts })
|
|
32182
|
+
);
|
|
31996
32183
|
wp.command("sections").description("the section library and page blueprints").action(
|
|
31997
32184
|
(_o, c) => run(
|
|
31998
32185
|
g(c),
|
|
@@ -32037,6 +32224,9 @@ function registerWp(program3) {
|
|
|
32037
32224
|
replace: pairs(o.replace).map(([find2, replace2]) => ({ find: find2, replace: replace2 }))
|
|
32038
32225
|
})
|
|
32039
32226
|
);
|
|
32227
|
+
page.command("publish <page>").description("publish a page (checks it first); --homepage also makes it the homepage").option("--homepage", "make it the site's homepage").action(
|
|
32228
|
+
(p, o, c) => run(g(c), "wp_page_publish", { page: p, homepage: !!o.homepage })
|
|
32229
|
+
);
|
|
32040
32230
|
page.command("undo <page>").description("back to the version before the last change").action((p, _o, c) => run(g(c), "wp_page_undo", { page: p }));
|
|
32041
32231
|
const section = wp.command("section").description("add, change, move or remove a page's sections");
|
|
32042
32232
|
const pos = (cmd) => cmd.option("--after <section>", "after this section (id or type)").option("--before <section>", "before this section").option("--region <region>", "main, story or aside").option("--index <n>", "position in the region");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.6",
|
|
4
4
|
"description": "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|