@genxis/n8nrockstars 1.0.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/PUBLISHING.md +148 -0
- package/README.md +276 -0
- package/bin/cli.js +121 -0
- package/index.js +22 -0
- package/lib/apache-proxy.js +65 -0
- package/lib/checker.js +90 -0
- package/lib/cpanel-subdomain.js +94 -0
- package/lib/deployer.js +209 -0
- package/lib/ftp-uploader.js +54 -0
- package/lib/n8n-starter.js +76 -0
- package/lib/postgres-installer.js +44 -0
- package/lib/ssh-executor.js +58 -0
- package/package.json +45 -0
- package/templates/install-complete.sh +119 -0
- package/templates/server.js.template +70 -0
package/lib/checker.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-deployment checks
|
|
3
|
+
* Verifies server meets requirements
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const https = require('https');
|
|
7
|
+
|
|
8
|
+
async function checkPrerequisites(options) {
|
|
9
|
+
const checks = [];
|
|
10
|
+
|
|
11
|
+
// Check 1: Server connectivity
|
|
12
|
+
checks.push({
|
|
13
|
+
name: 'Server connectivity',
|
|
14
|
+
test: async () => await testConnection(options.server)
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Check 2: WHM/cPanel access
|
|
18
|
+
checks.push({
|
|
19
|
+
name: 'WHM/cPanel API access',
|
|
20
|
+
test: async () => await testAPIAccess(options)
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Check 3: Disk space
|
|
24
|
+
checks.push({
|
|
25
|
+
name: 'Disk space (need 5GB+)',
|
|
26
|
+
test: async () => true // TODO: Check via API
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Check 4: Memory
|
|
30
|
+
checks.push({
|
|
31
|
+
name: 'Memory (recommend 2GB+)',
|
|
32
|
+
test: async () => true // TODO: Check via API
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Run all checks
|
|
36
|
+
for (const check of checks) {
|
|
37
|
+
try {
|
|
38
|
+
await check.test();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(`${check.name}: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { passed: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function testConnection(server) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
50
|
+
|
|
51
|
+
https.get(`https://${server}:2087`, { agent, timeout: 5000 }, (res) => {
|
|
52
|
+
resolve(true);
|
|
53
|
+
}).on('error', (err) => {
|
|
54
|
+
reject(new Error(`Cannot reach server: ${err.message}`));
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function testAPIAccess(options) {
|
|
60
|
+
const { server, user, password, whmToken } = options;
|
|
61
|
+
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
62
|
+
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const port = whmToken ? 2087 : 2083;
|
|
65
|
+
const path = whmToken ? '/json-api/version' : '/execute/Fileman/list_files?dir=/home';
|
|
66
|
+
const auth = whmToken
|
|
67
|
+
? `whm root:${whmToken}`
|
|
68
|
+
: `Basic ${Buffer.from(`${user}:${password}`).toString('base64')}`;
|
|
69
|
+
|
|
70
|
+
const reqOptions = {
|
|
71
|
+
hostname: server,
|
|
72
|
+
port: port,
|
|
73
|
+
path: path,
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers: { 'Authorization': auth },
|
|
76
|
+
agent,
|
|
77
|
+
timeout: 10000
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
https.request(reqOptions, (res) => {
|
|
81
|
+
if (res.statusCode === 200 || res.statusCode === 403) {
|
|
82
|
+
resolve(true);
|
|
83
|
+
} else {
|
|
84
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
85
|
+
}
|
|
86
|
+
}).on('error', reject).end();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { checkPrerequisites };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cPanel subdomain creation via UAPI
|
|
3
|
+
* Handles DNS record creation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const https = require('https');
|
|
7
|
+
|
|
8
|
+
async function createSubdomain(options) {
|
|
9
|
+
const { server, user, whmToken, subdomain, domain, docRoot } = options;
|
|
10
|
+
|
|
11
|
+
// Create subdomain via UAPI
|
|
12
|
+
const result = await callUAPI({
|
|
13
|
+
server,
|
|
14
|
+
user,
|
|
15
|
+
whmToken,
|
|
16
|
+
module: 'SubDomain',
|
|
17
|
+
func: 'addsubdomain',
|
|
18
|
+
params: {
|
|
19
|
+
domain: subdomain,
|
|
20
|
+
rootdomain: domain,
|
|
21
|
+
dir: docRoot
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Add DNS A record (manual since ZoneEdit module broken)
|
|
26
|
+
await addDNSRecord({
|
|
27
|
+
server,
|
|
28
|
+
user,
|
|
29
|
+
whmToken,
|
|
30
|
+
subdomain: `${subdomain}.${domain}`,
|
|
31
|
+
ip: server
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function callUAPI(options) {
|
|
38
|
+
const { server, user, whmToken, module, func, params } = options;
|
|
39
|
+
|
|
40
|
+
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
41
|
+
|
|
42
|
+
const queryString = Object.entries(params)
|
|
43
|
+
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
|
|
44
|
+
.join('&');
|
|
45
|
+
|
|
46
|
+
const apiUrl = whmToken
|
|
47
|
+
? `/json-api/cpanel?cpanel_jsonapi_user=${user}&cpanel_jsonapi_module=${module}&cpanel_jsonapi_func=${func}&${queryString}`
|
|
48
|
+
: `/execute/${module}/${func}?${queryString}`;
|
|
49
|
+
|
|
50
|
+
const port = whmToken ? 2087 : 2083;
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const reqOptions = {
|
|
54
|
+
hostname: server,
|
|
55
|
+
port: port,
|
|
56
|
+
path: apiUrl,
|
|
57
|
+
method: 'GET',
|
|
58
|
+
headers: whmToken
|
|
59
|
+
? { 'Authorization': `whm root:${whmToken}` }
|
|
60
|
+
: { 'Authorization': `Basic ${Buffer.from(`${user}:${options.password}`).toString('base64')}` },
|
|
61
|
+
agent,
|
|
62
|
+
timeout: 30000
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
https.request(reqOptions, (res) => {
|
|
66
|
+
let data = '';
|
|
67
|
+
res.on('data', chunk => data += chunk);
|
|
68
|
+
res.on('end', () => {
|
|
69
|
+
try {
|
|
70
|
+
resolve(JSON.parse(data));
|
|
71
|
+
} catch {
|
|
72
|
+
resolve({ raw: data });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}).on('error', reject).end();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function addDNSRecord(options) {
|
|
80
|
+
const { server, user, whmToken, subdomain, ip } = options;
|
|
81
|
+
const { executeSSH } = require('./ssh-executor');
|
|
82
|
+
|
|
83
|
+
// Add DNS via zone file edit (ZoneEdit module broken)
|
|
84
|
+
const domain = subdomain.split('.').slice(1).join('.');
|
|
85
|
+
const command = `echo "${subdomain}. 14400 IN A ${ip}" >> /var/named/${domain}.db && /scripts/rebuilddnsconfig`;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await executeSSH({ server, user, whmToken, command });
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// May fail if already exists
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { createSubdomain };
|
package/lib/deployer.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main deployment orchestrator
|
|
3
|
+
* Coordinates all the nightmare steps
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
const { installPostgreSQL } = require('./postgres-installer');
|
|
9
|
+
const { createSubdomain } = require('./cpanel-subdomain');
|
|
10
|
+
const { uploadFiles } = require('./ftp-uploader');
|
|
11
|
+
const { configureApache } = require('./apache-proxy');
|
|
12
|
+
const { startN8n } = require('./n8n-starter');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
|
|
15
|
+
async function deployN8n(options) {
|
|
16
|
+
const {
|
|
17
|
+
server,
|
|
18
|
+
user,
|
|
19
|
+
password,
|
|
20
|
+
whmToken,
|
|
21
|
+
subdomain = 'n8n',
|
|
22
|
+
domain,
|
|
23
|
+
skipPostgres = false,
|
|
24
|
+
skipSsl = false
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
const fullDomain = `${subdomain}.${domain}`;
|
|
28
|
+
const results = {
|
|
29
|
+
url: `https://${fullDomain}`,
|
|
30
|
+
username: 'admin',
|
|
31
|
+
password: generatePassword(16),
|
|
32
|
+
dbPassword: generatePassword(20)
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let spinner;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Step 1: Install PostgreSQL
|
|
39
|
+
if (!skipPostgres) {
|
|
40
|
+
spinner = ora('Installing PostgreSQL 15...').start();
|
|
41
|
+
await installPostgreSQL({ server, user, password, whmToken });
|
|
42
|
+
spinner.succeed('PostgreSQL 15 installed');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Step 2: Create PostgreSQL database
|
|
46
|
+
spinner = ora('Creating PostgreSQL database...').start();
|
|
47
|
+
await createPostgresDB({
|
|
48
|
+
server,
|
|
49
|
+
user,
|
|
50
|
+
password,
|
|
51
|
+
dbName: `${user}_n8n`,
|
|
52
|
+
dbUser: `${user}_n8n`,
|
|
53
|
+
dbPassword: results.dbPassword
|
|
54
|
+
});
|
|
55
|
+
spinner.succeed('Database created');
|
|
56
|
+
|
|
57
|
+
// Step 3: Create subdomain
|
|
58
|
+
spinner = ora(`Creating subdomain ${fullDomain}...`).start();
|
|
59
|
+
await createSubdomain({
|
|
60
|
+
server,
|
|
61
|
+
user,
|
|
62
|
+
password,
|
|
63
|
+
whmToken,
|
|
64
|
+
subdomain,
|
|
65
|
+
domain,
|
|
66
|
+
docRoot: `/home/${user}/n8n`
|
|
67
|
+
});
|
|
68
|
+
spinner.succeed('Subdomain created');
|
|
69
|
+
|
|
70
|
+
// Step 4: Generate server.js
|
|
71
|
+
spinner = ora('Generating n8n startup script...').start();
|
|
72
|
+
const serverJs = generateServerJs({
|
|
73
|
+
domain: fullDomain,
|
|
74
|
+
dbName: `${user}_n8n`,
|
|
75
|
+
dbUser: `${user}_n8n`,
|
|
76
|
+
dbPassword: results.dbPassword,
|
|
77
|
+
adminPassword: results.password
|
|
78
|
+
});
|
|
79
|
+
fs.writeFileSync('server.js', serverJs);
|
|
80
|
+
spinner.succeed('Startup script generated');
|
|
81
|
+
|
|
82
|
+
// Step 5: Upload files via FTP
|
|
83
|
+
spinner = ora('Uploading files to server...').start();
|
|
84
|
+
await uploadFiles({
|
|
85
|
+
server,
|
|
86
|
+
user,
|
|
87
|
+
password,
|
|
88
|
+
localPath: 'server.js',
|
|
89
|
+
remotePath: `/n8n/server.js`
|
|
90
|
+
});
|
|
91
|
+
spinner.succeed('Files uploaded');
|
|
92
|
+
|
|
93
|
+
// Step 6: Install n8n via SSH commands
|
|
94
|
+
spinner = ora('Installing n8n (this takes 1-2 minutes)...').start();
|
|
95
|
+
await installN8nPackage({ server, user, password, whmToken });
|
|
96
|
+
spinner.succeed('n8n installed');
|
|
97
|
+
|
|
98
|
+
// Step 7: Configure Apache proxy
|
|
99
|
+
spinner = ora('Configuring Apache reverse proxy...').start();
|
|
100
|
+
await configureApache({
|
|
101
|
+
server,
|
|
102
|
+
user,
|
|
103
|
+
password,
|
|
104
|
+
whmToken,
|
|
105
|
+
subdomain: fullDomain
|
|
106
|
+
});
|
|
107
|
+
spinner.succeed('Apache proxy configured');
|
|
108
|
+
|
|
109
|
+
// Step 8: Start n8n
|
|
110
|
+
spinner = ora('Starting n8n...').start();
|
|
111
|
+
await startN8n({
|
|
112
|
+
server,
|
|
113
|
+
user,
|
|
114
|
+
password,
|
|
115
|
+
whmToken,
|
|
116
|
+
path: `/home/${user}/n8n`
|
|
117
|
+
});
|
|
118
|
+
spinner.succeed('n8n started');
|
|
119
|
+
|
|
120
|
+
// Step 9: Verify it's running
|
|
121
|
+
spinner = ora('Verifying deployment...').start();
|
|
122
|
+
await verifyDeployment({ server, url: results.url });
|
|
123
|
+
spinner.succeed('Deployment verified');
|
|
124
|
+
|
|
125
|
+
// Save credentials
|
|
126
|
+
const credsFile = `n8n-credentials.txt`;
|
|
127
|
+
const creds = `n8n Deployment Credentials
|
|
128
|
+
=============================
|
|
129
|
+
|
|
130
|
+
URL: ${results.url}
|
|
131
|
+
Username: ${results.username}
|
|
132
|
+
Password: ${results.password}
|
|
133
|
+
|
|
134
|
+
Database: ${user}_n8n
|
|
135
|
+
DB User: ${user}_n8n
|
|
136
|
+
DB Password: ${results.dbPassword}
|
|
137
|
+
|
|
138
|
+
Server: ${server}
|
|
139
|
+
cPanel User: ${user}
|
|
140
|
+
|
|
141
|
+
Deployed: ${new Date().toISOString()}
|
|
142
|
+
`;
|
|
143
|
+
fs.writeFileSync(credsFile, creds);
|
|
144
|
+
|
|
145
|
+
return results;
|
|
146
|
+
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (spinner) spinner.fail('Deployment failed');
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function generatePassword(length) {
|
|
154
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
155
|
+
return Array(length).fill(0)
|
|
156
|
+
.map(() => chars[Math.floor(Math.random() * chars.length)])
|
|
157
|
+
.join('');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function generateServerJs(config) {
|
|
161
|
+
const template = fs.readFileSync(__dirname + '/../templates/server.js.template', 'utf8');
|
|
162
|
+
return template
|
|
163
|
+
.replace(/{{DOMAIN}}/g, config.domain)
|
|
164
|
+
.replace(/{{DB_NAME}}/g, config.dbName)
|
|
165
|
+
.replace(/{{DB_USER}}/g, config.dbUser)
|
|
166
|
+
.replace(/{{DB_PASSWORD}}/g, config.dbPassword)
|
|
167
|
+
.replace(/{{ADMIN_PASSWORD}}/g, config.adminPassword);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function createPostgresDB(options) {
|
|
171
|
+
const { executeSSH } = require('./ssh-executor');
|
|
172
|
+
|
|
173
|
+
const commands = [
|
|
174
|
+
`su - postgres -c "psql -c \\"CREATE DATABASE ${options.dbName};\\"" 2>/dev/null || true`,
|
|
175
|
+
`su - postgres -c "psql -c \\"CREATE USER ${options.dbUser} WITH PASSWORD '${options.dbPassword}';\\"" 2>/dev/null || true`,
|
|
176
|
+
`su - postgres -c "psql -c \\"ALTER DATABASE ${options.dbName} OWNER TO ${options.dbUser};\\""` ,
|
|
177
|
+
`su - postgres -c "psql -d ${options.dbName} -c \\"GRANT ALL ON SCHEMA public TO ${options.dbUser};\\""`
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
for (const cmd of commands) {
|
|
181
|
+
await executeSSH({ ...options, command: cmd });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function installN8nPackage(options) {
|
|
186
|
+
const { executeSSH } = require('./ssh-executor');
|
|
187
|
+
|
|
188
|
+
await executeSSH({
|
|
189
|
+
...options,
|
|
190
|
+
command: `cd /home/${options.user}/n8n && npm install n8n`
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function verifyDeployment(options) {
|
|
195
|
+
const https = require('https');
|
|
196
|
+
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
197
|
+
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
https.get(options.url, { agent, timeout: 10000 }, (res) => {
|
|
200
|
+
if (res.statusCode === 200) {
|
|
201
|
+
resolve(true);
|
|
202
|
+
} else {
|
|
203
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
204
|
+
}
|
|
205
|
+
}).on('error', reject);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = { deployN8n };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FTP file uploader
|
|
3
|
+
* Bypasses the UAPI file upload complexity
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ftp = require('basic-ftp');
|
|
7
|
+
|
|
8
|
+
async function uploadFiles(options) {
|
|
9
|
+
const { server, user, password, localPath, remotePath } = options;
|
|
10
|
+
|
|
11
|
+
const client = new ftp.Client();
|
|
12
|
+
client.ftp.verbose = false;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Connect to FTP
|
|
16
|
+
await client.access({
|
|
17
|
+
host: server,
|
|
18
|
+
user: user,
|
|
19
|
+
password: password,
|
|
20
|
+
secure: false
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Ensure remote directory exists
|
|
24
|
+
const remoteDir = remotePath.substring(0, remotePath.lastIndexOf('/'));
|
|
25
|
+
try {
|
|
26
|
+
await client.ensureDir(remoteDir);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Directory might exist
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Upload file
|
|
32
|
+
await client.uploadFrom(localPath, remotePath);
|
|
33
|
+
|
|
34
|
+
// Verify upload
|
|
35
|
+
const list = await client.list(remoteDir);
|
|
36
|
+
const fileName = remotePath.split('/').pop();
|
|
37
|
+
const uploaded = list.find(f => f.name === fileName);
|
|
38
|
+
|
|
39
|
+
if (!uploaded) {
|
|
40
|
+
throw new Error('File verification failed');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
uploaded: true,
|
|
45
|
+
size: uploaded.size,
|
|
46
|
+
path: remotePath
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
} finally {
|
|
50
|
+
client.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { uploadFiles };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start n8n as a persistent background service
|
|
3
|
+
* Survives terminal disconnects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { executeSSH } = require('./ssh-executor');
|
|
7
|
+
|
|
8
|
+
async function startN8n(options) {
|
|
9
|
+
const { server, user, password, whmToken, path } = options;
|
|
10
|
+
|
|
11
|
+
const commands = [
|
|
12
|
+
// Kill any existing n8n processes
|
|
13
|
+
`pkill -f "node.*server.js" 2>/dev/null || true`,
|
|
14
|
+
|
|
15
|
+
// Start n8n in background (detached from terminal)
|
|
16
|
+
`cd ${path} && nohup node server.js > n8n.log 2>&1 </dev/null &`,
|
|
17
|
+
|
|
18
|
+
// Disown to survive terminal close
|
|
19
|
+
`disown -a 2>/dev/null || true`,
|
|
20
|
+
|
|
21
|
+
// Wait for startup
|
|
22
|
+
'sleep 10',
|
|
23
|
+
|
|
24
|
+
// Verify it's running
|
|
25
|
+
`netstat -tulpn | grep 5678 || (echo "n8n failed to start" && exit 1)`
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const command of commands) {
|
|
29
|
+
try {
|
|
30
|
+
await executeSSH({ server, user, password, whmToken, command });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.message.includes('failed to start')) {
|
|
33
|
+
throw new Error('n8n process failed to start. Check logs at ' + path + '/n8n.log');
|
|
34
|
+
}
|
|
35
|
+
// Other errors might be OK (disown not supported, etc)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { status: 'running', port: 5678 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function createSystemdService(options) {
|
|
43
|
+
const { server, user, path } = options;
|
|
44
|
+
const { executeSSH } = require('./ssh-executor');
|
|
45
|
+
|
|
46
|
+
const serviceContent = `[Unit]
|
|
47
|
+
Description=n8n Workflow Automation
|
|
48
|
+
After=network.target postgresql-15.service
|
|
49
|
+
|
|
50
|
+
[Service]
|
|
51
|
+
Type=simple
|
|
52
|
+
User=${user}
|
|
53
|
+
WorkingDirectory=${path}
|
|
54
|
+
ExecStart=/usr/bin/node ${path}/server.js
|
|
55
|
+
Restart=always
|
|
56
|
+
RestartSec=10
|
|
57
|
+
|
|
58
|
+
[Install]
|
|
59
|
+
WantedBy=multi-user.target
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const commands = [
|
|
63
|
+
`cat > /etc/systemd/system/n8n.service << 'EOF'\n${serviceContent}\nEOF`,
|
|
64
|
+
'systemctl daemon-reload',
|
|
65
|
+
'systemctl enable n8n',
|
|
66
|
+
'systemctl start n8n'
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const command of commands) {
|
|
70
|
+
await executeSSH({ ...options, command });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { status: 'service_created' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { startN8n, createSystemdService };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL installer for CentOS/RHEL cPanel servers
|
|
3
|
+
* Handles the official repo setup nightmare
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { executeSSH } = require('./ssh-executor');
|
|
7
|
+
|
|
8
|
+
async function installPostgreSQL(options) {
|
|
9
|
+
const commands = [
|
|
10
|
+
// Install PostgreSQL official repo
|
|
11
|
+
'dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm 2>/dev/null || true',
|
|
12
|
+
|
|
13
|
+
// Disable built-in PostgreSQL module
|
|
14
|
+
'dnf -qy module disable postgresql',
|
|
15
|
+
|
|
16
|
+
// Install PostgreSQL 15
|
|
17
|
+
'dnf install -y postgresql15-server postgresql15',
|
|
18
|
+
|
|
19
|
+
// Initialize database
|
|
20
|
+
'export PGSETUP_INITDB_OPTIONS="--encoding=UTF8" && /usr/pgsql-15/bin/postgresql-15-setup initdb 2>/dev/null || true',
|
|
21
|
+
|
|
22
|
+
// Enable and start service
|
|
23
|
+
'systemctl enable postgresql-15',
|
|
24
|
+
'systemctl start postgresql-15',
|
|
25
|
+
|
|
26
|
+
// Wait for startup
|
|
27
|
+
'sleep 3'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const command of commands) {
|
|
31
|
+
try {
|
|
32
|
+
await executeSSH({ ...options, command });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Some commands expected to fail (like if already installed)
|
|
35
|
+
if (!error.message.includes('already') && !error.message.includes('exists')) {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { status: 'installed' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { installPostgreSQL };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH command executor via WHM API
|
|
3
|
+
* Uses api.shell when available, falls back to manual instructions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const https = require('https');
|
|
7
|
+
|
|
8
|
+
async function executeSSH(options) {
|
|
9
|
+
const { server, user, password, whmToken, command } = options;
|
|
10
|
+
|
|
11
|
+
// Try via WHM API first (if token provided)
|
|
12
|
+
if (whmToken) {
|
|
13
|
+
try {
|
|
14
|
+
return await executeViaWHM({ server, whmToken, command });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
// WHM api.shell might be disabled, fall back
|
|
17
|
+
console.warn('WHM SSH API unavailable, using alternative method');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Fallback: Return command for manual execution
|
|
22
|
+
throw new Error(`Please run this command on the server:\n${command}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function executeViaWHM(options) {
|
|
26
|
+
const { server, whmToken, command } = options;
|
|
27
|
+
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const reqOptions = {
|
|
31
|
+
hostname: server,
|
|
32
|
+
port: 2087,
|
|
33
|
+
path: `/json-api/api.shell?cmd=${encodeURIComponent(command)}`,
|
|
34
|
+
method: 'GET',
|
|
35
|
+
headers: { 'Authorization': `whm root:${whmToken}` },
|
|
36
|
+
agent,
|
|
37
|
+
timeout: 60000 // Some commands take time
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
https.request(reqOptions, (res) => {
|
|
41
|
+
let data = '';
|
|
42
|
+
res.on('data', chunk => data += chunk);
|
|
43
|
+
res.on('end', () => {
|
|
44
|
+
try {
|
|
45
|
+
const result = JSON.parse(data);
|
|
46
|
+
if (result.metadata && result.metadata.result === 0) {
|
|
47
|
+
reject(new Error(result.metadata.reason || 'Command failed'));
|
|
48
|
+
}
|
|
49
|
+
resolve(result);
|
|
50
|
+
} catch {
|
|
51
|
+
resolve({ raw: data });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}).on('error', reject).end();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { executeSSH };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@genxis/n8nrockstars",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Deploy n8n self-hosted on cPanel/WHM servers - handles all the nightmare edge cases",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"n8nrockstars": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"n8n",
|
|
14
|
+
"cpanel",
|
|
15
|
+
"whm",
|
|
16
|
+
"automation",
|
|
17
|
+
"workflow",
|
|
18
|
+
"deployment",
|
|
19
|
+
"postgresql",
|
|
20
|
+
"apache",
|
|
21
|
+
"proxy",
|
|
22
|
+
"genxis"
|
|
23
|
+
],
|
|
24
|
+
"author": "GenXis",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"axios": "^1.6.0",
|
|
28
|
+
"basic-ftp": "^5.0.5",
|
|
29
|
+
"commander": "^11.1.0",
|
|
30
|
+
"chalk": "^4.1.2",
|
|
31
|
+
"ora": "^5.4.1",
|
|
32
|
+
"inquirer": "^8.2.6"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=16.0.0"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/genxis/n8nrockstars.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/genxis/n8nrockstars/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/genxis/n8nrockstars#readme"
|
|
45
|
+
}
|