@hostinger/mcp 3.1.0 → 3.2.1

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