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