@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,164 @@
1
+ import path from 'path';
2
+ import fs from 'fs-extra';
3
+ import { execa } from 'execa';
4
+ import { createLogger } from '../../../logger/index.js';
5
+ import {
6
+ runCli,
7
+ saveDeploymentRecord,
8
+ readPreviousPlatformDeployment,
9
+ checkUrlHealth,
10
+ } from './_shared.js';
11
+
12
+ /**
13
+ * @param {import('../../../core/config.js').DeployHubConfig} config
14
+ * @param {string} envName
15
+ * @param {Record<string, string>} [env]
16
+ */
17
+ export function createAwsAmplifyProvider(config, envName, env = process.env) {
18
+ const environment = config.environments[envName];
19
+ const log = createLogger('aws-amplify');
20
+ const appId = environment?.appId || env.AMPLIFY_APP_ID;
21
+ const region = environment?.region || env.AWS_REGION || 'us-east-1';
22
+ const githubConnected = environment?.githubConnected ?? false;
23
+
24
+ async function deploy(artifactDir) {
25
+ if (!appId) throw new Error('AMPLIFY_APP_ID is required');
26
+
27
+ const awsEnv = {
28
+ AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID || '',
29
+ AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY || '',
30
+ AWS_DEFAULT_REGION: region,
31
+ };
32
+
33
+ if (githubConnected) {
34
+ log.info('Triggering Amplify release job (GitHub connected)...');
35
+ const result = await runCli(
36
+ `aws amplify start-job --app-id ${appId} --branch-name main --job-type RELEASE`,
37
+ process.cwd(),
38
+ awsEnv
39
+ );
40
+ if (result.exitCode !== 0) {
41
+ throw new Error(`Amplify start-job failed: ${result.stderr || result.stdout}`);
42
+ }
43
+
44
+ const jobMatch = (result.stdout || '').match(/"jobId":\s*"([^"]+)"/);
45
+ const jobId = jobMatch?.[1] || '';
46
+
47
+ await saveDeploymentRecord(
48
+ artifactDir,
49
+ {
50
+ platform: 'aws-amplify',
51
+ deploymentId: jobId,
52
+ appId,
53
+ method: 'github',
54
+ },
55
+ envName
56
+ );
57
+ log.success(`Amplify job started: ${jobId || 'RELEASE'}`);
58
+ return;
59
+ }
60
+
61
+ log.info('Uploading artifact zip to Amplify...');
62
+ const zipPath = path.join(artifactDir, 'artifact.zip');
63
+ if (!(await fs.pathExists(zipPath))) {
64
+ throw new Error('artifact.zip not found for Amplify upload');
65
+ }
66
+
67
+ const createResult = await runCli(
68
+ `aws amplify create-deployment --app-id ${appId} --branch-name main --output json`,
69
+ process.cwd(),
70
+ awsEnv
71
+ );
72
+ if (createResult.exitCode !== 0) {
73
+ throw new Error(`Amplify create-deployment failed: ${createResult.stderr}`);
74
+ }
75
+
76
+ const deployment = JSON.parse(createResult.stdout);
77
+ const jobId = deployment.jobId;
78
+ const zipUploadUrl = deployment.zipUploadUrl;
79
+
80
+ await execa('curl', ['-T', zipPath, zipUploadUrl], {
81
+ env: { ...process.env, ...awsEnv },
82
+ stdio: 'inherit',
83
+ shell: true,
84
+ });
85
+
86
+ const startResult = await runCli(
87
+ `aws amplify start-deployment --app-id ${appId} --branch-name main --job-id ${jobId}`,
88
+ process.cwd(),
89
+ awsEnv
90
+ );
91
+ if (startResult.exitCode !== 0) {
92
+ throw new Error(`Amplify start-deployment failed: ${startResult.stderr}`);
93
+ }
94
+
95
+ await saveDeploymentRecord(
96
+ artifactDir,
97
+ {
98
+ platform: 'aws-amplify',
99
+ deploymentId: jobId,
100
+ appId,
101
+ method: 'zip',
102
+ },
103
+ envName
104
+ );
105
+ log.success(`Amplify deployment started: job ${jobId}`);
106
+ }
107
+
108
+ async function rollback(artifactDir) {
109
+ if (!appId) throw new Error('AMPLIFY_APP_ID is required');
110
+
111
+ const previous = await readPreviousPlatformDeployment(artifactDir);
112
+ const jobId = previous?.deploymentId;
113
+ if (!jobId) {
114
+ throw new Error('No previous Amplify job ID found for rollback');
115
+ }
116
+
117
+ const awsEnv = {
118
+ AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID || '',
119
+ AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY || '',
120
+ AWS_DEFAULT_REGION: region,
121
+ };
122
+
123
+ log.info(`Re-triggering Amplify job ${jobId}...`);
124
+ const result = await runCli(
125
+ `aws amplify start-job --app-id ${appId} --branch-name main --job-id ${jobId} --job-type RETRY`,
126
+ process.cwd(),
127
+ awsEnv
128
+ );
129
+ if (result.exitCode !== 0) {
130
+ await runCli(
131
+ `aws amplify start-job --app-id ${appId} --branch-name main --job-type RELEASE`,
132
+ process.cwd(),
133
+ awsEnv
134
+ );
135
+ }
136
+ log.success('Amplify rollback triggered');
137
+ }
138
+
139
+ async function healthCheck() {
140
+ const url = config.healthCheck?.url;
141
+ if (!url) return true;
142
+ return checkUrlHealth(url);
143
+ }
144
+
145
+ async function testConnection() {
146
+ if (!appId) throw new Error('AMPLIFY_APP_ID is required');
147
+ const result = await runCli(
148
+ `aws amplify get-app --app-id ${appId}`,
149
+ process.cwd(),
150
+ {
151
+ AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID || '',
152
+ AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY || '',
153
+ AWS_DEFAULT_REGION: region,
154
+ }
155
+ );
156
+ if (result.exitCode !== 0) {
157
+ throw new Error('Could not find Amplify app — check AMPLIFY_APP_ID and AWS credentials');
158
+ }
159
+ }
160
+
161
+ return { deploy, rollback, healthCheck, testConnection };
162
+ }
163
+
164
+ export default { createAwsAmplifyProvider };
@@ -0,0 +1,68 @@
1
+ import { createLogger } from '../../../logger/index.js';
2
+ import {
3
+ runCli,
4
+ saveDeploymentRecord,
5
+ readPreviousPlatformDeployment,
6
+ checkUrlHealth,
7
+ resolveBuildOutputPath,
8
+ } from './_shared.js';
9
+
10
+ /**
11
+ * @param {import('../../../core/config.js').DeployHubConfig} config
12
+ * @param {string} envName
13
+ * @param {Record<string, string>} [env]
14
+ */
15
+ export function createAzureStaticWebAppsProvider(config, envName, env = process.env) {
16
+ const environment = config.environments[envName];
17
+ const log = createLogger('azure-swa');
18
+ const token = env.AZURE_STATIC_WEB_APPS_TOKEN;
19
+ const resourceName = environment?.resourceName || config.project;
20
+
21
+ async function deploy(artifactDir) {
22
+ if (!token) throw new Error('AZURE_STATIC_WEB_APPS_TOKEN is required');
23
+
24
+ const buildDir = resolveBuildOutputPath(config);
25
+ log.info(`Deploying to Azure Static Web Apps (${resourceName})...`);
26
+
27
+ const result = await runCli(
28
+ `swa deploy "${buildDir}" --deployment-token=${token}`,
29
+ process.cwd()
30
+ );
31
+
32
+ if (result.exitCode !== 0) {
33
+ throw new Error(`Azure SWA deploy failed: ${result.stderr || result.stdout}`);
34
+ }
35
+
36
+ await saveDeploymentRecord(
37
+ artifactDir,
38
+ {
39
+ platform: 'azure-static-web-apps',
40
+ deploymentId: `swa-${Date.now()}`,
41
+ resourceName,
42
+ deploymentUrl: config.healthCheck?.url || '',
43
+ },
44
+ envName
45
+ );
46
+
47
+ log.success('Deployed to Azure Static Web Apps');
48
+ }
49
+
50
+ async function rollback(artifactDir) {
51
+ log.info('Azure SWA has no native rollback — re-deploying previous artifact...');
52
+ await deploy(artifactDir);
53
+ }
54
+
55
+ async function healthCheck() {
56
+ const url = config.healthCheck?.url;
57
+ if (!url) return true;
58
+ return checkUrlHealth(url);
59
+ }
60
+
61
+ async function testConnection() {
62
+ if (!token) throw new Error('AZURE_STATIC_WEB_APPS_TOKEN is required');
63
+ }
64
+
65
+ return { deploy, rollback, healthCheck, testConnection };
66
+ }
67
+
68
+ export default { createAzureStaticWebAppsProvider };
@@ -0,0 +1,103 @@
1
+ import axios from 'axios';
2
+ import { createLogger } from '../../../logger/index.js';
3
+ import {
4
+ runCli,
5
+ saveDeploymentRecord,
6
+ readPreviousPlatformDeployment,
7
+ checkUrlHealth,
8
+ resolveBuildOutputPath,
9
+ } from './_shared.js';
10
+
11
+ /**
12
+ * @param {import('../../../core/config.js').DeployHubConfig} config
13
+ * @param {string} envName
14
+ * @param {Record<string, string>} [env]
15
+ */
16
+ export function createCloudflarePagesProvider(config, envName, env = process.env) {
17
+ const environment = config.environments[envName];
18
+ const log = createLogger('cloudflare-pages');
19
+ const apiToken = env.CLOUDFLARE_API_TOKEN;
20
+ const accountId = environment?.accountId || env.CLOUDFLARE_ACCOUNT_ID;
21
+ const projectName =
22
+ environment?.projectName || env.CF_PROJECT_NAME || config.project;
23
+
24
+ async function deploy(artifactDir) {
25
+ if (!apiToken || !accountId) {
26
+ throw new Error('CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are required');
27
+ }
28
+
29
+ const buildDir = resolveBuildOutputPath(config);
30
+ log.info(`Deploying to Cloudflare Pages project "${projectName}"...`);
31
+
32
+ const result = await runCli(
33
+ `wrangler pages deploy "${buildDir}" --project-name=${projectName} --branch=main`,
34
+ process.cwd(),
35
+ {
36
+ CLOUDFLARE_API_TOKEN: apiToken,
37
+ CLOUDFLARE_ACCOUNT_ID: accountId,
38
+ }
39
+ );
40
+
41
+ if (result.exitCode !== 0) {
42
+ throw new Error(`Cloudflare Pages deploy failed: ${result.stderr || result.stdout}`);
43
+ }
44
+
45
+ const urlMatch = (result.stdout || '').match(/https:\/\/[^\s]+\.pages\.dev/);
46
+ const deploymentUrl = urlMatch?.[0] || config.healthCheck?.url || '';
47
+
48
+ await saveDeploymentRecord(
49
+ artifactDir,
50
+ {
51
+ platform: 'cloudflare-pages',
52
+ deploymentUrl,
53
+ deploymentId: deploymentUrl,
54
+ projectName,
55
+ accountId,
56
+ },
57
+ envName
58
+ );
59
+
60
+ log.success(`Deployed to Cloudflare Pages: ${deploymentUrl || projectName}`);
61
+ }
62
+
63
+ async function rollback(artifactDir) {
64
+ if (!apiToken || !accountId) {
65
+ throw new Error('CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are required');
66
+ }
67
+
68
+ const previous = await readPreviousPlatformDeployment(artifactDir);
69
+ const deploymentId = previous?.deploymentId;
70
+ if (!deploymentId) {
71
+ throw new Error('No previous Cloudflare deployment ID found for rollback');
72
+ }
73
+
74
+ log.info('Rolling back Cloudflare Pages deployment...');
75
+ await axios.post(
76
+ `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/deployments/${deploymentId}/rollback`,
77
+ {},
78
+ { headers: { Authorization: `Bearer ${apiToken}` } }
79
+ );
80
+ log.success('Cloudflare Pages rollback complete');
81
+ }
82
+
83
+ async function healthCheck() {
84
+ const url = config.healthCheck?.url;
85
+ if (!url) return true;
86
+ return checkUrlHealth(url);
87
+ }
88
+
89
+ async function testConnection() {
90
+ if (!apiToken) throw new Error('CLOUDFLARE_API_TOKEN is required');
91
+ const response = await axios.get(
92
+ 'https://api.cloudflare.com/client/v4/user/tokens/verify',
93
+ { headers: { Authorization: `Bearer ${apiToken}` } }
94
+ );
95
+ if (!response.data?.success) {
96
+ throw new Error('Invalid CLOUDFLARE_API_TOKEN');
97
+ }
98
+ }
99
+
100
+ return { deploy, rollback, healthCheck, testConnection };
101
+ }
102
+
103
+ export default { createCloudflarePagesProvider };
@@ -0,0 +1,95 @@
1
+ import { createLogger } from '../../../logger/index.js';
2
+ import {
3
+ runCli,
4
+ saveDeploymentRecord,
5
+ checkUrlHealth,
6
+ } from './_shared.js';
7
+
8
+ /**
9
+ * @param {import('../../../core/config.js').DeployHubConfig} config
10
+ * @param {string} envName
11
+ * @param {Record<string, string>} [env]
12
+ */
13
+ export function createFirebaseAppHostingProvider(config, envName, env = process.env) {
14
+ const environment = config.environments[envName];
15
+ const log = createLogger('firebase-app-hosting');
16
+ const token = env.FIREBASE_TOKEN;
17
+ const projectId = environment?.projectId || env.FIREBASE_PROJECT_ID;
18
+ const backendName =
19
+ environment?.backendName || env.FIREBASE_APP_HOSTING_BACKEND || config.project;
20
+
21
+ async function deploy(artifactDir) {
22
+ if (!token || !projectId) {
23
+ throw new Error('FIREBASE_TOKEN and FIREBASE_PROJECT_ID are required');
24
+ }
25
+
26
+ log.info(`Deploying to Firebase App Hosting (${projectId}/${backendName})...`);
27
+
28
+ const createResult = await runCli(
29
+ `firebase apphosting:backends:list --project=${projectId} --token=${token} --non-interactive`,
30
+ process.cwd()
31
+ );
32
+
33
+ const backendExists =
34
+ createResult.exitCode === 0 && createResult.stdout.includes(backendName);
35
+
36
+ if (!backendExists) {
37
+ log.info(`Creating App Hosting backend "${backendName}"...`);
38
+ await runCli(
39
+ `firebase apphosting:backends:create --project=${projectId} --token=${token} --backend=${backendName} --non-interactive`,
40
+ process.cwd()
41
+ );
42
+ }
43
+
44
+ const result = await runCli(
45
+ `firebase deploy --only apphosting --token=${token} --project=${projectId} --non-interactive`,
46
+ process.cwd()
47
+ );
48
+
49
+ if (result.exitCode !== 0) {
50
+ throw new Error(`Firebase App Hosting deploy failed: ${result.stderr || result.stdout}`);
51
+ }
52
+
53
+ await saveDeploymentRecord(
54
+ artifactDir,
55
+ {
56
+ platform: 'firebase-app-hosting',
57
+ deploymentId: `apphosting-${Date.now()}`,
58
+ projectId,
59
+ backendName,
60
+ deploymentUrl: config.healthCheck?.url || '',
61
+ },
62
+ envName
63
+ );
64
+
65
+ log.success('Deployed to Firebase App Hosting');
66
+ }
67
+
68
+ async function rollback(artifactDir) {
69
+ log.info('Firebase App Hosting rollback — re-deploying previous artifact...');
70
+ await deploy(artifactDir);
71
+ }
72
+
73
+ async function healthCheck() {
74
+ const url = config.healthCheck?.url;
75
+ if (!url) return true;
76
+ return checkUrlHealth(url);
77
+ }
78
+
79
+ async function testConnection() {
80
+ if (!token || !projectId) {
81
+ throw new Error('FIREBASE_TOKEN and FIREBASE_PROJECT_ID are required');
82
+ }
83
+ const result = await runCli(
84
+ `firebase projects:list --token=${token} --non-interactive`,
85
+ process.cwd()
86
+ );
87
+ if (result.exitCode !== 0) {
88
+ throw new Error('Invalid FIREBASE_TOKEN');
89
+ }
90
+ }
91
+
92
+ return { deploy, rollback, healthCheck, testConnection };
93
+ }
94
+
95
+ export default { createFirebaseAppHostingProvider };
@@ -0,0 +1,99 @@
1
+ import { createLogger } from '../../../logger/index.js';
2
+ import { ensureFirebaseJson } from '../../../utils/firebase-config-generator.js';
3
+ import {
4
+ runCli,
5
+ saveDeploymentRecord,
6
+ readPreviousPlatformDeployment,
7
+ checkUrlHealth,
8
+ getBuildOutputDir,
9
+ } from './_shared.js';
10
+
11
+ /**
12
+ * @param {import('../../../core/config.js').DeployHubConfig} config
13
+ * @param {string} envName
14
+ * @param {Record<string, string>} [env]
15
+ */
16
+ export function createFirebaseHostingProvider(config, envName, env = process.env) {
17
+ const environment = config.environments[envName];
18
+ const log = createLogger('firebase-hosting');
19
+ const token = env.FIREBASE_TOKEN;
20
+ const projectId = environment?.projectId || env.FIREBASE_PROJECT_ID;
21
+
22
+ async function deploy(artifactDir) {
23
+ if (!token || !projectId) {
24
+ throw new Error('FIREBASE_TOKEN and FIREBASE_PROJECT_ID are required');
25
+ }
26
+
27
+ const cwd = process.cwd();
28
+ const buildOutput = getBuildOutputDir(config);
29
+ await ensureFirebaseJson(buildOutput, cwd);
30
+
31
+ log.info(`Deploying to Firebase Hosting (${projectId})...`);
32
+ const result = await runCli(
33
+ `firebase deploy --only hosting --token=${token} --project=${projectId} --non-interactive`,
34
+ cwd
35
+ );
36
+
37
+ if (result.exitCode !== 0) {
38
+ throw new Error(`Firebase deploy failed: ${result.stderr || result.stdout}`);
39
+ }
40
+
41
+ const urlMatch = (result.stdout || '').match(/Hosting URL:\s*(https:\/\/\S+)/i);
42
+ const deploymentUrl = urlMatch?.[1] || config.healthCheck?.url || '';
43
+
44
+ await saveDeploymentRecord(
45
+ artifactDir,
46
+ {
47
+ platform: 'firebase-hosting',
48
+ deploymentUrl,
49
+ deploymentId: deploymentUrl,
50
+ projectId,
51
+ },
52
+ envName
53
+ );
54
+
55
+ log.success(`Deployed to Firebase Hosting: ${deploymentUrl || projectId}`);
56
+ }
57
+
58
+ async function rollback(artifactDir) {
59
+ if (!token || !projectId) {
60
+ throw new Error('FIREBASE_TOKEN and FIREBASE_PROJECT_ID are required');
61
+ }
62
+
63
+ log.info('Rolling back Firebase Hosting...');
64
+ const result = await runCli(
65
+ `firebase hosting:rollback --token=${token} --project=${projectId} --non-interactive`,
66
+ process.cwd()
67
+ );
68
+ if (result.exitCode !== 0) {
69
+ throw new Error(`Firebase rollback failed: ${result.stderr || result.stdout}`);
70
+ }
71
+ log.success('Firebase Hosting rollback complete');
72
+ }
73
+
74
+ async function healthCheck() {
75
+ const url = config.healthCheck?.url;
76
+ if (!url) return true;
77
+ return checkUrlHealth(url);
78
+ }
79
+
80
+ async function testConnection() {
81
+ if (!token || !projectId) {
82
+ throw new Error('FIREBASE_TOKEN and FIREBASE_PROJECT_ID are required');
83
+ }
84
+ const result = await runCli(
85
+ `firebase projects:list --token=${token} --non-interactive`,
86
+ process.cwd()
87
+ );
88
+ if (result.exitCode !== 0) {
89
+ throw new Error('Invalid FIREBASE_TOKEN');
90
+ }
91
+ if (!result.stdout.includes(projectId)) {
92
+ throw new Error(`Firebase project "${projectId}" not accessible`);
93
+ }
94
+ }
95
+
96
+ return { deploy, rollback, healthCheck, testConnection };
97
+ }
98
+
99
+ export default { createFirebaseHostingProvider };
@@ -0,0 +1,44 @@
1
+ import { createVercelProvider } from './vercel.js';
2
+ import { createNetlifyProvider } from './netlify.js';
3
+ import { createCloudflarePagesProvider } from './cloudflare-pages.js';
4
+ import { createAwsAmplifyProvider } from './aws-amplify.js';
5
+ import { createAzureStaticWebAppsProvider } from './azure-static-web-apps.js';
6
+ import { createFirebaseHostingProvider } from './firebase-hosting.js';
7
+ import { createFirebaseAppHostingProvider } from './firebase-app-hosting.js';
8
+
9
+ /** @type {Record<string, Function>} */
10
+ const PLATFORM_FACTORIES = {
11
+ vercel: createVercelProvider,
12
+ netlify: createNetlifyProvider,
13
+ 'cloudflare-pages': createCloudflarePagesProvider,
14
+ 'aws-amplify': createAwsAmplifyProvider,
15
+ 'azure-static-web-apps': createAzureStaticWebAppsProvider,
16
+ 'firebase-hosting': createFirebaseHostingProvider,
17
+ 'firebase-app-hosting': createFirebaseAppHostingProvider,
18
+ };
19
+
20
+ /**
21
+ * @param {string} platform
22
+ * @param {import('../../../core/config.js').DeployHubConfig} config
23
+ * @param {string} envName
24
+ * @param {Record<string, string>} [env]
25
+ */
26
+ export function createPlatformProvider(platform, config, envName, env = process.env) {
27
+ const factory = PLATFORM_FACTORIES[platform];
28
+ if (!factory) {
29
+ throw new Error(`Unknown platform: ${platform}`);
30
+ }
31
+ return factory(config, envName, env);
32
+ }
33
+
34
+ export {
35
+ createVercelProvider,
36
+ createNetlifyProvider,
37
+ createCloudflarePagesProvider,
38
+ createAwsAmplifyProvider,
39
+ createAzureStaticWebAppsProvider,
40
+ createFirebaseHostingProvider,
41
+ createFirebaseAppHostingProvider,
42
+ };
43
+
44
+ export default { createPlatformProvider, PLATFORM_FACTORIES };
@@ -0,0 +1,102 @@
1
+ import axios from 'axios';
2
+ import { createLogger } from '../../../logger/index.js';
3
+ import {
4
+ runCli,
5
+ saveDeploymentRecord,
6
+ readPreviousPlatformDeployment,
7
+ checkUrlHealth,
8
+ resolveBuildOutputPath,
9
+ } from './_shared.js';
10
+
11
+ /**
12
+ * @param {import('../../../core/config.js').DeployHubConfig} config
13
+ * @param {string} envName
14
+ * @param {Record<string, string>} [env]
15
+ */
16
+ export function createNetlifyProvider(config, envName, env = process.env) {
17
+ const environment = config.environments[envName];
18
+ const log = createLogger('netlify');
19
+ const authToken = env.NETLIFY_AUTH_TOKEN;
20
+ const siteId = environment?.siteId || env.NETLIFY_SITE_ID;
21
+
22
+ async function deploy(artifactDir) {
23
+ if (!authToken || !siteId) {
24
+ throw new Error('NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID are required');
25
+ }
26
+
27
+ const buildDir = resolveBuildOutputPath(config);
28
+ log.info(`Deploying to Netlify from ${buildDir}...`);
29
+
30
+ const result = await runCli(
31
+ `netlify deploy --prod --dir="${buildDir}" --auth=${authToken} --site=${siteId}`,
32
+ process.cwd()
33
+ );
34
+
35
+ if (result.exitCode !== 0) {
36
+ throw new Error(`Netlify deploy failed: ${result.stderr || result.stdout}`);
37
+ }
38
+
39
+ const deployIdMatch = (result.stdout || '').match(/Deploy ID:\s*(\S+)/i);
40
+ const urlMatch = (result.stdout || '').match(/Website URL:\s*(https:\/\/\S+)/i);
41
+ const deployId = deployIdMatch?.[1] || '';
42
+ const deploymentUrl = urlMatch?.[1] || config.healthCheck?.url || '';
43
+
44
+ await saveDeploymentRecord(
45
+ artifactDir,
46
+ {
47
+ platform: 'netlify',
48
+ deploymentUrl,
49
+ deploymentId: deployId,
50
+ siteId,
51
+ },
52
+ envName
53
+ );
54
+
55
+ log.success(`Deployed to Netlify: ${deploymentUrl || siteId}`);
56
+ }
57
+
58
+ async function rollback(artifactDir) {
59
+ if (!authToken || !siteId) {
60
+ throw new Error('NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID are required');
61
+ }
62
+
63
+ const previous = await readPreviousPlatformDeployment(artifactDir);
64
+ const deployId = previous?.deploymentId;
65
+ if (!deployId) {
66
+ throw new Error('No previous Netlify deployment ID found for rollback');
67
+ }
68
+
69
+ log.info(`Restoring Netlify deploy ${deployId}...`);
70
+ await axios.post(
71
+ `https://api.netlify.com/api/v1/sites/${siteId}/deploys/${deployId}/restore`,
72
+ {},
73
+ { headers: { Authorization: `Bearer ${authToken}` } }
74
+ );
75
+ log.success('Netlify rollback complete');
76
+ }
77
+
78
+ async function healthCheck() {
79
+ const url = config.healthCheck?.url;
80
+ if (!url) return true;
81
+ return checkUrlHealth(url);
82
+ }
83
+
84
+ async function testConnection() {
85
+ if (!authToken) throw new Error('NETLIFY_AUTH_TOKEN is required');
86
+ const response = await axios.get('https://api.netlify.com/api/v1/user', {
87
+ headers: { Authorization: `Bearer ${authToken}` },
88
+ });
89
+ if (!response.data?.email) {
90
+ throw new Error('Invalid NETLIFY_AUTH_TOKEN');
91
+ }
92
+ if (siteId) {
93
+ await axios.get(`https://api.netlify.com/api/v1/sites/${siteId}`, {
94
+ headers: { Authorization: `Bearer ${authToken}` },
95
+ });
96
+ }
97
+ }
98
+
99
+ return { deploy, rollback, healthCheck, testConnection };
100
+ }
101
+
102
+ export default { createNetlifyProvider };