@cmo-so/cli 0.12.3 → 0.12.5

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.
Files changed (2) hide show
  1. package/dist/cmo.mjs +155 -9
  2. package/package.json +1 -1
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 >= 4 ? "grid-cols-2 md:grid-cols-4" : "sm:grid-cols-3"}`
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("cta-card", { title: aside.ctaTitle || "Book your visit", text: aside.ctaText ?? "" }, kit)
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(
@@ -16399,7 +16404,11 @@ var pageTools = [
16399
16404
  description: str2(""),
16400
16405
  keywords: str2("")
16401
16406
  }),
16402
- cta: obj("Sidebar booking card: title, text.", { title: str2(""), text: str2("") }),
16407
+ cta: obj("Sidebar booking card: title, text, button label (default Book Now).", {
16408
+ title: str2(""),
16409
+ text: str2(""),
16410
+ button: str2("")
16411
+ }),
16403
16412
  related: bool(
16404
16413
  "Add a related-services band from sibling pages (default true when the page has a reading column)."
16405
16414
  ),
@@ -16444,7 +16453,8 @@ var pageTools = [
16444
16453
  const cta = a.cta ?? {};
16445
16454
  const blocks = composePage(sections, kit, {
16446
16455
  ctaTitle: cta.title || `Book ${String(a.title)}`,
16447
- ctaText: cta.text
16456
+ ctaText: cta.text,
16457
+ ctaButton: cta.button
16448
16458
  });
16449
16459
  const outline = brief2(blocks, kit.syncedTitles);
16450
16460
  if (a.preview) return { preview: true, title: a.title, sections: outline };
@@ -17108,6 +17118,17 @@ var childTools = [
17108
17118
  slug: str2("The child theme (default the active one)."),
17109
17119
  colors: obj('e.g. { "olive": "#4F5B48" }.'),
17110
17120
  fonts: obj('Font families by slug: { "body": "Jost, sans-serif" } (files must already be in the child).'),
17121
+ font_files: arr(
17122
+ "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).",
17123
+ obj("", {
17124
+ slug: str2("Family slug in theme.json: display, body, \u2026"),
17125
+ family: str2('Family name, e.g. "Roboto Slab".'),
17126
+ fallback: str2('Fallback stack, e.g. "Georgia, serif".'),
17127
+ url: str2("https link to the font file."),
17128
+ weight: str2('e.g. "400", "800" or "100 900".'),
17129
+ style: str2('"normal" or "italic".')
17130
+ })
17131
+ ),
17111
17132
  styles: obj('theme.json styles to merge in (e.g. { "elements": { "button": { \u2026 } } }).'),
17112
17133
  css: str2("Replace the child's CSS (plain CSS)."),
17113
17134
  css_append: str2("Add CSS at the end."),
@@ -17134,9 +17155,54 @@ var childTools = [
17134
17155
  const cur = info.files ?? {};
17135
17156
  const files = {};
17136
17157
  const notes = [];
17158
+ if (Array.isArray(a.font_files) && a.font_files.length) {
17159
+ const tj0 = JSON.parse(files["theme.json"] ?? cur["theme.json"] ?? '{"version":3,"settings":{}}');
17160
+ if (!tj0.settings || Array.isArray(tj0.settings)) tj0.settings = {};
17161
+ const typo = tj0.settings.typography ?? {};
17162
+ const fams = typo.fontFamilies ?? [];
17163
+ for (const f of a.font_files) {
17164
+ if (!/^[a-z0-9-]{2,30}$/.test(f.slug ?? ""))
17165
+ throw new Error(`Font slug "${f.slug}": lowercase letters, digits and dashes.`);
17166
+ if (!/^https:\/\//.test(f.url ?? "")) throw new Error(`${f.family}: the font's link must be https.`);
17167
+ const r = await fetch(f.url);
17168
+ if (!r.ok) throw new Error(`${f.family}: the font's link answered HTTP ${r.status}.`);
17169
+ const bytes = new Uint8Array(await r.arrayBuffer());
17170
+ const head = String.fromCharCode(...bytes.subarray(0, 4));
17171
+ const ext = head === "wOF2" ? "woff2" : head === "wOFF" ? "woff" : head === "OTTO" ? "otf" : head === "\0\0\0" || head === "true" ? "ttf" : "";
17172
+ if (!ext) throw new Error(`${f.family}: that link isn't a font file.`);
17173
+ if (bytes.length > 4 * 1024 * 1024) throw new Error(`${f.family}: the font file is larger than 4 MB.`);
17174
+ const weight = f.weight || "400";
17175
+ const style = f.style || "normal";
17176
+ const name = `${f.family.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${weight.replace(/\s+/g, "-")}${style === "italic" ? "-italic" : ""}.${ext}`;
17177
+ let bin = "";
17178
+ for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
17179
+ files[`assets/fonts/${name}`] = btoa(bin);
17180
+ let fam = fams.find((x) => x.slug === f.slug);
17181
+ const stack = `"${f.family}", ${f.fallback || "sans-serif"}`;
17182
+ if (!fam || !fam.fontFamily.includes(`"${f.family}"`)) {
17183
+ const keep = fam ? fams.indexOf(fam) : -1;
17184
+ fam = { slug: f.slug, name: f.family, fontFamily: stack, fontFace: [] };
17185
+ if (keep >= 0) fams[keep] = fam;
17186
+ else fams.push(fam);
17187
+ }
17188
+ fam.fontFace = [
17189
+ ...(fam.fontFace ?? []).filter((x) => !(x.fontWeight === weight && x.fontStyle === style)),
17190
+ {
17191
+ fontFamily: f.family,
17192
+ fontWeight: weight,
17193
+ fontStyle: style,
17194
+ fontDisplay: "swap",
17195
+ src: [`file:./assets/fonts/${name}`]
17196
+ }
17197
+ ];
17198
+ }
17199
+ tj0.settings.typography = { ...typo, fontFamilies: fams };
17200
+ files["theme.json"] = JSON.stringify(tj0, null, " ");
17201
+ }
17137
17202
  if (a.colors || a.fonts || a.styles) {
17138
- const tj = JSON.parse(cur["theme.json"] ?? '{"version":3,"settings":{}}');
17139
- tj.settings = tj.settings ?? {};
17203
+ const tj = JSON.parse(files["theme.json"] ?? cur["theme.json"] ?? '{"version":3,"settings":{}}');
17204
+ if (!tj.settings || Array.isArray(tj.settings)) tj.settings = {};
17205
+ if (Array.isArray(tj.styles)) tj.styles = {};
17140
17206
  if (a.colors) {
17141
17207
  let palette = tj.settings.color?.palette ?? [];
17142
17208
  if (!palette.length) palette = ((await styleGet(site))._palette ?? []).map((c) => ({ ...c }));
@@ -17265,6 +17331,81 @@ var childTools = [
17265
17331
  })
17266
17332
  })
