@akash-chowdhury-24/deployhub 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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/install.ps1 +55 -0
  4. package/install.sh +99 -0
  5. package/package.json +86 -0
  6. package/src/adapters/dotnet.adapter.js +41 -0
  7. package/src/adapters/go.adapter.js +40 -0
  8. package/src/adapters/index.js +48 -0
  9. package/src/adapters/java.adapter.js +46 -0
  10. package/src/adapters/node.adapter.js +77 -0
  11. package/src/adapters/php.adapter.js +43 -0
  12. package/src/adapters/python.adapter.js +54 -0
  13. package/src/adapters/rails.adapter.js +66 -0
  14. package/src/artifact/engine.js +473 -0
  15. package/src/cli/index.js +45 -0
  16. package/src/commands/artifact.js +88 -0
  17. package/src/commands/build.js +44 -0
  18. package/src/commands/clean.js +50 -0
  19. package/src/commands/deploy.js +82 -0
  20. package/src/commands/doctor.js +630 -0
  21. package/src/commands/init.js +795 -0
  22. package/src/commands/logs.js +33 -0
  23. package/src/commands/rollback.js +50 -0
  24. package/src/commands/storage.js +116 -0
  25. package/src/commands/update.js +63 -0
  26. package/src/commands/verify.js +55 -0
  27. package/src/core/config.js +168 -0
  28. package/src/core/pipeline.js +77 -0
  29. package/src/core/stages.js +210 -0
  30. package/src/deployment/index.js +156 -0
  31. package/src/deployment/providers/azure-vm.js +7 -0
  32. package/src/deployment/providers/docker.js +30 -0
  33. package/src/deployment/providers/ec2.js +7 -0
  34. package/src/deployment/providers/gcp-vm.js +7 -0
  35. package/src/deployment/providers/kubernetes.js +30 -0
  36. package/src/deployment/providers/platforms/_shared.js +167 -0
  37. package/src/deployment/providers/platforms/aws-amplify.js +164 -0
  38. package/src/deployment/providers/platforms/azure-static-web-apps.js +68 -0
  39. package/src/deployment/providers/platforms/cloudflare-pages.js +103 -0
  40. package/src/deployment/providers/platforms/firebase-app-hosting.js +95 -0
  41. package/src/deployment/providers/platforms/firebase-hosting.js +99 -0
  42. package/src/deployment/providers/platforms/index.js +44 -0
  43. package/src/deployment/providers/platforms/netlify.js +102 -0
  44. package/src/deployment/providers/platforms/vercel.js +92 -0
  45. package/src/deployment/providers/ssh.js +365 -0
  46. package/src/detectors/angular.js +23 -0
  47. package/src/detectors/backend.detector.js +304 -0
  48. package/src/detectors/dotnet.js +18 -0
  49. package/src/detectors/frontend.detector.js +219 -0
  50. package/src/detectors/go.js +20 -0
  51. package/src/detectors/index.js +78 -0
  52. package/src/detectors/java.js +24 -0
  53. package/src/detectors/nextjs.js +23 -0
  54. package/src/detectors/node.js +28 -0
  55. package/src/detectors/php.js +17 -0
  56. package/src/detectors/python.js +22 -0
  57. package/src/detectors/react.js +29 -0
  58. package/src/detectors/vue.js +23 -0
  59. package/src/logger/index.js +44 -0
  60. package/src/notifications/email.js +53 -0
  61. package/src/notifications/index.js +34 -0
  62. package/src/notifications/slack.js +18 -0
  63. package/src/notifications/webhook.js +21 -0
  64. package/src/rollback/engine.js +102 -0
  65. package/src/storage/index.js +109 -0
  66. package/src/storage/providers/aws.js +96 -0
  67. package/src/storage/providers/azure.js +45 -0
  68. package/src/storage/providers/dropbox.js +49 -0
  69. package/src/storage/providers/ftp.js +69 -0
  70. package/src/storage/providers/gcp.js +45 -0
  71. package/src/storage/providers/gdrive.js +80 -0
  72. package/src/storage/providers/local.js +61 -0
  73. package/src/utils/author.js +141 -0
  74. package/src/utils/checksums.js +53 -0
  75. package/src/utils/firebase-config-generator.js +35 -0
  76. package/src/utils/github-actions.js +389 -0
  77. package/src/utils/init-platform.js +229 -0
  78. package/src/utils/nginx.js +34 -0
  79. package/src/utils/platform-env.js +132 -0
  80. package/src/utils/version.js +31 -0
