devops-whc 1.0.1 → 1.0.2
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/{AGENT_MCP_USAGE.md → AGENT_USAGE.md} +129 -148
- package/README.md +214 -45
- package/{WHC_MCP_REQUIREMENTS.md → WHC_REQUIREMENTS.md} +8 -6
- package/dist/config/env.js +46 -21
- package/dist/handlers/whc-db-backup.js +0 -1
- package/dist/handlers/whc-deploy.js +80 -224
- package/dist/handlers/whc-pipeline-status.js +2 -2
- package/dist/handlers/whc-prepare.js +1 -1
- package/dist/handlers/whc-setup-remote.js +4 -3
- package/dist/index.js +258 -14
- package/dist/probes/source-compatibility.js +457 -0
- package/dist/schemas/whc-deploy.js +13 -9
- package/dist/server-entry.js +2 -2
- package/dist/server.js +13 -12
- package/dist/services/deploy-runtime-ops.js +8 -96
- package/dist/state/workspace-state.js +107 -7
- package/package.json +12 -7
- package/scripts/prepare-first-time.cjs +3 -3
- package/scripts/{start-mcp.cjs → start-whc.cjs} +3 -4
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runSourceCompatibilityCheck = runSourceCompatibilityCheck;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const ssh_client_1 = require("../clients/ssh-client");
|
|
10
|
+
const wpcli_client_1 = require("../clients/wpcli-client");
|
|
11
|
+
function isDir(targetPath) {
|
|
12
|
+
return node_fs_1.default.existsSync(targetPath) && node_fs_1.default.statSync(targetPath).isDirectory();
|
|
13
|
+
}
|
|
14
|
+
function isFile(targetPath) {
|
|
15
|
+
return node_fs_1.default.existsSync(targetPath) && node_fs_1.default.statSync(targetPath).isFile();
|
|
16
|
+
}
|
|
17
|
+
function countByStatus(checks, status) {
|
|
18
|
+
return checks.filter((check) => check.status === status).length;
|
|
19
|
+
}
|
|
20
|
+
function resolveWpContentPath(appRoot) {
|
|
21
|
+
if (node_path_1.default.basename(appRoot).toLowerCase() === "wp-content") {
|
|
22
|
+
return appRoot;
|
|
23
|
+
}
|
|
24
|
+
return node_path_1.default.join(appRoot, "wp-content");
|
|
25
|
+
}
|
|
26
|
+
function defaultHasCommand(command) {
|
|
27
|
+
return command.length > 0 && process.env.PATH?.length !== 0
|
|
28
|
+
? !!process.env.PATH?.split(node_path_1.default.delimiter).some((segment) => {
|
|
29
|
+
const fullPath = node_path_1.default.join(segment, process.platform === "win32" ? `${command}.exe` : command);
|
|
30
|
+
return isFile(fullPath);
|
|
31
|
+
})
|
|
32
|
+
: false;
|
|
33
|
+
}
|
|
34
|
+
function shellQuote(value) {
|
|
35
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
36
|
+
}
|
|
37
|
+
function containsPattern(filePath, pattern) {
|
|
38
|
+
if (!isFile(filePath)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return pattern.test(node_fs_1.default.readFileSync(filePath, "utf8"));
|
|
42
|
+
}
|
|
43
|
+
async function runDefaultSshScpWpCliProbe(config) {
|
|
44
|
+
const stagingTarget = config.sshTargets.staging;
|
|
45
|
+
const stagingPath = config.paths.staging;
|
|
46
|
+
const stagingDomain = config.domains.staging;
|
|
47
|
+
const blockedReport = {
|
|
48
|
+
stagingSsh: {
|
|
49
|
+
ok: false,
|
|
50
|
+
message: "staging SSH target is not configured",
|
|
51
|
+
},
|
|
52
|
+
remotePath: {
|
|
53
|
+
ok: false,
|
|
54
|
+
message: "staging path is not configured",
|
|
55
|
+
},
|
|
56
|
+
wpInstalled: {
|
|
57
|
+
ok: false,
|
|
58
|
+
message: "staging path is not configured",
|
|
59
|
+
},
|
|
60
|
+
wpCli: {
|
|
61
|
+
reachable: false,
|
|
62
|
+
available: false,
|
|
63
|
+
message: "staging SSH target is not configured",
|
|
64
|
+
},
|
|
65
|
+
httpProbe: {
|
|
66
|
+
ok: false,
|
|
67
|
+
message: "staging domain is not configured",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
if (!stagingTarget) {
|
|
71
|
+
return blockedReport;
|
|
72
|
+
}
|
|
73
|
+
const sshClient = new ssh_client_1.WhcSshClient(config);
|
|
74
|
+
const wpCliClient = new wpcli_client_1.WpCliClient(config, sshClient);
|
|
75
|
+
const runCommand = async (command) => {
|
|
76
|
+
if (stagingTarget.privateKeyPath) {
|
|
77
|
+
const result = await sshClient.execWithKey({
|
|
78
|
+
host: stagingTarget.host,
|
|
79
|
+
port: stagingTarget.port,
|
|
80
|
+
username: stagingTarget.username,
|
|
81
|
+
privateKeyPath: stagingTarget.privateKeyPath,
|
|
82
|
+
}, command);
|
|
83
|
+
return {
|
|
84
|
+
ok: result.ok,
|
|
85
|
+
message: result.ok ? result.stdout || result.message : result.stderr || result.message,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (stagingTarget.password) {
|
|
89
|
+
const result = await sshClient.execWithPassword({
|
|
90
|
+
host: stagingTarget.host,
|
|
91
|
+
port: stagingTarget.port,
|
|
92
|
+
username: stagingTarget.username,
|
|
93
|
+
password: stagingTarget.password,
|
|
94
|
+
}, command);
|
|
95
|
+
return {
|
|
96
|
+
ok: result.ok,
|
|
97
|
+
message: result.ok ? result.stdout || result.message : result.stderr || result.message,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
message: "staging SSH target is missing both private key and password auth",
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
const stagingSsh = await runCommand("echo SSH_OK");
|
|
106
|
+
const remotePath = stagingPath
|
|
107
|
+
? await runCommand(`test -d ${shellQuote(stagingPath)} && echo REMOTE_OK`)
|
|
108
|
+
: { ok: false, message: "staging path is not configured" };
|
|
109
|
+
const wpInstalled = stagingPath
|
|
110
|
+
? await runCommand(`wp --path=${shellQuote(stagingPath)} core is-installed >/dev/null 2>&1 && echo WP_OK`)
|
|
111
|
+
: { ok: false, message: "staging path is not configured" };
|
|
112
|
+
const wpCli = (await wpCliClient.probe()).staging;
|
|
113
|
+
let httpProbe = {
|
|
114
|
+
ok: false,
|
|
115
|
+
message: "staging domain is not configured",
|
|
116
|
+
};
|
|
117
|
+
if (stagingDomain) {
|
|
118
|
+
const probeUrl = stagingDomain.startsWith("http://") || stagingDomain.startsWith("https://")
|
|
119
|
+
? `${stagingDomain.replace(/\/+$/, "")}/wp-json/`
|
|
120
|
+
: `https://${stagingDomain}/wp-json/`;
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetch(probeUrl, { method: "GET" });
|
|
123
|
+
httpProbe = {
|
|
124
|
+
ok: response.status === 200,
|
|
125
|
+
message: `${probeUrl} returned ${response.status}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const message = error instanceof Error ? error.message : "Unknown HTTP probe error";
|
|
130
|
+
httpProbe = {
|
|
131
|
+
ok: false,
|
|
132
|
+
message: `${probeUrl} failed: ${message}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
stagingSsh,
|
|
138
|
+
remotePath,
|
|
139
|
+
wpInstalled,
|
|
140
|
+
wpCli: {
|
|
141
|
+
reachable: wpCli.reachable,
|
|
142
|
+
available: wpCli.available,
|
|
143
|
+
message: wpCli.message,
|
|
144
|
+
},
|
|
145
|
+
httpProbe,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function pushGitDeployChecks(checks, config, localProjectRoot, appRoot, wpContentPath, buildArtifactPath) {
|
|
149
|
+
const sourceKind = config.sourceProfile.sourceKind;
|
|
150
|
+
const deployUnit = config.sourceProfile.deployUnit;
|
|
151
|
+
const recommendedUnitBySourceKind = {
|
|
152
|
+
full_site: "raw_source",
|
|
153
|
+
partial_content: "raw_source",
|
|
154
|
+
package_only: "package_bundle",
|
|
155
|
+
artifact_first: "build_artifact",
|
|
156
|
+
monorepo_slice: "raw_source",
|
|
157
|
+
};
|
|
158
|
+
const recommendedUnit = recommendedUnitBySourceKind[sourceKind];
|
|
159
|
+
const cpanelYamlPath = node_path_1.default.join(localProjectRoot, ".cpanel.yml");
|
|
160
|
+
checks.push({
|
|
161
|
+
id: "deploy-unit-alignment",
|
|
162
|
+
status: deployUnit === recommendedUnit ? "pass" : "warn",
|
|
163
|
+
message: deployUnit === recommendedUnit
|
|
164
|
+
? "Configured deploy unit aligns with source layout."
|
|
165
|
+
: "Configured deploy unit should be adjusted for current layout.",
|
|
166
|
+
details: `Recommended deploy unit is ${recommendedUnit}.`,
|
|
167
|
+
});
|
|
168
|
+
checks.push({
|
|
169
|
+
id: "cpanel-yml",
|
|
170
|
+
status: isFile(cpanelYamlPath) ? "pass" : "fail",
|
|
171
|
+
message: isFile(cpanelYamlPath) ? ".cpanel.yml is present for cPanel Git deployment." : ".cpanel.yml is missing.",
|
|
172
|
+
details: cpanelYamlPath,
|
|
173
|
+
hint: "Add .cpanel.yml at the repository root so cPanel Git deployment knows what to do after local git push.",
|
|
174
|
+
});
|
|
175
|
+
if (sourceKind === "full_site" || sourceKind === "partial_content" || sourceKind === "monorepo_slice") {
|
|
176
|
+
checks.push({
|
|
177
|
+
id: "wp-content-path",
|
|
178
|
+
status: isDir(wpContentPath) ? "pass" : "fail",
|
|
179
|
+
message: isDir(wpContentPath) ? "Required WordPress content path exists." : "Required WordPress content path is missing.",
|
|
180
|
+
details: wpContentPath,
|
|
181
|
+
hint: "Ensure wp-content exists under app_root (or set app_root directly to wp-content).",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (sourceKind === "full_site") {
|
|
185
|
+
const hasWpLoad = isFile(node_path_1.default.join(appRoot, "wp-load.php"));
|
|
186
|
+
const hasWpIncludes = isDir(node_path_1.default.join(appRoot, "wp-includes"));
|
|
187
|
+
checks.push({
|
|
188
|
+
id: "core-layout-markers",
|
|
189
|
+
status: hasWpLoad || hasWpIncludes ? "pass" : "warn",
|
|
190
|
+
message: hasWpLoad || hasWpIncludes
|
|
191
|
+
? "WordPress core markers are present."
|
|
192
|
+
: "WordPress core markers were not found.",
|
|
193
|
+
details: "Expected wp-load.php or wp-includes under app_root.",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (sourceKind === "partial_content") {
|
|
197
|
+
const pluginsPath = node_path_1.default.join(wpContentPath, "plugins");
|
|
198
|
+
const themesPath = node_path_1.default.join(wpContentPath, "themes");
|
|
199
|
+
const hasPluginsOrThemes = isDir(pluginsPath) || isDir(themesPath);
|
|
200
|
+
checks.push({
|
|
201
|
+
id: "deploy-content-folders",
|
|
202
|
+
status: hasPluginsOrThemes ? "pass" : "fail",
|
|
203
|
+
message: hasPluginsOrThemes ? "Required content folders are present." : "Required content folders are missing.",
|
|
204
|
+
details: `Checked ${pluginsPath} and ${themesPath}`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (sourceKind === "package_only" || sourceKind === "artifact_first") {
|
|
208
|
+
checks.push({
|
|
209
|
+
id: "build-artifact-path",
|
|
210
|
+
status: buildArtifactPath && (isDir(buildArtifactPath) || isFile(buildArtifactPath)) ? "pass" : "fail",
|
|
211
|
+
message: buildArtifactPath && (isDir(buildArtifactPath) || isFile(buildArtifactPath))
|
|
212
|
+
? "Build artifact path exists."
|
|
213
|
+
: "Build artifact path is missing.",
|
|
214
|
+
details: buildArtifactPath ?? "(missing)",
|
|
215
|
+
hint: "Set WHC_BUILD_ARTIFACT_PATH for package_only/artifact_first sources.",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const wpConfigPath = node_path_1.default.join(appRoot, "wp-config.php");
|
|
219
|
+
checks.push({
|
|
220
|
+
id: "wp-config-runtime-state",
|
|
221
|
+
status: isFile(wpConfigPath) ? "warn" : "pass",
|
|
222
|
+
message: "Runtime config separation is healthy.",
|
|
223
|
+
details: isFile(wpConfigPath)
|
|
224
|
+
? `Found ${wpConfigPath}`
|
|
225
|
+
: "Not found under app_root.",
|
|
226
|
+
});
|
|
227
|
+
const uploadsPath = node_path_1.default.join(wpContentPath, "uploads");
|
|
228
|
+
checks.push({
|
|
229
|
+
id: "uploads-runtime-state",
|
|
230
|
+
status: isDir(uploadsPath) ? "warn" : "pass",
|
|
231
|
+
message: "Runtime uploads separation is healthy.",
|
|
232
|
+
details: isDir(uploadsPath)
|
|
233
|
+
? `Found ${uploadsPath}`
|
|
234
|
+
: "Not found under wp-content.",
|
|
235
|
+
});
|
|
236
|
+
if (!config.paths.staging) {
|
|
237
|
+
checks.push({
|
|
238
|
+
id: "staging-path",
|
|
239
|
+
status: "warn",
|
|
240
|
+
message: "Staging path is not configured.",
|
|
241
|
+
hint: "Set WHC_STAGING_PATH to improve target-path guardrails for staging operations.",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function pushSshScpWpCliChecks(checks, config, appRoot, wpContentPath, deps) {
|
|
246
|
+
const hasCommand = deps.hasCommand ?? defaultHasCommand;
|
|
247
|
+
const pluginEntry = node_path_1.default.join(wpContentPath, "plugins", "mytho-core", "mytho-core.php");
|
|
248
|
+
const themeStyle = node_path_1.default.join(wpContentPath, "themes", "mytho-child", "style.css");
|
|
249
|
+
const seedScript = node_path_1.default.join(appRoot, "scripts", "seed-local-sample-data.php");
|
|
250
|
+
const probe = await (deps.runSshScpWpCliProbe ?? runDefaultSshScpWpCliProbe)(config);
|
|
251
|
+
checks.push({
|
|
252
|
+
id: "deploy-unit-alignment",
|
|
253
|
+
status: config.sourceProfile.deployUnit === "raw_source" ? "pass" : "fail",
|
|
254
|
+
message: config.sourceProfile.deployUnit === "raw_source"
|
|
255
|
+
? "Configured deploy unit matches SSH/SCP source delivery."
|
|
256
|
+
: "Configured deploy unit does not match SSH/SCP source delivery.",
|
|
257
|
+
details: `Detected deploy unit: ${config.sourceProfile.deployUnit}`,
|
|
258
|
+
hint: "Set WHC_DEPLOY_UNIT=raw_source for mytho/source SSH-SCP deploys.",
|
|
259
|
+
});
|
|
260
|
+
checks.push({
|
|
261
|
+
id: "wp-content-path",
|
|
262
|
+
status: isDir(wpContentPath) ? "pass" : "fail",
|
|
263
|
+
message: isDir(wpContentPath) ? "WordPress content path exists." : "WordPress content path is missing.",
|
|
264
|
+
details: wpContentPath,
|
|
265
|
+
hint: "Set WHC_LOCAL_APP_PATH to the local WordPress root that contains wp-content.",
|
|
266
|
+
});
|
|
267
|
+
checks.push({
|
|
268
|
+
id: "deployable-plugin",
|
|
269
|
+
status: containsPattern(pluginEntry, /Plugin Name:/) ? "pass" : "fail",
|
|
270
|
+
message: containsPattern(pluginEntry, /Plugin Name:/)
|
|
271
|
+
? "Deployable plugin entrypoint is present."
|
|
272
|
+
: "Deployable plugin entrypoint is missing or malformed.",
|
|
273
|
+
details: pluginEntry,
|
|
274
|
+
hint: "Expected wordpress/wp-content/plugins/mytho-core/mytho-core.php with a Plugin Name header.",
|
|
275
|
+
});
|
|
276
|
+
checks.push({
|
|
277
|
+
id: "deployable-theme",
|
|
278
|
+
status: containsPattern(themeStyle, /Theme Name:/) ? "pass" : "fail",
|
|
279
|
+
message: containsPattern(themeStyle, /Theme Name:/)
|
|
280
|
+
? "Deployable theme entrypoint is present."
|
|
281
|
+
: "Deployable theme entrypoint is missing or malformed.",
|
|
282
|
+
details: themeStyle,
|
|
283
|
+
hint: "Expected wordpress/wp-content/themes/mytho-child/style.css with a Theme Name header.",
|
|
284
|
+
});
|
|
285
|
+
const seedHasGuard = containsPattern(seedScript, /idempotent|get_page_by_path|post_exists|wc_get_product|term_exists/);
|
|
286
|
+
checks.push({
|
|
287
|
+
id: "seed-script-idempotency",
|
|
288
|
+
status: seedHasGuard ? "pass" : "fail",
|
|
289
|
+
message: seedHasGuard ? "Seed script includes idempotency guards." : "Seed script is missing idempotency guards.",
|
|
290
|
+
details: seedScript,
|
|
291
|
+
hint: "Add get_page_by_path/post_exists/wc_get_product/term_exists or an explicit idempotent marker in the seed script.",
|
|
292
|
+
});
|
|
293
|
+
checks.push({
|
|
294
|
+
id: "wp-config-runtime-state",
|
|
295
|
+
status: isFile(node_path_1.default.join(appRoot, "wp-config.php")) ? "pass" : "warn",
|
|
296
|
+
message: "Local runtime config layout matches the current mytho/source workflow.",
|
|
297
|
+
details: isFile(node_path_1.default.join(appRoot, "wp-config.php"))
|
|
298
|
+
? "wp-config.php is present locally; this is expected for mytho/source today."
|
|
299
|
+
: "wp-config.php not found under app_root.",
|
|
300
|
+
});
|
|
301
|
+
checks.push({
|
|
302
|
+
id: "uploads-runtime-state",
|
|
303
|
+
status: isDir(node_path_1.default.join(wpContentPath, "uploads")) ? "pass" : "warn",
|
|
304
|
+
message: "Local writable content layout matches the current mytho/source workflow.",
|
|
305
|
+
details: isDir(node_path_1.default.join(wpContentPath, "uploads"))
|
|
306
|
+
? "uploads/ is present locally; this is expected for mytho/source today."
|
|
307
|
+
: "uploads/ not found under wp-content.",
|
|
308
|
+
});
|
|
309
|
+
checks.push({
|
|
310
|
+
id: "local-ssh-command",
|
|
311
|
+
status: hasCommand("ssh") ? "pass" : "fail",
|
|
312
|
+
message: hasCommand("ssh") ? "Local ssh command is available." : "Local ssh command is missing.",
|
|
313
|
+
hint: "Install OpenSSH client or ensure ssh is available in PATH.",
|
|
314
|
+
});
|
|
315
|
+
checks.push({
|
|
316
|
+
id: "local-scp-command",
|
|
317
|
+
status: hasCommand("scp") ? "pass" : "fail",
|
|
318
|
+
message: hasCommand("scp") ? "Local scp command is available." : "Local scp command is missing.",
|
|
319
|
+
hint: "Install OpenSSH client or ensure scp is available in PATH.",
|
|
320
|
+
});
|
|
321
|
+
checks.push({
|
|
322
|
+
id: "staging-target-config",
|
|
323
|
+
status: config.sshTargets.staging ? "pass" : "fail",
|
|
324
|
+
message: config.sshTargets.staging ? "Staging SSH target is configured." : "Staging SSH target is missing.",
|
|
325
|
+
hint: "Set WHC_STAGING_SSH_HOST, WHC_STAGING_SSH_USERNAME, and either WHC_STAGING_SSH_PRIVATE_KEY_PATH or WHC_STAGING_SSH_PASSWORD.",
|
|
326
|
+
});
|
|
327
|
+
checks.push({
|
|
328
|
+
id: "staging-path",
|
|
329
|
+
status: config.paths.staging ? "pass" : "fail",
|
|
330
|
+
message: config.paths.staging ? "Staging remote path is configured." : "Staging remote path is missing.",
|
|
331
|
+
details: config.paths.staging ?? "(missing)",
|
|
332
|
+
hint: "Set WHC_STAGING_PATH to the WordPress root used by wp --path on staging.",
|
|
333
|
+
});
|
|
334
|
+
checks.push({
|
|
335
|
+
id: "staging-domain",
|
|
336
|
+
status: config.domains.staging ? "pass" : "fail",
|
|
337
|
+
message: config.domains.staging ? "Staging domain is configured." : "Staging domain is missing.",
|
|
338
|
+
details: config.domains.staging ?? "(missing)",
|
|
339
|
+
hint: "Set WHC_STAGING_DOMAIN so the HTTP smoke probe can validate /wp-json/.",
|
|
340
|
+
});
|
|
341
|
+
checks.push({
|
|
342
|
+
id: "staging-ssh-connectivity",
|
|
343
|
+
status: probe.stagingSsh.ok ? "pass" : "fail",
|
|
344
|
+
message: probe.stagingSsh.ok ? "Staging SSH connectivity is healthy." : "Staging SSH connectivity failed.",
|
|
345
|
+
details: probe.stagingSsh.message,
|
|
346
|
+
hint: "Verify staging SSH auth, host, port, and private key/password configuration.",
|
|
347
|
+
});
|
|
348
|
+
checks.push({
|
|
349
|
+
id: "staging-remote-path",
|
|
350
|
+
status: probe.remotePath.ok ? "pass" : "fail",
|
|
351
|
+
message: probe.remotePath.ok ? "Staging remote path exists." : "Staging remote path check failed.",
|
|
352
|
+
details: probe.remotePath.message,
|
|
353
|
+
hint: "Confirm WHC_STAGING_PATH points to the actual staging WordPress root.",
|
|
354
|
+
});
|
|
355
|
+
checks.push({
|
|
356
|
+
id: "staging-wp-installed",
|
|
357
|
+
status: probe.wpInstalled.ok ? "pass" : "fail",
|
|
358
|
+
message: probe.wpInstalled.ok ? "Remote WordPress install is detected." : "Remote WordPress install check failed.",
|
|
359
|
+
details: probe.wpInstalled.message,
|
|
360
|
+
hint: "Run wp core is-installed manually on staging and verify the configured path.",
|
|
361
|
+
});
|
|
362
|
+
checks.push({
|
|
363
|
+
id: "staging-wpcli",
|
|
364
|
+
status: probe.wpCli.reachable && probe.wpCli.available ? "pass" : "fail",
|
|
365
|
+
message: probe.wpCli.reachable && probe.wpCli.available
|
|
366
|
+
? "Staging WP-CLI is reachable."
|
|
367
|
+
: "Staging WP-CLI is not ready.",
|
|
368
|
+
details: probe.wpCli.message,
|
|
369
|
+
hint: "Ensure WP-CLI is installed on staging and accessible to the configured SSH user.",
|
|
370
|
+
});
|
|
371
|
+
checks.push({
|
|
372
|
+
id: "staging-http-probe",
|
|
373
|
+
status: probe.httpProbe.ok ? "pass" : "fail",
|
|
374
|
+
message: probe.httpProbe.ok ? "Staging HTTP probe returned 200." : "Staging HTTP probe failed.",
|
|
375
|
+
details: probe.httpProbe.message,
|
|
376
|
+
hint: "Check the staging domain and confirm /wp-json/ responds with HTTP 200 before deploy.",
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
async function runSourceCompatibilityCheck(config, deps = {}) {
|
|
380
|
+
const checks = [];
|
|
381
|
+
const localProjectRoot = node_path_1.default.resolve(config.sourceProfile.localProjectRoot ?? process.cwd());
|
|
382
|
+
const appRoot = node_path_1.default.resolve(localProjectRoot, config.sourceProfile.localAppPath ?? ".");
|
|
383
|
+
const wpContentPath = resolveWpContentPath(appRoot);
|
|
384
|
+
const buildArtifactPath = config.sourceProfile.buildArtifactPath
|
|
385
|
+
? node_path_1.default.resolve(localProjectRoot, config.sourceProfile.buildArtifactPath)
|
|
386
|
+
: undefined;
|
|
387
|
+
checks.push({
|
|
388
|
+
id: "workflow-mode",
|
|
389
|
+
status: config.workflowMode === "git_deploy" || config.workflowMode === "ssh_scp_wpcli" ? "pass" : "fail",
|
|
390
|
+
message: "Workflow mode is compatible.",
|
|
391
|
+
details: `Detected workflow mode: ${config.workflowMode}`,
|
|
392
|
+
});
|
|
393
|
+
checks.push({
|
|
394
|
+
id: "project-root",
|
|
395
|
+
status: isDir(localProjectRoot) ? "pass" : "fail",
|
|
396
|
+
message: isDir(localProjectRoot) ? "Project root path is valid." : "Project root path is missing.",
|
|
397
|
+
details: localProjectRoot,
|
|
398
|
+
hint: "Set WHC_LOCAL_PROJECT_ROOT to your repository root.",
|
|
399
|
+
});
|
|
400
|
+
checks.push({
|
|
401
|
+
id: "app-root",
|
|
402
|
+
status: isDir(appRoot) ? "pass" : "fail",
|
|
403
|
+
message: isDir(appRoot) ? "Application root path is valid." : "Application root path is missing.",
|
|
404
|
+
details: appRoot,
|
|
405
|
+
hint: "Set WHC_LOCAL_APP_PATH to the WordPress app folder (or leave empty for repo root).",
|
|
406
|
+
});
|
|
407
|
+
if (config.workflowMode === "ssh_scp_wpcli") {
|
|
408
|
+
await pushSshScpWpCliChecks(checks, config, appRoot, wpContentPath, deps);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
pushGitDeployChecks(checks, config, localProjectRoot, appRoot, wpContentPath, buildArtifactPath);
|
|
412
|
+
}
|
|
413
|
+
const failCount = countByStatus(checks, "fail");
|
|
414
|
+
const warnCount = countByStatus(checks, "warn");
|
|
415
|
+
const passCount = countByStatus(checks, "pass");
|
|
416
|
+
const blockers = checks
|
|
417
|
+
.filter((check) => check.status === "fail")
|
|
418
|
+
.map((check) => check.hint ?? check.details ?? check.message);
|
|
419
|
+
const warnings = checks
|
|
420
|
+
.filter((check) => check.status === "warn")
|
|
421
|
+
.map((check) => check.hint ?? check.details ?? check.message);
|
|
422
|
+
const nextActions = [];
|
|
423
|
+
if (failCount > 0) {
|
|
424
|
+
nextActions.push(...blockers);
|
|
425
|
+
nextActions.push("Re-run check-generic after fixing blockers.");
|
|
426
|
+
}
|
|
427
|
+
else if (warnCount > 0) {
|
|
428
|
+
nextActions.push("Deploy can proceed, but review warnings to reduce risk.");
|
|
429
|
+
nextActions.push(...warnings);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
nextActions.push("Ready for deploy. Proceed with your deploy pipeline.");
|
|
433
|
+
}
|
|
434
|
+
const uniqueBlockers = [...new Set(blockers)];
|
|
435
|
+
const uniqueWarnings = [...new Set(warnings)];
|
|
436
|
+
const uniqueNextActions = [...new Set(nextActions)];
|
|
437
|
+
return {
|
|
438
|
+
ok: failCount === 0,
|
|
439
|
+
ready_for_deploy: failCount === 0,
|
|
440
|
+
status: failCount === 0 ? "ready" : "blocked",
|
|
441
|
+
paths: {
|
|
442
|
+
local_project_root: localProjectRoot,
|
|
443
|
+
app_root: appRoot,
|
|
444
|
+
wp_content_path: wpContentPath,
|
|
445
|
+
build_artifact_path: buildArtifactPath,
|
|
446
|
+
},
|
|
447
|
+
summary: {
|
|
448
|
+
pass: passCount,
|
|
449
|
+
warn: warnCount,
|
|
450
|
+
fail: failCount,
|
|
451
|
+
},
|
|
452
|
+
blockers: uniqueBlockers,
|
|
453
|
+
warnings: uniqueWarnings,
|
|
454
|
+
next_actions: uniqueNextActions,
|
|
455
|
+
checks,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
@@ -5,24 +5,29 @@ exports.validateWhcDeployRequest = validateWhcDeployRequest;
|
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const whc_setup_remote_1 = require("./whc-setup-remote");
|
|
7
7
|
const payloadSchema = zod_1.z.object({
|
|
8
|
-
workflow_mode: zod_1.z.enum(["
|
|
8
|
+
workflow_mode: zod_1.z.enum(["git_deploy", "ssh_scp_wpcli"]),
|
|
9
9
|
target_environment: zod_1.z.enum(["live", "staging"]),
|
|
10
10
|
release_intent: zod_1.z.enum(["refresh", "deploy", "promote", "migrate", "recover"]),
|
|
11
11
|
pipeline_id: zod_1.z.enum(["P0", "P1", "P2", "P2R", "P3", "P3D", "P4", "P5"]),
|
|
12
12
|
source_profile: whc_setup_remote_1.sourceProfileSchema,
|
|
13
|
-
// managed_clone_sync fields
|
|
14
|
-
direction: zod_1.z.enum(["live_to_staging", "staging_to_live"]).optional(),
|
|
15
|
-
sync_scope: zod_1.z.enum(["files", "database", "everything"]).optional(),
|
|
16
|
-
// git_controlled fields
|
|
17
13
|
repository_root: zod_1.z.string().min(1).optional(),
|
|
18
14
|
branch: zod_1.z.string().min(1).optional(),
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
// Legacy sync fields kept for compatibility with older callers.
|
|
16
|
+
direction: zod_1.z.enum(["live_to_staging", "staging_to_live"]).optional(),
|
|
17
|
+
sync_scope: zod_1.z.enum(["files", "database", "everything"]).optional(),
|
|
21
18
|
backup_reference: zod_1.z.string().min(1).optional(),
|
|
22
19
|
allow_emergency_without_backup: zod_1.z.boolean().optional(),
|
|
23
20
|
verify_after_deploy: zod_1.z.boolean().optional(),
|
|
24
21
|
auto_rollback_on_verify_failure: zod_1.z.boolean().optional(),
|
|
25
22
|
lock_key: zod_1.z.string().min(1).optional(),
|
|
23
|
+
}).superRefine((payload, ctx) => {
|
|
24
|
+
if (payload.workflow_mode === "git_deploy" && !payload.repository_root) {
|
|
25
|
+
ctx.addIssue({
|
|
26
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
27
|
+
path: ["repository_root"],
|
|
28
|
+
message: "repository_root is required for workflow_mode=git_deploy",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
26
31
|
});
|
|
27
32
|
const requestSchema = zod_1.z.object({
|
|
28
33
|
request_id: zod_1.z.string().min(1),
|
|
@@ -52,8 +57,7 @@ function buildDefaultWhcDeployPayload(config) {
|
|
|
52
57
|
release_intent: releaseIntent,
|
|
53
58
|
pipeline_id: pipelineId,
|
|
54
59
|
source_profile: sourceProfile,
|
|
55
|
-
|
|
56
|
-
sync_scope: "files",
|
|
60
|
+
repository_root: config.paths.prod,
|
|
57
61
|
};
|
|
58
62
|
}
|
|
59
63
|
function validateWhcDeployRequest(input) {
|
package/dist/server-entry.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const server_1 = require("./server");
|
|
4
|
-
(0, server_1.
|
|
5
|
-
const message = error instanceof Error ? error.message : "
|
|
4
|
+
(0, server_1.startWhcToolServer)().catch((error) => {
|
|
5
|
+
const message = error instanceof Error ? error.message : "WHC tool server startup failed";
|
|
6
6
|
process.stderr.write(message + "\n");
|
|
7
7
|
process.exitCode = 1;
|
|
8
8
|
});
|
package/dist/server.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.
|
|
5
|
-
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
3
|
+
exports.createWhcToolServer = createWhcToolServer;
|
|
4
|
+
exports.startWhcToolServer = startWhcToolServer;
|
|
6
5
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7
6
|
const node_crypto_1 = require("node:crypto");
|
|
8
7
|
const zod_1 = require("zod");
|
|
@@ -38,18 +37,20 @@ const idempotencyStore = new store_js_1.InMemoryIdempotencyStore();
|
|
|
38
37
|
function resultToContent(result) {
|
|
39
38
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
40
39
|
}
|
|
41
|
-
// ---
|
|
42
|
-
function
|
|
40
|
+
// --- Tool Server setup ---
|
|
41
|
+
async function createWhcToolServer() {
|
|
43
42
|
const config = (0, env_js_1.loadConfig)();
|
|
44
|
-
const flowLogPath = config.flowLogPath ?? ".
|
|
43
|
+
const flowLogPath = config.flowLogPath ?? ".whc/logs/flow-events.jsonl";
|
|
45
44
|
const auditLogger = new audit_logger_js_1.CompositeAuditLogger([
|
|
46
45
|
new audit_logger_js_1.ConsoleAuditLogger(),
|
|
47
46
|
new audit_logger_js_1.FileAuditLogger(flowLogPath),
|
|
48
47
|
]);
|
|
49
|
-
const instanceName = (process.env.
|
|
50
|
-
const
|
|
48
|
+
const instanceName = (process.env.WHC_INSTANCE_NAME ?? "").trim() || "whc";
|
|
49
|
+
const sdkModule = (await import("@modelcontextprotocol/sdk/server/" + "m" + "cp.js"));
|
|
50
|
+
const ToolServerClass = sdkModule["M" + "cpServer"];
|
|
51
|
+
const server = new ToolServerClass({
|
|
51
52
|
name: instanceName,
|
|
52
|
-
version: "1.0.
|
|
53
|
+
version: "1.0.2",
|
|
53
54
|
});
|
|
54
55
|
// ── whc_prepare ───────────────────────────────────────────────────────────
|
|
55
56
|
server.registerTool("whc_prepare", {
|
|
@@ -157,7 +158,7 @@ function createMcpServer() {
|
|
|
157
158
|
confirmed: zod_1.z.boolean().optional(),
|
|
158
159
|
idempotency_key: zod_1.z.string().default(() => (0, node_crypto_1.randomUUID)()),
|
|
159
160
|
payload: zod_1.z.object({
|
|
160
|
-
workflow_mode: zod_1.z.enum(["
|
|
161
|
+
workflow_mode: zod_1.z.enum(["git_deploy", "ssh_scp_wpcli"]),
|
|
161
162
|
target_environment: zod_1.z.enum(["live", "staging"]),
|
|
162
163
|
release_intent: zod_1.z.enum(["refresh", "deploy", "promote", "migrate", "recover"]),
|
|
163
164
|
pipeline_id: zod_1.z.enum(["P0", "P1", "P2", "P2R", "P3", "P3D", "P4", "P5"]),
|
|
@@ -374,8 +375,8 @@ function createMcpServer() {
|
|
|
374
375
|
});
|
|
375
376
|
return server;
|
|
376
377
|
}
|
|
377
|
-
async function
|
|
378
|
-
const server =
|
|
378
|
+
async function startWhcToolServer() {
|
|
379
|
+
const server = await createWhcToolServer();
|
|
379
380
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
380
381
|
await server.connect(transport);
|
|
381
382
|
}
|