17267
17333
  ];
17334
+ var themeTools = [
17335
+ tool({
17336
+ name: "wp_theme_updates",
17337
+ description: "Is there a newer Maggie Core for this site? The installed and latest versions, release notes link, whether this host lets it update itself, auto-updates on or off, and the earlier versions kept for rollback.",
17338
+ parameters: { type: "object", properties: {} },
17339
+ group: "site",
17340
+ need: READ,
17341
+ risk: "R0",
17342
+ gates: ["WP"],
17343
+ run: (ctx) => call(siteOf(ctx), { path: "/maggie-core/v1/update", query: { fresh: 1 } })
17344
+ }),
17345
+ tool({
17346
+ name: "wp_theme_update",
17347
+ description: "Update Maggie Core on the site to the latest version (checked against its published checksum; the current version is kept for rollback), then compare the homepage before and after (words, fonts, brand colours). If the site breaks (an error page or most of the homepage gone) it rolls back by itself. rollback_to puts an earlier kept version back instead. The site's look lives in the database and its child theme, so an update doesn't change it.",
17348
+ parameters: {
17349
+ type: "object",
17350
+ properties: { rollback_to: str2("Put this kept version back instead (from wp_theme_updates backups).") }
17351
+ },
17352
+ group: "site",
17353
+ need: WRITE,
17354
+ risk: "W3",
17355
+ gates: [...GATES],
17356
+ confirm: (a, lang) => a.rollback_to ? say(
17357
+ lang,
17358
+ `Put Maggie Core ${String(a.rollback_to)} back on your website.`,
17359
+ `\u628A\u7DB2\u7AD9\u7684 Maggie Core \u9084\u539F\u5230 ${String(a.rollback_to)}\u3002`
17360
+ ) : say(
17361
+ lang,
17362
+ "Update Maggie Core on your website to the latest version (rolled back by itself if the site breaks).",
17363
+ "\u628A\u7DB2\u7AD9\u7684 Maggie Core \u66F4\u65B0\u5230\u6700\u65B0\u7248\u672C\uFF08\u7DB2\u7AD9\u51FA\u932F\u6703\u81EA\u52D5\u9084\u539F\uFF09\u3002"
17364
+ ),
17365
+ run: async (ctx, a) => {
17366
+ const site = siteOf(ctx);
17367
+ const before = await homeShape(site);
17368
+ if (a.rollback_to) {
17369
+ const r2 = await call(site, {
17370
+ method: "POST",
17371
+ path: "/maggie-core/v1/update/rollback",
17372
+ body: { version: a.rollback_to }
17373
+ });
17374
+ return { ...r2, check: compareShapes(before, await homeShape(site)) };
17375
+ }
17376
+ const info = await call(site, { path: "/maggie-core/v1/update", query: { fresh: 1 } });
17377
+ if (!info.available)
17378
+ return {
17379
+ updated: false,
17380
+ current: info.current,
17381
+ latest: info.latest,
17382
+ note: info.check_error ?? "Already the latest Maggie Core."
17383
+ };
17384
+ if (!info.can_update)
17385
+ throw new Error(info.update_issue ?? "This host doesn't let WordPress update themes itself.");
17386
+ const r = await call(site, {
17387
+ method: "POST",
17388
+ path: "/maggie-core/v1/update"
17389
+ });
17390
+ const check2 = compareShapes(before, await homeShape(site));
17391
+ const broken = check2.checked && (check2.problems.some((p) => p.startsWith("the homepage answers")) || (check2.same_text_percent ?? 100) < 60);
17392
+ if (broken) {
17393
+ const back = await call(site, {
17394
+ method: "POST",
17395
+ path: "/maggie-core/v1/update/rollback",
17396
+ body: { version: r.before }
17397
+ }).catch((e) => ({ error: e.message }));
17398
+ return {
17399
+ ...r,
17400
+ check: check2,
17401
+ rolled_back: back,
17402
+ note: `The homepage looked broken after the update, so Maggie Core ${r.before} was put back.`
17403
+ };
17404
+ }
17405
+ return { ...r, check: check2, notes: info.notes, undo: `wp_theme_update rollback_to ${r.before}` };
17406
+ }
17407
+ })
17408
+ ];
17268
17409
  function deepMerge(a, b) {
17269
17410
  const out = { ...a };
17270
17411
  for (const [k, v] of Object.entries(b)) {
@@ -17534,6 +17675,7 @@ var wordpressTools = [
17534
17675
  ...sectionTools,
17535
17676
  ...mediaTools,
17536
17677
  ...childTools,
17678
+ ...themeTools,
17537
17679
  ...seoTools,
17538
17680
  ...elementTools
17539
17681
  ];
@@ -20946,7 +21088,7 @@ var program = new Command();
20946
21088
  // package.json
20947
21089
  var package_default = {
20948
21090
  name: "@cmo-so/cli",
20949
- version: "0.12.3",
21091
+ version: "0.12.5",
20950
21092
  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.",
20951
21093
  type: "module",
20952
21094
  license: "MIT",
@@ -32095,6 +32237,10 @@ ${table(r.issues, ["severity", "code", "path", "detail"])}`;
32095
32237
  }
32096
32238
  })
32097
32239
  );
32240
+ const theme = wp.command("theme").description("Maggie Core updates");
32241
+ theme.command("status", { isDefault: true }).description("installed and latest version, rollback versions").action((_o, c) => run(g(c), "wp_theme_updates", {}));
32242
+ theme.command("update").description("update to the latest (homepage checked; rolled back by itself if it breaks)").action((_o, c) => run(g(c), "wp_theme_update", {}));
32243
+ theme.command("rollback <version>").description("put a kept earlier version back").action((version2, _o, c) => run(g(c), "wp_theme_update", { rollback_to: version2 }));
32098
32244
  const child = wp.command("child").description("the site's Maggie child theme, on the site itself (data files only, backups kept)");
32099
32245
  child.command("status [slug]", { isDefault: true }).description("child themes, their files and backups; with a slug, its files in full").action((slug, _o, c) => run(g(c), "wp_child_theme_status", { slug }));
32100
32246
  child.command("create <slug>").description("create maggie-<name> from the site's current look").requiredOption("--name <name>").option("--empty", "start empty instead of from the site").option("--activate", "switch the site to it (checks the homepage looks the same)").action(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/cli",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
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",