hostinger-api-mcp 0.1.37 → 0.1.40

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