@@ -0,0 +1,92 @@
1
+ import { createLogger } from '../../../logger/index.js';
2
+ import {
3
+ runCli,
4
+ saveDeploymentRecord,
5
+ readPreviousPlatformDeployment,
6
+ checkUrlHealth,
7
+ } from './_shared.js';
8
+
9
+ /**
10
+ * @param {import('../../../core/config.js').DeployHubConfig} config
11
+ * @param {string} envName
12
+ * @param {Record<string, string>} [env]
13
+ */
14
+ export function createVercelProvider(config, envName, env = process.env) {
15
+ const log = createLogger('vercel');
16
+ const token = env.VERCEL_TOKEN;
17
+ const orgId = env.VERCEL_ORG_ID;
18
+ const projectId = env.VERCEL_PROJECT_ID;
19
+
20
+ async function deploy(artifactDir) {
21
+ if (!token) throw new Error('VERCEL_TOKEN is required');
22
+
23
+ log.info('Deploying to Vercel...');
24
+ const cwd = process.cwd();
25
+ const deployEnv = {
26
+ VERCEL_ORG_ID: orgId || '',
27
+ VERCEL_PROJECT_ID: projectId || '',
28
+ };
29
+
30
+ const result = await runCli(
31
+ `vercel deploy --prod --token=${token} --yes`,
32
+ cwd,
33
+ deployEnv
34
+ );
35
+
36
+ if (result.exitCode !== 0) {
37
+ throw new Error(`Vercel deploy failed: ${result.stderr || result.stdout}`);
38
+ }
39
+
40
+ const output = result.stdout || '';
41
+ const urlMatch = output.match(/https:\/\/[^\s]+\.vercel\.app/);
42
+ const deploymentUrl = urlMatch ? urlMatch[0] : config.healthCheck?.url || '';
43
+
44
+ await saveDeploymentRecord(
45
+ artifactDir,
46
+ {
47
+ platform: 'vercel',
48
+ deploymentUrl,
49
+ deploymentId: deploymentUrl,
50
+ },
51
+ envName
52
+ );
53
+
54
+ log.success(`Deployed to Vercel: ${deploymentUrl || 'production'}`);
55
+ }
56
+
57
+ async function rollback(artifactDir) {
58
+ if (!token) throw new Error('VERCEL_TOKEN is required');
59
+
60
+ const previous = await readPreviousPlatformDeployment(artifactDir);
61
+ const deploymentUrl = previous?.deploymentUrl || previous?.deploymentId;
62
+
63
+ log.info('Rolling back on Vercel...');
64
+ const cmd = deploymentUrl
65
+ ? `vercel rollback ${deploymentUrl} --token=${token} --yes`
66
+ : `vercel rollback --token=${token} --yes`;
67
+
68
+ const result = await runCli(cmd, process.cwd());
69
+ if (result.exitCode !== 0) {
70
+ throw new Error(`Vercel rollback failed: ${result.stderr || result.stdout}`);
71
+ }
72
+ log.success('Vercel rollback complete');
73
+ }
74
+
75
+ async function healthCheck() {
76
+ const url = config.healthCheck?.url;
77
+ if (!url) return true;
78
+ return checkUrlHealth(url);
79
+ }
80
+
81
+ async function testConnection() {
82
+ if (!token) throw new Error('VERCEL_TOKEN is required');
83
+ const result = await runCli(`vercel whoami --token=${token}`, process.cwd());
84
+ if (result.exitCode !== 0) {
85
+ throw new Error('Invalid VERCEL_TOKEN');
86
+ }
87
+ }
88
+
89
+ return { deploy, rollback, healthCheck, testConnection };
90
+ }
91
+
92
+ export default { createVercelProvider };
@@ -0,0 +1,365 @@
1
+ import { NodeSSH } from 'node-ssh';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { createLogger } from '../../logger/index.js';
6
+ import { getNginxSitePath } from '../../utils/nginx.js';
7
+
8
+ /** @type {Set<string>} */
9
+ const NODE_FRAMEWORKS = new Set(['express', 'nestjs', 'fastify', 'koa', 'nextjs', 'node']);
10
+ /** @type {Set<string>} */
11
+ const PYTHON_FRAMEWORKS = new Set(['fastapi', 'django', 'flask', 'python']);
12
+ /** @type {Set<string>} */
13
+ const PHP_FRAMEWORKS = new Set(['laravel', 'symfony', 'php']);
14
+
15
+ /**
16
+ * @param {import('../../core/config.js').DeployHubConfig} config
17
+ * @param {string} envName
18
+ * @param {Record<string, string>} [env]
19
+ */
20
+ export function createSshProvider(config, envName, env = process.env) {
21
+ const environment = config.environments[envName];
22
+ if (!environment) {
23
+ throw new Error(`Environment "${envName}" not found in config`);
24
+ }
25
+
26
+ const host = environment.host || env.SSH_HOST;
27
+ const user = environment.user || env.SSH_USER;
28
+ const deployPath =
29
+ environment.deployPath ||
30
+ environment.path ||
31
+ env.SSH_DEPLOY_PATH ||
32
+ '/var/www/app';
33
+ const frontendDeployPath =
34
+ environment.frontendDeployPath || deployPath;
35
+ const backendDeployPath =
36
+ environment.backendDeployPath || deployPath;
37
+ const appName =
38
+ environment.appName || env.SSH_APP_NAME || config.project;
39
+ const port = environment.port || config.port || Number(env.SSH_PORT) || 3000;
40
+ const sshKey = env.SSH_KEY;
41
+ const keyPath = environment.keyPath || env.SSH_KEY_PATH;
42
+
43
+ const log = createLogger('ssh');
44
+
45
+ async function connect() {
46
+ if (!host || !user) {
47
+ throw new Error('SSH host and user are required. Set SSH_HOST and SSH_USER in .env');
48
+ }
49
+
50
+ const ssh = new NodeSSH();
51
+ /** @type {import('node-ssh').SSHConnectOptions} */
52
+ const connectOpts = { host, username: user };
53
+
54
+ if (sshKey) {
55
+ const tmpKeyPath = path.join(os.tmpdir(), 'deployhub-ssh-key');
56
+ await fs.writeFile(tmpKeyPath, sshKey, { mode: 0o600 });
57
+ connectOpts.privateKeyPath = tmpKeyPath;
58
+ } else if (keyPath) {
59
+ connectOpts.privateKeyPath = keyPath;
60
+ }
61
+
62
+ await ssh.connect(connectOpts);
63
+ return ssh;
64
+ }
65
+
66
+ /**
67
+ * @param {import('node-ssh').NodeSSH} ssh
68
+ * @param {string} command
69
+ */
70
+ async function exec(ssh, command) {
71
+ log.info(`$ ${command}`);
72
+ const result = await ssh.execCommand(command);
73
+ if (result.code !== 0 && result.code !== null) {
74
+ log.warn(`Command exited with code ${result.code}: ${result.stderr || result.stdout}`);
75
+ }
76
+ return result;
77
+ }
78
+
79
+ /**
80
+ * @param {import('node-ssh').NodeSSH} ssh
81
+ */
82
+ function resolveFramework() {
83
+ return (
84
+ environment.framework ||
85
+ config.backend?.framework ||
86
+ config.framework ||
87
+ 'express'
88
+ );
89
+ }
90
+
91
+ /**
92
+ * @param {import('node-ssh').NodeSSH} ssh
93
+ */
94
+ function resolveStartCommand() {
95
+ return (
96
+ config.startCommand ||
97
+ config.backend?.startCommand ||
98
+ null
99
+ );
100
+ }
101
+
102
+ /**
103
+ * @param {import('node-ssh').NodeSSH} ssh
104
+ * @param {string} targetPath
105
+ */
106
+ async function runBackendStartSequence(ssh, targetPath) {
107
+ const framework = resolveFramework();
108
+ const startCommand = resolveStartCommand();
109
+
110
+ if (NODE_FRAMEWORKS.has(framework)) {
111
+ await exec(ssh, `cd ${targetPath} && npm install --production`);
112
+ const start = startCommand || 'npm start';
113
+ if (start === 'npm start') {
114
+ await exec(
115
+ ssh,
116
+ `cd ${targetPath} && pm2 restart ${appName} || pm2 start npm --name "${appName}" -- start`
117
+ );
118
+ } else if (start.startsWith('npm run ')) {
119
+ const script = start.replace('npm run ', '');
120
+ await exec(
121
+ ssh,
122
+ `cd ${targetPath} && pm2 restart ${appName} || pm2 start npm --name "${appName}" -- run ${script}`
123
+ );
124
+ } else {
125
+ const [cmd, ...args] = start.split(' ');
126
+ await exec(
127
+ ssh,
128
+ `cd ${targetPath} && pm2 restart ${appName} || pm2 start ${cmd} --name "${appName}" -- ${args.join(' ')}`
129
+ );
130
+ }
131
+ await exec(ssh, 'pm2 save');
132
+ return;
133
+ }
134
+
135
+ if (PYTHON_FRAMEWORKS.has(framework)) {
136
+ await exec(ssh, `cd ${targetPath} && pip install -r requirements.txt`);
137
+ if (framework === 'django') {
138
+ await exec(ssh, `cd ${targetPath} && python manage.py migrate`);
139
+ }
140
+ if (framework === 'fastapi') {
141
+ await exec(ssh, 'pkill uvicorn || true');
142
+ await exec(
143
+ ssh,
144
+ `cd ${targetPath} && nohup uvicorn main:app --host 0.0.0.0 --port ${port} > app.log 2>&1 &`
145
+ );
146
+ } else {
147
+ await exec(ssh, 'pkill gunicorn || true');
148
+ const appTarget =
149
+ framework === 'django' ? 'config.wsgi:application' : 'app:app';
150
+ await exec(
151
+ ssh,
152
+ `cd ${targetPath} && nohup gunicorn ${appTarget} --bind 0.0.0.0:${port} --daemon`
153
+ );
154
+ }
155
+ return;
156
+ }
157
+
158
+ if (PHP_FRAMEWORKS.has(framework)) {
159
+ await exec(ssh, `cd ${targetPath} && composer install --no-dev`);
160
+ if (framework === 'laravel') {
161
+ await exec(ssh, `cd ${targetPath} && php artisan migrate --force`);
162
+ await exec(ssh, `cd ${targetPath} && php artisan config:cache`);
163
+ }
164
+ await exec(ssh, 'sudo systemctl restart php8.2-fpm');
165
+ await exec(ssh, 'sudo systemctl reload nginx');
166
+ return;
167
+ }
168
+
169
+ if (framework === 'spring' || framework === 'java') {
170
+ await exec(ssh, `cd ${targetPath} && pkill -f "*.jar" || true`);
171
+ await exec(
172
+ ssh,
173
+ `cd ${targetPath} && nohup java -jar target/*.jar > app.log 2>&1 &`
174
+ );
175
+ return;
176
+ }
177
+
178
+ if (framework === 'go') {
179
+ await exec(ssh, `cd ${targetPath} && pkill ${appName} || true`);
180
+ await exec(
181
+ ssh,
182
+ `cd ${targetPath} && nohup ./bin/app > app.log 2>&1 &`
183
+ );
184
+ return;
185
+ }
186
+
187
+ if (framework === 'dotnet') {
188
+ await exec(ssh, `cd ${targetPath} && pkill -f "dotnet" || true`);
189
+ const dll = startCommand?.replace('dotnet ', '') || 'App.dll';
190
+ await exec(
191
+ ssh,
192
+ `cd ${targetPath} && nohup dotnet ${dll} > app.log 2>&1 &`
193
+ );
194
+ return;
195
+ }
196
+
197
+ if (framework === 'rails') {
198
+ await exec(ssh, `cd ${targetPath} && bundle install --deployment`);
199
+ await exec(ssh, `cd ${targetPath} && pkill puma || true`);
200
+ await exec(
201
+ ssh,
202
+ `cd ${targetPath} && nohup bundle exec puma -p ${port} > app.log 2>&1 &`
203
+ );
204
+ return;
205
+ }
206
+
207
+ await exec(ssh, `cd ${targetPath} && npm install --production`);
208
+ await exec(
209
+ ssh,
210
+ `cd ${targetPath} && pm2 restart ${appName} || pm2 start npm --name "${appName}" -- start`
211
+ );
212
+ await exec(ssh, 'pm2 save');
213
+ }
214
+
215
+ /**
216
+ * @param {import('node-ssh').NodeSSH} ssh
217
+ * @param {string} targetPath
218
+ */
219
+ async function setupNginx(ssh, targetPath) {
220
+ const sitePath = getNginxSitePath(config.project);
221
+ const nginxConfRemote = `${targetPath}/nginx.conf`;
222
+
223
+ await exec(
224
+ ssh,
225
+ `sudo cp ${nginxConfRemote} ${sitePath} 2>/dev/null || sudo cp ${targetPath}/nginx.conf ${sitePath}`
226
+ );
227
+ await exec(
228
+ ssh,
229
+ `sudo ln -sf ${sitePath} /etc/nginx/sites-enabled/${path.basename(sitePath)}`
230
+ );
231
+ await exec(ssh, 'sudo nginx -t');
232
+ await exec(ssh, 'sudo systemctl reload nginx');
233
+ }
234
+
235
+ /**
236
+ * @param {import('node-ssh').NodeSSH} ssh
237
+ * @param {string} remoteZip
238
+ * @param {string} targetPath
239
+ */
240
+ async function extractToPath(ssh, remoteZip, targetPath) {
241
+ await exec(ssh, `mkdir -p ${targetPath}`);
242
+ await exec(ssh, `unzip -o ${remoteZip} -d ${targetPath}`);
243
+ }
244
+
245
+ /**
246
+ * @param {string} artifactDir
247
+ */
248
+ async function deploy(artifactDir) {
249
+ const ssh = await connect();
250
+ const projectType = config.projectType || 'frontend';
251
+
252
+ try {
253
+ const zipPath = path.join(artifactDir, 'artifact.zip');
254
+ const remoteZip = `/tmp/deployhub-${Date.now()}.zip`;
255
+
256
+ log.info(`Deploying to ${user}@${host}`);
257
+
258
+ await ssh.putFile(zipPath, remoteZip);
259
+
260
+ if (projectType === 'both') {
261
+ const remoteStaging = `/tmp/deployhub-staging-${Date.now()}`;
262
+ await exec(ssh, `mkdir -p ${remoteStaging}`);
263
+ await exec(ssh, `unzip -o ${remoteZip} -d ${remoteStaging}`);
264
+
265
+ await exec(ssh, `mkdir -p ${frontendDeployPath}`);
266
+ await exec(
267
+ ssh,
268
+ `rsync -a ${remoteStaging}/ ${frontendDeployPath}/ --exclude backend || cp -r ${remoteStaging}/* ${frontendDeployPath}/`
269
+ );
270
+
271
+ await exec(ssh, `mkdir -p ${backendDeployPath}`);
272
+ await exec(
273
+ ssh,
274
+ `rsync -a ${remoteStaging}/backend/ ${backendDeployPath}/ || cp -r ${remoteStaging}/backend/* ${backendDeployPath}/`
275
+ );
276
+
277
+ if (await remoteFileExists(ssh, `${frontendDeployPath}/nginx.conf`)) {
278
+ await setupNginx(ssh, frontendDeployPath);
279
+ }
280
+
281
+ await runBackendStartSequence(ssh, backendDeployPath);
282
+ await exec(ssh, `rm -rf ${remoteStaging}`);
283
+ } else if (projectType === 'backend') {
284
+ log.info(`Backend deploy path: ${deployPath}`);
285
+ await extractToPath(ssh, remoteZip, deployPath);
286
+ await runBackendStartSequence(ssh, deployPath);
287
+ } else {
288
+ log.info(`Frontend deploy path: ${deployPath}`);
289
+ await extractToPath(ssh, remoteZip, deployPath);
290
+
291
+ const framework = config.framework || 'react';
292
+ if (framework === 'nextjs') {
293
+ await runBackendStartSequence(ssh, deployPath);
294
+ } else if (await remoteFileExists(ssh, `${deployPath}/nginx.conf`)) {
295
+ await setupNginx(ssh, deployPath);
296
+ }
297
+ }
298
+
299
+ await exec(ssh, `rm -f ${remoteZip}`);
300
+ log.success('Deployment complete');
301
+ } finally {
302
+ ssh.dispose();
303
+ }
304
+ }
305
+
306
+ /**
307
+ * @param {import('node-ssh').NodeSSH} ssh
308
+ * @param {string} remotePath
309
+ */
310
+ async function remoteFileExists(ssh, remotePath) {
311
+ const result = await ssh.execCommand(`test -f ${remotePath} && echo yes`);
312
+ return result.stdout.trim() === 'yes';
313
+ }
314
+
315
+ async function rollback(artifactDir) {
316
+ await deploy(artifactDir);
317
+ }
318
+
319
+ async function healthCheck() {
320
+ const url = config.healthCheck?.url;
321
+ if (!url) return true;
322
+
323
+ const ssh = await connect();
324
+ try {
325
+ const result = await ssh.execCommand(`curl -sf -o /dev/null -w "%{http_code}" "${url}"`);
326
+ return result.stdout.trim().startsWith('2');
327
+ } finally {
328
+ ssh.dispose();
329
+ }
330
+ }
331
+
332
+ async function testConnection() {
333
+ const ssh = await connect();
334
+ ssh.dispose();
335
+ }
336
+
337
+ /**
338
+ * @param {string} command
339
+ * @returns {Promise<{ pass: boolean, message: string }>}
340
+ */
341
+ async function runRemoteCheck(command) {
342
+ const ssh = await connect();
343
+ try {
344
+ const result = await ssh.execCommand(command);
345
+ const ok = result.code === 0;
346
+ return {
347
+ pass: ok,
348
+ message: ok ? result.stdout.trim() || 'OK' : result.stderr.trim() || result.stdout.trim() || 'Failed',
349
+ };
350
+ } finally {
351
+ ssh.dispose();
352
+ }
353
+ }
354
+
355
+ return {
356
+ deploy,
357
+ rollback,
358
+ healthCheck,
359
+ testConnection,
360
+ runRemoteCheck,
361
+ connect,
362
+ };
363
+ }
364
+
365
+ export default { createSshProvider };
@@ -0,0 +1,23 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ function detect(cwd = process.cwd()) {
5
+ const pkgPath = path.join(cwd, 'package.json');
6
+ if (!fs.existsSync(pkgPath)) return false;
7
+ const pkg = fs.readJsonSync(pkgPath);
8
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
9
+ return !!(deps['@angular/core']);
10
+ }
11
+
12
+ function getInfo(cwd = process.cwd()) {
13
+ const pkg = fs.readJsonSync(path.join(cwd, 'package.json'));
14
+ const scripts = pkg.scripts || {};
15
+ return {
16
+ framework: 'angular',
17
+ buildCommand: scripts.build ? 'npm run build' : 'ng build',
18
+ buildOutput: 'dist',
19
+ hasDocker: fs.existsSync(path.join(cwd, 'Dockerfile')),
20
+ };
21
+ }
22
+
23
+ export default { detect, getInfo };