@cmo-so/cli 0.12.2 → 0.12.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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/cmo.mjs +398 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,7 +46,9 @@ cmo wp redirects [ls] | set <from> <to> [--code 301] | remove <from…>
46
46
  cmo wp menu [get] | set --from menu.json cmo wp header|footer [get] | set --replace a=b --link old=new
47
47
  cmo wp header set --button-text "Book Now" --button-url … cmo wp style [get] | set olive=#4F5B48
48
48
  cmo wp pattern contact --replace "old=new" shared contact band / share buttons on every page
49
- cmo wp child-theme <slug> --name "…" --from-site [--out dir] the site's look as a child theme of Maggie Core
49
+ cmo wp child [status] | create maggie-<name> --name … [--activate] | update --color olive=#4F5B48 --css-file x.css
50
+ cmo wp child activate <slug|maggie-core> | restore <slug> <backup> the site's child theme, on the site (backups kept)
51
+ cmo wp child-theme <slug> --name "…" --from-site [--out dir] export the site's look as a child theme folder (for git)
50
52
  cmo wp migrate plan [--out wp-baseline.json] | migrate verify --baseline wp-baseline.json the SEO gate
51
53
  ```
52
54
 
package/dist/cmo.mjs CHANGED
@@ -16983,6 +16983,371 @@ var mediaTools = [
16983
16983
  }
16984
16984
  })
16985
16985
  ];
16986
+ var childInfo = (site, slug) => call(site, { path: "/maggie-core/v1/child-theme", query: { slug } });
16987
+ async function childSlug(site, slug) {
16988
+ if (typeof slug === "string" && slug) return slug;
16989
+ const info = await childInfo(site);
16990
+ const active = info.children.find((c) => c.active);
16991
+ if (!active)
16992
+ throw new Error("No Maggie child theme is active here: name one (slug), or create one with wp_child_theme_create.");
16993
+ return active.slug;
16994
+ }
16995
+ async function homeShape(site) {
16996
+ if (!site.fetchPage || !site.url) return null;
16997
+ const r = await site.fetchPage(`${site.url}/?maggie-check=${Date.now()}`).catch(() => null);
16998
+ if (!r) return null;
16999
+ const text3 = r.html.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>/g, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
17000
+ const fonts = [
17001
+ ...new Set([...r.html.matchAll(/font-family:\s*([^;"}]+)/g)].map((m) => m[1].trim()))
17002
+ ].sort();
17003
+ const colors = [...r.html.matchAll(/--wp--preset--color--([a-z0-9-]+):\s*([^;]+);/g)].map((m) => `${m[1]}=${m[2].trim()}`).sort();
17004
+ return { status: r.status, words: new Set(text3.toLowerCase().split(" ")), fonts, colors };
17005
+ }
17006
+ function compareShapes(a, b) {
17007
+ if (!a || !b) return { checked: false };
17008
+ const shared = [...a.words].filter((w) => b.words.has(w)).length;
17009
+ const text3 = Math.round(shared / Math.max(1, Math.max(a.words.size, b.words.size)) * 100);
17010
+ const fontsSame = a.fonts.join("|") === b.fonts.join("|");
17011
+ const colorsSame = a.colors.join("|") === b.colors.join("|");
17012
+ const problems = [
17013
+ b.status >= 400 ? `the homepage answers HTTP ${b.status}` : "",
17014
+ text3 < 90 ? `only ${text3}% of the homepage's words are the same` : "",
17015
+ fontsSame ? "" : "the fonts changed",
17016
+ colorsSame ? "" : "the brand colours changed"
17017
+ ].filter(Boolean);
17018
+ return {
17019
+ checked: true,
17020
+ same_text_percent: text3,
17021
+ fonts_same: fontsSame,
17022
+ colors_same: colorsSame,
17023
+ ok: !problems.length,
17024
+ problems
17025
+ };
17026
+ }
17027
+ var PATTERN_SCHEMA = obj("A pattern for the editor's inserter.", {
17028
+ slug: str2("File name: lowercase, dashes (e.g. spring-offer)."),
17029
+ title: str2("Name shown in the inserter."),
17030
+ content: str2("Block markup (e.g. a section from wp_section_library written out, or copied from a page)."),
17031
+ description: str2(""),
17032
+ categories: arr("Pattern categories (default maggie).", { type: "string" })
17033
+ });
17034
+ var childTools = [
17035
+ tool({
17036
+ name: "wp_child_theme_status",
17037
+ description: "The site's Maggie child themes (Maggie Core is the parent): which is active, their files and backups, and whether files can be written on this host. With slug: that child's text files in full (theme.json, style.css, parts, patterns) to read before changing them.",
17038
+ parameters: { type: "object", properties: { slug: str2("A child theme to read in full, e.g. maggie-moonlake.") } },
17039
+ group: "site",
17040
+ need: READ,
17041
+ risk: "R0",
17042
+ gates: ["WP"],
17043
+ run: async (ctx, a) => {
17044
+ const info = await childInfo(siteOf(ctx), a.slug || void 0);
17045
+ const files = info.files ? Object.fromEntries(
17046
+ Object.entries(info.files).map(([k, v]) => [k, v.length > 2e4 ? `${v.slice(0, 2e4)}\u2026` : v])
17047
+ ) : void 0;
17048
+ return {
17049
+ ...info,
17050
+ children: info.children.map((c) => ({
17051
+ ...c,
17052
+ files: Object.keys(c.files).length,
17053
+ file_list: Object.keys(c.files)
17054
+ })),
17055
+ ...files ? { files } : {}
17056
+ };
17057
+ }
17058
+ }),
17059
+ tool({
17060
+ name: "wp_child_theme_create",
17061
+ description: "Create a child theme of Maggie Core for this site (slug maggie-<name>), by default from the site's current look: its Global Styles (colours, fonts with their files, styles), edited header and footer. Data files only, never code. activate: true switches the site to it and checks the homepage looks the same. Switching keeps the look only because it is copied into the child; Global Styles and edited parts are kept per theme in WordPress.",
17062
+ parameters: {
17063
+ type: "object",
17064
+ properties: {
17065
+ slug: str2("maggie-<name>, e.g. maggie-moonlake."),
17066
+ name: str2("Theme name, e.g. Moon Lake Spa."),
17067
+ from_site: bool("Copy the site's current look into it (default true)."),
17068
+ activate: bool("Switch the site to it now (default false).")
17069
+ },
17070
+ required: ["slug", "name"]
17071
+ },
17072
+ group: "site",
17073
+ need: WRITE,
17074
+ risk: "W3",
17075
+ gates: [...GATES],
17076
+ confirm: (a, lang) => say(
17077
+ lang,
17078
+ `Create the child theme ${q2(a.slug, "")} on your website${a.activate ? " and switch to it" : ""}.`,
17079
+ `\u5728\u7DB2\u7AD9\u4E0A\u5EFA\u7ACB\u5B50\u4E3B\u984C ${q2(a.slug, "")}${a.activate ? "\u4E26\u5207\u63DB\u904E\u53BB" : ""}\u3002`
17080
+ ),
17081
+ run: async (ctx, a) => {
17082
+ const site = siteOf(ctx);
17083
+ const out = await call(site, {
17084
+ method: "POST",
17085
+ path: "/maggie-core/v1/child-theme",
17086
+ body: { slug: a.slug, name: a.name, from_site: a.from_site !== false }
17087
+ });
17088
+ if (!a.activate)
17089
+ return {
17090
+ ...out,
17091
+ next: "Switch to it with wp_child_theme_activate when ready (the tool checks the homepage looks the same)."
17092
+ };
17093
+ const before = await homeShape(site);
17094
+ const act2 = await call(site, {
17095
+ method: "POST",
17096
+ path: "/maggie-core/v1/child-theme/activate",
17097
+ body: { slug: out.slug }
17098
+ });
17099
+ return { ...out, activated: act2, check: compareShapes(before, await homeShape(site)) };
17100
+ }
17101
+ }),
17102
+ tool({
17103
+ name: "wp_child_theme_update",
17104
+ description: "Change a Maggie child theme's files (default: the active child): brand colours (by name, hex) and fonts (by name, CSS font-family of fonts already in it) in theme.json, extra theme.json styles, plain CSS (replace or append), the header or footer (block markup), templates, and the site's own patterns (shown in the editor's inserter). Data only, never code. Each change keeps a backup (wp_child_theme_restore). Changes made with wp_style_set / wp_header_set are stored in the database and win over the child's files.",
17105
+ parameters: {
17106
+ type: "object",
17107
+ properties: {
17108
+ slug: str2("The child theme (default the active one)."),
17109
+ colors: obj('e.g. { "olive": "#4F5B48" }.'),
17110
+ fonts: obj('Font families by slug: { "body": "Jost, sans-serif" } (files must already be in the child).'),
17111
+ styles: obj('theme.json styles to merge in (e.g. { "elements": { "button": { \u2026 } } }).'),
17112
+ css: str2("Replace the child's CSS (plain CSS)."),
17113
+ css_append: str2("Add CSS at the end."),
17114
+ header: str2("Header block markup (parts/header.html)."),
17115
+ footer: str2("Footer block markup (parts/footer.html)."),
17116
+ templates: obj('Templates by slug \u2192 block markup, e.g. { "page-landing": "\u2026" }.'),
17117
+ patterns: arr("Patterns to add or replace.", PATTERN_SCHEMA),
17118
+ remove_patterns: arr("Pattern slugs to remove.", { type: "string" })
17119
+ }
17120
+ },
17121
+ group: "site",
17122
+ need: WRITE,
17123
+ risk: "W3",
17124
+ gates: [...GATES],
17125
+ confirm: (a, lang) => say(
17126
+ lang,
17127
+ `Change the child theme ${q2(a.slug, "(the active one)")} on your website.`,
17128
+ `\u4FEE\u6539\u7DB2\u7AD9\u7684\u5B50\u4E3B\u984C ${q2(a.slug, "\uFF08\u76EE\u524D\u4F7F\u7528\u4E2D\u7684\uFF09")}\u3002`
17129
+ ),
17130
+ run: async (ctx, a) => {
17131
+ const site = siteOf(ctx);
17132
+ const slug = await childSlug(site, a.slug);
17133
+ const info = await childInfo(site, slug);
17134
+ const cur = info.files ?? {};
17135
+ const files = {};
17136
+ const notes = [];
17137
+ if (a.colors || a.fonts || a.styles) {
17138
+ const tj = JSON.parse(cur["theme.json"] ?? '{"version":3,"settings":{}}');
17139
+ tj.settings = tj.settings ?? {};
17140
+ if (a.colors) {
17141
+ let palette = tj.settings.color?.palette ?? [];
17142
+ if (!palette.length) palette = ((await styleGet(site))._palette ?? []).map((c) => ({ ...c }));
17143
+ for (const [slugC, color] of Object.entries(a.colors)) {
17144
+ if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(color))
17145
+ throw new Error(`${slugC}: "${color}" isn't a hex colour.`);
17146
+ const c = palette.find((x) => x.slug === slugC);
17147
+ if (c) c.color = color;
17148
+ else
17149
+ palette.push({
17150
+ slug: slugC,
17151
+ color,
17152
+ name: slugC.replace(/-/g, " ").replace(/\b\w/g, (m) => m.toUpperCase())
17153
+ });
17154
+ }
17155
+ const map = Object.fromEntries(palette.map((c) => [c.slug, c.color]));
17156
+ const bad = contrastReport(map).filter((r) => !r.ok);
17157
+ if (bad.length)
17158
+ notes.push(`Low contrast: ${bad.map((r) => `${r.text} on ${r.background} ${r.ratio}:1`).join(", ")}.`);
17159
+ tj.settings.color = { ...tj.settings.color ?? {}, palette };
17160
+ }
17161
+ if (a.fonts) {
17162
+ const fams = tj.settings.typography?.fontFamilies ?? [];
17163
+ if (!fams.length)
17164
+ throw new Error(
17165
+ "This child has no font list yet (fonts come from the parent): create it from the site first, then change fonts."
17166
+ );
17167
+ for (const [slugF, family] of Object.entries(a.fonts)) {
17168
+ const f = fams.find((x) => x.slug === slugF);
17169
+ if (!f) throw new Error(`No font "${slugF}" in the child. Fonts: ${fams.map((x) => x.slug).join(", ")}`);
17170
+ f.fontFamily = family;
17171
+ }
17172
+ }
17173
+ if (a.styles)
17174
+ tj.styles = deepMerge(tj.styles ?? {}, a.styles);
17175
+ files["theme.json"] = JSON.stringify(tj, null, " ");
17176
+ }
17177
+ if (typeof a.css === "string") files["style.css"] = a.css;
17178
+ if (typeof a.css_append === "string")
17179
+ files["style.css"] = `${(files["style.css"] ?? cur["style.css"] ?? "").replace(/\n*$/, "\n")}${a.css_append}
17180
+ `;
17181
+ const markup = (path3, m) => {
17182
+ if (typeof m !== "string" || !m.trim()) return;
17183
+ if (!parse(m).some((b) => b.name)) throw new Error(`${path3}: give block markup (<!-- wp:\u2026 -->).`);
17184
+ files[path3] = m;
17185
+ };
17186
+ markup("parts/header.html", a.header);
17187
+ markup("parts/footer.html", a.footer);
17188
+ for (const [t, m] of Object.entries(a.templates ?? {}))
17189
+ markup(`templates/${t}.html`, m);
17190
+ for (const p of a.patterns ?? []) {
17191
+ if (!/^[a-z0-9][a-z0-9-]{0,60}$/.test(p.slug ?? ""))
17192
+ throw new Error(`Pattern slug "${p.slug}": lowercase letters, digits and dashes.`);
17193
+ files[`maggie-patterns/${p.slug}.json`] = JSON.stringify({
17194
+ title: p.title,
17195
+ content: p.content,
17196
+ description: p.description ?? "",
17197
+ categories: p.categories?.length ? p.categories : ["maggie"]
17198
+ });
17199
+ }
17200
+ const del = (a.remove_patterns ?? []).map((x) => `maggie-patterns/${x}.json`);
17201
+ if (!Object.keys(files).length && !del.length) throw new Error("Nothing to change.");
17202
+ const out = await call(site, {
17203
+ method: "POST",
17204
+ path: "/maggie-core/v1/child-theme",
17205
+ body: { slug, files, delete: del }
17206
+ });
17207
+ return {
17208
+ slug,
17209
+ ...out,
17210
+ ...notes.length ? { notes } : {},
17211
+ ...out.active ? {} : { note: "This child isn't active: switch with wp_child_theme_activate to see it." }
17212
+ };
17213
+ }
17214
+ }),
17215
+ tool({
17216
+ name: "wp_child_theme_activate",
17217
+ description: "Switch the site to a Maggie child theme, or back to Maggie Core (slug maggie-core). Checks the homepage before and after (words, fonts, brand colours) and reports differences. WordPress keeps Global Styles and edited header/footer per theme: a child made with wp_child_theme_create from the site carries them; switching back to Maggie Core brings its own back.",
17218
+ parameters: {
17219
+ type: "object",
17220
+ properties: { slug: str2("maggie-<name>, or maggie-core to go back.") },
17221
+ required: ["slug"]
17222
+ },
17223
+ group: "site",
17224
+ need: WRITE,
17225
+ risk: "W3",
17226
+ gates: [...GATES],
17227
+ confirm: (a, lang) => say(lang, `Switch your website's theme to ${q2(a.slug, "")}.`, `\u628A\u7DB2\u7AD9\u7684\u4E3B\u984C\u5207\u63DB\u70BA ${q2(a.slug, "")}\u3002`),
17228
+ run: async (ctx, a) => {
17229
+ const site = siteOf(ctx);
17230
+ const before = await homeShape(site);
17231
+ const out = await call(site, {
17232
+ method: "POST",
17233
+ path: "/maggie-core/v1/child-theme/activate",
17234
+ body: { slug: a.slug }
17235
+ });
17236
+ const check2 = compareShapes(before, await homeShape(site));
17237
+ return {
17238
+ ...out,
17239
+ check: check2,
17240
+ ...check2.checked && !check2.ok ? { undo: `Switch back with wp_child_theme_activate slug ${out.before}.` } : {}
17241
+ };
17242
+ }
17243
+ }),
17244
+ tool({
17245
+ name: "wp_child_theme_restore",
17246
+ description: "Put a child theme's files back as they were before one change (backup ids from wp_child_theme_status). The current files are backed up first, so a restore can be undone too.",
17247
+ parameters: {
17248
+ type: "object",
17249
+ properties: { slug: str2("The child theme."), backup: str2("Backup id, e.g. 20261104-183208-728.") },
17250
+ required: ["slug", "backup"]
17251
+ },
17252
+ group: "site",
17253
+ need: WRITE,
17254
+ risk: "W3",
17255
+ gates: [...GATES],
17256
+ confirm: (a, lang) => say(
17257
+ lang,
17258
+ `Restore the child theme ${q2(a.slug, "")} to backup ${String(a.backup ?? "")}.`,
17259
+ `\u628A\u5B50\u4E3B\u984C ${q2(a.slug, "")} \u9084\u539F\u5230\u5099\u4EFD ${String(a.backup ?? "")}\u3002`
17260
+ ),
17261
+ run: (ctx, a) => call(siteOf(ctx), {
17262
+ method: "POST",
17263
+ path: "/maggie-core/v1/child-theme/restore",
17264
+ body: { slug: a.slug, backup: a.backup }
17265
+ })
17266
+ })
17267
+ ];
17268
+ var themeTools = [
17269
+ tool({
17270
+ name: "wp_theme_updates",
17271
+ 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.",
17272
+ parameters: { type: "object", properties: {} },
17273
+ group: "site",
17274
+ need: READ,
17275
+ risk: "R0",
17276
+ gates: ["WP"],
17277
+ run: (ctx) => call(siteOf(ctx), { path: "/maggie-core/v1/update", query: { fresh: 1 } })
17278
+ }),
17279
+ tool({
17280
+ name: "wp_theme_update",
17281
+ 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.",
17282
+ parameters: {
17283
+ type: "object",
17284
+ properties: { rollback_to: str2("Put this kept version back instead (from wp_theme_updates backups).") }
17285
+ },
17286
+ group: "site",
17287
+ need: WRITE,
17288
+ risk: "W3",
17289
+ gates: [...GATES],
17290
+ confirm: (a, lang) => a.rollback_to ? say(
17291
+ lang,
17292
+ `Put Maggie Core ${String(a.rollback_to)} back on your website.`,
17293
+ `\u628A\u7DB2\u7AD9\u7684 Maggie Core \u9084\u539F\u5230 ${String(a.rollback_to)}\u3002`
17294
+ ) : say(
17295
+ lang,
17296
+ "Update Maggie Core on your website to the latest version (rolled back by itself if the site breaks).",
17297
+ "\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"
17298
+ ),
17299
+ run: async (ctx, a) => {
17300
+ const site = siteOf(ctx);
17301
+ const before = await homeShape(site);
17302
+ if (a.rollback_to) {
17303
+ const r2 = await call(site, {
17304
+ method: "POST",
17305
+ path: "/maggie-core/v1/update/rollback",
17306
+ body: { version: a.rollback_to }
17307
+ });
17308
+ return { ...r2, check: compareShapes(before, await homeShape(site)) };
17309
+ }
17310
+ const info = await call(site, { path: "/maggie-core/v1/update", query: { fresh: 1 } });
17311
+ if (!info.available)
17312
+ return {
17313
+ updated: false,
17314
+ current: info.current,
17315
+ latest: info.latest,
17316
+ note: info.check_error ?? "Already the latest Maggie Core."
17317
+ };
17318
+ if (!info.can_update)
17319
+ throw new Error(info.update_issue ?? "This host doesn't let WordPress update themes itself.");
17320
+ const r = await call(site, {
17321
+ method: "POST",
17322
+ path: "/maggie-core/v1/update"
17323
+ });
17324
+ const check2 = compareShapes(before, await homeShape(site));
17325
+ const broken = check2.checked && (check2.problems.some((p) => p.startsWith("the homepage answers")) || (check2.same_text_percent ?? 100) < 60);
17326
+ if (broken) {
17327
+ const back = await call(site, {
17328
+ method: "POST",
17329
+ path: "/maggie-core/v1/update/rollback",
17330
+ body: { version: r.before }
17331
+ }).catch((e) => ({ error: e.message }));
17332
+ return {
17333
+ ...r,
17334
+ check: check2,
17335
+ rolled_back: back,
17336
+ note: `The homepage looked broken after the update, so Maggie Core ${r.before} was put back.`
17337
+ };
17338
+ }
17339
+ return { ...r, check: check2, notes: info.notes, undo: `wp_theme_update rollback_to ${r.before}` };
17340
+ }
17341
+ })
17342
+ ];
17343
+ function deepMerge(a, b) {
17344
+ const out = { ...a };
17345
+ for (const [k, v] of Object.entries(b)) {
17346
+ const cur = out[k];
17347
+ out[k] = v && typeof v === "object" && !Array.isArray(v) && cur && typeof cur === "object" && !Array.isArray(cur) ? deepMerge(cur, v) : v;
17348
+ }
17349
+ return out;
17350
+ }
16986
17351
  var seoFields = {
16987
17352
  title: str2("SEO title (\u224860 characters; the site name is added by the theme)."),
16988
17353
  description: str2("Meta description (\u2248150-160 characters)."),
@@ -17243,6 +17608,8 @@ var wordpressTools = [
17243
17608
  ...pageTools,
17244
17609
  ...sectionTools,
17245
17610
  ...mediaTools,
17611
+ ...childTools,
17612
+ ...themeTools,
17246
17613
  ...seoTools,
17247
17614
  ...elementTools
17248
17615
  ];
@@ -20655,7 +21022,7 @@ var program = new Command();
20655
21022
  // package.json
20656
21023
  var package_default = {
20657
21024
  name: "@cmo-so/cli",
20658
- version: "0.12.2",
21025
+ version: "0.12.4",
20659
21026
  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.",
20660
21027
  type: "module",
20661
21028
  license: "MIT",
@@ -31804,7 +32171,36 @@ ${table(r.issues, ["severity", "code", "path", "detail"])}`;
31804
32171
  }
31805
32172
  })
31806
32173
  );
31807
- wp.command("child-theme <slug>").description("write a child theme of Maggie Core (the site's brand, header, footer, patterns) to a folder").requiredOption("--name <name>", 'theme name, e.g. "Moon Lake Spa"').option("--out <dir>", "where to write it (default ./<slug>)").option("--from-site", "take the brand, header and footer from the connected site").option("--color <slug=hex...>", "brand colours").option("--font <slug=family...>", "fonts (replaces Maggie Core's font list: ship their files too)").action(
32174
+ const theme = wp.command("theme").description("Maggie Core updates");
32175
+ theme.command("status", { isDefault: true }).description("installed and latest version, rollback versions").action((_o, c) => run(g(c), "wp_theme_updates", {}));
32176
+ 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", {}));
32177
+ theme.command("rollback <version>").description("put a kept earlier version back").action((version2, _o, c) => run(g(c), "wp_theme_update", { rollback_to: version2 }));
32178
+ const child = wp.command("child").description("the site's Maggie child theme, on the site itself (data files only, backups kept)");
32179
+ 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 }));
32180
+ 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(
32181
+ (slug, o, c) => run(g(c), "wp_child_theme_create", { slug, name: o.name, from_site: !o.empty, activate: !!o.activate })
32182
+ );
32183
+ child.command("update [slug]").description(
32184
+ "change the child: colours, fonts, CSS, header/footer, templates, patterns (JSON file with the tool's arguments)"
32185
+ ).option(
32186
+ "--from <file>",
32187
+ "JSON: { colors, fonts, styles, css, css_append, header, footer, templates, patterns, remove_patterns }"
32188
+ ).option("--color <slug=hex...>").option("--css-file <file>", "replace the CSS with this file").option("--header-file <file>", "header block markup").option("--footer-file <file>", "footer block markup").action(
32189
+ async (slug, o, c) => {
32190
+ const args = o.from ? await json(o.from) : {};
32191
+ if (slug) args.slug = slug;
32192
+ if (o.color) args.colors = Object.fromEntries(pairs(o.color));
32193
+ if (o.cssFile) args.css = await readFile2(o.cssFile, "utf8");
32194
+ if (o.headerFile) args.header = await readFile2(o.headerFile, "utf8");
32195
+ if (o.footerFile) args.footer = await readFile2(o.footerFile, "utf8");
32196
+ await run(g(c), "wp_child_theme_update", args);
32197
+ }
32198
+ );
32199
+ child.command("activate <slug>").description("switch the site to a child (or maggie-core to go back); checks the homepage before and after").action((slug, _o, c) => run(g(c), "wp_child_theme_activate", { slug }));
32200
+ child.command("restore <slug> <backup>").description("put a backup of the child's files back").action((slug, backup, _o, c) => run(g(c), "wp_child_theme_restore", { slug, backup }));
32201
+ wp.command("child-theme <slug>").description(
32202
+ "export the site's look as a child theme folder on this computer (to keep in git); cmo wp child works on the site itself"
32203
+ ).requiredOption("--name <name>", 'theme name, e.g. "Moon Lake Spa"').option("--out <dir>", "where to write it (default ./<slug>)").option("--from-site", "take the brand, header and footer from the connected site").option("--color <slug=hex...>", "brand colours").option("--font <slug=family...>", "fonts (replaces Maggie Core's font list: ship their files too)").action(
31808
32204
  async (slug, o, c) => {
31809
32205
  const opts = g(c);
31810
32206
  const colors = Object.fromEntries(pairs(o.color));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/cli",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
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",