hostfn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1136 -0
- package/_conduct/specs/1.v0.spec.md +1041 -0
- package/examples/express-api/package.json +22 -0
- package/examples/express-api/src/index.ts +16 -0
- package/examples/express-api/tsconfig.json +11 -0
- package/examples/github-actions-deploy.yml +40 -0
- package/examples/monorepo-config.json +76 -0
- package/examples/monorepo-multi-server-config.json +74 -0
- package/package.json +39 -0
- package/packages/cli/package.json +40 -0
- package/packages/cli/src/__tests__/core/backup.test.ts +137 -0
- package/packages/cli/src/__tests__/core/health.test.ts +125 -0
- package/packages/cli/src/__tests__/core/lock.test.ts +173 -0
- package/packages/cli/src/__tests__/core/nginx-multi-domain.test.ts +176 -0
- package/packages/cli/src/__tests__/runtimes/pm2.test.ts +130 -0
- package/packages/cli/src/__tests__/utils/validation.test.ts +164 -0
- package/packages/cli/src/commands/deploy.ts +817 -0
- package/packages/cli/src/commands/env.ts +391 -0
- package/packages/cli/src/commands/expose.ts +438 -0
- package/packages/cli/src/commands/init.ts +192 -0
- package/packages/cli/src/commands/logs.ts +106 -0
- package/packages/cli/src/commands/rollback.ts +142 -0
- package/packages/cli/src/commands/server/info.ts +131 -0
- package/packages/cli/src/commands/server/setup.ts +200 -0
- package/packages/cli/src/commands/status.ts +149 -0
- package/packages/cli/src/config/loader.ts +66 -0
- package/packages/cli/src/config/schema.ts +140 -0
- package/packages/cli/src/core/backup.ts +128 -0
- package/packages/cli/src/core/health.ts +116 -0
- package/packages/cli/src/core/local.ts +67 -0
- package/packages/cli/src/core/lock.ts +108 -0
- package/packages/cli/src/core/nginx.ts +170 -0
- package/packages/cli/src/core/ssh.ts +335 -0
- package/packages/cli/src/core/sync.ts +138 -0
- package/packages/cli/src/core/workspace.ts +180 -0
- package/packages/cli/src/index.ts +240 -0
- package/packages/cli/src/runtimes/base.ts +144 -0
- package/packages/cli/src/runtimes/nodejs/detector.ts +157 -0
- package/packages/cli/src/runtimes/nodejs/index.ts +228 -0
- package/packages/cli/src/runtimes/nodejs/pm2.ts +71 -0
- package/packages/cli/src/runtimes/registry.ts +76 -0
- package/packages/cli/src/utils/logger.ts +86 -0
- package/packages/cli/src/utils/validation.ts +147 -0
- package/packages/cli/tsconfig.json +25 -0
- package/packages/cli/vitest.config.ts +19 -0
- package/turbo.json +24 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { Logger } from '../utils/logger.js';
|
|
5
|
+
import { ConfigLoader } from '../config/loader.js';
|
|
6
|
+
import { createSSHConnection } from '../core/ssh.js';
|
|
7
|
+
import { RuntimeRegistry } from '../runtimes/registry.js';
|
|
8
|
+
import { Runtime } from '../config/schema.js';
|
|
9
|
+
|
|
10
|
+
interface LogsOptions {
|
|
11
|
+
lines: string;
|
|
12
|
+
errors: boolean;
|
|
13
|
+
follow?: boolean;
|
|
14
|
+
output?: string;
|
|
15
|
+
service?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function logsCommand(
|
|
19
|
+
environment: string,
|
|
20
|
+
options: LogsOptions
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
Logger.header(`Logs - ${environment}`);
|
|
23
|
+
|
|
24
|
+
// Load configuration
|
|
25
|
+
const config = ConfigLoader.load();
|
|
26
|
+
const envConfig = config.environments[environment];
|
|
27
|
+
|
|
28
|
+
if (!envConfig) {
|
|
29
|
+
throw new Error(`Environment '${environment}' not found`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Handle multi-service logs
|
|
33
|
+
if (config.services && Object.keys(config.services).length > 0) {
|
|
34
|
+
if (!options.service) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'Multi-service deployment detected. Please specify --service <name>\n' +
|
|
37
|
+
`Available services: ${Object.keys(config.services).join(', ')}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!config.services[options.service]) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Service '${options.service}' not found\n` +
|
|
44
|
+
`Available: ${Object.keys(config.services).join(', ')}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const serviceConfig = config.services[options.service];
|
|
49
|
+
const serviceServer = serviceConfig.server || envConfig.server;
|
|
50
|
+
const serviceName = `${config.name}-${options.service}-${environment}`;
|
|
51
|
+
await fetchLogs(serviceName, serviceServer, config.runtime, options);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const serviceName = `${config.name}-${environment}`;
|
|
56
|
+
await fetchLogs(serviceName, envConfig.server, config.runtime, options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchLogs(
|
|
60
|
+
serviceName: string,
|
|
61
|
+
server: string,
|
|
62
|
+
runtime: Runtime,
|
|
63
|
+
options: LogsOptions
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const adapter = RuntimeRegistry.get(runtime);
|
|
66
|
+
const pm2 = adapter.getProcessManager();
|
|
67
|
+
|
|
68
|
+
const spinner = ora('Connecting to server...').start();
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const ssh = await createSSHConnection(server);
|
|
72
|
+
spinner.succeed('Connected');
|
|
73
|
+
|
|
74
|
+
Logger.br();
|
|
75
|
+
|
|
76
|
+
// Generate logs command
|
|
77
|
+
let logsCmd = pm2.generateLogsCommand(serviceName, parseInt(options.lines));
|
|
78
|
+
|
|
79
|
+
// Add error filtering if requested
|
|
80
|
+
if (options.errors) {
|
|
81
|
+
logsCmd = `${logsCmd} 2>&1 | grep -E "(error|Error|ERROR|fail|Fail|FAIL|exception|Exception)"`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Execute logs command
|
|
85
|
+
const result = await ssh.exec(logsCmd, { streaming: !options.output });
|
|
86
|
+
|
|
87
|
+
if (result.exitCode !== 0) {
|
|
88
|
+
Logger.error('Failed to fetch logs');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Save to file if requested
|
|
92
|
+
if (options.output) {
|
|
93
|
+
const outputPath = path.resolve(process.cwd(), options.output);
|
|
94
|
+
fs.writeFileSync(outputPath, result.stdout);
|
|
95
|
+
Logger.success(`Logs saved to: ${outputPath}`);
|
|
96
|
+
Logger.kv('Lines', result.stdout.split('\n').length.toString());
|
|
97
|
+
Logger.kv('Size', `${(result.stdout.length / 1024).toFixed(2)} KB`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ssh.disconnect();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
spinner.fail('Failed to get logs');
|
|
103
|
+
Logger.error(error instanceof Error ? error.message : String(error));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { Logger } from '../utils/logger.js';
|
|
4
|
+
import { ConfigLoader } from '../config/loader.js';
|
|
5
|
+
import { createSSHConnection } from '../core/ssh.js';
|
|
6
|
+
import { BackupManager } from '../core/backup.js';
|
|
7
|
+
import { RuntimeRegistry } from '../runtimes/registry.js';
|
|
8
|
+
|
|
9
|
+
interface RollbackOptions {
|
|
10
|
+
to?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function rollbackCommand(
|
|
14
|
+
environment: string,
|
|
15
|
+
options: RollbackOptions
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
Logger.header(`Rollback - ${environment}`);
|
|
18
|
+
|
|
19
|
+
// Load configuration
|
|
20
|
+
const config = ConfigLoader.load();
|
|
21
|
+
const envConfig = config.environments[environment];
|
|
22
|
+
|
|
23
|
+
if (!envConfig) {
|
|
24
|
+
throw new Error(`Environment '${environment}' not found`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const remoteDir = `/var/www/${config.name}-${environment}`;
|
|
28
|
+
const serviceName = `${config.name}-${environment}`;
|
|
29
|
+
|
|
30
|
+
const spinner = ora('Connecting to server...').start();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const ssh = await createSSHConnection(envConfig.server);
|
|
34
|
+
spinner.succeed('Connected');
|
|
35
|
+
|
|
36
|
+
Logger.br();
|
|
37
|
+
|
|
38
|
+
const backupManager = new BackupManager(ssh, remoteDir);
|
|
39
|
+
|
|
40
|
+
// List available backups
|
|
41
|
+
const listSpinner = ora('Fetching available backups...').start();
|
|
42
|
+
const backups = await backupManager.list();
|
|
43
|
+
listSpinner.succeed(`Found ${backups.length} backup(s)`);
|
|
44
|
+
|
|
45
|
+
if (backups.length === 0) {
|
|
46
|
+
Logger.warn('No backups available');
|
|
47
|
+
ssh.disconnect();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Logger.br();
|
|
52
|
+
|
|
53
|
+
let backupToRestore: string;
|
|
54
|
+
|
|
55
|
+
if (options.to) {
|
|
56
|
+
// Use specified backup
|
|
57
|
+
if (!backups.includes(options.to)) {
|
|
58
|
+
throw new Error(`Backup not found: ${options.to}`);
|
|
59
|
+
}
|
|
60
|
+
backupToRestore = options.to;
|
|
61
|
+
} else {
|
|
62
|
+
// Interactive selection
|
|
63
|
+
Logger.section('Available Backups');
|
|
64
|
+
backups.slice(0, 10).forEach((backup, idx) => {
|
|
65
|
+
Logger.log(` ${idx + 1}. ${backup}`);
|
|
66
|
+
});
|
|
67
|
+
Logger.br();
|
|
68
|
+
|
|
69
|
+
const { selectedBackup } = await inquirer.prompt([
|
|
70
|
+
{
|
|
71
|
+
type: 'list',
|
|
72
|
+
name: 'selectedBackup',
|
|
73
|
+
message: 'Select backup to restore:',
|
|
74
|
+
choices: backups.slice(0, 10).map((b, idx) => ({
|
|
75
|
+
name: `${idx + 1}. ${b}${idx === 0 ? ' (latest)' : ''}`,
|
|
76
|
+
value: b,
|
|
77
|
+
})),
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
backupToRestore = selectedBackup;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Confirm rollback
|
|
85
|
+
Logger.br();
|
|
86
|
+
Logger.warn(`This will rollback to: ${backupToRestore}`);
|
|
87
|
+
|
|
88
|
+
const { confirm } = await inquirer.prompt([
|
|
89
|
+
{
|
|
90
|
+
type: 'confirm',
|
|
91
|
+
name: 'confirm',
|
|
92
|
+
message: 'Are you sure you want to rollback?',
|
|
93
|
+
default: false,
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
if (!confirm) {
|
|
98
|
+
Logger.info('Rollback cancelled');
|
|
99
|
+
ssh.disconnect();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
Logger.br();
|
|
104
|
+
|
|
105
|
+
// Perform rollback
|
|
106
|
+
const rollbackSpinner = ora('Restoring backup...').start();
|
|
107
|
+
await backupManager.restore(backupToRestore);
|
|
108
|
+
rollbackSpinner.succeed('Backup restored');
|
|
109
|
+
|
|
110
|
+
// Reload PM2
|
|
111
|
+
const adapter = RuntimeRegistry.get(config.runtime);
|
|
112
|
+
const pm2 = adapter.getProcessManager();
|
|
113
|
+
|
|
114
|
+
const reloadSpinner = ora('Reloading service...').start();
|
|
115
|
+
const reloadResult = await ssh.exec(
|
|
116
|
+
pm2.generateReloadCommand(serviceName),
|
|
117
|
+
{ cwd: remoteDir }
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (reloadResult.exitCode !== 0) {
|
|
121
|
+
reloadSpinner.fail('Service reload failed');
|
|
122
|
+
throw new Error(`PM2 reload failed: ${reloadResult.stderr}`);
|
|
123
|
+
}
|
|
124
|
+
reloadSpinner.succeed('Service reloaded');
|
|
125
|
+
|
|
126
|
+
Logger.br();
|
|
127
|
+
Logger.success('Rollback completed successfully!');
|
|
128
|
+
Logger.br();
|
|
129
|
+
Logger.kv('Restored backup', backupToRestore);
|
|
130
|
+
Logger.kv('Service', serviceName);
|
|
131
|
+
Logger.br();
|
|
132
|
+
Logger.info('Check status:');
|
|
133
|
+
Logger.command(`hostfn status ${environment}`);
|
|
134
|
+
Logger.br();
|
|
135
|
+
|
|
136
|
+
ssh.disconnect();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
spinner.fail('Rollback failed');
|
|
139
|
+
Logger.error(error instanceof Error ? error.message : String(error));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Logger } from '../../utils/logger.js';
|
|
4
|
+
import { createSSHConnection } from '../../core/ssh.js';
|
|
5
|
+
|
|
6
|
+
export async function serverInfoCommand(host: string): Promise<void> {
|
|
7
|
+
Logger.header('Server Information');
|
|
8
|
+
|
|
9
|
+
Logger.kv('Host', host);
|
|
10
|
+
Logger.br();
|
|
11
|
+
|
|
12
|
+
const spinner = ora('Connecting to server...').start();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const ssh = await createSSHConnection(host);
|
|
16
|
+
spinner.succeed('Connected');
|
|
17
|
+
|
|
18
|
+
Logger.br();
|
|
19
|
+
|
|
20
|
+
// Get system information
|
|
21
|
+
const infoSpinner = ora('Gathering system information...').start();
|
|
22
|
+
|
|
23
|
+
const [
|
|
24
|
+
osInfo,
|
|
25
|
+
nodeVersion,
|
|
26
|
+
pm2Version,
|
|
27
|
+
nginxStatus,
|
|
28
|
+
diskSpace,
|
|
29
|
+
memoryInfo,
|
|
30
|
+
pm2List,
|
|
31
|
+
] = await Promise.all([
|
|
32
|
+
ssh.exec('cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d \'"\''),
|
|
33
|
+
ssh.exec('source ~/.nvm/nvm.sh && node --version 2>/dev/null || echo "not installed"'),
|
|
34
|
+
ssh.exec('pm2 --version 2>/dev/null || echo "not installed"'),
|
|
35
|
+
ssh.exec('systemctl is-active nginx 2>/dev/null || echo "not running"'),
|
|
36
|
+
ssh.exec('df -h / | tail -1'),
|
|
37
|
+
ssh.exec('free -h'),
|
|
38
|
+
ssh.exec('pm2 jlist 2>/dev/null || echo "[]"'),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
infoSpinner.succeed('Information gathered');
|
|
42
|
+
|
|
43
|
+
Logger.br();
|
|
44
|
+
|
|
45
|
+
// Display OS information
|
|
46
|
+
Logger.section('System');
|
|
47
|
+
Logger.kv('OS', osInfo.stdout.trim() || 'Unknown');
|
|
48
|
+
|
|
49
|
+
// Display disk space
|
|
50
|
+
const diskParts = diskSpace.stdout.trim().split(/\s+/);
|
|
51
|
+
if (diskParts.length >= 5) {
|
|
52
|
+
Logger.kv('Disk', `${diskParts[2]} used / ${diskParts[1]} total (${diskParts[4]} used)`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Display memory
|
|
56
|
+
const memLines = memoryInfo.stdout.trim().split('\n');
|
|
57
|
+
if (memLines.length >= 2) {
|
|
58
|
+
const memParts = memLines[1].split(/\s+/);
|
|
59
|
+
if (memParts.length >= 3) {
|
|
60
|
+
Logger.kv('Memory', `${memParts[2]} used / ${memParts[1]} total`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Logger.br();
|
|
65
|
+
|
|
66
|
+
// Display runtime information
|
|
67
|
+
Logger.section('Runtime');
|
|
68
|
+
const node = nodeVersion.stdout.trim();
|
|
69
|
+
Logger.kv('Node.js', node === 'not installed' ? chalk.red(node) : chalk.green(node));
|
|
70
|
+
|
|
71
|
+
const pm2 = pm2Version.stdout.trim();
|
|
72
|
+
Logger.kv('PM2', pm2 === 'not installed' ? chalk.red(pm2) : chalk.green(pm2));
|
|
73
|
+
|
|
74
|
+
const nginx = nginxStatus.stdout.trim();
|
|
75
|
+
Logger.kv('Nginx', nginx === 'active' ? chalk.green(nginx) : chalk.yellow(nginx));
|
|
76
|
+
|
|
77
|
+
Logger.br();
|
|
78
|
+
|
|
79
|
+
// Display running services
|
|
80
|
+
Logger.section('Services');
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const services = JSON.parse(pm2List.stdout || '[]');
|
|
84
|
+
|
|
85
|
+
if (services.length === 0) {
|
|
86
|
+
Logger.info('No PM2 services running');
|
|
87
|
+
} else {
|
|
88
|
+
for (const service of services) {
|
|
89
|
+
const status = service.pm2_env?.status || 'unknown';
|
|
90
|
+
const memory = service.monit?.memory
|
|
91
|
+
? `${Math.round(service.monit.memory / 1024 / 1024)}MB`
|
|
92
|
+
: 'N/A';
|
|
93
|
+
const cpu = service.monit?.cpu !== undefined
|
|
94
|
+
? `${service.monit.cpu}%`
|
|
95
|
+
: 'N/A';
|
|
96
|
+
const uptime = service.pm2_env?.pm_uptime
|
|
97
|
+
? formatUptime(Date.now() - service.pm2_env.pm_uptime)
|
|
98
|
+
: 'N/A';
|
|
99
|
+
|
|
100
|
+
const statusColor = status === 'online' ? chalk.green : chalk.red;
|
|
101
|
+
|
|
102
|
+
Logger.log(` ${chalk.bold(service.name)}`);
|
|
103
|
+
Logger.log(` Status: ${statusColor(status)}`);
|
|
104
|
+
Logger.log(` Memory: ${memory} CPU: ${cpu} Uptime: ${uptime}`);
|
|
105
|
+
Logger.br();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
Logger.warn('Could not parse PM2 service list');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ssh.disconnect();
|
|
113
|
+
|
|
114
|
+
} catch (error) {
|
|
115
|
+
spinner.fail('Failed to get server information');
|
|
116
|
+
Logger.error(error instanceof Error ? error.message : String(error));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatUptime(ms: number): string {
|
|
122
|
+
const seconds = Math.floor(ms / 1000);
|
|
123
|
+
const minutes = Math.floor(seconds / 60);
|
|
124
|
+
const hours = Math.floor(minutes / 60);
|
|
125
|
+
const days = Math.floor(hours / 24);
|
|
126
|
+
|
|
127
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
128
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
129
|
+
if (minutes > 0) return `${minutes}m`;
|
|
130
|
+
return `${seconds}s`;
|
|
131
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { writeFileSync } from 'fs';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import { Logger } from '../../utils/logger.js';
|
|
7
|
+
import { ConfigLoader } from '../../config/loader.js';
|
|
8
|
+
import { RuntimeRegistry } from '../../runtimes/registry.js';
|
|
9
|
+
import { createSSHConnection } from '../../core/ssh.js';
|
|
10
|
+
import { validateSSHConnection, validatePort, validateNodeVersion } from '../../utils/validation.js';
|
|
11
|
+
|
|
12
|
+
interface SetupOptions {
|
|
13
|
+
env: string;
|
|
14
|
+
nodeVersion: string;
|
|
15
|
+
port: string;
|
|
16
|
+
redis: boolean;
|
|
17
|
+
password?: string;
|
|
18
|
+
execute?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function serverSetupCommand(
|
|
22
|
+
host: string,
|
|
23
|
+
options: SetupOptions
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
Logger.header('Server Setup');
|
|
26
|
+
|
|
27
|
+
// Validate inputs
|
|
28
|
+
if (!validateSSHConnection(host)) {
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!validatePort(parseInt(options.port))) {
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!validateNodeVersion(options.nodeVersion)) {
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Logger.kv('Host', host);
|
|
41
|
+
Logger.kv('Environment', options.env);
|
|
42
|
+
Logger.kv('Port', options.port);
|
|
43
|
+
Logger.kv('Redis', options.redis ? 'Yes' : 'No');
|
|
44
|
+
Logger.br();
|
|
45
|
+
|
|
46
|
+
// Load config to detect runtime (or use default Node.js)
|
|
47
|
+
let runtime = 'nodejs';
|
|
48
|
+
try {
|
|
49
|
+
if (ConfigLoader.exists()) {
|
|
50
|
+
const config = ConfigLoader.load();
|
|
51
|
+
runtime = config.runtime;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// If no config, assume Node.js
|
|
55
|
+
Logger.warn('No config found, assuming Node.js runtime');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Get runtime adapter
|
|
59
|
+
const adapter = RuntimeRegistry.get(runtime as any);
|
|
60
|
+
|
|
61
|
+
// Generate setup script
|
|
62
|
+
const spinner = ora('Generating setup script...').start();
|
|
63
|
+
|
|
64
|
+
const setupScript = adapter.generateSetupScript(options.nodeVersion, {
|
|
65
|
+
port: parseInt(options.port),
|
|
66
|
+
environment: options.env,
|
|
67
|
+
installRedis: options.redis,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Write script to temp file
|
|
71
|
+
const scriptPath = join(tmpdir(), 'hostfn-setup.sh');
|
|
72
|
+
writeFileSync(scriptPath, setupScript, { mode: 0o755 });
|
|
73
|
+
|
|
74
|
+
spinner.succeed('Setup script generated');
|
|
75
|
+
|
|
76
|
+
Logger.br();
|
|
77
|
+
|
|
78
|
+
// Ask if user wants to execute now
|
|
79
|
+
const { executeNow } = await inquirer.prompt([
|
|
80
|
+
{
|
|
81
|
+
type: 'confirm',
|
|
82
|
+
name: 'executeNow',
|
|
83
|
+
message: 'Execute setup on server now?',
|
|
84
|
+
default: true,
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
if (!executeNow) {
|
|
89
|
+
Logger.br();
|
|
90
|
+
Logger.info('Setup script saved at: ' + scriptPath);
|
|
91
|
+
Logger.br();
|
|
92
|
+
Logger.info('Run manually with:');
|
|
93
|
+
Logger.command(`scp ${scriptPath} ${host}:~/hostfn-setup.sh`);
|
|
94
|
+
Logger.command(`ssh ${host} 'bash ~/hostfn-setup.sh'`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Logger.br();
|
|
99
|
+
Logger.section('Executing Setup on Server');
|
|
100
|
+
Logger.br();
|
|
101
|
+
|
|
102
|
+
const connectSpinner = ora('Connecting to server...').start();
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Connect to server
|
|
106
|
+
const ssh = await createSSHConnection(host, {
|
|
107
|
+
password: options.password,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
connectSpinner.succeed('Connected to server');
|
|
111
|
+
|
|
112
|
+
// Upload setup script
|
|
113
|
+
const uploadSpinner = ora('Uploading setup script...').start();
|
|
114
|
+
await ssh.uploadFile(scriptPath, '/tmp/hostfn-setup.sh');
|
|
115
|
+
uploadSpinner.succeed('Setup script uploaded');
|
|
116
|
+
|
|
117
|
+
// Make script executable
|
|
118
|
+
await ssh.exec('chmod +x /tmp/hostfn-setup.sh');
|
|
119
|
+
|
|
120
|
+
// Execute setup script with streaming output
|
|
121
|
+
Logger.br();
|
|
122
|
+
Logger.info('Executing setup (this may take several minutes)...');
|
|
123
|
+
Logger.br();
|
|
124
|
+
|
|
125
|
+
const result = await ssh.exec('bash /tmp/hostfn-setup.sh', {
|
|
126
|
+
streaming: true,
|
|
127
|
+
skipNvmSetup: true, // Setup script installs nvm, so skip the nvm check
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
Logger.br();
|
|
131
|
+
|
|
132
|
+
if (result.exitCode === 0) {
|
|
133
|
+
Logger.success('Server setup completed successfully!');
|
|
134
|
+
Logger.br();
|
|
135
|
+
Logger.info('Your server is ready for deployments.');
|
|
136
|
+
Logger.br();
|
|
137
|
+
Logger.info('Next steps:');
|
|
138
|
+
Logger.br();
|
|
139
|
+
Logger.info('1. Set environment variables (if needed):');
|
|
140
|
+
Logger.command(`hostfn env push ${options.env} .env`);
|
|
141
|
+
Logger.info(' Or set individual variables:');
|
|
142
|
+
Logger.command(`hostfn env set ${options.env} KEY value`);
|
|
143
|
+
Logger.br();
|
|
144
|
+
Logger.info('2. Deploy your application:');
|
|
145
|
+
Logger.command(`hostfn deploy ${options.env}`);
|
|
146
|
+
} else {
|
|
147
|
+
Logger.error('Setup failed with exit code: ' + result.exitCode);
|
|
148
|
+
Logger.br();
|
|
149
|
+
|
|
150
|
+
// Try to download the log file from the server
|
|
151
|
+
try {
|
|
152
|
+
const localLogPath = join(tmpdir(), `hostfn-setup-error-${Date.now()}.log`);
|
|
153
|
+
await ssh.downloadFile('/tmp/hostfn-setup.log', localLogPath);
|
|
154
|
+
Logger.info('Setup log downloaded to: ' + localLogPath);
|
|
155
|
+
Logger.br();
|
|
156
|
+
|
|
157
|
+
// Read and display the last 50 lines of the log
|
|
158
|
+
const logContent = require('fs').readFileSync(localLogPath, 'utf-8');
|
|
159
|
+
const lines = logContent.split('\n');
|
|
160
|
+
const relevantLines = lines.slice(-50).join('\n');
|
|
161
|
+
|
|
162
|
+
Logger.error('Last 50 lines of setup log:');
|
|
163
|
+
Logger.log(relevantLines);
|
|
164
|
+
} catch (logError) {
|
|
165
|
+
Logger.warn('Could not download setup log from server');
|
|
166
|
+
|
|
167
|
+
if (result.stderr) {
|
|
168
|
+
Logger.br();
|
|
169
|
+
Logger.error('Stderr output:');
|
|
170
|
+
Logger.log(result.stderr);
|
|
171
|
+
}
|
|
172
|
+
if (result.stdout) {
|
|
173
|
+
Logger.br();
|
|
174
|
+
Logger.error('Stdout output (last 1000 chars):');
|
|
175
|
+
Logger.log(result.stdout.slice(-1000));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
Logger.br();
|
|
180
|
+
Logger.info('To inspect the full log on the server, run:');
|
|
181
|
+
Logger.command(`ssh ${host} 'cat /tmp/hostfn-setup.log'`);
|
|
182
|
+
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Cleanup
|
|
187
|
+
await ssh.exec('rm /tmp/hostfn-setup.sh').catch(() => {});
|
|
188
|
+
ssh.disconnect();
|
|
189
|
+
|
|
190
|
+
} catch (error) {
|
|
191
|
+
connectSpinner.fail('Setup failed');
|
|
192
|
+
Logger.error(error instanceof Error ? error.message : String(error));
|
|
193
|
+
Logger.br();
|
|
194
|
+
Logger.info('Setup script saved at: ' + scriptPath);
|
|
195
|
+
Logger.info('You can try running it manually:');
|
|
196
|
+
Logger.command(`scp ${scriptPath} ${host}:~/hostfn-setup.sh`);
|
|
197
|
+
Logger.command(`ssh ${host} 'bash ~/hostfn-setup.sh'`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|