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,132 @@
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.loadConfig = loadConfig;
7
+ const dotenv_1 = __importDefault(require("dotenv"));
8
+ const zod_1 = require("zod");
9
+ if (process.env.WHC_ENV_FILE && process.env.WHC_ENV_FILE.trim().length > 0) {
10
+ dotenv_1.default.config({ path: process.env.WHC_ENV_FILE });
11
+ }
12
+ else {
13
+ dotenv_1.default.config();
14
+ }
15
+ const envSchema = zod_1.z.object({
16
+ WHC_API_TOKEN: zod_1.z.string().min(1),
17
+ WHC_USER: zod_1.z.string().min(1),
18
+ WHC_HOST: zod_1.z.string().min(1),
19
+ WHC_WORKFLOW_MODE: zod_1.z.enum(["managed_clone_sync", "git_controlled"]).default("managed_clone_sync"),
20
+ WHC_PRIMARY_DOMAIN: zod_1.z.string().min(1).optional(),
21
+ WHC_TESTING_DOMAIN: zod_1.z.string().min(1).optional(),
22
+ WHC_STAGING_DOMAIN: zod_1.z.string().min(1).optional(),
23
+ WHC_PROD_PATH: zod_1.z.string().min(1).default("/public_html"),
24
+ WHC_STAGING_PATH: zod_1.z.string().min(1).optional(),
25
+ WHC_REQUIRE_STAGING_CONFIRM: zod_1.z
26
+ .enum(["true", "false"])
27
+ .default("true")
28
+ .transform((value) => value === "true"),
29
+ WHC_WARN_DYNAMIC_DATA_SYNC: zod_1.z
30
+ .enum(["true", "false"])
31
+ .default("true")
32
+ .transform((value) => value === "true"),
33
+ WHC_ENFORCE_STAGING_FIRST: zod_1.z
34
+ .enum(["true", "false"])
35
+ .default("true")
36
+ .transform((value) => value === "true"),
37
+ WHC_ALLOW_STAGING_GIT_CONTROLLED: zod_1.z
38
+ .enum(["true", "false"])
39
+ .default("false")
40
+ .transform((value) => value === "true"),
41
+ WHC_PROD_SSH_HOST: zod_1.z.string().min(1),
42
+ WHC_PROD_SSH_PORT: zod_1.z.coerce.number().int().positive().default(27),
43
+ WHC_PROD_SSH_USERNAME: zod_1.z.string().min(1),
44
+ WHC_PROD_SSH_PRIVATE_KEY_PATH: zod_1.z.string().min(1),
45
+ WHC_STAGING_SSH_HOST: zod_1.z.string().min(1).optional(),
46
+ WHC_STAGING_SSH_PORT: zod_1.z.coerce.number().int().positive().optional(),
47
+ WHC_STAGING_SSH_USERNAME: zod_1.z.string().min(1).optional(),
48
+ WHC_STAGING_SSH_PRIVATE_KEY_PATH: zod_1.z.string().min(1).optional(),
49
+ WHC_STAGING_SSH_PASSWORD: zod_1.z.string().min(1).optional(),
50
+ WHC_STAGING_SSH_HOSTKEY: zod_1.z.string().min(1).optional(),
51
+ WHC_LOCAL_PROJECT_ROOT: zod_1.z.string().min(1).optional(),
52
+ WHC_LOCAL_APP_PATH: zod_1.z.string().min(1).optional(),
53
+ WHC_SOURCE_KIND: zod_1.z
54
+ .enum(["full_site", "partial_content", "package_only", "artifact_first", "monorepo_slice"])
55
+ .default("full_site"),
56
+ WHC_DEPLOY_UNIT: zod_1.z.enum(["raw_source", "build_artifact", "package_bundle"]).default("raw_source"),
57
+ WHC_BUILD_COMMAND: zod_1.z.string().min(1).optional(),
58
+ WHC_BUILD_ARTIFACT_PATH: zod_1.z.string().min(1).optional(),
59
+ WHC_DEFAULT_RELEASE_INTENT: zod_1.z
60
+ .enum(["refresh", "deploy", "promote", "migrate", "recover"])
61
+ .default("deploy"),
62
+ WHC_API_TIMEOUT_MS: zod_1.z.coerce.number().int().positive().default(15000),
63
+ WHC_SSH_TIMEOUT_MS: zod_1.z.coerce.number().int().positive().default(10000),
64
+ WHC_FLOW_LOG_PATH: zod_1.z.string().min(1).optional(),
65
+ });
66
+ function loadConfig(env = process.env) {
67
+ const parsed = envSchema.safeParse(env);
68
+ if (!parsed.success) {
69
+ const issues = parsed.error.issues
70
+ .map((issue) => issue.path.join("."))
71
+ .filter((field) => field.length > 0);
72
+ throw new Error(`VALIDATION_ERROR: Missing or invalid env vars: ${issues.join(", ")}`);
73
+ }
74
+ const data = parsed.data;
75
+ const prodSshHost = data.WHC_PROD_SSH_HOST;
76
+ const prodSshPort = data.WHC_PROD_SSH_PORT;
77
+ const prodSshUsername = data.WHC_PROD_SSH_USERNAME;
78
+ const prodSshPrivateKeyPath = data.WHC_PROD_SSH_PRIVATE_KEY_PATH;
79
+ const hasStagingSsh = !!data.WHC_STAGING_SSH_HOST || !!data.WHC_STAGING_SSH_USERNAME || !!data.WHC_STAGING_SSH_PRIVATE_KEY_PATH;
80
+ return {
81
+ apiToken: data.WHC_API_TOKEN,
82
+ user: data.WHC_USER,
83
+ host: data.WHC_HOST,
84
+ apiBaseUrl: `https://${data.WHC_HOST}:2083`,
85
+ workflowMode: data.WHC_WORKFLOW_MODE,
86
+ domains: {
87
+ primary: data.WHC_PRIMARY_DOMAIN,
88
+ testing: data.WHC_TESTING_DOMAIN,
89
+ staging: data.WHC_STAGING_DOMAIN,
90
+ },
91
+ paths: {
92
+ prod: data.WHC_PROD_PATH,
93
+ staging: data.WHC_STAGING_PATH,
94
+ },
95
+ sourceProfile: {
96
+ localProjectRoot: data.WHC_LOCAL_PROJECT_ROOT,
97
+ localAppPath: data.WHC_LOCAL_APP_PATH,
98
+ sourceKind: data.WHC_SOURCE_KIND,
99
+ deployUnit: data.WHC_DEPLOY_UNIT,
100
+ buildCommand: data.WHC_BUILD_COMMAND,
101
+ buildArtifactPath: data.WHC_BUILD_ARTIFACT_PATH,
102
+ defaultReleaseIntent: data.WHC_DEFAULT_RELEASE_INTENT,
103
+ },
104
+ safety: {
105
+ requireStagingConfirm: data.WHC_REQUIRE_STAGING_CONFIRM,
106
+ warnDynamicDataSync: data.WHC_WARN_DYNAMIC_DATA_SYNC,
107
+ enforceStagingFirst: data.WHC_ENFORCE_STAGING_FIRST,
108
+ allowStagingGitControlled: data.WHC_ALLOW_STAGING_GIT_CONTROLLED,
109
+ },
110
+ sshTargets: {
111
+ prod: {
112
+ host: prodSshHost,
113
+ port: prodSshPort,
114
+ username: prodSshUsername,
115
+ privateKeyPath: prodSshPrivateKeyPath,
116
+ },
117
+ staging: hasStagingSsh
118
+ ? {
119
+ host: data.WHC_STAGING_SSH_HOST ?? prodSshHost,
120
+ port: data.WHC_STAGING_SSH_PORT ?? prodSshPort,
121
+ username: data.WHC_STAGING_SSH_USERNAME ?? prodSshUsername,
122
+ privateKeyPath: data.WHC_STAGING_SSH_PRIVATE_KEY_PATH,
123
+ password: data.WHC_STAGING_SSH_PASSWORD,
124
+ hostKey: data.WHC_STAGING_SSH_HOSTKEY,
125
+ }
126
+ : undefined,
127
+ },
128
+ apiTimeoutMs: data.WHC_API_TIMEOUT_MS,
129
+ sshTimeoutMs: data.WHC_SSH_TIMEOUT_MS,
130
+ flowLogPath: data.WHC_FLOW_LOG_PATH,
131
+ };
132
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dispatch = dispatch;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_crypto_2 = require("node:crypto");
6
+ const policy_engine_1 = require("../policy/policy-engine");
7
+ const workspace_state_1 = require("../state/workspace-state");
8
+ async function dispatch(options) {
9
+ const { config, toolDef, rawInput, validate, execute, idempotencyStore, auditLogger, actionIdFactory = node_crypto_2.randomUUID, } = options;
10
+ const startedAt = Date.now();
11
+ const startedAtIso = new Date(startedAt).toISOString();
12
+ // Step 1: Validate input schema
13
+ let request;
14
+ try {
15
+ request = validate(rawInput);
16
+ }
17
+ catch (error) {
18
+ const message = error instanceof Error ? error.message : "Unknown validation error";
19
+ return buildErrorEnvelope(toolDef.name, actionIdFactory(), startedAt, toolDef.safetyLevel, {
20
+ code: "VALIDATION_ERROR",
21
+ message,
22
+ retryable: false,
23
+ });
24
+ }
25
+ // Step 2: Policy evaluation
26
+ const policy = (0, policy_engine_1.evaluatePolicy)({
27
+ toolName: toolDef.name,
28
+ mode: toolDef.mode,
29
+ safetyLevel: toolDef.safetyLevel,
30
+ dryRun: request.dry_run,
31
+ confirmed: request.confirmed,
32
+ });
33
+ if (!policy.allowed) {
34
+ return buildErrorEnvelope(toolDef.name, actionIdFactory(), startedAt, toolDef.safetyLevel, {
35
+ code: "BUSINESS_POLICY_BLOCKED",
36
+ message: policy.reason ?? "Operation blocked by safety policy",
37
+ retryable: false,
38
+ });
39
+ }
40
+ // Step 3: Idempotency check (write operations only)
41
+ let idempotencyKey;
42
+ let payloadHash;
43
+ if (toolDef.mode === "write" && request.idempotency_key) {
44
+ idempotencyKey = request.idempotency_key;
45
+ payloadHash = hashPayload(request.payload);
46
+ const existing = idempotencyStore.get(idempotencyKey);
47
+ if (existing && existing.payloadHash === payloadHash) {
48
+ return existing.response;
49
+ }
50
+ }
51
+ // Step 4: Execute handler
52
+ let response;
53
+ if (toolDef.mode === "write" && process.env.NODE_ENV !== "test") {
54
+ (0, workspace_state_1.writePipelineStartState)({
55
+ rootDir: process.cwd(),
56
+ toolName: toolDef.name,
57
+ requestId: request.request_id,
58
+ targetEnvironment: extractStringField(request.payload, "target_environment"),
59
+ pipelineId: extractStringField(request.payload, "pipeline_id"),
60
+ releaseIntent: extractStringField(request.payload, "release_intent"),
61
+ startedAtIso,
62
+ });
63
+ }
64
+ try {
65
+ response = await execute(config, request);
66
+ }
67
+ catch (error) {
68
+ const message = error instanceof Error ? error.message : "Unknown execution error";
69
+ response = buildErrorEnvelope(toolDef.name, actionIdFactory(), startedAt, toolDef.safetyLevel, {
70
+ code: "UNKNOWN_UPSTREAM_ERROR",
71
+ message,
72
+ retryable: true,
73
+ });
74
+ }
75
+ if (toolDef.mode === "write" && process.env.NODE_ENV !== "test") {
76
+ const data = (response.data ?? null);
77
+ const processTracking = data && typeof data === "object" ? data["process_tracking"] : undefined;
78
+ const nextStep = processTracking && typeof processTracking["next_step"] === "string" ? processTracking["next_step"] : undefined;
79
+ (0, workspace_state_1.writePipelineEndState)({
80
+ rootDir: process.cwd(),
81
+ toolName: toolDef.name,
82
+ requestId: request.request_id,
83
+ targetEnvironment: extractStringField(request.payload, "target_environment"),
84
+ pipelineId: extractStringField(request.payload, "pipeline_id"),
85
+ releaseIntent: extractStringField(request.payload, "release_intent"),
86
+ startedAtIso,
87
+ }, {
88
+ ok: response.ok,
89
+ actionId: response.action_id,
90
+ nextStep,
91
+ errorCode: response.error?.code,
92
+ errorMessage: response.error?.message,
93
+ });
94
+ }
95
+ // Step 5: Audit write operations
96
+ if (toolDef.mode === "write") {
97
+ auditLogger.log({
98
+ actionId: response.action_id,
99
+ tool: toolDef.name,
100
+ status: response.ok ? "success" : "failure",
101
+ timestamp: new Date().toISOString(),
102
+ details: {
103
+ dry_run: request.dry_run ?? false,
104
+ ok: response.ok,
105
+ idempotency_key: idempotencyKey ?? "",
106
+ request_id: request.request_id,
107
+ target_environment: extractStringField(request.payload, "target_environment"),
108
+ pipeline_id: extractStringField(request.payload, "pipeline_id"),
109
+ release_intent: extractStringField(request.payload, "release_intent"),
110
+ },
111
+ });
112
+ }
113
+ // Step 6: Store idempotency record for successful writes
114
+ if (toolDef.mode === "write" && idempotencyKey && payloadHash && response.ok) {
115
+ idempotencyStore.set(idempotencyKey, {
116
+ actionId: response.action_id,
117
+ payloadHash,
118
+ response,
119
+ });
120
+ }
121
+ return response;
122
+ }
123
+ function buildErrorEnvelope(tool, actionId, startedAt, safetyLevel, error) {
124
+ return {
125
+ ok: false,
126
+ action_id: actionId,
127
+ tool,
128
+ data: null,
129
+ error,
130
+ meta: {
131
+ latency_ms: Date.now() - startedAt,
132
+ safety_level: safetyLevel,
133
+ },
134
+ };
135
+ }
136
+ function hashPayload(payload) {
137
+ return (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(payload)).digest("hex");
138
+ }
139
+ function extractStringField(payload, key) {
140
+ if (!payload || typeof payload !== "object") {
141
+ return "";
142
+ }
143
+ const value = payload[key];
144
+ return typeof value === "string" ? value : "";
145
+ }
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeWhcCheckHealth = executeWhcCheckHealth;
4
+ exports.handleWhcCheckHealth = handleWhcCheckHealth;
5
+ exports.buildDefaultWhcCheckHealthRequest = buildDefaultWhcCheckHealthRequest;
6
+ const node_crypto_1 = require("node:crypto");
7
+ const connectivity_1 = require("../probes/connectivity");
8
+ const whc_check_health_1 = require("../schemas/whc-check-health");
9
+ /**
10
+ * Pure executor: assumes request is already validated.
11
+ * Used by the dispatcher (validate → policy → idempotency → execute → audit).
12
+ */
13
+ async function executeWhcCheckHealth(config, request, deps = {}) {
14
+ const startedAt = Date.now();
15
+ const actionIdFactory = deps.actionIdFactory ?? node_crypto_1.randomUUID;
16
+ const probeRunner = deps.probeRunner ?? connectivity_1.runConnectivityProbe;
17
+ try {
18
+ const report = await probeRunner(config);
19
+ const readiness = computeReadiness(report);
20
+ const warnings = buildWarnings(report);
21
+ return {
22
+ ok: true,
23
+ action_id: actionIdFactory(),
24
+ tool: "whc_check_health",
25
+ data: {
26
+ readiness,
27
+ capabilities: report,
28
+ paths: {
29
+ prod: config.paths.prod,
30
+ staging: config.paths.staging,
31
+ },
32
+ warnings,
33
+ },
34
+ error: null,
35
+ meta: {
36
+ latency_ms: Date.now() - startedAt,
37
+ safety_level: "A",
38
+ workflow_mode: config.workflowMode,
39
+ pipeline_id: request.payload.pipeline_id,
40
+ release_intent: request.payload.release_intent,
41
+ source_profile: request.payload.source_profile,
42
+ delivery_mechanism: "read_probe",
43
+ },
44
+ };
45
+ }
46
+ catch (error) {
47
+ const message = error instanceof Error ? error.message : "Unknown health-check error";
48
+ return {
49
+ ok: false,
50
+ action_id: actionIdFactory(),
51
+ tool: "whc_check_health",
52
+ data: null,
53
+ error: {
54
+ code: "UNKNOWN_UPSTREAM_ERROR",
55
+ message,
56
+ retryable: true,
57
+ },
58
+ meta: {
59
+ latency_ms: Date.now() - startedAt,
60
+ safety_level: "A",
61
+ workflow_mode: config.workflowMode,
62
+ pipeline_id: request.payload.pipeline_id,
63
+ release_intent: request.payload.release_intent,
64
+ source_profile: request.payload.source_profile,
65
+ delivery_mechanism: "read_probe",
66
+ },
67
+ };
68
+ }
69
+ }
70
+ /**
71
+ * Standalone convenience wrapper: validates input then executes.
72
+ * Used for direct CLI invocation without the dispatcher.
73
+ */
74
+ async function handleWhcCheckHealth(config, input, deps = {}) {
75
+ const startedAt = Date.now();
76
+ const actionIdFactory = deps.actionIdFactory ?? node_crypto_1.randomUUID;
77
+ let request;
78
+ try {
79
+ request = (0, whc_check_health_1.validateWhcCheckHealthRequest)(input);
80
+ }
81
+ catch (error) {
82
+ const message = error instanceof Error ? error.message : "Unknown validation error";
83
+ return {
84
+ ok: false,
85
+ action_id: actionIdFactory(),
86
+ tool: "whc_check_health",
87
+ data: null,
88
+ error: { code: "VALIDATION_ERROR", message, retryable: false },
89
+ meta: { latency_ms: Date.now() - startedAt, safety_level: "A" },
90
+ };
91
+ }
92
+ return executeWhcCheckHealth(config, request, deps);
93
+ }
94
+ function buildDefaultWhcCheckHealthRequest(config) {
95
+ return {
96
+ request_id: (0, node_crypto_1.randomUUID)(),
97
+ actor: {
98
+ kind: "copilot",
99
+ user_hint: "local-cli",
100
+ },
101
+ payload: (0, whc_check_health_1.buildDefaultWhcCheckHealthPayload)(config),
102
+ };
103
+ }
104
+ function computeReadiness(report) {
105
+ if (report.uapi.ok && report.ssh.ok) {
106
+ return "ready";
107
+ }
108
+ if (!report.uapi.ok || !report.ssh.ok) {
109
+ return "needs_fix";
110
+ }
111
+ return "unknown";
112
+ }
113
+ function buildWarnings(report) {
114
+ const warnings = [];
115
+ if (!report.uapi.ok) {
116
+ warnings.push(`UAPI connectivity issue: ${report.uapi.message}`);
117
+ }
118
+ if (!report.ssh.ok) {
119
+ warnings.push(`Production SSH issue: ${report.ssh.message}`);
120
+ }
121
+ if (!report.sshEnvironments.staging.ok) {
122
+ warnings.push(`Staging SSH issue: ${report.sshEnvironments.staging.message}`);
123
+ }
124
+ if (!report.wpcli.prod.available) {
125
+ warnings.push(`WP-CLI unavailable on production: ${report.wpcli.prod.message}`);
126
+ }
127
+ if (!report.wpcli.staging.available) {
128
+ warnings.push(`WP-CLI unavailable on staging: ${report.wpcli.staging.message}`);
129
+ }
130
+ return warnings;
131
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeWhcDbBackup = executeWhcDbBackup;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const ssh_client_1 = require("../clients/ssh-client");
6
+ function buildDumpPath(outputPath, env, compress) {
7
+ if (outputPath)
8
+ return outputPath;
9
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
10
+ const ext = compress ? ".sql.gz" : ".sql";
11
+ return `~/db-backups/${env}-${ts}${ext}`;
12
+ }
13
+ function buildWpCliCommand(dumpPath, compress, tables) {
14
+ const tableFlag = tables && tables.length > 0 ? ` --tables=${tables.join(",")}` : "";
15
+ const compressFlag = compress ? " | gzip" : "";
16
+ if (compress) {
17
+ return `wp db export -${tableFlag} 2>/dev/null${compressFlag} > ${dumpPath}`;
18
+ }
19
+ return `wp db export ${dumpPath}${tableFlag}`;
20
+ }
21
+ async function executeWhcDbBackup(config, request, deps = {}) {
22
+ const startedAt = Date.now();
23
+ const actionIdFactory = deps.actionIdFactory ?? node_crypto_1.randomUUID;
24
+ const sshClient = deps.sshClient ?? new ssh_client_1.WhcSshClient(config);
25
+ const { target_environment, output_path, compress, tables } = request.payload;
26
+ const dumpPath = buildDumpPath(output_path, target_environment, compress);
27
+ // Dry run: preview command without executing
28
+ if (request.dry_run) {
29
+ const previewCmd = buildWpCliCommand(dumpPath, compress, tables);
30
+ return {
31
+ ok: true,
32
+ action_id: actionIdFactory(),
33
+ tool: "whc_db_backup",
34
+ data: {
35
+ target_environment,
36
+ dump_path: dumpPath,
37
+ compressed: compress,
38
+ tables,
39
+ },
40
+ error: null,
41
+ meta: {
42
+ latency_ms: Date.now() - startedAt,
43
+ safety_level: "D",
44
+ delivery_mechanism: "ssh_exec",
45
+ },
46
+ };
47
+ }
48
+ // Resolve SSH target
49
+ let execResult;
50
+ const ensureDirCmd = `mkdir -p $(dirname ${dumpPath})`;
51
+ const backupCmd = buildWpCliCommand(dumpPath, compress, tables);
52
+ const sizeCmd = `du -sh ${dumpPath} 2>/dev/null | cut -f1 || echo 'unknown'`;
53
+ // Ensure backup dir exists, then run wp db export, then get size
54
+ const fullCommand = `${ensureDirCmd} && ${backupCmd} && ${sizeCmd}`;
55
+ try {
56
+ if (target_environment === "staging") {
57
+ const stagingTarget = config.sshTargets.staging;
58
+ if (!stagingTarget) {
59
+ return buildErrorResponse(actionIdFactory(), startedAt, "BUSINESS_POLICY_BLOCKED", "Staging SSH target is not configured.");
60
+ }
61
+ if (!stagingTarget.privateKeyPath) {
62
+ return buildErrorResponse(actionIdFactory(), startedAt, "BUSINESS_POLICY_BLOCKED", "whc_db_backup requires key-based SSH auth on staging.");
63
+ }
64
+ execResult = await sshClient.execWithKey({
65
+ host: stagingTarget.host,
66
+ port: stagingTarget.port,
67
+ username: stagingTarget.username,
68
+ privateKeyPath: stagingTarget.privateKeyPath,
69
+ }, fullCommand);
70
+ }
71
+ else {
72
+ execResult = await sshClient.execWithKey(config.sshTargets.prod, fullCommand);
73
+ }
74
+ }
75
+ catch (error) {
76
+ const message = error instanceof Error ? error.message : "SSH exec failed";
77
+ return buildErrorResponse(actionIdFactory(), startedAt, "AUTH_ERROR", message);
78
+ }
79
+ if (!execResult.ok) {
80
+ return buildErrorResponse(actionIdFactory(), startedAt, "TEMPORARY_UPSTREAM_ERROR", execResult.message || execResult.stderr || "wp db export failed");
81
+ }
82
+ const sizeHint = execResult.stdout.split("\n").filter((l) => l.trim().length > 0).pop()?.trim();
83
+ return {
84
+ ok: true,
85
+ action_id: actionIdFactory(),
86
+ tool: "whc_db_backup",
87
+ data: {
88
+ target_environment,
89
+ dump_path: dumpPath,
90
+ compressed: compress,
91
+ tables,
92
+ size_hint: sizeHint,
93
+ },
94
+ error: null,
95
+ meta: {
96
+ latency_ms: Date.now() - startedAt,
97
+ safety_level: "D",
98
+ delivery_mechanism: "ssh_exec",
99
+ },
100
+ };
101
+ }
102
+ function buildErrorResponse(actionId, startedAt, code, message) {
103
+ return {
104
+ ok: false,
105
+ action_id: actionId,
106
+ tool: "whc_db_backup",
107
+ data: null,
108
+ error: { code, message, retryable: code === "TEMPORARY_UPSTREAM_ERROR" },
109
+ meta: { latency_ms: Date.now() - startedAt, safety_level: "D" },
110
+ };
111
+ }