@cmo-so/cli 0.12.5 → 0.12.7
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 +684 -6
- 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
|
@@ -16181,6 +16181,27 @@ async function changePage(ctx, a, fn) {
|
|
|
16181
16181
|
undo: "wp_page_undo goes back to the version before this change."
|
|
16182
16182
|
};
|
|
16183
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
|
+
}
|
|
16184
16205
|
function position(a) {
|
|
16185
16206
|
return {
|
|
16186
16207
|
after: a.after || void 0,
|
|
@@ -16553,6 +16574,103 @@ var pageTools = [
|
|
|
16553
16574
|
};
|
|
16554
16575
|
}
|
|
16555
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
|
+
}),
|
|
16556
16674
|
tool({
|
|
16557
16675
|
name: "wp_page_delete",
|
|
16558
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.",
|
|
@@ -16726,6 +16844,9 @@ var sectionTools = [
|
|
|
16726
16844
|
section: str2("Section id from wp_page_get (s2, m4) or its type (faq)."),
|
|
16727
16845
|
edits: EDITS,
|
|
16728
16846
|
slots: obj("Complete new content for the section (same type unless type is given)."),
|
|
16847
|
+
markup: str2(
|
|
16848
|
+
"Replace the section with this block markup (one top-level block; written by a coding agent; PHP never)."
|
|
16849
|
+
),
|
|
16729
16850
|
type: str2("With slots: change the section to this type."),
|
|
16730
16851
|
faq: obj("FAQ changes.", {
|
|
16731
16852
|
add: arr("Questions to add.", obj("", { q: str2(""), a: str2("") })),
|
|
@@ -16757,6 +16878,10 @@ var sectionTools = [
|
|
|
16757
16878
|
if (type !== "faq") throw new Error(`Section ${ref} is a ${type} section, not the FAQ.`);
|
|
16758
16879
|
next = editFaq(target.block, a.faq, kit);
|
|
16759
16880
|
note = { questions: describe2({ ...target, block: next }, kit.syncedTitles).counts?.questions };
|
|
16881
|
+
} else if (typeof a.markup === "string") {
|
|
16882
|
+
const bl = checkMarkup(a.markup, "markup").filter((b) => b.name);
|
|
16883
|
+
if (bl.length !== 1) throw new Error("markup: give exactly one top-level block (wrap several in a group).");
|
|
16884
|
+
next = bl[0];
|
|
16760
16885
|
} else if (a.slots) {
|
|
16761
16886
|
const t = String(a.type ?? type);
|
|
16762
16887
|
const keepName = target.block.attrs.metadata?.name;
|
|
@@ -16777,7 +16902,7 @@ var sectionTools = [
|
|
|
16777
16902
|
);
|
|
16778
16903
|
next = r.block;
|
|
16779
16904
|
note = { changed: r.changed };
|
|
16780
|
-
} else throw new Error("Give edits, slots or faq.");
|
|
16905
|
+
} else throw new Error("Give edits, slots, markup or faq.");
|
|
16781
16906
|
return { blocks: replace(blocks, ref, next, kit.syncedTitles), note };
|
|
16782
16907
|
})
|
|
16783
16908
|
}),
|
|
@@ -17669,7 +17794,493 @@ var elementTools = [
|
|
|
17669
17794
|
}
|
|
17670
17795
|
})
|
|
17671
17796
|
];
|
|
17797
|
+
function checkMarkup(markup, what) {
|
|
17798
|
+
if (typeof markup !== "string" || !markup.trim()) throw new Error(`${what}: give block markup (<!-- wp:\u2026 -->).`);
|
|
17799
|
+
if (/<\?(php|=)?/i.test(markup)) throw new Error(`${what}: PHP isn't allowed.`);
|
|
17800
|
+
const blocks = parse(markup);
|
|
17801
|
+
if (!blocks.some((b) => b.name)) throw new Error(`${what}: give block markup (<!-- wp:\u2026 -->), not plain HTML.`);
|
|
17802
|
+
return blocks;
|
|
17803
|
+
}
|
|
17804
|
+
var POST_FIELDS = "id,title,slug,link,status,date,modified,categories,tags,featured_media,excerpt,meta";
|
|
17805
|
+
async function termIds(site, taxonomy, names) {
|
|
17806
|
+
if (!Array.isArray(names)) return void 0;
|
|
17807
|
+
const ids3 = [];
|
|
17808
|
+
for (const n2 of names) {
|
|
17809
|
+
if (typeof n2 === "number") {
|
|
17810
|
+
ids3.push(n2);
|
|
17811
|
+
continue;
|
|
17812
|
+
}
|
|
17813
|
+
const name = String(n2).trim();
|
|
17814
|
+
if (!name) continue;
|
|
17815
|
+
const found = await call(site, {
|
|
17816
|
+
path: `/wp/v2/${taxonomy}`,
|
|
17817
|
+
query: { search: name, per_page: 20, _fields: "id,name" }
|
|
17818
|
+
});
|
|
17819
|
+
const hit = found.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
17820
|
+
ids3.push(
|
|
17821
|
+
hit ? hit.id : (await call(site, { method: "POST", path: `/wp/v2/${taxonomy}`, body: { name } })).id
|
|
17822
|
+
);
|
|
17823
|
+
}
|
|
17824
|
+
return ids3;
|
|
17825
|
+
}
|
|
17826
|
+
var fullTools = [
|
|
17827
|
+
tool({
|
|
17828
|
+
name: "wp_page_markup_get",
|
|
17829
|
+
description: "A page's full block markup (Gutenberg), for coding agents that edit blocks directly; with section, only that section's markup. Pair with wp_page_markup_set or wp_section_update markup.",
|
|
17830
|
+
parameters: {
|
|
17831
|
+
type: "object",
|
|
17832
|
+
properties: { page: PAGE, section: str2("Only this section (id or type).") },
|
|
17833
|
+
required: ["page"]
|
|
17834
|
+
},
|
|
17835
|
+
group: "site",
|
|
17836
|
+
need: READ,
|
|
17837
|
+
risk: "R0",
|
|
17838
|
+
gates: ["WP"],
|
|
17839
|
+
run: async (ctx, a) => {
|
|
17840
|
+
const site = siteOf(ctx);
|
|
17841
|
+
const p = await findPage(site, a.page, true);
|
|
17842
|
+
const raw = p.content?.raw ?? "";
|
|
17843
|
+
if (a.section) {
|
|
17844
|
+
const kit = await siteKit(site);
|
|
17845
|
+
const l = sectionAt(parse(raw), String(a.section), kit.syncedTitles);
|
|
17846
|
+
return { id: p.id, path: path(p.link), modified: p.modified, section: l.id, markup: serialize([l.block]) };
|
|
17847
|
+
}
|
|
17848
|
+
return { id: p.id, path: path(p.link), modified: p.modified, template: p.template || "default", markup: raw };
|
|
17849
|
+
}
|
|
17850
|
+
}),
|
|
17851
|
+
tool({
|
|
17852
|
+
name: "wp_page_markup_set",
|
|
17853
|
+
description: "Replace a page's whole content with block markup a coding agent wrote (Gutenberg blocks, Maggie Core classes and Tailwind classes allowed; PHP never). Read it first with wp_page_markup_get and pass its modified as expect_modified. preview shows the sections before and after without saving. wp_page_undo goes back.",
|
|
17854
|
+
parameters: {
|
|
17855
|
+
type: "object",
|
|
17856
|
+
properties: {
|
|
17857
|
+
page: PAGE,
|
|
17858
|
+
markup: str2("The page's new block markup."),
|
|
17859
|
+
expect_modified: EXPECT,
|
|
17860
|
+
preview: PREVIEW
|
|
17861
|
+
},
|
|
17862
|
+
required: ["page", "markup"]
|
|
17863
|
+
},
|
|
17864
|
+
group: "site",
|
|
17865
|
+
need: WRITE,
|
|
17866
|
+
risk: "W3",
|
|
17867
|
+
gates: [...GATES],
|
|
17868
|
+
confirm: (a, lang) => say(
|
|
17869
|
+
lang,
|
|
17870
|
+
`Replace the content of page ${q2(a.page, "")} with new block markup.`,
|
|
17871
|
+
`\u7528\u65B0\u7684\u5340\u584A\u5167\u5BB9\u53D6\u4EE3\u9801\u9762 ${q2(a.page, "")}\u3002`
|
|
17872
|
+
),
|
|
17873
|
+
run: (ctx, a) => changePage(ctx, a, () => ({ blocks: checkMarkup(a.markup, "markup") }))
|
|
17874
|
+
}),
|
|
17875
|
+
tool({
|
|
17876
|
+
name: "wp_posts_list",
|
|
17877
|
+
description: "Blog posts on the site (newest first): id, title, path, status, date, categories and tags. Search words, status, category.",
|
|
17878
|
+
parameters: {
|
|
17879
|
+
type: "object",
|
|
17880
|
+
properties: {
|
|
17881
|
+
search: str2("Words in the title or content."),
|
|
17882
|
+
status: { type: "string", enum: ["publish", "draft", "future", "private", "any"], description: "Default any." },
|
|
17883
|
+
category: str2("Category name or id."),
|
|
17884
|
+
limit: int2("How many (default 30, at most 100).")
|
|
17885
|
+
}
|
|
17886
|
+
},
|
|
17887
|
+
group: "site",
|
|
17888
|
+
need: READ,
|
|
17889
|
+
risk: "R0",
|
|
17890
|
+
gates: ["WP"],
|
|
17891
|
+
run: async (ctx, a) => {
|
|
17892
|
+
const site = siteOf(ctx);
|
|
17893
|
+
const cat = a.category ? (await termIds(site, "categories", [a.category]).catch(() => void 0))?.[0] : void 0;
|
|
17894
|
+
const rows = await call(site, {
|
|
17895
|
+
path: "/wp/v2/posts",
|
|
17896
|
+
query: {
|
|
17897
|
+
context: "edit",
|
|
17898
|
+
search: a.search || void 0,
|
|
17899
|
+
status: a.status && a.status !== "any" ? String(a.status) : "publish,draft,future,private,pending",
|
|
17900
|
+
categories: cat,
|
|
17901
|
+
per_page: Math.min(100, Math.max(1, Number(a.limit) || 30)),
|
|
17902
|
+
_fields: "id,title,link,status,date,categories,tags"
|
|
17903
|
+
}
|
|
17904
|
+
});
|
|
17905
|
+
return {
|
|
17906
|
+
count: rows.length,
|
|
17907
|
+
items: rows.map((r) => ({
|
|
17908
|
+
id: r.id,
|
|
17909
|
+
title: r.title.raw ?? r.title.rendered,
|
|
17910
|
+
path: path(r.link),
|
|
17911
|
+
status: r.status,
|
|
17912
|
+
date: r.date,
|
|
17913
|
+
categories: r.categories,
|
|
17914
|
+
tags: r.tags
|
|
17915
|
+
}))
|
|
17916
|
+
};
|
|
17917
|
+
}
|
|
17918
|
+
}),
|
|
17919
|
+
tool({
|
|
17920
|
+
name: "wp_post_get",
|
|
17921
|
+
description: "One blog post in full: title, slug, status, dates, categories, tags, featured image, excerpt, SEO fields and its content (block markup or HTML).",
|
|
17922
|
+
parameters: { type: "object", properties: { post: int2("The post id (wp_posts_list).") }, required: ["post"] },
|
|
17923
|
+
group: "site",
|
|
17924
|
+
need: READ,
|
|
17925
|
+
risk: "R0",
|
|
17926
|
+
gates: ["WP"],
|
|
17927
|
+
run: async (ctx, a) => {
|
|
17928
|
+
const p = await call(siteOf(ctx), {
|
|
17929
|
+
path: `/wp/v2/posts/${Number(a.post)}`,
|
|
17930
|
+
query: { context: "edit", _fields: `${POST_FIELDS},content` }
|
|
17931
|
+
});
|
|
17932
|
+
return {
|
|
17933
|
+
...p,
|
|
17934
|
+
title: p.title.raw,
|
|
17935
|
+
path: path(p.link),
|
|
17936
|
+
excerpt: p.excerpt?.raw ?? "",
|
|
17937
|
+
content: p.content?.raw ?? ""
|
|
17938
|
+
};
|
|
17939
|
+
}
|
|
17940
|
+
}),
|
|
17941
|
+
tool({
|
|
17942
|
+
name: "wp_post_save",
|
|
17943
|
+
description: "Create a blog post (no post id) or change one (post id): title, content (block markup or HTML), excerpt, slug (a published post's old address gets a 301), status (draft, publish, private, future with date), date, categories and tags (names; new ones are created), featured_media, SEO title/description. New posts are drafts unless status says otherwise. CMO.so's blog autopilot keeps writing its own posts; this is for the owner's own changes.",
|
|
17944
|
+
parameters: {
|
|
17945
|
+
type: "object",
|
|
17946
|
+
properties: {
|
|
17947
|
+
post: int2("The post to change (leave out to create one)."),
|
|
17948
|
+
title: str2(""),
|
|
17949
|
+
content: str2("Block markup or HTML."),
|
|
17950
|
+
excerpt: str2(""),
|
|
17951
|
+
slug: str2(""),
|
|
17952
|
+
status: { type: "string", enum: ["draft", "publish", "private", "future"], description: "" },
|
|
17953
|
+
date: str2("Publish date (ISO 8601), for scheduled posts."),
|
|
17954
|
+
categories: arr("Category names (or ids).", { type: "string" }),
|
|
17955
|
+
tags: arr("Tag names (or ids).", { type: "string" }),
|
|
17956
|
+
featured_media: int2("Media id (wp_media_upload)."),
|
|
17957
|
+
seo_title: str2(""),
|
|
17958
|
+
seo_description: str2(""),
|
|
17959
|
+
expect_modified: EXPECT
|
|
17960
|
+
}
|
|
17961
|
+
},
|
|
17962
|
+
group: "site",
|
|
17963
|
+
need: WRITE,
|
|
17964
|
+
risk: "W3",
|
|
17965
|
+
gates: [...GATES],
|
|
17966
|
+
confirm: (a, lang) => a.post ? say(
|
|
17967
|
+
lang,
|
|
17968
|
+
`Change blog post #${String(a.post)} on your website${a.status === "publish" ? " (published)" : ""}.`,
|
|
17969
|
+
`\u4FEE\u6539\u7DB2\u7AD9\u4E0A\u7684\u6587\u7AE0 #${String(a.post)}\u3002`
|
|
17970
|
+
) : say(
|
|
17971
|
+
lang,
|
|
17972
|
+
`Create the blog post ${q2(a.title, "")} on your website (${a.status === "publish" ? "published" : "as a draft"}).`,
|
|
17973
|
+
`\u5728\u7DB2\u7AD9\u4E0A\u5EFA\u7ACB\u6587\u7AE0 ${q2(a.title, "")}\u3002`
|
|
17974
|
+
),
|
|
17975
|
+
run: async (ctx, a) => {
|
|
17976
|
+
const site = siteOf(ctx);
|
|
17977
|
+
const body = {};
|
|
17978
|
+
for (const k of ["title", "excerpt", "slug", "status", "date"])
|
|
17979
|
+
if (typeof a[k] === "string" && a[k] !== "") body[k] = a[k];
|
|
17980
|
+
if (typeof a.content === "string") {
|
|
17981
|
+
if (/<\?(php|=)?/i.test(a.content)) throw new Error("content: PHP isn't allowed.");
|
|
17982
|
+
body.content = a.content;
|
|
17983
|
+
}
|
|
17984
|
+
if (typeof a.featured_media === "number") body.featured_media = a.featured_media;
|
|
17985
|
+
const cats = await termIds(site, "categories", a.categories);
|
|
17986
|
+
const tags = await termIds(site, "tags", a.tags);
|
|
17987
|
+
if (cats) body.categories = cats;
|
|
17988
|
+
if (tags) body.tags = tags;
|
|
17989
|
+
const meta4 = {};
|
|
17990
|
+
if (typeof a.seo_title === "string") meta4.maggie_seo_title = a.seo_title;
|
|
17991
|
+
if (typeof a.seo_description === "string") meta4.maggie_seo_description = a.seo_description;
|
|
17992
|
+
if (Object.keys(meta4).length) body.meta = meta4;
|
|
17993
|
+
if (!a.post) {
|
|
17994
|
+
if (!body.title) throw new Error("A new post needs a title.");
|
|
17995
|
+
body.status = body.status ?? "draft";
|
|
17996
|
+
const p2 = await call(site, {
|
|
17997
|
+
method: "POST",
|
|
17998
|
+
path: "/wp/v2/posts",
|
|
17999
|
+
query: { _fields: "id,link,status,modified" },
|
|
18000
|
+
body
|
|
18001
|
+
});
|
|
18002
|
+
if (!p2?.id) throw new Error("WordPress didn't create the post.");
|
|
18003
|
+
return { created: true, id: p2.id, path: path(p2.link), status: p2.status, modified: p2.modified };
|
|
18004
|
+
}
|
|
18005
|
+
const before = await call(site, {
|
|
18006
|
+
path: `/wp/v2/posts/${Number(a.post)}`,
|
|
18007
|
+
query: { context: "edit", _fields: "id,link,status,modified" }
|
|
18008
|
+
});
|
|
18009
|
+
if (a.expect_modified && before.modified !== a.expect_modified)
|
|
18010
|
+
throw new Error(
|
|
18011
|
+
`The post changed in WordPress since you read it (${before.modified}). Read it again and redo the change.`
|
|
18012
|
+
);
|
|
18013
|
+
if (!Object.keys(body).length) throw new Error("Nothing to change.");
|
|
18014
|
+
const p = await call(site, {
|
|
18015
|
+
method: "POST",
|
|
18016
|
+
path: `/wp/v2/posts/${Number(a.post)}`,
|
|
18017
|
+
query: { _fields: "id,link,status,modified" },
|
|
18018
|
+
body
|
|
18019
|
+
});
|
|
18020
|
+
let redirect = null;
|
|
18021
|
+
if (before.status === "publish" && path(p.link) !== path(before.link))
|
|
18022
|
+
redirect = (await redirectsSet(site, { set: [{ from: path(before.link), to: path(p.link), code: 301 }] })).items.find((r) => r.from === path(before.link));
|
|
18023
|
+
return { saved: true, id: p.id, path: path(p.link), status: p.status, modified: p.modified, redirect };
|
|
18024
|
+
}
|
|
18025
|
+
}),
|
|
18026
|
+
tool({
|
|
18027
|
+
name: "wp_post_delete",
|
|
18028
|
+
description: "Move a blog post to the WordPress trash (restorable for 30 days) and redirect its address (301) to another page, by default the blog list or the homepage.",
|
|
18029
|
+
parameters: {
|
|
18030
|
+
type: "object",
|
|
18031
|
+
properties: {
|
|
18032
|
+
post: int2("The post id."),
|
|
18033
|
+
redirect_to: str2('Where its visitors go (path); default /. "410" = gone.')
|
|
18034
|
+
},
|
|
18035
|
+
required: ["post"]
|
|
18036
|
+
},
|
|
18037
|
+
group: "site",
|
|
18038
|
+
need: WRITE,
|
|
18039
|
+
risk: "W3",
|
|
18040
|
+
gates: [...GATES],
|
|
18041
|
+
confirm: (a, lang) => say(
|
|
18042
|
+
lang,
|
|
18043
|
+
`Move blog post #${String(a.post)} to the trash and redirect its address.`,
|
|
18044
|
+
`\u628A\u6587\u7AE0 #${String(a.post)} \u79FB\u5230\u5783\u573E\u6876\u4E26\u8F49\u5740\u3002`
|
|
18045
|
+
),
|
|
18046
|
+
run: async (ctx, a) => {
|
|
18047
|
+
const site = siteOf(ctx);
|
|
18048
|
+
const p = await call(site, {
|
|
18049
|
+
path: `/wp/v2/posts/${Number(a.post)}`,
|
|
18050
|
+
query: { context: "edit", _fields: "id,link,status" }
|
|
18051
|
+
});
|
|
18052
|
+
await call(site, { method: "DELETE", path: `/wp/v2/posts/${p.id}`, query: { _fields: "id,status" } });
|
|
18053
|
+
const to = String(a.redirect_to ?? "/");
|
|
18054
|
+
const from = path(p.link);
|
|
18055
|
+
const r = p.status === "publish" ? await redirectsSet(site, { set: to === "410" ? [{ from, code: 410 }] : [{ from, to, code: 301 }] }) : null;
|
|
18056
|
+
return {
|
|
18057
|
+
trashed: true,
|
|
18058
|
+
id: p.id,
|
|
18059
|
+
path: from,
|
|
18060
|
+
redirect: r ? to === "410" ? { from, code: 410 } : { from, to, code: 301 } : null
|
|
18061
|
+
};
|
|
18062
|
+
}
|
|
18063
|
+
}),
|
|
18064
|
+
tool({
|
|
18065
|
+
name: "wp_terms_list",
|
|
18066
|
+
description: "The blog's categories or tags: id, name, slug, description, post count.",
|
|
18067
|
+
parameters: {
|
|
18068
|
+
type: "object",
|
|
18069
|
+
properties: {
|
|
18070
|
+
taxonomy: { type: "string", enum: ["categories", "tags"], description: "Default categories." },
|
|
18071
|
+
search: str2("")
|
|
18072
|
+
}
|
|
18073
|
+
},
|
|
18074
|
+
group: "site",
|
|
18075
|
+
need: READ,
|
|
18076
|
+
risk: "R0",
|
|
18077
|
+
gates: ["WP"],
|
|
18078
|
+
run: async (ctx, a) => {
|
|
18079
|
+
const tax = a.taxonomy === "tags" ? "tags" : "categories";
|
|
18080
|
+
const rows = await all(
|
|
18081
|
+
siteOf(ctx),
|
|
18082
|
+
`/wp/v2/${tax}`,
|
|
18083
|
+
{ search: a.search || void 0, _fields: "id,name,slug,description,count,parent" },
|
|
18084
|
+
500
|
|
18085
|
+
);
|
|
18086
|
+
return { count: rows.length, items: rows };
|
|
18087
|
+
}
|
|
18088
|
+
}),
|
|
18089
|
+
tool({
|
|
18090
|
+
name: "wp_term_save",
|
|
18091
|
+
description: "Create a category or tag (no id) or change one (id): name, slug, description, parent (categories). The theme shows the description on its archive page.",
|
|
18092
|
+
parameters: {
|
|
18093
|
+
type: "object",
|
|
18094
|
+
properties: {
|
|
18095
|
+
taxonomy: { type: "string", enum: ["categories", "tags"], description: "Default categories." },
|
|
18096
|
+
id: int2("The term to change."),
|
|
18097
|
+
name: str2(""),
|
|
18098
|
+
slug: str2(""),
|
|
18099
|
+
description: str2(""),
|
|
18100
|
+
parent: int2("Parent category id.")
|
|
18101
|
+
}
|
|
18102
|
+
},
|
|
18103
|
+
group: "site",
|
|
18104
|
+
need: WRITE,
|
|
18105
|
+
risk: "W3",
|
|
18106
|
+
gates: [...GATES],
|
|
18107
|
+
confirm: (a, lang) => say(
|
|
18108
|
+
lang,
|
|
18109
|
+
`${a.id ? "Change" : "Create"} the ${a.taxonomy === "tags" ? "tag" : "category"} ${q2(a.name, a.id ? `#${String(a.id)}` : "")} on your website.`,
|
|
18110
|
+
`${a.id ? "\u4FEE\u6539" : "\u5EFA\u7ACB"}\u7DB2\u7AD9\u7684${a.taxonomy === "tags" ? "\u6A19\u7C64" : "\u5206\u985E"} ${q2(a.name, "")}\u3002`
|
|
18111
|
+
),
|
|
18112
|
+
run: async (ctx, a) => {
|
|
18113
|
+
const tax = a.taxonomy === "tags" ? "tags" : "categories";
|
|
18114
|
+
const body = {};
|
|
18115
|
+
for (const k of ["name", "slug", "description"]) if (typeof a[k] === "string") body[k] = a[k];
|
|
18116
|
+
if (typeof a.parent === "number" && tax === "categories") body.parent = a.parent;
|
|
18117
|
+
if (!a.id && !body.name) throw new Error("A new term needs a name.");
|
|
18118
|
+
return call(siteOf(ctx), {
|
|
18119
|
+
method: "POST",
|
|
18120
|
+
path: a.id ? `/wp/v2/${tax}/${Number(a.id)}` : `/wp/v2/${tax}`,
|
|
18121
|
+
query: { _fields: "id,name,slug,description,count,link" },
|
|
18122
|
+
body
|
|
18123
|
+
});
|
|
18124
|
+
}
|
|
18125
|
+
}),
|
|
18126
|
+
tool({
|
|
18127
|
+
name: "wp_settings_get",
|
|
18128
|
+
description: "The site's general settings: title, tagline, search-engine visibility, language, timezone, date and time formats, start of week, posts per page, homepage and posts page, logo and icon.",
|
|
18129
|
+
parameters: { type: "object", properties: {} },
|
|
18130
|
+
group: "site",
|
|
18131
|
+
need: READ,
|
|
18132
|
+
risk: "R0",
|
|
18133
|
+
gates: ["WP"],
|
|
18134
|
+
run: (ctx) => call(siteOf(ctx), {
|
|
18135
|
+
path: "/wp/v2/settings",
|
|
18136
|
+
query: {
|
|
18137
|
+
_fields: "title,description,url,blog_public,language,timezone,date_format,time_format,start_of_week,posts_per_page,show_on_front,page_on_front,page_for_posts,site_logo,site_icon"
|
|
18138
|
+
}
|
|
18139
|
+
})
|
|
18140
|
+
}),
|
|
18141
|
+
tool({
|
|
18142
|
+
name: "wp_settings_set",
|
|
18143
|
+
description: "Change the site's general settings: title, tagline (description), search_engines (true = let Google index the site, false = discourage it), timezone (e.g. Europe/London), date_format, time_format, start_of_week (0 Sunday \u2026 6), posts_per_page, site_logo and site_icon (media ids). Returns before and after. Homepage and posts page: wp_site_home_set.",
|
|
18144
|
+
parameters: {
|
|
18145
|
+
type: "object",
|
|
18146
|
+
properties: {
|
|
18147
|
+
title: str2("Site title."),
|
|
18148
|
+
description: str2("Tagline."),
|
|
18149
|
+
search_engines: bool("true: visible to search engines; false: ask them not to index the site."),
|
|
18150
|
+
timezone: str2("e.g. Europe/London."),
|
|
18151
|
+
date_format: str2("e.g. j F Y."),
|
|
18152
|
+
time_format: str2("e.g. H:i."),
|
|
18153
|
+
start_of_week: int2("0 Sunday \u2026 6 Saturday."),
|
|
18154
|
+
posts_per_page: int2("Posts on blog pages."),
|
|
18155
|
+
site_logo: int2("Media id of the logo."),
|
|
18156
|
+
site_icon: int2("Media id of the site icon (512\xD7512).")
|
|
18157
|
+
}
|
|
18158
|
+
},
|
|
18159
|
+
group: "site",
|
|
18160
|
+
need: WRITE,
|
|
18161
|
+
risk: "W3",
|
|
18162
|
+
gates: [...GATES],
|
|
18163
|
+
confirm: (a, lang) => a.search_engines === true ? say(
|
|
18164
|
+
lang,
|
|
18165
|
+
"Change your website's settings and let search engines index it.",
|
|
18166
|
+
"\u4FEE\u6539\u7DB2\u7AD9\u8A2D\u5B9A\uFF0C\u4E26\u5141\u8A31\u641C\u5C0B\u5F15\u64CE\u6536\u9304\u3002"
|
|
18167
|
+
) : a.search_engines === false ? say(
|
|
18168
|
+
lang,
|
|
18169
|
+
"Change your website's settings and ask search engines not to index it.",
|
|
18170
|
+
"\u4FEE\u6539\u7DB2\u7AD9\u8A2D\u5B9A\uFF0C\u4E26\u8981\u6C42\u641C\u5C0B\u5F15\u64CE\u4E0D\u8981\u6536\u9304\u3002"
|
|
18171
|
+
) : say(lang, "Change your website's general settings.", "\u4FEE\u6539\u7DB2\u7AD9\u7684\u4E00\u822C\u8A2D\u5B9A\u3002"),
|
|
18172
|
+
run: async (ctx, a) => {
|
|
18173
|
+
const site = siteOf(ctx);
|
|
18174
|
+
const body = {};
|
|
18175
|
+
for (const k of ["title", "description", "date_format", "time_format", "timezone"])
|
|
18176
|
+
if (typeof a[k] === "string") body[k] = a[k];
|
|
18177
|
+
for (const k of ["start_of_week", "posts_per_page", "site_logo", "site_icon"])
|
|
18178
|
+
if (typeof a[k] === "number") body[k] = a[k];
|
|
18179
|
+
if (typeof a.search_engines === "boolean") body.blog_public = a.search_engines ? 1 : 0;
|
|
18180
|
+
if (!Object.keys(body).length) throw new Error("Nothing to change.");
|
|
18181
|
+
const fields = Object.keys(body).join(",");
|
|
18182
|
+
const before = await call(site, { path: "/wp/v2/settings", query: { _fields: fields } });
|
|
18183
|
+
if ("blog_public" in body && !("blog_public" in before))
|
|
18184
|
+
throw new Error(
|
|
18185
|
+
"This site's theme doesn't expose search-engine visibility yet: update Maggie Core (wp_theme_update) to 0.11.4 or later."
|
|
18186
|
+
);
|
|
18187
|
+
const after = await call(site, {
|
|
18188
|
+
method: "POST",
|
|
18189
|
+
path: "/wp/v2/settings",
|
|
18190
|
+
query: { _fields: fields },
|
|
18191
|
+
body
|
|
18192
|
+
});
|
|
18193
|
+
return { before, after };
|
|
18194
|
+
}
|
|
18195
|
+
}),
|
|
18196
|
+
tool({
|
|
18197
|
+
name: "wp_plugins_list",
|
|
18198
|
+
description: "The site's plugins: name, version, active or not, and whether an update is waiting.",
|
|
18199
|
+
parameters: { type: "object", properties: {} },
|
|
18200
|
+
group: "site",
|
|
18201
|
+
need: READ,
|
|
18202
|
+
risk: "R0",
|
|
18203
|
+
gates: ["WP"],
|
|
18204
|
+
run: async (ctx) => {
|
|
18205
|
+
const rows = await call(siteOf(ctx), { path: "/wp/v2/plugins", query: { _fields: "plugin,name,version,status" } });
|
|
18206
|
+
return { count: rows.length, items: rows };
|
|
18207
|
+
}
|
|
18208
|
+
}),
|
|
18209
|
+
tool({
|
|
18210
|
+
name: "wp_plugin_set",
|
|
18211
|
+
description: "Install a plugin from WordPress.org by its slug (e.g. wpforms-lite), and/or activate or deactivate an installed one (plugin id from wp_plugins_list, e.g. akismet/akismet). Plugins can't be deleted from here. Installing runs the plugin's code on the site: only well-known plugins the owner asked for.",
|
|
18212
|
+
parameters: {
|
|
18213
|
+
type: "object",
|
|
18214
|
+
properties: {
|
|
18215
|
+
install: str2("WordPress.org slug to install."),
|
|
18216
|
+
plugin: str2("Installed plugin id (folder/file) to change."),
|
|
18217
|
+
status: {
|
|
18218
|
+
type: "string",
|
|
18219
|
+
enum: ["active", "inactive"],
|
|
18220
|
+
description: "Activate or deactivate (after installing, default inactive)."
|
|
18221
|
+
}
|
|
18222
|
+
}
|
|
18223
|
+
},
|
|
18224
|
+
group: "site",
|
|
18225
|
+
need: WRITE,
|
|
18226
|
+
risk: "W3",
|
|
18227
|
+
gates: [...GATES],
|
|
18228
|
+
confirm: (a, lang) => a.install ? say(
|
|
18229
|
+
lang,
|
|
18230
|
+
`Install the plugin ${q2(a.install, "")} from WordPress.org on your website${a.status === "active" ? " and activate it" : ""}.`,
|
|
18231
|
+
`\u5728\u7DB2\u7AD9\u4E0A\u5B89\u88DD WordPress.org \u63D2\u4EF6 ${q2(a.install, "")}${a.status === "active" ? "\u4E26\u555F\u7528" : ""}\u3002`
|
|
18232
|
+
) : say(
|
|
18233
|
+
lang,
|
|
18234
|
+
`${a.status === "active" ? "Activate" : "Deactivate"} the plugin ${q2(a.plugin, "")} on your website.`,
|
|
18235
|
+
`${a.status === "active" ? "\u555F\u7528" : "\u505C\u7528"}\u7DB2\u7AD9\u4E0A\u7684\u63D2\u4EF6 ${q2(a.plugin, "")}\u3002`
|
|
18236
|
+
),
|
|
18237
|
+
run: async (ctx, a) => {
|
|
18238
|
+
const site = siteOf(ctx);
|
|
18239
|
+
if (a.install) {
|
|
18240
|
+
if (!/^[a-z0-9-]{2,80}$/.test(String(a.install)))
|
|
18241
|
+
throw new Error("install: a WordPress.org plugin slug, e.g. wpforms-lite.");
|
|
18242
|
+
return call(site, {
|
|
18243
|
+
method: "POST",
|
|
18244
|
+
path: "/wp/v2/plugins",
|
|
18245
|
+
query: { _fields: "plugin,name,version,status" },
|
|
18246
|
+
body: { slug: a.install, status: a.status === "active" ? "active" : "inactive" }
|
|
18247
|
+
});
|
|
18248
|
+
}
|
|
18249
|
+
if (!a.plugin || !a.status) throw new Error("Give install (a slug), or plugin and status.");
|
|
18250
|
+
if (!/^[a-z0-9._-]+(\/[a-z0-9._-]+)?$/i.test(String(a.plugin)))
|
|
18251
|
+
throw new Error("plugin: the id from wp_plugins_list, e.g. akismet/akismet.");
|
|
18252
|
+
return call(site, {
|
|
18253
|
+
method: "POST",
|
|
18254
|
+
path: `/wp/v2/plugins/${String(a.plugin).replace(/\.php$/, "")}`,
|
|
18255
|
+
query: { _fields: "plugin,name,version,status" },
|
|
18256
|
+
body: { status: a.status }
|
|
18257
|
+
});
|
|
18258
|
+
}
|
|
18259
|
+
}),
|
|
18260
|
+
tool({
|
|
18261
|
+
name: "wp_api_read",
|
|
18262
|
+
description: "Read any WordPress REST route the site builder may use (GET only), for coding agents exploring a site: e.g. /wp/v2/types, /wp/v2/taxonomies, /wp/v2/templates, /wp/v2/block-patterns/patterns, /wp/v2/global-styles/themes/maggie-core, /maggie-core/v1/site. Results are trimmed to 60 items.",
|
|
18263
|
+
parameters: {
|
|
18264
|
+
type: "object",
|
|
18265
|
+
properties: { route: str2("The REST route under /wp-json, e.g. /wp/v2/types."), query: obj("Query parameters.") },
|
|
18266
|
+
required: ["route"]
|
|
18267
|
+
},
|
|
18268
|
+
group: "site",
|
|
18269
|
+
need: READ,
|
|
18270
|
+
risk: "R0",
|
|
18271
|
+
gates: ["WP"],
|
|
18272
|
+
run: async (ctx, a) => {
|
|
18273
|
+
const r = await siteOf(ctx).request({
|
|
18274
|
+
path: String(a.route),
|
|
18275
|
+
query: a.query ?? {}
|
|
18276
|
+
});
|
|
18277
|
+
const data = Array.isArray(r.data) ? r.data.slice(0, 60) : r.data;
|
|
18278
|
+
return { status: r.status, total: r.total ?? null, data };
|
|
18279
|
+
}
|
|
18280
|
+
})
|
|
18281
|
+
];
|
|
17672
18282
|
var wordpressTools = [
|
|
18283
|
+
...fullTools,
|
|
17673
18284
|
...siteTools,
|
|
17674
18285
|
...pageTools,
|
|
17675
18286
|
...sectionTools,
|
|
@@ -21088,7 +21699,7 @@ var program = new Command();
|
|
|
21088
21699
|
// package.json
|
|
21089
21700
|
var package_default = {
|
|
21090
21701
|
name: "@cmo-so/cli",
|
|
21091
|
-
version: "0.12.
|
|
21702
|
+
version: "0.12.7",
|
|
21092
21703
|
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.",
|
|
21093
21704
|
type: "module",
|
|
21094
21705
|
license: "MIT",
|
|
@@ -32059,6 +32670,9 @@ function registerWp(program3) {
|
|
|
32059
32670
|
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(
|
|
32060
32671
|
(o, c) => run(g(c), "wp_pages_list", { search: o.search, missing_seo: !!o.missingSeo, template: o.template })
|
|
32061
32672
|
);
|
|
32673
|
+
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(
|
|
32674
|
+
(p, o, c) => run(g(c), "wp_site_home_set", { page: p, posts_page: o.postsPage, latest_posts: !!o.latestPosts })
|
|
32675
|
+
);
|
|
32062
32676
|
wp.command("sections").description("the section library and page blueprints").action(
|
|
32063
32677
|
(_o, c) => run(
|
|
32064
32678
|
g(c),
|
|
@@ -32103,6 +32717,9 @@ function registerWp(program3) {
|
|
|
32103
32717
|
replace: pairs(o.replace).map(([find2, replace2]) => ({ find: find2, replace: replace2 }))
|
|
32104
32718
|
})
|
|
32105
32719
|
);
|
|
32720
|
+
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(
|
|
32721
|
+
(p, o, c) => run(g(c), "wp_page_publish", { page: p, homepage: !!o.homepage })
|
|
32722
|
+
);
|
|
32106
32723
|
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 }));
|
|
32107
32724
|
const section = wp.command("section").description("add, change, move or remove a page's sections");
|
|
32108
32725
|
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");
|
|
@@ -32241,6 +32858,67 @@ ${table(r.issues, ["severity", "code", "path", "detail"])}`;
|
|
|
32241
32858
|
theme.command("status", { isDefault: true }).description("installed and latest version, rollback versions").action((_o, c) => run(g(c), "wp_theme_updates", {}));
|
|
32242
32859
|
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
32860
|
theme.command("rollback <version>").description("put a kept earlier version back").action((version2, _o, c) => run(g(c), "wp_theme_update", { rollback_to: version2 }));
|
|
32861
|
+
wp.command("markup <page>").description(
|
|
32862
|
+
"a page's block markup (Gutenberg); --set replaces it from a file, --section reads or sets one section"
|
|
32863
|
+
).option("--section <id>", "one section (id or type)").option("--set <file>", "replace with this block markup (- for stdin)").option("--preview", "show the sections before and after, save nothing").action(async (p, o, c) => {
|
|
32864
|
+
if (!o.set)
|
|
32865
|
+
return run(
|
|
32866
|
+
g(c),
|
|
32867
|
+
"wp_page_markup_get",
|
|
32868
|
+
{ page: p, section: o.section },
|
|
32869
|
+
(v) => String(v.markup)
|
|
32870
|
+
);
|
|
32871
|
+
const markup = o.set === "-" ? await readStdin() : await readFile2(o.set, "utf8");
|
|
32872
|
+
if (o.section)
|
|
32873
|
+
return run(g(c), "wp_section_update", { page: p, section: o.section, markup, preview: !!o.preview });
|
|
32874
|
+
return run(g(c), "wp_page_markup_set", { page: p, markup, preview: !!o.preview });
|
|
32875
|
+
});
|
|
32876
|
+
const posts2 = wp.command("posts").description("blog posts on the site");
|
|
32877
|
+
posts2.command("ls", { isDefault: true }).option("--search <words>").option("--status <status>", "publish, draft, future, private or any").option("--category <name>").option("-n, --limit <n>", "how many", "30").action(
|
|
32878
|
+
(o, c) => run(g(c), "wp_posts_list", { search: o.search, status: o.status, category: o.category, limit: Number(o.limit) })
|
|
32879
|
+
);
|
|
32880
|
+
posts2.command("get <id>").action((id, _o, c) => run(g(c), "wp_post_get", { post: Number(id) }));
|
|
32881
|
+
posts2.command("save [id]").description(
|
|
32882
|
+
"create (no id) or change a post: JSON file with title, content, excerpt, slug, status, categories, tags\u2026"
|
|
32883
|
+
).requiredOption("--from <file>", "JSON (- for stdin)").action(
|
|
32884
|
+
async (id, o, c) => run(g(c), "wp_post_save", { ...await json(o.from), ...id ? { post: Number(id) } : {} })
|
|
32885
|
+
);
|
|
32886
|
+
posts2.command("delete <id>").description("move to the trash and redirect its address").option("--redirect-to <path>").action(
|
|
32887
|
+
(id, o, c) => run(g(c), "wp_post_delete", { post: Number(id), redirect_to: o.redirectTo })
|
|
32888
|
+
);
|
|
32889
|
+
const terms = wp.command("terms").description("categories and tags");
|
|
32890
|
+
terms.command("ls", { isDefault: true }).option("--tags", "tags instead of categories").option("--search <words>").action(
|
|
32891
|
+
(o, c) => run(g(c), "wp_terms_list", { taxonomy: o.tags ? "tags" : "categories", search: o.search })
|
|
32892
|
+
);
|
|
32893
|
+
terms.command("save [id]").option("--tags", "a tag instead of a category").option("--name <name>").option("--slug <slug>").option("--description <text>").action(
|
|
32894
|
+
(id, o, c) => run(g(c), "wp_term_save", {
|
|
32895
|
+
taxonomy: o.tags ? "tags" : "categories",
|
|
32896
|
+
id: id ? Number(id) : void 0,
|
|
32897
|
+
name: o.name,
|
|
32898
|
+
slug: o.slug,
|
|
32899
|
+
description: o.description
|
|
32900
|
+
})
|
|
32901
|
+
);
|
|
32902
|
+
const settings2 = wp.command("settings").description("the site's general settings");
|
|
32903
|
+
settings2.command("get", { isDefault: true }).action((_o, c) => run(g(c), "wp_settings_get", {}));
|
|
32904
|
+
settings2.command("set [fields...]").description(
|
|
32905
|
+
"title=\u2026 description=\u2026 timezone=Europe/London date_format=\u2026 posts_per_page=10 search_engines=true|false"
|
|
32906
|
+
).action(async (fields, _o, c) => {
|
|
32907
|
+
const args = {};
|
|
32908
|
+
for (const [k, v] of pairs(fields))
|
|
32909
|
+
args[k] = /^(true|false)$/.test(v) ? v === "true" : /^\d+$/.test(v) && k !== "title" && k !== "description" ? Number(v) : v;
|
|
32910
|
+
await run(g(c), "wp_settings_set", args);
|
|
32911
|
+
});
|
|
32912
|
+
const plugins = wp.command("plugins").description("plugins: list, install from WordPress.org, activate, deactivate (never delete)");
|
|
32913
|
+
plugins.command("ls", { isDefault: true }).action((_o, c) => run(g(c), "wp_plugins_list", {}));
|
|
32914
|
+
plugins.command("install <slug>").option("--activate").action(
|
|
32915
|
+
(slug, o, c) => run(g(c), "wp_plugin_set", { install: slug, status: o.activate ? "active" : "inactive" })
|
|
32916
|
+
);
|
|
32917
|
+
plugins.command("activate <plugin>").action((p, _o, c) => run(g(c), "wp_plugin_set", { plugin: p, status: "active" }));
|
|
32918
|
+
plugins.command("deactivate <plugin>").action((p, _o, c) => run(g(c), "wp_plugin_set", { plugin: p, status: "inactive" }));
|
|
32919
|
+
wp.command("api <route>").description("read any allowed WordPress REST route (GET), e.g. /wp/v2/types").option("--query <json>", "query parameters as JSON").action(
|
|
32920
|
+
(route, o, c) => run(g(c), "wp_api_read", { route, query: o.query ? JSON.parse(o.query) : void 0 })
|
|
32921
|
+
);
|
|
32244
32922
|
const child = wp.command("child").description("the site's Maggie child theme, on the site itself (data files only, backups kept)");
|
|
32245
32923
|
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 }));
|
|
32246
32924
|
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(
|
|
@@ -32271,7 +32949,7 @@ ${table(r.issues, ["severity", "code", "path", "detail"])}`;
|
|
|
32271
32949
|
const opts = g(c);
|
|
32272
32950
|
const colors = Object.fromEntries(pairs(o.color));
|
|
32273
32951
|
const fonts = Object.fromEntries(pairs(o.font));
|
|
32274
|
-
let
|
|
32952
|
+
let settings3;
|
|
32275
32953
|
let styles;
|
|
32276
32954
|
const fontFiles = {};
|
|
32277
32955
|
let header;
|
|
@@ -32279,9 +32957,9 @@ ${table(r.issues, ["severity", "code", "path", "detail"])}`;
|
|
|
32279
32957
|
if (o.fromSite) {
|
|
32280
32958
|
const site = await siteFor(opts);
|
|
32281
32959
|
const s = await styleGet(site);
|
|
32282
|
-
|
|
32960
|
+
settings3 = structuredClone(s._user.settings ?? {});
|
|
32283
32961
|
styles = s._user.styles ?? {};
|
|
32284
|
-
const families =
|
|
32962
|
+
const families = settings3.typography?.fontFamilies?.theme ?? [];
|
|
32285
32963
|
for (const f of families)
|
|
32286
32964
|
for (const face of f.fontFace ?? []) {
|
|
32287
32965
|
const srcs = Array.isArray(face.src) ? face.src : [face.src];
|
|
@@ -32300,7 +32978,7 @@ ${table(r.issues, ["severity", "code", "path", "detail"])}`;
|
|
|
32300
32978
|
footer = (await partGet(site, "footer").catch(() => null))?.content;
|
|
32301
32979
|
}
|
|
32302
32980
|
const files = {
|
|
32303
|
-
...childThemeFiles({ slug, name: o.name, colors, fonts, header, footer, settings:
|
|
32981
|
+
...childThemeFiles({ slug, name: o.name, colors, fonts, header, footer, settings: settings3, styles }),
|
|
32304
32982
|
...fontFiles
|
|
32305
32983
|
};
|
|
32306
32984
|
const dir = o.out ?? slug;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.7",
|
|
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",
|