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,201 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_STATE_ROOT = void 0;
4
+ exports.ensureWorkspaceState = ensureWorkspaceState;
5
+ exports.writePipelineStartState = writePipelineStartState;
6
+ exports.writePipelineEndState = writePipelineEndState;
7
+ exports.resolveStateRoot = resolveStateRoot;
8
+ exports.getPipelineStatusFile = getPipelineStatusFile;
9
+ exports.getReleaseManifestFile = getReleaseManifestFile;
10
+ exports.readPipelineStatus = readPipelineStatus;
11
+ exports.readReleaseManifest = readReleaseManifest;
12
+ exports.getVerifyReportFile = getVerifyReportFile;
13
+ exports.findLatestVerifyReportFile = findLatestVerifyReportFile;
14
+ exports.readJsonFile = readJsonFile;
15
+ const node_child_process_1 = require("node:child_process");
16
+ const node_fs_1 = require("node:fs");
17
+ const node_path_1 = require("node:path");
18
+ exports.DEFAULT_STATE_ROOT = ".mcp/whc-mcp";
19
+ function ensureWorkspaceState(rootDir = process.cwd()) {
20
+ const stateRoot = resolveStateRoot(rootDir);
21
+ const stateDir = (0, node_path_1.join)(stateRoot, "state");
22
+ const releasesDir = (0, node_path_1.join)(stateDir, "releases");
23
+ const reportsDir = (0, node_path_1.join)(stateDir, "reports");
24
+ const logsDir = (0, node_path_1.join)(stateRoot, "logs");
25
+ const envDir = (0, node_path_1.join)(stateRoot, "env");
26
+ (0, node_fs_1.mkdirSync)(stateRoot, { recursive: true });
27
+ (0, node_fs_1.mkdirSync)(stateDir, { recursive: true });
28
+ (0, node_fs_1.mkdirSync)(releasesDir, { recursive: true });
29
+ (0, node_fs_1.mkdirSync)(reportsDir, { recursive: true });
30
+ (0, node_fs_1.mkdirSync)(logsDir, { recursive: true });
31
+ (0, node_fs_1.mkdirSync)(envDir, { recursive: true });
32
+ ensureGitignoreRule(rootDir, ".mcp/");
33
+ }
34
+ function writePipelineStartState(input) {
35
+ ensureWorkspaceState(input.rootDir);
36
+ const stateFile = getPipelineStatusFile(input.rootDir);
37
+ const payload = {
38
+ started: true,
39
+ completed: false,
40
+ status: "in_progress",
41
+ tool: input.toolName,
42
+ request_id: input.requestId,
43
+ target_environment: input.targetEnvironment,
44
+ pipeline_id: input.pipelineId,
45
+ release_intent: input.releaseIntent,
46
+ started_at: input.startedAtIso,
47
+ next_step: "Wait for tool execution result.",
48
+ };
49
+ (0, node_fs_1.writeFileSync)(stateFile, JSON.stringify(payload, null, 2), { encoding: "utf8" });
50
+ }
51
+ function writePipelineEndState(input, result) {
52
+ ensureWorkspaceState(input.rootDir);
53
+ const stateFile = getPipelineStatusFile(input.rootDir);
54
+ const completedAtIso = new Date().toISOString();
55
+ const payload = {
56
+ started: true,
57
+ completed: true,
58
+ status: result.ok ? "success" : "failure",
59
+ tool: input.toolName,
60
+ request_id: input.requestId,
61
+ action_id: result.actionId,
62
+ target_environment: input.targetEnvironment,
63
+ pipeline_id: input.pipelineId,
64
+ release_intent: input.releaseIntent,
65
+ started_at: input.startedAtIso,
66
+ completed_at: completedAtIso,
67
+ next_step: result.nextStep ?? defaultNextStep(result.ok),
68
+ error: result.ok
69
+ ? null
70
+ : {
71
+ code: result.errorCode ?? "UNKNOWN",
72
+ message: result.errorMessage ?? "Unknown error",
73
+ },
74
+ };
75
+ (0, node_fs_1.writeFileSync)(stateFile, JSON.stringify(payload, null, 2), { encoding: "utf8" });
76
+ writeAutoManifest(input, completedAtIso, result.ok);
77
+ }
78
+ function writeAutoManifest(input, completedAtIso, ok) {
79
+ const releaseFile = getReleaseManifestFile(input.rootDir, input.requestId);
80
+ const changedFiles = collectGitChangedFiles(input.rootDir);
81
+ const payload = {
82
+ request_id: input.requestId,
83
+ tool: input.toolName,
84
+ target_environment: input.targetEnvironment,
85
+ pipeline_id: input.pipelineId,
86
+ release_intent: input.releaseIntent,
87
+ started_at: input.startedAtIso,
88
+ completed_at: completedAtIso,
89
+ status: ok ? "success" : "failure",
90
+ changed_files: changedFiles,
91
+ };
92
+ (0, node_fs_1.writeFileSync)(releaseFile, JSON.stringify(payload, null, 2), { encoding: "utf8" });
93
+ }
94
+ function collectGitChangedFiles(rootDir) {
95
+ try {
96
+ const output = (0, node_child_process_1.execSync)("git status --porcelain", { cwd: rootDir, stdio: ["ignore", "pipe", "ignore"] })
97
+ .toString("utf8")
98
+ .trim();
99
+ if (!output) {
100
+ return [];
101
+ }
102
+ return output
103
+ .split(/\r?\n/)
104
+ .map((line) => line.slice(3).trim())
105
+ .filter((path) => path.length > 0);
106
+ }
107
+ catch {
108
+ return [];
109
+ }
110
+ }
111
+ function ensureGitignoreRule(rootDir, rule) {
112
+ const gitignorePath = (0, node_path_1.join)(rootDir, ".gitignore");
113
+ if (!(0, node_fs_1.existsSync)(gitignorePath)) {
114
+ (0, node_fs_1.writeFileSync)(gitignorePath, `${rule}\n`, { encoding: "utf8" });
115
+ return;
116
+ }
117
+ const content = (0, node_fs_1.readFileSync)(gitignorePath, "utf8");
118
+ const hasRule = content.split(/\r?\n/).some((line) => line.trim() === rule);
119
+ if (hasRule) {
120
+ return;
121
+ }
122
+ const next = content.endsWith("\n") ? `${content}${rule}\n` : `${content}\n${rule}\n`;
123
+ (0, node_fs_1.writeFileSync)(gitignorePath, next, { encoding: "utf8" });
124
+ }
125
+ function safeFileName(value) {
126
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
127
+ }
128
+ function defaultNextStep(ok) {
129
+ if (ok) {
130
+ return "Proceed to next pipeline step based on policy and verification scope.";
131
+ }
132
+ return "Inspect .mcp/whc-mcp/logs/flow-events.jsonl and retry after fixing the reported error.";
133
+ }
134
+ function resolveStateRoot(rootDir = process.cwd()) {
135
+ const configured = (process.env.WHC_STATE_ROOT ?? "").trim();
136
+ const configuredOrDefault = configured.length > 0 ? configured : exports.DEFAULT_STATE_ROOT;
137
+ if ((0, node_path_1.isAbsolute)(configuredOrDefault)) {
138
+ return configuredOrDefault;
139
+ }
140
+ const segments = configuredOrDefault.split(/[\\/]+/).filter((part) => part.length > 0);
141
+ return (0, node_path_1.join)(rootDir, ...segments);
142
+ }
143
+ function getPipelineStatusFile(rootDir = process.cwd()) {
144
+ return (0, node_path_1.join)(resolveStateRoot(rootDir), "state", "pipeline-status.json");
145
+ }
146
+ function getReleaseManifestFile(rootDir, requestId) {
147
+ return (0, node_path_1.join)(resolveStateRoot(rootDir), "state", "releases", `${safeFileName(requestId)}.auto-manifest.json`);
148
+ }
149
+ function readPipelineStatus(rootDir = process.cwd()) {
150
+ const statusFile = getPipelineStatusFile(rootDir);
151
+ if (!(0, node_fs_1.existsSync)(statusFile)) {
152
+ return null;
153
+ }
154
+ try {
155
+ const raw = (0, node_fs_1.readFileSync)(statusFile, "utf8");
156
+ return JSON.parse(raw);
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ }
162
+ function readReleaseManifest(rootDir, requestId) {
163
+ const file = getReleaseManifestFile(rootDir, requestId);
164
+ if (!(0, node_fs_1.existsSync)(file)) {
165
+ return null;
166
+ }
167
+ try {
168
+ return JSON.parse((0, node_fs_1.readFileSync)(file, "utf8"));
169
+ }
170
+ catch {
171
+ return null;
172
+ }
173
+ }
174
+ function getVerifyReportFile(rootDir, releaseId) {
175
+ return (0, node_path_1.join)(resolveStateRoot(rootDir), "state", "reports", `${safeFileName(releaseId)}.verify-report.json`);
176
+ }
177
+ function findLatestVerifyReportFile(rootDir = process.cwd()) {
178
+ const reportsDir = (0, node_path_1.join)(resolveStateRoot(rootDir), "state", "reports");
179
+ if (!(0, node_fs_1.existsSync)(reportsDir)) {
180
+ return null;
181
+ }
182
+ const files = (0, node_fs_1.readdirSync)(reportsDir)
183
+ .filter((name) => name.endsWith(".verify-report.json"))
184
+ .map((name) => {
185
+ const fullPath = (0, node_path_1.join)(reportsDir, name);
186
+ return { fullPath, mtime: (0, node_fs_1.statSync)(fullPath).mtimeMs };
187
+ })
188
+ .sort((a, b) => b.mtime - a.mtime);
189
+ return files[0]?.fullPath ?? null;
190
+ }
191
+ function readJsonFile(filePath) {
192
+ if (!(0, node_fs_1.existsSync)(filePath)) {
193
+ return null;
194
+ }
195
+ try {
196
+ return JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf8"));
197
+ }
198
+ catch {
199
+ return null;
200
+ }
201
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "devops-whc",
3
+ "version": "1.0.1",
4
+ "description": "WHC cPanel MCP server for deploy, verify, and rollback flows",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "whc-mcp": "dist/index.js"
8
+ },
9
+ "exports": {
10
+ ".": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "scripts/start-mcp.cjs",
15
+ "scripts/prepare-first-time.cjs",
16
+ "README.md",
17
+ "AGENT_MCP_USAGE.md",
18
+ "WHC_MCP_REQUIREMENTS.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "prepack": "node ../../scripts/prepare-publish-package.cjs",
23
+ "pack:check": "npm pack --dry-run",
24
+ "prehelp": "node ../../scripts/prepare-publish-package.cjs",
25
+ "help": "node dist/index.js --help",
26
+ "preserve": "node ../../scripts/prepare-publish-package.cjs",
27
+ "serve": "node dist/index.js --serve",
28
+ "preprobe": "node ../../scripts/prepare-publish-package.cjs",
29
+ "probe": "node dist/index.js --probe",
30
+ "precheck:health": "node ../../scripts/prepare-publish-package.cjs",
31
+ "check:health": "node dist/index.js --check-health"
32
+ },
33
+ "keywords": [
34
+ "mcp",
35
+ "whc",
36
+ "cpanel",
37
+ "deploy"
38
+ ],
39
+ "author": "",
40
+ "license": "ISC",
41
+ "type": "commonjs",
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.29.0",
44
+ "dotenv": "^17.4.2",
45
+ "ssh2": "^1.17.0",
46
+ "zod": "^4.4.3"
47
+ }
48
+ }
@@ -0,0 +1,75 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ function ensureDir(p) {
5
+ fs.mkdirSync(p, { recursive: true });
6
+ }
7
+
8
+ function ensureGitignoreRule(rootDir, rule) {
9
+ const gitignorePath = path.join(rootDir, ".gitignore");
10
+ if (!fs.existsSync(gitignorePath)) {
11
+ fs.writeFileSync(gitignorePath, `${rule}\n`, "utf8");
12
+ return;
13
+ }
14
+
15
+ const content = fs.readFileSync(gitignorePath, "utf8");
16
+ const hasRule = content.split(/\r?\n/).some((line) => line.trim() === rule);
17
+ if (hasRule) {
18
+ return;
19
+ }
20
+
21
+ const next = content.endsWith("\n") ? `${content}${rule}\n` : `${content}\n${rule}\n`;
22
+ fs.writeFileSync(gitignorePath, next, "utf8");
23
+ }
24
+
25
+ function ensureFileIfMissing(filePath, data) {
26
+ if (!fs.existsSync(filePath)) {
27
+ fs.writeFileSync(filePath, data, "utf8");
28
+ }
29
+ }
30
+
31
+ function resolveStateRoot(rootDir) {
32
+ const configured = (process.env.WHC_STATE_ROOT || "").trim();
33
+ const relative = configured.length > 0 ? configured : ".mcp/whc-mcp";
34
+ const segments = relative.split(/[\\/]+/).filter(Boolean);
35
+ return path.join(rootDir, ...segments);
36
+ }
37
+
38
+ function main() {
39
+ const rootDir = path.resolve(__dirname, "..");
40
+ const stateRoot = resolveStateRoot(rootDir);
41
+ const stateDir = path.join(stateRoot, "state");
42
+ const releasesDir = path.join(stateDir, "releases");
43
+ const reportsDir = path.join(stateDir, "reports");
44
+ const logsDir = path.join(stateRoot, "logs");
45
+ const envDir = path.join(stateRoot, "env");
46
+
47
+ ensureDir(stateRoot);
48
+ ensureDir(stateDir);
49
+ ensureDir(releasesDir);
50
+ ensureDir(reportsDir);
51
+ ensureDir(logsDir);
52
+ ensureDir(envDir);
53
+
54
+ ensureGitignoreRule(rootDir, ".mcp/");
55
+
56
+ const statusFile = path.join(stateDir, "pipeline-status.json");
57
+ ensureFileIfMissing(
58
+ statusFile,
59
+ JSON.stringify(
60
+ {
61
+ started: false,
62
+ completed: false,
63
+ status: "idle",
64
+ next_step: "Run a write tool (deploy/setup/backup/ssh) to start tracking.",
65
+ },
66
+ null,
67
+ 2,
68
+ ) + "\n",
69
+ );
70
+
71
+ console.log("[prepare-first-time] Initialized .mcp/whc-mcp hidden state and ensured .gitignore contains .mcp/");
72
+ console.log(`[prepare-first-time] State file: ${statusFile}`);
73
+ }
74
+
75
+ main();
@@ -0,0 +1,42 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const dotenv = require("dotenv");
4
+
5
+ const workspaceRoot = path.resolve(__dirname, "..");
6
+ const requestedEnvPath = process.env.WHC_ENV_FILE;
7
+ const projectRootHint = process.env.WHC_LOCAL_PROJECT_ROOT;
8
+
9
+ const envCandidates = [
10
+ requestedEnvPath,
11
+ projectRootHint ? path.join(projectRootHint, ".vscode", "whc.env") : undefined,
12
+ projectRootHint ? path.join(projectRootHint, ".mcp", "whc-mcp", "env", "whc.env") : undefined,
13
+ projectRootHint ? path.join(projectRootHint, ".mcp", "whc.env") : undefined,
14
+ path.join(workspaceRoot, ".env"),
15
+ ]
16
+ .filter(Boolean)
17
+ .map((p) => path.resolve(p));
18
+
19
+ const envFilePath = envCandidates.find((p) => fs.existsSync(p));
20
+
21
+ // Load .env manually before importing the compiled server
22
+ // (VS Code Copilot starts the process without any env pre-loaded)
23
+ if (envFilePath && fs.existsSync(envFilePath)) {
24
+ const parsed = dotenv.parse(fs.readFileSync(envFilePath));
25
+ for (const [key, value] of Object.entries(parsed)) {
26
+ if (!process.env[key]) {
27
+ process.env[key] = value;
28
+ }
29
+ }
30
+ }
31
+
32
+ const serverEntryCandidates = [
33
+ path.join(workspaceRoot, "dist", "server-entry.js"),
34
+ path.join(workspaceRoot, "dist", "src", "server-entry.js"),
35
+ ];
36
+
37
+ const serverEntry = serverEntryCandidates.find((p) => fs.existsSync(p));
38
+ if (!serverEntry) {
39
+ throw new Error("MCP build output not found. Run npm run build before npm run mcp:start.");
40
+ }
41
+
42
+ require(serverEntry);