claude-plugin-wordpress-manager 1.8.0 → 1.9.0
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/.claude-plugin/plugin.json +5 -3
- package/CHANGELOG.md +27 -0
- package/agents/wp-site-manager.md +26 -0
- package/docs/plans/2026-02-28-multisite-v1.9.0-design.md +258 -0
- package/docs/plans/2026-02-28-multisite-v1.9.0.md +1604 -0
- package/package.json +5 -3
- package/servers/wp-rest-bridge/build/tools/index.d.ts +260 -0
- package/servers/wp-rest-bridge/build/tools/index.js +6 -0
- package/servers/wp-rest-bridge/build/tools/multisite-network.d.ts +132 -0
- package/servers/wp-rest-bridge/build/tools/multisite-network.js +157 -0
- package/servers/wp-rest-bridge/build/tools/multisite-sites.d.ts +150 -0
- package/servers/wp-rest-bridge/build/tools/multisite-sites.js +160 -0
- package/servers/wp-rest-bridge/build/types.d.ts +13 -0
- package/servers/wp-rest-bridge/build/wordpress.d.ts +19 -0
- package/servers/wp-rest-bridge/build/wordpress.js +10 -0
- package/servers/wp-rest-bridge/build/wpcli.d.ts +23 -0
- package/servers/wp-rest-bridge/build/wpcli.js +72 -0
- package/skills/wordpress-router/references/decision-tree.md +4 -2
- package/skills/wp-multisite/SKILL.md +92 -0
- package/skills/wp-multisite/references/domain-mapping.md +70 -0
- package/skills/wp-multisite/references/migration-multisite.md +76 -0
- package/skills/wp-multisite/references/network-plugins.md +66 -0
- package/skills/wp-multisite/references/network-setup.md +69 -0
- package/skills/wp-multisite/references/site-management.md +67 -0
- package/skills/wp-multisite/references/user-roles.md +73 -0
- package/skills/wp-multisite/scripts/multisite_inspect.mjs +160 -0
- package/skills/wp-security/SKILL.md +4 -0
- package/skills/wp-wpcli-and-ops/SKILL.md +4 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
declare const msListSitesSchema: z.ZodObject<{
|
|
4
|
+
site_id: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, "strict", z.ZodTypeAny, {
|
|
6
|
+
site_id?: string | undefined;
|
|
7
|
+
}, {
|
|
8
|
+
site_id?: string | undefined;
|
|
9
|
+
}>;
|
|
10
|
+
declare const msGetSiteSchema: z.ZodObject<{
|
|
11
|
+
blog_id: z.ZodNumber;
|
|
12
|
+
site_id: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, "strict", z.ZodTypeAny, {
|
|
14
|
+
blog_id: number;
|
|
15
|
+
site_id?: string | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
blog_id: number;
|
|
18
|
+
site_id?: string | undefined;
|
|
19
|
+
}>;
|
|
20
|
+
declare const msCreateSiteSchema: z.ZodObject<{
|
|
21
|
+
slug: z.ZodString;
|
|
22
|
+
title: z.ZodString;
|
|
23
|
+
email: z.ZodString;
|
|
24
|
+
site_id: z.ZodOptional<z.ZodString>;
|
|
25
|
+
}, "strict", z.ZodTypeAny, {
|
|
26
|
+
slug: string;
|
|
27
|
+
title: string;
|
|
28
|
+
email: string;
|
|
29
|
+
site_id?: string | undefined;
|
|
30
|
+
}, {
|
|
31
|
+
slug: string;
|
|
32
|
+
title: string;
|
|
33
|
+
email: string;
|
|
34
|
+
site_id?: string | undefined;
|
|
35
|
+
}>;
|
|
36
|
+
declare const msActivateSiteSchema: z.ZodObject<{
|
|
37
|
+
blog_id: z.ZodNumber;
|
|
38
|
+
active: z.ZodBoolean;
|
|
39
|
+
site_id: z.ZodOptional<z.ZodString>;
|
|
40
|
+
}, "strict", z.ZodTypeAny, {
|
|
41
|
+
active: boolean;
|
|
42
|
+
blog_id: number;
|
|
43
|
+
site_id?: string | undefined;
|
|
44
|
+
}, {
|
|
45
|
+
active: boolean;
|
|
46
|
+
blog_id: number;
|
|
47
|
+
site_id?: string | undefined;
|
|
48
|
+
}>;
|
|
49
|
+
declare const msDeleteSiteSchema: z.ZodObject<{
|
|
50
|
+
blog_id: z.ZodNumber;
|
|
51
|
+
confirm: z.ZodLiteral<true>;
|
|
52
|
+
site_id: z.ZodOptional<z.ZodString>;
|
|
53
|
+
}, "strict", z.ZodTypeAny, {
|
|
54
|
+
blog_id: number;
|
|
55
|
+
confirm: true;
|
|
56
|
+
site_id?: string | undefined;
|
|
57
|
+
}, {
|
|
58
|
+
blog_id: number;
|
|
59
|
+
confirm: true;
|
|
60
|
+
site_id?: string | undefined;
|
|
61
|
+
}>;
|
|
62
|
+
export declare const multisiteSiteTools: Tool[];
|
|
63
|
+
export declare const multisiteSiteHandlers: {
|
|
64
|
+
ms_list_sites: (params: z.infer<typeof msListSitesSchema>) => Promise<{
|
|
65
|
+
toolResult: {
|
|
66
|
+
content: {
|
|
67
|
+
type: string;
|
|
68
|
+
text: string;
|
|
69
|
+
}[];
|
|
70
|
+
isError?: undefined;
|
|
71
|
+
};
|
|
72
|
+
} | {
|
|
73
|
+
toolResult: {
|
|
74
|
+
isError: boolean;
|
|
75
|
+
content: {
|
|
76
|
+
type: string;
|
|
77
|
+
text: string;
|
|
78
|
+
}[];
|
|
79
|
+
};
|
|
80
|
+
}>;
|
|
81
|
+
ms_get_site: (params: z.infer<typeof msGetSiteSchema>) => Promise<{
|
|
82
|
+
toolResult: {
|
|
83
|
+
content: {
|
|
84
|
+
type: string;
|
|
85
|
+
text: string;
|
|
86
|
+
}[];
|
|
87
|
+
isError?: undefined;
|
|
88
|
+
};
|
|
89
|
+
} | {
|
|
90
|
+
toolResult: {
|
|
91
|
+
isError: boolean;
|
|
92
|
+
content: {
|
|
93
|
+
type: string;
|
|
94
|
+
text: string;
|
|
95
|
+
}[];
|
|
96
|
+
};
|
|
97
|
+
}>;
|
|
98
|
+
ms_create_site: (params: z.infer<typeof msCreateSiteSchema>) => Promise<{
|
|
99
|
+
toolResult: {
|
|
100
|
+
content: {
|
|
101
|
+
type: string;
|
|
102
|
+
text: string;
|
|
103
|
+
}[];
|
|
104
|
+
isError?: undefined;
|
|
105
|
+
};
|
|
106
|
+
} | {
|
|
107
|
+
toolResult: {
|
|
108
|
+
isError: boolean;
|
|
109
|
+
content: {
|
|
110
|
+
type: string;
|
|
111
|
+
text: string;
|
|
112
|
+
}[];
|
|
113
|
+
};
|
|
114
|
+
}>;
|
|
115
|
+
ms_activate_site: (params: z.infer<typeof msActivateSiteSchema>) => Promise<{
|
|
116
|
+
toolResult: {
|
|
117
|
+
content: {
|
|
118
|
+
type: string;
|
|
119
|
+
text: string;
|
|
120
|
+
}[];
|
|
121
|
+
isError?: undefined;
|
|
122
|
+
};
|
|
123
|
+
} | {
|
|
124
|
+
toolResult: {
|
|
125
|
+
isError: boolean;
|
|
126
|
+
content: {
|
|
127
|
+
type: string;
|
|
128
|
+
text: string;
|
|
129
|
+
}[];
|
|
130
|
+
};
|
|
131
|
+
}>;
|
|
132
|
+
ms_delete_site: (params: z.infer<typeof msDeleteSiteSchema>) => Promise<{
|
|
133
|
+
toolResult: {
|
|
134
|
+
content: {
|
|
135
|
+
type: string;
|
|
136
|
+
text: string;
|
|
137
|
+
}[];
|
|
138
|
+
isError?: undefined;
|
|
139
|
+
};
|
|
140
|
+
} | {
|
|
141
|
+
toolResult: {
|
|
142
|
+
isError: boolean;
|
|
143
|
+
content: {
|
|
144
|
+
type: string;
|
|
145
|
+
text: string;
|
|
146
|
+
}[];
|
|
147
|
+
};
|
|
148
|
+
}>;
|
|
149
|
+
};
|
|
150
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { executeWpCli, isMultisite } from '../wpcli.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
// ── Schemas ──────────────────────────────────────────────────────────
|
|
4
|
+
const msListSitesSchema = z.object({
|
|
5
|
+
site_id: z.string().optional().describe('Target site ID (defaults to active site)')
|
|
6
|
+
}).strict();
|
|
7
|
+
const msGetSiteSchema = z.object({
|
|
8
|
+
blog_id: z.number().describe('Blog ID of the sub-site to retrieve'),
|
|
9
|
+
site_id: z.string().optional().describe('Target site ID (defaults to active site)')
|
|
10
|
+
}).strict();
|
|
11
|
+
const msCreateSiteSchema = z.object({
|
|
12
|
+
slug: z.string().describe('URL slug for the new sub-site (e.g., "blog", "shop")'),
|
|
13
|
+
title: z.string().describe('Title of the new sub-site'),
|
|
14
|
+
email: z.string().describe('Admin email for the new sub-site'),
|
|
15
|
+
site_id: z.string().optional().describe('Target site ID (defaults to active site)')
|
|
16
|
+
}).strict();
|
|
17
|
+
const msActivateSiteSchema = z.object({
|
|
18
|
+
blog_id: z.number().describe('Blog ID of the sub-site'),
|
|
19
|
+
active: z.boolean().describe('true to activate, false to deactivate'),
|
|
20
|
+
site_id: z.string().optional().describe('Target site ID (defaults to active site)')
|
|
21
|
+
}).strict();
|
|
22
|
+
const msDeleteSiteSchema = z.object({
|
|
23
|
+
blog_id: z.number().describe('Blog ID of the sub-site to delete'),
|
|
24
|
+
confirm: z.literal(true).describe('Must be true to confirm deletion (safety gate)'),
|
|
25
|
+
site_id: z.string().optional().describe('Target site ID (defaults to active site)')
|
|
26
|
+
}).strict();
|
|
27
|
+
// ── Tools ────────────────────────────────────────────────────────────
|
|
28
|
+
export const multisiteSiteTools = [
|
|
29
|
+
{
|
|
30
|
+
name: 'ms_list_sites',
|
|
31
|
+
description: 'Lists all sub-sites in a WordPress Multisite network. Requires wp-cli and is_multisite configuration.',
|
|
32
|
+
inputSchema: { type: 'object', properties: msListSitesSchema.shape }
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'ms_get_site',
|
|
36
|
+
description: 'Gets details of a specific sub-site by blog ID.',
|
|
37
|
+
inputSchema: { type: 'object', properties: msGetSiteSchema.shape }
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'ms_create_site',
|
|
41
|
+
description: 'Creates a new sub-site in the multisite network.',
|
|
42
|
+
inputSchema: { type: 'object', properties: msCreateSiteSchema.shape }
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'ms_activate_site',
|
|
46
|
+
description: 'Activates or deactivates a sub-site in the multisite network.',
|
|
47
|
+
inputSchema: { type: 'object', properties: msActivateSiteSchema.shape }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'ms_delete_site',
|
|
51
|
+
description: 'Permanently deletes a sub-site. Requires confirm: true as safety gate.',
|
|
52
|
+
inputSchema: { type: 'object', properties: msDeleteSiteSchema.shape }
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
56
|
+
function requireMultisite(siteId) {
|
|
57
|
+
if (!isMultisite(siteId)) {
|
|
58
|
+
throw new Error(`Site is not configured as multisite. ` +
|
|
59
|
+
`Set is_multisite: true in WP_SITES_CONFIG.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ── Handlers ─────────────────────────────────────────────────────────
|
|
63
|
+
export const multisiteSiteHandlers = {
|
|
64
|
+
ms_list_sites: async (params) => {
|
|
65
|
+
try {
|
|
66
|
+
requireMultisite(params.site_id);
|
|
67
|
+
const result = await executeWpCli('site list', params.site_id);
|
|
68
|
+
return {
|
|
69
|
+
toolResult: {
|
|
70
|
+
content: [{ type: 'text', text: result }]
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
toolResult: {
|
|
77
|
+
isError: true,
|
|
78
|
+
content: [{ type: 'text', text: `Error listing sites: ${error.message}` }]
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
ms_get_site: async (params) => {
|
|
84
|
+
try {
|
|
85
|
+
requireMultisite(params.site_id);
|
|
86
|
+
const result = await executeWpCli(`site list --blog_id=${params.blog_id}`, params.site_id);
|
|
87
|
+
return {
|
|
88
|
+
toolResult: {
|
|
89
|
+
content: [{ type: 'text', text: result }]
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return {
|
|
95
|
+
toolResult: {
|
|
96
|
+
isError: true,
|
|
97
|
+
content: [{ type: 'text', text: `Error getting site: ${error.message}` }]
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
ms_create_site: async (params) => {
|
|
103
|
+
try {
|
|
104
|
+
requireMultisite(params.site_id);
|
|
105
|
+
const result = await executeWpCli(`site create --slug=${params.slug} --title="${params.title}" --email=${params.email}`, params.site_id, { skipJson: true });
|
|
106
|
+
return {
|
|
107
|
+
toolResult: {
|
|
108
|
+
content: [{ type: 'text', text: result }]
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
toolResult: {
|
|
115
|
+
isError: true,
|
|
116
|
+
content: [{ type: 'text', text: `Error creating site: ${error.message}` }]
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
ms_activate_site: async (params) => {
|
|
122
|
+
try {
|
|
123
|
+
requireMultisite(params.site_id);
|
|
124
|
+
const action = params.active ? 'activate' : 'deactivate';
|
|
125
|
+
const result = await executeWpCli(`site ${action} ${params.blog_id}`, params.site_id, { skipJson: true });
|
|
126
|
+
return {
|
|
127
|
+
toolResult: {
|
|
128
|
+
content: [{ type: 'text', text: result || `Site ${params.blog_id} ${action}d successfully.` }]
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return {
|
|
134
|
+
toolResult: {
|
|
135
|
+
isError: true,
|
|
136
|
+
content: [{ type: 'text', text: `Error ${params.active ? 'activating' : 'deactivating'} site: ${error.message}` }]
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
ms_delete_site: async (params) => {
|
|
142
|
+
try {
|
|
143
|
+
requireMultisite(params.site_id);
|
|
144
|
+
const result = await executeWpCli(`site delete ${params.blog_id} --yes`, params.site_id, { skipJson: true });
|
|
145
|
+
return {
|
|
146
|
+
toolResult: {
|
|
147
|
+
content: [{ type: 'text', text: result || `Site ${params.blog_id} deleted successfully.` }]
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
toolResult: {
|
|
154
|
+
isError: true,
|
|
155
|
+
content: [{ type: 'text', text: `Error deleting site: ${error.message}` }]
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
@@ -233,4 +233,17 @@ export interface WCCoupon {
|
|
|
233
233
|
minimum_amount: string;
|
|
234
234
|
maximum_amount: string;
|
|
235
235
|
}
|
|
236
|
+
export interface WPNetworkSite {
|
|
237
|
+
blog_id: number;
|
|
238
|
+
url: string;
|
|
239
|
+
domain: string;
|
|
240
|
+
path: string;
|
|
241
|
+
registered: string;
|
|
242
|
+
last_updated: string;
|
|
243
|
+
public: boolean;
|
|
244
|
+
archived: boolean;
|
|
245
|
+
mature: boolean;
|
|
246
|
+
spam: boolean;
|
|
247
|
+
deleted: boolean;
|
|
248
|
+
}
|
|
236
249
|
export {};
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
interface SiteConfig {
|
|
2
|
+
id: string;
|
|
3
|
+
url: string;
|
|
4
|
+
username: string;
|
|
5
|
+
password: string;
|
|
6
|
+
wc_consumer_key?: string;
|
|
7
|
+
wc_consumer_secret?: string;
|
|
8
|
+
wp_path?: string;
|
|
9
|
+
ssh_host?: string;
|
|
10
|
+
ssh_user?: string;
|
|
11
|
+
ssh_key?: string;
|
|
12
|
+
ssh_port?: number;
|
|
13
|
+
is_multisite?: boolean;
|
|
14
|
+
}
|
|
1
15
|
/**
|
|
2
16
|
* Parse WP_SITES_CONFIG JSON and initialize all site clients
|
|
3
17
|
*/
|
|
@@ -14,6 +28,10 @@ export declare function listSites(): string[];
|
|
|
14
28
|
* Get the active site ID
|
|
15
29
|
*/
|
|
16
30
|
export declare function getActiveSite(): string;
|
|
31
|
+
/**
|
|
32
|
+
* Get the SiteConfig for a given site (needed by wpcli module).
|
|
33
|
+
*/
|
|
34
|
+
export declare function getSiteConfig(siteId?: string): SiteConfig | undefined;
|
|
17
35
|
/**
|
|
18
36
|
* Log to stderr (safe for MCP stdio transport)
|
|
19
37
|
*/
|
|
@@ -55,3 +73,4 @@ export declare function makeWooCommerceRequest(method: string, endpoint: string,
|
|
|
55
73
|
* Search the WordPress.org Plugin Repository
|
|
56
74
|
*/
|
|
57
75
|
export declare function searchWordPressPluginRepository(searchQuery: string, page?: number, perPage?: number): Promise<any>;
|
|
76
|
+
export {};
|
|
@@ -33,6 +33,7 @@ const siteClients = new Map();
|
|
|
33
33
|
const siteLimiters = new Map();
|
|
34
34
|
const wcSiteClients = new Map();
|
|
35
35
|
let activeSiteId = '';
|
|
36
|
+
const parsedSiteConfigs = new Map();
|
|
36
37
|
const MAX_CONCURRENT_PER_SITE = 5;
|
|
37
38
|
const DEFAULT_TIMEOUT_MS = parseInt(process.env.WP_REQUEST_TIMEOUT_MS || '30000', 10);
|
|
38
39
|
const MAX_RETRIES = 3;
|
|
@@ -58,6 +59,7 @@ export async function initWordPress() {
|
|
|
58
59
|
const siteId = 'default';
|
|
59
60
|
await initSiteClient(siteId, url, username || '', password || '');
|
|
60
61
|
activeSiteId = siteId;
|
|
62
|
+
parsedSiteConfigs.set(siteId, { id: siteId, url, username: username || '', password: password || '' });
|
|
61
63
|
logToStderr(`Initialized single site: ${url}`);
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
@@ -73,6 +75,7 @@ export async function initWordPress() {
|
|
|
73
75
|
}
|
|
74
76
|
for (const site of sites) {
|
|
75
77
|
await initSiteClient(site.id, site.url, site.username, site.password);
|
|
78
|
+
parsedSiteConfigs.set(site.id, site);
|
|
76
79
|
logToStderr(`Initialized site: ${site.id} (${site.url})`);
|
|
77
80
|
}
|
|
78
81
|
// Initialize WooCommerce clients for sites with WC credentials
|
|
@@ -194,6 +197,13 @@ export function listSites() {
|
|
|
194
197
|
export function getActiveSite() {
|
|
195
198
|
return activeSiteId;
|
|
196
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Get the SiteConfig for a given site (needed by wpcli module).
|
|
202
|
+
*/
|
|
203
|
+
export function getSiteConfig(siteId) {
|
|
204
|
+
const id = siteId || activeSiteId;
|
|
205
|
+
return parsedSiteConfigs.get(id);
|
|
206
|
+
}
|
|
197
207
|
// ── Logging ──────────────────────────────────────────────────────────
|
|
198
208
|
/**
|
|
199
209
|
* Log to stderr (safe for MCP stdio transport)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a site has WP-CLI access configured (wp_path and optionally ssh_host).
|
|
3
|
+
*/
|
|
4
|
+
export declare function hasWpCli(siteId?: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Check if a site is configured as multisite.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isMultisite(siteId?: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Execute a WP-CLI command for a given site.
|
|
11
|
+
*
|
|
12
|
+
* - If ssh_host is set: runs via SSH
|
|
13
|
+
* - If only wp_path is set: runs locally
|
|
14
|
+
* - Appends --format=json by default for structured output
|
|
15
|
+
*
|
|
16
|
+
* @param command WP-CLI command without the leading "wp " (e.g., "site list", "plugin activate hello --network")
|
|
17
|
+
* @param siteId Site ID (defaults to active site)
|
|
18
|
+
* @param options.skipJson Don't append --format=json (for commands that don't support it)
|
|
19
|
+
* @returns stdout as string
|
|
20
|
+
*/
|
|
21
|
+
export declare function executeWpCli(command: string, siteId?: string, options?: {
|
|
22
|
+
skipJson?: boolean;
|
|
23
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/wpcli.ts - WP-CLI execution module (local + SSH)
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
3
|
+
import { getSiteConfig, getActiveSite, logToStderr } from './wordpress.js';
|
|
4
|
+
const WPCLI_TIMEOUT_MS = 30000;
|
|
5
|
+
/**
|
|
6
|
+
* Check if a site has WP-CLI access configured (wp_path and optionally ssh_host).
|
|
7
|
+
*/
|
|
8
|
+
export function hasWpCli(siteId) {
|
|
9
|
+
const config = getSiteConfig(siteId || getActiveSite());
|
|
10
|
+
if (!config)
|
|
11
|
+
return false;
|
|
12
|
+
return !!config.wp_path;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check if a site is configured as multisite.
|
|
16
|
+
*/
|
|
17
|
+
export function isMultisite(siteId) {
|
|
18
|
+
const config = getSiteConfig(siteId || getActiveSite());
|
|
19
|
+
if (!config)
|
|
20
|
+
return false;
|
|
21
|
+
return !!config.is_multisite;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Execute a WP-CLI command for a given site.
|
|
25
|
+
*
|
|
26
|
+
* - If ssh_host is set: runs via SSH
|
|
27
|
+
* - If only wp_path is set: runs locally
|
|
28
|
+
* - Appends --format=json by default for structured output
|
|
29
|
+
*
|
|
30
|
+
* @param command WP-CLI command without the leading "wp " (e.g., "site list", "plugin activate hello --network")
|
|
31
|
+
* @param siteId Site ID (defaults to active site)
|
|
32
|
+
* @param options.skipJson Don't append --format=json (for commands that don't support it)
|
|
33
|
+
* @returns stdout as string
|
|
34
|
+
*/
|
|
35
|
+
export async function executeWpCli(command, siteId, options) {
|
|
36
|
+
const id = siteId || getActiveSite();
|
|
37
|
+
const config = getSiteConfig(id);
|
|
38
|
+
if (!config) {
|
|
39
|
+
throw new Error(`Site "${id}" not found in configuration.`);
|
|
40
|
+
}
|
|
41
|
+
if (!config.wp_path) {
|
|
42
|
+
throw new Error(`WP-CLI not configured for site "${id}". ` +
|
|
43
|
+
`Add wp_path to WP_SITES_CONFIG for this site.`);
|
|
44
|
+
}
|
|
45
|
+
const formatFlag = options?.skipJson ? '' : ' --format=json';
|
|
46
|
+
const wpCommand = `wp ${command}${formatFlag}`;
|
|
47
|
+
let shellCommand;
|
|
48
|
+
if (config.ssh_host) {
|
|
49
|
+
// Remote execution via SSH
|
|
50
|
+
const sshUser = config.ssh_user || 'root';
|
|
51
|
+
const sshPort = config.ssh_port || 22;
|
|
52
|
+
const sshKeyFlag = config.ssh_key ? `-i ${config.ssh_key} ` : '';
|
|
53
|
+
const escapedCommand = `cd ${config.wp_path} && ${wpCommand}`;
|
|
54
|
+
shellCommand = `ssh ${sshKeyFlag}-p ${sshPort} -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 ${sshUser}@${config.ssh_host} '${escapedCommand}'`;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Local execution
|
|
58
|
+
shellCommand = `cd ${config.wp_path} && ${wpCommand}`;
|
|
59
|
+
}
|
|
60
|
+
logToStderr(`[${id}] WP-CLI: ${wpCommand}`);
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
exec(shellCommand, { timeout: WPCLI_TIMEOUT_MS }, (error, stdout, stderr) => {
|
|
63
|
+
if (error) {
|
|
64
|
+
const msg = stderr?.trim() || error.message;
|
|
65
|
+
logToStderr(`[${id}] WP-CLI error: ${msg}`);
|
|
66
|
+
reject(new Error(`WP-CLI error on site "${id}": ${msg}`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
resolve(stdout.trim());
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Router decision tree (
|
|
1
|
+
# Router decision tree (v6 — development + local environment + operations + multisite)
|
|
2
2
|
|
|
3
3
|
This routing guide covers WordPress **development**, **local environment**, and **operations** workflows.
|
|
4
4
|
|
|
@@ -14,7 +14,7 @@ Keywords that indicate **local environment**:
|
|
|
14
14
|
local site, Studio, LocalWP, Local by Flywheel, wp-env, local WordPress, start site, stop site, create local site, local development, symlink plugin, local database, switch PHP version, localhost, local preview, detect environment, WASM, SQLite local
|
|
15
15
|
|
|
16
16
|
Keywords that indicate **operations**:
|
|
17
|
-
deploy, push to production, audit, security check, backup, restore, migrate, move site, create post, manage content, site status, check plugins, performance check, SEO audit, WooCommerce, prodotto, ordine, coupon, negozio, catalogo, inventario, vendite, carrello
|
|
17
|
+
deploy, push to production, audit, security check, backup, restore, migrate, move site, create post, manage content, site status, check plugins, performance check, SEO audit, WooCommerce, prodotto, ordine, coupon, negozio, catalogo, inventario, vendite, carrello, multisite, network, sub-site, sub-sito, domain mapping, super admin, network activate
|
|
18
18
|
|
|
19
19
|
Keywords that indicate **development**:
|
|
20
20
|
create block, block.json, theme.json, register_rest_route, plugin development, hooks, PHPStan, build, test, scaffold, i18n, translation, accessibility, a11y, headless, decoupled, WPGraphQL
|
|
@@ -88,6 +88,8 @@ Priority: `gutenberg` > `wp-core` > `wp-site` > `wp-block-theme` > `wp-block-plu
|
|
|
88
88
|
→ `wp-site-manager` agent (Hostinger MCP tools)
|
|
89
89
|
- **WooCommerce / woo / shop / products / orders / coupons / cart / store management / sales report / inventory**
|
|
90
90
|
→ `wp-woocommerce` skill + `wp-ecommerce-manager` agent
|
|
91
|
+
- **Multisite / network / sub-sites / domain mapping / super admin / network activate**
|
|
92
|
+
→ `wp-multisite` skill + `wp-site-manager` agent
|
|
91
93
|
|
|
92
94
|
## Step 2c: route by local environment intent (keywords)
|
|
93
95
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wp-multisite
|
|
3
|
+
description: |
|
|
4
|
+
This skill should be used when the user asks about "multisite", "network admin",
|
|
5
|
+
"sub-sites", "domain mapping", "super admin", "network activate",
|
|
6
|
+
"WordPress Multisite network", or any multisite network management operations.
|
|
7
|
+
version: 1.0.0
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
WordPress Multisite network management via WP-CLI (10 MCP tools). Covers sub-site CRUD, network plugin management, Super Admin listing, network settings, and domain mapping guidance. Uses a hybrid approach: REST API where available, WP-CLI for network-only operations.
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
- User mentions multisite, network, sub-sites, or domain mapping
|
|
17
|
+
- User needs to create, activate, deactivate, or delete sub-sites
|
|
18
|
+
- User wants to network-activate or network-deactivate plugins
|
|
19
|
+
- User needs Super Admin listing or network settings
|
|
20
|
+
- User asks about migrating single-site to multisite or vice versa
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
WP-CLI access and multisite flag must be configured in `WP_SITES_CONFIG`:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"id": "mynetwork",
|
|
29
|
+
"url": "https://network.example.com",
|
|
30
|
+
"username": "superadmin",
|
|
31
|
+
"password": "xxxx xxxx xxxx xxxx",
|
|
32
|
+
"wp_path": "/var/www/wordpress",
|
|
33
|
+
"ssh_host": "network.example.com",
|
|
34
|
+
"ssh_user": "deploy",
|
|
35
|
+
"ssh_key": "~/.ssh/id_rsa",
|
|
36
|
+
"is_multisite": true
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `wp_path` — required for all wp-cli operations
|
|
41
|
+
- `ssh_host` / `ssh_user` — required for remote sites (omit for local)
|
|
42
|
+
- `is_multisite: true` — required flag to enable ms_* tools
|
|
43
|
+
|
|
44
|
+
## Detection
|
|
45
|
+
|
|
46
|
+
Run the detection script to check multisite presence:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node skills/wp-multisite/scripts/multisite_inspect.mjs
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Multisite Operations Decision Tree
|
|
53
|
+
|
|
54
|
+
1. **Sub-site management?**
|
|
55
|
+
- List all sub-sites → `ms_list_sites`
|
|
56
|
+
- Get sub-site details → `ms_get_site`
|
|
57
|
+
- Create new sub-site → `ms_create_site`
|
|
58
|
+
- Activate/deactivate → `ms_activate_site`
|
|
59
|
+
- Delete sub-site → `ms_delete_site`
|
|
60
|
+
|
|
61
|
+
2. **Network plugin management?**
|
|
62
|
+
- List all plugins (with network status) → `ms_list_network_plugins`
|
|
63
|
+
- Network-activate plugin → `ms_network_activate_plugin`
|
|
64
|
+
- Network-deactivate plugin → `ms_network_deactivate_plugin`
|
|
65
|
+
|
|
66
|
+
3. **Network administration?**
|
|
67
|
+
- List Super Admins → `ms_list_super_admins`
|
|
68
|
+
- Get network settings → `ms_get_network_settings`
|
|
69
|
+
|
|
70
|
+
4. **Domain mapping / network setup / migration?**
|
|
71
|
+
- See reference files below (no dedicated MCP tool — use wp-cli via Bash)
|
|
72
|
+
|
|
73
|
+
## Recommended Agent
|
|
74
|
+
|
|
75
|
+
For complex multi-step multisite operations, use the `wp-site-manager` agent (which has a dedicated Multisite Network Management section).
|
|
76
|
+
|
|
77
|
+
## Additional Resources
|
|
78
|
+
|
|
79
|
+
### Reference Files
|
|
80
|
+
|
|
81
|
+
- **`references/network-setup.md`** — Sub-directory vs sub-domain, wp-config constants, installation
|
|
82
|
+
- **`references/site-management.md`** — CRUD sub-sites, templates, bulk operations
|
|
83
|
+
- **`references/domain-mapping.md`** — Custom domains, SSL, DNS CNAME, sunrise.php
|
|
84
|
+
- **`references/network-plugins.md`** — Network-activated vs per-site plugins, must-use plugins
|
|
85
|
+
- **`references/user-roles.md`** — Super Admin capabilities, site-level roles
|
|
86
|
+
- **`references/migration-multisite.md`** — Single to multisite and back, database tables
|
|
87
|
+
|
|
88
|
+
### Related Skills
|
|
89
|
+
|
|
90
|
+
- `wp-wpcli-and-ops` — WP-CLI command reference and multisite flags
|
|
91
|
+
- `wp-security` — Super Admin capabilities and multisite security
|
|
92
|
+
- `wp-deploy` — Deploy to multisite network
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Domain Mapping
|
|
2
|
+
|
|
3
|
+
Domain mapping allows each sub-site in a WordPress Multisite network to use its own custom domain instead of the default sub-directory or sub-domain URL.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
| Default URL | Mapped Domain |
|
|
8
|
+
|-------------|---------------|
|
|
9
|
+
| `network.com/shopA/` | `shopA.com` |
|
|
10
|
+
| `shopB.network.com` | `shopB.com` |
|
|
11
|
+
|
|
12
|
+
Since WordPress 4.5+, domain mapping is built into core (no plugin required for basic mapping).
|
|
13
|
+
|
|
14
|
+
## Setup Procedure
|
|
15
|
+
|
|
16
|
+
### 1. DNS Configuration
|
|
17
|
+
|
|
18
|
+
For each custom domain, create a DNS record pointing to the network server:
|
|
19
|
+
|
|
20
|
+
| Record Type | Name | Value |
|
|
21
|
+
|-------------|------|-------|
|
|
22
|
+
| A | `shopA.com` | `<server-ip>` |
|
|
23
|
+
| CNAME | `www.shopA.com` | `shopA.com` |
|
|
24
|
+
|
|
25
|
+
### 2. WordPress Configuration
|
|
26
|
+
|
|
27
|
+
In Network Admin > Sites > Edit Site > Domain:
|
|
28
|
+
- Change the site URL to the custom domain
|
|
29
|
+
|
|
30
|
+
Or via WP-CLI:
|
|
31
|
+
```bash
|
|
32
|
+
wp site list # find the blog_id
|
|
33
|
+
wp option update home 'https://shopA.com' --url=network.com/shopA/
|
|
34
|
+
wp option update siteurl 'https://shopA.com' --url=network.com/shopA/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. SSL Certificate
|
|
38
|
+
|
|
39
|
+
Each mapped domain needs its own SSL certificate:
|
|
40
|
+
- **Let's Encrypt**: Use Certbot with `--domains shopA.com,shopB.com`
|
|
41
|
+
- **Wildcard**: Only covers `*.network.com`, NOT custom domains
|
|
42
|
+
- **Multi-domain SAN cert**: Can cover all mapped domains in one cert
|
|
43
|
+
|
|
44
|
+
### 4. Web Server Configuration
|
|
45
|
+
|
|
46
|
+
The web server must accept requests for all mapped domains. In Nginx:
|
|
47
|
+
|
|
48
|
+
```nginx
|
|
49
|
+
server {
|
|
50
|
+
server_name shopA.com shopB.com network.com *.network.com;
|
|
51
|
+
# ... standard WordPress config
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## sunrise.php (Advanced)
|
|
56
|
+
|
|
57
|
+
For complex domain mapping logic, WordPress supports a `sunrise.php` drop-in:
|
|
58
|
+
|
|
59
|
+
- Location: `wp-content/sunrise.php`
|
|
60
|
+
- Loaded very early in the WordPress bootstrap (before plugins)
|
|
61
|
+
- Must be enabled: `define('SUNRISE', true);` in wp-config.php
|
|
62
|
+
- Used by plugins like "WordPress MU Domain Mapping" (legacy) or "Mercator"
|
|
63
|
+
|
|
64
|
+
## Tips and Gotchas
|
|
65
|
+
|
|
66
|
+
- **Cookie domain**: After mapping, update `COOKIE_DOMAIN` if login issues occur.
|
|
67
|
+
- **Mixed content**: Ensure all mapped domains use HTTPS to avoid mixed content warnings.
|
|
68
|
+
- **Caching**: Flush caches after domain mapping changes — both server-side and CDN.
|
|
69
|
+
- **Search Console**: Register each mapped domain separately in Google Search Console.
|
|
70
|
+
- **Reverse proxy**: If using Cloudflare or similar, configure the DNS to point to the origin server's IP, not the CDN.
|