UNPKG

superjolt

Version:

AI-powered deployment platform with MCP support - Deploy JavaScript apps using natural language with Claude Desktop

550 lines • 28 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DeployCommand = void 0; const nest_commander_1 = require("nest-commander"); const authenticated_command_1 = require("./authenticated.command"); const common_1 = require("@nestjs/common"); const archiver_1 = __importDefault(require("archiver")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const fs_1 = require("fs"); const axios_1 = require("@nestjs/axios"); const config_service_1 = require("../services/config.service"); const auth_service_1 = require("../services/auth.service"); const rxjs_1 = require("rxjs"); const form_data_1 = __importDefault(require("form-data")); const project_1 = require("../utils/project"); const ignore_1 = require("../utils/ignore"); const chalk_1 = __importDefault(require("chalk")); const logger_service_1 = require("../services/logger.service"); let DeployCommand = class DeployCommand extends authenticated_command_1.AuthenticatedCommand { httpService; configService; authService; logger; constructor(httpService, configService, authService, logger) { super(); this.httpService = httpService; this.configService = configService; this.authService = authService; this.logger = logger; } async execute(passedParams, options) { const machineId = options.machine || passedParams[0]; let serviceId = options.service; const projectRoot = (0, project_1.findProjectRoot)(); let serviceIdFromConfig = false; if (!serviceId && projectRoot) { const config = (0, project_1.readSuperjoltConfig)(projectRoot); if (config?.serviceId) { serviceId = config.serviceId; serviceIdFromConfig = true; this.logger.log(`${chalk_1.default.dim('Using service ID from .superjolt file:')} ${chalk_1.default.cyan(serviceId)}`); } } let serviceName = options.name; if (!serviceName && !serviceId && projectRoot) { const packageJson = (0, project_1.readPackageJson)(projectRoot); if (packageJson?.name) { serviceName = packageJson.name; this.logger.log(`${chalk_1.default.dim('Using service name from package.json:')} ${chalk_1.default.cyan(serviceName)}`); } } try { let deployPath = options.path || projectRoot || process.cwd(); const resolvedDeployPath = path.resolve(deployPath); const currentDir = process.cwd(); if (!resolvedDeployPath.startsWith(currentDir)) { if (deployPath === projectRoot && projectRoot) { this.logger.warn(chalk_1.default.yellow('\nāš ļø Warning: Deploying from parent directory:'), chalk_1.default.cyan(resolvedDeployPath)); this.logger.warn(chalk_1.default.yellow(' Use -p option to explicitly specify a different path\n')); } else { this.logger.error(chalk_1.default.red('Error: Path must be within the current directory or its subdirectories')); this.logger.error(chalk_1.default.red(` Current directory: ${currentDir}`)); this.logger.error(chalk_1.default.red(` Requested path: ${resolvedDeployPath}`)); process.exit(1); } } deployPath = resolvedDeployPath; if (!fs.existsSync(deployPath)) { this.logger.error(`Path does not exist: ${deployPath}`); process.exit(1); } this.logger.log(`\nšŸ“¦ Preparing deployment from: ${chalk_1.default.cyan(deployPath)}`); const customIgnore = (0, ignore_1.readSuperjoltIgnore)(projectRoot || deployPath); if (customIgnore) { this.logger.log(` ${chalk_1.default.dim('Using ignore patterns from:')} ${chalk_1.default.cyan('.superjoltignore')}`); if (options.verbose) { this.logger.log(` ${chalk_1.default.dim('Custom patterns:')} ${customIgnore.patterns.join(', ')}`); } } const ignorePatterns = (0, ignore_1.combineIgnorePatterns)(customIgnore?.patterns || []); const tempZipPath = `/tmp/deploy-${Date.now()}.zip`; const output = fs.createWriteStream(tempZipPath); const archive = (0, archiver_1.default)('zip', { zlib: { level: 9 }, }); archive.on('error', (err) => { throw err; }); archive.pipe(output); archive.glob('**/*', { cwd: deployPath, ignore: ignorePatterns, dot: true, }); let fileCount = 0; const files = []; archive.on('entry', (entry) => { fileCount++; files.push(entry.name); }); await archive.finalize(); await new Promise((resolve, reject) => { output.on('close', resolve); output.on('error', reject); }); const stats = fs.statSync(tempZipPath); const sizeInMB = (stats.size / 1024 / 1024).toFixed(2); this.logger.log(` ${chalk_1.default.green('āœ“')} Created ${sizeInMB} MB archive (${fileCount} files)`); if (options.verbose && fileCount < 20) { this.logger.log(chalk_1.default.gray(' Files:', files.join(', '))); } const form = new form_data_1.default(); form.append('file', (0, fs_1.createReadStream)(tempZipPath), { filename: 'deploy.zip', contentType: 'application/zip', }); this.logger.log(`\nšŸš€ Deploying to Superjolt...`); const apiUrl = this.configService.getApiUrl(); await this.deployWithStreaming(apiUrl, machineId, form, tempZipPath, serviceId, projectRoot, serviceIdFromConfig, serviceName, options.verbose); } catch (error) { try { const tempFiles = fs .readdirSync('/tmp') .filter((f) => f.startsWith('deploy-') && f.endsWith('.zip')); tempFiles.forEach((f) => fs.unlinkSync(path.join('/tmp', f))); } catch { } if (error instanceof Error && 'response' in error && error.response) { const response = error.response; this.logger.error(`\n${chalk_1.default.red('āŒ Deployment failed:')} ${response.data?.message || response.statusText}`); } else if (error && typeof error === 'object' && 'request' in error && error.request) { this.logger.error(`\n${chalk_1.default.red('āŒ Network Error:')} Unable to connect to the API`); this.logger.error(chalk_1.default.dim(' Please check your internet connection')); } else { this.logger.error(`\n${chalk_1.default.red('āŒ Error:')} ${error instanceof Error ? error.message : String(error)}`); } process.exit(1); } } parsePath(val) { const resolvedPath = path.resolve(val); const currentDir = process.cwd(); if (!resolvedPath.startsWith(currentDir)) { throw new Error('Path must be within the current directory or its subdirectories'); } return resolvedPath; } parseService(val) { return val; } parseMachine(val) { return val; } parseName(val) { return val; } parseVerbose() { return true; } async deployWithStreaming(apiUrl, machineId, form, tempZipPath, serviceId, projectRoot, serviceIdFromConfig, serviceName, verbose) { const { EventSource } = await Promise.resolve().then(() => __importStar(require('eventsource'))); try { let token = await this.authService.getToken(); if (!token) { await this.authService.performOAuthFlow(); token = await this.authService.getToken(); if (!token) { throw new Error('Authentication failed'); } } let deployUrl = `${apiUrl}/service/deploy`; const params = new URLSearchParams(); if (machineId) params.append('machineId', machineId); if (serviceId) params.append('serviceId', serviceId); if (serviceIdFromConfig) params.append('serviceIdFromConfig', 'true'); if (serviceName) params.append('name', serviceName); deployUrl += `?${params.toString()}`; const response = await (0, rxjs_1.firstValueFrom)(this.httpService.post(deployUrl, form, { headers: { ...form.getHeaders(), Authorization: `Bearer ${token}`, }, maxContentLength: Infinity, maxBodyLength: Infinity, })); const responseData = response.data; if (responseData.needsSelection) { this.logger.log(`\n${chalk_1.default.yellow('šŸ–„ļø Multiple machines available')}`); this.logger.log(chalk_1.default.dim('Please select a machine to deploy to:\n')); const machines = responseData.availableMachines || []; machines.forEach((machine, index) => { const status = machine.status === 'running' ? chalk_1.default.green('ā—') : chalk_1.default.red('ā—‹'); const number = chalk_1.default.cyan(`${index + 1}.`); this.logger.log(` ${number} ${status} ${chalk_1.default.bold(machine.id)} ${chalk_1.default.dim(`(${machine.name})`)}`); }); const readline = await Promise.resolve().then(() => __importStar(require('readline'))); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const selection = await new Promise((resolve) => { rl.question('\nSelect a machine (enter number): ', (answer) => { rl.close(); resolve(parseInt(answer)); }); }); if (selection < 1 || selection > machines.length) { this.logger.error('Invalid selection'); process.exit(1); } const selectedMachine = machines[selection - 1]; this.logger.log(`\n${chalk_1.default.green('āœ“')} Selected machine: ${chalk_1.default.cyan(selectedMachine.id)}`); await this.deployWithStreaming(apiUrl, selectedMachine.id, form, tempZipPath, serviceId, projectRoot, serviceIdFromConfig, serviceName, verbose); return; } const deployResponse = response.data; const { streamId, serviceId: deployedServiceId, machineId: responseMachineId, message, url: serviceUrl, } = deployResponse; if (serviceName) { this.logger.log(` ${chalk_1.default.dim('Service:')} ${chalk_1.default.cyan(serviceName)}`); } if (deployedServiceId || serviceId) { const idToShow = deployedServiceId || serviceId; this.logger.log(` ${chalk_1.default.dim('Service ID:')} ${chalk_1.default.cyan(idToShow)}`); } else if (responseMachineId) { this.logger.log(` ${chalk_1.default.dim('Machine:')} ${chalk_1.default.cyan(responseMachineId)}`); } if (message && message.includes('created')) { this.logger.log(` ${chalk_1.default.green('āœ“')} ${message}`); } const streamUrl = `${apiUrl}/service/${responseMachineId}/deploy/stream/${streamId}?token=${encodeURIComponent(token)}`; const eventSource = new EventSource(streamUrl); return new Promise((resolve, reject) => { let isCompleted = false; let hasConnected = false; let buildOutputStarted = false; let currentStage = ''; let spinnerInterval = null; let spinnerIndex = 0; const spinnerFrames = [ 'ā ‹', 'ā ™', 'ā ¹', 'ā ø', 'ā ¼', 'ā “', 'ā ¦', 'ā §', 'ā ‡', 'ā ', ]; const stageIcons = { connected: 'šŸ”—', extracting: 'šŸ“¦', uploading: 'ā˜ļø ', building: 'šŸ”Ø', starting: 'šŸƒ', 'capturing-logs': 'šŸ“', complete: 'āœ…', }; eventSource.onmessage = (event) => { try { const progress = JSON.parse(event.data); switch (progress.type) { case 'status': if (progress.stage === 'connected') { hasConnected = true; this.logger.log(`\n${stageIcons['connected'] || ''} Connected to deployment service`); } else if (progress.stage && progress.stage !== currentStage) { if (buildOutputStarted && !verbose && currentStage === 'building') { if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; } process.stdout.write(`\b${chalk_1.default.green('āœ“')}\n`); buildOutputStarted = false; } currentStage = progress.stage; const icon = stageIcons[progress.stage] || 'šŸ”ø'; const stageName = progress.stage.charAt(0).toUpperCase() + progress.stage.slice(1).replace(/-/g, ' '); this.logger.log(`\n${icon} ${stageName}...`); } break; case 'log-stream': if (progress.data?.buildLog && verbose) { if (!buildOutputStarted) { this.logger.log('\n' + chalk_1.default.gray('Build output:')); this.logger.log(chalk_1.default.gray('─'.repeat(80))); buildOutputStarted = true; } process.stdout.write(progress.data.buildLog); } else if (progress.data?.buildLog && !verbose && !buildOutputStarted) { buildOutputStarted = true; process.stdout.write(` ${chalk_1.default.dim('Building application')} ${chalk_1.default.blue(spinnerFrames[0])}`); spinnerInterval = setInterval(() => { spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length; process.stdout.write(`\b${chalk_1.default.blue(spinnerFrames[spinnerIndex])}`); }, 80); } break; case 'log': if (buildOutputStarted && !verbose) { if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; } process.stdout.write(`\b${chalk_1.default.green('āœ“')}\n`); } if (verbose) { if (progress.data?.buildLog && !buildOutputStarted) { this.logger.log('\n' + chalk_1.default.gray('Build output:')); this.logger.log(chalk_1.default.gray('─'.repeat(80))); this.logger.log(progress.data.buildLog); } if (progress.data?.startupLog) { this.logger.log('\n' + chalk_1.default.gray('Startup logs:')); this.logger.log(chalk_1.default.gray('─'.repeat(80))); this.logger.log(progress.data.startupLog); } if (progress.data?.output) { this.logger.log('\n' + chalk_1.default.gray('Output:')); this.logger.log(chalk_1.default.gray('─'.repeat(80))); this.logger.log(progress.data.output); } } else { if (progress.data?.startupLog && progress.data.startupLog.toLowerCase().includes('error')) { this.logger.log('\n' + chalk_1.default.yellow('āš ļø Startup warnings detected. Run with --verbose to see details.')); } } buildOutputStarted = false; break; case 'complete': if (buildOutputStarted && !verbose) { if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; } process.stdout.write(`\b${chalk_1.default.green('āœ“')}\n`); } this.logger.log(`\n${chalk_1.default.green('āœ… Deployment completed successfully!')}`); if (deployedServiceId) { try { (0, project_1.writeSuperjoltConfig)({ serviceId: deployedServiceId }, projectRoot || undefined); this.logger.log(` ${chalk_1.default.green('āœ“')} Saved service ID to .superjolt file`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.warn(chalk_1.default.yellow('āš ļø Could not save .superjolt file:', errorMessage)); } } if (serviceUrl) { this.logger.log('\n' + chalk_1.default.cyan('🌐 Your app is now available at:')); this.logger.log(' ' + chalk_1.default.bold.underline(serviceUrl)); this.logger.log(); } isCompleted = true; eventSource.close(); fs.unlinkSync(tempZipPath); resolve(); break; case 'error': this.logger.error('\n' + chalk_1.default.red(`āŒ Deployment failed: ${progress.message}`)); if (progress.data?.error) { this.logger.error(chalk_1.default.red(progress.data.error)); } eventSource.close(); fs.unlinkSync(tempZipPath); reject(new Error(progress.message)); break; default: this.logger.log(chalk_1.default.gray(`[${progress.type}] ${progress.message}`)); } } catch { this.logger.error('Failed to parse event:', event.data); } }; eventSource.onerror = (error) => { if (isCompleted) { return; } if (hasConnected && !isCompleted) { this.logger.log(`\n${chalk_1.default.yellow('āš ļø Lost connection to deployment stream')}`); this.logger.log(chalk_1.default.dim(' The deployment may have completed successfully')); this.logger.log(chalk_1.default.dim(` Run ${chalk_1.default.cyan(`superjolt status`)} to check the service status`)); eventSource.close(); try { fs.unlinkSync(tempZipPath); } catch { } resolve(); return; } this.logger.error(`\n${chalk_1.default.red('āŒ Stream connection error')}`); if (verbose) { this.logger.error(chalk_1.default.dim('Stream URL:'), streamUrl); if (error) { this.logger.error(chalk_1.default.dim('Error details:'), error); } } else { this.logger.error(chalk_1.default.dim(' Run with --verbose to see connection details')); } eventSource.close(); try { fs.unlinkSync(tempZipPath); } catch { } reject(new Error('Stream connection failed')); }; }); } catch (error) { try { fs.unlinkSync(tempZipPath); } catch { } throw error; } } }; exports.DeployCommand = DeployCommand; __decorate([ (0, nest_commander_1.Option)({ flags: '-p, --path <path>', description: 'Path to the application directory (defaults to current directory)', }), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", String) ], DeployCommand.prototype, "parsePath", null); __decorate([ (0, nest_commander_1.Option)({ flags: '-s, --service <serviceId>', description: 'Deploy to existing service (optional)', }), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", String) ], DeployCommand.prototype, "parseService", null); __decorate([ (0, nest_commander_1.Option)({ flags: '-m, --machine <machineId>', description: 'Machine ID to deploy to', }), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", String) ], DeployCommand.prototype, "parseMachine", null); __decorate([ (0, nest_commander_1.Option)({ flags: '-n, --name <name>', description: 'Service name (defaults to package.json name for new services)', }), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", String) ], DeployCommand.prototype, "parseName", null); __decorate([ (0, nest_commander_1.Option)({ flags: '-v, --verbose', description: 'Show detailed build output and logs', }), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Boolean) ], DeployCommand.prototype, "parseVerbose", null); exports.DeployCommand = DeployCommand = __decorate([ (0, common_1.Injectable)(), (0, nest_commander_1.Command)({ name: 'deploy', description: 'Deploy a Node.js application to a machine or service', }), __metadata("design:paramtypes", [axios_1.HttpService, config_service_1.ConfigService, auth_service_1.AuthService, logger_service_1.LoggerService]) ], DeployCommand); //# sourceMappingURL=deploy.command.js.map