@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.
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/install.ps1 +55 -0
- package/install.sh +99 -0
- package/package.json +86 -0
- package/src/adapters/dotnet.adapter.js +41 -0
- package/src/adapters/go.adapter.js +40 -0
- package/src/adapters/index.js +48 -0
- package/src/adapters/java.adapter.js +46 -0
- package/src/adapters/node.adapter.js +77 -0
- package/src/adapters/php.adapter.js +43 -0
- package/src/adapters/python.adapter.js +54 -0
- package/src/adapters/rails.adapter.js +66 -0
- package/src/artifact/engine.js +473 -0
- package/src/cli/index.js +45 -0
- package/src/commands/artifact.js +88 -0
- package/src/commands/build.js +44 -0
- package/src/commands/clean.js +50 -0
- package/src/commands/deploy.js +82 -0
- package/src/commands/doctor.js +630 -0
- package/src/commands/init.js +795 -0
- package/src/commands/logs.js +33 -0
- package/src/commands/rollback.js +50 -0
- package/src/commands/storage.js +116 -0
- package/src/commands/update.js +63 -0
- package/src/commands/verify.js +55 -0
- package/src/core/config.js +168 -0
- package/src/core/pipeline.js +77 -0
- package/src/core/stages.js +210 -0
- package/src/deployment/index.js +156 -0
- package/src/deployment/providers/azure-vm.js +7 -0
- package/src/deployment/providers/docker.js +30 -0
- package/src/deployment/providers/ec2.js +7 -0
- package/src/deployment/providers/gcp-vm.js +7 -0
- package/src/deployment/providers/kubernetes.js +30 -0
- package/src/deployment/providers/platforms/_shared.js +167 -0
- package/src/deployment/providers/platforms/aws-amplify.js +164 -0
- package/src/deployment/providers/platforms/azure-static-web-apps.js +68 -0
- package/src/deployment/providers/platforms/cloudflare-pages.js +103 -0
- package/src/deployment/providers/platforms/firebase-app-hosting.js +95 -0
- package/src/deployment/providers/platforms/firebase-hosting.js +99 -0
- package/src/deployment/providers/platforms/index.js +44 -0
- package/src/deployment/providers/platforms/netlify.js +102 -0
- package/src/deployment/providers/platforms/vercel.js +92 -0
- package/src/deployment/providers/ssh.js +365 -0
- package/src/detectors/angular.js +23 -0
- package/src/detectors/backend.detector.js +304 -0
- package/src/detectors/dotnet.js +18 -0
- package/src/detectors/frontend.detector.js +219 -0
- package/src/detectors/go.js +20 -0
- package/src/detectors/index.js +78 -0
- package/src/detectors/java.js +24 -0
- package/src/detectors/nextjs.js +23 -0
- package/src/detectors/node.js +28 -0
- package/src/detectors/php.js +17 -0
- package/src/detectors/python.js +22 -0
- package/src/detectors/react.js +29 -0
- package/src/detectors/vue.js +23 -0
- package/src/logger/index.js +44 -0
- package/src/notifications/email.js +53 -0
- package/src/notifications/index.js +34 -0
- package/src/notifications/slack.js +18 -0
- package/src/notifications/webhook.js +21 -0
- package/src/rollback/engine.js +102 -0
- package/src/storage/index.js +109 -0
- package/src/storage/providers/aws.js +96 -0
- package/src/storage/providers/azure.js +45 -0
- package/src/storage/providers/dropbox.js +49 -0
- package/src/storage/providers/ftp.js +69 -0
- package/src/storage/providers/gcp.js +45 -0
- package/src/storage/providers/gdrive.js +80 -0
- package/src/storage/providers/local.js +61 -0
- package/src/utils/author.js +141 -0
- package/src/utils/checksums.js +53 -0
- package/src/utils/firebase-config-generator.js +35 -0
- package/src/utils/github-actions.js +389 -0
- package/src/utils/init-platform.js +229 -0
- package/src/utils/nginx.js +34 -0
- package/src/utils/platform-env.js +132 -0
- package/src/utils/version.js +31 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { saveConfig } from '../core/config.js';
|
|
7
|
+
import { detectFrontend, detectBackend } from '../detectors/index.js';
|
|
8
|
+
import {
|
|
9
|
+
getFrontendInfo,
|
|
10
|
+
} from '../detectors/frontend.detector.js';
|
|
11
|
+
import {
|
|
12
|
+
getBackendInfo,
|
|
13
|
+
} from '../detectors/backend.detector.js';
|
|
14
|
+
import { getProjectVersion } from '../utils/version.js';
|
|
15
|
+
import {
|
|
16
|
+
writeWorkflowFile,
|
|
17
|
+
getRequiredSecrets,
|
|
18
|
+
guessCliGithubRepo,
|
|
19
|
+
addDeployhubToPackageJson,
|
|
20
|
+
} from '../utils/github-actions.js';
|
|
21
|
+
import {
|
|
22
|
+
promptPlatformQuestions,
|
|
23
|
+
appendPlatformEnvExample,
|
|
24
|
+
showPlatformComparison,
|
|
25
|
+
suggestHealthUrl,
|
|
26
|
+
PLATFORM_CHOICES,
|
|
27
|
+
} from '../utils/init-platform.js';
|
|
28
|
+
import { printAuthorFooter } from '../utils/author.js';
|
|
29
|
+
import { ensureFirebaseJson } from '../utils/firebase-config-generator.js';
|
|
30
|
+
import { generateNginxConfig } from '../utils/nginx.js';
|
|
31
|
+
|
|
32
|
+
const FRONTEND_CHOICES = [
|
|
33
|
+
{ name: 'React', value: 'react' },
|
|
34
|
+
{ name: 'Vue', value: 'vue' },
|
|
35
|
+
{ name: 'Angular', value: 'angular' },
|
|
36
|
+
{ name: 'Next.js', value: 'nextjs' },
|
|
37
|
+
{ name: 'Svelte', value: 'svelte' },
|
|
38
|
+
{ name: 'Astro', value: 'astro' },
|
|
39
|
+
{ name: 'Vanilla JS / HTML', value: 'vanilla' },
|
|
40
|
+
{ name: "Other (I'll configure manually)", value: 'other' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const BACKEND_CHOICES = [
|
|
44
|
+
{ name: 'Node.js — Express', value: 'express' },
|
|
45
|
+
{ name: 'Node.js — NestJS', value: 'nestjs' },
|
|
46
|
+
{ name: 'Node.js — Fastify', value: 'fastify' },
|
|
47
|
+
{ name: 'Node.js — Koa', value: 'koa' },
|
|
48
|
+
{ name: 'Python — FastAPI', value: 'fastapi' },
|
|
49
|
+
{ name: 'Python — Django', value: 'django' },
|
|
50
|
+
{ name: 'Python — Flask', value: 'flask' },
|
|
51
|
+
{ name: 'PHP — Laravel', value: 'laravel' },
|
|
52
|
+
{ name: 'PHP — Symfony', value: 'symfony' },
|
|
53
|
+
{ name: 'Java — Spring Boot', value: 'spring' },
|
|
54
|
+
{ name: 'Go', value: 'go' },
|
|
55
|
+
{ name: '.NET (ASP.NET Core)', value: 'dotnet' },
|
|
56
|
+
{ name: 'Ruby on Rails', value: 'rails' },
|
|
57
|
+
{ name: "Other (I'll configure manually)", value: 'other' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const PROJECT_TYPE_CHOICES = [
|
|
61
|
+
{ name: 'Frontend only', value: 'frontend' },
|
|
62
|
+
{ name: 'Backend only', value: 'backend' },
|
|
63
|
+
{ name: 'Both (monorepo / fullstack)', value: 'both' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {'frontend'|'backend'} side
|
|
68
|
+
* @param {string} [cwd]
|
|
69
|
+
* @returns {string|null}
|
|
70
|
+
*/
|
|
71
|
+
function detectSideFramework(side, cwd) {
|
|
72
|
+
if (side === 'frontend') {
|
|
73
|
+
return detectFrontend(cwd)?.framework || null;
|
|
74
|
+
}
|
|
75
|
+
return detectBackend(cwd)?.framework || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} framework
|
|
80
|
+
* @param {'frontend'|'backend'} side
|
|
81
|
+
* @param {string} [cwd]
|
|
82
|
+
*/
|
|
83
|
+
function getFrameworkDefaults(framework, side, cwd = process.cwd()) {
|
|
84
|
+
if (framework === 'other') {
|
|
85
|
+
return side === 'frontend'
|
|
86
|
+
? getFrontendInfo('vanilla', cwd)
|
|
87
|
+
: getBackendInfo('express', cwd);
|
|
88
|
+
}
|
|
89
|
+
return side === 'frontend'
|
|
90
|
+
? getFrontendInfo(framework, cwd)
|
|
91
|
+
: getBackendInfo(framework, cwd);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {'frontend'|'backend'} side
|
|
96
|
+
* @param {string|null} detectedFramework
|
|
97
|
+
*/
|
|
98
|
+
async function promptFramework(side, detectedFramework) {
|
|
99
|
+
const choices = side === 'frontend' ? FRONTEND_CHOICES : BACKEND_CHOICES;
|
|
100
|
+
const detected = detectedFramework && choices.some((c) => c.value === detectedFramework)
|
|
101
|
+
? detectedFramework
|
|
102
|
+
: choices[0].value;
|
|
103
|
+
|
|
104
|
+
const { framework } = await inquirer.prompt([
|
|
105
|
+
{
|
|
106
|
+
type: 'list',
|
|
107
|
+
name: 'framework',
|
|
108
|
+
message:
|
|
109
|
+
side === 'frontend'
|
|
110
|
+
? 'Select your framework:'
|
|
111
|
+
: 'Select your language / framework:',
|
|
112
|
+
choices,
|
|
113
|
+
default: detected,
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
return framework;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {ReturnType<typeof getFrameworkDefaults>} defaults
|
|
122
|
+
* @param {'frontend'|'backend'} side
|
|
123
|
+
*/
|
|
124
|
+
async function promptBuildSettings(defaults, side) {
|
|
125
|
+
const questions = [];
|
|
126
|
+
|
|
127
|
+
if (side === 'frontend' || defaults.buildCommand) {
|
|
128
|
+
questions.push({
|
|
129
|
+
type: 'input',
|
|
130
|
+
name: 'buildCommand',
|
|
131
|
+
message: 'Build command:',
|
|
132
|
+
default: defaults.buildCommand || 'npm run build',
|
|
133
|
+
when: () => side === 'frontend' || defaults.buildCommand,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
questions.push({
|
|
138
|
+
type: 'input',
|
|
139
|
+
name: 'buildOutput',
|
|
140
|
+
message: 'Build output directory:',
|
|
141
|
+
default: defaults.buildOutput,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (side === 'backend') {
|
|
145
|
+
questions.push({
|
|
146
|
+
type: 'input',
|
|
147
|
+
name: 'startCommand',
|
|
148
|
+
message: 'Start command:',
|
|
149
|
+
default: defaults.startCommand || 'npm start',
|
|
150
|
+
});
|
|
151
|
+
questions.push({
|
|
152
|
+
type: 'number',
|
|
153
|
+
name: 'port',
|
|
154
|
+
message: 'Default port:',
|
|
155
|
+
default: defaults.port || 3000,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return inquirer.prompt(questions);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const SERVER_DEPLOY_TYPES = [
|
|
163
|
+
'ssh',
|
|
164
|
+
'docker',
|
|
165
|
+
'ec2',
|
|
166
|
+
'azure-vm',
|
|
167
|
+
'gcp-vm',
|
|
168
|
+
'kubernetes',
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {string} projectName
|
|
173
|
+
* @param {'frontend'|'backend'|'both'} projectType
|
|
174
|
+
* @param {Record<string, unknown>|null} backendConfig
|
|
175
|
+
* @param {Record<string, unknown>|null} singleConfig
|
|
176
|
+
*/
|
|
177
|
+
async function promptServerDeployment(projectName, projectType, backendConfig, singleConfig) {
|
|
178
|
+
return inquirer.prompt([
|
|
179
|
+
{
|
|
180
|
+
type: 'list',
|
|
181
|
+
name: 'deployType',
|
|
182
|
+
message: 'Deployment type:',
|
|
183
|
+
choices: SERVER_DEPLOY_TYPES,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
type: 'input',
|
|
187
|
+
name: 'envName',
|
|
188
|
+
message: 'Environment name:',
|
|
189
|
+
default: 'production',
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
type: 'input',
|
|
193
|
+
name: 'host',
|
|
194
|
+
message: 'Host (for SSH-based targets):',
|
|
195
|
+
when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: 'input',
|
|
199
|
+
name: 'user',
|
|
200
|
+
message: 'SSH user:',
|
|
201
|
+
when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
type: 'input',
|
|
205
|
+
name: 'deployPath',
|
|
206
|
+
message: 'Deploy path:',
|
|
207
|
+
default: `/var/www/${projectName}`,
|
|
208
|
+
when: (a) =>
|
|
209
|
+
['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
|
|
210
|
+
projectType !== 'both',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
type: 'input',
|
|
214
|
+
name: 'frontendDeployPath',
|
|
215
|
+
message: 'Frontend deploy path:',
|
|
216
|
+
default: `/var/www/${projectName}/public`,
|
|
217
|
+
when: (a) =>
|
|
218
|
+
['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
|
|
219
|
+
projectType === 'both',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: 'input',
|
|
223
|
+
name: 'backendDeployPath',
|
|
224
|
+
message: 'Backend deploy path:',
|
|
225
|
+
default: `/var/www/${projectName}/api`,
|
|
226
|
+
when: (a) =>
|
|
227
|
+
['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
|
|
228
|
+
projectType === 'both',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
type: 'input',
|
|
232
|
+
name: 'appName',
|
|
233
|
+
message: 'App name (for PM2):',
|
|
234
|
+
default: projectType === 'both' ? `${projectName}-api` : projectName,
|
|
235
|
+
when: (a) =>
|
|
236
|
+
['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
|
|
237
|
+
(projectType === 'backend' || projectType === 'both'),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
type: 'input',
|
|
241
|
+
name: 'healthUrl',
|
|
242
|
+
message: 'Health check URL (optional):',
|
|
243
|
+
},
|
|
244
|
+
]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {Awaited<ReturnType<typeof promptServerDeployment>>} deployAnswers
|
|
249
|
+
* @param {'frontend'|'backend'|'both'} projectType
|
|
250
|
+
* @param {string} projectName
|
|
251
|
+
* @param {Record<string, unknown>|null} backendConfig
|
|
252
|
+
* @param {Record<string, unknown>|null} singleConfig
|
|
253
|
+
*/
|
|
254
|
+
function buildServerEnvEntry(
|
|
255
|
+
deployAnswers,
|
|
256
|
+
projectType,
|
|
257
|
+
projectName,
|
|
258
|
+
backendConfig,
|
|
259
|
+
singleConfig
|
|
260
|
+
) {
|
|
261
|
+
/** @type {Record<string, unknown>} */
|
|
262
|
+
const envEntry = {
|
|
263
|
+
deploymentType: 'server',
|
|
264
|
+
type: deployAnswers.deployType,
|
|
265
|
+
host: deployAnswers.host || '',
|
|
266
|
+
user: deployAnswers.user || '',
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (projectType === 'both') {
|
|
270
|
+
envEntry.frontendDeployPath =
|
|
271
|
+
deployAnswers.frontendDeployPath || `/var/www/${projectName}/public`;
|
|
272
|
+
envEntry.backendDeployPath =
|
|
273
|
+
deployAnswers.backendDeployPath || `/var/www/${projectName}/api`;
|
|
274
|
+
envEntry.appName = deployAnswers.appName || `${projectName}-api`;
|
|
275
|
+
envEntry.framework = backendConfig?.framework || 'express';
|
|
276
|
+
envEntry.path = envEntry.backendDeployPath;
|
|
277
|
+
envEntry.backendDeploymentType = 'server';
|
|
278
|
+
} else if (projectType === 'backend') {
|
|
279
|
+
envEntry.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
|
|
280
|
+
envEntry.path = envEntry.deployPath;
|
|
281
|
+
envEntry.appName = deployAnswers.appName || projectName;
|
|
282
|
+
envEntry.framework = singleConfig?.framework || 'express';
|
|
283
|
+
envEntry.port = singleConfig?.port || 3000;
|
|
284
|
+
} else {
|
|
285
|
+
envEntry.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
|
|
286
|
+
envEntry.path = envEntry.deployPath;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return envEntry;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @param {Record<string, unknown>} config
|
|
294
|
+
* @param {Record<string, Record<string, unknown>>} environments
|
|
295
|
+
* @param {string} cwd
|
|
296
|
+
*/
|
|
297
|
+
async function generateProjectScaffold(config, environments, cwd) {
|
|
298
|
+
const envList = Object.values(environments);
|
|
299
|
+
|
|
300
|
+
const usesFirebase = envList.some(
|
|
301
|
+
(env) =>
|
|
302
|
+
env.platform === 'firebase-hosting' || env.platform === 'firebase-app-hosting'
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (usesFirebase && !(await fs.pathExists(path.join(cwd, 'firebase.json')))) {
|
|
306
|
+
const buildOutput =
|
|
307
|
+
config.frontend?.buildOutput || config.buildOutput || 'dist';
|
|
308
|
+
await ensureFirebaseJson(buildOutput, cwd);
|
|
309
|
+
console.log(chalk.gray(' • firebase.json (auto-generated)'));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const usesFrontendSsh = envList.some((env) => {
|
|
313
|
+
if (env.frontendDeploymentType === 'platform') return false;
|
|
314
|
+
if (env.deploymentType === 'platform' && !env.type) return false;
|
|
315
|
+
return env.type === 'ssh' || env.deploymentType === 'server';
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const isFrontendProject =
|
|
319
|
+
config.projectType === 'frontend' || config.projectType === 'both';
|
|
320
|
+
|
|
321
|
+
if (
|
|
322
|
+
isFrontendProject &&
|
|
323
|
+
usesFrontendSsh &&
|
|
324
|
+
!(await fs.pathExists(path.join(cwd, 'nginx.conf')))
|
|
325
|
+
) {
|
|
326
|
+
const deployPath =
|
|
327
|
+
envList.find((e) => e.deployPath || e.path)?.deployPath ||
|
|
328
|
+
envList.find((e) => e.deployPath || e.path)?.path ||
|
|
329
|
+
`/var/www/${config.project}`;
|
|
330
|
+
const buildOutput =
|
|
331
|
+
config.frontend?.buildOutput || config.buildOutput || 'dist';
|
|
332
|
+
const nginxConf = generateNginxConfig(config.project, deployPath, buildOutput);
|
|
333
|
+
await fs.writeFile(path.join(cwd, 'nginx.conf'), nginxConf);
|
|
334
|
+
console.log(chalk.gray(' • nginx.conf (auto-generated)'));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* @param {import('commander').Command} program
|
|
340
|
+
*/
|
|
341
|
+
export function registerInitCommand(program) {
|
|
342
|
+
program
|
|
343
|
+
.command('init')
|
|
344
|
+
.description('Interactive setup for DeployHub')
|
|
345
|
+
.action(async () => {
|
|
346
|
+
const cwd = process.cwd();
|
|
347
|
+
|
|
348
|
+
if (await fs.pathExists(path.join(cwd, 'deployhub.config.json'))) {
|
|
349
|
+
const { overwrite } = await inquirer.prompt([
|
|
350
|
+
{
|
|
351
|
+
type: 'confirm',
|
|
352
|
+
name: 'overwrite',
|
|
353
|
+
message: 'deployhub.config.json already exists. Overwrite?',
|
|
354
|
+
default: false,
|
|
355
|
+
},
|
|
356
|
+
]);
|
|
357
|
+
if (!overwrite) {
|
|
358
|
+
console.log(chalk.yellow('Init cancelled.'));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const detectedFrontend = detectFrontend(cwd);
|
|
364
|
+
const detectedBackend = detectBackend(cwd);
|
|
365
|
+
const defaultName = path.basename(cwd) || 'my-app';
|
|
366
|
+
const defaultCliRepo = await guessCliGithubRepo(cwd);
|
|
367
|
+
|
|
368
|
+
const { projectName } = await inquirer.prompt([
|
|
369
|
+
{
|
|
370
|
+
type: 'input',
|
|
371
|
+
name: 'projectName',
|
|
372
|
+
message: 'Project name:',
|
|
373
|
+
default: defaultName,
|
|
374
|
+
},
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
const { projectType } = await inquirer.prompt([
|
|
378
|
+
{
|
|
379
|
+
type: 'list',
|
|
380
|
+
name: 'projectType',
|
|
381
|
+
message: 'What are you deploying?',
|
|
382
|
+
choices: PROJECT_TYPE_CHOICES,
|
|
383
|
+
default: detectedFrontend && detectedBackend
|
|
384
|
+
? 'both'
|
|
385
|
+
: detectedBackend
|
|
386
|
+
? 'backend'
|
|
387
|
+
: 'frontend',
|
|
388
|
+
},
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
/** @type {Record<string, unknown>} */
|
|
392
|
+
let frontendConfig = null;
|
|
393
|
+
/** @type {Record<string, unknown>} */
|
|
394
|
+
let backendConfig = null;
|
|
395
|
+
/** @type {Record<string, unknown>} */
|
|
396
|
+
let singleConfig = null;
|
|
397
|
+
|
|
398
|
+
if (projectType === 'frontend' || projectType === 'both') {
|
|
399
|
+
const fw = await promptFramework('frontend', detectedFrontend?.framework || null);
|
|
400
|
+
const defaults = getFrameworkDefaults(fw, 'frontend', cwd);
|
|
401
|
+
const settings = await promptBuildSettings(defaults, 'frontend');
|
|
402
|
+
frontendConfig = {
|
|
403
|
+
framework: fw === 'other' ? 'custom' : fw,
|
|
404
|
+
buildCommand: settings.buildCommand ?? defaults.buildCommand,
|
|
405
|
+
buildOutput: settings.buildOutput || defaults.buildOutput,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (projectType === 'backend' || projectType === 'both') {
|
|
410
|
+
const fw = await promptFramework('backend', detectedBackend?.framework || null);
|
|
411
|
+
const defaults = getFrameworkDefaults(fw, 'backend', cwd);
|
|
412
|
+
const settings = await promptBuildSettings(defaults, 'backend');
|
|
413
|
+
backendConfig = {
|
|
414
|
+
framework: fw === 'other' ? 'custom' : fw,
|
|
415
|
+
language: defaults.language,
|
|
416
|
+
buildCommand: settings.buildCommand ?? defaults.buildCommand ?? null,
|
|
417
|
+
startCommand: settings.startCommand ?? defaults.startCommand,
|
|
418
|
+
buildOutput: settings.buildOutput || defaults.buildOutput,
|
|
419
|
+
port: settings.port ?? defaults.port,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (projectType === 'frontend') {
|
|
424
|
+
singleConfig = { ...frontendConfig };
|
|
425
|
+
} else if (projectType === 'backend') {
|
|
426
|
+
singleConfig = { ...backendConfig };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const answers = await inquirer.prompt([
|
|
430
|
+
{
|
|
431
|
+
type: 'checkbox',
|
|
432
|
+
name: 'storage',
|
|
433
|
+
message: 'Select storage providers:',
|
|
434
|
+
choices: [
|
|
435
|
+
{ name: 'Local', value: 'local', checked: true },
|
|
436
|
+
{ name: 'AWS S3', value: 'aws' },
|
|
437
|
+
{ name: 'Google Drive', value: 'gdrive' },
|
|
438
|
+
{ name: 'Azure Blob', value: 'azure' },
|
|
439
|
+
{ name: 'GCP Storage', value: 'gcp' },
|
|
440
|
+
{ name: 'Dropbox', value: 'dropbox' },
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
type: 'confirm',
|
|
445
|
+
name: 'configureDeploy',
|
|
446
|
+
message: 'Configure deployment?',
|
|
447
|
+
default: false,
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
type: 'input',
|
|
451
|
+
name: 'cliSource',
|
|
452
|
+
message:
|
|
453
|
+
'DeployHub CLI source for GitHub Actions (github:user/repo or npm:deployhub):',
|
|
454
|
+
default: defaultCliRepo,
|
|
455
|
+
},
|
|
456
|
+
]);
|
|
457
|
+
|
|
458
|
+
/** @type {Record<string, Record<string, unknown>>} */
|
|
459
|
+
const environments = {};
|
|
460
|
+
/** @type {string[]} */
|
|
461
|
+
const deploy = [];
|
|
462
|
+
let healthUrl = '';
|
|
463
|
+
|
|
464
|
+
if (answers.configureDeploy) {
|
|
465
|
+
const buildOutput =
|
|
466
|
+
frontendConfig?.buildOutput || singleConfig?.buildOutput || 'dist';
|
|
467
|
+
|
|
468
|
+
if (projectType === 'backend') {
|
|
469
|
+
const deployAnswers = await promptServerDeployment(
|
|
470
|
+
projectName,
|
|
471
|
+
projectType,
|
|
472
|
+
backendConfig,
|
|
473
|
+
singleConfig
|
|
474
|
+
);
|
|
475
|
+
deploy.push(deployAnswers.envName);
|
|
476
|
+
environments[deployAnswers.envName] = buildServerEnvEntry(
|
|
477
|
+
deployAnswers,
|
|
478
|
+
projectType,
|
|
479
|
+
projectName,
|
|
480
|
+
backendConfig,
|
|
481
|
+
singleConfig
|
|
482
|
+
);
|
|
483
|
+
if (deployAnswers.healthUrl) {
|
|
484
|
+
healthUrl = deployAnswers.healthUrl;
|
|
485
|
+
} else if (singleConfig?.port) {
|
|
486
|
+
healthUrl = `http://localhost:${singleConfig.port}/health`;
|
|
487
|
+
}
|
|
488
|
+
} else if (projectType === 'frontend') {
|
|
489
|
+
const { deployMethod } = await inquirer.prompt([
|
|
490
|
+
{
|
|
491
|
+
type: 'list',
|
|
492
|
+
name: 'deployMethod',
|
|
493
|
+
message: 'How do you want to deploy?',
|
|
494
|
+
choices: [
|
|
495
|
+
{
|
|
496
|
+
name: 'Managed platform (Vercel, Netlify, Cloudflare Pages, etc.)',
|
|
497
|
+
value: 'platform',
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: 'Self-hosted server (SSH, Docker, EC2, etc.)',
|
|
501
|
+
value: 'server',
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
]);
|
|
506
|
+
|
|
507
|
+
const { envName } = await inquirer.prompt([
|
|
508
|
+
{
|
|
509
|
+
type: 'input',
|
|
510
|
+
name: 'envName',
|
|
511
|
+
message: 'Environment name:',
|
|
512
|
+
default: 'production',
|
|
513
|
+
},
|
|
514
|
+
]);
|
|
515
|
+
deploy.push(envName);
|
|
516
|
+
|
|
517
|
+
if (deployMethod === 'platform') {
|
|
518
|
+
showPlatformComparison();
|
|
519
|
+
const { platform } = await inquirer.prompt([
|
|
520
|
+
{
|
|
521
|
+
type: 'list',
|
|
522
|
+
name: 'platform',
|
|
523
|
+
message: 'Select platform:',
|
|
524
|
+
choices: PLATFORM_CHOICES,
|
|
525
|
+
},
|
|
526
|
+
]);
|
|
527
|
+
|
|
528
|
+
const platformConfig = await promptPlatformQuestions(
|
|
529
|
+
platform,
|
|
530
|
+
projectName,
|
|
531
|
+
buildOutput,
|
|
532
|
+
cwd
|
|
533
|
+
);
|
|
534
|
+
await appendPlatformEnvExample(platform, cwd);
|
|
535
|
+
|
|
536
|
+
const { healthUrlInput } = await inquirer.prompt([
|
|
537
|
+
{
|
|
538
|
+
type: 'input',
|
|
539
|
+
name: 'healthUrlInput',
|
|
540
|
+
message: 'Health check URL (optional):',
|
|
541
|
+
default: suggestHealthUrl(platform, projectName),
|
|
542
|
+
},
|
|
543
|
+
]);
|
|
544
|
+
|
|
545
|
+
environments[envName] = {
|
|
546
|
+
deploymentType: 'platform',
|
|
547
|
+
...platformConfig,
|
|
548
|
+
};
|
|
549
|
+
healthUrl = healthUrlInput || suggestHealthUrl(platform, projectName);
|
|
550
|
+
} else {
|
|
551
|
+
const deployAnswers = await promptServerDeployment(
|
|
552
|
+
projectName,
|
|
553
|
+
projectType,
|
|
554
|
+
backendConfig,
|
|
555
|
+
singleConfig
|
|
556
|
+
);
|
|
557
|
+
deployAnswers.envName = envName;
|
|
558
|
+
environments[envName] = buildServerEnvEntry(
|
|
559
|
+
deployAnswers,
|
|
560
|
+
projectType,
|
|
561
|
+
projectName,
|
|
562
|
+
backendConfig,
|
|
563
|
+
singleConfig
|
|
564
|
+
);
|
|
565
|
+
if (deployAnswers.healthUrl) healthUrl = deployAnswers.healthUrl;
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
const { frontendDeployMethod } = await inquirer.prompt([
|
|
569
|
+
{
|
|
570
|
+
type: 'list',
|
|
571
|
+
name: 'frontendDeployMethod',
|
|
572
|
+
message: 'How do you want to deploy the frontend?',
|
|
573
|
+
choices: [
|
|
574
|
+
{
|
|
575
|
+
name: 'Managed platform (Vercel, Netlify, Cloudflare Pages, etc.)',
|
|
576
|
+
value: 'platform',
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: 'Self-hosted server (SSH, Docker, EC2, etc.)',
|
|
580
|
+
value: 'server',
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
},
|
|
584
|
+
]);
|
|
585
|
+
|
|
586
|
+
const { envName } = await inquirer.prompt([
|
|
587
|
+
{
|
|
588
|
+
type: 'input',
|
|
589
|
+
name: 'envName',
|
|
590
|
+
message: 'Environment name:',
|
|
591
|
+
default: 'production',
|
|
592
|
+
},
|
|
593
|
+
]);
|
|
594
|
+
deploy.push(envName);
|
|
595
|
+
|
|
596
|
+
/** @type {Record<string, unknown>} */
|
|
597
|
+
const envEntry = { backendDeploymentType: 'server' };
|
|
598
|
+
|
|
599
|
+
if (frontendDeployMethod === 'platform') {
|
|
600
|
+
showPlatformComparison();
|
|
601
|
+
const { platform } = await inquirer.prompt([
|
|
602
|
+
{
|
|
603
|
+
type: 'list',
|
|
604
|
+
name: 'platform',
|
|
605
|
+
message: 'Select frontend platform:',
|
|
606
|
+
choices: PLATFORM_CHOICES,
|
|
607
|
+
},
|
|
608
|
+
]);
|
|
609
|
+
|
|
610
|
+
const platformConfig = await promptPlatformQuestions(
|
|
611
|
+
platform,
|
|
612
|
+
projectName,
|
|
613
|
+
buildOutput,
|
|
614
|
+
cwd
|
|
615
|
+
);
|
|
616
|
+
await appendPlatformEnvExample(platform, cwd);
|
|
617
|
+
|
|
618
|
+
Object.assign(envEntry, {
|
|
619
|
+
frontendDeploymentType: 'platform',
|
|
620
|
+
deploymentType: 'platform',
|
|
621
|
+
...platformConfig,
|
|
622
|
+
});
|
|
623
|
+
healthUrl = suggestHealthUrl(platform, projectName);
|
|
624
|
+
} else {
|
|
625
|
+
envEntry.frontendDeploymentType = 'server';
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
console.log(chalk.gray('\nBackend will be deployed to a self-hosted server.'));
|
|
629
|
+
const backendDeployAnswers = await inquirer.prompt([
|
|
630
|
+
{
|
|
631
|
+
type: 'list',
|
|
632
|
+
name: 'deployType',
|
|
633
|
+
message: 'Backend deployment type:',
|
|
634
|
+
choices: SERVER_DEPLOY_TYPES,
|
|
635
|
+
default: 'ssh',
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
type: 'input',
|
|
639
|
+
name: 'host',
|
|
640
|
+
message: 'Host (for SSH-based targets):',
|
|
641
|
+
when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
type: 'input',
|
|
645
|
+
name: 'user',
|
|
646
|
+
message: 'SSH user:',
|
|
647
|
+
when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
type: 'input',
|
|
651
|
+
name: 'backendDeployPath',
|
|
652
|
+
message: 'Backend deploy path:',
|
|
653
|
+
default: `/var/www/${projectName}/api`,
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
type: 'input',
|
|
657
|
+
name: 'appName',
|
|
658
|
+
message: 'App name (for PM2):',
|
|
659
|
+
default: `${projectName}-api`,
|
|
660
|
+
},
|
|
661
|
+
]);
|
|
662
|
+
|
|
663
|
+
Object.assign(envEntry, {
|
|
664
|
+
type: backendDeployAnswers.deployType,
|
|
665
|
+
host: backendDeployAnswers.host || '',
|
|
666
|
+
user: backendDeployAnswers.user || '',
|
|
667
|
+
backendDeployPath:
|
|
668
|
+
backendDeployAnswers.backendDeployPath || `/var/www/${projectName}/api`,
|
|
669
|
+
appName: backendDeployAnswers.appName || `${projectName}-api`,
|
|
670
|
+
framework: backendConfig?.framework || 'express',
|
|
671
|
+
path: backendDeployAnswers.backendDeployPath || `/var/www/${projectName}/api`,
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
if (frontendDeployMethod === 'server') {
|
|
675
|
+
const { frontendDeployPath } = await inquirer.prompt([
|
|
676
|
+
{
|
|
677
|
+
type: 'input',
|
|
678
|
+
name: 'frontendDeployPath',
|
|
679
|
+
message: 'Frontend deploy path:',
|
|
680
|
+
default: `/var/www/${projectName}/public`,
|
|
681
|
+
},
|
|
682
|
+
]);
|
|
683
|
+
envEntry.frontendDeployPath = frontendDeployPath;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const { healthUrlInput } = await inquirer.prompt([
|
|
687
|
+
{
|
|
688
|
+
type: 'input',
|
|
689
|
+
name: 'healthUrlInput',
|
|
690
|
+
message: 'Health check URL (optional):',
|
|
691
|
+
default: healthUrl || '',
|
|
692
|
+
},
|
|
693
|
+
]);
|
|
694
|
+
if (healthUrlInput) healthUrl = healthUrlInput;
|
|
695
|
+
else if (!healthUrl && backendConfig?.port) {
|
|
696
|
+
healthUrl = `http://localhost:${backendConfig.port}/health`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
environments[envName] = envEntry;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const version = await getProjectVersion(cwd);
|
|
704
|
+
const hasDocker =
|
|
705
|
+
(detectedFrontend?.hasDocker || detectedBackend?.hasDocker) ?? false;
|
|
706
|
+
|
|
707
|
+
/** @type {Record<string, unknown>} */
|
|
708
|
+
const config = {
|
|
709
|
+
project: projectName,
|
|
710
|
+
version,
|
|
711
|
+
projectType,
|
|
712
|
+
artifact: true,
|
|
713
|
+
storage: answers.storage.length > 0 ? answers.storage : ['local'],
|
|
714
|
+
deploy,
|
|
715
|
+
environments,
|
|
716
|
+
healthCheck: {
|
|
717
|
+
url: healthUrl,
|
|
718
|
+
timeout: 30,
|
|
719
|
+
},
|
|
720
|
+
notifications: {
|
|
721
|
+
slack: false,
|
|
722
|
+
email: false,
|
|
723
|
+
webhook: false,
|
|
724
|
+
},
|
|
725
|
+
pipeline: {
|
|
726
|
+
test: true,
|
|
727
|
+
docker: hasDocker,
|
|
728
|
+
deploy: deploy.length > 0,
|
|
729
|
+
verify: !!healthUrl,
|
|
730
|
+
notify: false,
|
|
731
|
+
},
|
|
732
|
+
artifactRetention: 10,
|
|
733
|
+
cli: {
|
|
734
|
+
source: answers.cliSource,
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
if (projectType === 'both') {
|
|
739
|
+
config.frontend = frontendConfig;
|
|
740
|
+
config.backend = backendConfig;
|
|
741
|
+
config.framework = backendConfig?.framework;
|
|
742
|
+
} else {
|
|
743
|
+
Object.assign(config, singleConfig);
|
|
744
|
+
if (projectType === 'backend') {
|
|
745
|
+
config.docker = hasDocker;
|
|
746
|
+
} else {
|
|
747
|
+
config.docker = hasDocker;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
await saveConfig(config, cwd);
|
|
752
|
+
await addDeployhubToPackageJson(answers.cliSource, cwd);
|
|
753
|
+
await writeWorkflowFile(
|
|
754
|
+
config.storage,
|
|
755
|
+
deploy,
|
|
756
|
+
environments,
|
|
757
|
+
cwd,
|
|
758
|
+
answers.cliSource,
|
|
759
|
+
config
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
await generateProjectScaffold(config, environments, cwd);
|
|
763
|
+
|
|
764
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
765
|
+
const envExampleSrc = path.resolve(__dirname, '../../.env.example');
|
|
766
|
+
const envExampleDest = path.join(cwd, '.env.example');
|
|
767
|
+
if (await fs.pathExists(envExampleSrc)) {
|
|
768
|
+
await fs.copy(envExampleSrc, envExampleDest);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const secrets = getRequiredSecrets(config.storage, deploy, environments, config);
|
|
772
|
+
|
|
773
|
+
console.log('');
|
|
774
|
+
console.log(chalk.green.bold('✓ DeployHub initialized successfully!'));
|
|
775
|
+
console.log('');
|
|
776
|
+
console.log(chalk.bold('Generated files:'));
|
|
777
|
+
console.log(' • deployhub.config.json');
|
|
778
|
+
console.log(' • .github/workflows/deployhub.yml');
|
|
779
|
+
console.log(' • .env.example');
|
|
780
|
+
console.log('');
|
|
781
|
+
console.log(chalk.bold('Next steps:'));
|
|
782
|
+
console.log(' 1. Push the DeployHub CLI repo to GitHub (if using github: source)');
|
|
783
|
+
console.log(' 2. Copy .env.example to .env and fill in credentials');
|
|
784
|
+
if (secrets.length > 0) {
|
|
785
|
+
console.log(' 3. Add these secrets to GitHub (Settings → Secrets):');
|
|
786
|
+
secrets.forEach((s) => console.log(` • ${s}`));
|
|
787
|
+
}
|
|
788
|
+
console.log(` ${secrets.length > 0 ? '4' : '3'}. Run ${chalk.cyan('deployhub doctor')} to verify your setup`);
|
|
789
|
+
console.log(` ${secrets.length > 0 ? '5' : '4'}. Push to main — GitHub Actions will run ${chalk.cyan('deployhub build')} automatically`);
|
|
790
|
+
console.log('');
|
|
791
|
+
printAuthorFooter();
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export default { registerInitCommand };
|