@cmo-so/cli 0.12.8 → 0.12.9

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 +173 -17
  2. package/package.json +1 -1
package/dist/cmo.mjs CHANGED
@@ -14658,6 +14658,54 @@ function replace(blocks, ref, next, synced = {}) {
14658
14658
  function sectionAt(blocks, ref, synced = {}) {
14659
14659
  return pick(locate(blocks), ref, synced);
14660
14660
  }
14661
+ function widthValue(width) {
14662
+ const w = String(width ?? "").trim();
14663
+ if (w === "content") return null;
14664
+ if (w === "wide") return "var(--wp--style--global--wide-size)";
14665
+ if (/^\d{2,4}(\.\d+)?(px|rem|em|vw|%)$/.test(w)) return w;
14666
+ throw new Error('width: "content", "wide" or a CSS length such as 1240px.');
14667
+ }
14668
+ var NARROW_PX = 700;
14669
+ var sizePx = (v) => {
14670
+ const m = /^(\d+(?:\.\d+)?)(px|rem|em)$/.exec(v);
14671
+ return m ? Number(m[1]) * (m[2] === "px" ? 1 : 16) : null;
14672
+ };
14673
+ function setSectionWidth(b, width) {
14674
+ const size = widthValue(width);
14675
+ const before = [];
14676
+ const visit = (x, top) => {
14677
+ let changed = false;
14678
+ for (const c of x.inner) if (visit(c, false)) changed = true;
14679
+ const lay = x.attrs.layout;
14680
+ const constrained = lay?.type === "constrained";
14681
+ const cur = lay?.contentSize;
14682
+ const px = cur ? sizePx(cur) : null;
14683
+ const eligible = constrained && (!cur || cur.includes("--wp--style--global") || px == null || px >= NARROW_PX);
14684
+ if (eligible || top && !lay && (x.name === "core/group" || x.name === "core/cover")) {
14685
+ if ((cur ?? null) !== size) {
14686
+ before.push(cur ?? "content");
14687
+ const next = { ...lay ?? {}, type: "constrained" };
14688
+ if (size) next.contentSize = size;
14689
+ else delete next.contentSize;
14690
+ x.attrs = { ...x.attrs, layout: next };
14691
+ changed = true;
14692
+ }
14693
+ }
14694
+ if (changed) delete x.raw;
14695
+ return changed;
14696
+ };
14697
+ visit(b, true);
14698
+ return { containers: before.length, before };
14699
+ }
14700
+ function refreshRaw(list3) {
14701
+ let any = false;
14702
+ for (const b of list3) {
14703
+ const inner = refreshRaw(b.inner);
14704
+ if (inner) delete b.raw;
14705
+ if (inner || b.raw == null) any = true;
14706
+ }
14707
+ return any;
14708
+ }
14661
14709
  var escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14662
14710
  function applyEdits(b, e) {
14663
14711
  let m = serializeBlock(b);
@@ -15195,6 +15243,15 @@ async function styleGet(site) {
15195
15243
  fonts: (user.settings?.typography?.fontFamilies?.theme ?? []).map((f) => f.slug)
15196
15244
  },
15197
15245
  contrast: contrastReport(Object.fromEntries([...palette.values()].map((c) => [c.slug, c.color]))),
15246
+ layout: {
15247
+ content: user.settings?.layout?.contentSize ?? base.settings?.layout?.contentSize ?? null,
15248
+ wide: user.settings?.layout?.wideSize ?? base.settings?.layout?.wideSize ?? null,
15249
+ padding: {
15250
+ left: user.styles?.spacing?.padding?.left ?? base.styles?.spacing?.padding?.left ?? null,
15251
+ right: user.styles?.spacing?.padding?.right ?? base.styles?.spacing?.padding?.right ?? null
15252
+ }
15253
+ },
15254
+ css: user.styles?.css ?? "",
15198
15255
  _palette: [...palette.values()],
15199
15256
  _fonts: [...fonts.values()],
15200
15257
  _user: user
@@ -15235,6 +15292,17 @@ function contrastReport(colors) {
15235
15292
  });
15236
15293
  }
