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.
@@ -2,103 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runDeployVerification = runDeployVerification;
4
4
  exports.runDeployRollback = runDeployRollback;
5
- async function runDeployVerification(config, request, sshClient) {
6
- const payload = request.payload;
7
- if (payload.workflow_mode !== "git_controlled") {
8
- return { ok: true, message: "Verification passed for managed_clone_sync mode" };
9
- }
10
- if (!payload.repository_root) {
11
- return { ok: false, message: "Missing repository_root for verification" };
12
- }
13
- const repoPath = shellQuote(payload.repository_root);
14
- const verifyProfile = resolveVerifyProfile(payload.source_profile.source_kind);
15
- const commandParts = [
16
- `test -d ${repoPath} && echo __PATH_OK__ || echo __PATH_MISSING__`,
17
- `if [ -f ${repoPath}/.cpanel.yml ]; then echo __CPANEL_YML_OK__; else echo __CPANEL_YML_MISSING__; fi`,
18
- ];
19
- if (verifyProfile.requireWpCliCheck) {
20
- commandParts.push(`if command -v wp >/dev/null 2>&1; then wp core is-installed --path=${repoPath} >/dev/null 2>&1 && echo __WP_OK__ || echo __WP_NOT_READY__; else echo __WPCLI_MISSING__; fi`);
21
- }
22
- const command = commandParts.join(" ; ");
23
- const target = resolveTarget(config, payload.target_environment);
24
- if (!target.ok) {
25
- return { ok: false, message: target.message };
26
- }
27
- const sshResult = await sshClient.execWithKey(target.value, command);
28
- if (!sshResult.ok) {
29
- return { ok: false, message: `SSH verify command failed: ${sshResult.message}` };
30
- }
31
- const output = sshResult.stdout;
32
- if (output.includes("__PATH_MISSING__")) {
33
- return { ok: false, message: "Deployment path does not exist after deploy" };
34
- }
35
- if (output.includes("__CPANEL_YML_MISSING__")) {
36
- return { ok: false, message: ".cpanel.yml is missing in deployment root" };
37
- }
38
- if (verifyProfile.requireWpCliCheck && output.includes("__WPCLI_MISSING__")) {
39
- return { ok: false, message: "WP-CLI is required for full_site verification but is not available" };
40
- }
41
- if (verifyProfile.requireWpCliCheck && output.includes("__WP_NOT_READY__")) {
42
- return { ok: false, message: "WordPress core is not installed/ready after deploy" };
43
- }
44
- return { ok: true, message: `Runtime verification passed (${verifyProfile.name})` };
5
+ async function runDeployVerification(_config, _request, _sshClient) {
6
+ // Verification for git_deploy is transport-level only.
7
+ return { ok: true, message: "Verification passed for git_deploy mode" };
45
8
  }
46
- async function runDeployRollback(_config, request, uapiClient) {
47
- const payload = request.payload;
48
- if (payload.workflow_mode !== "git_controlled") {
49
- return {
50
- ok: false,
51
- message: "Automatic rollback is currently supported only for git_controlled mode",
52
- };
53
- }
54
- if (!payload.repository_root) {
55
- return { ok: false, message: "Missing repository_root for rollback" };
56
- }
57
- if (!payload.rollback_branch) {
58
- return {
59
- ok: false,
60
- message: "rollback_branch is required for automatic rollback",
61
- };
62
- }
63
- const rollbackResult = await uapiClient.triggerDeployment(payload.repository_root, payload.rollback_branch);
64
- if (!rollbackResult.ok) {
65
- return {
66
- ok: false,
67
- message: `Rollback deployment failed: ${rollbackResult.message}`,
68
- };
69
- }
70
- return { ok: true, message: `Rollback deployed from branch ${payload.rollback_branch}` };
71
- }
72
- function shellQuote(value) {
73
- return `'${value.replace(/'/g, `'"'"'`)}'`;
74
- }
75
- function resolveVerifyProfile(sourceKind) {
76
- if (sourceKind === "full_site") {
77
- return { name: "wordpress", requireWpCliCheck: true };
78
- }
79
- if (sourceKind === "package_only" || sourceKind === "artifact_first") {
80
- return { name: "generic", requireWpCliCheck: false };
81
- }
82
- return { name: "generic", requireWpCliCheck: false };
83
- }
84
- function resolveTarget(config, environment) {
85
- if (environment === "live") {
86
- return { ok: true, value: config.sshTargets.prod };
87
- }
88
- const staging = config.sshTargets.staging;
89
- if (!staging) {
90
- return { ok: false, message: "Staging SSH target is not configured" };
91
- }
92
- if (!staging.privateKeyPath) {
93
- return { ok: false, message: "Staging verification requires key-based SSH auth" };
94
- }
9
+ async function runDeployRollback(_config, _request, _uapiClient) {
10
+ // Automatic rollback via UAPI is not supported on WHC Managed WordPress.
11
+ // WHC does not expose cPanel Git Version Control deployment for rollback.
95
12
  return {
96
- ok: true,
97
- value: {
98
- host: staging.host,
99
- port: staging.port,
100
- username: staging.username,
101
- privateKeyPath: staging.privateKeyPath,
102
- },
13
+ ok: false,
14
+ message: "Automatic rollback is not supported on this platform. Restore from DB backup created before deploy.",
103
15
  };
104
16
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_STATE_ROOT = void 0;
4
4
  exports.ensureWorkspaceState = ensureWorkspaceState;
5
+ exports.ensureWorkspaceBootstrap = ensureWorkspaceBootstrap;
5
6
  exports.writePipelineStartState = writePipelineStartState;
6
7
  exports.writePipelineEndState = writePipelineEndState;
7
8
  exports.resolveStateRoot = resolveStateRoot;
@@ -15,7 +16,9 @@ exports.readJsonFile = readJsonFile;
15
16
  const node_child_process_1 = require("node:child_process");
16
17
  const node_fs_1 = require("node:fs");
17
18
  const node_path_1 = require("node:path");
18
- exports.DEFAULT_STATE_ROOT = ".mcp/whc-mcp";
19
+ exports.DEFAULT_STATE_ROOT = ".whc";
20
+ const DEFAULT_BOOTSTRAP_ENV_FILE = "whc.env";
21
+ const DEFAULT_BOOTSTRAP_ENV_EXAMPLE_FILE = "whc.env.example";
19
22
  function ensureWorkspaceState(rootDir = process.cwd()) {
20
23
  const stateRoot = resolveStateRoot(rootDir);
21
24
  const stateDir = (0, node_path_1.join)(stateRoot, "state");
@@ -29,7 +32,61 @@ function ensureWorkspaceState(rootDir = process.cwd()) {
29
32
  (0, node_fs_1.mkdirSync)(reportsDir, { recursive: true });
30
33
  (0, node_fs_1.mkdirSync)(logsDir, { recursive: true });
31
34
  (0, node_fs_1.mkdirSync)(envDir, { recursive: true });
32
- ensureGitignoreRule(rootDir, ".mcp/");
35
+ ensureGitignoreRule(rootDir, ".whc/");
36
+ }
37
+ function ensureWorkspaceBootstrap(rootDir = process.cwd(), options = {}) {
38
+ const stateRootAbsolute = resolveStateRoot(rootDir);
39
+ const stateRootRelative = getRelativeStateRoot();
40
+ const stateDir = (0, node_path_1.join)(stateRootAbsolute, "state");
41
+ const releasesDir = (0, node_path_1.join)(stateDir, "releases");
42
+ const reportsDir = (0, node_path_1.join)(stateDir, "reports");
43
+ const logsDir = (0, node_path_1.join)(stateRootAbsolute, "logs");
44
+ const envDir = (0, node_path_1.join)(stateRootAbsolute, "env");
45
+ const trackedPaths = [
46
+ stateRootAbsolute,
47
+ stateDir,
48
+ releasesDir,
49
+ reportsDir,
50
+ logsDir,
51
+ envDir,
52
+ ];
53
+ const createdPaths = [];
54
+ for (const dirPath of trackedPaths) {
55
+ if (!(0, node_fs_1.existsSync)(dirPath)) {
56
+ (0, node_fs_1.mkdirSync)(dirPath, { recursive: true });
57
+ createdPaths.push(dirPath);
58
+ }
59
+ }
60
+ const gitignoreUpdated = ensureGitignoreRule(rootDir, ".whc/");
61
+ const pipelineStatusFile = getPipelineStatusFile(rootDir);
62
+ if (!(0, node_fs_1.existsSync)(pipelineStatusFile)) {
63
+ (0, node_fs_1.writeFileSync)(pipelineStatusFile, JSON.stringify({
64
+ started: false,
65
+ completed: false,
66
+ status: "idle",
67
+ next_step: "Run a write tool (deploy/setup/backup/ssh) to start tracking.",
68
+ }, null, 2) + "\n", { encoding: "utf8" });
69
+ createdPaths.push(pipelineStatusFile);
70
+ }
71
+ const envFile = (0, node_path_1.join)(envDir, DEFAULT_BOOTSTRAP_ENV_FILE);
72
+ if (!(0, node_fs_1.existsSync)(envFile)) {
73
+ (0, node_fs_1.writeFileSync)(envFile, buildBootstrapEnvTemplate(rootDir, options), { encoding: "utf8" });
74
+ createdPaths.push(envFile);
75
+ }
76
+ const envExampleFile = (0, node_path_1.join)(envDir, DEFAULT_BOOTSTRAP_ENV_EXAMPLE_FILE);
77
+ if (!(0, node_fs_1.existsSync)(envExampleFile)) {
78
+ (0, node_fs_1.writeFileSync)(envExampleFile, buildBootstrapEnvTemplate(rootDir, options), { encoding: "utf8" });
79
+ createdPaths.push(envExampleFile);
80
+ }
81
+ return {
82
+ stateRootRelative,
83
+ stateRootAbsolute,
84
+ createdPaths,
85
+ gitignoreUpdated,
86
+ pipelineStatusFile,
87
+ envFile,
88
+ envExampleFile,
89
+ };
33
90
  }
34
91
  function writePipelineStartState(input) {
35
92
  ensureWorkspaceState(input.rootDir);
@@ -112,15 +169,16 @@ function ensureGitignoreRule(rootDir, rule) {
112
169
  const gitignorePath = (0, node_path_1.join)(rootDir, ".gitignore");
113
170
  if (!(0, node_fs_1.existsSync)(gitignorePath)) {
114
171
  (0, node_fs_1.writeFileSync)(gitignorePath, `${rule}\n`, { encoding: "utf8" });
115
- return;
172
+ return true;
116
173
  }
117
174
  const content = (0, node_fs_1.readFileSync)(gitignorePath, "utf8");
118
175
  const hasRule = content.split(/\r?\n/).some((line) => line.trim() === rule);
119
176
  if (hasRule) {
120
- return;
177
+ return false;
121
178
  }
122
179
  const next = content.endsWith("\n") ? `${content}${rule}\n` : `${content}\n${rule}\n`;
123
180
  (0, node_fs_1.writeFileSync)(gitignorePath, next, { encoding: "utf8" });
181
+ return true;
124
182
  }
125
183
  function safeFileName(value) {
126
184
  return value.replace(/[^a-zA-Z0-9._-]/g, "_");
@@ -129,17 +187,59 @@ function defaultNextStep(ok) {
129
187
  if (ok) {
130
188
  return "Proceed to next pipeline step based on policy and verification scope.";
131
189
  }
132
- return "Inspect .mcp/whc-mcp/logs/flow-events.jsonl and retry after fixing the reported error.";
190
+ return "Inspect .whc/logs/flow-events.jsonl and retry after fixing the reported error.";
133
191
  }
134
192
  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;
193
+ const configuredOrDefault = getRelativeStateRoot();
137
194
  if ((0, node_path_1.isAbsolute)(configuredOrDefault)) {
138
195
  return configuredOrDefault;
139
196
  }
140
197
  const segments = configuredOrDefault.split(/[\\/]+/).filter((part) => part.length > 0);
141
198
  return (0, node_path_1.join)(rootDir, ...segments);
142
199
  }
200
+ function getRelativeStateRoot() {
201
+ const configured = (process.env.WHC_STATE_ROOT ?? "").trim();
202
+ return configured.length > 0 ? configured : exports.DEFAULT_STATE_ROOT;
203
+ }
204
+ function buildBootstrapEnvTemplate(rootDir, options) {
205
+ return [
206
+ "# Project-local WHC bootstrap env",
207
+ "# Fill the values needed for this specific deploy workspace.",
208
+ `WHC_LOCAL_PROJECT_ROOT=${options.localProjectRoot ?? rootDir}`,
209
+ `WHC_LOCAL_APP_PATH=${options.localAppPath ?? ""}`,
210
+ "WHC_WORKFLOW_MODE=",
211
+ "WHC_SOURCE_KIND=full_site",
212
+ "WHC_DEPLOY_UNIT=raw_source",
213
+ "WHC_DEFAULT_RELEASE_INTENT=deploy",
214
+ "",
215
+ "# Managed WHC mode",
216
+ "WHC_API_TOKEN=",
217
+ "WHC_USER=",
218
+ "WHC_HOST=",
219
+ "WHC_PROD_PATH=/public_html",
220
+ "WHC_PROD_SSH_HOST=",
221
+ "WHC_PROD_SSH_PORT=27",
222
+ "WHC_PROD_SSH_USERNAME=",
223
+ "WHC_PROD_SSH_PRIVATE_KEY_PATH=",
224
+ "",
225
+ "# Staging/source-driven mode",
226
+ "WHC_STAGING_DOMAIN=",
227
+ "WHC_STAGING_PATH=",
228
+ "WHC_STAGING_SSH_HOST=",
229
+ "WHC_STAGING_SSH_PORT=27",
230
+ "WHC_STAGING_SSH_USERNAME=",
231
+ "WHC_STAGING_SSH_PRIVATE_KEY_PATH=",
232
+ "WHC_STAGING_SSH_PASSWORD=",
233
+ "WHC_STAGING_SSH_HOSTKEY=",
234
+ "",
235
+ "WHC_REQUIRE_STAGING_CONFIRM=true",
236
+ "WHC_WARN_DYNAMIC_DATA_SYNC=true",
237
+ "WHC_ENFORCE_STAGING_FIRST=true",
238
+ "WHC_API_TIMEOUT_MS=15000",
239
+ "WHC_SSH_TIMEOUT_MS=10000",
240
+ "",
241
+ ].join("\n");
242
+ }
143
243
  function getPipelineStatusFile(rootDir = process.cwd()) {
144
244
  return (0, node_path_1.join)(resolveStateRoot(rootDir), "state", "pipeline-status.json");
145
245
  }
package/package.json CHANGED
@@ -1,37 +1,42 @@
1
1
  {
2
2
  "name": "devops-whc",
3
- "version": "1.0.1",
4
- "description": "WHC cPanel MCP server for deploy, verify, and rollback flows",
3
+ "version": "1.0.2",
4
+ "description": "WHC cPanel automation CLI for deploy, verify, and rollback flows",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
- "whc-mcp": "dist/index.js"
7
+ "whc": "dist/index.js"
8
8
  },
9
9
  "exports": {
10
10
  ".": "./dist/index.js"
11
11
  },
12
12
  "files": [
13
13
  "dist",
14
- "scripts/start-mcp.cjs",
14
+ "scripts/start-whc.cjs",
15
15
  "scripts/prepare-first-time.cjs",
16
16
  "README.md",
17
- "AGENT_MCP_USAGE.md",
18
- "WHC_MCP_REQUIREMENTS.md",
17
+ "AGENT_USAGE.md",
18
+ "WHC_REQUIREMENTS.md",
19
19
  "LICENSE"
20
20
  ],
21
21
  "scripts": {
22
22
  "prepack": "node ../../scripts/prepare-publish-package.cjs",
23
+ "publish:release": "npm publish",
23
24
  "pack:check": "npm pack --dry-run",
24
25
  "prehelp": "node ../../scripts/prepare-publish-package.cjs",
25
26
  "help": "node dist/index.js --help",
27
+ "prestart": "node ../../scripts/prepare-publish-package.cjs",
28
+ "start": "node scripts/start-whc.cjs",
26
29
  "preserve": "node ../../scripts/prepare-publish-package.cjs",
27
30
  "serve": "node dist/index.js --serve",
28
31
  "preprobe": "node ../../scripts/prepare-publish-package.cjs",
29
32
  "probe": "node dist/index.js --probe",
33
+ "precheck:generic": "node ../../scripts/prepare-publish-package.cjs",
34
+ "check:generic": "node dist/index.js --check-generic",
30
35
  "precheck:health": "node ../../scripts/prepare-publish-package.cjs",
31
36
  "check:health": "node dist/index.js --check-health"
32
37
  },
33
38
  "keywords": [
34
- "mcp",
39
+ "cli",
35
40
  "whc",
36
41
  "cpanel",
37
42
  "deploy"
@@ -30,7 +30,7 @@ function ensureFileIfMissing(filePath, data) {
30
30
 
31
31
  function resolveStateRoot(rootDir) {
32
32
  const configured = (process.env.WHC_STATE_ROOT || "").trim();
33
- const relative = configured.length > 0 ? configured : ".mcp/whc-mcp";
33
+ const relative = configured.length > 0 ? configured : ".whc";
34
34
  const segments = relative.split(/[\\/]+/).filter(Boolean);
35
35
  return path.join(rootDir, ...segments);
36
36
  }
@@ -51,7 +51,7 @@ function main() {
51
51
  ensureDir(logsDir);
52
52
  ensureDir(envDir);
53
53
 
54
- ensureGitignoreRule(rootDir, ".mcp/");
54
+ ensureGitignoreRule(rootDir, ".whc/");
55
55
 
56
56
  const statusFile = path.join(stateDir, "pipeline-status.json");
57
57
  ensureFileIfMissing(
@@ -68,7 +68,7 @@ function main() {
68
68
  ) + "\n",
69
69
  );
70
70
 
71
- console.log("[prepare-first-time] Initialized .mcp/whc-mcp hidden state and ensured .gitignore contains .mcp/");
71
+ console.log("[prepare-first-time] Initialized .whc hidden state and ensured .gitignore contains .whc/");
72
72
  console.log(`[prepare-first-time] State file: ${statusFile}`);
73
73
  }
74
74
 
@@ -9,8 +9,7 @@ const projectRootHint = process.env.WHC_LOCAL_PROJECT_ROOT;
9
9
  const envCandidates = [
10
10
  requestedEnvPath,
11
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,
12
+ projectRootHint ? path.join(projectRootHint, ".whc", "env", "whc.env") : undefined,
14
13
  path.join(workspaceRoot, ".env"),
15
14
  ]
16
15
  .filter(Boolean)
@@ -19,7 +18,7 @@ const envCandidates = [
19
18
  const envFilePath = envCandidates.find((p) => fs.existsSync(p));
20
19
 
21
20
  // Load .env manually before importing the compiled server
22
- // (VS Code Copilot starts the process without any env pre-loaded)
21
+ // (tool clients can start the process without any env pre-loaded)
23
22
  if (envFilePath && fs.existsSync(envFilePath)) {
24
23
  const parsed = dotenv.parse(fs.readFileSync(envFilePath));
25
24
  for (const [key, value] of Object.entries(parsed)) {
@@ -36,7 +35,7 @@ const serverEntryCandidates = [
36
35
 
37
36
  const serverEntry = serverEntryCandidates.find((p) => fs.existsSync(p));
38
37
  if (!serverEntry) {
39
- throw new Error("MCP build output not found. Run npm run build before npm run mcp:start.");
38
+ throw new Error("Build output not found. Run npm run build before npm run start.");
40
39
  }
41
40
 
42
41
  require(serverEntry);