superjolt
Version:
AI-powered deployment platform with MCP support - Deploy JavaScript apps using natural language with Claude Desktop
550 lines ⢠28 kB
JavaScript
;
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