15237
15294
  async function styleSet(site, change, force = false) {
15295
+ const len = (v, what) => {
15296
+ const t = String(v ?? "").trim();
15297
+ if (!/^(0|\d{1,4}(\.\d+)?(px|rem|em|vw|%)|var\(--wp--preset--spacing--[a-z0-9-]+\)|clamp\([0-9a-z.,%+\-\s()]+\))$/i.test(
15298
+ t
15299
+ ))
15300
+ throw new Error(`${what}: "${t}" isn't a CSS length like 1240px, 2rem or var(--wp--preset--spacing--50).`);
15301
+ return t;
15302
+ };
15303
+ for (const css of [change.css, change.css_append])
15304
+ if (css != null && /<\/?style|<\?|<script|@import\s+url\(\s*['"]?(?!https:)/i.test(css))
15305
+ throw new Error("css: plain CSS only (no <style>, <script>, PHP or non-https imports).");
15238
15306
  const cur = await styleGet(site);
15239
15307
  const palette = cur._palette.map((c) => ({ ...c }));
15240
15308
  for (const [slug, color] of Object.entries(change.colors ?? {})) {
@@ -15259,15 +15327,48 @@ async function styleSet(site, change, force = false) {
15259
15327
  );
15260
15328
  const user = cur._user;
15261
15329
  const settings2 = { ...user.settings ?? {} };
15330
+ const styles = { ...user.styles ?? {} };
15262
15331
  if (change.colors) settings2.color = { ...settings2.color ?? {}, palette: { theme: palette } };
15263
15332
  if (change.fonts)
15264
15333
  settings2.typography = { ...settings2.typography ?? {}, fontFamilies: { theme: fonts } };
15334
+ if (change.layout) {
15335
+ const layout = { ...settings2.layout ?? {} };
15336
+ if (change.layout.content != null) layout.contentSize = len(change.layout.content, "layout.content");
15337
+ if (change.layout.wide != null) layout.wideSize = len(change.layout.wide, "layout.wide");
15338
+ settings2.layout = layout;
15339
+ }
15340
+ if (change.padding) {
15341
+ const spacing = { ...styles.spacing ?? {} };
15342
+ const pad = { ...spacing.padding ?? {} };
15343
+ if (change.padding.left != null) pad.left = len(change.padding.left, "padding.left");
15344
+ if (change.padding.right != null) pad.right = len(change.padding.right, "padding.right");
15345
+ styles.spacing = { ...spacing, padding: pad };
15346
+ settings2.useRootPaddingAwareAlignments = true;
15347
+ }
15348
+ if (change.css != null) styles.css = change.css;
15349
+ if (change.css_append)
15350
+ styles.css = `${String(styles.css ?? "").trimEnd()}
15351
+
15352
+ ${change.css_append.trim()}
15353
+ `.trimStart();
15265
15354
  await call(site, {
15266
15355
  method: "POST",
15267
15356
  path: `/wp/v2/global-styles/${cur.id}`,
15268
- body: { settings: settings2, styles: user.styles ?? {} }
15357
+ body: { settings: settings2, styles }
15269
15358
  });
15359
+ const changedLayout = change.layout || change.padding;
15270
15360
  return {
15361
+ ...changedLayout ? {
15362
+ layout: {
15363
+ before: cur.layout,
15364
+ after: {
15365
+ content: settings2.layout?.contentSize ?? cur.layout.content,
15366
+ wide: settings2.layout?.wideSize ?? cur.layout.wide,
15367
+ padding: change.padding ? { ...cur.layout.padding, ...change.padding } : cur.layout.padding
15368
+ }
15369
+ }
15370
+ } : {},
15371
+ ...change.css != null || change.css_append ? { css: { before: cur.css.length, after: String(styles.css ?? "").length, before_text: cur.css } } : {},
15271
15372
  colors: Object.fromEntries(
15272
15373
  Object.entries(change.colors ?? {}).map(([k, v]) => [k, { before: cur.colors[k], after: v }])
15273
15374
  ),
@@ -16386,7 +16487,7 @@ var siteTools = [
16386
16487
  }),
16387
16488
  tool({
16388
16489
  name: "wp_style_get",
16389
- description: "The brand: colours (by name: canvas, panel, soft, ink, muted, olive, accent\u2026) and fonts (display, body), and the text contrast of each colour pair.",
16490
+ description: "The site's look: colours (by name: canvas, panel, soft, ink, muted, olive, accent\u2026), fonts (display, body), layout widths (content, wide) and page padding, the extra CSS, and the text contrast of each colour pair.",
16390
16491
  parameters: { type: "object", properties: {} },
16391
16492
  group: "site",
16392
16493
  need: READ,
@@ -16394,7 +16495,14 @@ var siteTools = [
16394
16495
  gates: ["WP"],
16395
16496
  run: async (ctx) => {
16396
16497
  const s = await styleGet(siteOf(ctx));
16397
- return { colors: s.colors, fonts: s.fonts, customized: s.customized, contrast: s.contrast };
16498
+ return {
16499
+ colors: s.colors,
16500
+ fonts: s.fonts,
16501
+ layout: s.layout,
16502
+ css: s.css,
16503
+ customized: s.customized,
16504
+ contrast: s.contrast
16505
+ };
16398
16506
  }
16399
16507
  })
16400
16508
  ];
@@ -16836,12 +16944,15 @@ var sectionTools = [
16836
16944
  }),
16837
16945
  tool({
16838
16946
  name: "wp_section_update",
16839
- description: "Change one section. edits: change text, links or pictures inside it and keep its design (works on any section, the homepage too). slots: write the section again from new content (generated types; give the complete content). faq: add, remove (1-based numbers) or replace questions.",
16947
+ description: 'Change one section. width: line its content up with the header and footer ("wide") or another width, one section or all. edits: change text, links or pictures inside it and keep its design (works on any section, the homepage too). slots: write the section again from new content (generated types; give the complete content). faq: add, remove (1-based numbers) or replace questions.',
16840
16948
  parameters: {
16841
16949
  type: "object",
16842
16950
  properties: {
16843
16951
  page: PAGE,
16844
- section: str2("Section id from wp_page_get (s2, m4) or its type (faq)."),
16952
+ section: str2('Section id from wp_page_get (s2, m4) or its type (faq); "all" with width only.'),
16953
+ width: str2(
16954
+ `The section's inner width: "content" (the site's content width), "wide" (the site's wide width, as the header and footer) or a CSS length (1240px). Changes the section and its containers wider than 700px; with section "all", every section on the page.`
16955
+ ),
16845
16956
  edits: EDITS,
16846
16957
  slots: obj("Complete new content for the section (same type unless type is given)."),
16847
16958
  markup: str2(
@@ -16870,6 +16981,19 @@ var sectionTools = [
16870
16981
  ),
16871
16982
  run: (ctx, a) => changePage(ctx, a, (blocks, kit) => {
16872
16983
  const ref = String(a.section);
16984
+ if (a.width != null && a.width !== "") {
16985
+ const targets = ref === "all" ? locate(blocks) : [sectionAt(blocks, ref, kit.syncedTitles)];
16986
+ const changed = {};
16987
+ for (const t of targets) {
16988
+ if (t.block.name === "core/block") continue;
16989
+ const r = setSectionWidth(t.block, a.width);
16990
+ if (r.containers) changed[t.id] = r;
16991
+ }
16992
+ refreshRaw(blocks);
16993
+ if (!Object.keys(changed).length) throw new Error("Those sections already have that width.");
16994
+ return { blocks, note: { width: String(a.width), sections: changed } };
16995
+ }
16996
+ if (ref === "all") throw new Error('section "all" works with width only.');
16873
16997
  const target = sectionAt(blocks, ref, kit.syncedTitles);
16874
16998
  const type = classify(target.block, target.region, kit.syncedTitles);
16875
16999
  let next;
@@ -17775,23 +17899,41 @@ var elementTools = [
17775
17899
  }),
17776
17900
  tool({
17777
17901
  name: "wp_style_set",
17778
- description: "Change brand colours (by name, hex) and fonts (by name, CSS font-family) for the whole site. Refuses colours that would make text hard to read (contrast under 4.5:1).",
17902
+ description: "Change the whole site's look: brand colours (by name, hex), fonts (by name, CSS font-family), layout widths (content: the width sections and text use; wide: the header, footer and wide blocks), the left and right page padding, and extra CSS. Saved in the site's Global Styles (works on any block theme; returns before and after). Refuses colours that would make text hard to read (contrast under 4.5:1). One section's width: wp_section_update width.",
17779
17903
  parameters: {
17780
17904
  type: "object",
17781
17905
  properties: {
17782
17906
  colors: obj('e.g. { "olive": "#4F5B48", "accent": "#A8834F" }.'),
17783
- fonts: obj('e.g. { "display": "\\"Playfair Display\\", Georgia, serif" }.')
17907
+ fonts: obj('e.g. { "display": "\\"Playfair Display\\", Georgia, serif" }.'),
17908
+ layout: obj('Widths, e.g. { "content": "1240px", "wide": "1240px" }.', {
17909
+ content: str2("Content width (CSS length)."),
17910
+ wide: str2("Wide width (CSS length).")
17911
+ }),
17912
+ padding: obj('Left and right page padding, e.g. { "left": "24px", "right": "24px" }.', {
17913
+ left: str2(""),
17914
+ right: str2("")
17915
+ }),
17916
+ css: str2("Replace the site's extra CSS (Global Styles \u2192 Additional CSS); plain CSS only."),
17917
+ css_append: str2("Add this CSS after the site's extra CSS.")
17784
17918
  }
17785
17919
  },
17786
17920
  group: "site",
17787
17921
  need: WRITE,
17788
17922
  risk: "W3",
17789
17923
  gates: [...GATES],
17790
- confirm: (_a3, lang) => say(lang, "Change the website's brand colours or fonts (every page).", "\u4FEE\u6539\u7DB2\u7AD9\u7684\u54C1\u724C\u984F\u8272\u6216\u5B57\u9AD4\uFF08\u6240\u6709\u9801\u9762\uFF09\u3002"),
17791
- run: async (ctx, a) => {
17792
- const r = await styleSet(siteOf(ctx), { colors: a.colors, fonts: a.fonts });
17793
- return r;
17794
- }
17924
+ confirm: (_a3, lang) => say(
17925
+ lang,
17926
+ "Change the website's look for every page (colours, fonts, widths, padding or extra CSS); the result shows before and after.",
17927
+ "\u4FEE\u6539\u7DB2\u7AD9\u6240\u6709\u9801\u9762\u7684\u5916\u89C0\uFF08\u984F\u8272\u3001\u5B57\u9AD4\u3001\u5BEC\u5EA6\u3001\u908A\u8DDD\u6216\u984D\u5916 CSS\uFF09\uFF1B\u7D50\u679C\u6703\u5217\u51FA\u4FEE\u6539\u524D\u5F8C\u3002"
17928
+ ),
17929
+ run: async (ctx, a) => styleSet(siteOf(ctx), {
17930
+ colors: a.colors,
17931
+ fonts: a.fonts,
17932
+ layout: a.layout,
17933
+ padding: a.padding,
17934
+ css: typeof a.css === "string" ? a.css : void 0,
17935
+ css_append: typeof a.css_append === "string" ? a.css_append : void 0
17936
+ })
17795
17937
  })
17796
17938
  ];
17797
17939
  function checkMarkup(markup, what) {
@@ -21766,7 +21908,7 @@ var program = new Command();
21766
21908
  // package.json
21767
21909
  var package_default = {
21768
21910
  name: "@cmo-so/cli",
21769
- version: "0.12.8",
21911
+ version: "0.12.9",
21770
21912
  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.",
21771
21913
  type: "module",
21772
21914
  license: "MIT",
@@ -32813,6 +32955,16 @@ function registerWp(program3) {
32813
32955
  o.preview ? { tool: "wp_section_update", args } : args
32814
32956
  );
32815
32957
  });
32958
+ section.command("width <page> <section> <width>").description(
32959
+ 'line sections up: width "wide" (as the header and footer), "content" or 1240px; section "all" = every section'
32960
+ ).option("--preview", "show the result, save nothing").action(async (p, s, w, o, c) => {
32961
+ const args = { page: p, section: s, width: w };
32962
+ await run(
32963
+ g(c),
32964
+ o.preview ? "wp_preview" : "wp_section_update",
32965
+ o.preview ? { tool: "wp_section_update", args } : args
32966
+ );
32967
+ });
32816
32968
  pos(section.command("move <page> <section>").description("move a section")).action(
32817
32969
  (p, s, o, c) => run(g(c), "wp_section_move", { page: p, section: s, ...posArgs(o) })
32818
32970
  );
@@ -32878,12 +33030,16 @@ ${table(r.issues, ["severity", "code", "path", "detail"])}`;
32878
33030
  await run(g(c), part === "header" ? "wp_header_set" : "wp_footer_set", args);
32879
33031
  });
32880
33032
  }
32881
- const style = wp.command("style").description("brand colours and fonts");
33033
+ const style = wp.command("style").description("brand colours, fonts, layout widths, page padding and extra CSS");
32882
33034
  style.command("get", { isDefault: true }).action((_o, c) => run(g(c), "wp_style_get", {}));
32883
- style.command("set [colors...]").description(`e.g. cmo wp style set olive=#4F5B48 --font 'display="Playfair Display", serif'`).option("--font <slug=family...>").action(
32884
- (colors, o, c) => run(g(c), "wp_style_set", {
33035
+ style.command("set [colors...]").description(`e.g. cmo wp style set olive=#4F5B48 --font 'display="Playfair Display", serif'`).option("--font <slug=family...>").option("--content <width>", "content width, e.g. 1240px").option("--wide <width>", "wide width (header, footer, wide blocks)").option("--padding <length>", "left and right page padding, e.g. 24px").option("--css-file <file>", "replace the site's extra CSS with this file").option("--css-append <file>", "add this CSS file after the site's extra CSS").action(
33036
+ async (colors, o, c) => run(g(c), "wp_style_set", {
32885
33037
  colors: colors.length ? Object.fromEntries(pairs(colors)) : void 0,
32886
- fonts: o.font ? Object.fromEntries(pairs(o.font)) : void 0
33038
+ fonts: o.font ? Object.fromEntries(pairs(o.font)) : void 0,
33039
+ layout: o.content || o.wide ? { content: o.content, wide: o.wide } : void 0,
33040
+ padding: o.padding ? { left: o.padding, right: o.padding } : void 0,
33041
+ css: o.cssFile ? await readFile2(o.cssFile, "utf8") : void 0,
33042
+ css_append: o.cssAppend ? await readFile2(o.cssAppend, "utf8") : void 0
32887
33043
  })
32888
33044
  );
32889
33045
  const media = wp.command("media").description("pictures in the media library");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/cli",
3
- "version": "0.12.8",
3
+ "version": "0.12.9",
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",