hostinger-api-mcp 1.6.2 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -3
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/core/runtime.js +532 -1
- package/src/core/runtime.ts +540 -1
- package/src/core/tools/agency-hosting.js +68 -0
- package/src/core/tools/agency-hosting.ts +68 -0
- package/src/core/tools/all.js +92 -0
- package/src/core/tools/all.ts +92 -0
- package/src/core/tools/domains.js +24 -0
- package/src/core/tools/domains.ts +24 -0
- package/src/servers/agency-hosting.js +1 -1
- package/src/servers/agency-hosting.ts +1 -1
- package/src/servers/all.js +1 -1
- package/src/servers/all.ts +1 -1
- package/src/servers/billing.js +1 -1
- package/src/servers/billing.ts +1 -1
- package/src/servers/dns.js +1 -1
- package/src/servers/dns.ts +1 -1
- package/src/servers/domains.js +1 -1
- package/src/servers/domains.ts +1 -1
- package/src/servers/ecommerce.js +1 -1
- package/src/servers/ecommerce.ts +1 -1
- package/src/servers/horizons.js +1 -1
- package/src/servers/horizons.ts +1 -1
- package/src/servers/hosting.js +1 -1
- package/src/servers/hosting.ts +1 -1
- package/src/servers/reach.js +1 -1
- package/src/servers/reach.ts +1 -1
- package/src/servers/vps.js +1 -1
- package/src/servers/vps.ts +1 -1
- package/src/servers/wordpress.js +1 -1
- package/src/servers/wordpress.ts +1 -1
- package/types.d.ts +58 -0
package/README.md
CHANGED
|
@@ -49,11 +49,11 @@ pnpm update -g hostinger-api-mcp
|
|
|
49
49
|
|
|
50
50
|
This package installs the following MCP server commands:
|
|
51
51
|
|
|
52
|
-
- `hostinger-api-mcp` — unified server with every tool (
|
|
53
|
-
- `hostinger-agency-hosting-mcp` —
|
|
52
|
+
- `hostinger-api-mcp` — unified server with every tool (217 total)
|
|
53
|
+
- `hostinger-agency-hosting-mcp` — 14 tools for agency-hosting
|
|
54
54
|
- `hostinger-billing-mcp` — 7 tools for billing
|
|
55
55
|
- `hostinger-dns-mcp` — 8 tools for dns
|
|
56
|
-
- `hostinger-domains-mcp` —
|
|
56
|
+
- `hostinger-domains-mcp` — 19 tools for domains
|
|
57
57
|
- `hostinger-ecommerce-mcp` — 12 tools for ecommerce
|
|
58
58
|
- `hostinger-horizons-mcp` — 2 tools for horizons
|
|
59
59
|
- `hostinger-hosting-mcp` — 46 tools for hosting
|
|
@@ -203,6 +203,20 @@ This MCP server provides the following tools:
|
|
|
203
203
|
|
|
204
204
|
### `hostinger-agency-hosting-mcp`
|
|
205
205
|
|
|
206
|
+
#### agencyHosting_deployNodeStaticWebsite
|
|
207
|
+
|
|
208
|
+
Deploy a node-static Agency Plan (h5g) website from an archive file. WARNING: this overwrites the website's existing contents and cannot be undone — always confirm with the user before proceeding. Use this for Agency Plan websites of type node-static (a Node.js-built static site that requires a build step or a plain simple static site). The tool resolves the website from its domain, uploads the archive to the website's file browser over TUS, and triggers the build-assets process which builds the site and deploys the result to public_html. This operation is synchronous: the build and deployment complete before the tool returns, so the website is live as soon as the tool finishes successfully — there is no separate asynchronous build to wait for or poll. For plain PHP applications that should be extracted as-is, use agencyHosting_deployPhpApplication instead. The website UID is automatically resolved from the domain.
|
|
209
|
+
|
|
210
|
+
- **Method**: `custom`
|
|
211
|
+
- **Path**: `custom`
|
|
212
|
+
|
|
213
|
+
#### agencyHosting_deployPhpApplication
|
|
214
|
+
|
|
215
|
+
Deploy a PHP (or other non-build) Agency Plan (h5g) website from an archive file. WARNING: this overwrites the website's existing contents and cannot be undone — always confirm with the user before proceeding. Use this for Agency Plan websites where the archive contents should be extracted and served as-is with no build step (e.g., PHP applications). The tool resolves the website from its domain, uploads the archive to the website's file browser over TUS, and triggers the import-archive process which overwrites the website contents with the archive contents. This operation is synchronous: the archive is extracted and deployed before the tool returns, so the website is live as soon as the tool finishes successfully — there is no separate asynchronous build to wait for or poll. For node-static websites that require a build step, use agencyHosting_deployNodeStaticWebsite instead. The website UID is automatically resolved from the domain.
|
|
216
|
+
|
|
217
|
+
- **Method**: `custom`
|
|
218
|
+
- **Path**: `custom`
|
|
219
|
+
|
|
206
220
|
#### agency-hosting_listAvailableDatacentersForAnAgencyPlanOrderV1
|
|
207
221
|
|
|
208
222
|
Lists the datacenters available for provisioning a new website on the given Agency Plan
|
|
@@ -625,6 +639,16 @@ Use this endpoint to make domain owner's information publicly visible.
|
|
|
625
639
|
- **Method**: `DELETE`
|
|
626
640
|
- **Path**: `/api/domains/v1/portfolio/{domain}/privacy-protection`
|
|
627
641
|
|
|
642
|
+
#### domains_getDomainRenewalInformationV1
|
|
643
|
+
|
|
644
|
+
Retrieve renewal information for a specified domain, including its status and current
|
|
645
|
+
expiration date.
|
|
646
|
+
|
|
647
|
+
Use this endpoint to build renewal automation and expiry monitoring for a single domain.
|
|
648
|
+
|
|
649
|
+
- **Method**: `GET`
|
|
650
|
+
- **Path**: `/api/domains/v1/portfolio/{domain}/renewal`
|
|
651
|
+
|
|
628
652
|
#### domains_updateDomainNameserversV1
|
|
629
653
|
|
|
630
654
|
Set nameservers for a specified domain.
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/hostinger/api-mcp-server",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.7.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "hostinger-api-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.7.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/src/core/runtime.js
CHANGED
|
@@ -29,7 +29,7 @@ const SECURITY_SCHEMES = {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* MCP Server for Hostinger API
|
|
32
|
-
* Generated from OpenAPI spec version 1.7.
|
|
32
|
+
* Generated from OpenAPI spec version 1.7.1
|
|
33
33
|
*/
|
|
34
34
|
class MCPServer {
|
|
35
35
|
constructor({ name, version, tools }) {
|
|
@@ -197,6 +197,10 @@ class MCPServer {
|
|
|
197
197
|
|
|
198
198
|
async executeCustomTool(tool, params) {
|
|
199
199
|
switch (tool.name) {
|
|
200
|
+
case 'agencyHosting_deployNodeStaticWebsite':
|
|
201
|
+
return await this.handleNodeStaticDeploy(params);
|
|
202
|
+
case 'agencyHosting_deployPhpApplication':
|
|
203
|
+
return await this.handlePhpAppDeploy(params);
|
|
200
204
|
case 'hosting_importWordpressWebsite':
|
|
201
205
|
return await this.handleWordpressWebsiteImport(params);
|
|
202
206
|
case 'hosting_deployWordpressPlugin':
|
|
@@ -216,6 +220,533 @@ class MCPServer {
|
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
222
|
|
|
223
|
+
agencyHosting_normalizePath(pathString) {
|
|
224
|
+
return pathString.replace(/\\/g, '/');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
agencyHosting_generateUploadDir() {
|
|
228
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
229
|
+
let result = '';
|
|
230
|
+
for (let i = 0; i < 12; i++) {
|
|
231
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
agencyHosting_validateRequiredParams(params) {
|
|
237
|
+
const { domain, archivePath, removeArchive } = params;
|
|
238
|
+
|
|
239
|
+
if (!domain || typeof domain !== 'string') {
|
|
240
|
+
throw new Error('domain is required and must be a string');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!archivePath || typeof archivePath !== 'string') {
|
|
244
|
+
throw new Error('archivePath is required and must be a string');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
|
|
248
|
+
throw new Error('removeArchive must be a boolean if provided');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
agencyHosting_validateArchiveFile(archivePath) {
|
|
253
|
+
if (!fs.existsSync(archivePath)) {
|
|
254
|
+
throw new Error(`Archive file not found: ${archivePath}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const archiveStats = fs.statSync(archivePath);
|
|
258
|
+
if (!archiveStats.isFile()) {
|
|
259
|
+
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
agencyHosting_removeArchive(archivePath, removeArchive) {
|
|
264
|
+
if (!removeArchive) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
this.log('info', `Removing archive file: ${archivePath}`);
|
|
270
|
+
fs.unlinkSync(archivePath);
|
|
271
|
+
this.log('info', `Successfully removed archive file: ${archivePath}`);
|
|
272
|
+
return true;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
275
|
+
this.log('error', `Failed to remove archive file: ${errorMessage}`);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async agencyHosting_resolveWebsiteUid(domain) {
|
|
281
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
282
|
+
const perPage = 100;
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const bearerToken = await this.getAuthToken();
|
|
286
|
+
let page = 1;
|
|
287
|
+
let total = 0;
|
|
288
|
+
|
|
289
|
+
do {
|
|
290
|
+
const url = new URL(
|
|
291
|
+
`api/agency-hosting/v1/domains?page=${page}&per_page=${perPage}`,
|
|
292
|
+
baseUrl
|
|
293
|
+
).toString();
|
|
294
|
+
|
|
295
|
+
const config = {
|
|
296
|
+
method: 'get',
|
|
297
|
+
url,
|
|
298
|
+
headers: {
|
|
299
|
+
...this.headers,
|
|
300
|
+
'Authorization': `Bearer ${bearerToken}`
|
|
301
|
+
},
|
|
302
|
+
timeout: 60000, // 60s
|
|
303
|
+
validateStatus: function (status) {
|
|
304
|
+
return status < 500;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const response = await axios(config);
|
|
309
|
+
|
|
310
|
+
if (response.status !== 200) {
|
|
311
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const domains = response.data?.data ?? [];
|
|
315
|
+
const match = domains.find((item) => item.fqdn === domain);
|
|
316
|
+
if (match?.website_uid) {
|
|
317
|
+
this.log('info', `Resolved website UID: ${match.website_uid} for domain: ${domain}`);
|
|
318
|
+
return match.website_uid;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
total = response.data?.meta?.total ?? domains.length;
|
|
322
|
+
page++;
|
|
323
|
+
} while ((page - 1) * perPage < total);
|
|
324
|
+
|
|
325
|
+
throw new Error(`No Agency Plan website found for domain: ${domain}`);
|
|
326
|
+
|
|
327
|
+
} catch (error) {
|
|
328
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
329
|
+
this.log('error', `Failed to resolve website UID for domain ${domain}: ${errorMessage}`);
|
|
330
|
+
|
|
331
|
+
if (axios.isAxiosError(error)) {
|
|
332
|
+
const responseData = error.response?.data;
|
|
333
|
+
const responseStatus = error.response?.status;
|
|
334
|
+
this.log('error', 'API Error Details:', {
|
|
335
|
+
status: responseStatus,
|
|
336
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async agencyHosting_fetchUploadCredentials(websiteUid) {
|
|
345
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
346
|
+
const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/upload-urls`, baseUrl).toString();
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const bearerToken = await this.getAuthToken();
|
|
350
|
+
|
|
351
|
+
const config = {
|
|
352
|
+
method: 'post',
|
|
353
|
+
url,
|
|
354
|
+
headers: {
|
|
355
|
+
...this.headers,
|
|
356
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
357
|
+
'Content-Type': 'application/json'
|
|
358
|
+
},
|
|
359
|
+
timeout: 60000, // 60s
|
|
360
|
+
validateStatus: function (status) {
|
|
361
|
+
return status < 500;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const response = await axios(config);
|
|
366
|
+
|
|
367
|
+
if (response.status !== 200) {
|
|
368
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return response.data;
|
|
372
|
+
|
|
373
|
+
} catch (error) {
|
|
374
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
375
|
+
this.log('error', `Failed to fetch upload credentials: ${errorMessage}`);
|
|
376
|
+
|
|
377
|
+
if (axios.isAxiosError(error)) {
|
|
378
|
+
const responseData = error.response?.data;
|
|
379
|
+
const responseStatus = error.response?.status;
|
|
380
|
+
this.log('error', 'API Error Details:', {
|
|
381
|
+
status: responseStatus,
|
|
382
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async agencyHosting_uploadFile(filePath, relativePath, uploadUrl, authRestToken, authToken) {
|
|
391
|
+
return new Promise(async (resolve, reject) => {
|
|
392
|
+
try {
|
|
393
|
+
const stats = fs.statSync(filePath);
|
|
394
|
+
const fileStream = fs.createReadStream(filePath);
|
|
395
|
+
|
|
396
|
+
const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
|
|
397
|
+
const normalizedPath = this.agencyHosting_normalizePath(relativePath);
|
|
398
|
+
const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
|
|
399
|
+
|
|
400
|
+
const requestHeaders = {
|
|
401
|
+
'X-Auth': authToken,
|
|
402
|
+
'X-Auth-Rest': authRestToken,
|
|
403
|
+
'upload-length': stats.size.toString(),
|
|
404
|
+
'upload-offset': '0'
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
this.log('debug', `Making pre-upload POST request to ${uploadUrlWithFile}`);
|
|
409
|
+
await axios.post(uploadUrlWithFile, '', {
|
|
410
|
+
headers: requestHeaders,
|
|
411
|
+
timeout: 60000, // 60s
|
|
412
|
+
validateStatus: function (status) {
|
|
413
|
+
return status == 201;
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
} catch (error) {
|
|
417
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
418
|
+
|
|
419
|
+
if (axios.isAxiosError(error)) {
|
|
420
|
+
const responseData = error.response?.data;
|
|
421
|
+
const responseStatus = error.response?.status;
|
|
422
|
+
const responseHeaders = error.response?.headers;
|
|
423
|
+
const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
|
|
424
|
+
|
|
425
|
+
this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
|
|
426
|
+
status: responseStatus,
|
|
427
|
+
headers: responseHeaders,
|
|
428
|
+
data: responseText,
|
|
429
|
+
message: errorMessage
|
|
430
|
+
});
|
|
431
|
+
reject(new Error(`Pre-upload request failed: ${errorMessage}`));
|
|
432
|
+
return;
|
|
433
|
+
} else {
|
|
434
|
+
this.log('error', `Pre-upload POST request failed: ${errorMessage}`);
|
|
435
|
+
reject(new Error(`Pre-upload request failed: ${errorMessage}`));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const upload = new tus.Upload(fileStream, {
|
|
441
|
+
uploadUrl: uploadUrlWithFile,
|
|
442
|
+
retryDelays: [1000, 2000, 4000, 8000, 16000, 20000],
|
|
443
|
+
uploadDataDuringCreation: false,
|
|
444
|
+
parallelUploads: 1,
|
|
445
|
+
chunkSize: 10485760,
|
|
446
|
+
headers: requestHeaders,
|
|
447
|
+
removeFingerprintOnSuccess: true,
|
|
448
|
+
uploadSize: stats.size,
|
|
449
|
+
metadata: {
|
|
450
|
+
filename: path.basename(relativePath)
|
|
451
|
+
},
|
|
452
|
+
onError: (error) => {
|
|
453
|
+
this.log('error', `TUS upload error for ${relativePath}`, { error: error.message });
|
|
454
|
+
reject(new Error(`Upload failed: ${error.message}`));
|
|
455
|
+
},
|
|
456
|
+
onSuccess: () => {
|
|
457
|
+
this.log('info', `TUS upload completed for ${relativePath}`, { url: upload.url });
|
|
458
|
+
resolve({
|
|
459
|
+
url: upload.url || uploadUrlWithFile,
|
|
460
|
+
filename: relativePath
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
upload.start();
|
|
466
|
+
|
|
467
|
+
} catch (error) {
|
|
468
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
469
|
+
this.log('error', `Error preparing upload for ${filePath}`, { error: errorMessage });
|
|
470
|
+
reject(new Error(`Failed to prepare upload: ${errorMessage}`));
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid, archivePath) {
|
|
476
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
477
|
+
const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/build-assets`, baseUrl).toString();
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const bearerToken = await this.getAuthToken();
|
|
481
|
+
|
|
482
|
+
const config = {
|
|
483
|
+
method: 'post',
|
|
484
|
+
url,
|
|
485
|
+
headers: {
|
|
486
|
+
...this.headers,
|
|
487
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
488
|
+
'Content-Type': 'application/json'
|
|
489
|
+
},
|
|
490
|
+
data: {
|
|
491
|
+
archive_path: archivePath
|
|
492
|
+
},
|
|
493
|
+
timeout: 60000,
|
|
494
|
+
validateStatus: function (status) {
|
|
495
|
+
return status < 500;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const response = await axios(config);
|
|
500
|
+
|
|
501
|
+
if (response.status < 200 || response.status >= 300) {
|
|
502
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
this.log('info', `Successfully triggered build for website ${websiteUid}`);
|
|
506
|
+
return response.data;
|
|
507
|
+
|
|
508
|
+
} catch (error) {
|
|
509
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
510
|
+
this.log('error', `Failed to trigger build: ${errorMessage}`);
|
|
511
|
+
|
|
512
|
+
if (axios.isAxiosError(error)) {
|
|
513
|
+
const responseData = error.response?.data;
|
|
514
|
+
const responseStatus = error.response?.status;
|
|
515
|
+
this.log('error', 'API Error Details:', {
|
|
516
|
+
status: responseStatus,
|
|
517
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async handleNodeStaticDeploy(params) {
|
|
526
|
+
const { domain, archivePath, removeArchive = true } = params;
|
|
527
|
+
|
|
528
|
+
this.agencyHosting_validateRequiredParams(params);
|
|
529
|
+
this.agencyHosting_validateArchiveFile(archivePath);
|
|
530
|
+
|
|
531
|
+
this.log('info', `Resolving website UID from domain: ${domain}`);
|
|
532
|
+
const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
|
|
533
|
+
|
|
534
|
+
this.log('info', `Starting archive upload for ${domain}`);
|
|
535
|
+
|
|
536
|
+
let uploadCredentials;
|
|
537
|
+
try {
|
|
538
|
+
uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
541
|
+
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
|
|
545
|
+
|
|
546
|
+
if (!uploadUrl || !authToken || !authRestToken) {
|
|
547
|
+
throw new Error('Invalid upload credentials received from API');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const archiveBasename = path.basename(archivePath);
|
|
551
|
+
const uploadDir = `.h5g/${this.agencyHosting_generateUploadDir()}`;
|
|
552
|
+
let uploadResult;
|
|
553
|
+
try {
|
|
554
|
+
uploadResult = await this.agencyHosting_uploadFile(
|
|
555
|
+
archivePath,
|
|
556
|
+
`${uploadDir}/${archiveBasename}`,
|
|
557
|
+
uploadUrl,
|
|
558
|
+
authRestToken,
|
|
559
|
+
authToken
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
565
|
+
throw new Error(`Failed to upload archive: ${errorMessage}`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let deployResult;
|
|
569
|
+
try {
|
|
570
|
+
this.log('info', `Triggering build for ${domain}`);
|
|
571
|
+
deployResult = await this.agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid, uploadDir);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
574
|
+
this.log('error', `Failed to trigger build: ${errorMessage}`);
|
|
575
|
+
const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
upload: {
|
|
579
|
+
status: 'success',
|
|
580
|
+
data: {
|
|
581
|
+
filename: uploadResult.filename
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
deploy: {
|
|
585
|
+
status: 'error',
|
|
586
|
+
error: errorMessage
|
|
587
|
+
},
|
|
588
|
+
removeArchive: {
|
|
589
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
upload: {
|
|
598
|
+
status: 'success',
|
|
599
|
+
data: {
|
|
600
|
+
filename: uploadResult.filename
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
deploy: {
|
|
604
|
+
status: 'success',
|
|
605
|
+
data: deployResult
|
|
606
|
+
},
|
|
607
|
+
removeArchive: {
|
|
608
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async agencyHosting_deployPhpApp_triggerImportArchive(websiteUid, archiveName) {
|
|
614
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
615
|
+
const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/import-archive`, baseUrl).toString();
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
const bearerToken = await this.getAuthToken();
|
|
619
|
+
|
|
620
|
+
const config = {
|
|
621
|
+
method: 'post',
|
|
622
|
+
url,
|
|
623
|
+
headers: {
|
|
624
|
+
...this.headers,
|
|
625
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
626
|
+
'Content-Type': 'application/json'
|
|
627
|
+
},
|
|
628
|
+
data: {
|
|
629
|
+
archive_name: archiveName
|
|
630
|
+
},
|
|
631
|
+
timeout: 60000,
|
|
632
|
+
validateStatus: function (status) {
|
|
633
|
+
return status < 500;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const response = await axios(config);
|
|
638
|
+
|
|
639
|
+
if (response.status < 200 || response.status >= 300) {
|
|
640
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
this.log('info', `Successfully triggered import for website ${websiteUid}`);
|
|
644
|
+
return response.data;
|
|
645
|
+
|
|
646
|
+
} catch (error) {
|
|
647
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
648
|
+
this.log('error', `Failed to trigger import: ${errorMessage}`);
|
|
649
|
+
|
|
650
|
+
if (axios.isAxiosError(error)) {
|
|
651
|
+
const responseData = error.response?.data;
|
|
652
|
+
const responseStatus = error.response?.status;
|
|
653
|
+
this.log('error', 'API Error Details:', {
|
|
654
|
+
status: responseStatus,
|
|
655
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async handlePhpAppDeploy(params) {
|
|
664
|
+
const { domain, archivePath, removeArchive = true } = params;
|
|
665
|
+
|
|
666
|
+
this.agencyHosting_validateRequiredParams(params);
|
|
667
|
+
this.agencyHosting_validateArchiveFile(archivePath);
|
|
668
|
+
|
|
669
|
+
this.log('info', `Resolving website UID from domain: ${domain}`);
|
|
670
|
+
const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
|
|
671
|
+
|
|
672
|
+
this.log('info', `Starting archive upload for ${domain}`);
|
|
673
|
+
|
|
674
|
+
let uploadCredentials;
|
|
675
|
+
try {
|
|
676
|
+
uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
679
|
+
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
|
|
683
|
+
|
|
684
|
+
if (!uploadUrl || !authToken || !authRestToken) {
|
|
685
|
+
throw new Error('Invalid upload credentials received from API');
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const archiveBasename = path.basename(archivePath);
|
|
689
|
+
let uploadResult;
|
|
690
|
+
try {
|
|
691
|
+
uploadResult = await this.agencyHosting_uploadFile(
|
|
692
|
+
archivePath,
|
|
693
|
+
`.h5g/${archiveBasename}`,
|
|
694
|
+
uploadUrl,
|
|
695
|
+
authRestToken,
|
|
696
|
+
authToken
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
702
|
+
throw new Error(`Failed to upload archive: ${errorMessage}`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
let deployResult;
|
|
706
|
+
try {
|
|
707
|
+
this.log('info', `Triggering import for ${domain}`);
|
|
708
|
+
deployResult = await this.agencyHosting_deployPhpApp_triggerImportArchive(websiteUid, archiveBasename);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
711
|
+
this.log('error', `Failed to trigger import: ${errorMessage}`);
|
|
712
|
+
const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
|
|
713
|
+
|
|
714
|
+
return {
|
|
715
|
+
upload: {
|
|
716
|
+
status: 'success',
|
|
717
|
+
data: {
|
|
718
|
+
filename: uploadResult.filename
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
deploy: {
|
|
722
|
+
status: 'error',
|
|
723
|
+
error: errorMessage
|
|
724
|
+
},
|
|
725
|
+
removeArchive: {
|
|
726
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
|
|
732
|
+
|
|
733
|
+
return {
|
|
734
|
+
upload: {
|
|
735
|
+
status: 'success',
|
|
736
|
+
data: {
|
|
737
|
+
filename: uploadResult.filename
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
deploy: {
|
|
741
|
+
status: 'success',
|
|
742
|
+
data: deployResult
|
|
743
|
+
},
|
|
744
|
+
removeArchive: {
|
|
745
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
219
750
|
normalizePath(pathString) {
|
|
220
751
|
return pathString.replace(/\\/g, '/');
|
|
221
752
|
}
|