@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/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 };
@@ -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
+ }