hostinger-api-mcp 0.1.37 → 0.1.41
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/.github/workflows/build-release.yaml +1 -0
- package/README.md +179 -740
- package/build.js +7 -2
- package/package.json +11 -5
- package/src/core/runtime.js +2106 -0
- package/src/core/runtime.ts +2137 -0
- package/{server.ts → src/core/tools/all.js} +238 -2267
- package/{server.js → src/core/tools/all.ts} +248 -2236
- package/src/core/tools/billing.js +163 -0
- package/src/core/tools/billing.ts +174 -0
- package/src/core/tools/dns.js +343 -0
- package/src/core/tools/dns.ts +354 -0
- package/src/core/tools/domains.js +533 -0
- package/src/core/tools/domains.ts +544 -0
- package/src/core/tools/hosting.js +448 -0
- package/src/core/tools/hosting.ts +459 -0
- package/src/core/tools/reach.js +349 -0
- package/src/core/tools/reach.ts +360 -0
- package/src/core/tools/vps.js +1816 -0
- package/src/core/tools/vps.ts +1827 -0
- package/src/servers/all.js +6 -0
- package/src/servers/all.ts +6 -0
- package/src/servers/billing.js +6 -0
- package/src/servers/billing.ts +6 -0
- package/src/servers/dns.js +6 -0
- package/src/servers/dns.ts +6 -0
- package/src/servers/domains.js +6 -0
- package/src/servers/domains.ts +6 -0
- package/src/servers/hosting.js +6 -0
- package/src/servers/hosting.ts +6 -0
- package/src/servers/reach.js +6 -0
- package/src/servers/reach.ts +6 -0
- package/src/servers/vps.js +6 -0
- package/src/servers/vps.ts +6 -0
- package/tsconfig.json +3 -2
|
@@ -0,0 +1,2106 @@
|
|
|
1
|
+
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import minimist from 'minimist';
|
|
6
|
+
import cors from "cors";
|
|
7
|
+
import express from "express";
|
|
8
|
+
import axios from "axios";
|
|
9
|
+
import { config as dotenvConfig } from "dotenv";
|
|
10
|
+
import {
|
|
11
|
+
ListToolsRequestSchema,
|
|
12
|
+
CallToolRequestSchema,
|
|
13
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import * as tus from "tus-js-client";
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
|
|
18
|
+
// Load environment variables
|
|
19
|
+
dotenvConfig();
|
|
20
|
+
|
|
21
|
+
const SECURITY_SCHEMES = {
|
|
22
|
+
"apiToken": {
|
|
23
|
+
"type": "http",
|
|
24
|
+
"description": "API Token authentication",
|
|
25
|
+
"scheme": "bearer"
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* MCP Server for Hostinger API
|
|
31
|
+
* Generated from OpenAPI spec version 0.11.7
|
|
32
|
+
*/
|
|
33
|
+
class MCPServer {
|
|
34
|
+
constructor({ name, version, tools }) {
|
|
35
|
+
// Initialize class properties
|
|
36
|
+
this.name = name;
|
|
37
|
+
this.version = version;
|
|
38
|
+
this.toolList = tools;
|
|
39
|
+
this.server = null;
|
|
40
|
+
this.tools = new Map();
|
|
41
|
+
this.debug = process.env.DEBUG === "true";
|
|
42
|
+
this.baseUrl = process.env.API_BASE_URL || "https://developers.hostinger.com";
|
|
43
|
+
this.headers = this.parseHeaders(process.env.API_HEADERS || "");
|
|
44
|
+
|
|
45
|
+
// Initialize tools map - do this before creating server
|
|
46
|
+
this.initializeTools();
|
|
47
|
+
|
|
48
|
+
// Create MCP server with correct capabilities
|
|
49
|
+
this.server = new Server(
|
|
50
|
+
{
|
|
51
|
+
name: this.name,
|
|
52
|
+
version: this.version,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
capabilities: {
|
|
56
|
+
tools: {}, // Enable tools capability
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Set up request handlers - don't log here
|
|
62
|
+
this.setupHandlers();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse headers from string
|
|
67
|
+
*/
|
|
68
|
+
parseHeaders(headerStr) {
|
|
69
|
+
const headers = {};
|
|
70
|
+
if (headerStr) {
|
|
71
|
+
headerStr.split(",").forEach((header) => {
|
|
72
|
+
const [key, value] = header.split(":");
|
|
73
|
+
if (key && value) headers[key.trim()] = value.trim();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
headers['User-Agent'] = `hostinger-mcp-server/${this.version}`;
|
|
78
|
+
|
|
79
|
+
return headers;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Initialize tools map from OpenAPI spec
|
|
84
|
+
* This runs before the server is connected, so don't log here
|
|
85
|
+
*/
|
|
86
|
+
initializeTools() {
|
|
87
|
+
// Initialize each tool in the tools map
|
|
88
|
+
for (const tool of this.toolList) {
|
|
89
|
+
this.tools.set(tool.name, {
|
|
90
|
+
name: tool.name,
|
|
91
|
+
description: tool.description,
|
|
92
|
+
inputSchema: tool.inputSchema,
|
|
93
|
+
// Don't include security at the tool level
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Don't log here, we're not connected yet
|
|
98
|
+
console.error(`Initialized ${this.tools.size} tools`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Set up request handlers
|
|
103
|
+
*/
|
|
104
|
+
setupHandlers() {
|
|
105
|
+
// Handle tool listing requests
|
|
106
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
107
|
+
this.log('debug', "Handling ListTools request");
|
|
108
|
+
// Return tools in the format expected by MCP SDK
|
|
109
|
+
return {
|
|
110
|
+
tools: Array.from(this.tools.entries()).map(([id, tool]) => ({
|
|
111
|
+
id,
|
|
112
|
+
...tool,
|
|
113
|
+
})),
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Handle tool execution requests
|
|
118
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
119
|
+
const { id, name, arguments: params } = request.params;
|
|
120
|
+
this.log('debug', "Handling CallTool request", { id, name, params });
|
|
121
|
+
|
|
122
|
+
let toolName;
|
|
123
|
+
let toolDetails;
|
|
124
|
+
|
|
125
|
+
// Find the requested tool
|
|
126
|
+
for (const [tid, tool] of this.tools.entries()) {
|
|
127
|
+
if (tool.name === name) {
|
|
128
|
+
toolName = name;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!toolName) {
|
|
134
|
+
throw new Error(`Tool not found: ${name}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
toolDetails = this.toolList.find(t => t.name === toolName);
|
|
138
|
+
if (!toolDetails) {
|
|
139
|
+
throw new Error(`Tool details not found for ID: ${toolName}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
this.log('info', `Executing tool: ${toolName}`);
|
|
144
|
+
|
|
145
|
+
let result;
|
|
146
|
+
|
|
147
|
+
if (toolDetails.custom) {
|
|
148
|
+
result = await this.executeCustomTool(toolDetails, params || {});
|
|
149
|
+
} else {
|
|
150
|
+
result = await this.executeApiCall(toolDetails, params || {});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Return the result in the correct MCP format
|
|
154
|
+
return {
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: "text",
|
|
158
|
+
text: JSON.stringify(result)
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
165
|
+
const response = error.response;
|
|
166
|
+
this.log('error', `Error executing tool ${name}: ${errorMessage}`);
|
|
167
|
+
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async executeCustomTool(tool, params) {
|
|
174
|
+
switch (tool.name) {
|
|
175
|
+
case 'hosting_importWordpressWebsite':
|
|
176
|
+
return await this.handleWordpressWebsiteImport(params);
|
|
177
|
+
case 'hosting_deployWordpressPlugin':
|
|
178
|
+
return await this.handleWordpressPluginDeploy(params);
|
|
179
|
+
case 'hosting_deployWordpressTheme':
|
|
180
|
+
return await this.handleWordpressThemeDeploy(params);
|
|
181
|
+
case 'hosting_deployJsApplication':
|
|
182
|
+
return await this.handleJavascriptApplicationDeploy(params);
|
|
183
|
+
case 'hosting_deployStaticWebsite':
|
|
184
|
+
return await this.handleStaticWebsiteDeploy(params);
|
|
185
|
+
case 'hosting_listJsDeployments':
|
|
186
|
+
return await this.handleListJavascriptDeployments(params);
|
|
187
|
+
case 'hosting_showJsDeploymentLogs':
|
|
188
|
+
return await this.handleShowJsDeploymentLogs(params);
|
|
189
|
+
default:
|
|
190
|
+
throw new Error(`Unknown custom tool: ${tool.name}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
normalizePath(pathString) {
|
|
195
|
+
return pathString.replace(/\\/g, '/');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async resolveUsername(domain) {
|
|
199
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
200
|
+
const url = new URL(`api/hosting/v1/websites?domain=${encodeURIComponent(domain)}`, baseUrl).toString();
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
|
|
204
|
+
if (!bearerToken) {
|
|
205
|
+
throw new Error('API_TOKEN environment variable not found');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const config = {
|
|
209
|
+
method: 'get',
|
|
210
|
+
url,
|
|
211
|
+
headers: {
|
|
212
|
+
...this.headers,
|
|
213
|
+
'Authorization': `Bearer ${bearerToken}`
|
|
214
|
+
},
|
|
215
|
+
timeout: 60000, // 60s
|
|
216
|
+
validateStatus: function (status) {
|
|
217
|
+
return status < 500;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const response = await axios(config);
|
|
222
|
+
|
|
223
|
+
if (response.status !== 200) {
|
|
224
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const websites = response.data?.data;
|
|
228
|
+
if (!websites || websites.length === 0) {
|
|
229
|
+
throw new Error(`No website found for domain: ${domain}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const username = websites[0].username;
|
|
233
|
+
if (!username) {
|
|
234
|
+
throw new Error(`Username not found in website data for domain: ${domain}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.log('info', `Resolved username: ${username} for domain: ${domain}`);
|
|
238
|
+
return username;
|
|
239
|
+
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
242
|
+
this.log('error', `Failed to resolve username for domain ${domain}: ${errorMessage}`);
|
|
243
|
+
|
|
244
|
+
if (axios.isAxiosError(error)) {
|
|
245
|
+
const responseData = error.response?.data;
|
|
246
|
+
const responseStatus = error.response?.status;
|
|
247
|
+
this.log('error', 'API Error Details:', {
|
|
248
|
+
status: responseStatus,
|
|
249
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async fetchUploadCredentials(username, domain) {
|
|
258
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
259
|
+
const url = new URL('api/hosting/v1/files/upload-urls', baseUrl).toString();
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
|
|
263
|
+
if (!bearerToken) {
|
|
264
|
+
throw new Error('API_TOKEN environment variable not found');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const config = {
|
|
268
|
+
method: 'post',
|
|
269
|
+
url,
|
|
270
|
+
headers: {
|
|
271
|
+
...this.headers,
|
|
272
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
273
|
+
'Content-Type': 'application/json'
|
|
274
|
+
},
|
|
275
|
+
data: {
|
|
276
|
+
username,
|
|
277
|
+
domain
|
|
278
|
+
},
|
|
279
|
+
timeout: 60000, // 60s
|
|
280
|
+
validateStatus: function (status) {
|
|
281
|
+
return status < 500;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const response = await axios(config);
|
|
286
|
+
|
|
287
|
+
if (response.status !== 200) {
|
|
288
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return response.data;
|
|
292
|
+
|
|
293
|
+
} catch (error) {
|
|
294
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
295
|
+
this.log('error', `Failed to fetch upload credentials: ${errorMessage}`);
|
|
296
|
+
|
|
297
|
+
if (axios.isAxiosError(error)) {
|
|
298
|
+
const responseData = error.response?.data;
|
|
299
|
+
const responseStatus = error.response?.status;
|
|
300
|
+
this.log('error', 'API Error Details:', {
|
|
301
|
+
status: responseStatus,
|
|
302
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async uploadFile(filePath, relativePath, uploadUrl, authRestToken, authToken) {
|
|
311
|
+
return new Promise(async (resolve, reject) => {
|
|
312
|
+
try {
|
|
313
|
+
const stats = fs.statSync(filePath);
|
|
314
|
+
const fileStream = fs.createReadStream(filePath);
|
|
315
|
+
|
|
316
|
+
const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
|
|
317
|
+
const normalizedPath = this.normalizePath(relativePath);
|
|
318
|
+
const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
|
|
319
|
+
|
|
320
|
+
const requestHeaders = {
|
|
321
|
+
'X-Auth': authToken,
|
|
322
|
+
'X-Auth-Rest': authRestToken,
|
|
323
|
+
'upload-length': stats.size.toString(),
|
|
324
|
+
'upload-offset': '0'
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
this.log('debug', `Making pre-upload POST request to ${uploadUrlWithFile}`);
|
|
329
|
+
await axios.post(uploadUrlWithFile, '', {
|
|
330
|
+
headers: requestHeaders,
|
|
331
|
+
timeout: 60000, // 60s
|
|
332
|
+
validateStatus: function (status) {
|
|
333
|
+
return status == 201;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
338
|
+
|
|
339
|
+
if (axios.isAxiosError(error)) {
|
|
340
|
+
const responseData = error.response?.data;
|
|
341
|
+
const responseStatus = error.response?.status;
|
|
342
|
+
const responseHeaders = error.response?.headers;
|
|
343
|
+
const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
|
|
344
|
+
|
|
345
|
+
this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
|
|
346
|
+
status: responseStatus,
|
|
347
|
+
headers: responseHeaders,
|
|
348
|
+
data: responseText,
|
|
349
|
+
message: errorMessage
|
|
350
|
+
});
|
|
351
|
+
reject(new Error(`Pre-upload request failed: ${errorMessage}`));
|
|
352
|
+
return;
|
|
353
|
+
} else {
|
|
354
|
+
this.log('error', `Pre-upload POST request failed: ${errorMessage}`);
|
|
355
|
+
reject(new Error(`Pre-upload request failed: ${errorMessage}`));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const upload = new tus.Upload(fileStream, {
|
|
361
|
+
uploadUrl: uploadUrlWithFile,
|
|
362
|
+
retryDelays: [1000, 2000, 4000, 8000, 16000, 20000],
|
|
363
|
+
uploadDataDuringCreation: false,
|
|
364
|
+
parallelUploads: 1,
|
|
365
|
+
chunkSize: 10485760,
|
|
366
|
+
headers: requestHeaders,
|
|
367
|
+
removeFingerprintOnSuccess: true,
|
|
368
|
+
uploadSize: stats.size,
|
|
369
|
+
metadata: {
|
|
370
|
+
filename: path.basename(relativePath)
|
|
371
|
+
},
|
|
372
|
+
onError: (error) => {
|
|
373
|
+
this.log('error', `TUS upload error for ${relativePath}`, { error: error.message });
|
|
374
|
+
reject(new Error(`Upload failed: ${error.message}`));
|
|
375
|
+
},
|
|
376
|
+
onSuccess: () => {
|
|
377
|
+
this.log('info', `TUS upload completed for ${relativePath}`, { url: upload.url });
|
|
378
|
+
resolve({
|
|
379
|
+
url: upload.url,
|
|
380
|
+
filename: relativePath
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
upload.start();
|
|
386
|
+
|
|
387
|
+
} catch (error) {
|
|
388
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
389
|
+
this.log('error', `Error preparing upload for ${filePath}`, { error: errorMessage });
|
|
390
|
+
reject(new Error(`Failed to prepare upload: ${errorMessage}`));
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
hosting_importWordpressWebsite_validateArchiveFormat(filePath) {
|
|
396
|
+
const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
|
|
397
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
398
|
+
|
|
399
|
+
for (const ext of validExtensions) {
|
|
400
|
+
if (fileName.endsWith(`.${ext}`)) {
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
hosting_importWordpressWebsite_validateRequiredParams(params) {
|
|
409
|
+
const { domain, archivePath, databaseDump } = params;
|
|
410
|
+
|
|
411
|
+
if (!domain || typeof domain !== 'string') {
|
|
412
|
+
throw new Error('domain is required and must be a string');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (!archivePath || typeof archivePath !== 'string') {
|
|
416
|
+
throw new Error('archivePath is required and must be a string');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (!databaseDump || typeof databaseDump !== 'string') {
|
|
420
|
+
throw new Error('databaseDump is required and must be a string');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
hosting_importWordpressWebsite_validateArchiveFile(archivePath) {
|
|
425
|
+
if (!fs.existsSync(archivePath)) {
|
|
426
|
+
throw new Error(`Archive file not found: ${archivePath}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const archiveStats = fs.statSync(archivePath);
|
|
430
|
+
if (!archiveStats.isFile()) {
|
|
431
|
+
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (!this.hosting_importWordpressWebsite_validateArchiveFormat(archivePath)) {
|
|
435
|
+
throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
hosting_importWordpressWebsite_validateDatabaseFile(databaseDump) {
|
|
440
|
+
if (!fs.existsSync(databaseDump)) {
|
|
441
|
+
throw new Error(`Database dump file not found: ${databaseDump}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const dbStats = fs.statSync(databaseDump);
|
|
445
|
+
if (!dbStats.isFile()) {
|
|
446
|
+
throw new Error(`Database dump path is not a file: ${databaseDump}`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (!databaseDump.toLowerCase().endsWith('.sql')) {
|
|
450
|
+
throw new Error('Database dump must be a .sql file');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async hosting_importWordpressWebsite_checkWebsiteIsEmpty(username, domain) {
|
|
455
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
456
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/domains/${domain}/is-empty`, baseUrl).toString();
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
|
|
460
|
+
if (!bearerToken) {
|
|
461
|
+
throw new Error('API_TOKEN environment variable not found');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const config = {
|
|
465
|
+
method: 'get',
|
|
466
|
+
url,
|
|
467
|
+
headers: {
|
|
468
|
+
...this.headers,
|
|
469
|
+
'Authorization': `Bearer ${bearerToken}`
|
|
470
|
+
},
|
|
471
|
+
timeout: 60000, // 60s
|
|
472
|
+
validateStatus: function (status) {
|
|
473
|
+
return status < 500;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const response = await axios(config);
|
|
478
|
+
|
|
479
|
+
if (response.status !== 200) {
|
|
480
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const { is_empty } = response.data;
|
|
484
|
+
|
|
485
|
+
if (!is_empty) {
|
|
486
|
+
throw new Error('Website is not empty. WordPress import can only be performed on empty sites. Please visit hPanel (https://hpanel.hostinger.com) and remove all existing files from the website before attempting to import.');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
this.log('info', `Website ${domain} is empty, proceeding with import`);
|
|
490
|
+
return true;
|
|
491
|
+
|
|
492
|
+
} catch (error) {
|
|
493
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
494
|
+
this.log('error', `Failed to check if website is empty: ${errorMessage}`);
|
|
495
|
+
|
|
496
|
+
if (axios.isAxiosError(error)) {
|
|
497
|
+
const responseData = error.response?.data;
|
|
498
|
+
const responseStatus = error.response?.status;
|
|
499
|
+
this.log('error', 'API Error Details:', {
|
|
500
|
+
status: responseStatus,
|
|
501
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async hosting_importWordpressWebsite_extractFiles(username, domain, archivePath, databaseDump) {
|
|
510
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
511
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/import`, baseUrl).toString();
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
|
|
515
|
+
if (!bearerToken) {
|
|
516
|
+
throw new Error('API_TOKEN environment variable not found');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const config = {
|
|
520
|
+
method: 'post',
|
|
521
|
+
url,
|
|
522
|
+
headers: {
|
|
523
|
+
...this.headers,
|
|
524
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
525
|
+
'Content-Type': 'application/json'
|
|
526
|
+
},
|
|
527
|
+
data: {
|
|
528
|
+
archive_path: path.basename(archivePath),
|
|
529
|
+
sql_path: path.basename(databaseDump)
|
|
530
|
+
},
|
|
531
|
+
timeout: 60000, // 60s
|
|
532
|
+
validateStatus: function (status) {
|
|
533
|
+
return status < 500;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const response = await axios(config);
|
|
538
|
+
|
|
539
|
+
this.log('info', `Successfully triggered file extraction for ${domain}`);
|
|
540
|
+
return true;
|
|
541
|
+
|
|
542
|
+
} catch (error) {
|
|
543
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
544
|
+
this.log('error', `Failed to trigger file extraction: ${errorMessage}`);
|
|
545
|
+
|
|
546
|
+
if (axios.isAxiosError(error)) {
|
|
547
|
+
const responseData = error.response?.data;
|
|
548
|
+
const responseStatus = error.response?.status;
|
|
549
|
+
this.log('error', 'API Error Details:', {
|
|
550
|
+
status: responseStatus,
|
|
551
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async handleWordpressWebsiteImport(params) {
|
|
560
|
+
const { domain, archivePath, databaseDump } = params;
|
|
561
|
+
|
|
562
|
+
this.hosting_importWordpressWebsite_validateRequiredParams(params);
|
|
563
|
+
this.hosting_importWordpressWebsite_validateArchiveFile(archivePath);
|
|
564
|
+
this.hosting_importWordpressWebsite_validateDatabaseFile(databaseDump);
|
|
565
|
+
|
|
566
|
+
// Auto-resolve username from domain
|
|
567
|
+
this.log('info', `Resolving username from domain: ${domain}`);
|
|
568
|
+
const username = await this.resolveUsername(domain);
|
|
569
|
+
|
|
570
|
+
await this.hosting_importWordpressWebsite_checkWebsiteIsEmpty(username, domain);
|
|
571
|
+
|
|
572
|
+
const filesToUpload = [{
|
|
573
|
+
absolutePath: archivePath,
|
|
574
|
+
relativePath: path.basename(archivePath),
|
|
575
|
+
type: 'archive'
|
|
576
|
+
}, {
|
|
577
|
+
absolutePath: databaseDump,
|
|
578
|
+
relativePath: path.basename(databaseDump),
|
|
579
|
+
type: 'database'
|
|
580
|
+
}];
|
|
581
|
+
|
|
582
|
+
let uploadCredentials;
|
|
583
|
+
try {
|
|
584
|
+
uploadCredentials = await this.fetchUploadCredentials(username, domain);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
587
|
+
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
|
|
591
|
+
|
|
592
|
+
if (!uploadUrl || !authToken || !authRestToken) {
|
|
593
|
+
throw new Error('Invalid upload credentials received from API');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
this.log('info', `Starting website archive import to ${uploadUrl}`);
|
|
597
|
+
|
|
598
|
+
const results = [];
|
|
599
|
+
let successCount = 0;
|
|
600
|
+
let failureCount = 0;
|
|
601
|
+
|
|
602
|
+
for (const fileInfo of filesToUpload) {
|
|
603
|
+
try {
|
|
604
|
+
this.log('info', `Uploading ${fileInfo.type}: ${fileInfo.absolutePath}`);
|
|
605
|
+
|
|
606
|
+
const stats = fs.statSync(fileInfo.absolutePath);
|
|
607
|
+
const uploadResult = await this.uploadFile(
|
|
608
|
+
fileInfo.absolutePath,
|
|
609
|
+
fileInfo.relativePath,
|
|
610
|
+
uploadUrl,
|
|
611
|
+
authRestToken,
|
|
612
|
+
authToken
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
results.push({
|
|
616
|
+
file: fileInfo.absolutePath,
|
|
617
|
+
remotePath: fileInfo.relativePath,
|
|
618
|
+
type: fileInfo.type,
|
|
619
|
+
status: 'success',
|
|
620
|
+
uploadUrl: uploadResult.url,
|
|
621
|
+
size: stats.size
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
successCount++;
|
|
625
|
+
this.log('info', `Successfully uploaded ${fileInfo.type}: ${fileInfo.relativePath}`);
|
|
626
|
+
|
|
627
|
+
} catch (error) {
|
|
628
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
629
|
+
results.push({
|
|
630
|
+
file: fileInfo.absolutePath,
|
|
631
|
+
remotePath: fileInfo.relativePath,
|
|
632
|
+
type: fileInfo.type,
|
|
633
|
+
status: 'error',
|
|
634
|
+
error: errorMessage
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
failureCount++;
|
|
638
|
+
this.log('error', `Failed to upload ${fileInfo.type} ${fileInfo.absolutePath}: ${errorMessage}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
|
|
643
|
+
|
|
644
|
+
if (failureCount === 0) {
|
|
645
|
+
try {
|
|
646
|
+
this.log('info', 'All files uploaded successfully, triggering extraction...');
|
|
647
|
+
await this.hosting_importWordpressWebsite_extractFiles(username, domain, archivePath, databaseDump);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
650
|
+
this.log('error', `File extraction failed: ${errorMessage}`);
|
|
651
|
+
return {
|
|
652
|
+
status: 'partial',
|
|
653
|
+
summary: {
|
|
654
|
+
total: filesToUpload.length,
|
|
655
|
+
successful: successCount,
|
|
656
|
+
failed: failureCount
|
|
657
|
+
},
|
|
658
|
+
results,
|
|
659
|
+
extractionError: errorMessage
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
status: overallStatus,
|
|
666
|
+
summary: {
|
|
667
|
+
total: filesToUpload.length,
|
|
668
|
+
successful: successCount,
|
|
669
|
+
failed: failureCount
|
|
670
|
+
},
|
|
671
|
+
results
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
hosting_deployWordpressPlugin_generateRandomString(length) {
|
|
676
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
677
|
+
let result = '';
|
|
678
|
+
for (let i = 0; i < length; i++) {
|
|
679
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
680
|
+
}
|
|
681
|
+
return result;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
hosting_deployWordpressPlugin_validateRequiredParams(params) {
|
|
685
|
+
const { domain, slug, pluginPath } = params;
|
|
686
|
+
|
|
687
|
+
if (!domain || typeof domain !== 'string') {
|
|
688
|
+
throw new Error('domain is required and must be a string');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (!slug || typeof slug !== 'string') {
|
|
692
|
+
throw new Error('slug is required and must be a string');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!pluginPath || typeof pluginPath !== 'string') {
|
|
696
|
+
throw new Error('pluginPath is required and must be a string');
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath) {
|
|
701
|
+
if (!fs.existsSync(pluginPath)) {
|
|
702
|
+
throw new Error(`Plugin directory not found: ${pluginPath}`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const pluginStats = fs.statSync(pluginPath);
|
|
706
|
+
if (!pluginStats.isDirectory()) {
|
|
707
|
+
throw new Error(`Plugin path is not a directory: ${pluginPath}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
hosting_deployWordpressPlugin_scanDirectory(dirPath, basePath = dirPath) {
|
|
712
|
+
const files = [];
|
|
713
|
+
|
|
714
|
+
const scanDir = (currentPath) => {
|
|
715
|
+
const items = fs.readdirSync(currentPath);
|
|
716
|
+
|
|
717
|
+
for (const item of items) {
|
|
718
|
+
const itemPath = path.join(currentPath, item);
|
|
719
|
+
const stats = fs.statSync(itemPath);
|
|
720
|
+
|
|
721
|
+
if (stats.isDirectory()) {
|
|
722
|
+
scanDir(itemPath);
|
|
723
|
+
} else if (stats.isFile()) {
|
|
724
|
+
const relativePath = path.relative(basePath, itemPath);
|
|
725
|
+
files.push({
|
|
726
|
+
absolutePath: itemPath,
|
|
727
|
+
relativePath: relativePath
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
scanDir(dirPath);
|
|
734
|
+
return files;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, pluginPath) {
|
|
738
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
739
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/plugins/deploy`, baseUrl).toString();
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
|
|
743
|
+
if (!bearerToken) {
|
|
744
|
+
throw new Error('API_TOKEN environment variable not found');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const config = {
|
|
748
|
+
method: 'post',
|
|
749
|
+
url,
|
|
750
|
+
headers: {
|
|
751
|
+
...this.headers,
|
|
752
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
753
|
+
'Content-Type': 'application/json'
|
|
754
|
+
},
|
|
755
|
+
data: {
|
|
756
|
+
slug,
|
|
757
|
+
plugin_path: pluginPath
|
|
758
|
+
},
|
|
759
|
+
timeout: 60000, // 60s
|
|
760
|
+
validateStatus: function (status) {
|
|
761
|
+
return status < 500;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
const response = await axios(config);
|
|
766
|
+
|
|
767
|
+
if (response.status !== 200) {
|
|
768
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
this.log('info', `Successfully triggered plugin deployment for ${domain}`);
|
|
772
|
+
return true;
|
|
773
|
+
|
|
774
|
+
} catch (error) {
|
|
775
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
776
|
+
this.log('error', `Failed to trigger plugin deployment: ${errorMessage}`);
|
|
777
|
+
|
|
778
|
+
if (axios.isAxiosError(error)) {
|
|
779
|
+
const responseData = error.response?.data;
|
|
780
|
+
const responseStatus = error.response?.status;
|
|
781
|
+
this.log('error', 'API Error Details:', {
|
|
782
|
+
status: responseStatus,
|
|
783
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
throw error;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async handleWordpressPluginDeploy(params) {
|
|
792
|
+
const { domain, slug, pluginPath } = params;
|
|
793
|
+
|
|
794
|
+
this.hosting_deployWordpressPlugin_validateRequiredParams(params);
|
|
795
|
+
this.hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath);
|
|
796
|
+
|
|
797
|
+
this.log('info', `Resolving username from domain: ${domain}`);
|
|
798
|
+
const username = await this.resolveUsername(domain);
|
|
799
|
+
|
|
800
|
+
const randomSuffix = this.hosting_deployWordpressPlugin_generateRandomString(8);
|
|
801
|
+
const uploadDirName = `${slug}-${randomSuffix}`;
|
|
802
|
+
|
|
803
|
+
this.log('info', `Scanning plugin directory: ${pluginPath}`);
|
|
804
|
+
const pluginFiles = this.hosting_deployWordpressPlugin_scanDirectory(pluginPath);
|
|
805
|
+
|
|
806
|
+
if (pluginFiles.length === 0) {
|
|
807
|
+
throw new Error(`No files found in plugin directory: ${pluginPath}`);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
this.log('info', `Found ${pluginFiles.length} files to upload`);
|
|
811
|
+
|
|
812
|
+
let uploadCredentials;
|
|
813
|
+
try {
|
|
814
|
+
uploadCredentials = await this.fetchUploadCredentials(username, domain);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
817
|
+
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
|
|
821
|
+
|
|
822
|
+
if (!uploadUrl || !authToken || !authRestToken) {
|
|
823
|
+
throw new Error('Invalid upload credentials received from API');
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
this.log('info', `Starting plugin file upload to ${uploadUrl}`);
|
|
827
|
+
|
|
828
|
+
const results = [];
|
|
829
|
+
let successCount = 0;
|
|
830
|
+
let failureCount = 0;
|
|
831
|
+
|
|
832
|
+
for (const fileInfo of pluginFiles) {
|
|
833
|
+
try {
|
|
834
|
+
const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
|
|
835
|
+
const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
|
|
836
|
+
this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
|
|
837
|
+
|
|
838
|
+
const stats = fs.statSync(fileInfo.absolutePath);
|
|
839
|
+
const uploadResult = await this.uploadFile(
|
|
840
|
+
fileInfo.absolutePath,
|
|
841
|
+
uploadPath,
|
|
842
|
+
uploadUrl,
|
|
843
|
+
authRestToken,
|
|
844
|
+
authToken
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
results.push({
|
|
848
|
+
file: fileInfo.absolutePath,
|
|
849
|
+
remotePath: uploadPath,
|
|
850
|
+
status: 'success',
|
|
851
|
+
uploadUrl: uploadResult.url,
|
|
852
|
+
size: stats.size
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
successCount++;
|
|
856
|
+
this.log('info', `Successfully uploaded: ${uploadPath}`);
|
|
857
|
+
|
|
858
|
+
} catch (error) {
|
|
859
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
860
|
+
const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
|
|
861
|
+
const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
|
|
862
|
+
|
|
863
|
+
results.push({
|
|
864
|
+
file: fileInfo.absolutePath,
|
|
865
|
+
remotePath: uploadPath,
|
|
866
|
+
status: 'error',
|
|
867
|
+
error: errorMessage
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
failureCount++;
|
|
871
|
+
this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
|
|
876
|
+
|
|
877
|
+
if (failureCount === 0) {
|
|
878
|
+
try {
|
|
879
|
+
this.log('info', 'All files uploaded successfully, triggering plugin deployment...');
|
|
880
|
+
await this.hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, uploadDirName);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
883
|
+
this.log('error', `Plugin deployment failed: ${errorMessage}`);
|
|
884
|
+
return {
|
|
885
|
+
status: 'partial',
|
|
886
|
+
summary: {
|
|
887
|
+
total: pluginFiles.length,
|
|
888
|
+
successful: successCount,
|
|
889
|
+
failed: failureCount
|
|
890
|
+
},
|
|
891
|
+
results,
|
|
892
|
+
deploymentError: errorMessage
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return {
|
|
898
|
+
status: overallStatus,
|
|
899
|
+
summary: {
|
|
900
|
+
total: pluginFiles.length,
|
|
901
|
+
successful: successCount,
|
|
902
|
+
failed: failureCount
|
|
903
|
+
},
|
|
904
|
+
results,
|
|
905
|
+
uploadDirName
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
hosting_deployWordpressTheme_generateRandomString(length) {
|
|
910
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
911
|
+
let result = '';
|
|
912
|
+
for (let i = 0; i < length; i++) {
|
|
913
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
914
|
+
}
|
|
915
|
+
return result;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
hosting_deployWordpressTheme_validateRequiredParams(params) {
|
|
919
|
+
const { domain, slug, themePath } = params;
|
|
920
|
+
|
|
921
|
+
if (!domain || typeof domain !== 'string') {
|
|
922
|
+
throw new Error('domain is required and must be a string');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (!slug || typeof slug !== 'string') {
|
|
926
|
+
throw new Error('slug is required and must be a string');
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (!themePath || typeof themePath !== 'string') {
|
|
930
|
+
throw new Error('themePath is required and must be a string');
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
hosting_deployWordpressTheme_validateThemeDirectory(themePath) {
|
|
935
|
+
if (!fs.existsSync(themePath)) {
|
|
936
|
+
throw new Error(`Theme directory not found: ${themePath}`);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const themeStats = fs.statSync(themePath);
|
|
940
|
+
if (!themeStats.isDirectory()) {
|
|
941
|
+
throw new Error(`Theme path is not a directory: ${themePath}`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
hosting_deployWordpressTheme_scanDirectory(dirPath, basePath = dirPath) {
|
|
946
|
+
const files = [];
|
|
947
|
+
|
|
948
|
+
const scanDir = (currentPath) => {
|
|
949
|
+
const items = fs.readdirSync(currentPath);
|
|
950
|
+
|
|
951
|
+
for (const item of items) {
|
|
952
|
+
const itemPath = path.join(currentPath, item);
|
|
953
|
+
const stats = fs.statSync(itemPath);
|
|
954
|
+
|
|
955
|
+
if (stats.isDirectory()) {
|
|
956
|
+
scanDir(itemPath);
|
|
957
|
+
} else if (stats.isFile()) {
|
|
958
|
+
const relativePath = path.relative(basePath, itemPath);
|
|
959
|
+
files.push({
|
|
960
|
+
absolutePath: itemPath,
|
|
961
|
+
relativePath: relativePath
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
scanDir(dirPath);
|
|
968
|
+
return files;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async hosting_deployWordpressTheme_deployTheme(username, domain, slug, themePath, activate = false) {
|
|
972
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
973
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/themes/deploy`, baseUrl).toString();
|
|
974
|
+
|
|
975
|
+
try {
|
|
976
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
|
|
977
|
+
if (!bearerToken) {
|
|
978
|
+
throw new Error('API_TOKEN environment variable not found');
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const config = {
|
|
982
|
+
method: 'post',
|
|
983
|
+
url,
|
|
984
|
+
headers: {
|
|
985
|
+
...this.headers,
|
|
986
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
987
|
+
'Content-Type': 'application/json'
|
|
988
|
+
},
|
|
989
|
+
data: {
|
|
990
|
+
slug,
|
|
991
|
+
theme_path: themePath,
|
|
992
|
+
is_activated: activate
|
|
993
|
+
},
|
|
994
|
+
timeout: 60000, // 60s
|
|
995
|
+
validateStatus: function (status) {
|
|
996
|
+
return status < 500;
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
const response = await axios(config);
|
|
1001
|
+
|
|
1002
|
+
if (response.status !== 200) {
|
|
1003
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
this.log('info', `Successfully triggered theme deployment for ${domain}`);
|
|
1007
|
+
return true;
|
|
1008
|
+
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1011
|
+
this.log('error', `Failed to trigger theme deployment: ${errorMessage}`);
|
|
1012
|
+
|
|
1013
|
+
if (axios.isAxiosError(error)) {
|
|
1014
|
+
const responseData = error.response?.data;
|
|
1015
|
+
const responseStatus = error.response?.status;
|
|
1016
|
+
this.log('error', 'API Error Details:', {
|
|
1017
|
+
status: responseStatus,
|
|
1018
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
throw error;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async handleWordpressThemeDeploy(params) {
|
|
1027
|
+
const { domain, slug, themePath, activate = false } = params;
|
|
1028
|
+
|
|
1029
|
+
this.hosting_deployWordpressTheme_validateRequiredParams(params);
|
|
1030
|
+
this.hosting_deployWordpressTheme_validateThemeDirectory(themePath);
|
|
1031
|
+
|
|
1032
|
+
this.log('info', `Resolving username from domain: ${domain}`);
|
|
1033
|
+
const username = await this.resolveUsername(domain);
|
|
1034
|
+
|
|
1035
|
+
const randomSuffix = this.hosting_deployWordpressTheme_generateRandomString(8);
|
|
1036
|
+
const uploadDirName = `${slug}-${randomSuffix}`;
|
|
1037
|
+
|
|
1038
|
+
this.log('info', `Scanning theme directory: ${themePath}`);
|
|
1039
|
+
const themeFiles = this.hosting_deployWordpressTheme_scanDirectory(themePath);
|
|
1040
|
+
|
|
1041
|
+
if (themeFiles.length === 0) {
|
|
1042
|
+
throw new Error(`No files found in theme directory: ${themePath}`);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
this.log('info', `Found ${themeFiles.length} files to upload`);
|
|
1046
|
+
|
|
1047
|
+
let uploadCredentials;
|
|
1048
|
+
try {
|
|
1049
|
+
uploadCredentials = await this.fetchUploadCredentials(username, domain);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1052
|
+
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
|
|
1056
|
+
|
|
1057
|
+
if (!uploadUrl || !authToken || !authRestToken) {
|
|
1058
|
+
throw new Error('Invalid upload credentials received from API');
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
this.log('info', `Starting theme file upload to ${uploadUrl}`);
|
|
1062
|
+
|
|
1063
|
+
const results = [];
|
|
1064
|
+
let successCount = 0;
|
|
1065
|
+
let failureCount = 0;
|
|
1066
|
+
|
|
1067
|
+
for (const fileInfo of themeFiles) {
|
|
1068
|
+
try {
|
|
1069
|
+
const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
|
|
1070
|
+
const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
|
|
1071
|
+
this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
|
|
1072
|
+
|
|
1073
|
+
const stats = fs.statSync(fileInfo.absolutePath);
|
|
1074
|
+
const uploadResult = await this.uploadFile(
|
|
1075
|
+
fileInfo.absolutePath,
|
|
1076
|
+
uploadPath,
|
|
1077
|
+
uploadUrl,
|
|
1078
|
+
authRestToken,
|
|
1079
|
+
authToken
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
results.push({
|
|
1083
|
+
file: fileInfo.absolutePath,
|
|
1084
|
+
remotePath: uploadPath,
|
|
1085
|
+
status: 'success',
|
|
1086
|
+
uploadUrl: uploadResult.url,
|
|
1087
|
+
size: stats.size
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
successCount++;
|
|
1091
|
+
this.log('info', `Successfully uploaded: ${uploadPath}`);
|
|
1092
|
+
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1095
|
+
const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
|
|
1096
|
+
const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
|
|
1097
|
+
|
|
1098
|
+
results.push({
|
|
1099
|
+
file: fileInfo.absolutePath,
|
|
1100
|
+
remotePath: uploadPath,
|
|
1101
|
+
status: 'error',
|
|
1102
|
+
error: errorMessage
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
failureCount++;
|
|
1106
|
+
this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
|
|
1111
|
+
|
|
1112
|
+
if (failureCount === 0) {
|
|
1113
|
+
try {
|
|
1114
|
+
this.log('info', 'All files uploaded successfully, triggering theme deployment...');
|
|
1115
|
+
await this.hosting_deployWordpressTheme_deployTheme(username, domain, slug, uploadDirName, activate);
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1118
|
+
this.log('error', `Theme deployment failed: ${errorMessage}`);
|
|
1119
|
+
return {
|
|
1120
|
+
status: 'partial',
|
|
1121
|
+
summary: {
|
|
1122
|
+
total: themeFiles.length,
|
|
1123
|
+
successful: successCount,
|
|
1124
|
+
failed: failureCount
|
|
1125
|
+
},
|
|
1126
|
+
results,
|
|
1127
|
+
deploymentError: errorMessage
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return {
|
|
1133
|
+
status: overallStatus,
|
|
1134
|
+
summary: {
|
|
1135
|
+
total: themeFiles.length,
|
|
1136
|
+
successful: successCount,
|
|
1137
|
+
failed: failureCount
|
|
1138
|
+
},
|
|
1139
|
+
results,
|
|
1140
|
+
uploadDirName,
|
|
1141
|
+
activated: activate
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
hosting_deployJsApplication_validateArchiveFormat(filePath) {
|
|
1146
|
+
const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
|
|
1147
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
1148
|
+
|
|
1149
|
+
for (const ext of validExtensions) {
|
|
1150
|
+
if (fileName.endsWith(`.${ext}`)) {
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
hosting_deployJsApplication_validateRequiredParams(params) {
|
|
1159
|
+
const { domain, archivePath, removeArchive } = params;
|
|
1160
|
+
|
|
1161
|
+
if (!domain || typeof domain !== 'string') {
|
|
1162
|
+
throw new Error('domain is required and must be a string');
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (!archivePath || typeof archivePath !== 'string') {
|
|
1166
|
+
throw new Error('archivePath is required and must be a string');
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
|
|
1170
|
+
throw new Error('removeArchive must be a boolean if provided');
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
hosting_deployJsApplication_removeArchive(archivePath, removeArchive) {
|
|
1175
|
+
if (!removeArchive) {
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
try {
|
|
1180
|
+
this.log('info', `Removing archive file: ${archivePath}`);
|
|
1181
|
+
fs.unlinkSync(archivePath);
|
|
1182
|
+
this.log('info', `Successfully removed archive file: ${archivePath}`);
|
|
1183
|
+
return true;
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1186
|
+
this.log('error', `Failed to remove archive file: ${errorMessage}`);
|
|
1187
|
+
// Don't fail the entire operation if archive removal fails
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
hosting_deployJsApplication_validateArchiveFile(archivePath) {
|
|
1193
|
+
if (!fs.existsSync(archivePath)) {
|
|
1194
|
+
throw new Error(`Archive file not found: ${archivePath}`);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const archiveStats = fs.statSync(archivePath);
|
|
1198
|
+
if (!archiveStats.isFile()) {
|
|
1199
|
+
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (!this.hosting_deployJsApplication_validateArchiveFormat(archivePath)) {
|
|
1203
|
+
throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
async hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath) {
|
|
1208
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1209
|
+
const archiveBasename = path.basename(archivePath);
|
|
1210
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/settings/from-archive?archive_path=${encodeURIComponent(archiveBasename)}`, baseUrl).toString();
|
|
1211
|
+
|
|
1212
|
+
try {
|
|
1213
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
|
|
1214
|
+
if (!bearerToken) {
|
|
1215
|
+
throw new Error('API_TOKEN environment variable not found');
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const config = {
|
|
1219
|
+
method: 'get',
|
|
1220
|
+
url,
|
|
1221
|
+
headers: {
|
|
1222
|
+
...this.headers,
|
|
1223
|
+
'Authorization': `Bearer ${bearerToken}`
|
|
1224
|
+
},
|
|
1225
|
+
timeout: 60000, // 60s
|
|
1226
|
+
validateStatus: function (status) {
|
|
1227
|
+
return status < 500;
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
const response = await axios(config);
|
|
1232
|
+
|
|
1233
|
+
if (response.status !== 200) {
|
|
1234
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
this.log('info', `Successfully fetched build settings for ${domain}`);
|
|
1238
|
+
return response.data;
|
|
1239
|
+
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1242
|
+
this.log('error', `Failed to fetch build settings: ${errorMessage}`);
|
|
1243
|
+
|
|
1244
|
+
if (axios.isAxiosError(error)) {
|
|
1245
|
+
const responseData = error.response?.data;
|
|
1246
|
+
const responseStatus = error.response?.status;
|
|
1247
|
+
this.log('error', 'API Error Details:', {
|
|
1248
|
+
status: responseStatus,
|
|
1249
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
throw error;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
async hosting_deployJsApplication_triggerBuild(username, domain, archivePath, buildSettings) {
|
|
1258
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1259
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
|
|
1260
|
+
|
|
1261
|
+
try {
|
|
1262
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
|
|
1263
|
+
if (!bearerToken) {
|
|
1264
|
+
throw new Error('API_TOKEN environment variable not found');
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const archiveBasename = path.basename(archivePath);
|
|
1268
|
+
const buildData = {
|
|
1269
|
+
...buildSettings,
|
|
1270
|
+
node_version: buildSettings?.node_version || 20,
|
|
1271
|
+
source_type: 'archive',
|
|
1272
|
+
source_options: {
|
|
1273
|
+
archive_path: archiveBasename
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
const config = {
|
|
1278
|
+
method: 'post',
|
|
1279
|
+
url,
|
|
1280
|
+
headers: {
|
|
1281
|
+
...this.headers,
|
|
1282
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
1283
|
+
'Content-Type': 'application/json'
|
|
1284
|
+
},
|
|
1285
|
+
data: buildData,
|
|
1286
|
+
timeout: 60000, // 60s
|
|
1287
|
+
validateStatus: function (status) {
|
|
1288
|
+
return status < 500;
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
const response = await axios(config);
|
|
1293
|
+
|
|
1294
|
+
if (response.status !== 200) {
|
|
1295
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
this.log('info', `Successfully triggered build for ${domain}`);
|
|
1299
|
+
return response.data;
|
|
1300
|
+
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1303
|
+
this.log('error', `Failed to trigger build: ${errorMessage}`);
|
|
1304
|
+
|
|
1305
|
+
if (axios.isAxiosError(error)) {
|
|
1306
|
+
const responseData = error.response?.data;
|
|
1307
|
+
const responseStatus = error.response?.status;
|
|
1308
|
+
this.log('error', 'API Error Details:', {
|
|
1309
|
+
status: responseStatus,
|
|
1310
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
throw error;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
async handleJavascriptApplicationDeploy(params) {
|
|
1319
|
+
const { domain, archivePath, removeArchive = false } = params;
|
|
1320
|
+
|
|
1321
|
+
this.hosting_deployJsApplication_validateRequiredParams(params);
|
|
1322
|
+
this.hosting_deployJsApplication_validateArchiveFile(archivePath);
|
|
1323
|
+
|
|
1324
|
+
// Auto-resolve username from domain
|
|
1325
|
+
this.log('info', `Resolving username from domain: ${domain}`);
|
|
1326
|
+
const username = await this.resolveUsername(domain);
|
|
1327
|
+
|
|
1328
|
+
// Upload archive file
|
|
1329
|
+
this.log('info', `Starting archive upload for ${domain}`);
|
|
1330
|
+
|
|
1331
|
+
let uploadCredentials;
|
|
1332
|
+
try {
|
|
1333
|
+
uploadCredentials = await this.fetchUploadCredentials(username, domain);
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1336
|
+
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
|
|
1340
|
+
|
|
1341
|
+
if (!uploadUrl || !authToken || !authRestToken) {
|
|
1342
|
+
throw new Error('Invalid upload credentials received from API');
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const archiveBasename = path.basename(archivePath);
|
|
1346
|
+
let uploadResult;
|
|
1347
|
+
try {
|
|
1348
|
+
const stats = fs.statSync(archivePath);
|
|
1349
|
+
uploadResult = await this.uploadFile(
|
|
1350
|
+
archivePath,
|
|
1351
|
+
archiveBasename,
|
|
1352
|
+
uploadUrl,
|
|
1353
|
+
authRestToken,
|
|
1354
|
+
authToken
|
|
1355
|
+
);
|
|
1356
|
+
|
|
1357
|
+
this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1360
|
+
throw new Error(`Failed to upload archive: ${errorMessage}`);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Fetch build settings
|
|
1364
|
+
let buildSettings;
|
|
1365
|
+
try {
|
|
1366
|
+
this.log('info', `Fetching build settings for ${domain}`);
|
|
1367
|
+
buildSettings = await this.hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath);
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1370
|
+
this.log('error', `Failed to fetch build settings: ${errorMessage}`);
|
|
1371
|
+
const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
|
|
1372
|
+
|
|
1373
|
+
return {
|
|
1374
|
+
upload: {
|
|
1375
|
+
status: 'success',
|
|
1376
|
+
data: {
|
|
1377
|
+
filename: uploadResult.filename
|
|
1378
|
+
}
|
|
1379
|
+
},
|
|
1380
|
+
resolveSettings: {
|
|
1381
|
+
status: 'error',
|
|
1382
|
+
error: errorMessage
|
|
1383
|
+
},
|
|
1384
|
+
build: {
|
|
1385
|
+
status: 'skipped'
|
|
1386
|
+
},
|
|
1387
|
+
removeArchive: {
|
|
1388
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Trigger build
|
|
1394
|
+
let buildResult;
|
|
1395
|
+
try {
|
|
1396
|
+
this.log('info', `Triggering build for ${domain}`);
|
|
1397
|
+
buildResult = await this.hosting_deployJsApplication_triggerBuild(username, domain, archivePath, buildSettings);
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1400
|
+
this.log('error', `Failed to trigger build: ${errorMessage}`);
|
|
1401
|
+
const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
|
|
1402
|
+
|
|
1403
|
+
return {
|
|
1404
|
+
upload: {
|
|
1405
|
+
status: 'success',
|
|
1406
|
+
data: {
|
|
1407
|
+
filename: uploadResult.filename
|
|
1408
|
+
}
|
|
1409
|
+
},
|
|
1410
|
+
resolveSettings: {
|
|
1411
|
+
status: 'success',
|
|
1412
|
+
data: buildSettings
|
|
1413
|
+
},
|
|
1414
|
+
build: {
|
|
1415
|
+
status: 'error',
|
|
1416
|
+
error: errorMessage
|
|
1417
|
+
},
|
|
1418
|
+
removeArchive: {
|
|
1419
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
|
|
1425
|
+
|
|
1426
|
+
return {
|
|
1427
|
+
upload: {
|
|
1428
|
+
status: 'success',
|
|
1429
|
+
data: {
|
|
1430
|
+
filename: uploadResult.filename
|
|
1431
|
+
}
|
|
1432
|
+
},
|
|
1433
|
+
resolveSettings: {
|
|
1434
|
+
status: 'success',
|
|
1435
|
+
data: buildSettings
|
|
1436
|
+
},
|
|
1437
|
+
build: {
|
|
1438
|
+
status: 'success',
|
|
1439
|
+
data: buildResult
|
|
1440
|
+
},
|
|
1441
|
+
removeArchive: {
|
|
1442
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
hosting_deployStaticWebsite_validateArchiveFormat(filePath) {
|
|
1448
|
+
const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
|
|
1449
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
1450
|
+
|
|
1451
|
+
for (const ext of validExtensions) {
|
|
1452
|
+
if (fileName.endsWith(`.${ext}`)) {
|
|
1453
|
+
return true;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
hosting_deployStaticWebsite_validateRequiredParams(params) {
|
|
1461
|
+
const { domain, archivePath, removeArchive } = params;
|
|
1462
|
+
|
|
1463
|
+
if (!domain || typeof domain !== 'string') {
|
|
1464
|
+
throw new Error('domain is required and must be a string');
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (!archivePath || typeof archivePath !== 'string') {
|
|
1468
|
+
throw new Error('archivePath is required and must be a string');
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
|
|
1472
|
+
throw new Error('removeArchive must be a boolean if provided');
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive) {
|
|
1477
|
+
if (!removeArchive) {
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
try {
|
|
1482
|
+
this.log('info', `Removing archive file: ${archivePath}`);
|
|
1483
|
+
fs.unlinkSync(archivePath);
|
|
1484
|
+
this.log('info', `Successfully removed archive file: ${archivePath}`);
|
|
1485
|
+
return true;
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1488
|
+
this.log('error', `Failed to remove archive file: ${errorMessage}`);
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
hosting_deployStaticWebsite_validateArchiveFile(archivePath) {
|
|
1494
|
+
if (!fs.existsSync(archivePath)) {
|
|
1495
|
+
throw new Error(`Archive file not found: ${archivePath}`);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const archiveStats = fs.statSync(archivePath);
|
|
1499
|
+
if (!archiveStats.isFile()) {
|
|
1500
|
+
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if (!this.hosting_deployStaticWebsite_validateArchiveFormat(archivePath)) {
|
|
1504
|
+
throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
async hosting_deployStaticWebsite_triggerDeploy(username, domain, archivePath) {
|
|
1509
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1510
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/deploy`, baseUrl).toString();
|
|
1511
|
+
|
|
1512
|
+
try {
|
|
1513
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
|
|
1514
|
+
if (!bearerToken) {
|
|
1515
|
+
throw new Error('API_TOKEN environment variable not found');
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const archiveBasename = path.basename(archivePath);
|
|
1519
|
+
const deployData = {
|
|
1520
|
+
archive_path: archiveBasename
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
const config = {
|
|
1524
|
+
method: 'post',
|
|
1525
|
+
url,
|
|
1526
|
+
headers: {
|
|
1527
|
+
...this.headers,
|
|
1528
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
1529
|
+
'Content-Type': 'application/json'
|
|
1530
|
+
},
|
|
1531
|
+
data: deployData,
|
|
1532
|
+
timeout: 60000,
|
|
1533
|
+
validateStatus: function (status) {
|
|
1534
|
+
return status < 500;
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
const response = await axios(config);
|
|
1539
|
+
|
|
1540
|
+
if (response.status !== 200) {
|
|
1541
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
this.log('info', `Successfully triggered deployment for ${domain}`);
|
|
1545
|
+
return response.data;
|
|
1546
|
+
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1549
|
+
this.log('error', `Failed to trigger deployment: ${errorMessage}`);
|
|
1550
|
+
|
|
1551
|
+
if (axios.isAxiosError(error)) {
|
|
1552
|
+
const responseData = error.response?.data;
|
|
1553
|
+
const responseStatus = error.response?.status;
|
|
1554
|
+
this.log('error', 'API Error Details:', {
|
|
1555
|
+
status: responseStatus,
|
|
1556
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
throw error;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
async handleStaticWebsiteDeploy(params) {
|
|
1565
|
+
const { domain, archivePath, removeArchive = false } = params;
|
|
1566
|
+
|
|
1567
|
+
this.hosting_deployStaticWebsite_validateRequiredParams(params);
|
|
1568
|
+
this.hosting_deployStaticWebsite_validateArchiveFile(archivePath);
|
|
1569
|
+
|
|
1570
|
+
this.log('info', `Resolving username from domain: ${domain}`);
|
|
1571
|
+
const username = await this.resolveUsername(domain);
|
|
1572
|
+
|
|
1573
|
+
this.log('info', `Starting archive upload for ${domain}`);
|
|
1574
|
+
|
|
1575
|
+
let uploadCredentials;
|
|
1576
|
+
try {
|
|
1577
|
+
uploadCredentials = await this.fetchUploadCredentials(username, domain);
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1580
|
+
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
|
|
1584
|
+
|
|
1585
|
+
if (!uploadUrl || !authToken || !authRestToken) {
|
|
1586
|
+
throw new Error('Invalid upload credentials received from API');
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
const archiveBasename = path.basename(archivePath);
|
|
1590
|
+
let uploadResult;
|
|
1591
|
+
try {
|
|
1592
|
+
const stats = fs.statSync(archivePath);
|
|
1593
|
+
uploadResult = await this.uploadFile(
|
|
1594
|
+
archivePath,
|
|
1595
|
+
archiveBasename,
|
|
1596
|
+
uploadUrl,
|
|
1597
|
+
authRestToken,
|
|
1598
|
+
authToken
|
|
1599
|
+
);
|
|
1600
|
+
|
|
1601
|
+
this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1604
|
+
throw new Error(`Failed to upload archive: ${errorMessage}`);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
let deployResult;
|
|
1608
|
+
try {
|
|
1609
|
+
this.log('info', `Triggering deployment for ${domain}`);
|
|
1610
|
+
deployResult = await this.hosting_deployStaticWebsite_triggerDeploy(username, domain, archivePath);
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1613
|
+
this.log('error', `Failed to trigger deployment: ${errorMessage}`);
|
|
1614
|
+
const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
|
|
1615
|
+
|
|
1616
|
+
return {
|
|
1617
|
+
upload: {
|
|
1618
|
+
status: 'success',
|
|
1619
|
+
data: {
|
|
1620
|
+
filename: uploadResult.filename
|
|
1621
|
+
}
|
|
1622
|
+
},
|
|
1623
|
+
deploy: {
|
|
1624
|
+
status: 'error',
|
|
1625
|
+
error: errorMessage
|
|
1626
|
+
},
|
|
1627
|
+
removeArchive: {
|
|
1628
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
|
|
1634
|
+
|
|
1635
|
+
return {
|
|
1636
|
+
upload: {
|
|
1637
|
+
status: 'success',
|
|
1638
|
+
data: {
|
|
1639
|
+
filename: uploadResult.filename
|
|
1640
|
+
}
|
|
1641
|
+
},
|
|
1642
|
+
deploy: {
|
|
1643
|
+
status: 'success',
|
|
1644
|
+
data: deployResult
|
|
1645
|
+
},
|
|
1646
|
+
removeArchive: {
|
|
1647
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
hosting_listJsDeployments_validateRequiredParams(params) {
|
|
1653
|
+
const { domain } = params;
|
|
1654
|
+
|
|
1655
|
+
if (!domain || typeof domain !== 'string') {
|
|
1656
|
+
throw new Error('domain is required and must be a string');
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
hosting_listJsDeployments_buildQueryParams(params) {
|
|
1661
|
+
const { page, perPage, states } = params;
|
|
1662
|
+
const queryParams = new URLSearchParams();
|
|
1663
|
+
|
|
1664
|
+
if (page !== undefined && page !== null) {
|
|
1665
|
+
queryParams.append('page', page.toString());
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
if (perPage !== undefined && perPage !== null) {
|
|
1669
|
+
queryParams.append('per_page', perPage.toString());
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (states && Array.isArray(states) && states.length > 0) {
|
|
1673
|
+
states.forEach(state => {
|
|
1674
|
+
queryParams.append('states[]', state);
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
return queryParams.toString();
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
async hosting_listJsDeployments_fetchDeployments(username, domain, queryParams) {
|
|
1682
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1683
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
|
|
1684
|
+
|
|
1685
|
+
const fullUrl = queryParams ? `${url}?${queryParams}` : url;
|
|
1686
|
+
|
|
1687
|
+
try {
|
|
1688
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
|
|
1689
|
+
if (!bearerToken) {
|
|
1690
|
+
throw new Error('API_TOKEN environment variable not found');
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
const config = {
|
|
1694
|
+
method: 'get',
|
|
1695
|
+
url: fullUrl,
|
|
1696
|
+
headers: {
|
|
1697
|
+
...this.headers,
|
|
1698
|
+
'Authorization': `Bearer ${bearerToken}`
|
|
1699
|
+
},
|
|
1700
|
+
timeout: 60000, // 60s
|
|
1701
|
+
validateStatus: function (status) {
|
|
1702
|
+
return status < 500;
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
const response = await axios(config);
|
|
1707
|
+
|
|
1708
|
+
if (response.status !== 200) {
|
|
1709
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
this.log('info', `Successfully fetched deployments for ${domain}`);
|
|
1713
|
+
return response.data;
|
|
1714
|
+
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1717
|
+
this.log('error', `Failed to fetch deployments: ${errorMessage}`);
|
|
1718
|
+
|
|
1719
|
+
if (axios.isAxiosError(error)) {
|
|
1720
|
+
const responseData = error.response?.data;
|
|
1721
|
+
const responseStatus = error.response?.status;
|
|
1722
|
+
this.log('error', 'API Error Details:', {
|
|
1723
|
+
status: responseStatus,
|
|
1724
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
throw error;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
async handleListJavascriptDeployments(params) {
|
|
1733
|
+
const { domain, page, perPage, states } = params;
|
|
1734
|
+
|
|
1735
|
+
this.hosting_listJsDeployments_validateRequiredParams(params);
|
|
1736
|
+
|
|
1737
|
+
// Auto-resolve username from domain
|
|
1738
|
+
this.log('info', `Resolving username from domain: ${domain}`);
|
|
1739
|
+
const username = await this.resolveUsername(domain);
|
|
1740
|
+
|
|
1741
|
+
// Build query parameters
|
|
1742
|
+
const queryParams = this.hosting_listJsDeployments_buildQueryParams(params);
|
|
1743
|
+
|
|
1744
|
+
// Fetch deployments
|
|
1745
|
+
let deployments;
|
|
1746
|
+
try {
|
|
1747
|
+
this.log('info', `Fetching deployments for ${domain}`);
|
|
1748
|
+
deployments = await this.hosting_listJsDeployments_fetchDeployments(username, domain, queryParams);
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1751
|
+
this.log('error', `Failed to fetch deployments: ${errorMessage}`);
|
|
1752
|
+
throw error;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
return {
|
|
1756
|
+
status: 'success',
|
|
1757
|
+
domain,
|
|
1758
|
+
username,
|
|
1759
|
+
queryParams: {
|
|
1760
|
+
page,
|
|
1761
|
+
perPage,
|
|
1762
|
+
states
|
|
1763
|
+
},
|
|
1764
|
+
deployments
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
hosting_showJsDeploymentLogs_validateRequiredParams(params) {
|
|
1769
|
+
const { domain, buildUuid, fromLine } = params;
|
|
1770
|
+
|
|
1771
|
+
if (!domain || typeof domain !== 'string') {
|
|
1772
|
+
throw new Error('domain is required and must be a string');
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
if (!buildUuid || typeof buildUuid !== 'string') {
|
|
1776
|
+
throw new Error('buildUuid is required and must be a string');
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
if (fromLine !== undefined && (typeof fromLine !== 'number' || !Number.isInteger(fromLine) || fromLine < 0)) {
|
|
1780
|
+
throw new Error('fromLine must be a non-negative integer when provided');
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
hosting_showJsDeploymentLogs_buildQueryParams(params) {
|
|
1785
|
+
const { fromLine } = params;
|
|
1786
|
+
const queryParams = new URLSearchParams();
|
|
1787
|
+
|
|
1788
|
+
const line = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
|
|
1789
|
+
queryParams.append('from_line', line.toString());
|
|
1790
|
+
|
|
1791
|
+
return queryParams.toString();
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
async hosting_showJsDeploymentLogs_fetchLogs(username, domain, buildUuid, queryParams) {
|
|
1795
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1796
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/${buildUuid}/logs`, baseUrl).toString();
|
|
1797
|
+
|
|
1798
|
+
const fullUrl = queryParams ? `${url}?${queryParams}` : url;
|
|
1799
|
+
|
|
1800
|
+
try {
|
|
1801
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
|
|
1802
|
+
if (!bearerToken) {
|
|
1803
|
+
throw new Error('API_TOKEN environment variable not found');
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const config = {
|
|
1807
|
+
method: 'get',
|
|
1808
|
+
url: fullUrl,
|
|
1809
|
+
headers: {
|
|
1810
|
+
...this.headers,
|
|
1811
|
+
'Authorization': `Bearer ${bearerToken}`
|
|
1812
|
+
},
|
|
1813
|
+
timeout: 60000,
|
|
1814
|
+
validateStatus: function (status) {
|
|
1815
|
+
return status < 500;
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
const response = await axios(config);
|
|
1820
|
+
|
|
1821
|
+
if (response.status !== 200) {
|
|
1822
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
this.log('info', `Successfully fetched logs for ${domain} build ${buildUuid}`);
|
|
1826
|
+
return response.data;
|
|
1827
|
+
|
|
1828
|
+
} catch (error) {
|
|
1829
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1830
|
+
this.log('error', `Failed to fetch logs: ${errorMessage}`);
|
|
1831
|
+
|
|
1832
|
+
if (axios.isAxiosError(error)) {
|
|
1833
|
+
const responseData = error.response?.data;
|
|
1834
|
+
const responseStatus = error.response?.status;
|
|
1835
|
+
this.log('error', 'API Error Details:', {
|
|
1836
|
+
status: responseStatus,
|
|
1837
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
throw error;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
async handleShowJsDeploymentLogs(params) {
|
|
1846
|
+
const { domain, buildUuid, fromLine } = params;
|
|
1847
|
+
|
|
1848
|
+
this.hosting_showJsDeploymentLogs_validateRequiredParams(params);
|
|
1849
|
+
|
|
1850
|
+
this.log('info', `Resolving username from domain: ${domain}`);
|
|
1851
|
+
const username = await this.resolveUsername(domain);
|
|
1852
|
+
|
|
1853
|
+
const queryParams = this.hosting_showJsDeploymentLogs_buildQueryParams(params);
|
|
1854
|
+
|
|
1855
|
+
let logs;
|
|
1856
|
+
try {
|
|
1857
|
+
this.log('info', `Fetching logs for ${domain}, build ${buildUuid}`);
|
|
1858
|
+
logs = await this.hosting_showJsDeploymentLogs_fetchLogs(username, domain, buildUuid, queryParams);
|
|
1859
|
+
} catch (error) {
|
|
1860
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1861
|
+
this.log('error', `Failed to fetch logs: ${errorMessage}`);
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const effectiveFromLine = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
|
|
1866
|
+
|
|
1867
|
+
return {
|
|
1868
|
+
domain,
|
|
1869
|
+
username,
|
|
1870
|
+
buildUuid,
|
|
1871
|
+
fromLine: effectiveFromLine,
|
|
1872
|
+
logs
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* Execute an API call for a tool
|
|
1878
|
+
*/
|
|
1879
|
+
async executeApiCall(tool, params) {
|
|
1880
|
+
// Get method and path from tool
|
|
1881
|
+
const method = tool.method;
|
|
1882
|
+
let path = tool.path;
|
|
1883
|
+
|
|
1884
|
+
// Clone params to avoid modifying the original
|
|
1885
|
+
const requestParams = { ...params };
|
|
1886
|
+
|
|
1887
|
+
// Replace path parameters with values from params
|
|
1888
|
+
Object.entries(requestParams).forEach(([key, value]) => {
|
|
1889
|
+
const placeholder = `{${key}}`;
|
|
1890
|
+
if (path.includes(placeholder)) {
|
|
1891
|
+
path = path.replace(placeholder, encodeURIComponent(String(value)));
|
|
1892
|
+
delete requestParams[key]; // Remove used parameter
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
// Build the full URL
|
|
1897
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1898
|
+
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
|
|
1899
|
+
const url = new URL(cleanPath, baseUrl).toString();
|
|
1900
|
+
|
|
1901
|
+
this.log('debug', `API Request: ${method} ${url}`);
|
|
1902
|
+
|
|
1903
|
+
try {
|
|
1904
|
+
// Configure the request
|
|
1905
|
+
const config = {
|
|
1906
|
+
method: method.toLowerCase(),
|
|
1907
|
+
url,
|
|
1908
|
+
headers: { ...this.headers },
|
|
1909
|
+
timeout: 60000, // 60s
|
|
1910
|
+
validateStatus: function (status) {
|
|
1911
|
+
return status < 500; // Resolve only if the status code is less than 500
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
|
|
1915
|
+
const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN']; // APITOKEN for backwards compatibility
|
|
1916
|
+
if (bearerToken) {
|
|
1917
|
+
config.headers['Authorization'] = `Bearer ${bearerToken}`;
|
|
1918
|
+
} else {
|
|
1919
|
+
this.log('error', `Bearer Token environment variable not found: API_TOKEN`);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// Add parameters based on request method
|
|
1923
|
+
if (["GET", "DELETE"].includes(method)) {
|
|
1924
|
+
// For GET/DELETE, send params as query string
|
|
1925
|
+
config.params = { ...(config.params || {}), ...requestParams };
|
|
1926
|
+
} else {
|
|
1927
|
+
// For POST/PUT/PATCH, send params as JSON body
|
|
1928
|
+
config.data = requestParams;
|
|
1929
|
+
config.headers["Content-Type"] = "application/json";
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
this.log('debug', "Request config:", {
|
|
1933
|
+
url: config.url,
|
|
1934
|
+
method: config.method,
|
|
1935
|
+
params: config.params,
|
|
1936
|
+
headers: Object.keys(config.headers)
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
// Execute the request
|
|
1940
|
+
const response = await axios(config);
|
|
1941
|
+
this.log('debug', `Response status: ${response.status}`);
|
|
1942
|
+
|
|
1943
|
+
return response.data;
|
|
1944
|
+
|
|
1945
|
+
} catch (error) {
|
|
1946
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1947
|
+
this.log('error', `API request failed: ${errorMessage}`);
|
|
1948
|
+
|
|
1949
|
+
if (axios.isAxiosError(error)) {
|
|
1950
|
+
const responseData = error.response?.data;
|
|
1951
|
+
const responseStatus = error.response?.status;
|
|
1952
|
+
|
|
1953
|
+
this.log('error', 'API Error Details:', {
|
|
1954
|
+
status: responseStatus,
|
|
1955
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
// Rethrow with more context for better error handling
|
|
1959
|
+
const detailedError = new Error(`API request failed with status ${responseStatus}: ${errorMessage}`);
|
|
1960
|
+
detailedError.response = error.response;
|
|
1961
|
+
throw detailedError;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
throw error;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/**
|
|
1969
|
+
* Log messages with appropriate level
|
|
1970
|
+
* Only sends to MCP if we're connected
|
|
1971
|
+
*/
|
|
1972
|
+
log(level, message, data) {
|
|
1973
|
+
// Always log to stderr for visibility
|
|
1974
|
+
console.error(`[${level.toUpperCase()}] ${message}${data ? ': ' + JSON.stringify(data) : ''}`);
|
|
1975
|
+
|
|
1976
|
+
// Only try to send via MCP if we're in debug mode or it's important
|
|
1977
|
+
if (this.debug || level !== 'debug') {
|
|
1978
|
+
try {
|
|
1979
|
+
// Only send if server exists and is connected
|
|
1980
|
+
if (this.server && this.server.isConnected) {
|
|
1981
|
+
this.server.sendLoggingMessage({
|
|
1982
|
+
level,
|
|
1983
|
+
data: `[MCP Server] ${message}${data ? ': ' + JSON.stringify(data) : ''}`
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
} catch (e) {
|
|
1987
|
+
// If logging fails, log to stderr
|
|
1988
|
+
console.error('Failed to send log via MCP:', e.message);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* Create and configure Express app with shared middleware
|
|
1995
|
+
*/
|
|
1996
|
+
createApp() {
|
|
1997
|
+
const app = express();
|
|
1998
|
+
app.use(express.json());
|
|
1999
|
+
app.use(cors());
|
|
2000
|
+
return app;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
/**
|
|
2004
|
+
* Start the server with HTTP streaming transport
|
|
2005
|
+
*/
|
|
2006
|
+
async startHttp(host, port) {
|
|
2007
|
+
try {
|
|
2008
|
+
const app = this.createApp();
|
|
2009
|
+
|
|
2010
|
+
// Create HTTP transport with session management
|
|
2011
|
+
const httpTransport = new StreamableHTTPServerTransport({
|
|
2012
|
+
sessionIdGenerator: () => {
|
|
2013
|
+
// Generate a simple session ID
|
|
2014
|
+
return `session-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
2015
|
+
},
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
// Set up CORS for all routes
|
|
2019
|
+
app.options("*", (req, res) => {
|
|
2020
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
2021
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
2022
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, x-session-id");
|
|
2023
|
+
res.sendStatus(200);
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
// Health check endpoint
|
|
2027
|
+
app.get("/health", (req, res) => {
|
|
2028
|
+
res.status(200).json({ status: "ok", transport: "http" });
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
// Set up the HTTP transport endpoint
|
|
2032
|
+
app.post("/", async (req, res) => {
|
|
2033
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
2034
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
2035
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, x-session-id");
|
|
2036
|
+
await httpTransport.handleRequest(req, res, req.body);
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
// Start the server
|
|
2040
|
+
const server = app.listen(port, host, () => {
|
|
2041
|
+
this.log('info', `MCP Server with HTTP streaming transport started successfully with ${this.tools.size} tools`);
|
|
2042
|
+
this.log('info', `Listening on http://${host}:${port}`);
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
// Connect the MCP server to the transport
|
|
2046
|
+
await this.server.connect(httpTransport);
|
|
2047
|
+
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
console.error("Failed to start MCP server:", error);
|
|
2050
|
+
process.exit(1);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* Start the server
|
|
2056
|
+
*/
|
|
2057
|
+
async startStdio() {
|
|
2058
|
+
try {
|
|
2059
|
+
// Create stdio transport
|
|
2060
|
+
const transport = new StdioServerTransport();
|
|
2061
|
+
console.error("MCP Server starting on stdio transport");
|
|
2062
|
+
|
|
2063
|
+
// Connect to the transport
|
|
2064
|
+
await this.server.connect(transport);
|
|
2065
|
+
|
|
2066
|
+
// Now we can safely log via MCP
|
|
2067
|
+
console.error(`Registered ${this.tools.size} tools`);
|
|
2068
|
+
this.log('info', `MCP Server with stdio transport started successfully with ${this.tools.size} tools`);
|
|
2069
|
+
} catch (error) {
|
|
2070
|
+
console.error("Failed to start MCP server:", error);
|
|
2071
|
+
process.exit(1);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
export async function startServer({ name, version, tools }) {
|
|
2077
|
+
const argv = minimist(process.argv.slice(2), {
|
|
2078
|
+
string: ['host'],
|
|
2079
|
+
boolean: ['stdio', 'http', 'help'],
|
|
2080
|
+
default: { host: '127.0.0.1', port: 8100, stdio: true }
|
|
2081
|
+
});
|
|
2082
|
+
|
|
2083
|
+
if (argv.help) {
|
|
2084
|
+
console.log(`
|
|
2085
|
+
${name}
|
|
2086
|
+
Usage: ${name} [options]
|
|
2087
|
+
Options:
|
|
2088
|
+
--http Use HTTP streaming transport
|
|
2089
|
+
--stdio Use standard input/output transport (default)
|
|
2090
|
+
--host <host> Host to bind to (default: 127.0.0.1)
|
|
2091
|
+
--port <port> Port to bind to (default: 8100)
|
|
2092
|
+
--help Show this help message
|
|
2093
|
+
Environment Variables:
|
|
2094
|
+
API_TOKEN Your Hostinger API token (required)
|
|
2095
|
+
DEBUG Enable debug logging (true/false)
|
|
2096
|
+
`);
|
|
2097
|
+
process.exit(0);
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
const server = new MCPServer({ name, version, tools });
|
|
2101
|
+
if (argv.http) {
|
|
2102
|
+
await server.startHttp(argv.host, argv.port);
|
|
2103
|
+
} else {
|
|
2104
|
+
await server.startStdio();
|
|
2105
|
+
}
|
|
2106
|
+
}
|