@hostinger/mcp 3.1.0 → 3.2.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/CHANGELOG.md +33 -0
- package/README.md +47 -301
- package/gemini-extension.json +1 -1
- package/package.json +2 -1
- package/server.json +2 -2
- package/skills/audit-hosting/SKILL.md +1 -1
- package/skills/deploy-to-hosting/SKILL.md +32 -15
- package/skills/headless/references/DATABASE.md +3 -3
- package/skills/headless/references/DEPLOYMENT.md +10 -12
- package/skills/migrate-to-hosting/SKILL.md +3 -3
- package/skills/troubleshoot-website/SKILL.md +2 -2
- package/src/core/catalog.js +4 -0
- package/src/core/catalog.ts +6 -1
- package/src/core/instructions.agency-hosting.js +2 -0
- package/src/core/instructions.agency-hosting.ts +3 -0
- package/src/core/instructions.js +1 -1
- package/src/core/instructions.ts +1 -1
- package/src/core/runtime.js +1925 -1228
- package/src/core/runtime.ts +1891 -1193
- package/src/core/skills.js +18 -18
- package/src/core/skills.ts +18 -18
- package/src/core/tools/agency-hosting.ts +2 -0
- package/src/core/tools/agent-readiness.ts +2 -0
- package/src/core/tools/all.js +223 -30
- package/src/core/tools/all.ts +225 -30
- package/src/core/tools/billing.ts +2 -0
- package/src/core/tools/dns.ts +2 -0
- package/src/core/tools/domains.ts +2 -0
- package/src/core/tools/ecommerce.ts +2 -0
- package/src/core/tools/horizons.ts +2 -0
- package/src/core/tools/hosting.js +207 -25
- package/src/core/tools/hosting.ts +209 -25
- package/src/core/tools/mail.ts +2 -0
- package/src/core/tools/reach.ts +2 -0
- package/src/core/tools/vps.ts +2 -0
- package/src/core/tools/wordpress.js +10 -2
- package/src/core/tools/wordpress.ts +12 -2
- package/src/servers/agency-hosting.js +2 -2
- package/src/servers/agency-hosting.ts +2 -2
- package/src/servers/agent-readiness.js +1 -1
- package/src/servers/agent-readiness.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/mail.js +1 -1
- package/src/servers/mail.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 +131 -22
package/src/core/runtime.ts
CHANGED
|
@@ -23,6 +23,8 @@ import axios,{ AxiosRequestConfig, AxiosError, AxiosResponse } from "axios";
|
|
|
23
23
|
import * as tus from "tus-js-client";
|
|
24
24
|
import fs from "fs";
|
|
25
25
|
import path from "path";
|
|
26
|
+
import os from "os";
|
|
27
|
+
import { Zip, ZipDeflate } from "fflate";
|
|
26
28
|
import { META_TOOLS, searchOperations, renderSearch, findOperation, runSteps } from "./catalog.js";
|
|
27
29
|
import type { CatalogOperation } from "./catalog.js";
|
|
28
30
|
|
|
@@ -35,6 +37,7 @@ interface OpenApiTool extends Tool {
|
|
|
35
37
|
path: string;
|
|
36
38
|
security: unknown[];
|
|
37
39
|
custom?: boolean;
|
|
40
|
+
hidden?: boolean;
|
|
38
41
|
group?: string;
|
|
39
42
|
topic?: string;
|
|
40
43
|
handlerMethod?: string;
|
|
@@ -94,7 +97,7 @@ interface Skill { uri: string; frontmatter: Record<string, unknown>; resources:
|
|
|
94
97
|
|
|
95
98
|
/**
|
|
96
99
|
* MCP Server for Hostinger API
|
|
97
|
-
* Generated from OpenAPI spec version 2.1.
|
|
100
|
+
* Generated from OpenAPI spec version 2.1.1
|
|
98
101
|
*/
|
|
99
102
|
class MCPServer {
|
|
100
103
|
private readonly name: string;
|
|
@@ -303,6 +306,9 @@ class MCPServer {
|
|
|
303
306
|
private async executeOperation(operation: unknown, params: unknown): Promise<any> {
|
|
304
307
|
const tool = findOperation(this.operations, operation, params);
|
|
305
308
|
const input = params as Record<string, any>;
|
|
309
|
+
if (tool.hidden) {
|
|
310
|
+
this.log('warning', `Operation ${tool.name} is hidden from search and will be removed in a future release`);
|
|
311
|
+
}
|
|
306
312
|
this.log('info', `Executing operation: ${tool.name}`);
|
|
307
313
|
return tool.custom
|
|
308
314
|
? this.executeCustomTool(tool, input)
|
|
@@ -323,10 +329,10 @@ class MCPServer {
|
|
|
323
329
|
|
|
324
330
|
private async executeCustomTool(tool: OpenApiTool, params: Record<string, any>): Promise<any> {
|
|
325
331
|
switch (tool.name) {
|
|
326
|
-
case '
|
|
327
|
-
return await this.
|
|
328
|
-
case '
|
|
329
|
-
return await this.
|
|
332
|
+
case 'hosting_deploy':
|
|
333
|
+
return await this.handleHostingDeploy(params);
|
|
334
|
+
case 'hosting_get-deployment':
|
|
335
|
+
return await this.handleHostingGetDeployment(params);
|
|
330
336
|
case 'hosting_import-wordpress-website':
|
|
331
337
|
return await this.handleWordpressWebsiteImport(params);
|
|
332
338
|
case 'hosting_deploy-wordpress-plugin':
|
|
@@ -341,119 +347,71 @@ class MCPServer {
|
|
|
341
347
|
return await this.handleListJavascriptDeployments(params);
|
|
342
348
|
case 'hosting_show-js-deployment-logs':
|
|
343
349
|
return await this.handleShowJsDeploymentLogs(params);
|
|
350
|
+
case 'agency-hosting_deploy-node-static-website':
|
|
351
|
+
return await this.handleNodeStaticDeploy(params);
|
|
352
|
+
case 'agency-hosting_deploy-php-application':
|
|
353
|
+
return await this.handlePhpAppDeploy(params);
|
|
344
354
|
default:
|
|
345
355
|
throw new Error(`Unknown custom tool: ${tool.name}`);
|
|
346
356
|
}
|
|
347
357
|
}
|
|
348
358
|
|
|
349
|
-
private
|
|
359
|
+
private normalizePath(pathString: string): string {
|
|
350
360
|
return pathString.replace(/\\/g, '/');
|
|
351
361
|
}
|
|
352
362
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return result;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
private agencyHosting_validateRequiredParams(params: Record<string, any>): void {
|
|
363
|
-
const { domain, archivePath, removeArchive } = params;
|
|
364
|
-
|
|
365
|
-
if (!domain || typeof domain !== 'string') {
|
|
366
|
-
throw new Error('domain is required and must be a string');
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (!archivePath || typeof archivePath !== 'string') {
|
|
370
|
-
throw new Error('archivePath is required and must be a string');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
|
|
374
|
-
throw new Error('removeArchive must be a boolean if provided');
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
private agencyHosting_validateArchiveFile(archivePath: string): void {
|
|
379
|
-
if (!fs.existsSync(archivePath)) {
|
|
380
|
-
throw new Error(`Archive file not found: ${archivePath}`);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const archiveStats = fs.statSync(archivePath);
|
|
384
|
-
if (!archiveStats.isFile()) {
|
|
385
|
-
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
private agencyHosting_removeArchive(archivePath: string, removeArchive: boolean): boolean {
|
|
390
|
-
if (!removeArchive) {
|
|
391
|
-
return false;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
this.log('info', `Removing archive file: ${archivePath}`);
|
|
396
|
-
fs.unlinkSync(archivePath);
|
|
397
|
-
this.log('info', `Successfully removed archive file: ${archivePath}`);
|
|
398
|
-
return true;
|
|
399
|
-
} catch (error) {
|
|
400
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
401
|
-
this.log('error', `Failed to remove archive file: ${errorMessage}`);
|
|
402
|
-
return false;
|
|
363
|
+
// The path becomes part of a URL, where a . or .. segment resolves outside its directory. Separators are
|
|
364
|
+
// normalized first, because a backslash in a file name on Linux or macOS turns into one.
|
|
365
|
+
private assertSafeUploadPath(relativePath: string): void {
|
|
366
|
+
const segments = this.normalizePath(relativePath).split('/');
|
|
367
|
+
if (segments.some((segment: string) => segment === '' || segment === '.' || segment === '..')) {
|
|
368
|
+
throw new Error('Unsafe upload path: ' + relativePath);
|
|
403
369
|
}
|
|
404
370
|
}
|
|
405
371
|
|
|
406
|
-
private async
|
|
372
|
+
private async resolveUsername(domain: string): Promise<string> {
|
|
407
373
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
408
|
-
const
|
|
409
|
-
|
|
374
|
+
const url = new URL(`api/hosting/v1/websites?domain=${encodeURIComponent(domain)}`, baseUrl).toString();
|
|
375
|
+
|
|
410
376
|
try {
|
|
411
377
|
const bearerToken = await this.getAuthToken();
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
url,
|
|
424
|
-
headers: {
|
|
425
|
-
...this.headers,
|
|
426
|
-
'Authorization': `Bearer ${bearerToken}`
|
|
427
|
-
},
|
|
428
|
-
timeout: 60000, // 60s
|
|
429
|
-
validateStatus: function (status: number): boolean {
|
|
430
|
-
return status < 500;
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const response = await axios(config);
|
|
435
|
-
|
|
436
|
-
if (response.status !== 200) {
|
|
437
|
-
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const domains = response.data?.data ?? [];
|
|
441
|
-
const match = domains.find((item: any) => item.fqdn === domain);
|
|
442
|
-
if (match?.website_uid) {
|
|
443
|
-
this.log('info', `Resolved website UID: ${match.website_uid} for domain: ${domain}`);
|
|
444
|
-
return match.website_uid;
|
|
378
|
+
|
|
379
|
+
const config: AxiosRequestConfig = {
|
|
380
|
+
method: 'get',
|
|
381
|
+
url,
|
|
382
|
+
headers: {
|
|
383
|
+
...this.headers,
|
|
384
|
+
'Authorization': `Bearer ${bearerToken}`
|
|
385
|
+
},
|
|
386
|
+
timeout: 60000, // 60s
|
|
387
|
+
validateStatus: function (status: number): boolean {
|
|
388
|
+
return status < 500;
|
|
445
389
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const response = await axios(config);
|
|
393
|
+
|
|
394
|
+
if (response.status !== 200) {
|
|
395
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const websites = response.data?.data;
|
|
399
|
+
if (!websites || websites.length === 0) {
|
|
400
|
+
throw new Error(`No website found for domain: ${domain}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const username = websites[0].username;
|
|
404
|
+
if (!username) {
|
|
405
|
+
throw new Error(`Username not found in website data for domain: ${domain}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.log('info', `Resolved username: ${username} for domain: ${domain}`);
|
|
409
|
+
return username;
|
|
410
|
+
|
|
453
411
|
} catch (error) {
|
|
454
412
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
455
|
-
this.log('error', `Failed to resolve
|
|
456
|
-
|
|
413
|
+
this.log('error', `Failed to resolve username for domain ${domain}: ${errorMessage}`);
|
|
414
|
+
|
|
457
415
|
if (axios.isAxiosError(error)) {
|
|
458
416
|
const responseData = error.response?.data;
|
|
459
417
|
const responseStatus = error.response?.status;
|
|
@@ -462,16 +420,14 @@ class MCPServer {
|
|
|
462
420
|
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
463
421
|
});
|
|
464
422
|
}
|
|
465
|
-
|
|
423
|
+
|
|
466
424
|
throw error;
|
|
467
425
|
}
|
|
468
426
|
}
|
|
469
427
|
|
|
470
|
-
private async
|
|
471
|
-
websiteUid: string
|
|
472
|
-
): Promise<{ url: string; auth_key: string; rest_auth_key: string }> {
|
|
428
|
+
private async fetchUploadCredentials(username: string, domain: string): Promise<{ uploadUrl: string; authRestToken: string; authToken: string }> {
|
|
473
429
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
474
|
-
const url = new URL(
|
|
430
|
+
const url = new URL('api/hosting/v1/files/upload-urls', baseUrl).toString();
|
|
475
431
|
|
|
476
432
|
try {
|
|
477
433
|
const bearerToken = await this.getAuthToken();
|
|
@@ -484,6 +440,10 @@ class MCPServer {
|
|
|
484
440
|
'Authorization': `Bearer ${bearerToken}`,
|
|
485
441
|
'Content-Type': 'application/json'
|
|
486
442
|
},
|
|
443
|
+
data: {
|
|
444
|
+
username,
|
|
445
|
+
domain
|
|
446
|
+
},
|
|
487
447
|
timeout: 60000, // 60s
|
|
488
448
|
validateStatus: function (status: number): boolean {
|
|
489
449
|
return status < 500;
|
|
@@ -515,20 +475,23 @@ class MCPServer {
|
|
|
515
475
|
}
|
|
516
476
|
}
|
|
517
477
|
|
|
518
|
-
private async
|
|
519
|
-
filePath: string,
|
|
520
|
-
relativePath: string,
|
|
521
|
-
uploadUrl: string,
|
|
522
|
-
authRestToken: string,
|
|
478
|
+
private async uploadFile(
|
|
479
|
+
filePath: string,
|
|
480
|
+
relativePath: string,
|
|
481
|
+
uploadUrl: string,
|
|
482
|
+
authRestToken: string,
|
|
523
483
|
authToken: string
|
|
524
484
|
): Promise<{ url: string; filename: string }> {
|
|
525
485
|
return new Promise(async (resolve, reject) => {
|
|
526
486
|
try {
|
|
527
487
|
const stats = fs.statSync(filePath);
|
|
528
488
|
const fileStream = fs.createReadStream(filePath);
|
|
529
|
-
|
|
489
|
+
|
|
490
|
+
this.assertSafeUploadPath(relativePath);
|
|
530
491
|
const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
|
|
531
|
-
|
|
492
|
+
// The path is part of a URL: encode each segment so a #, ? or % in a file name is not read as
|
|
493
|
+
// a fragment, a query or an escape, and keep the / separators.
|
|
494
|
+
const normalizedPath = this.normalizePath(relativePath).split('/').map((segment: string) => encodeURIComponent(segment)).join('/');
|
|
532
495
|
const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
|
|
533
496
|
|
|
534
497
|
const requestHeaders: Record<string, string> = {
|
|
@@ -549,13 +512,13 @@ class MCPServer {
|
|
|
549
512
|
});
|
|
550
513
|
} catch (error) {
|
|
551
514
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
552
|
-
|
|
515
|
+
|
|
553
516
|
if (axios.isAxiosError(error)) {
|
|
554
517
|
const responseData = error.response?.data;
|
|
555
518
|
const responseStatus = error.response?.status;
|
|
556
519
|
const responseHeaders = error.response?.headers;
|
|
557
520
|
const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
|
|
558
|
-
|
|
521
|
+
|
|
559
522
|
this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
|
|
560
523
|
status: responseStatus,
|
|
561
524
|
headers: responseHeaders,
|
|
@@ -606,292 +569,748 @@ class MCPServer {
|
|
|
606
569
|
});
|
|
607
570
|
}
|
|
608
571
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
572
|
+
// Run a catalog operation by name through the runtime's own request code (auth,
|
|
573
|
+
// path parameters, API errors). Hidden operations are still in the catalog, and
|
|
574
|
+
// calling them here does not log the deprecation warning that execute does.
|
|
575
|
+
private async callOperation(name: string, params: Record<string, any>): Promise<any> {
|
|
576
|
+
const tool = this.operations.get(name);
|
|
577
|
+
if (!tool) {
|
|
578
|
+
throw new Error(`Operation ${name} is not available in this server`);
|
|
579
|
+
}
|
|
580
|
+
return this.executeApiCall(tool, params);
|
|
581
|
+
}
|
|
612
582
|
|
|
613
|
-
|
|
614
|
-
|
|
583
|
+
// The website for a domain, matched exactly (the list endpoint filters by substring).
|
|
584
|
+
private async findWebsite(domain: string): Promise<any> {
|
|
585
|
+
const response = await this.callOperation('hosting_websites_list', { domain });
|
|
586
|
+
const sites = response && Array.isArray(response.data) ? response.data : [];
|
|
587
|
+
const site = sites.find((website: any) => website.domain && website.domain.toLowerCase() === domain.toLowerCase());
|
|
588
|
+
if (!site) {
|
|
589
|
+
throw new Error('No website found for domain: ' + domain);
|
|
590
|
+
}
|
|
591
|
+
return site;
|
|
592
|
+
}
|
|
615
593
|
|
|
616
|
-
|
|
617
|
-
method: 'post',
|
|
618
|
-
url,
|
|
619
|
-
headers: {
|
|
620
|
-
...this.headers,
|
|
621
|
-
'Authorization': `Bearer ${bearerToken}`,
|
|
622
|
-
'Content-Type': 'application/json'
|
|
623
|
-
},
|
|
624
|
-
data: {
|
|
625
|
-
archive_path: archivePath
|
|
626
|
-
},
|
|
627
|
-
timeout: 60000,
|
|
628
|
-
validateStatus: function (status: number): boolean {
|
|
629
|
-
return status < 500;
|
|
630
|
-
}
|
|
631
|
-
};
|
|
594
|
+
private hosting_deploy_types = ['static', 'nodejs', 'wordpress-site', 'wordpress-plugin', 'wordpress-theme'];
|
|
632
595
|
|
|
633
|
-
|
|
596
|
+
// A package.json that depends on one of these is an app with a server, not dev tooling.
|
|
597
|
+
private hosting_deploy_serverDependencies = ['express', 'fastify', 'koa', 'hono', 'next', 'nuxt', '@nestjs/core', '@remix-run/node', '@sveltejs/kit', 'astro'];
|
|
634
598
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
599
|
+
// WordPress reads plugin and theme headers from the first 8 KB of a file.
|
|
600
|
+
private hosting_deploy_head(file: string): string {
|
|
601
|
+
const fd = fs.openSync(file, 'r');
|
|
602
|
+
try {
|
|
603
|
+
const buffer = Buffer.alloc(8192);
|
|
604
|
+
const read = fs.readSync(fd, buffer, 0, 8192, 0);
|
|
605
|
+
return buffer.toString('utf8', 0, read).toLowerCase();
|
|
606
|
+
} finally {
|
|
607
|
+
fs.closeSync(fd);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
638
610
|
|
|
639
|
-
|
|
640
|
-
|
|
611
|
+
// What a directory is, from files in its top level. WordPress signals win over package.json,
|
|
612
|
+
// because plugins and themes routinely carry one for their assets. Two WordPress signals at
|
|
613
|
+
// once are ambiguous and need an explicit type.
|
|
614
|
+
private hosting_deploy_detectType(dir: string): string {
|
|
615
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
616
|
+
throw new Error('Directory not found: ' + dir);
|
|
617
|
+
}
|
|
618
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
619
|
+
const hasFile = (name: string): boolean => entries.some((entry) => entry.isFile() && entry.name === name);
|
|
641
620
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
621
|
+
const wordpress: string[] = [];
|
|
622
|
+
if (entries.some((entry) => entry.isFile() && entry.name.endsWith('.php') && this.hosting_deploy_head(path.join(dir, entry.name)).includes('plugin name:'))) {
|
|
623
|
+
wordpress.push('wordpress-plugin');
|
|
624
|
+
}
|
|
625
|
+
if (hasFile('style.css') && this.hosting_deploy_head(path.join(dir, 'style.css')).includes('theme name:')) {
|
|
626
|
+
wordpress.push('wordpress-theme');
|
|
627
|
+
}
|
|
628
|
+
if (hasFile('wp-config.php') || entries.some((entry) => entry.isDirectory() && entry.name === 'wp-content')) {
|
|
629
|
+
wordpress.push('wordpress-site');
|
|
630
|
+
}
|
|
631
|
+
if (wordpress.length > 1) {
|
|
632
|
+
throw new Error('Ambiguous directory: it looks like ' + wordpress.join(' and ') + '. Pass type to choose.');
|
|
633
|
+
}
|
|
634
|
+
if (wordpress.length === 1) {
|
|
635
|
+
return wordpress[0];
|
|
636
|
+
}
|
|
645
637
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
638
|
+
let pkg: any = null;
|
|
639
|
+
const manifest = path.join(dir, 'package.json');
|
|
640
|
+
if (fs.existsSync(manifest)) {
|
|
641
|
+
try {
|
|
642
|
+
pkg = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
643
|
+
} catch (error) {
|
|
644
|
+
throw new Error('package.json could not be read (' + (error instanceof Error ? error.message : String(error)) + '): fix it, or pass type to skip detection');
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (pkg) {
|
|
648
|
+
const dependencies = pkg.dependencies || {};
|
|
649
|
+
const hasServer = this.hosting_deploy_serverDependencies.some((name) => dependencies[name] !== undefined);
|
|
650
|
+
if ((pkg.scripts && (pkg.scripts.build || pkg.scripts.start)) || hasServer) {
|
|
651
|
+
return 'nodejs';
|
|
653
652
|
}
|
|
654
|
-
|
|
655
|
-
throw error;
|
|
656
653
|
}
|
|
654
|
+
return 'static';
|
|
657
655
|
}
|
|
658
656
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
this.agencyHosting_validateArchiveFile(archivePath);
|
|
664
|
-
|
|
665
|
-
this.log('info', `Resolving website UID from domain: ${domain}`);
|
|
666
|
-
const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
|
|
667
|
-
|
|
668
|
-
this.log('info', `Starting archive upload for ${domain}`);
|
|
669
|
-
|
|
670
|
-
let uploadCredentials: any;
|
|
657
|
+
// The explicit type if given, else detected from a directory. Archives cannot be inspected.
|
|
658
|
+
private hosting_deploy_resolveType(params: Record<string, any>): string {
|
|
659
|
+
const { path: sourcePath, type, databaseDump } = params;
|
|
660
|
+
let stats: fs.Stats;
|
|
671
661
|
try {
|
|
672
|
-
|
|
662
|
+
stats = fs.statSync(sourcePath);
|
|
673
663
|
} catch (error) {
|
|
674
|
-
|
|
675
|
-
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
664
|
+
throw new Error('Path not found: ' + sourcePath);
|
|
676
665
|
}
|
|
677
666
|
|
|
678
|
-
|
|
667
|
+
if (type !== undefined && !this.hosting_deploy_types.includes(type)) {
|
|
668
|
+
throw new Error('type must be one of: ' + this.hosting_deploy_types.join(', '));
|
|
669
|
+
}
|
|
670
|
+
let resolved = type;
|
|
671
|
+
if (resolved === undefined) {
|
|
672
|
+
if (!stats.isDirectory()) {
|
|
673
|
+
throw new Error('The path is an archive, which cannot be inspected: pass type (static, nodejs or wordpress-site), or pass the directory to detect it');
|
|
674
|
+
}
|
|
675
|
+
resolved = this.hosting_deploy_detectType(sourcePath);
|
|
676
|
+
}
|
|
677
|
+
if ((resolved === 'wordpress-plugin' || resolved === 'wordpress-theme') && !stats.isDirectory()) {
|
|
678
|
+
throw new Error('A ' + (resolved === 'wordpress-plugin' ? 'plugin' : 'theme') + ' deploy needs a directory, not a file');
|
|
679
|
+
}
|
|
680
|
+
if (resolved === 'wordpress-site' && !databaseDump) {
|
|
681
|
+
throw new Error('databaseDump (path to a .sql file) is required to import a WordPress site');
|
|
682
|
+
}
|
|
683
|
+
return resolved;
|
|
684
|
+
}
|
|
679
685
|
|
|
680
|
-
|
|
681
|
-
|
|
686
|
+
// A WordPress slug from a directory name: lower case letters, digits, "-" and "_".
|
|
687
|
+
private hosting_deploy_slug(dir: string): string {
|
|
688
|
+
const base = path.basename(dir);
|
|
689
|
+
const slug = base.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
690
|
+
if (!slug) {
|
|
691
|
+
throw new Error('Cannot derive a slug from "' + base + '"; pass slug');
|
|
682
692
|
}
|
|
693
|
+
return slug;
|
|
694
|
+
}
|
|
683
695
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
archivePath,
|
|
690
|
-
`${uploadDir}/${archiveBasename}`,
|
|
691
|
-
uploadUrl,
|
|
692
|
-
authRestToken,
|
|
693
|
-
authToken
|
|
694
|
-
);
|
|
696
|
+
// .env and .env.* hold secrets, and one in a document root can be fetched over HTTP; the platform
|
|
697
|
+
// takes variables from its own settings, so they are never packed. Examples are kept.
|
|
698
|
+
private hosting_deploy_isEnvFile(name: string): boolean {
|
|
699
|
+
return name === '.env' || (name.startsWith('.env.') && !['.env.example', '.env.sample', '.env.template', '.env.dist'].includes(name));
|
|
700
|
+
}
|
|
695
701
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
702
|
+
// The files to ship, as sorted forward-slash paths relative to dir, and the .env files left out.
|
|
703
|
+
// node_modules, .git and OS junk are skipped; symlinks are not followed. .gitignore is not read:
|
|
704
|
+
// keep build output out of the directory.
|
|
705
|
+
// exclude is a file that is uploaded on its own (the database dump) and must not also travel inside
|
|
706
|
+
// the archive, where extracting it would leave a second, downloadable copy.
|
|
707
|
+
private hosting_deploy_listFiles(dir: string, exclude?: string): { files: string[]; skipped: string[] } {
|
|
708
|
+
const files: string[] = [];
|
|
709
|
+
const skipped: string[] = [];
|
|
710
|
+
const walk = (relativeDir: string): void => {
|
|
711
|
+
for (const entry of fs.readdirSync(path.join(dir, relativeDir), { withFileTypes: true })) {
|
|
712
|
+
if (entry.isSymbolicLink()) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const relativePath = relativeDir ? relativeDir + '/' + entry.name : entry.name;
|
|
716
|
+
if (entry.isDirectory()) {
|
|
717
|
+
if (entry.name !== 'node_modules' && entry.name !== '.git') {
|
|
718
|
+
walk(relativePath);
|
|
719
|
+
}
|
|
720
|
+
} else if (entry.isFile() && entry.name !== '.DS_Store' && path.resolve(dir, relativePath) !== exclude) {
|
|
721
|
+
if (this.hosting_deploy_isEnvFile(entry.name)) {
|
|
722
|
+
skipped.push(relativePath);
|
|
723
|
+
} else {
|
|
724
|
+
files.push(relativePath);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
walk('');
|
|
730
|
+
return { files: files.sort(), skipped: skipped.sort() };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Adds what was left out to a result, so it is never silent.
|
|
734
|
+
private hosting_deploy_withSkipped(result: any, skipped: string[]): any {
|
|
735
|
+
if (skipped.length === 0) {
|
|
736
|
+
return result;
|
|
700
737
|
}
|
|
738
|
+
return {
|
|
739
|
+
...result,
|
|
740
|
+
skippedFiles: skipped,
|
|
741
|
+
skippedNote: 'These .env files were not uploaded. Set their values with hosting_nodejs_replace-environment-variables.',
|
|
742
|
+
};
|
|
743
|
+
}
|
|
701
744
|
|
|
702
|
-
|
|
745
|
+
private async hosting_deploy_createZip(dir: string, files: string[], dest: string): Promise<void> {
|
|
746
|
+
const out = fs.createWriteStream(dest);
|
|
747
|
+
const finished = new Promise<void>((resolve, reject) => {
|
|
748
|
+
out.on('finish', () => resolve());
|
|
749
|
+
out.on('error', reject);
|
|
750
|
+
});
|
|
751
|
+
// The error is reported by the final await; this keeps an early one from being an unhandled rejection.
|
|
752
|
+
finished.catch(() => {});
|
|
753
|
+
const zip = new Zip((error, chunk, final) => {
|
|
754
|
+
if (error) {
|
|
755
|
+
out.destroy(error);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
out.write(chunk);
|
|
759
|
+
if (final) {
|
|
760
|
+
out.end();
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
// When the output stream is full, stop reading and compressing until it drains, so a large site cannot
|
|
764
|
+
// pile up in memory; a write error ends the wait.
|
|
765
|
+
const drained = async (): Promise<void> => {
|
|
766
|
+
if (out.writableNeedDrain) {
|
|
767
|
+
await Promise.race([new Promise<void>((resolve) => out.once('drain', () => resolve())), finished]);
|
|
768
|
+
}
|
|
769
|
+
};
|
|
703
770
|
try {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
upload: {
|
|
713
|
-
status: 'success',
|
|
714
|
-
data: {
|
|
715
|
-
filename: uploadResult.filename
|
|
771
|
+
for (const relativePath of files) {
|
|
772
|
+
const entry = new ZipDeflate(relativePath, { level: 6 });
|
|
773
|
+
zip.add(entry);
|
|
774
|
+
for await (const chunk of fs.createReadStream(path.join(dir, relativePath))) {
|
|
775
|
+
entry.push(chunk);
|
|
776
|
+
await drained();
|
|
777
|
+
if (out.destroyed) {
|
|
778
|
+
break;
|
|
716
779
|
}
|
|
717
|
-
},
|
|
718
|
-
deploy: {
|
|
719
|
-
status: 'error',
|
|
720
|
-
error: errorMessage
|
|
721
|
-
},
|
|
722
|
-
removeArchive: {
|
|
723
|
-
status: archiveRemoved ? 'success' : 'skipped'
|
|
724
780
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
|
|
729
|
-
|
|
730
|
-
return {
|
|
731
|
-
upload: {
|
|
732
|
-
status: 'success',
|
|
733
|
-
data: {
|
|
734
|
-
filename: uploadResult.filename
|
|
781
|
+
if (out.destroyed) {
|
|
782
|
+
break;
|
|
735
783
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
status: 'success',
|
|
739
|
-
data: deployResult
|
|
740
|
-
},
|
|
741
|
-
removeArchive: {
|
|
742
|
-
status: archiveRemoved ? 'success' : 'skipped'
|
|
784
|
+
entry.push(new Uint8Array(0), true);
|
|
785
|
+
await drained();
|
|
743
786
|
}
|
|
787
|
+
zip.end();
|
|
788
|
+
} catch (error) {
|
|
789
|
+
out.destroy(error as Error);
|
|
790
|
+
}
|
|
791
|
+
await finished;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// The start-build fields: explicit overrides (the tool's camelCase params) beat base, which is
|
|
795
|
+
// the settings detected from an archive or stored for the site. Only the framework is required.
|
|
796
|
+
// An app that runs straight from source (plain-JavaScript Express) has no build script or output
|
|
797
|
+
// directory, and the API stores them as null, so a missing one is sent as null. Detection supplies
|
|
798
|
+
// the build script but only guesses a root-level entry file, so a compiled app (TypeScript) must
|
|
799
|
+
// pass entryFile.
|
|
800
|
+
// storedSettings tells base apart: settings stored for the site (Git deploy) belong to whatever
|
|
801
|
+
// framework it ran before, so a different appType starts from a clean build step, while settings
|
|
802
|
+
// detected from the project itself (archive deploy) are always right to keep.
|
|
803
|
+
private hosting_deploy_nodeSettings(base: Record<string, any> | null, params: Record<string, any>, storedSettings: boolean): Record<string, any> {
|
|
804
|
+
const given = base || {};
|
|
805
|
+
const appType = params.appType ?? given.app_type;
|
|
806
|
+
const sameFramework = !storedSettings || params.appType === undefined || params.appType === given.app_type;
|
|
807
|
+
const stored = sameFramework ? given : { node_version: given.node_version, root_directory: given.root_directory, package_manager: given.package_manager };
|
|
808
|
+
if (appType === undefined || appType === null) {
|
|
809
|
+
throw new Error("Missing Node.js build setting: appType. Pass it to hosting_deploy, with outputDirectory and buildScript when the app has a build step; read them from the project's package.json.");
|
|
810
|
+
}
|
|
811
|
+
const optional: Record<string, any> = {
|
|
812
|
+
entry_file: params.entryFile ?? stored.entry_file,
|
|
813
|
+
package_manager: params.packageManager ?? stored.package_manager,
|
|
814
|
+
};
|
|
815
|
+
return {
|
|
816
|
+
node_version: params.nodeVersion ?? stored.node_version ?? 22,
|
|
817
|
+
app_type: appType,
|
|
818
|
+
root_directory: params.rootDirectory ?? stored.root_directory ?? '.',
|
|
819
|
+
output_directory: params.outputDirectory ?? stored.output_directory ?? null,
|
|
820
|
+
build_script: params.buildScript ?? stored.build_script ?? null,
|
|
821
|
+
...Object.fromEntries(Object.entries(optional).filter(([, value]) => value !== undefined && value !== null)),
|
|
744
822
|
};
|
|
745
823
|
}
|
|
746
824
|
|
|
747
|
-
private
|
|
748
|
-
|
|
749
|
-
|
|
825
|
+
private hosting_deploy_hasNodeOverrides(params: Record<string, any>): boolean {
|
|
826
|
+
return ['nodeVersion', 'appType', 'rootDirectory', 'outputDirectory', 'buildScript', 'entryFile', 'packageManager'].some((name) => params[name] !== undefined);
|
|
827
|
+
}
|
|
750
828
|
|
|
751
|
-
|
|
752
|
-
|
|
829
|
+
// The folder of the site a static Git deploy goes into, as a clean relative path ('' is the document root).
|
|
830
|
+
// Separators are normalized first, because some servers read a backslash as one: a value made of dots and
|
|
831
|
+
// backslashes, or one that starts with a drive letter, must not get past as a name.
|
|
832
|
+
private hosting_deploy_gitDirectory(git: Record<string, any>): string {
|
|
833
|
+
if (git.directory === undefined || git.directory === '') {
|
|
834
|
+
return '';
|
|
835
|
+
}
|
|
836
|
+
let directory = typeof git.directory === 'string' ? this.normalizePath(git.directory) : '';
|
|
837
|
+
while (directory.length > 1 && directory.endsWith('/')) {
|
|
838
|
+
directory = directory.slice(0, -1);
|
|
839
|
+
}
|
|
840
|
+
const driveRooted = directory.length > 1 && directory[1] === ':' && /[A-Za-z]/.test(directory[0]);
|
|
841
|
+
if (directory === '' || directory.startsWith('/') || driveRooted || directory.split('/').some((segment: string) => segment === '' || segment === '.' || segment === '..')) {
|
|
842
|
+
throw new Error('git.directory must be a relative path inside the website, without ..');
|
|
843
|
+
}
|
|
844
|
+
return directory;
|
|
845
|
+
}
|
|
753
846
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
url,
|
|
757
|
-
headers: {
|
|
758
|
-
...this.headers,
|
|
759
|
-
'Authorization': `Bearer ${bearerToken}`,
|
|
760
|
-
'Content-Type': 'application/json'
|
|
761
|
-
},
|
|
762
|
-
data: {
|
|
763
|
-
archive_name: archiveName
|
|
764
|
-
},
|
|
765
|
-
timeout: 60000,
|
|
766
|
-
validateStatus: function (status: number): boolean {
|
|
767
|
-
return status < 500;
|
|
768
|
-
}
|
|
769
|
-
};
|
|
847
|
+
private hosting_deploy_validate(params: Record<string, any>): void {
|
|
848
|
+
const { domain, path: sourcePath, git } = params;
|
|
770
849
|
|
|
771
|
-
|
|
850
|
+
if (!domain || typeof domain !== 'string') {
|
|
851
|
+
throw new Error('domain is required and must be a string');
|
|
852
|
+
}
|
|
853
|
+
if ((sourcePath === undefined) === (git === undefined)) {
|
|
854
|
+
throw new Error('Pass exactly one of path (a local directory or archive) or git ({ repository, branch })');
|
|
855
|
+
}
|
|
856
|
+
if (sourcePath !== undefined && (typeof sourcePath !== 'string' || sourcePath === '')) {
|
|
857
|
+
throw new Error('path must be a non-empty string');
|
|
858
|
+
}
|
|
859
|
+
// The slug becomes part of the remote upload path, so it cannot carry path or URL syntax.
|
|
860
|
+
if (params.slug !== undefined && !(typeof params.slug === 'string' && /^[a-z0-9_-]+$/.test(params.slug))) {
|
|
861
|
+
throw new Error('slug must contain only lower-case letters, digits, "-" and "_"');
|
|
862
|
+
}
|
|
863
|
+
if (git !== undefined && (typeof git !== 'object' || git === null || typeof git.repository !== 'string' || typeof git.branch !== 'string' || git.branch === '')) {
|
|
864
|
+
throw new Error('git must be an object with repository (owner/name) and branch');
|
|
865
|
+
}
|
|
866
|
+
// Read only now that git is known to be an object.
|
|
867
|
+
if (git !== undefined) {
|
|
868
|
+
this.hosting_deploy_gitDirectory(git);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
772
871
|
|
|
773
|
-
|
|
774
|
-
|
|
872
|
+
private hosting_deploy_timestamp(): string {
|
|
873
|
+
const pad = (value: number): string => String(value).padStart(2, '0');
|
|
874
|
+
const now = new Date();
|
|
875
|
+
return String(now.getFullYear()) + pad(now.getMonth() + 1) + pad(now.getDate()) + '_' + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds());
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// The WordPress deploy endpoints, called directly. hosting_deploy also runs in hostinger-hosting-mcp,
|
|
879
|
+
// whose catalog holds the hosting group only, so it cannot look these operations up.
|
|
880
|
+
private hosting_deploy_wordpressEndpoints: Record<string, string> = {
|
|
881
|
+
import: '/api/hosting/v1/accounts/{username}/websites/{domain}/wordpress/import',
|
|
882
|
+
plugin: '/api/hosting/v1/accounts/{username}/websites/{domain}/wordpress/plugins/deploy',
|
|
883
|
+
theme: '/api/hosting/v1/accounts/{username}/websites/{domain}/wordpress/themes/deploy',
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
private async hosting_deploy_callWordpress(kind: string, params: Record<string, any>): Promise<any> {
|
|
887
|
+
return this.executeApiCall({ name: 'wordpress_' + kind, method: 'POST', path: this.hosting_deploy_wordpressEndpoints[kind] } as any, params);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private hosting_deploy_randomSuffix(): string {
|
|
891
|
+
return Math.random().toString(36).slice(2, 10).padEnd(8, '0');
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// A directory is zipped into a temp dir (removed by the caller); an archive is used as is.
|
|
895
|
+
private async hosting_deploy_prepareArchive(sourcePath: string, exclude?: string): Promise<{ archivePath: string; tempDir?: string; skipped: string[] }> {
|
|
896
|
+
const resolved = path.resolve(sourcePath);
|
|
897
|
+
if (fs.statSync(resolved).isFile()) {
|
|
898
|
+
const name = path.basename(resolved).toLowerCase();
|
|
899
|
+
const formats = ['.zip', '.tar', '.tar.gz', '.tgz', '.7z', '.gz', '.gzip'];
|
|
900
|
+
if (!formats.some((extension) => name.endsWith(extension))) {
|
|
901
|
+
throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
|
|
775
902
|
}
|
|
903
|
+
return { archivePath: resolved, skipped: [] };
|
|
904
|
+
}
|
|
776
905
|
|
|
777
|
-
|
|
778
|
-
|
|
906
|
+
const { files, skipped } = this.hosting_deploy_listFiles(resolved, exclude);
|
|
907
|
+
if (files.length === 0) {
|
|
908
|
+
throw new Error('No files to deploy in ' + resolved);
|
|
909
|
+
}
|
|
910
|
+
let baseName = 'site';
|
|
911
|
+
try {
|
|
912
|
+
baseName = this.hosting_deploy_slug(resolved);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
// an unusable directory name only changes the archive's file name
|
|
915
|
+
}
|
|
916
|
+
// A file name made of dots and backslashes would be written into the zip as it is, where an extractor
|
|
917
|
+
// that treats backslashes as separators reads it as a path outside the site.
|
|
918
|
+
for (const file of files) {
|
|
919
|
+
this.assertSafeUploadPath(file);
|
|
920
|
+
}
|
|
921
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hostinger-deploy-'));
|
|
922
|
+
const archivePath = path.join(tempDir, baseName + '_' + this.hosting_deploy_timestamp() + '.zip');
|
|
923
|
+
this.log('info', 'Packing ' + files.length + ' files from ' + resolved);
|
|
924
|
+
await this.hosting_deploy_createZip(resolved, files, archivePath);
|
|
925
|
+
return { archivePath, tempDir, skipped };
|
|
926
|
+
}
|
|
779
927
|
|
|
928
|
+
private async hosting_deploy_upload(username: string, domain: string, files: Array<{ absolutePath: string; relativePath: string }>): Promise<void> {
|
|
929
|
+
// Check every name first, so a bad one fails the deploy before anything is uploaded.
|
|
930
|
+
for (const file of files) {
|
|
931
|
+
this.assertSafeUploadPath(file.relativePath);
|
|
932
|
+
}
|
|
933
|
+
let credentials: any;
|
|
934
|
+
try {
|
|
935
|
+
credentials = await this.fetchUploadCredentials(username, domain);
|
|
780
936
|
} catch (error) {
|
|
781
|
-
|
|
782
|
-
|
|
937
|
+
throw new Error('Failed to fetch upload credentials: ' + (error instanceof Error ? error.message : String(error)));
|
|
938
|
+
}
|
|
939
|
+
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = credentials;
|
|
940
|
+
if (!uploadUrl || !authToken || !authRestToken) {
|
|
941
|
+
throw new Error('Invalid upload credentials received from API');
|
|
942
|
+
}
|
|
783
943
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
944
|
+
for (const file of files) {
|
|
945
|
+
try {
|
|
946
|
+
await this.uploadFile(file.absolutePath, file.relativePath, uploadUrl, authRestToken, authToken);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
throw new Error('Failed to upload ' + file.relativePath + ': ' + (error instanceof Error ? error.message : String(error)));
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private async hosting_deploy_wordpressExtension(username: string, domain: string, type: string, params: Record<string, any>): Promise<any> {
|
|
954
|
+
const isPlugin = type === 'wordpress-plugin';
|
|
955
|
+
const directory = path.resolve(params.path);
|
|
956
|
+
const slug = params.slug !== undefined ? params.slug : this.hosting_deploy_slug(directory);
|
|
957
|
+
const { files, skipped } = this.hosting_deploy_listFiles(directory);
|
|
958
|
+
if (files.length === 0) {
|
|
959
|
+
throw new Error('No files found in directory: ' + directory);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// The files land in a fresh directory; the deploy call then installs from it.
|
|
963
|
+
const uploadDirName = slug + '-' + this.hosting_deploy_randomSuffix();
|
|
964
|
+
const remoteRoot = (isPlugin ? 'wp-content/plugins/' : 'wp-content/themes/') + uploadDirName;
|
|
965
|
+
await this.hosting_deploy_upload(username, domain, files.map((relativePath) => ({
|
|
966
|
+
absolutePath: path.join(directory, relativePath),
|
|
967
|
+
relativePath: remoteRoot + '/' + relativePath,
|
|
968
|
+
})));
|
|
969
|
+
|
|
970
|
+
if (isPlugin) {
|
|
971
|
+
await this.hosting_deploy_callWordpress('plugin', { username, domain, slug, plugin_path: uploadDirName });
|
|
972
|
+
} else {
|
|
973
|
+
await this.hosting_deploy_callWordpress('theme', { username, domain, slug, theme_path: uploadDirName, is_activated: params.activate === true });
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return this.hosting_deploy_withSkipped({
|
|
977
|
+
type,
|
|
978
|
+
domain,
|
|
979
|
+
source: 'path',
|
|
980
|
+
status: 'deployed',
|
|
981
|
+
slug,
|
|
982
|
+
files: files.length,
|
|
983
|
+
...(isPlugin ? {} : { activated: params.activate === true }),
|
|
984
|
+
}, skipped);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
private async hosting_deploy_fromPath(username: string, domain: string, type: string, params: Record<string, any>): Promise<any> {
|
|
988
|
+
if (type === 'wordpress-plugin' || type === 'wordpress-theme') {
|
|
989
|
+
return this.hosting_deploy_wordpressExtension(username, domain, type, params);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const uploads: Array<{ absolutePath: string; relativePath: string }> = [];
|
|
993
|
+
let databaseDump: string | undefined;
|
|
994
|
+
if (type === 'wordpress-site') {
|
|
995
|
+
databaseDump = path.resolve(params.databaseDump);
|
|
996
|
+
if (!databaseDump.toLowerCase().endsWith('.sql')) {
|
|
997
|
+
throw new Error('databaseDump must be a .sql file');
|
|
998
|
+
}
|
|
999
|
+
if (!fs.existsSync(databaseDump)) {
|
|
1000
|
+
throw new Error('Database dump not found: ' + databaseDump);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const prepared = await this.hosting_deploy_prepareArchive(params.path, databaseDump);
|
|
1005
|
+
try {
|
|
1006
|
+
const archiveName = path.basename(prepared.archivePath);
|
|
1007
|
+
uploads.push({ absolutePath: prepared.archivePath, relativePath: archiveName });
|
|
1008
|
+
if (databaseDump !== undefined) {
|
|
1009
|
+
uploads.push({ absolutePath: databaseDump, relativePath: path.basename(databaseDump) });
|
|
1010
|
+
}
|
|
1011
|
+
await this.hosting_deploy_upload(username, domain, uploads);
|
|
1012
|
+
|
|
1013
|
+
if (type === 'static') {
|
|
1014
|
+
const result = await this.callOperation('hosting_websites_deploy-static-site-archive', { username, domain, archive_path: archiveName });
|
|
1015
|
+
return this.hosting_deploy_withSkipped({ type, domain, source: 'path', status: 'deployed', message: result?.message }, prepared.skipped);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (type === 'wordpress-site') {
|
|
1019
|
+
const result = await this.hosting_deploy_callWordpress('import', {
|
|
1020
|
+
username, domain, archive_path: archiveName, sql_path: path.basename(databaseDump as string),
|
|
790
1021
|
});
|
|
1022
|
+
return this.hosting_deploy_withSkipped({
|
|
1023
|
+
type, domain, source: 'path', status: 'import_started', message: result?.message,
|
|
1024
|
+
next: 'Files are extracted on the server and the site is available in a few minutes.',
|
|
1025
|
+
}, prepared.skipped);
|
|
791
1026
|
}
|
|
792
1027
|
|
|
793
|
-
|
|
1028
|
+
// nodejs: let the server detect the settings from the archive, then apply any overrides.
|
|
1029
|
+
let detected: any = null;
|
|
1030
|
+
let detectionError = '';
|
|
1031
|
+
try {
|
|
1032
|
+
detected = await this.callOperation('hosting_nodejs_build-settings-from-archive', { username, domain, archive_path: archiveName });
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
detectionError = error instanceof Error ? error.message : String(error);
|
|
1035
|
+
this.log('warning', 'Build settings detection failed: ' + detectionError);
|
|
1036
|
+
}
|
|
1037
|
+
let settings: Record<string, any>;
|
|
1038
|
+
try {
|
|
1039
|
+
settings = this.hosting_deploy_nodeSettings(detected, params, false);
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1042
|
+
throw new Error(detectionError ? message + ' (automatic detection failed: ' + detectionError + ')' : message);
|
|
1043
|
+
}
|
|
1044
|
+
const build = await this.callOperation('hosting_nodejs_start-build', {
|
|
1045
|
+
username, domain, ...settings, source_type: 'archive', source_options: { archive_path: archiveName },
|
|
1046
|
+
});
|
|
1047
|
+
return this.hosting_deploy_withSkipped(this.hosting_deploy_buildStarted(type, domain, 'path', build), prepared.skipped);
|
|
1048
|
+
} finally {
|
|
1049
|
+
if (prepared.tempDir) {
|
|
1050
|
+
fs.rmSync(prepared.tempDir, { recursive: true, force: true });
|
|
1051
|
+
}
|
|
794
1052
|
}
|
|
795
1053
|
}
|
|
796
1054
|
|
|
797
|
-
private
|
|
798
|
-
|
|
1055
|
+
private hosting_deploy_buildStarted(type: string, domain: string, source: string, build: any): any {
|
|
1056
|
+
return {
|
|
1057
|
+
type,
|
|
1058
|
+
domain,
|
|
1059
|
+
source,
|
|
1060
|
+
status: 'build_started',
|
|
1061
|
+
buildUuid: build?.uuid,
|
|
1062
|
+
state: build?.state,
|
|
1063
|
+
next: 'The build runs on the server. Call hosting_get-deployment with this domain until the build state is completed or failed; a failed build comes back with its logs and analysis.',
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
799
1066
|
|
|
800
|
-
|
|
801
|
-
|
|
1067
|
+
// The Git installation that can reach owner/name (GitLab owners may contain slashes).
|
|
1068
|
+
private async hosting_deploy_findRepository(fullName: string): Promise<{ installationUuid: string; owner: string; repository: string }> {
|
|
1069
|
+
if (!fullName.includes('/')) {
|
|
1070
|
+
throw new Error('git.repository must be given as owner/name');
|
|
1071
|
+
}
|
|
1072
|
+
const installations = await this.callOperation('hosting_git_list-installations', { status: 'active' });
|
|
1073
|
+
if (!Array.isArray(installations) || installations.length === 0) {
|
|
1074
|
+
throw new Error('No active Git installation. Connect GitHub or GitLab once in hPanel (Websites, Manage, Advanced, Git), then try again.');
|
|
1075
|
+
}
|
|
1076
|
+
for (const installation of installations) {
|
|
1077
|
+
const repositories = await this.callOperation('hosting_git_list-installation-repositories', { uuid: installation.uuid });
|
|
1078
|
+
const repo = (Array.isArray(repositories) ? repositories : []).find((candidate: any) => String(candidate.full_name).toLowerCase() === fullName.toLowerCase());
|
|
1079
|
+
if (repo) {
|
|
1080
|
+
return { installationUuid: installation.uuid, owner: repo.owner, repository: repo.name };
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
throw new Error('Repository ' + fullName + ' was not found in your ' + installations.length + ' active Git installation(s). Check the name, or give the Git provider access to it in hPanel (Websites, Manage, Advanced, Git).');
|
|
1084
|
+
}
|
|
802
1085
|
|
|
803
|
-
|
|
804
|
-
|
|
1086
|
+
// What a Git deploy is. There are no local files to look at, so guessing is dangerous: a Node.js
|
|
1087
|
+
// repository deployed as a static site would publish its source and switch on push-to-deploy. The
|
|
1088
|
+
// type given, the site already running Node.js, or Node.js settings in the request are clear
|
|
1089
|
+
// intent; anything else has to say what it is.
|
|
1090
|
+
private hosting_deploy_gitType(website: any, params: Record<string, any>): string {
|
|
1091
|
+
if (params.type !== undefined) {
|
|
1092
|
+
return params.type;
|
|
1093
|
+
}
|
|
1094
|
+
if (website.website_type === 'nodejs' || this.hosting_deploy_hasNodeOverrides(params)) {
|
|
1095
|
+
return 'nodejs';
|
|
1096
|
+
}
|
|
1097
|
+
throw new Error("A Git deploy needs type ('static' or 'nodejs'): this site is not a Node.js site yet, and a repository's kind cannot be told without its files.");
|
|
1098
|
+
}
|
|
805
1099
|
|
|
806
|
-
|
|
1100
|
+
private async hosting_deploy_fromGit(website: any, domain: string, git: Record<string, any>, params: Record<string, any>): Promise<any> {
|
|
1101
|
+
const username = website.username;
|
|
1102
|
+
const type = this.hosting_deploy_gitType(website, params);
|
|
1103
|
+
if (type !== 'nodejs' && type !== 'static') {
|
|
1104
|
+
throw new Error('A Git deploy supports type static or nodejs, not ' + type);
|
|
1105
|
+
}
|
|
1106
|
+
const directory = this.hosting_deploy_gitDirectory(git);
|
|
1107
|
+
if (type === 'nodejs' && directory !== '') {
|
|
1108
|
+
throw new Error('git.directory is for static deploys: a Node.js build has no target folder');
|
|
1109
|
+
}
|
|
1110
|
+
const repo = await this.hosting_deploy_findRepository(git.repository);
|
|
807
1111
|
|
|
808
|
-
|
|
1112
|
+
if (type === 'static') {
|
|
1113
|
+
// Saving the settings deploys the branch now and again on every push.
|
|
1114
|
+
const result = await this.callOperation('hosting_git_update-auto-deployment-settings', {
|
|
1115
|
+
username, domain, installation_uuid: repo.installationUuid, owner: repo.owner, repository: repo.repository, branch: git.branch, is_enabled: true,
|
|
1116
|
+
...(directory !== '' ? { directory } : {}),
|
|
1117
|
+
});
|
|
1118
|
+
return { type, domain, source: 'git', status: 'deployed', autoDeploy: true, message: result?.message };
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Nothing is uploaded, so there is no archive to detect from: use the settings stored for the site.
|
|
1122
|
+
let stored: any = null;
|
|
809
1123
|
try {
|
|
810
|
-
|
|
1124
|
+
stored = await this.callOperation('hosting_nodejs_build-settings', { username, domain });
|
|
811
1125
|
} catch (error) {
|
|
812
|
-
|
|
813
|
-
|
|
1126
|
+
if (!(error instanceof ApiExecutionError && error.status === 404)) {
|
|
1127
|
+
throw error;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const settings = this.hosting_deploy_nodeSettings(stored, params, true);
|
|
1131
|
+
if (this.hosting_deploy_hasNodeOverrides(params)) {
|
|
1132
|
+
// Later pushes build with the stored settings, not with this request: keep what was given.
|
|
1133
|
+
await this.callOperation('hosting_nodejs_update-build-settings', { username, domain, ...settings });
|
|
814
1134
|
}
|
|
1135
|
+
const build = await this.callOperation('hosting_nodejs_start-build', {
|
|
1136
|
+
username, domain, ...settings, source_type: 'git',
|
|
1137
|
+
source_options: { owner: repo.owner, repository: repo.repository, branch: git.branch, installation_uuid: repo.installationUuid },
|
|
1138
|
+
});
|
|
1139
|
+
return this.hosting_deploy_buildStarted(type, domain, 'git', build);
|
|
1140
|
+
}
|
|
815
1141
|
|
|
816
|
-
|
|
1142
|
+
private async handleHostingDeploy(params: Record<string, any>): Promise<any> {
|
|
1143
|
+
this.hosting_deploy_validate(params);
|
|
1144
|
+
const { domain, git } = params;
|
|
817
1145
|
|
|
818
|
-
if (
|
|
819
|
-
|
|
1146
|
+
if (git === undefined) {
|
|
1147
|
+
// Local checks first, so a bad path fails before any request is made.
|
|
1148
|
+
const type = this.hosting_deploy_resolveType(params);
|
|
1149
|
+
const website = await this.findWebsite(domain);
|
|
1150
|
+
return this.hosting_deploy_fromPath(website.username, domain, type, params);
|
|
820
1151
|
}
|
|
1152
|
+
const website = await this.findWebsite(domain);
|
|
1153
|
+
return this.hosting_deploy_fromGit(website, domain, git, params);
|
|
1154
|
+
}
|
|
821
1155
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
uploadResult = await this.agencyHosting_uploadFile(
|
|
826
|
-
archivePath,
|
|
827
|
-
`.h5g/${archiveBasename}`,
|
|
828
|
-
uploadUrl,
|
|
829
|
-
authRestToken,
|
|
830
|
-
authToken
|
|
831
|
-
);
|
|
1156
|
+
// The failure analysis re-runs on every call and is limited to 5 a minute, so keep one request per
|
|
1157
|
+
// build, stored as soon as it starts so that polls at the same moment share it.
|
|
1158
|
+
private hosting_getDeployment_analyses = new Map<string, Promise<any>>();
|
|
832
1159
|
|
|
833
|
-
|
|
1160
|
+
// A failed analysis (429, outage) is remembered for a minute, so polling does not retry the
|
|
1161
|
+
// rate-limited endpoint on every call; after that it is tried again.
|
|
1162
|
+
private hosting_getDeployment_failures = new Map<string, { error: string; at: number }>();
|
|
1163
|
+
|
|
1164
|
+
// Settings that do not exist yet answer 404; that is "none", not a failure.
|
|
1165
|
+
private async hosting_getDeployment_orNull(request: Promise<any>): Promise<any> {
|
|
1166
|
+
try {
|
|
1167
|
+
return await request;
|
|
834
1168
|
} catch (error) {
|
|
835
|
-
|
|
836
|
-
|
|
1169
|
+
if (error instanceof ApiExecutionError && error.status === 404) {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
throw error;
|
|
837
1173
|
}
|
|
1174
|
+
}
|
|
838
1175
|
|
|
839
|
-
|
|
1176
|
+
private async hosting_getDeployment_analysis(username: string, domain: string, uuid: string): Promise<any> {
|
|
1177
|
+
const key = username + '/' + domain + '/' + uuid;
|
|
1178
|
+
const failure = this.hosting_getDeployment_failures.get(key);
|
|
1179
|
+
if (failure && Date.now() - failure.at < 60000) {
|
|
1180
|
+
return { error: failure.error };
|
|
1181
|
+
}
|
|
1182
|
+
if (!this.hosting_getDeployment_analyses.has(key)) {
|
|
1183
|
+
this.hosting_getDeployment_analyses.set(key, this.callOperation('hosting_nodejs_analyse-failed-build', { username, domain, uuid }));
|
|
1184
|
+
}
|
|
840
1185
|
try {
|
|
841
|
-
|
|
842
|
-
|
|
1186
|
+
const analysis = await this.hosting_getDeployment_analyses.get(key);
|
|
1187
|
+
this.hosting_getDeployment_failures.delete(key);
|
|
1188
|
+
return analysis;
|
|
843
1189
|
} catch (error) {
|
|
844
|
-
|
|
845
|
-
this.
|
|
846
|
-
const
|
|
1190
|
+
// Rate limited or unavailable: report it, but do not lose the build and its logs.
|
|
1191
|
+
this.hosting_getDeployment_analyses.delete(key);
|
|
1192
|
+
const message = this.formatToolError(error);
|
|
1193
|
+
this.hosting_getDeployment_failures.set(key, { error: message, at: Date.now() });
|
|
1194
|
+
return { error: message };
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
private async handleHostingGetDeployment(params: Record<string, any>): Promise<any> {
|
|
1199
|
+
const { domain, buildUuid, fromLine } = params;
|
|
1200
|
+
|
|
1201
|
+
if (!domain || typeof domain !== 'string') {
|
|
1202
|
+
throw new Error('domain is required and must be a string');
|
|
1203
|
+
}
|
|
1204
|
+
if (buildUuid !== undefined && (typeof buildUuid !== 'string' || buildUuid === '')) {
|
|
1205
|
+
throw new Error('buildUuid must be a non-empty string when provided');
|
|
1206
|
+
}
|
|
1207
|
+
if (fromLine !== undefined && (typeof fromLine !== 'number' || !Number.isInteger(fromLine) || fromLine < 0)) {
|
|
1208
|
+
throw new Error('fromLine must be a non-negative integer when provided');
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const { username } = await this.findWebsite(domain);
|
|
1212
|
+
const target = { username, domain };
|
|
1213
|
+
|
|
1214
|
+
const gitSettings = await this.callOperation('hosting_git_auto-deployment-settings', target);
|
|
1215
|
+
const gitAutoDeployment = gitSettings && gitSettings.installation_uuid ? gitSettings : null;
|
|
1216
|
+
const buildSettings = await this.hosting_getDeployment_orNull(this.callOperation('hosting_nodejs_build-settings', target));
|
|
847
1217
|
|
|
1218
|
+
let build: any = null;
|
|
1219
|
+
if (buildUuid !== undefined) {
|
|
1220
|
+
build = await this.callOperation('hosting_nodejs_build', { ...target, uuid: buildUuid });
|
|
1221
|
+
} else {
|
|
1222
|
+
const builds = await this.hosting_getDeployment_orNull(this.callOperation('hosting_nodejs_list-builds', { ...target, per_page: 1 }));
|
|
1223
|
+
build = builds && Array.isArray(builds.data) && builds.data.length > 0 ? builds.data[0] : null;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (build === null) {
|
|
848
1227
|
return {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
},
|
|
855
|
-
deploy: {
|
|
856
|
-
status: 'error',
|
|
857
|
-
error: errorMessage
|
|
858
|
-
},
|
|
859
|
-
removeArchive: {
|
|
860
|
-
status: archiveRemoved ? 'success' : 'skipped'
|
|
861
|
-
}
|
|
1228
|
+
domain,
|
|
1229
|
+
build: null,
|
|
1230
|
+
buildSettings,
|
|
1231
|
+
gitAutoDeployment,
|
|
1232
|
+
note: 'No Node.js builds found for this website. Static, PHP, plugin and theme deploys finish before hosting_deploy returns, and a WordPress site import is ready a few minutes after it starts; a Node.js build appears here once one has been started.',
|
|
862
1233
|
};
|
|
863
1234
|
}
|
|
864
1235
|
|
|
865
|
-
const
|
|
1236
|
+
const logs = await this.callOperation('hosting_nodejs_build-logs', { ...target, uuid: build.uuid, from_line: fromLine !== undefined ? fromLine : 0 });
|
|
866
1237
|
|
|
867
1238
|
return {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
deploy: {
|
|
875
|
-
status: 'success',
|
|
876
|
-
data: deployResult
|
|
877
|
-
},
|
|
878
|
-
removeArchive: {
|
|
879
|
-
status: archiveRemoved ? 'success' : 'skipped'
|
|
880
|
-
}
|
|
1239
|
+
domain,
|
|
1240
|
+
build,
|
|
1241
|
+
logs,
|
|
1242
|
+
...(build.state === 'failed' ? { analysis: await this.hosting_getDeployment_analysis(username, domain, build.uuid) } : {}),
|
|
1243
|
+
buildSettings,
|
|
1244
|
+
gitAutoDeployment,
|
|
881
1245
|
};
|
|
882
1246
|
}
|
|
883
1247
|
|
|
884
|
-
private
|
|
885
|
-
|
|
1248
|
+
private hosting_importWordpressWebsite_validateArchiveFormat(filePath: string): boolean {
|
|
1249
|
+
const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
|
|
1250
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
1251
|
+
|
|
1252
|
+
for (const ext of validExtensions) {
|
|
1253
|
+
if (fileName.endsWith(`.${ext}`)) {
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
private hosting_importWordpressWebsite_validateRequiredParams(params: Record<string, any>): void {
|
|
1262
|
+
const { domain, archivePath, databaseDump } = params;
|
|
1263
|
+
|
|
1264
|
+
if (!domain || typeof domain !== 'string') {
|
|
1265
|
+
throw new Error('domain is required and must be a string');
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (!archivePath || typeof archivePath !== 'string') {
|
|
1269
|
+
throw new Error('archivePath is required and must be a string');
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
if (!databaseDump || typeof databaseDump !== 'string') {
|
|
1273
|
+
throw new Error('databaseDump is required and must be a string');
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
private hosting_importWordpressWebsite_validateArchiveFile(archivePath: string): void {
|
|
1278
|
+
if (!fs.existsSync(archivePath)) {
|
|
1279
|
+
throw new Error(`Archive file not found: ${archivePath}`);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const archiveStats = fs.statSync(archivePath);
|
|
1283
|
+
if (!archiveStats.isFile()) {
|
|
1284
|
+
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (!this.hosting_importWordpressWebsite_validateArchiveFormat(archivePath)) {
|
|
1288
|
+
throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
private hosting_importWordpressWebsite_validateDatabaseFile(databaseDump: string): void {
|
|
1293
|
+
if (!fs.existsSync(databaseDump)) {
|
|
1294
|
+
throw new Error(`Database dump file not found: ${databaseDump}`);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const dbStats = fs.statSync(databaseDump);
|
|
1298
|
+
if (!dbStats.isFile()) {
|
|
1299
|
+
throw new Error(`Database dump path is not a file: ${databaseDump}`);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (!databaseDump.toLowerCase().endsWith('.sql')) {
|
|
1303
|
+
throw new Error('Database dump must be a .sql file');
|
|
1304
|
+
}
|
|
886
1305
|
}
|
|
887
1306
|
|
|
888
|
-
private async
|
|
1307
|
+
private async hosting_importWordpressWebsite_checkWebsiteIsEmpty(username: string, domain: string): Promise<boolean> {
|
|
889
1308
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
890
|
-
const url = new URL(`api/hosting/v1/
|
|
891
|
-
|
|
1309
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/domains/${domain}/is-empty`, baseUrl).toString();
|
|
1310
|
+
|
|
892
1311
|
try {
|
|
893
1312
|
const bearerToken = await this.getAuthToken();
|
|
894
|
-
|
|
1313
|
+
|
|
895
1314
|
const config: AxiosRequestConfig = {
|
|
896
1315
|
method: 'get',
|
|
897
1316
|
url,
|
|
@@ -904,30 +1323,26 @@ class MCPServer {
|
|
|
904
1323
|
return status < 500;
|
|
905
1324
|
}
|
|
906
1325
|
};
|
|
907
|
-
|
|
1326
|
+
|
|
908
1327
|
const response = await axios(config);
|
|
909
|
-
|
|
1328
|
+
|
|
910
1329
|
if (response.status !== 200) {
|
|
911
1330
|
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
912
1331
|
}
|
|
913
|
-
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const username = websites[0].username;
|
|
920
|
-
if (!username) {
|
|
921
|
-
throw new Error(`Username not found in website data for domain: ${domain}`);
|
|
1332
|
+
|
|
1333
|
+
const { is_empty } = response.data;
|
|
1334
|
+
|
|
1335
|
+
if (!is_empty) {
|
|
1336
|
+
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.');
|
|
922
1337
|
}
|
|
923
|
-
|
|
924
|
-
this.log('info', `
|
|
925
|
-
return
|
|
926
|
-
|
|
1338
|
+
|
|
1339
|
+
this.log('info', `Website ${domain} is empty, proceeding with import`);
|
|
1340
|
+
return true;
|
|
1341
|
+
|
|
927
1342
|
} catch (error) {
|
|
928
1343
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
929
|
-
this.log('error', `Failed to
|
|
930
|
-
|
|
1344
|
+
this.log('error', `Failed to check if website is empty: ${errorMessage}`);
|
|
1345
|
+
|
|
931
1346
|
if (axios.isAxiosError(error)) {
|
|
932
1347
|
const responseData = error.response?.data;
|
|
933
1348
|
const responseStatus = error.response?.status;
|
|
@@ -936,14 +1351,14 @@ class MCPServer {
|
|
|
936
1351
|
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
937
1352
|
});
|
|
938
1353
|
}
|
|
939
|
-
|
|
1354
|
+
|
|
940
1355
|
throw error;
|
|
941
1356
|
}
|
|
942
1357
|
}
|
|
943
1358
|
|
|
944
|
-
private async
|
|
1359
|
+
private async hosting_importWordpressWebsite_extractFiles(username: string, domain: string, archivePath: string, databaseDump: string): Promise<boolean> {
|
|
945
1360
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
946
|
-
const url = new URL(
|
|
1361
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/import`, baseUrl).toString();
|
|
947
1362
|
|
|
948
1363
|
try {
|
|
949
1364
|
const bearerToken = await this.getAuthToken();
|
|
@@ -957,8 +1372,8 @@ class MCPServer {
|
|
|
957
1372
|
'Content-Type': 'application/json'
|
|
958
1373
|
},
|
|
959
1374
|
data: {
|
|
960
|
-
|
|
961
|
-
|
|
1375
|
+
archive_path: path.basename(archivePath),
|
|
1376
|
+
sql_path: path.basename(databaseDump)
|
|
962
1377
|
},
|
|
963
1378
|
timeout: 60000, // 60s
|
|
964
1379
|
validateStatus: function (status: number): boolean {
|
|
@@ -968,15 +1383,12 @@ class MCPServer {
|
|
|
968
1383
|
|
|
969
1384
|
const response = await axios(config);
|
|
970
1385
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
return response.data;
|
|
1386
|
+
this.log('info', `Successfully triggered file extraction for ${domain}`);
|
|
1387
|
+
return true;
|
|
976
1388
|
|
|
977
1389
|
} catch (error) {
|
|
978
1390
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
979
|
-
this.log('error', `Failed to
|
|
1391
|
+
this.log('error', `Failed to trigger file extraction: ${errorMessage}`);
|
|
980
1392
|
|
|
981
1393
|
if (axios.isAxiosError(error)) {
|
|
982
1394
|
const responseData = error.response?.data;
|
|
@@ -991,169 +1403,202 @@ class MCPServer {
|
|
|
991
1403
|
}
|
|
992
1404
|
}
|
|
993
1405
|
|
|
994
|
-
private async
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1406
|
+
private async handleWordpressWebsiteImport(params: Record<string, any>): Promise<any> {
|
|
1407
|
+
const { domain, archivePath, databaseDump } = params;
|
|
1408
|
+
|
|
1409
|
+
this.hosting_importWordpressWebsite_validateRequiredParams(params);
|
|
1410
|
+
this.hosting_importWordpressWebsite_validateArchiveFile(archivePath);
|
|
1411
|
+
this.hosting_importWordpressWebsite_validateDatabaseFile(databaseDump);
|
|
1412
|
+
|
|
1413
|
+
// Auto-resolve username from domain
|
|
1414
|
+
this.log('info', `Resolving username from domain: ${domain}`);
|
|
1415
|
+
const username = await this.resolveUsername(domain);
|
|
1416
|
+
|
|
1417
|
+
await this.hosting_importWordpressWebsite_checkWebsiteIsEmpty(username, domain);
|
|
1418
|
+
|
|
1419
|
+
const filesToUpload: Array<{absolutePath: string, relativePath: string, type: string}> = [{
|
|
1420
|
+
absolutePath: archivePath,
|
|
1421
|
+
relativePath: path.basename(archivePath),
|
|
1422
|
+
type: 'archive'
|
|
1423
|
+
}, {
|
|
1424
|
+
absolutePath: databaseDump,
|
|
1425
|
+
relativePath: path.basename(databaseDump),
|
|
1426
|
+
type: 'database'
|
|
1427
|
+
}];
|
|
1428
|
+
|
|
1429
|
+
let uploadCredentials: any;
|
|
1430
|
+
try {
|
|
1431
|
+
uploadCredentials = await this.fetchUploadCredentials(username, domain);
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1434
|
+
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
|
|
1438
|
+
|
|
1439
|
+
if (!uploadUrl || !authToken || !authRestToken) {
|
|
1440
|
+
throw new Error('Invalid upload credentials received from API');
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
this.log('info', `Starting website archive import to ${uploadUrl}`);
|
|
1444
|
+
|
|
1445
|
+
const results: any[] = [];
|
|
1446
|
+
let successCount = 0;
|
|
1447
|
+
let failureCount = 0;
|
|
1448
|
+
|
|
1449
|
+
for (const fileInfo of filesToUpload) {
|
|
1002
1450
|
try {
|
|
1003
|
-
|
|
1004
|
-
const fileStream = fs.createReadStream(filePath);
|
|
1005
|
-
|
|
1006
|
-
const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
|
|
1007
|
-
const normalizedPath = this.normalizePath(relativePath);
|
|
1008
|
-
const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
|
|
1451
|
+
this.log('info', `Uploading ${fileInfo.type}: ${fileInfo.absolutePath}`);
|
|
1009
1452
|
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1453
|
+
const stats = fs.statSync(fileInfo.absolutePath);
|
|
1454
|
+
const uploadResult = await this.uploadFile(
|
|
1455
|
+
fileInfo.absolutePath,
|
|
1456
|
+
fileInfo.relativePath,
|
|
1457
|
+
uploadUrl,
|
|
1458
|
+
authRestToken,
|
|
1459
|
+
authToken
|
|
1460
|
+
);
|
|
1016
1461
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
});
|
|
1026
|
-
} catch (error) {
|
|
1027
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1028
|
-
|
|
1029
|
-
if (axios.isAxiosError(error)) {
|
|
1030
|
-
const responseData = error.response?.data;
|
|
1031
|
-
const responseStatus = error.response?.status;
|
|
1032
|
-
const responseHeaders = error.response?.headers;
|
|
1033
|
-
const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
|
|
1034
|
-
|
|
1035
|
-
this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
|
|
1036
|
-
status: responseStatus,
|
|
1037
|
-
headers: responseHeaders,
|
|
1038
|
-
data: responseText,
|
|
1039
|
-
message: errorMessage
|
|
1040
|
-
});
|
|
1041
|
-
reject(new Error(`Pre-upload request failed: ${errorMessage}`));
|
|
1042
|
-
return;
|
|
1043
|
-
} else {
|
|
1044
|
-
this.log('error', `Pre-upload POST request failed: ${errorMessage}`);
|
|
1045
|
-
reject(new Error(`Pre-upload request failed: ${errorMessage}`));
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1462
|
+
results.push({
|
|
1463
|
+
file: fileInfo.absolutePath,
|
|
1464
|
+
remotePath: fileInfo.relativePath,
|
|
1465
|
+
type: fileInfo.type,
|
|
1466
|
+
status: 'success',
|
|
1467
|
+
uploadUrl: uploadResult.url,
|
|
1468
|
+
size: stats.size
|
|
1469
|
+
});
|
|
1049
1470
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
},
|
|
1062
|
-
onError: (error: Error) => {
|
|
1063
|
-
this.log('error', `TUS upload error for ${relativePath}`, { error: error.message });
|
|
1064
|
-
reject(new Error(`Upload failed: ${error.message}`));
|
|
1065
|
-
},
|
|
1066
|
-
onSuccess: () => {
|
|
1067
|
-
this.log('info', `TUS upload completed for ${relativePath}`, { url: upload.url });
|
|
1068
|
-
resolve({
|
|
1069
|
-
url: upload.url || uploadUrlWithFile,
|
|
1070
|
-
filename: relativePath
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
1471
|
+
successCount++;
|
|
1472
|
+
this.log('info', `Successfully uploaded ${fileInfo.type}: ${fileInfo.relativePath}`);
|
|
1473
|
+
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1476
|
+
results.push({
|
|
1477
|
+
file: fileInfo.absolutePath,
|
|
1478
|
+
remotePath: fileInfo.relativePath,
|
|
1479
|
+
type: fileInfo.type,
|
|
1480
|
+
status: 'error',
|
|
1481
|
+
error: errorMessage
|
|
1073
1482
|
});
|
|
1074
1483
|
|
|
1075
|
-
|
|
1484
|
+
failureCount++;
|
|
1485
|
+
this.log('error', `Failed to upload ${fileInfo.type} ${fileInfo.absolutePath}: ${errorMessage}`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
|
|
1076
1490
|
|
|
1491
|
+
if (failureCount === 0) {
|
|
1492
|
+
try {
|
|
1493
|
+
this.log('info', 'All files uploaded successfully, triggering extraction...');
|
|
1494
|
+
await this.hosting_importWordpressWebsite_extractFiles(username, domain, archivePath, databaseDump);
|
|
1077
1495
|
} catch (error) {
|
|
1078
1496
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1079
|
-
this.log('error', `
|
|
1080
|
-
|
|
1497
|
+
this.log('error', `File extraction failed: ${errorMessage}`);
|
|
1498
|
+
return {
|
|
1499
|
+
status: 'partial',
|
|
1500
|
+
summary: {
|
|
1501
|
+
total: filesToUpload.length,
|
|
1502
|
+
successful: successCount,
|
|
1503
|
+
failed: failureCount
|
|
1504
|
+
},
|
|
1505
|
+
results,
|
|
1506
|
+
extractionError: errorMessage
|
|
1507
|
+
};
|
|
1081
1508
|
}
|
|
1082
|
-
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
return {
|
|
1512
|
+
status: overallStatus,
|
|
1513
|
+
summary: {
|
|
1514
|
+
total: filesToUpload.length,
|
|
1515
|
+
successful: successCount,
|
|
1516
|
+
failed: failureCount
|
|
1517
|
+
},
|
|
1518
|
+
results
|
|
1519
|
+
};
|
|
1083
1520
|
}
|
|
1084
1521
|
|
|
1085
|
-
private
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
if (fileName.endsWith(`.${ext}`)) {
|
|
1091
|
-
return true;
|
|
1092
|
-
}
|
|
1522
|
+
private hosting_deployWordpressPlugin_generateRandomString(length: number): string {
|
|
1523
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
1524
|
+
let result = '';
|
|
1525
|
+
for (let i = 0; i < length; i++) {
|
|
1526
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
1093
1527
|
}
|
|
1094
|
-
|
|
1095
|
-
return false;
|
|
1528
|
+
return result;
|
|
1096
1529
|
}
|
|
1097
1530
|
|
|
1098
|
-
private
|
|
1099
|
-
const { domain,
|
|
1531
|
+
private hosting_deployWordpressPlugin_validateRequiredParams(params: Record<string, any>): void {
|
|
1532
|
+
const { domain, slug, pluginPath } = params;
|
|
1100
1533
|
|
|
1101
1534
|
if (!domain || typeof domain !== 'string') {
|
|
1102
1535
|
throw new Error('domain is required and must be a string');
|
|
1103
1536
|
}
|
|
1104
1537
|
|
|
1105
|
-
if (!
|
|
1106
|
-
throw new Error('
|
|
1538
|
+
if (!slug || typeof slug !== 'string') {
|
|
1539
|
+
throw new Error('slug is required and must be a string');
|
|
1107
1540
|
}
|
|
1108
1541
|
|
|
1109
|
-
if (!
|
|
1110
|
-
throw new Error('
|
|
1542
|
+
if (!pluginPath || typeof pluginPath !== 'string') {
|
|
1543
|
+
throw new Error('pluginPath is required and must be a string');
|
|
1111
1544
|
}
|
|
1112
1545
|
}
|
|
1113
1546
|
|
|
1114
|
-
private
|
|
1115
|
-
if (!fs.existsSync(
|
|
1116
|
-
throw new Error(`
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
const archiveStats = fs.statSync(archivePath);
|
|
1120
|
-
if (!archiveStats.isFile()) {
|
|
1121
|
-
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
1547
|
+
private hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath: string): void {
|
|
1548
|
+
if (!fs.existsSync(pluginPath)) {
|
|
1549
|
+
throw new Error(`Plugin directory not found: ${pluginPath}`);
|
|
1122
1550
|
}
|
|
1123
1551
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1552
|
+
const pluginStats = fs.statSync(pluginPath);
|
|
1553
|
+
if (!pluginStats.isDirectory()) {
|
|
1554
|
+
throw new Error(`Plugin path is not a directory: ${pluginPath}`);
|
|
1126
1555
|
}
|
|
1127
1556
|
}
|
|
1128
1557
|
|
|
1129
|
-
private
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1558
|
+
private hosting_deployWordpressPlugin_scanDirectory(dirPath: string, basePath: string = dirPath): Array<{absolutePath: string, relativePath: string}> {
|
|
1559
|
+
const files: Array<{absolutePath: string, relativePath: string}> = [];
|
|
1560
|
+
|
|
1561
|
+
const scanDir = (currentPath: string) => {
|
|
1562
|
+
const items = fs.readdirSync(currentPath);
|
|
1563
|
+
|
|
1564
|
+
for (const item of items) {
|
|
1565
|
+
const itemPath = path.join(currentPath, item);
|
|
1566
|
+
const stats = fs.statSync(itemPath);
|
|
1567
|
+
|
|
1568
|
+
if (stats.isDirectory()) {
|
|
1569
|
+
scanDir(itemPath);
|
|
1570
|
+
} else if (stats.isFile()) {
|
|
1571
|
+
const relativePath = path.relative(basePath, itemPath);
|
|
1572
|
+
files.push({
|
|
1573
|
+
absolutePath: itemPath,
|
|
1574
|
+
relativePath: relativePath
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
scanDir(dirPath);
|
|
1581
|
+
return files;
|
|
1142
1582
|
}
|
|
1143
1583
|
|
|
1144
|
-
private async
|
|
1584
|
+
private async hosting_deployWordpressPlugin_deployPlugin(username: string, domain: string, slug: string, pluginPath: string): Promise<boolean> {
|
|
1145
1585
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1146
|
-
const url = new URL(`api/hosting/v1/accounts/${username}/
|
|
1586
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/plugins/deploy`, baseUrl).toString();
|
|
1147
1587
|
|
|
1148
1588
|
try {
|
|
1149
1589
|
const bearerToken = await this.getAuthToken();
|
|
1150
1590
|
|
|
1151
1591
|
const config: AxiosRequestConfig = {
|
|
1152
|
-
method: '
|
|
1592
|
+
method: 'post',
|
|
1153
1593
|
url,
|
|
1154
1594
|
headers: {
|
|
1155
1595
|
...this.headers,
|
|
1156
|
-
'Authorization': `Bearer ${bearerToken}
|
|
1596
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
1597
|
+
'Content-Type': 'application/json'
|
|
1598
|
+
},
|
|
1599
|
+
data: {
|
|
1600
|
+
slug,
|
|
1601
|
+
plugin_path: pluginPath
|
|
1157
1602
|
},
|
|
1158
1603
|
timeout: 60000, // 60s
|
|
1159
1604
|
validateStatus: function (status: number): boolean {
|
|
@@ -1167,35 +1612,209 @@ class MCPServer {
|
|
|
1167
1612
|
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1168
1613
|
}
|
|
1169
1614
|
|
|
1170
|
-
|
|
1615
|
+
this.log('info', `Successfully triggered plugin deployment for ${domain}`);
|
|
1616
|
+
return true;
|
|
1171
1617
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1620
|
+
this.log('error', `Failed to trigger plugin deployment: ${errorMessage}`);
|
|
1621
|
+
|
|
1622
|
+
if (axios.isAxiosError(error)) {
|
|
1623
|
+
const responseData = error.response?.data;
|
|
1624
|
+
const responseStatus = error.response?.status;
|
|
1625
|
+
this.log('error', 'API Error Details:', {
|
|
1626
|
+
status: responseStatus,
|
|
1627
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
throw error;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
private async handleWordpressPluginDeploy(params: Record<string, any>): Promise<any> {
|
|
1636
|
+
const { domain, slug, pluginPath } = params;
|
|
1637
|
+
|
|
1638
|
+
this.hosting_deployWordpressPlugin_validateRequiredParams(params);
|
|
1639
|
+
this.hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath);
|
|
1640
|
+
|
|
1641
|
+
this.log('info', `Resolving username from domain: ${domain}`);
|
|
1642
|
+
const username = await this.resolveUsername(domain);
|
|
1643
|
+
|
|
1644
|
+
const randomSuffix = this.hosting_deployWordpressPlugin_generateRandomString(8);
|
|
1645
|
+
const uploadDirName = `${slug}-${randomSuffix}`;
|
|
1646
|
+
|
|
1647
|
+
this.log('info', `Scanning plugin directory: ${pluginPath}`);
|
|
1648
|
+
const pluginFiles = this.hosting_deployWordpressPlugin_scanDirectory(pluginPath);
|
|
1649
|
+
|
|
1650
|
+
if (pluginFiles.length === 0) {
|
|
1651
|
+
throw new Error(`No files found in plugin directory: ${pluginPath}`);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
this.log('info', `Found ${pluginFiles.length} files to upload`);
|
|
1655
|
+
|
|
1656
|
+
let uploadCredentials: any;
|
|
1657
|
+
try {
|
|
1658
|
+
uploadCredentials = await this.fetchUploadCredentials(username, domain);
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1661
|
+
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
|
|
1665
|
+
|
|
1666
|
+
if (!uploadUrl || !authToken || !authRestToken) {
|
|
1667
|
+
throw new Error('Invalid upload credentials received from API');
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
this.log('info', `Starting plugin file upload to ${uploadUrl}`);
|
|
1671
|
+
|
|
1672
|
+
const results: any[] = [];
|
|
1673
|
+
let successCount = 0;
|
|
1674
|
+
let failureCount = 0;
|
|
1675
|
+
|
|
1676
|
+
for (const fileInfo of pluginFiles) {
|
|
1677
|
+
try {
|
|
1678
|
+
const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
|
|
1679
|
+
const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
|
|
1680
|
+
this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
|
|
1681
|
+
|
|
1682
|
+
const stats = fs.statSync(fileInfo.absolutePath);
|
|
1683
|
+
const uploadResult = await this.uploadFile(
|
|
1684
|
+
fileInfo.absolutePath,
|
|
1685
|
+
uploadPath,
|
|
1686
|
+
uploadUrl,
|
|
1687
|
+
authRestToken,
|
|
1688
|
+
authToken
|
|
1689
|
+
);
|
|
1690
|
+
|
|
1691
|
+
results.push({
|
|
1692
|
+
file: fileInfo.absolutePath,
|
|
1693
|
+
remotePath: uploadPath,
|
|
1694
|
+
status: 'success',
|
|
1695
|
+
uploadUrl: uploadResult.url,
|
|
1696
|
+
size: stats.size
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
successCount++;
|
|
1700
|
+
this.log('info', `Successfully uploaded: ${uploadPath}`);
|
|
1701
|
+
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1704
|
+
const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
|
|
1705
|
+
const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
|
|
1706
|
+
|
|
1707
|
+
results.push({
|
|
1708
|
+
file: fileInfo.absolutePath,
|
|
1709
|
+
remotePath: uploadPath,
|
|
1710
|
+
status: 'error',
|
|
1711
|
+
error: errorMessage
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
failureCount++;
|
|
1715
|
+
this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
|
|
1720
|
+
|
|
1721
|
+
if (failureCount === 0) {
|
|
1722
|
+
try {
|
|
1723
|
+
this.log('info', 'All files uploaded successfully, triggering plugin deployment...');
|
|
1724
|
+
await this.hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, uploadDirName);
|
|
1725
|
+
} catch (error) {
|
|
1726
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1727
|
+
this.log('error', `Plugin deployment failed: ${errorMessage}`);
|
|
1728
|
+
return {
|
|
1729
|
+
status: 'partial',
|
|
1730
|
+
summary: {
|
|
1731
|
+
total: pluginFiles.length,
|
|
1732
|
+
successful: successCount,
|
|
1733
|
+
failed: failureCount
|
|
1734
|
+
},
|
|
1735
|
+
results,
|
|
1736
|
+
deploymentError: errorMessage
|
|
1737
|
+
};
|
|
1174
1738
|
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
return {
|
|
1742
|
+
status: overallStatus,
|
|
1743
|
+
summary: {
|
|
1744
|
+
total: pluginFiles.length,
|
|
1745
|
+
successful: successCount,
|
|
1746
|
+
failed: failureCount
|
|
1747
|
+
},
|
|
1748
|
+
results,
|
|
1749
|
+
uploadDirName
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
private hosting_deployWordpressTheme_generateRandomString(length: number): string {
|
|
1754
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
1755
|
+
let result = '';
|
|
1756
|
+
for (let i = 0; i < length; i++) {
|
|
1757
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
1758
|
+
}
|
|
1759
|
+
return result;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
private hosting_deployWordpressTheme_validateRequiredParams(params: Record<string, any>): void {
|
|
1763
|
+
const { domain, slug, themePath } = params;
|
|
1764
|
+
|
|
1765
|
+
if (!domain || typeof domain !== 'string') {
|
|
1766
|
+
throw new Error('domain is required and must be a string');
|
|
1767
|
+
}
|
|
1175
1768
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1769
|
+
if (!slug || typeof slug !== 'string') {
|
|
1770
|
+
throw new Error('slug is required and must be a string');
|
|
1771
|
+
}
|
|
1178
1772
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1773
|
+
if (!themePath || typeof themePath !== 'string') {
|
|
1774
|
+
throw new Error('themePath is required and must be a string');
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1182
1777
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
status: responseStatus,
|
|
1188
|
-
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1778
|
+
private hosting_deployWordpressTheme_validateThemeDirectory(themePath: string): void {
|
|
1779
|
+
if (!fs.existsSync(themePath)) {
|
|
1780
|
+
throw new Error(`Theme directory not found: ${themePath}`);
|
|
1781
|
+
}
|
|
1191
1782
|
|
|
1192
|
-
|
|
1783
|
+
const themeStats = fs.statSync(themePath);
|
|
1784
|
+
if (!themeStats.isDirectory()) {
|
|
1785
|
+
throw new Error(`Theme path is not a directory: ${themePath}`);
|
|
1193
1786
|
}
|
|
1194
1787
|
}
|
|
1195
1788
|
|
|
1196
|
-
private
|
|
1789
|
+
private hosting_deployWordpressTheme_scanDirectory(dirPath: string, basePath: string = dirPath): Array<{absolutePath: string, relativePath: string}> {
|
|
1790
|
+
const files: Array<{absolutePath: string, relativePath: string}> = [];
|
|
1791
|
+
|
|
1792
|
+
const scanDir = (currentPath: string) => {
|
|
1793
|
+
const items = fs.readdirSync(currentPath);
|
|
1794
|
+
|
|
1795
|
+
for (const item of items) {
|
|
1796
|
+
const itemPath = path.join(currentPath, item);
|
|
1797
|
+
const stats = fs.statSync(itemPath);
|
|
1798
|
+
|
|
1799
|
+
if (stats.isDirectory()) {
|
|
1800
|
+
scanDir(itemPath);
|
|
1801
|
+
} else if (stats.isFile()) {
|
|
1802
|
+
const relativePath = path.relative(basePath, itemPath);
|
|
1803
|
+
files.push({
|
|
1804
|
+
absolutePath: itemPath,
|
|
1805
|
+
relativePath: relativePath
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
|
|
1811
|
+
scanDir(dirPath);
|
|
1812
|
+
return files;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
private async hosting_deployWordpressTheme_deployTheme(username: string, domain: string, slug: string, themePath: string, activate: boolean = false): Promise<boolean> {
|
|
1197
1816
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1198
|
-
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/
|
|
1817
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/themes/deploy`, baseUrl).toString();
|
|
1199
1818
|
|
|
1200
1819
|
try {
|
|
1201
1820
|
const bearerToken = await this.getAuthToken();
|
|
@@ -1209,8 +1828,9 @@ class MCPServer {
|
|
|
1209
1828
|
'Content-Type': 'application/json'
|
|
1210
1829
|
},
|
|
1211
1830
|
data: {
|
|
1212
|
-
|
|
1213
|
-
|
|
1831
|
+
slug,
|
|
1832
|
+
theme_path: themePath,
|
|
1833
|
+
is_activated: activate
|
|
1214
1834
|
},
|
|
1215
1835
|
timeout: 60000, // 60s
|
|
1216
1836
|
validateStatus: function (status: number): boolean {
|
|
@@ -1220,12 +1840,16 @@ class MCPServer {
|
|
|
1220
1840
|
|
|
1221
1841
|
const response = await axios(config);
|
|
1222
1842
|
|
|
1223
|
-
|
|
1843
|
+
if (response.status !== 200) {
|
|
1844
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
this.log('info', `Successfully triggered theme deployment for ${domain}`);
|
|
1224
1848
|
return true;
|
|
1225
1849
|
|
|
1226
1850
|
} catch (error) {
|
|
1227
1851
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1228
|
-
this.log('error', `Failed to trigger
|
|
1852
|
+
this.log('error', `Failed to trigger theme deployment: ${errorMessage}`);
|
|
1229
1853
|
|
|
1230
1854
|
if (axios.isAxiosError(error)) {
|
|
1231
1855
|
const responseData = error.response?.data;
|
|
@@ -1240,28 +1864,26 @@ class MCPServer {
|
|
|
1240
1864
|
}
|
|
1241
1865
|
}
|
|
1242
1866
|
|
|
1243
|
-
private async
|
|
1244
|
-
const { domain,
|
|
1867
|
+
private async handleWordpressThemeDeploy(params: Record<string, any>): Promise<any> {
|
|
1868
|
+
const { domain, slug, themePath, activate = false } = params;
|
|
1245
1869
|
|
|
1246
|
-
this.
|
|
1247
|
-
this.
|
|
1248
|
-
this.hosting_importWordpressWebsite_validateDatabaseFile(databaseDump);
|
|
1870
|
+
this.hosting_deployWordpressTheme_validateRequiredParams(params);
|
|
1871
|
+
this.hosting_deployWordpressTheme_validateThemeDirectory(themePath);
|
|
1249
1872
|
|
|
1250
|
-
// Auto-resolve username from domain
|
|
1251
1873
|
this.log('info', `Resolving username from domain: ${domain}`);
|
|
1252
1874
|
const username = await this.resolveUsername(domain);
|
|
1253
1875
|
|
|
1254
|
-
|
|
1876
|
+
const randomSuffix = this.hosting_deployWordpressTheme_generateRandomString(8);
|
|
1877
|
+
const uploadDirName = `${slug}-${randomSuffix}`;
|
|
1255
1878
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
}];
|
|
1879
|
+
this.log('info', `Scanning theme directory: ${themePath}`);
|
|
1880
|
+
const themeFiles = this.hosting_deployWordpressTheme_scanDirectory(themePath);
|
|
1881
|
+
|
|
1882
|
+
if (themeFiles.length === 0) {
|
|
1883
|
+
throw new Error(`No files found in theme directory: ${themePath}`);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
this.log('info', `Found ${themeFiles.length} files to upload`);
|
|
1265
1887
|
|
|
1266
1888
|
let uploadCredentials: any;
|
|
1267
1889
|
try {
|
|
@@ -1277,20 +1899,22 @@ class MCPServer {
|
|
|
1277
1899
|
throw new Error('Invalid upload credentials received from API');
|
|
1278
1900
|
}
|
|
1279
1901
|
|
|
1280
|
-
this.log('info', `Starting
|
|
1902
|
+
this.log('info', `Starting theme file upload to ${uploadUrl}`);
|
|
1281
1903
|
|
|
1282
1904
|
const results: any[] = [];
|
|
1283
1905
|
let successCount = 0;
|
|
1284
1906
|
let failureCount = 0;
|
|
1285
1907
|
|
|
1286
|
-
for (const fileInfo of
|
|
1908
|
+
for (const fileInfo of themeFiles) {
|
|
1287
1909
|
try {
|
|
1288
|
-
this.
|
|
1910
|
+
const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
|
|
1911
|
+
const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
|
|
1912
|
+
this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
|
|
1289
1913
|
|
|
1290
1914
|
const stats = fs.statSync(fileInfo.absolutePath);
|
|
1291
1915
|
const uploadResult = await this.uploadFile(
|
|
1292
1916
|
fileInfo.absolutePath,
|
|
1293
|
-
|
|
1917
|
+
uploadPath,
|
|
1294
1918
|
uploadUrl,
|
|
1295
1919
|
authRestToken,
|
|
1296
1920
|
authToken
|
|
@@ -1298,28 +1922,29 @@ class MCPServer {
|
|
|
1298
1922
|
|
|
1299
1923
|
results.push({
|
|
1300
1924
|
file: fileInfo.absolutePath,
|
|
1301
|
-
remotePath:
|
|
1302
|
-
type: fileInfo.type,
|
|
1925
|
+
remotePath: uploadPath,
|
|
1303
1926
|
status: 'success',
|
|
1304
1927
|
uploadUrl: uploadResult.url,
|
|
1305
1928
|
size: stats.size
|
|
1306
1929
|
});
|
|
1307
1930
|
|
|
1308
1931
|
successCount++;
|
|
1309
|
-
this.log('info', `Successfully uploaded
|
|
1932
|
+
this.log('info', `Successfully uploaded: ${uploadPath}`);
|
|
1310
1933
|
|
|
1311
1934
|
} catch (error) {
|
|
1312
1935
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1936
|
+
const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
|
|
1937
|
+
const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
|
|
1938
|
+
|
|
1313
1939
|
results.push({
|
|
1314
1940
|
file: fileInfo.absolutePath,
|
|
1315
|
-
remotePath:
|
|
1316
|
-
type: fileInfo.type,
|
|
1941
|
+
remotePath: uploadPath,
|
|
1317
1942
|
status: 'error',
|
|
1318
1943
|
error: errorMessage
|
|
1319
1944
|
});
|
|
1320
1945
|
|
|
1321
1946
|
failureCount++;
|
|
1322
|
-
this.log('error', `Failed to upload ${fileInfo.
|
|
1947
|
+
this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
|
|
1323
1948
|
}
|
|
1324
1949
|
}
|
|
1325
1950
|
|
|
@@ -1327,20 +1952,20 @@ class MCPServer {
|
|
|
1327
1952
|
|
|
1328
1953
|
if (failureCount === 0) {
|
|
1329
1954
|
try {
|
|
1330
|
-
this.log('info', 'All files uploaded successfully, triggering
|
|
1331
|
-
await this.
|
|
1955
|
+
this.log('info', 'All files uploaded successfully, triggering theme deployment...');
|
|
1956
|
+
await this.hosting_deployWordpressTheme_deployTheme(username, domain, slug, uploadDirName, activate);
|
|
1332
1957
|
} catch (error) {
|
|
1333
1958
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1334
|
-
this.log('error', `
|
|
1959
|
+
this.log('error', `Theme deployment failed: ${errorMessage}`);
|
|
1335
1960
|
return {
|
|
1336
1961
|
status: 'partial',
|
|
1337
1962
|
summary: {
|
|
1338
|
-
total:
|
|
1963
|
+
total: themeFiles.length,
|
|
1339
1964
|
successful: successCount,
|
|
1340
1965
|
failed: failureCount
|
|
1341
1966
|
},
|
|
1342
1967
|
results,
|
|
1343
|
-
|
|
1968
|
+
deploymentError: errorMessage
|
|
1344
1969
|
};
|
|
1345
1970
|
}
|
|
1346
1971
|
}
|
|
@@ -1348,83 +1973,142 @@ class MCPServer {
|
|
|
1348
1973
|
return {
|
|
1349
1974
|
status: overallStatus,
|
|
1350
1975
|
summary: {
|
|
1351
|
-
total:
|
|
1976
|
+
total: themeFiles.length,
|
|
1352
1977
|
successful: successCount,
|
|
1353
1978
|
failed: failureCount
|
|
1354
1979
|
},
|
|
1355
|
-
results
|
|
1980
|
+
results,
|
|
1981
|
+
uploadDirName,
|
|
1982
|
+
activated: activate
|
|
1356
1983
|
};
|
|
1357
1984
|
}
|
|
1358
1985
|
|
|
1359
|
-
private
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1986
|
+
private hosting_deployJsApplication_validateArchiveFormat(filePath: string): boolean {
|
|
1987
|
+
const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
|
|
1988
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
1989
|
+
|
|
1990
|
+
for (const ext of validExtensions) {
|
|
1991
|
+
if (fileName.endsWith(`.${ext}`)) {
|
|
1992
|
+
return true;
|
|
1993
|
+
}
|
|
1364
1994
|
}
|
|
1365
|
-
|
|
1995
|
+
|
|
1996
|
+
return false;
|
|
1366
1997
|
}
|
|
1367
1998
|
|
|
1368
|
-
private
|
|
1369
|
-
const { domain,
|
|
1999
|
+
private hosting_deployJsApplication_validateRequiredParams(params: Record<string, any>): void {
|
|
2000
|
+
const { domain, archivePath, removeArchive } = params;
|
|
1370
2001
|
|
|
1371
2002
|
if (!domain || typeof domain !== 'string') {
|
|
1372
2003
|
throw new Error('domain is required and must be a string');
|
|
1373
2004
|
}
|
|
1374
2005
|
|
|
1375
|
-
if (!
|
|
1376
|
-
throw new Error('
|
|
2006
|
+
if (!archivePath || typeof archivePath !== 'string') {
|
|
2007
|
+
throw new Error('archivePath is required and must be a string');
|
|
1377
2008
|
}
|
|
1378
2009
|
|
|
1379
|
-
if (
|
|
1380
|
-
throw new Error('
|
|
2010
|
+
if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
|
|
2011
|
+
throw new Error('removeArchive must be a boolean if provided');
|
|
1381
2012
|
}
|
|
1382
2013
|
}
|
|
1383
2014
|
|
|
1384
|
-
private
|
|
1385
|
-
if (!
|
|
1386
|
-
|
|
2015
|
+
private hosting_deployJsApplication_removeArchive(archivePath: string, removeArchive: boolean): boolean {
|
|
2016
|
+
if (!removeArchive) {
|
|
2017
|
+
return false;
|
|
1387
2018
|
}
|
|
1388
2019
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
2020
|
+
try {
|
|
2021
|
+
this.log('info', `Removing archive file: ${archivePath}`);
|
|
2022
|
+
fs.unlinkSync(archivePath);
|
|
2023
|
+
this.log('info', `Successfully removed archive file: ${archivePath}`);
|
|
2024
|
+
return true;
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2027
|
+
this.log('error', `Failed to remove archive file: ${errorMessage}`);
|
|
2028
|
+
// Don't fail the entire operation if archive removal fails
|
|
2029
|
+
return false;
|
|
1392
2030
|
}
|
|
1393
2031
|
}
|
|
1394
2032
|
|
|
1395
|
-
private
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
2033
|
+
private hosting_deployJsApplication_validateArchiveFile(archivePath: string): void {
|
|
2034
|
+
if (!fs.existsSync(archivePath)) {
|
|
2035
|
+
throw new Error(`Archive file not found: ${archivePath}`);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
const archiveStats = fs.statSync(archivePath);
|
|
2039
|
+
if (!archiveStats.isFile()) {
|
|
2040
|
+
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
if (!this.hosting_deployJsApplication_validateArchiveFormat(archivePath)) {
|
|
2044
|
+
throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
private async hosting_deployJsApplication_fetchBuildSettings(username: string, domain: string, archivePath: string): Promise<any> {
|
|
2049
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
2050
|
+
const archiveBasename = path.basename(archivePath);
|
|
2051
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/settings/from-archive?archive_path=${encodeURIComponent(archiveBasename)}`, baseUrl).toString();
|
|
2052
|
+
|
|
2053
|
+
try {
|
|
2054
|
+
const bearerToken = await this.getAuthToken();
|
|
2055
|
+
|
|
2056
|
+
const config: AxiosRequestConfig = {
|
|
2057
|
+
method: 'get',
|
|
2058
|
+
url,
|
|
2059
|
+
headers: {
|
|
2060
|
+
...this.headers,
|
|
2061
|
+
'Authorization': `Bearer ${bearerToken}`
|
|
2062
|
+
},
|
|
2063
|
+
timeout: 60000, // 60s
|
|
2064
|
+
validateStatus: function (status: number): boolean {
|
|
2065
|
+
return status < 500;
|
|
1413
2066
|
}
|
|
2067
|
+
};
|
|
2068
|
+
|
|
2069
|
+
const response = await axios(config);
|
|
2070
|
+
|
|
2071
|
+
if (response.status !== 200) {
|
|
2072
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
this.log('info', `Successfully fetched build settings for ${domain}`);
|
|
2076
|
+
return response.data;
|
|
2077
|
+
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2080
|
+
this.log('error', `Failed to fetch build settings: ${errorMessage}`);
|
|
2081
|
+
|
|
2082
|
+
if (axios.isAxiosError(error)) {
|
|
2083
|
+
const responseData = error.response?.data;
|
|
2084
|
+
const responseStatus = error.response?.status;
|
|
2085
|
+
this.log('error', 'API Error Details:', {
|
|
2086
|
+
status: responseStatus,
|
|
2087
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
2088
|
+
});
|
|
1414
2089
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
return files;
|
|
2090
|
+
|
|
2091
|
+
throw error;
|
|
2092
|
+
}
|
|
1419
2093
|
}
|
|
1420
2094
|
|
|
1421
|
-
private async
|
|
2095
|
+
private async hosting_deployJsApplication_triggerBuild(username: string, domain: string, archivePath: string, buildSettings: any): Promise<any> {
|
|
1422
2096
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1423
|
-
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/
|
|
2097
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
|
|
1424
2098
|
|
|
1425
2099
|
try {
|
|
1426
2100
|
const bearerToken = await this.getAuthToken();
|
|
1427
2101
|
|
|
2102
|
+
const archiveBasename = path.basename(archivePath);
|
|
2103
|
+
const buildData = {
|
|
2104
|
+
...buildSettings,
|
|
2105
|
+
node_version: buildSettings?.node_version || 20,
|
|
2106
|
+
source_type: 'archive',
|
|
2107
|
+
source_options: {
|
|
2108
|
+
archive_path: archiveBasename
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
2111
|
+
|
|
1428
2112
|
const config: AxiosRequestConfig = {
|
|
1429
2113
|
method: 'post',
|
|
1430
2114
|
url,
|
|
@@ -1433,10 +2117,7 @@ class MCPServer {
|
|
|
1433
2117
|
'Authorization': `Bearer ${bearerToken}`,
|
|
1434
2118
|
'Content-Type': 'application/json'
|
|
1435
2119
|
},
|
|
1436
|
-
data:
|
|
1437
|
-
slug,
|
|
1438
|
-
plugin_path: pluginPath
|
|
1439
|
-
},
|
|
2120
|
+
data: buildData,
|
|
1440
2121
|
timeout: 60000, // 60s
|
|
1441
2122
|
validateStatus: function (status: number): boolean {
|
|
1442
2123
|
return status < 500;
|
|
@@ -1449,12 +2130,12 @@ class MCPServer {
|
|
|
1449
2130
|
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1450
2131
|
}
|
|
1451
2132
|
|
|
1452
|
-
this.log('info', `Successfully triggered
|
|
1453
|
-
return
|
|
2133
|
+
this.log('info', `Successfully triggered build for ${domain}`);
|
|
2134
|
+
return response.data;
|
|
1454
2135
|
|
|
1455
2136
|
} catch (error) {
|
|
1456
2137
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1457
|
-
this.log('error', `Failed to trigger
|
|
2138
|
+
this.log('error', `Failed to trigger build: ${errorMessage}`);
|
|
1458
2139
|
|
|
1459
2140
|
if (axios.isAxiosError(error)) {
|
|
1460
2141
|
const responseData = error.response?.data;
|
|
@@ -1469,27 +2150,19 @@ class MCPServer {
|
|
|
1469
2150
|
}
|
|
1470
2151
|
}
|
|
1471
2152
|
|
|
1472
|
-
private async
|
|
1473
|
-
const { domain,
|
|
2153
|
+
private async handleJavascriptApplicationDeploy(params: Record<string, any>): Promise<any> {
|
|
2154
|
+
const { domain, archivePath, removeArchive = false } = params;
|
|
1474
2155
|
|
|
1475
|
-
this.
|
|
1476
|
-
this.
|
|
2156
|
+
this.hosting_deployJsApplication_validateRequiredParams(params);
|
|
2157
|
+
this.hosting_deployJsApplication_validateArchiveFile(archivePath);
|
|
1477
2158
|
|
|
2159
|
+
// Auto-resolve username from domain
|
|
1478
2160
|
this.log('info', `Resolving username from domain: ${domain}`);
|
|
1479
2161
|
const username = await this.resolveUsername(domain);
|
|
1480
2162
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
this.log('info', `Scanning plugin directory: ${pluginPath}`);
|
|
1485
|
-
const pluginFiles = this.hosting_deployWordpressPlugin_scanDirectory(pluginPath);
|
|
1486
|
-
|
|
1487
|
-
if (pluginFiles.length === 0) {
|
|
1488
|
-
throw new Error(`No files found in plugin directory: ${pluginPath}`);
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
this.log('info', `Found ${pluginFiles.length} files to upload`);
|
|
1492
|
-
|
|
2163
|
+
// Upload archive file
|
|
2164
|
+
this.log('info', `Starting archive upload for ${domain}`);
|
|
2165
|
+
|
|
1493
2166
|
let uploadCredentials: any;
|
|
1494
2167
|
try {
|
|
1495
2168
|
uploadCredentials = await this.fetchUploadCredentials(username, domain);
|
|
@@ -1504,158 +2177,181 @@ class MCPServer {
|
|
|
1504
2177
|
throw new Error('Invalid upload credentials received from API');
|
|
1505
2178
|
}
|
|
1506
2179
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
2180
|
+
const archiveBasename = path.basename(archivePath);
|
|
2181
|
+
let uploadResult: any;
|
|
2182
|
+
try {
|
|
2183
|
+
const stats = fs.statSync(archivePath);
|
|
2184
|
+
uploadResult = await this.uploadFile(
|
|
2185
|
+
archivePath,
|
|
2186
|
+
archiveBasename,
|
|
2187
|
+
uploadUrl,
|
|
2188
|
+
authRestToken,
|
|
2189
|
+
authToken
|
|
2190
|
+
);
|
|
1512
2191
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
2192
|
+
this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2195
|
+
throw new Error(`Failed to upload archive: ${errorMessage}`);
|
|
2196
|
+
}
|
|
1518
2197
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
2198
|
+
// Fetch build settings
|
|
2199
|
+
let buildSettings: any;
|
|
2200
|
+
try {
|
|
2201
|
+
this.log('info', `Fetching build settings for ${domain}`);
|
|
2202
|
+
buildSettings = await this.hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath);
|
|
2203
|
+
} catch (error) {
|
|
2204
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2205
|
+
this.log('error', `Failed to fetch build settings: ${errorMessage}`);
|
|
2206
|
+
const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
|
|
1527
2207
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
remotePath: uploadPath,
|
|
2208
|
+
return {
|
|
2209
|
+
upload: {
|
|
1531
2210
|
status: 'success',
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
this.log('info', `Successfully uploaded: ${uploadPath}`);
|
|
1538
|
-
|
|
1539
|
-
} catch (error) {
|
|
1540
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1541
|
-
const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
|
|
1542
|
-
const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
|
|
1543
|
-
|
|
1544
|
-
results.push({
|
|
1545
|
-
file: fileInfo.absolutePath,
|
|
1546
|
-
remotePath: uploadPath,
|
|
2211
|
+
data: {
|
|
2212
|
+
filename: uploadResult.filename
|
|
2213
|
+
}
|
|
2214
|
+
},
|
|
2215
|
+
resolveSettings: {
|
|
1547
2216
|
status: 'error',
|
|
1548
2217
|
error: errorMessage
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
2218
|
+
},
|
|
2219
|
+
build: {
|
|
2220
|
+
status: 'skipped'
|
|
2221
|
+
},
|
|
2222
|
+
removeArchive: {
|
|
2223
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
2224
|
+
}
|
|
2225
|
+
};
|
|
1554
2226
|
}
|
|
1555
2227
|
|
|
1556
|
-
|
|
2228
|
+
// Trigger build
|
|
2229
|
+
let buildResult: any;
|
|
2230
|
+
try {
|
|
2231
|
+
this.log('info', `Triggering build for ${domain}`);
|
|
2232
|
+
buildResult = await this.hosting_deployJsApplication_triggerBuild(username, domain, archivePath, buildSettings);
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2235
|
+
this.log('error', `Failed to trigger build: ${errorMessage}`);
|
|
2236
|
+
const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
|
|
1557
2237
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
status: '
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2238
|
+
return {
|
|
2239
|
+
upload: {
|
|
2240
|
+
status: 'success',
|
|
2241
|
+
data: {
|
|
2242
|
+
filename: uploadResult.filename
|
|
2243
|
+
}
|
|
2244
|
+
},
|
|
2245
|
+
resolveSettings: {
|
|
2246
|
+
status: 'success',
|
|
2247
|
+
data: buildSettings
|
|
2248
|
+
},
|
|
2249
|
+
build: {
|
|
2250
|
+
status: 'error',
|
|
2251
|
+
error: errorMessage
|
|
2252
|
+
},
|
|
2253
|
+
removeArchive: {
|
|
2254
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
1576
2257
|
}
|
|
1577
2258
|
|
|
2259
|
+
const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
|
|
2260
|
+
|
|
1578
2261
|
return {
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
2262
|
+
upload: {
|
|
2263
|
+
status: 'success',
|
|
2264
|
+
data: {
|
|
2265
|
+
filename: uploadResult.filename
|
|
2266
|
+
}
|
|
1584
2267
|
},
|
|
1585
|
-
|
|
1586
|
-
|
|
2268
|
+
resolveSettings: {
|
|
2269
|
+
status: 'success',
|
|
2270
|
+
data: buildSettings
|
|
2271
|
+
},
|
|
2272
|
+
build: {
|
|
2273
|
+
status: 'success',
|
|
2274
|
+
data: buildResult
|
|
2275
|
+
},
|
|
2276
|
+
removeArchive: {
|
|
2277
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
2278
|
+
}
|
|
1587
2279
|
};
|
|
1588
2280
|
}
|
|
1589
2281
|
|
|
1590
|
-
private
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
2282
|
+
private hosting_deployStaticWebsite_validateArchiveFormat(filePath: string): boolean {
|
|
2283
|
+
const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
|
|
2284
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
2285
|
+
|
|
2286
|
+
for (const ext of validExtensions) {
|
|
2287
|
+
if (fileName.endsWith(`.${ext}`)) {
|
|
2288
|
+
return true;
|
|
2289
|
+
}
|
|
1595
2290
|
}
|
|
1596
|
-
|
|
2291
|
+
|
|
2292
|
+
return false;
|
|
1597
2293
|
}
|
|
1598
2294
|
|
|
1599
|
-
private
|
|
1600
|
-
const { domain,
|
|
2295
|
+
private hosting_deployStaticWebsite_validateRequiredParams(params: Record<string, any>): void {
|
|
2296
|
+
const { domain, archivePath, removeArchive } = params;
|
|
1601
2297
|
|
|
1602
2298
|
if (!domain || typeof domain !== 'string') {
|
|
1603
2299
|
throw new Error('domain is required and must be a string');
|
|
1604
2300
|
}
|
|
1605
2301
|
|
|
1606
|
-
if (!
|
|
1607
|
-
throw new Error('
|
|
2302
|
+
if (!archivePath || typeof archivePath !== 'string') {
|
|
2303
|
+
throw new Error('archivePath is required and must be a string');
|
|
1608
2304
|
}
|
|
1609
2305
|
|
|
1610
|
-
if (
|
|
1611
|
-
throw new Error('
|
|
2306
|
+
if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
|
|
2307
|
+
throw new Error('removeArchive must be a boolean if provided');
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
private hosting_deployStaticWebsite_removeArchive(archivePath: string, removeArchive: boolean): boolean {
|
|
2312
|
+
if (!removeArchive) {
|
|
2313
|
+
return false;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
try {
|
|
2317
|
+
this.log('info', `Removing archive file: ${archivePath}`);
|
|
2318
|
+
fs.unlinkSync(archivePath);
|
|
2319
|
+
this.log('info', `Successfully removed archive file: ${archivePath}`);
|
|
2320
|
+
return true;
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2323
|
+
this.log('error', `Failed to remove archive file: ${errorMessage}`);
|
|
2324
|
+
return false;
|
|
1612
2325
|
}
|
|
1613
2326
|
}
|
|
1614
2327
|
|
|
1615
|
-
private
|
|
1616
|
-
if (!fs.existsSync(
|
|
1617
|
-
throw new Error(`
|
|
2328
|
+
private hosting_deployStaticWebsite_validateArchiveFile(archivePath: string): void {
|
|
2329
|
+
if (!fs.existsSync(archivePath)) {
|
|
2330
|
+
throw new Error(`Archive file not found: ${archivePath}`);
|
|
1618
2331
|
}
|
|
1619
2332
|
|
|
1620
|
-
const
|
|
1621
|
-
if (!
|
|
1622
|
-
throw new Error(`
|
|
2333
|
+
const archiveStats = fs.statSync(archivePath);
|
|
2334
|
+
if (!archiveStats.isFile()) {
|
|
2335
|
+
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
1623
2336
|
}
|
|
1624
|
-
}
|
|
1625
2337
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
const scanDir = (currentPath: string) => {
|
|
1630
|
-
const items = fs.readdirSync(currentPath);
|
|
1631
|
-
|
|
1632
|
-
for (const item of items) {
|
|
1633
|
-
const itemPath = path.join(currentPath, item);
|
|
1634
|
-
const stats = fs.statSync(itemPath);
|
|
1635
|
-
|
|
1636
|
-
if (stats.isDirectory()) {
|
|
1637
|
-
scanDir(itemPath);
|
|
1638
|
-
} else if (stats.isFile()) {
|
|
1639
|
-
const relativePath = path.relative(basePath, itemPath);
|
|
1640
|
-
files.push({
|
|
1641
|
-
absolutePath: itemPath,
|
|
1642
|
-
relativePath: relativePath
|
|
1643
|
-
});
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
};
|
|
1647
|
-
|
|
1648
|
-
scanDir(dirPath);
|
|
1649
|
-
return files;
|
|
2338
|
+
if (!this.hosting_deployStaticWebsite_validateArchiveFormat(archivePath)) {
|
|
2339
|
+
throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
|
|
2340
|
+
}
|
|
1650
2341
|
}
|
|
1651
2342
|
|
|
1652
|
-
private async
|
|
2343
|
+
private async hosting_deployStaticWebsite_triggerDeploy(username: string, domain: string, archivePath: string): Promise<any> {
|
|
1653
2344
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1654
|
-
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/
|
|
2345
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/deploy`, baseUrl).toString();
|
|
1655
2346
|
|
|
1656
2347
|
try {
|
|
1657
2348
|
const bearerToken = await this.getAuthToken();
|
|
1658
2349
|
|
|
2350
|
+
const archiveBasename = path.basename(archivePath);
|
|
2351
|
+
const deployData = {
|
|
2352
|
+
archive_path: archiveBasename
|
|
2353
|
+
};
|
|
2354
|
+
|
|
1659
2355
|
const config: AxiosRequestConfig = {
|
|
1660
2356
|
method: 'post',
|
|
1661
2357
|
url,
|
|
@@ -1664,12 +2360,8 @@ class MCPServer {
|
|
|
1664
2360
|
'Authorization': `Bearer ${bearerToken}`,
|
|
1665
2361
|
'Content-Type': 'application/json'
|
|
1666
2362
|
},
|
|
1667
|
-
data:
|
|
1668
|
-
|
|
1669
|
-
theme_path: themePath,
|
|
1670
|
-
is_activated: activate
|
|
1671
|
-
},
|
|
1672
|
-
timeout: 60000, // 60s
|
|
2363
|
+
data: deployData,
|
|
2364
|
+
timeout: 60000,
|
|
1673
2365
|
validateStatus: function (status: number): boolean {
|
|
1674
2366
|
return status < 500;
|
|
1675
2367
|
}
|
|
@@ -1681,12 +2373,12 @@ class MCPServer {
|
|
|
1681
2373
|
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1682
2374
|
}
|
|
1683
2375
|
|
|
1684
|
-
this.log('info', `Successfully triggered
|
|
1685
|
-
return
|
|
2376
|
+
this.log('info', `Successfully triggered deployment for ${domain}`);
|
|
2377
|
+
return response.data;
|
|
1686
2378
|
|
|
1687
2379
|
} catch (error) {
|
|
1688
2380
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1689
|
-
this.log('error', `Failed to trigger
|
|
2381
|
+
this.log('error', `Failed to trigger deployment: ${errorMessage}`);
|
|
1690
2382
|
|
|
1691
2383
|
if (axios.isAxiosError(error)) {
|
|
1692
2384
|
const responseData = error.response?.data;
|
|
@@ -1701,27 +2393,17 @@ class MCPServer {
|
|
|
1701
2393
|
}
|
|
1702
2394
|
}
|
|
1703
2395
|
|
|
1704
|
-
private async
|
|
1705
|
-
const { domain,
|
|
2396
|
+
private async handleStaticWebsiteDeploy(params: Record<string, any>): Promise<any> {
|
|
2397
|
+
const { domain, archivePath, removeArchive = false } = params;
|
|
1706
2398
|
|
|
1707
|
-
this.
|
|
1708
|
-
this.
|
|
2399
|
+
this.hosting_deployStaticWebsite_validateRequiredParams(params);
|
|
2400
|
+
this.hosting_deployStaticWebsite_validateArchiveFile(archivePath);
|
|
1709
2401
|
|
|
1710
2402
|
this.log('info', `Resolving username from domain: ${domain}`);
|
|
1711
2403
|
const username = await this.resolveUsername(domain);
|
|
1712
2404
|
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
this.log('info', `Scanning theme directory: ${themePath}`);
|
|
1717
|
-
const themeFiles = this.hosting_deployWordpressTheme_scanDirectory(themePath);
|
|
1718
|
-
|
|
1719
|
-
if (themeFiles.length === 0) {
|
|
1720
|
-
throw new Error(`No files found in theme directory: ${themePath}`);
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
this.log('info', `Found ${themeFiles.length} files to upload`);
|
|
1724
|
-
|
|
2405
|
+
this.log('info', `Starting archive upload for ${domain}`);
|
|
2406
|
+
|
|
1725
2407
|
let uploadCredentials: any;
|
|
1726
2408
|
try {
|
|
1727
2409
|
uploadCredentials = await this.fetchUploadCredentials(username, domain);
|
|
@@ -1736,163 +2418,110 @@ class MCPServer {
|
|
|
1736
2418
|
throw new Error('Invalid upload credentials received from API');
|
|
1737
2419
|
}
|
|
1738
2420
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2421
|
+
const archiveBasename = path.basename(archivePath);
|
|
2422
|
+
let uploadResult: any;
|
|
2423
|
+
try {
|
|
2424
|
+
const stats = fs.statSync(archivePath);
|
|
2425
|
+
uploadResult = await this.uploadFile(
|
|
2426
|
+
archivePath,
|
|
2427
|
+
archiveBasename,
|
|
2428
|
+
uploadUrl,
|
|
2429
|
+
authRestToken,
|
|
2430
|
+
authToken
|
|
2431
|
+
);
|
|
1744
2432
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
2433
|
+
this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2436
|
+
throw new Error(`Failed to upload archive: ${errorMessage}`);
|
|
2437
|
+
}
|
|
1750
2438
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
2439
|
+
let deployResult: any;
|
|
2440
|
+
try {
|
|
2441
|
+
this.log('info', `Triggering deployment for ${domain}`);
|
|
2442
|
+
deployResult = await this.hosting_deployStaticWebsite_triggerDeploy(username, domain, archivePath);
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2445
|
+
this.log('error', `Failed to trigger deployment: ${errorMessage}`);
|
|
2446
|
+
const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
|
|
1759
2447
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
remotePath: uploadPath,
|
|
2448
|
+
return {
|
|
2449
|
+
upload: {
|
|
1763
2450
|
status: 'success',
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
this.log('info', `Successfully uploaded: ${uploadPath}`);
|
|
1770
|
-
|
|
1771
|
-
} catch (error) {
|
|
1772
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1773
|
-
const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
|
|
1774
|
-
const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
|
|
1775
|
-
|
|
1776
|
-
results.push({
|
|
1777
|
-
file: fileInfo.absolutePath,
|
|
1778
|
-
remotePath: uploadPath,
|
|
2451
|
+
data: {
|
|
2452
|
+
filename: uploadResult.filename
|
|
2453
|
+
}
|
|
2454
|
+
},
|
|
2455
|
+
deploy: {
|
|
1779
2456
|
status: 'error',
|
|
1780
2457
|
error: errorMessage
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
}
|
|
2458
|
+
},
|
|
2459
|
+
removeArchive: {
|
|
2460
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
1786
2463
|
}
|
|
1787
2464
|
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
if (failureCount === 0) {
|
|
1791
|
-
try {
|
|
1792
|
-
this.log('info', 'All files uploaded successfully, triggering theme deployment...');
|
|
1793
|
-
await this.hosting_deployWordpressTheme_deployTheme(username, domain, slug, uploadDirName, activate);
|
|
1794
|
-
} catch (error) {
|
|
1795
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1796
|
-
this.log('error', `Theme deployment failed: ${errorMessage}`);
|
|
1797
|
-
return {
|
|
1798
|
-
status: 'partial',
|
|
1799
|
-
summary: {
|
|
1800
|
-
total: themeFiles.length,
|
|
1801
|
-
successful: successCount,
|
|
1802
|
-
failed: failureCount
|
|
1803
|
-
},
|
|
1804
|
-
results,
|
|
1805
|
-
deploymentError: errorMessage
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
2465
|
+
const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
|
|
1809
2466
|
|
|
1810
2467
|
return {
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2468
|
+
upload: {
|
|
2469
|
+
status: 'success',
|
|
2470
|
+
data: {
|
|
2471
|
+
filename: uploadResult.filename
|
|
2472
|
+
}
|
|
1816
2473
|
},
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
private hosting_deployJsApplication_validateArchiveFormat(filePath: string): boolean {
|
|
1824
|
-
const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
|
|
1825
|
-
const fileName = path.basename(filePath).toLowerCase();
|
|
1826
|
-
|
|
1827
|
-
for (const ext of validExtensions) {
|
|
1828
|
-
if (fileName.endsWith(`.${ext}`)) {
|
|
1829
|
-
return true;
|
|
2474
|
+
deploy: {
|
|
2475
|
+
status: 'success',
|
|
2476
|
+
data: deployResult
|
|
2477
|
+
},
|
|
2478
|
+
removeArchive: {
|
|
2479
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
1830
2480
|
}
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
return false;
|
|
2481
|
+
};
|
|
1834
2482
|
}
|
|
1835
2483
|
|
|
1836
|
-
private
|
|
1837
|
-
const { domain
|
|
2484
|
+
private hosting_listJsDeployments_validateRequiredParams(params: Record<string, any>): void {
|
|
2485
|
+
const { domain } = params;
|
|
1838
2486
|
|
|
1839
2487
|
if (!domain || typeof domain !== 'string') {
|
|
1840
2488
|
throw new Error('domain is required and must be a string');
|
|
1841
2489
|
}
|
|
1842
|
-
|
|
1843
|
-
if (!archivePath || typeof archivePath !== 'string') {
|
|
1844
|
-
throw new Error('archivePath is required and must be a string');
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
|
|
1848
|
-
throw new Error('removeArchive must be a boolean if provided');
|
|
1849
|
-
}
|
|
1850
2490
|
}
|
|
1851
2491
|
|
|
1852
|
-
private
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
}
|
|
2492
|
+
private hosting_listJsDeployments_buildQueryParams(params: Record<string, any>): string {
|
|
2493
|
+
const { page, perPage, states } = params;
|
|
2494
|
+
const queryParams = new URLSearchParams();
|
|
1856
2495
|
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
fs.unlinkSync(archivePath);
|
|
1860
|
-
this.log('info', `Successfully removed archive file: ${archivePath}`);
|
|
1861
|
-
return true;
|
|
1862
|
-
} catch (error) {
|
|
1863
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1864
|
-
this.log('error', `Failed to remove archive file: ${errorMessage}`);
|
|
1865
|
-
// Don't fail the entire operation if archive removal fails
|
|
1866
|
-
return false;
|
|
2496
|
+
if (page !== undefined && page !== null) {
|
|
2497
|
+
queryParams.append('page', page.toString());
|
|
1867
2498
|
}
|
|
1868
|
-
}
|
|
1869
2499
|
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
throw new Error(`Archive file not found: ${archivePath}`);
|
|
2500
|
+
if (perPage !== undefined && perPage !== null) {
|
|
2501
|
+
queryParams.append('per_page', perPage.toString());
|
|
1873
2502
|
}
|
|
1874
2503
|
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2504
|
+
if (states && Array.isArray(states) && states.length > 0) {
|
|
2505
|
+
states.forEach(state => {
|
|
2506
|
+
queryParams.append('states[]', state);
|
|
2507
|
+
});
|
|
1878
2508
|
}
|
|
1879
2509
|
|
|
1880
|
-
|
|
1881
|
-
throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
|
|
1882
|
-
}
|
|
2510
|
+
return queryParams.toString();
|
|
1883
2511
|
}
|
|
1884
2512
|
|
|
1885
|
-
private async
|
|
2513
|
+
private async hosting_listJsDeployments_fetchDeployments(username: string, domain: string, queryParams: string): Promise<any> {
|
|
1886
2514
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1887
|
-
const
|
|
1888
|
-
|
|
2515
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
|
|
2516
|
+
|
|
2517
|
+
const fullUrl = queryParams ? `${url}?${queryParams}` : url;
|
|
1889
2518
|
|
|
1890
2519
|
try {
|
|
1891
2520
|
const bearerToken = await this.getAuthToken();
|
|
1892
2521
|
|
|
1893
2522
|
const config: AxiosRequestConfig = {
|
|
1894
2523
|
method: 'get',
|
|
1895
|
-
url,
|
|
2524
|
+
url: fullUrl,
|
|
1896
2525
|
headers: {
|
|
1897
2526
|
...this.headers,
|
|
1898
2527
|
'Authorization': `Bearer ${bearerToken}`
|
|
@@ -1909,12 +2538,12 @@ class MCPServer {
|
|
|
1909
2538
|
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1910
2539
|
}
|
|
1911
2540
|
|
|
1912
|
-
this.log('info', `Successfully fetched
|
|
2541
|
+
this.log('info', `Successfully fetched deployments for ${domain}`);
|
|
1913
2542
|
return response.data;
|
|
1914
2543
|
|
|
1915
2544
|
} catch (error) {
|
|
1916
2545
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1917
|
-
this.log('error', `Failed to fetch
|
|
2546
|
+
this.log('error', `Failed to fetch deployments: ${errorMessage}`);
|
|
1918
2547
|
|
|
1919
2548
|
if (axios.isAxiosError(error)) {
|
|
1920
2549
|
const responseData = error.response?.data;
|
|
@@ -1929,33 +2558,85 @@ class MCPServer {
|
|
|
1929
2558
|
}
|
|
1930
2559
|
}
|
|
1931
2560
|
|
|
1932
|
-
private async
|
|
2561
|
+
private async handleListJavascriptDeployments(params: Record<string, any>): Promise<any> {
|
|
2562
|
+
const { domain, page, perPage, states } = params;
|
|
2563
|
+
|
|
2564
|
+
this.hosting_listJsDeployments_validateRequiredParams(params);
|
|
2565
|
+
|
|
2566
|
+
// Auto-resolve username from domain
|
|
2567
|
+
this.log('info', `Resolving username from domain: ${domain}`);
|
|
2568
|
+
const username = await this.resolveUsername(domain);
|
|
2569
|
+
|
|
2570
|
+
// Build query parameters
|
|
2571
|
+
const queryParams = this.hosting_listJsDeployments_buildQueryParams(params);
|
|
2572
|
+
|
|
2573
|
+
// Fetch deployments
|
|
2574
|
+
let deployments: any;
|
|
2575
|
+
try {
|
|
2576
|
+
this.log('info', `Fetching deployments for ${domain}`);
|
|
2577
|
+
deployments = await this.hosting_listJsDeployments_fetchDeployments(username, domain, queryParams);
|
|
2578
|
+
} catch (error) {
|
|
2579
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2580
|
+
this.log('error', `Failed to fetch deployments: ${errorMessage}`);
|
|
2581
|
+
throw error;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
return {
|
|
2585
|
+
status: 'success',
|
|
2586
|
+
domain,
|
|
2587
|
+
username,
|
|
2588
|
+
queryParams: {
|
|
2589
|
+
page,
|
|
2590
|
+
perPage,
|
|
2591
|
+
states
|
|
2592
|
+
},
|
|
2593
|
+
deployments
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
private hosting_showJsDeploymentLogs_validateRequiredParams(params: Record<string, any>): void {
|
|
2598
|
+
const { domain, buildUuid, fromLine } = params;
|
|
2599
|
+
|
|
2600
|
+
if (!domain || typeof domain !== 'string') {
|
|
2601
|
+
throw new Error('domain is required and must be a string');
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
if (!buildUuid || typeof buildUuid !== 'string') {
|
|
2605
|
+
throw new Error('buildUuid is required and must be a string');
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
if (fromLine !== undefined && (typeof fromLine !== 'number' || !Number.isInteger(fromLine) || fromLine < 0)) {
|
|
2609
|
+
throw new Error('fromLine must be a non-negative integer when provided');
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
private hosting_showJsDeploymentLogs_buildQueryParams(params: Record<string, any>): string {
|
|
2614
|
+
const { fromLine } = params;
|
|
2615
|
+
const queryParams = new URLSearchParams();
|
|
2616
|
+
|
|
2617
|
+
const line = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
|
|
2618
|
+
queryParams.append('from_line', line.toString());
|
|
2619
|
+
|
|
2620
|
+
return queryParams.toString();
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
private async hosting_showJsDeploymentLogs_fetchLogs(username: string, domain: string, buildUuid: string, queryParams: string): Promise<any> {
|
|
1933
2624
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1934
|
-
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
|
|
2625
|
+
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/${buildUuid}/logs`, baseUrl).toString();
|
|
2626
|
+
|
|
2627
|
+
const fullUrl = queryParams ? `${url}?${queryParams}` : url;
|
|
1935
2628
|
|
|
1936
2629
|
try {
|
|
1937
2630
|
const bearerToken = await this.getAuthToken();
|
|
1938
2631
|
|
|
1939
|
-
const archiveBasename = path.basename(archivePath);
|
|
1940
|
-
const buildData = {
|
|
1941
|
-
...buildSettings,
|
|
1942
|
-
node_version: buildSettings?.node_version || 20,
|
|
1943
|
-
source_type: 'archive',
|
|
1944
|
-
source_options: {
|
|
1945
|
-
archive_path: archiveBasename
|
|
1946
|
-
}
|
|
1947
|
-
};
|
|
1948
|
-
|
|
1949
2632
|
const config: AxiosRequestConfig = {
|
|
1950
|
-
method: '
|
|
1951
|
-
url,
|
|
2633
|
+
method: 'get',
|
|
2634
|
+
url: fullUrl,
|
|
1952
2635
|
headers: {
|
|
1953
2636
|
...this.headers,
|
|
1954
|
-
'Authorization': `Bearer ${bearerToken}
|
|
1955
|
-
'Content-Type': 'application/json'
|
|
2637
|
+
'Authorization': `Bearer ${bearerToken}`
|
|
1956
2638
|
},
|
|
1957
|
-
|
|
1958
|
-
timeout: 60000, // 60s
|
|
2639
|
+
timeout: 60000,
|
|
1959
2640
|
validateStatus: function (status: number): boolean {
|
|
1960
2641
|
return status < 500;
|
|
1961
2642
|
}
|
|
@@ -1967,12 +2648,12 @@ class MCPServer {
|
|
|
1967
2648
|
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
1968
2649
|
}
|
|
1969
2650
|
|
|
1970
|
-
this.log('info', `Successfully
|
|
2651
|
+
this.log('info', `Successfully fetched logs for ${domain} build ${buildUuid}`);
|
|
1971
2652
|
return response.data;
|
|
1972
2653
|
|
|
1973
2654
|
} catch (error) {
|
|
1974
2655
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1975
|
-
this.log('error', `Failed to
|
|
2656
|
+
this.log('error', `Failed to fetch logs: ${errorMessage}`);
|
|
1976
2657
|
|
|
1977
2658
|
if (axios.isAxiosError(error)) {
|
|
1978
2659
|
const responseData = error.response?.data;
|
|
@@ -1987,149 +2668,51 @@ class MCPServer {
|
|
|
1987
2668
|
}
|
|
1988
2669
|
}
|
|
1989
2670
|
|
|
1990
|
-
private async
|
|
1991
|
-
const { domain,
|
|
2671
|
+
private async handleShowJsDeploymentLogs(params: Record<string, any>): Promise<any> {
|
|
2672
|
+
const { domain, buildUuid, fromLine } = params;
|
|
1992
2673
|
|
|
1993
|
-
this.
|
|
1994
|
-
this.hosting_deployJsApplication_validateArchiveFile(archivePath);
|
|
2674
|
+
this.hosting_showJsDeploymentLogs_validateRequiredParams(params);
|
|
1995
2675
|
|
|
1996
|
-
// Auto-resolve username from domain
|
|
1997
2676
|
this.log('info', `Resolving username from domain: ${domain}`);
|
|
1998
2677
|
const username = await this.resolveUsername(domain);
|
|
1999
2678
|
|
|
2000
|
-
|
|
2001
|
-
this.log('info', `Starting archive upload for ${domain}`);
|
|
2002
|
-
|
|
2003
|
-
let uploadCredentials: any;
|
|
2004
|
-
try {
|
|
2005
|
-
uploadCredentials = await this.fetchUploadCredentials(username, domain);
|
|
2006
|
-
} catch (error) {
|
|
2007
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2008
|
-
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
|
|
2012
|
-
|
|
2013
|
-
if (!uploadUrl || !authToken || !authRestToken) {
|
|
2014
|
-
throw new Error('Invalid upload credentials received from API');
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
const archiveBasename = path.basename(archivePath);
|
|
2018
|
-
let uploadResult: any;
|
|
2019
|
-
try {
|
|
2020
|
-
const stats = fs.statSync(archivePath);
|
|
2021
|
-
uploadResult = await this.uploadFile(
|
|
2022
|
-
archivePath,
|
|
2023
|
-
archiveBasename,
|
|
2024
|
-
uploadUrl,
|
|
2025
|
-
authRestToken,
|
|
2026
|
-
authToken
|
|
2027
|
-
);
|
|
2028
|
-
|
|
2029
|
-
this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
|
|
2030
|
-
} catch (error) {
|
|
2031
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2032
|
-
throw new Error(`Failed to upload archive: ${errorMessage}`);
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
// Fetch build settings
|
|
2036
|
-
let buildSettings: any;
|
|
2037
|
-
try {
|
|
2038
|
-
this.log('info', `Fetching build settings for ${domain}`);
|
|
2039
|
-
buildSettings = await this.hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath);
|
|
2040
|
-
} catch (error) {
|
|
2041
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2042
|
-
this.log('error', `Failed to fetch build settings: ${errorMessage}`);
|
|
2043
|
-
const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
|
|
2044
|
-
|
|
2045
|
-
return {
|
|
2046
|
-
upload: {
|
|
2047
|
-
status: 'success',
|
|
2048
|
-
data: {
|
|
2049
|
-
filename: uploadResult.filename
|
|
2050
|
-
}
|
|
2051
|
-
},
|
|
2052
|
-
resolveSettings: {
|
|
2053
|
-
status: 'error',
|
|
2054
|
-
error: errorMessage
|
|
2055
|
-
},
|
|
2056
|
-
build: {
|
|
2057
|
-
status: 'skipped'
|
|
2058
|
-
},
|
|
2059
|
-
removeArchive: {
|
|
2060
|
-
status: archiveRemoved ? 'success' : 'skipped'
|
|
2061
|
-
}
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2679
|
+
const queryParams = this.hosting_showJsDeploymentLogs_buildQueryParams(params);
|
|
2064
2680
|
|
|
2065
|
-
|
|
2066
|
-
let buildResult: any;
|
|
2681
|
+
let logs: any;
|
|
2067
2682
|
try {
|
|
2068
|
-
this.log('info', `
|
|
2069
|
-
|
|
2683
|
+
this.log('info', `Fetching logs for ${domain}, build ${buildUuid}`);
|
|
2684
|
+
logs = await this.hosting_showJsDeploymentLogs_fetchLogs(username, domain, buildUuid, queryParams);
|
|
2070
2685
|
} catch (error) {
|
|
2071
2686
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2072
|
-
this.log('error', `Failed to
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
return {
|
|
2076
|
-
upload: {
|
|
2077
|
-
status: 'success',
|
|
2078
|
-
data: {
|
|
2079
|
-
filename: uploadResult.filename
|
|
2080
|
-
}
|
|
2081
|
-
},
|
|
2082
|
-
resolveSettings: {
|
|
2083
|
-
status: 'success',
|
|
2084
|
-
data: buildSettings
|
|
2085
|
-
},
|
|
2086
|
-
build: {
|
|
2087
|
-
status: 'error',
|
|
2088
|
-
error: errorMessage
|
|
2089
|
-
},
|
|
2090
|
-
removeArchive: {
|
|
2091
|
-
status: archiveRemoved ? 'success' : 'skipped'
|
|
2092
|
-
}
|
|
2093
|
-
};
|
|
2687
|
+
this.log('error', `Failed to fetch logs: ${errorMessage}`);
|
|
2688
|
+
throw error;
|
|
2094
2689
|
}
|
|
2095
2690
|
|
|
2096
|
-
const
|
|
2691
|
+
const effectiveFromLine = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
|
|
2097
2692
|
|
|
2098
2693
|
return {
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
};
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
private hosting_deployStaticWebsite_validateArchiveFormat(filePath: string): boolean {
|
|
2120
|
-
const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
|
|
2121
|
-
const fileName = path.basename(filePath).toLowerCase();
|
|
2122
|
-
|
|
2123
|
-
for (const ext of validExtensions) {
|
|
2124
|
-
if (fileName.endsWith(`.${ext}`)) {
|
|
2125
|
-
return true;
|
|
2126
|
-
}
|
|
2694
|
+
domain,
|
|
2695
|
+
username,
|
|
2696
|
+
buildUuid,
|
|
2697
|
+
fromLine: effectiveFromLine,
|
|
2698
|
+
logs
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
private agencyHosting_normalizePath(pathString: string): string {
|
|
2703
|
+
return pathString.replace(/\\/g, '/');
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
private agencyHosting_generateUploadDir(): string {
|
|
2707
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
2708
|
+
let result = '';
|
|
2709
|
+
for (let i = 0; i < 12; i++) {
|
|
2710
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
2127
2711
|
}
|
|
2128
|
-
|
|
2129
|
-
return false;
|
|
2712
|
+
return result;
|
|
2130
2713
|
}
|
|
2131
2714
|
|
|
2132
|
-
private
|
|
2715
|
+
private agencyHosting_validateRequiredParams(params: Record<string, any>): void {
|
|
2133
2716
|
const { domain, archivePath, removeArchive } = params;
|
|
2134
2717
|
|
|
2135
2718
|
if (!domain || typeof domain !== 'string') {
|
|
@@ -2145,7 +2728,18 @@ class MCPServer {
|
|
|
2145
2728
|
}
|
|
2146
2729
|
}
|
|
2147
2730
|
|
|
2148
|
-
private
|
|
2731
|
+
private agencyHosting_validateArchiveFile(archivePath: string): void {
|
|
2732
|
+
if (!fs.existsSync(archivePath)) {
|
|
2733
|
+
throw new Error(`Archive file not found: ${archivePath}`);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
const archiveStats = fs.statSync(archivePath);
|
|
2737
|
+
if (!archiveStats.isFile()) {
|
|
2738
|
+
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
private agencyHosting_removeArchive(archivePath: string, removeArchive: boolean): boolean {
|
|
2149
2743
|
if (!removeArchive) {
|
|
2150
2744
|
return false;
|
|
2151
2745
|
}
|
|
@@ -2162,33 +2756,216 @@ class MCPServer {
|
|
|
2162
2756
|
}
|
|
2163
2757
|
}
|
|
2164
2758
|
|
|
2165
|
-
private
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
}
|
|
2759
|
+
private async agencyHosting_resolveWebsiteUid(domain: string): Promise<string> {
|
|
2760
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
2761
|
+
const perPage = 100;
|
|
2169
2762
|
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2763
|
+
try {
|
|
2764
|
+
const bearerToken = await this.getAuthToken();
|
|
2765
|
+
let page = 1;
|
|
2766
|
+
let total = 0;
|
|
2174
2767
|
|
|
2175
|
-
|
|
2176
|
-
|
|
2768
|
+
do {
|
|
2769
|
+
const url = new URL(
|
|
2770
|
+
`api/agency-hosting/v1/domains?page=${page}&per_page=${perPage}`,
|
|
2771
|
+
baseUrl
|
|
2772
|
+
).toString();
|
|
2773
|
+
|
|
2774
|
+
const config: AxiosRequestConfig = {
|
|
2775
|
+
method: 'get',
|
|
2776
|
+
url,
|
|
2777
|
+
headers: {
|
|
2778
|
+
...this.headers,
|
|
2779
|
+
'Authorization': `Bearer ${bearerToken}`
|
|
2780
|
+
},
|
|
2781
|
+
timeout: 60000, // 60s
|
|
2782
|
+
validateStatus: function (status: number): boolean {
|
|
2783
|
+
return status < 500;
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
|
|
2787
|
+
const response = await axios(config);
|
|
2788
|
+
|
|
2789
|
+
if (response.status !== 200) {
|
|
2790
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
const domains = response.data?.data ?? [];
|
|
2794
|
+
const match = domains.find((item: any) => item.fqdn === domain);
|
|
2795
|
+
if (match?.website_uid) {
|
|
2796
|
+
this.log('info', `Resolved website UID: ${match.website_uid} for domain: ${domain}`);
|
|
2797
|
+
return match.website_uid;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
total = response.data?.meta?.total ?? domains.length;
|
|
2801
|
+
page++;
|
|
2802
|
+
} while ((page - 1) * perPage < total);
|
|
2803
|
+
|
|
2804
|
+
throw new Error(`No Agency Plan website found for domain: ${domain}`);
|
|
2805
|
+
|
|
2806
|
+
} catch (error) {
|
|
2807
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2808
|
+
this.log('error', `Failed to resolve website UID for domain ${domain}: ${errorMessage}`);
|
|
2809
|
+
|
|
2810
|
+
if (axios.isAxiosError(error)) {
|
|
2811
|
+
const responseData = error.response?.data;
|
|
2812
|
+
const responseStatus = error.response?.status;
|
|
2813
|
+
this.log('error', 'API Error Details:', {
|
|
2814
|
+
status: responseStatus,
|
|
2815
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
2816
|
+
});
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
throw error;
|
|
2177
2820
|
}
|
|
2178
2821
|
}
|
|
2179
2822
|
|
|
2180
|
-
private async
|
|
2823
|
+
private async agencyHosting_fetchUploadCredentials(
|
|
2824
|
+
websiteUid: string
|
|
2825
|
+
): Promise<{ url: string; auth_key: string; rest_auth_key: string }> {
|
|
2181
2826
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
2182
|
-
const url = new URL(`api/hosting/v1/
|
|
2827
|
+
const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/upload-urls`, baseUrl).toString();
|
|
2183
2828
|
|
|
2184
2829
|
try {
|
|
2185
2830
|
const bearerToken = await this.getAuthToken();
|
|
2186
2831
|
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2832
|
+
const config: AxiosRequestConfig = {
|
|
2833
|
+
method: 'post',
|
|
2834
|
+
url,
|
|
2835
|
+
headers: {
|
|
2836
|
+
...this.headers,
|
|
2837
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
2838
|
+
'Content-Type': 'application/json'
|
|
2839
|
+
},
|
|
2840
|
+
timeout: 60000, // 60s
|
|
2841
|
+
validateStatus: function (status: number): boolean {
|
|
2842
|
+
return status < 500;
|
|
2843
|
+
}
|
|
2190
2844
|
};
|
|
2191
2845
|
|
|
2846
|
+
const response = await axios(config);
|
|
2847
|
+
|
|
2848
|
+
if (response.status !== 200) {
|
|
2849
|
+
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
return response.data;
|
|
2853
|
+
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2856
|
+
this.log('error', `Failed to fetch upload credentials: ${errorMessage}`);
|
|
2857
|
+
|
|
2858
|
+
if (axios.isAxiosError(error)) {
|
|
2859
|
+
const responseData = error.response?.data;
|
|
2860
|
+
const responseStatus = error.response?.status;
|
|
2861
|
+
this.log('error', 'API Error Details:', {
|
|
2862
|
+
status: responseStatus,
|
|
2863
|
+
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
throw error;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
private async agencyHosting_uploadFile(
|
|
2872
|
+
filePath: string,
|
|
2873
|
+
relativePath: string,
|
|
2874
|
+
uploadUrl: string,
|
|
2875
|
+
authRestToken: string,
|
|
2876
|
+
authToken: string
|
|
2877
|
+
): Promise<{ url: string; filename: string }> {
|
|
2878
|
+
return new Promise(async (resolve, reject) => {
|
|
2879
|
+
try {
|
|
2880
|
+
const stats = fs.statSync(filePath);
|
|
2881
|
+
const fileStream = fs.createReadStream(filePath);
|
|
2882
|
+
|
|
2883
|
+
const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
|
|
2884
|
+
const normalizedPath = this.agencyHosting_normalizePath(relativePath);
|
|
2885
|
+
const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
|
|
2886
|
+
|
|
2887
|
+
const requestHeaders: Record<string, string> = {
|
|
2888
|
+
'X-Auth': authToken,
|
|
2889
|
+
'X-Auth-Rest': authRestToken,
|
|
2890
|
+
'upload-length': stats.size.toString(),
|
|
2891
|
+
'upload-offset': '0'
|
|
2892
|
+
};
|
|
2893
|
+
|
|
2894
|
+
try {
|
|
2895
|
+
this.log('debug', `Making pre-upload POST request to ${uploadUrlWithFile}`);
|
|
2896
|
+
await axios.post(uploadUrlWithFile, '', {
|
|
2897
|
+
headers: requestHeaders,
|
|
2898
|
+
timeout: 60000, // 60s
|
|
2899
|
+
validateStatus: function (status: number): boolean {
|
|
2900
|
+
return status == 201;
|
|
2901
|
+
}
|
|
2902
|
+
});
|
|
2903
|
+
} catch (error) {
|
|
2904
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2905
|
+
|
|
2906
|
+
if (axios.isAxiosError(error)) {
|
|
2907
|
+
const responseData = error.response?.data;
|
|
2908
|
+
const responseStatus = error.response?.status;
|
|
2909
|
+
const responseHeaders = error.response?.headers;
|
|
2910
|
+
const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
|
|
2911
|
+
|
|
2912
|
+
this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
|
|
2913
|
+
status: responseStatus,
|
|
2914
|
+
headers: responseHeaders,
|
|
2915
|
+
data: responseText,
|
|
2916
|
+
message: errorMessage
|
|
2917
|
+
});
|
|
2918
|
+
reject(new Error(`Pre-upload request failed: ${errorMessage}`));
|
|
2919
|
+
return;
|
|
2920
|
+
} else {
|
|
2921
|
+
this.log('error', `Pre-upload POST request failed: ${errorMessage}`);
|
|
2922
|
+
reject(new Error(`Pre-upload request failed: ${errorMessage}`));
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
const upload = new tus.Upload(fileStream, {
|
|
2928
|
+
uploadUrl: uploadUrlWithFile,
|
|
2929
|
+
retryDelays: [1000, 2000, 4000, 8000, 16000, 20000],
|
|
2930
|
+
uploadDataDuringCreation: false,
|
|
2931
|
+
parallelUploads: 1,
|
|
2932
|
+
chunkSize: 10485760,
|
|
2933
|
+
headers: requestHeaders,
|
|
2934
|
+
removeFingerprintOnSuccess: true,
|
|
2935
|
+
uploadSize: stats.size,
|
|
2936
|
+
metadata: {
|
|
2937
|
+
filename: path.basename(relativePath)
|
|
2938
|
+
},
|
|
2939
|
+
onError: (error: Error) => {
|
|
2940
|
+
this.log('error', `TUS upload error for ${relativePath}`, { error: error.message });
|
|
2941
|
+
reject(new Error(`Upload failed: ${error.message}`));
|
|
2942
|
+
},
|
|
2943
|
+
onSuccess: () => {
|
|
2944
|
+
this.log('info', `TUS upload completed for ${relativePath}`, { url: upload.url });
|
|
2945
|
+
resolve({
|
|
2946
|
+
url: upload.url || uploadUrlWithFile,
|
|
2947
|
+
filename: relativePath
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
});
|
|
2951
|
+
|
|
2952
|
+
upload.start();
|
|
2953
|
+
|
|
2954
|
+
} catch (error) {
|
|
2955
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2956
|
+
this.log('error', `Error preparing upload for ${filePath}`, { error: errorMessage });
|
|
2957
|
+
reject(new Error(`Failed to prepare upload: ${errorMessage}`));
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
private async agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid: string, archivePath: string): Promise<any> {
|
|
2963
|
+
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
2964
|
+
const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/build-assets`, baseUrl).toString();
|
|
2965
|
+
|
|
2966
|
+
try {
|
|
2967
|
+
const bearerToken = await this.getAuthToken();
|
|
2968
|
+
|
|
2192
2969
|
const config: AxiosRequestConfig = {
|
|
2193
2970
|
method: 'post',
|
|
2194
2971
|
url,
|
|
@@ -2197,7 +2974,9 @@ class MCPServer {
|
|
|
2197
2974
|
'Authorization': `Bearer ${bearerToken}`,
|
|
2198
2975
|
'Content-Type': 'application/json'
|
|
2199
2976
|
},
|
|
2200
|
-
data:
|
|
2977
|
+
data: {
|
|
2978
|
+
archive_path: archivePath
|
|
2979
|
+
},
|
|
2201
2980
|
timeout: 60000,
|
|
2202
2981
|
validateStatus: function (status: number): boolean {
|
|
2203
2982
|
return status < 500;
|
|
@@ -2206,16 +2985,16 @@ class MCPServer {
|
|
|
2206
2985
|
|
|
2207
2986
|
const response = await axios(config);
|
|
2208
2987
|
|
|
2209
|
-
if (response.status
|
|
2988
|
+
if (response.status < 200 || response.status >= 300) {
|
|
2210
2989
|
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
2211
2990
|
}
|
|
2212
2991
|
|
|
2213
|
-
this.log('info', `Successfully triggered
|
|
2992
|
+
this.log('info', `Successfully triggered build for website ${websiteUid}`);
|
|
2214
2993
|
return response.data;
|
|
2215
2994
|
|
|
2216
2995
|
} catch (error) {
|
|
2217
2996
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2218
|
-
this.log('error', `Failed to trigger
|
|
2997
|
+
this.log('error', `Failed to trigger build: ${errorMessage}`);
|
|
2219
2998
|
|
|
2220
2999
|
if (axios.isAxiosError(error)) {
|
|
2221
3000
|
const responseData = error.response?.data;
|
|
@@ -2230,20 +3009,20 @@ class MCPServer {
|
|
|
2230
3009
|
}
|
|
2231
3010
|
}
|
|
2232
3011
|
|
|
2233
|
-
private async
|
|
2234
|
-
const { domain, archivePath, removeArchive =
|
|
2235
|
-
|
|
2236
|
-
this.hosting_deployStaticWebsite_validateRequiredParams(params);
|
|
2237
|
-
this.hosting_deployStaticWebsite_validateArchiveFile(archivePath);
|
|
3012
|
+
private async handleNodeStaticDeploy(params: Record<string, any>): Promise<any> {
|
|
3013
|
+
const { domain, archivePath, removeArchive = true } = params;
|
|
2238
3014
|
|
|
2239
|
-
this.
|
|
2240
|
-
|
|
3015
|
+
this.agencyHosting_validateRequiredParams(params);
|
|
3016
|
+
this.agencyHosting_validateArchiveFile(archivePath);
|
|
3017
|
+
|
|
3018
|
+
this.log('info', `Resolving website UID from domain: ${domain}`);
|
|
3019
|
+
const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
|
|
2241
3020
|
|
|
2242
3021
|
this.log('info', `Starting archive upload for ${domain}`);
|
|
2243
|
-
|
|
3022
|
+
|
|
2244
3023
|
let uploadCredentials: any;
|
|
2245
3024
|
try {
|
|
2246
|
-
uploadCredentials = await this.
|
|
3025
|
+
uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
|
|
2247
3026
|
} catch (error) {
|
|
2248
3027
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2249
3028
|
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
@@ -2256,12 +3035,12 @@ class MCPServer {
|
|
|
2256
3035
|
}
|
|
2257
3036
|
|
|
2258
3037
|
const archiveBasename = path.basename(archivePath);
|
|
3038
|
+
const uploadDir = `.h5g/${this.agencyHosting_generateUploadDir()}`;
|
|
2259
3039
|
let uploadResult: any;
|
|
2260
3040
|
try {
|
|
2261
|
-
|
|
2262
|
-
uploadResult = await this.uploadFile(
|
|
3041
|
+
uploadResult = await this.agencyHosting_uploadFile(
|
|
2263
3042
|
archivePath,
|
|
2264
|
-
archiveBasename
|
|
3043
|
+
`${uploadDir}/${archiveBasename}`,
|
|
2265
3044
|
uploadUrl,
|
|
2266
3045
|
authRestToken,
|
|
2267
3046
|
authToken
|
|
@@ -2275,12 +3054,12 @@ class MCPServer {
|
|
|
2275
3054
|
|
|
2276
3055
|
let deployResult: any;
|
|
2277
3056
|
try {
|
|
2278
|
-
this.log('info', `Triggering
|
|
2279
|
-
deployResult = await this.
|
|
3057
|
+
this.log('info', `Triggering build for ${domain}`);
|
|
3058
|
+
deployResult = await this.agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid, uploadDir);
|
|
2280
3059
|
} catch (error) {
|
|
2281
3060
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2282
|
-
this.log('error', `Failed to trigger
|
|
2283
|
-
const archiveRemoved = this.
|
|
3061
|
+
this.log('error', `Failed to trigger build: ${errorMessage}`);
|
|
3062
|
+
const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
|
|
2284
3063
|
|
|
2285
3064
|
return {
|
|
2286
3065
|
upload: {
|
|
@@ -2299,7 +3078,7 @@ class MCPServer {
|
|
|
2299
3078
|
};
|
|
2300
3079
|
}
|
|
2301
3080
|
|
|
2302
|
-
const archiveRemoved = this.
|
|
3081
|
+
const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
|
|
2303
3082
|
|
|
2304
3083
|
return {
|
|
2305
3084
|
upload: {
|
|
@@ -2318,52 +3097,25 @@ class MCPServer {
|
|
|
2318
3097
|
};
|
|
2319
3098
|
}
|
|
2320
3099
|
|
|
2321
|
-
private
|
|
2322
|
-
const { domain } = params;
|
|
2323
|
-
|
|
2324
|
-
if (!domain || typeof domain !== 'string') {
|
|
2325
|
-
throw new Error('domain is required and must be a string');
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
private hosting_listJsDeployments_buildQueryParams(params: Record<string, any>): string {
|
|
2330
|
-
const { page, perPage, states } = params;
|
|
2331
|
-
const queryParams = new URLSearchParams();
|
|
2332
|
-
|
|
2333
|
-
if (page !== undefined && page !== null) {
|
|
2334
|
-
queryParams.append('page', page.toString());
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
if (perPage !== undefined && perPage !== null) {
|
|
2338
|
-
queryParams.append('per_page', perPage.toString());
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
if (states && Array.isArray(states) && states.length > 0) {
|
|
2342
|
-
states.forEach(state => {
|
|
2343
|
-
queryParams.append('states[]', state);
|
|
2344
|
-
});
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
return queryParams.toString();
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
private async hosting_listJsDeployments_fetchDeployments(username: string, domain: string, queryParams: string): Promise<any> {
|
|
3100
|
+
private async agencyHosting_deployPhpApp_triggerImportArchive(websiteUid: string, archiveName: string): Promise<any> {
|
|
2351
3101
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
2352
|
-
const url = new URL(`api/hosting/v1/
|
|
2353
|
-
|
|
2354
|
-
const fullUrl = queryParams ? `${url}?${queryParams}` : url;
|
|
3102
|
+
const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/import-archive`, baseUrl).toString();
|
|
2355
3103
|
|
|
2356
3104
|
try {
|
|
2357
3105
|
const bearerToken = await this.getAuthToken();
|
|
2358
3106
|
|
|
2359
3107
|
const config: AxiosRequestConfig = {
|
|
2360
|
-
method: '
|
|
2361
|
-
url
|
|
3108
|
+
method: 'post',
|
|
3109
|
+
url,
|
|
2362
3110
|
headers: {
|
|
2363
3111
|
...this.headers,
|
|
2364
|
-
'Authorization': `Bearer ${bearerToken}
|
|
3112
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
3113
|
+
'Content-Type': 'application/json'
|
|
2365
3114
|
},
|
|
2366
|
-
|
|
3115
|
+
data: {
|
|
3116
|
+
archive_name: archiveName
|
|
3117
|
+
},
|
|
3118
|
+
timeout: 60000,
|
|
2367
3119
|
validateStatus: function (status: number): boolean {
|
|
2368
3120
|
return status < 500;
|
|
2369
3121
|
}
|
|
@@ -2371,16 +3123,16 @@ class MCPServer {
|
|
|
2371
3123
|
|
|
2372
3124
|
const response = await axios(config);
|
|
2373
3125
|
|
|
2374
|
-
if (response.status
|
|
3126
|
+
if (response.status < 200 || response.status >= 300) {
|
|
2375
3127
|
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
2376
3128
|
}
|
|
2377
3129
|
|
|
2378
|
-
this.log('info', `Successfully
|
|
3130
|
+
this.log('info', `Successfully triggered import for website ${websiteUid}`);
|
|
2379
3131
|
return response.data;
|
|
2380
3132
|
|
|
2381
3133
|
} catch (error) {
|
|
2382
3134
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2383
|
-
this.log('error', `Failed to
|
|
3135
|
+
this.log('error', `Failed to trigger import: ${errorMessage}`);
|
|
2384
3136
|
|
|
2385
3137
|
if (axios.isAxiosError(error)) {
|
|
2386
3138
|
const responseData = error.response?.data;
|
|
@@ -2395,144 +3147,90 @@ class MCPServer {
|
|
|
2395
3147
|
}
|
|
2396
3148
|
}
|
|
2397
3149
|
|
|
2398
|
-
private async
|
|
2399
|
-
const { domain,
|
|
3150
|
+
private async handlePhpAppDeploy(params: Record<string, any>): Promise<any> {
|
|
3151
|
+
const { domain, archivePath, removeArchive = true } = params;
|
|
2400
3152
|
|
|
2401
|
-
this.
|
|
3153
|
+
this.agencyHosting_validateRequiredParams(params);
|
|
3154
|
+
this.agencyHosting_validateArchiveFile(archivePath);
|
|
2402
3155
|
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
const username = await this.resolveUsername(domain);
|
|
3156
|
+
this.log('info', `Resolving website UID from domain: ${domain}`);
|
|
3157
|
+
const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
|
|
2406
3158
|
|
|
2407
|
-
|
|
2408
|
-
const queryParams = this.hosting_listJsDeployments_buildQueryParams(params);
|
|
3159
|
+
this.log('info', `Starting archive upload for ${domain}`);
|
|
2409
3160
|
|
|
2410
|
-
|
|
2411
|
-
let deployments: any;
|
|
3161
|
+
let uploadCredentials: any;
|
|
2412
3162
|
try {
|
|
2413
|
-
this.
|
|
2414
|
-
deployments = await this.hosting_listJsDeployments_fetchDeployments(username, domain, queryParams);
|
|
3163
|
+
uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
|
|
2415
3164
|
} catch (error) {
|
|
2416
3165
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2417
|
-
|
|
2418
|
-
throw error;
|
|
2419
|
-
}
|
|
2420
|
-
|
|
2421
|
-
return {
|
|
2422
|
-
status: 'success',
|
|
2423
|
-
domain,
|
|
2424
|
-
username,
|
|
2425
|
-
queryParams: {
|
|
2426
|
-
page,
|
|
2427
|
-
perPage,
|
|
2428
|
-
states
|
|
2429
|
-
},
|
|
2430
|
-
deployments
|
|
2431
|
-
};
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
private hosting_showJsDeploymentLogs_validateRequiredParams(params: Record<string, any>): void {
|
|
2435
|
-
const { domain, buildUuid, fromLine } = params;
|
|
2436
|
-
|
|
2437
|
-
if (!domain || typeof domain !== 'string') {
|
|
2438
|
-
throw new Error('domain is required and must be a string');
|
|
3166
|
+
throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
|
|
2439
3167
|
}
|
|
2440
3168
|
|
|
2441
|
-
|
|
2442
|
-
throw new Error('buildUuid is required and must be a string');
|
|
2443
|
-
}
|
|
3169
|
+
const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
|
|
2444
3170
|
|
|
2445
|
-
if (
|
|
2446
|
-
throw new Error('
|
|
3171
|
+
if (!uploadUrl || !authToken || !authRestToken) {
|
|
3172
|
+
throw new Error('Invalid upload credentials received from API');
|
|
2447
3173
|
}
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
|
-
private hosting_showJsDeploymentLogs_buildQueryParams(params: Record<string, any>): string {
|
|
2451
|
-
const { fromLine } = params;
|
|
2452
|
-
const queryParams = new URLSearchParams();
|
|
2453
|
-
|
|
2454
|
-
const line = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
|
|
2455
|
-
queryParams.append('from_line', line.toString());
|
|
2456
|
-
|
|
2457
|
-
return queryParams.toString();
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
private async hosting_showJsDeploymentLogs_fetchLogs(username: string, domain: string, buildUuid: string, queryParams: string): Promise<any> {
|
|
2461
|
-
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
2462
|
-
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/${buildUuid}/logs`, baseUrl).toString();
|
|
2463
|
-
|
|
2464
|
-
const fullUrl = queryParams ? `${url}?${queryParams}` : url;
|
|
2465
3174
|
|
|
3175
|
+
const archiveBasename = path.basename(archivePath);
|
|
3176
|
+
let uploadResult: any;
|
|
2466
3177
|
try {
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
'Authorization': `Bearer ${bearerToken}`
|
|
2475
|
-
},
|
|
2476
|
-
timeout: 60000,
|
|
2477
|
-
validateStatus: function (status: number): boolean {
|
|
2478
|
-
return status < 500;
|
|
2479
|
-
}
|
|
2480
|
-
};
|
|
2481
|
-
|
|
2482
|
-
const response = await axios(config);
|
|
2483
|
-
|
|
2484
|
-
if (response.status !== 200) {
|
|
2485
|
-
throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
this.log('info', `Successfully fetched logs for ${domain} build ${buildUuid}`);
|
|
2489
|
-
return response.data;
|
|
3178
|
+
uploadResult = await this.agencyHosting_uploadFile(
|
|
3179
|
+
archivePath,
|
|
3180
|
+
`.h5g/${archiveBasename}`,
|
|
3181
|
+
uploadUrl,
|
|
3182
|
+
authRestToken,
|
|
3183
|
+
authToken
|
|
3184
|
+
);
|
|
2490
3185
|
|
|
3186
|
+
this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
|
|
2491
3187
|
} catch (error) {
|
|
2492
3188
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
if (axios.isAxiosError(error)) {
|
|
2496
|
-
const responseData = error.response?.data;
|
|
2497
|
-
const responseStatus = error.response?.status;
|
|
2498
|
-
this.log('error', 'API Error Details:', {
|
|
2499
|
-
status: responseStatus,
|
|
2500
|
-
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
|
|
2501
|
-
});
|
|
2502
|
-
}
|
|
2503
|
-
|
|
2504
|
-
throw error;
|
|
3189
|
+
throw new Error(`Failed to upload archive: ${errorMessage}`);
|
|
2505
3190
|
}
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
private async handleShowJsDeploymentLogs(params: Record<string, any>): Promise<any> {
|
|
2509
|
-
const { domain, buildUuid, fromLine } = params;
|
|
2510
|
-
|
|
2511
|
-
this.hosting_showJsDeploymentLogs_validateRequiredParams(params);
|
|
2512
|
-
|
|
2513
|
-
this.log('info', `Resolving username from domain: ${domain}`);
|
|
2514
|
-
const username = await this.resolveUsername(domain);
|
|
2515
3191
|
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
let logs: any;
|
|
3192
|
+
let deployResult: any;
|
|
2519
3193
|
try {
|
|
2520
|
-
this.log('info', `
|
|
2521
|
-
|
|
3194
|
+
this.log('info', `Triggering import for ${domain}`);
|
|
3195
|
+
deployResult = await this.agencyHosting_deployPhpApp_triggerImportArchive(websiteUid, archiveBasename);
|
|
2522
3196
|
} catch (error) {
|
|
2523
3197
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2524
|
-
this.log('error', `Failed to
|
|
2525
|
-
|
|
3198
|
+
this.log('error', `Failed to trigger import: ${errorMessage}`);
|
|
3199
|
+
const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
|
|
3200
|
+
|
|
3201
|
+
return {
|
|
3202
|
+
upload: {
|
|
3203
|
+
status: 'success',
|
|
3204
|
+
data: {
|
|
3205
|
+
filename: uploadResult.filename
|
|
3206
|
+
}
|
|
3207
|
+
},
|
|
3208
|
+
deploy: {
|
|
3209
|
+
status: 'error',
|
|
3210
|
+
error: errorMessage
|
|
3211
|
+
},
|
|
3212
|
+
removeArchive: {
|
|
3213
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
3214
|
+
}
|
|
3215
|
+
};
|
|
2526
3216
|
}
|
|
2527
3217
|
|
|
2528
|
-
const
|
|
3218
|
+
const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
|
|
2529
3219
|
|
|
2530
3220
|
return {
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
3221
|
+
upload: {
|
|
3222
|
+
status: 'success',
|
|
3223
|
+
data: {
|
|
3224
|
+
filename: uploadResult.filename
|
|
3225
|
+
}
|
|
3226
|
+
},
|
|
3227
|
+
deploy: {
|
|
3228
|
+
status: 'success',
|
|
3229
|
+
data: deployResult
|
|
3230
|
+
},
|
|
3231
|
+
removeArchive: {
|
|
3232
|
+
status: archiveRemoved ? 'success' : 'skipped'
|
|
3233
|
+
}
|
|
2536
3234
|
};
|
|
2537
3235
|
}
|
|
2538
3236
|
|