devops-whc 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/AGENT_MCP_USAGE.md +394 -0
  2. package/LICENSE +15 -0
  3. package/README.md +208 -0
  4. package/WHC_MCP_REQUIREMENTS.md +112 -0
  5. package/dist/audit/audit-logger.js +57 -0
  6. package/dist/clients/ssh-client.js +199 -0
  7. package/dist/clients/whc-uapi-client.js +178 -0
  8. package/dist/clients/wpcli-client.js +125 -0
  9. package/dist/config/env.js +132 -0
  10. package/dist/contracts/deployment.js +2 -0
  11. package/dist/contracts/envelope.js +2 -0
  12. package/dist/dispatcher/tool-dispatcher.js +145 -0
  13. package/dist/handlers/whc-check-health.js +131 -0
  14. package/dist/handlers/whc-db-backup.js +111 -0
  15. package/dist/handlers/whc-deploy.js +381 -0
  16. package/dist/handlers/whc-get-logs.js +108 -0
  17. package/dist/handlers/whc-pipeline-status.js +96 -0
  18. package/dist/handlers/whc-prepare.js +127 -0
  19. package/dist/handlers/whc-rollback.js +141 -0
  20. package/dist/handlers/whc-setup-remote.js +262 -0
  21. package/dist/handlers/whc-ssh-exec.js +138 -0
  22. package/dist/handlers/whc-verify.js +304 -0
  23. package/dist/idempotency/store.js +13 -0
  24. package/dist/index.js +109 -0
  25. package/dist/policy/policy-engine.js +41 -0
  26. package/dist/probes/connectivity.js +41 -0
  27. package/dist/registry/tool-registry.js +69 -0
  28. package/dist/schemas/whc-check-health.js +55 -0
  29. package/dist/schemas/whc-db-backup.js +29 -0
  30. package/dist/schemas/whc-deploy.js +66 -0
  31. package/dist/schemas/whc-get-logs.js +25 -0
  32. package/dist/schemas/whc-pipeline-status.js +24 -0
  33. package/dist/schemas/whc-prepare.js +29 -0
  34. package/dist/schemas/whc-rollback.js +58 -0
  35. package/dist/schemas/whc-setup-remote.js +60 -0
  36. package/dist/schemas/whc-ssh-exec.js +117 -0
  37. package/dist/schemas/whc-verify.js +28 -0
  38. package/dist/server-entry.js +8 -0
  39. package/dist/server.js +381 -0
  40. package/dist/services/deploy-runtime-ops.js +104 -0
  41. package/dist/services/deployment-locks.js +34 -0
  42. package/dist/state/workspace-state.js +201 -0
  43. package/package.json +48 -0
  44. package/scripts/prepare-first-time.cjs +75 -0
  45. package/scripts/start-mcp.cjs +42 -0
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeWhcVerify = executeWhcVerify;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const ssh_client_1 = require("../clients/ssh-client");
8
+ const connectivity_1 = require("../probes/connectivity");
9
+ const workspace_state_1 = require("../state/workspace-state");
10
+ async function executeWhcVerify(config, request, deps = {}) {
11
+ const startedAt = Date.now();
12
+ const actionIdFactory = deps.actionIdFactory ?? node_crypto_1.randomUUID;
13
+ const sshClient = deps.sshClient ?? new ssh_client_1.WhcSshClient(config);
14
+ const probeRunner = deps.probeRunner ?? connectivity_1.runConnectivityProbe;
15
+ const rootDir = process.cwd();
16
+ (0, workspace_state_1.ensureWorkspaceState)(rootDir);
17
+ const manifestSource = resolveManifest(rootDir, request.payload.release_id, request.payload.manifest_path);
18
+ if (!manifestSource.manifest) {
19
+ return {
20
+ ok: false,
21
+ action_id: actionIdFactory(),
22
+ tool: "whc_verify",
23
+ data: null,
24
+ error: {
25
+ code: "NOT_FOUND",
26
+ message: "No release manifest found. Run whc_deploy first or provide payload.manifest_path.",
27
+ retryable: false,
28
+ },
29
+ meta: {
30
+ latency_ms: Date.now() - startedAt,
31
+ safety_level: "A",
32
+ delivery_mechanism: "read_probe",
33
+ },
34
+ };
35
+ }
36
+ const releaseId = manifestSource.releaseId;
37
+ const changedFiles = getChangedFiles(manifestSource.manifest);
38
+ const sampleSize = Math.min(request.payload.random_sample_size ?? 10, changedFiles.length);
39
+ const sampledFiles = sampleDeterministic(changedFiles, sampleSize, request.payload.random_seed ?? releaseId);
40
+ const filesToCheck = uniqueStrings([...changedFiles, ...sampledFiles]);
41
+ const targetBasePath = request.payload.target_environment === "staging"
42
+ ? config.paths.staging ?? config.paths.prod
43
+ : config.paths.prod;
44
+ const missingFiles = await findMissingFilesOnTarget(sshClient, config, request.payload.target_environment, targetBasePath, filesToCheck);
45
+ const fileStatus = deriveFileStatus(changedFiles.length, filesToCheck.length, missingFiles.length);
46
+ const configNotes = [];
47
+ if (!targetBasePath) {
48
+ configNotes.push("Missing target path in config.");
49
+ }
50
+ if (request.payload.target_environment === "staging" && !config.paths.staging) {
51
+ configNotes.push("WHC_STAGING_PATH is not set; fallback to production path was used.");
52
+ }
53
+ const configStatus = targetBasePath && configNotes.length === 0 ? "PASS" : targetBasePath ? "PARTIAL" : "FAIL";
54
+ const health = await probeRunner(config);
55
+ const healthDetails = [];
56
+ if (!health.uapi.ok)
57
+ healthDetails.push(`UAPI: ${health.uapi.message}`);
58
+ if (!health.ssh.ok)
59
+ healthDetails.push(`SSH: ${health.ssh.message}`);
60
+ const healthStatus = healthDetails.length === 0 ? "PASS" : health.uapi.ok || health.ssh.ok ? "PARTIAL" : "FAIL";
61
+ const shouldCheckDb = request.payload.verify_level === "with_db" || inferDataImpact(manifestSource.manifest);
62
+ const dbVerification = shouldCheckDb
63
+ ? await runDbVerification(sshClient, config, request.payload.target_environment, targetBasePath)
64
+ : undefined;
65
+ const finalStatus = deriveFinalStatus(fileStatus, configStatus, healthStatus, dbVerification?.status);
66
+ const notes = [];
67
+ if (missingFiles.length > 0) {
68
+ notes.push(`Missing ${missingFiles.length} file(s) from manifest check.`);
69
+ }
70
+ if (!shouldCheckDb) {
71
+ notes.push("DB verification skipped (no inferred data-impacting changes).");
72
+ }
73
+ const stateRootRelative = (process.env.WHC_STATE_ROOT ?? workspace_state_1.DEFAULT_STATE_ROOT).trim() || workspace_state_1.DEFAULT_STATE_ROOT;
74
+ const reportsDir = (0, node_path_1.join)((0, workspace_state_1.resolveStateRoot)(rootDir), "state", "reports");
75
+ const reportFile = (0, node_path_1.join)(reportsDir, `${releaseId}.verify-report.json`);
76
+ const data = {
77
+ release_id: releaseId,
78
+ final_status: finalStatus,
79
+ file_verification: {
80
+ declared_changed_files: changedFiles.length,
81
+ checked_files: filesToCheck.length,
82
+ missing_files: missingFiles,
83
+ sampled_files: sampledFiles,
84
+ status: fileStatus,
85
+ },
86
+ config_verification: {
87
+ target_environment: request.payload.target_environment,
88
+ target_path: targetBasePath,
89
+ status: configStatus,
90
+ notes: configNotes,
91
+ },
92
+ health_verification: {
93
+ status: healthStatus,
94
+ details: healthDetails,
95
+ },
96
+ db_verification: dbVerification,
97
+ notes,
98
+ report_file: reportFile,
99
+ next_step: finalStatus === "PASS" ? "Verification passed. Continue pipeline." : "Inspect report and logs, then redeploy or rollback based on policy.",
100
+ };
101
+ (0, node_fs_1.writeFileSync)(reportFile, JSON.stringify({
102
+ ...data,
103
+ manifest_source: manifestSource.path,
104
+ state_root: stateRootRelative,
105
+ verified_at: new Date().toISOString(),
106
+ }, null, 2) + "\n", "utf8");
107
+ return {
108
+ ok: true,
109
+ action_id: actionIdFactory(),
110
+ tool: "whc_verify",
111
+ data,
112
+ error: null,
113
+ meta: {
114
+ latency_ms: Date.now() - startedAt,
115
+ safety_level: "A",
116
+ release_intent: readReleaseIntent(manifestSource.manifest),
117
+ delivery_mechanism: "read_probe",
118
+ },
119
+ };
120
+ }
121
+ function resolveManifest(rootDir, releaseId, manifestPath) {
122
+ if (manifestPath) {
123
+ if (!(0, node_fs_1.existsSync)(manifestPath)) {
124
+ return { releaseId: releaseId ?? "unknown", path: manifestPath, manifest: null };
125
+ }
126
+ return {
127
+ releaseId: releaseId ?? extractReleaseId(manifestPath),
128
+ path: manifestPath,
129
+ manifest: parseJsonFile(manifestPath),
130
+ };
131
+ }
132
+ if (releaseId) {
133
+ const file = (0, workspace_state_1.getReleaseManifestFile)(rootDir, releaseId);
134
+ return {
135
+ releaseId,
136
+ path: file,
137
+ manifest: parseJsonFile(file),
138
+ };
139
+ }
140
+ const releasesDir = (0, node_path_1.join)((0, workspace_state_1.resolveStateRoot)(rootDir), "state", "releases");
141
+ if (!(0, node_fs_1.existsSync)(releasesDir)) {
142
+ return { releaseId: "latest", path: releasesDir, manifest: null };
143
+ }
144
+ const candidates = (0, node_fs_1.readdirSync)(releasesDir)
145
+ .filter((name) => name.endsWith(".auto-manifest.json"))
146
+ .map((name) => ({ name, fullPath: (0, node_path_1.join)(releasesDir, name), mtime: (0, node_fs_1.statSync)((0, node_path_1.join)(releasesDir, name)).mtimeMs }))
147
+ .sort((a, b) => b.mtime - a.mtime);
148
+ const latest = candidates[0];
149
+ if (!latest) {
150
+ return { releaseId: "latest", path: releasesDir, manifest: null };
151
+ }
152
+ return {
153
+ releaseId: extractReleaseId(latest.name),
154
+ path: latest.fullPath,
155
+ manifest: parseJsonFile(latest.fullPath),
156
+ };
157
+ }
158
+ function parseJsonFile(filePath) {
159
+ if (!(0, node_fs_1.existsSync)(filePath)) {
160
+ return null;
161
+ }
162
+ try {
163
+ return JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf8"));
164
+ }
165
+ catch {
166
+ return null;
167
+ }
168
+ }
169
+ function getChangedFiles(manifest) {
170
+ const value = manifest.changed_files;
171
+ if (!Array.isArray(value)) {
172
+ return [];
173
+ }
174
+ return value.filter((item) => typeof item === "string" && item.length > 0);
175
+ }
176
+ function uniqueStrings(values) {
177
+ return Array.from(new Set(values));
178
+ }
179
+ function sampleDeterministic(source, sampleSize, seed) {
180
+ if (sampleSize <= 0) {
181
+ return [];
182
+ }
183
+ const pool = [...source];
184
+ const sampled = [];
185
+ let state = seedToNumber(seed);
186
+ while (pool.length > 0 && sampled.length < sampleSize) {
187
+ state = (state * 1664525 + 1013904223) >>> 0;
188
+ const index = state % pool.length;
189
+ sampled.push(pool[index]);
190
+ pool.splice(index, 1);
191
+ }
192
+ return sampled;
193
+ }
194
+ function seedToNumber(seed) {
195
+ let result = 2166136261;
196
+ for (const ch of seed) {
197
+ result ^= ch.charCodeAt(0);
198
+ result = Math.imul(result, 16777619);
199
+ }
200
+ return result >>> 0;
201
+ }
202
+ async function findMissingFilesOnTarget(sshClient, config, targetEnvironment, targetBasePath, filesToCheck) {
203
+ if (!targetBasePath || filesToCheck.length === 0) {
204
+ return [];
205
+ }
206
+ const quotedBase = quoteShell(targetBasePath);
207
+ const checks = filesToCheck
208
+ .map((file) => {
209
+ const quotedRelative = quoteShell(file);
210
+ return `[ -e ${quotedBase}/${quotedRelative} ] && echo OK:${file} || echo MISS:${file}`;
211
+ })
212
+ .join("; ");
213
+ try {
214
+ const result = await execOnTarget(sshClient, config, targetEnvironment, checks);
215
+ return result.stdout
216
+ .split("\n")
217
+ .map((line) => line.trim())
218
+ .filter((line) => line.startsWith("MISS:"))
219
+ .map((line) => line.slice(5));
220
+ }
221
+ catch {
222
+ return filesToCheck;
223
+ }
224
+ }
225
+ async function runDbVerification(sshClient, config, targetEnvironment, targetBasePath) {
226
+ if (!targetBasePath) {
227
+ return { status: "FAIL", details: ["Cannot run DB check because target path is missing."] };
228
+ }
229
+ const command = `cd ${quoteShell(targetBasePath)} && wp db check --quiet >/dev/null 2>&1 && echo DB_OK || echo DB_FAIL`;
230
+ try {
231
+ const result = await execOnTarget(sshClient, config, targetEnvironment, command);
232
+ if (result.stdout.includes("DB_OK")) {
233
+ return { status: "PASS", details: ["wp db check passed."] };
234
+ }
235
+ return { status: "FAIL", details: ["wp db check failed."] };
236
+ }
237
+ catch (error) {
238
+ const message = error instanceof Error ? error.message : "DB verification failed";
239
+ return { status: "PARTIAL", details: [message] };
240
+ }
241
+ }
242
+ async function execOnTarget(sshClient, config, targetEnvironment, command) {
243
+ if (targetEnvironment === "staging") {
244
+ const target = config.sshTargets.staging;
245
+ if (!target || !target.privateKeyPath) {
246
+ throw new Error("Staging SSH target is not configured for key-based execution.");
247
+ }
248
+ const result = await sshClient.execWithKey({
249
+ host: target.host,
250
+ port: target.port,
251
+ username: target.username,
252
+ privateKeyPath: target.privateKeyPath,
253
+ }, command);
254
+ if (!result.ok) {
255
+ throw new Error(result.message || result.stderr || "SSH command failed");
256
+ }
257
+ return { stdout: result.stdout };
258
+ }
259
+ const result = await sshClient.execWithKey(config.sshTargets.prod, command);
260
+ if (!result.ok) {
261
+ throw new Error(result.message || result.stderr || "SSH command failed");
262
+ }
263
+ return { stdout: result.stdout };
264
+ }
265
+ function deriveFileStatus(declaredCount, checkedCount, missingCount) {
266
+ if (declaredCount === 0) {
267
+ return "PARTIAL";
268
+ }
269
+ if (checkedCount === 0 || missingCount === checkedCount) {
270
+ return "FAIL";
271
+ }
272
+ if (missingCount > 0) {
273
+ return "PARTIAL";
274
+ }
275
+ return "PASS";
276
+ }
277
+ function deriveFinalStatus(fileStatus, configStatus, healthStatus, dbStatus) {
278
+ const statuses = [fileStatus, configStatus, healthStatus, dbStatus].filter(Boolean);
279
+ if (statuses.includes("FAIL")) {
280
+ return "FAIL";
281
+ }
282
+ if (statuses.includes("PARTIAL")) {
283
+ return "PARTIAL";
284
+ }
285
+ return "PASS";
286
+ }
287
+ function inferDataImpact(manifest) {
288
+ const releaseIntent = readReleaseIntent(manifest);
289
+ return releaseIntent === "migrate" || releaseIntent === "recover";
290
+ }
291
+ function readReleaseIntent(manifest) {
292
+ const value = manifest.release_intent;
293
+ if (value === "refresh" || value === "deploy" || value === "promote" || value === "migrate" || value === "recover") {
294
+ return value;
295
+ }
296
+ return undefined;
297
+ }
298
+ function quoteShell(input) {
299
+ return `'${input.replace(/'/g, `'"'"'`)}'`;
300
+ }
301
+ function extractReleaseId(filePath) {
302
+ const base = filePath.split(/[\\/]/).pop() ?? "release";
303
+ return base.replace(/\.auto-manifest\.json$/, "").replace(/\.json$/, "");
304
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemoryIdempotencyStore = void 0;
4
+ class InMemoryIdempotencyStore {
5
+ store = new Map();
6
+ get(key) {
7
+ return this.store.get(key);
8
+ }
9
+ set(key, record) {
10
+ this.store.set(key, record);
11
+ }
12
+ }
13
+ exports.InMemoryIdempotencyStore = InMemoryIdempotencyStore;
package/dist/index.js ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const node_crypto_1 = require("node:crypto");
5
+ const env_1 = require("./config/env");
6
+ const tool_dispatcher_1 = require("./dispatcher/tool-dispatcher");
7
+ const whc_check_health_1 = require("./handlers/whc-check-health");
8
+ const connectivity_1 = require("./probes/connectivity");
9
+ const whc_check_health_2 = require("./schemas/whc-check-health");
10
+ const tool_registry_1 = require("./registry/tool-registry");
11
+ const store_1 = require("./idempotency/store");
12
+ const audit_logger_1 = require("./audit/audit-logger");
13
+ const server_1 = require("./server");
14
+ const packageJson = require("../package.json");
15
+ const APP_VERSION = packageJson.version;
16
+ const HELP_TEXT = [
17
+ `WHC MCP CLI Help v${APP_VERSION}`,
18
+ "",
19
+ "Modes:",
20
+ " --serve Start MCP stdio server (registers all tools)",
21
+ " --probe Run connectivity probe (UAPI, SSH, WP-CLI)",
22
+ " --check-health Execute whc_check_health once and print JSON",
23
+ " --version Print CLI version",
24
+ " --help Show this help",
25
+ "",
26
+ "Quick start:",
27
+ " 1) npm install",
28
+ " 2) copy .env.example to .env and fill secrets",
29
+ " 3) npm run mcp:prepare",
30
+ " 4) npm run serve",
31
+ "",
32
+ "Published package use:",
33
+ " npx -y devops-whc --serve",
34
+ " npx -y devops-whc --probe",
35
+ " npx -y devops-whc --check-health",
36
+ "",
37
+ "Safe release flow:",
38
+ " 1) check-health",
39
+ " 2) pre-deploy logs",
40
+ " 3) staging deploy dry-run (confirmed=true if policy requires)",
41
+ " 4) staging deploy real",
42
+ " 5) smoke gate",
43
+ " 6) promote live only when smoke is green",
44
+ "",
45
+ "Managed clone/sync payload reminders:",
46
+ " - direction is required: live_to_staging or staging_to_live",
47
+ " - sync_scope is required for clear safety intent",
48
+ " - staging_to_live + database/everything needs backup policy",
49
+ "",
50
+ "Flow log:",
51
+ " - default: .mcp/whc-mcp/logs/flow-events.jsonl",
52
+ " - override: set WHC_FLOW_LOG_PATH",
53
+ "",
54
+ "Secrets safety:",
55
+ " - do not commit .env or .vscode/whc.env",
56
+ " - npm tarball excludes local env files by design",
57
+ "",
58
+ "Reference:",
59
+ " - AGENT_MCP_USAGE.md",
60
+ ].join("\n");
61
+ async function main() {
62
+ if (process.argv.includes("--help")) {
63
+ console.log(HELP_TEXT);
64
+ return;
65
+ }
66
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
67
+ console.log(APP_VERSION);
68
+ return;
69
+ }
70
+ // MCP stdio server mode — all 6 tools registered, reads from stdin/stdout
71
+ if (process.argv.includes("--serve")) {
72
+ await (0, server_1.startMcpServer)();
73
+ return;
74
+ }
75
+ const config = (0, env_1.loadConfig)();
76
+ if (process.argv.includes("--probe")) {
77
+ const report = await (0, connectivity_1.runConnectivityProbe)(config);
78
+ const stagingWpCliOk = report.wpcli.staging.reachable;
79
+ console.log(JSON.stringify({
80
+ ok: report.uapi.ok && report.ssh.ok && report.wpcli.prod.reachable && stagingWpCliOk,
81
+ action_id: (0, node_crypto_1.randomUUID)(),
82
+ tool: "whc_connectivity_probe",
83
+ data: report,
84
+ }, null, 2));
85
+ return;
86
+ }
87
+ if (process.argv.includes("--check-health")) {
88
+ const request = (0, whc_check_health_1.buildDefaultWhcCheckHealthRequest)(config);
89
+ const toolDef = tool_registry_1.TOOL_REGISTRY["whc_check_health"];
90
+ const result = await (0, tool_dispatcher_1.dispatch)({
91
+ config,
92
+ toolDef,
93
+ rawInput: request,
94
+ validate: whc_check_health_2.validateWhcCheckHealthRequest,
95
+ execute: (cfg, req) => (0, whc_check_health_1.executeWhcCheckHealth)(cfg, req),
96
+ idempotencyStore: new store_1.InMemoryIdempotencyStore(),
97
+ auditLogger: new audit_logger_1.ConsoleAuditLogger(),
98
+ actionIdFactory: node_crypto_1.randomUUID,
99
+ });
100
+ console.log(JSON.stringify(result, null, 2));
101
+ return;
102
+ }
103
+ console.log(HELP_TEXT);
104
+ }
105
+ main().catch((error) => {
106
+ const message = error instanceof Error ? error.message : "Unknown startup error";
107
+ console.error(message);
108
+ process.exitCode = 1;
109
+ });
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.evaluatePolicy = evaluatePolicy;
4
+ function evaluatePolicy(input) {
5
+ if (input.mode === "read") {
6
+ return {
7
+ allowed: true,
8
+ requiresConfirm: false,
9
+ requiresDryRun: false,
10
+ safetyLevel: input.safetyLevel,
11
+ };
12
+ }
13
+ if (input.safetyLevel === "D") {
14
+ const missingDryRun = !input.dryRun;
15
+ const missingConfirm = !input.confirmed;
16
+ if (missingDryRun || missingConfirm) {
17
+ return {
18
+ allowed: false,
19
+ requiresConfirm: true,
20
+ requiresDryRun: true,
21
+ reason: "Level D operation requires both dry_run and explicit confirmation",
22
+ safetyLevel: input.safetyLevel,
23
+ };
24
+ }
25
+ }
26
+ if (input.safetyLevel === "C" && !input.confirmed) {
27
+ return {
28
+ allowed: false,
29
+ requiresConfirm: true,
30
+ requiresDryRun: false,
31
+ reason: "Level C operation requires explicit confirmation",
32
+ safetyLevel: input.safetyLevel,
33
+ };
34
+ }
35
+ return {
36
+ allowed: true,
37
+ requiresConfirm: input.safetyLevel === "B" || input.safetyLevel === "C" || input.safetyLevel === "D",
38
+ requiresDryRun: input.safetyLevel === "D",
39
+ safetyLevel: input.safetyLevel,
40
+ };
41
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runConnectivityProbe = runConnectivityProbe;
4
+ const ssh_client_1 = require("../clients/ssh-client");
5
+ const wpcli_client_1 = require("../clients/wpcli-client");
6
+ const whc_uapi_client_1 = require("../clients/whc-uapi-client");
7
+ async function runConnectivityProbe(config) {
8
+ const uapiClient = new whc_uapi_client_1.WhcUapiClient(config);
9
+ const sshClient = new ssh_client_1.WhcSshClient(config);
10
+ const wpCliClient = new wpcli_client_1.WpCliClient(config, sshClient);
11
+ const [uapi, prodSsh, wpcli] = await Promise.all([uapiClient.probe(), sshClient.probe(), wpCliClient.probe()]);
12
+ let stagingSsh = {
13
+ ok: false,
14
+ latencyMs: 0,
15
+ message: "staging SSH key target not configured",
16
+ };
17
+ if (config.sshTargets.staging?.privateKeyPath) {
18
+ stagingSsh = await sshClient.probeTarget({
19
+ host: config.sshTargets.staging.host,
20
+ port: config.sshTargets.staging.port,
21
+ username: config.sshTargets.staging.username,
22
+ privateKeyPath: config.sshTargets.staging.privateKeyPath,
23
+ });
24
+ }
25
+ else if (config.sshTargets.staging) {
26
+ stagingSsh = {
27
+ ok: false,
28
+ latencyMs: 0,
29
+ message: "staging SSH uses password mode; key-based SSH probe skipped",
30
+ };
31
+ }
32
+ return {
33
+ uapi,
34
+ ssh: prodSsh,
35
+ sshEnvironments: {
36
+ prod: prodSsh,
37
+ staging: stagingSsh,
38
+ },
39
+ wpcli,
40
+ };
41
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TOOL_REGISTRY = void 0;
4
+ exports.getToolDefinition = getToolDefinition;
5
+ exports.TOOL_REGISTRY = {
6
+ whc_prepare: {
7
+ name: "whc_prepare",
8
+ mode: "write",
9
+ safetyLevel: "B",
10
+ description: "Initialize hidden workspace state and validate deploy readiness",
11
+ },
12
+ whc_setup_remote: {
13
+ name: "whc_setup_remote",
14
+ mode: "write",
15
+ safetyLevel: "C",
16
+ description: "Create cPanel Git repository and return SSH remote URL",
17
+ },
18
+ whc_deploy: {
19
+ name: "whc_deploy",
20
+ mode: "write",
21
+ safetyLevel: "C",
22
+ description: "Trigger VersionControl::deployment for staging or production",
23
+ },
24
+ whc_ssh_exec: {
25
+ name: "whc_ssh_exec",
26
+ mode: "write",
27
+ safetyLevel: "C",
28
+ description: "Execute safe read/sync SSH command on WHC host through enforced policy",
29
+ },
30
+ whc_get_logs: {
31
+ name: "whc_get_logs",
32
+ mode: "read",
33
+ safetyLevel: "A",
34
+ description: "Fetch latest app logs from WHC server",
35
+ },
36
+ whc_db_backup: {
37
+ name: "whc_db_backup",
38
+ mode: "write",
39
+ safetyLevel: "D",
40
+ description: "Create remote compressed SQL backup with audit tracking",
41
+ },
42
+ whc_check_health: {
43
+ name: "whc_check_health",
44
+ mode: "read",
45
+ safetyLevel: "A",
46
+ description: "Collect disk usage, SSL expiry, and load summary",
47
+ },
48
+ whc_verify: {
49
+ name: "whc_verify",
50
+ mode: "read",
51
+ safetyLevel: "A",
52
+ description: "Run post-deploy DevOps verification based on release manifest",
53
+ },
54
+ whc_pipeline_status: {
55
+ name: "whc_pipeline_status",
56
+ mode: "read",
57
+ safetyLevel: "A",
58
+ description: "Read latest hidden pipeline status and artifact pointers",
59
+ },
60
+ whc_rollback: {
61
+ name: "whc_rollback",
62
+ mode: "write",
63
+ safetyLevel: "D",
64
+ description: "Execute guarded rollback with mandatory dry-run, confirmation, and verify-chain",
65
+ },
66
+ };
67
+ function getToolDefinition(toolName) {
68
+ return exports.TOOL_REGISTRY[toolName];
69
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildDefaultWhcCheckHealthPayload = buildDefaultWhcCheckHealthPayload;
4
+ exports.validateWhcCheckHealthRequest = validateWhcCheckHealthRequest;
5
+ const zod_1 = require("zod");
6
+ const sourceProfileSchema = zod_1.z.object({
7
+ local_project_root: zod_1.z.string().min(1).optional(),
8
+ local_app_path: zod_1.z.string().min(1).optional(),
9
+ source_kind: zod_1.z.enum(["full_site", "partial_content", "package_only", "artifact_first", "monorepo_slice"]),
10
+ deploy_unit: zod_1.z.enum(["raw_source", "build_artifact", "package_bundle"]),
11
+ build_command: zod_1.z.string().min(1).optional(),
12
+ build_artifact_path: zod_1.z.string().min(1).optional(),
13
+ });
14
+ const payloadSchema = zod_1.z.object({
15
+ target_environment: zod_1.z.enum(["live", "staging", "auto"]).default("auto"),
16
+ release_intent: zod_1.z.enum(["refresh", "deploy", "promote", "migrate", "recover"]),
17
+ pipeline_id: zod_1.z.enum(["P0", "P1", "P2", "P2R", "P3", "P3D", "P4", "P5"]),
18
+ source_profile: sourceProfileSchema,
19
+ });
20
+ const requestSchema = zod_1.z.object({
21
+ request_id: zod_1.z.string().min(1),
22
+ actor: zod_1.z.object({
23
+ kind: zod_1.z.literal("copilot"),
24
+ user_hint: zod_1.z.string().min(1).optional(),
25
+ }),
26
+ dry_run: zod_1.z.boolean().optional(),
27
+ idempotency_key: zod_1.z.string().min(1).optional(),
28
+ payload: payloadSchema,
29
+ });
30
+ function buildDefaultWhcCheckHealthPayload(config) {
31
+ const releaseIntent = config.sourceProfile.defaultReleaseIntent;
32
+ const sourceProfile = {
33
+ local_project_root: config.sourceProfile.localProjectRoot,
34
+ local_app_path: config.sourceProfile.localAppPath,
35
+ source_kind: config.sourceProfile.sourceKind,
36
+ deploy_unit: config.sourceProfile.deployUnit,
37
+ build_command: config.sourceProfile.buildCommand,
38
+ build_artifact_path: config.sourceProfile.buildArtifactPath,
39
+ };
40
+ const pipelineId = "P1";
41
+ return {
42
+ target_environment: "auto",
43
+ release_intent: releaseIntent,
44
+ pipeline_id: pipelineId,
45
+ source_profile: sourceProfile,
46
+ };
47
+ }
48
+ function validateWhcCheckHealthRequest(input) {
49
+ const parsed = requestSchema.safeParse(input);
50
+ if (!parsed.success) {
51
+ const issues = parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
52
+ throw new Error(`VALIDATION_ERROR: ${issues}`);
53
+ }
54
+ return parsed.data;
55
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateWhcDbBackupRequest = validateWhcDbBackupRequest;
4
+ const zod_1 = require("zod");
5
+ const payloadSchema = zod_1.z.object({
6
+ target_environment: zod_1.z.enum(["live", "staging"]),
7
+ output_path: zod_1.z.string().min(1).optional(),
8
+ compress: zod_1.z.boolean().default(true),
9
+ tables: zod_1.z.array(zod_1.z.string().min(1)).optional(),
10
+ });
11
+ const requestSchema = zod_1.z.object({
12
+ request_id: zod_1.z.string().min(1),
13
+ actor: zod_1.z.object({
14
+ kind: zod_1.z.literal("copilot"),
15
+ user_hint: zod_1.z.string().min(1).optional(),
16
+ }),
17
+ dry_run: zod_1.z.boolean().optional(),
18
+ confirmed: zod_1.z.boolean().optional(),
19
+ idempotency_key: zod_1.z.string().min(1),
20
+ payload: payloadSchema,
21
+ });
22
+ function validateWhcDbBackupRequest(input) {
23
+ const parsed = requestSchema.safeParse(input);
24
+ if (!parsed.success) {
25
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
26
+ throw new Error(`VALIDATION_ERROR: ${issues}`);
27
+ }
28
+ return parsed.data;
29
+ }