@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,210 @@
1
+ import { detectFramework } from '../detectors/index.js';
2
+ import { getAdapter } from '../adapters/index.js';
3
+ import { createArtifact, repackArtifactZip } from '../artifact/engine.js';
4
+ import { uploadToAll } from '../storage/index.js';
5
+ import { deployToAll } from '../deployment/index.js';
6
+ import { sendNotifications } from '../notifications/index.js';
7
+ import axios from 'axios';
8
+ import fs from 'fs-extra';
9
+ import path from 'path';
10
+ import { getProjectVersion } from '../utils/version.js';
11
+
12
+ /**
13
+ * @param {import('../core/config.js').DeployHubConfig} config
14
+ * @param {string} cwd
15
+ * @param {Record<string, unknown>} state
16
+ */
17
+ export function buildPipelineStages(config, cwd, state) {
18
+ /** @type {import('../core/pipeline.js').PipelineStage[]} */
19
+ const stages = [
20
+ {
21
+ name: 'detect',
22
+ async run(ctx) {
23
+ const detected = await detectFramework(ctx.cwd);
24
+ if (detected) {
25
+ if (!ctx.config.framework) ctx.config.framework = detected.framework;
26
+ if (!ctx.config.projectType && detected.projectType) {
27
+ ctx.config.projectType = detected.projectType;
28
+ }
29
+ if (!ctx.config.language && detected.language) {
30
+ ctx.config.language = detected.language;
31
+ }
32
+ if (ctx.config.buildCommand === undefined && detected.buildCommand !== undefined) {
33
+ ctx.config.buildCommand = detected.buildCommand;
34
+ }
35
+ if (!ctx.config.buildOutput && detected.buildOutput) {
36
+ ctx.config.buildOutput = detected.buildOutput;
37
+ }
38
+ if (!ctx.config.startCommand && detected.startCommand) {
39
+ ctx.config.startCommand = detected.startCommand;
40
+ }
41
+ if (!ctx.config.port && detected.port) {
42
+ ctx.config.port = detected.port;
43
+ }
44
+ }
45
+ ctx.state.framework = ctx.config.framework;
46
+ ctx.state.projectType = ctx.config.projectType || 'frontend';
47
+ },
48
+ },
49
+ {
50
+ name: 'install',
51
+ async run(ctx) {
52
+ const adapter = getAdapter(ctx.config.framework, ctx.config, ctx.cwd);
53
+ await adapter.install();
54
+ },
55
+ },
56
+ {
57
+ name: 'test',
58
+ enabled: (ctx) => ctx.config.pipeline.test === true,
59
+ async run(ctx) {
60
+ const adapter = getAdapter(ctx.config.framework, ctx.config, ctx.cwd);
61
+ await adapter.test();
62
+ },
63
+ },
64
+ {
65
+ name: 'build',
66
+ async run(ctx) {
67
+ if (ctx.config.projectType === 'both') {
68
+ if (ctx.config.frontend?.buildCommand) {
69
+ const frontendAdapter = getAdapter(
70
+ ctx.config.frontend.framework,
71
+ ctx.config,
72
+ ctx.cwd
73
+ );
74
+ const saved = ctx.config.buildCommand;
75
+ ctx.config.buildCommand = ctx.config.frontend.buildCommand;
76
+ await frontendAdapter.build();
77
+ ctx.config.buildCommand = saved;
78
+ }
79
+ if (ctx.config.backend?.buildCommand) {
80
+ const backendAdapter = getAdapter(
81
+ ctx.config.backend.framework,
82
+ ctx.config,
83
+ ctx.cwd
84
+ );
85
+ const saved = ctx.config.buildCommand;
86
+ ctx.config.buildCommand = ctx.config.backend.buildCommand;
87
+ await backendAdapter.build();
88
+ ctx.config.buildCommand = saved;
89
+ }
90
+ return;
91
+ }
92
+
93
+ const adapter = getAdapter(ctx.config.framework, ctx.config, ctx.cwd);
94
+ await adapter.build();
95
+ },
96
+ },
97
+ {
98
+ name: 'docker',
99
+ enabled: (ctx) =>
100
+ ctx.config.pipeline.docker === true && ctx.config.docker === true,
101
+ async run(ctx) {
102
+ const adapter = getAdapter(ctx.config.framework, ctx.config, ctx.cwd);
103
+ await adapter.docker();
104
+ },
105
+ },
106
+ {
107
+ name: 'artifact',
108
+ enabled: (ctx) => ctx.config.artifact !== false,
109
+ async run(ctx) {
110
+ ctx.config.version = await getProjectVersion(ctx.cwd);
111
+ const result = await createArtifact(
112
+ ctx.config,
113
+ /** @type {string[]} */ (ctx.state.deployedTargets || []),
114
+ ctx.cwd
115
+ );
116
+ ctx.state.artifactDir = result.artifactDir;
117
+ ctx.state.zipPath = result.zipPath;
118
+ },
119
+ },
120
+ {
121
+ name: 'storage',
122
+ enabled: (ctx) => {
123
+ const willDeploy =
124
+ ctx.config.pipeline.deploy === true && (ctx.config.deploy?.length || 0) > 0;
125
+ if (willDeploy && (!ctx.config.storage || ctx.config.storage.length === 0)) {
126
+ ctx.config.storage = ['local'];
127
+ }
128
+ return (ctx.config.storage?.length || 0) > 0;
129
+ },
130
+ async run(ctx) {
131
+ const zipPath = /** @type {string} */ (ctx.state.zipPath);
132
+ if (!zipPath) throw new Error('No artifact to upload');
133
+ await uploadToAll(ctx.config.storage, zipPath, ctx.config);
134
+ ctx.state.storageCompleted = true;
135
+ },
136
+ },
137
+ {
138
+ name: 'deploy',
139
+ enabled: (ctx) =>
140
+ ctx.config.pipeline.deploy === true && (ctx.config.deploy?.length || 0) > 0,
141
+ async run(ctx) {
142
+ if (!ctx.state.storageCompleted) {
143
+ throw new Error(
144
+ 'Storage upload must complete before deploy. Enable at least one storage provider.'
145
+ );
146
+ }
147
+ const artifactDir = /** @type {string} */ (ctx.state.artifactDir);
148
+ if (!artifactDir) throw new Error('No artifact to deploy');
149
+ const deployed = await deployToAll(ctx.config, artifactDir);
150
+ ctx.state.deployedTargets = deployed;
151
+
152
+ const deploymentPath = path.join(artifactDir, 'deployment.json');
153
+ if (await fs.pathExists(deploymentPath)) {
154
+ const data = await fs.readJson(deploymentPath);
155
+ const last = data.lastDeployment;
156
+ if (last?.deployUrl || last?.deploymentUrl) {
157
+ ctx.state.lastDeployUrl = last.deployUrl || last.deploymentUrl;
158
+ }
159
+ }
160
+
161
+ await repackArtifactZip(artifactDir);
162
+ const zipPath = /** @type {string} */ (ctx.state.zipPath);
163
+ if (zipPath) {
164
+ await uploadToAll(ctx.config.storage, zipPath, ctx.config);
165
+ }
166
+ },
167
+ },
168
+ {
169
+ name: 'verify',
170
+ enabled: (ctx) =>
171
+ ctx.config.pipeline.verify === true && !!ctx.config.healthCheck?.url,
172
+ async run(ctx) {
173
+ const url = ctx.config.healthCheck.url;
174
+ const timeout = (ctx.config.healthCheck.timeout || 30) * 1000;
175
+ const start = Date.now();
176
+ const response = await axios.get(url, {
177
+ timeout,
178
+ validateStatus: () => true,
179
+ });
180
+ const elapsed = Date.now() - start;
181
+ if (response.status < 200 || response.status >= 400) {
182
+ throw new Error(
183
+ `Health check failed: HTTP ${response.status} (${elapsed}ms)`
184
+ );
185
+ }
186
+ ctx.state.healthCheck = { status: response.status, elapsed };
187
+ },
188
+ },
189
+ {
190
+ name: 'notify',
191
+ enabled: (ctx) => ctx.config.pipeline.notify === true,
192
+ async run(ctx) {
193
+ const lastDeploy = ctx.state.lastDeployUrl;
194
+ await sendNotifications(ctx.config, {
195
+ success: !ctx.state.failure,
196
+ version: ctx.config.version,
197
+ message: ctx.state.failure
198
+ ? String(ctx.state.failure)
199
+ : 'Build and deploy completed',
200
+ deployUrl: typeof lastDeploy === 'string' ? lastDeploy : ctx.config.healthCheck?.url,
201
+ environment: process.env.DEPLOYHUB_ENV || 'production',
202
+ });
203
+ },
204
+ },
205
+ ];
206
+
207
+ return stages;
208
+ }
209
+
210
+ export default { buildPipelineStages };
@@ -0,0 +1,156 @@
1
+ import { createSshProvider } from './providers/ssh.js';
2
+ import { createDockerProvider } from './providers/docker.js';
3
+ import { createEc2Provider } from './providers/ec2.js';
4
+ import { createAzureVmProvider } from './providers/azure-vm.js';
5
+ import { createGcpVmProvider } from './providers/gcp-vm.js';
6
+ import { createKubernetesProvider } from './providers/kubernetes.js';
7
+ import { createPlatformProvider } from './providers/platforms/index.js';
8
+ import { createLogger } from '../logger/index.js';
9
+
10
+ /** @type {Record<string, Function>} */
11
+ const PROVIDER_FACTORIES = {
12
+ ssh: createSshProvider,
13
+ docker: createDockerProvider,
14
+ ec2: createEc2Provider,
15
+ 'azure-vm': createAzureVmProvider,
16
+ 'gcp-vm': createGcpVmProvider,
17
+ kubernetes: createKubernetesProvider,
18
+ };
19
+
20
+ /**
21
+ * @param {Record<string, unknown>} environment
22
+ * @returns {boolean}
23
+ */
24
+ function isPlatformDeployment(environment) {
25
+ return environment.deploymentType === 'platform';
26
+ }
27
+
28
+ /**
29
+ * @param {Record<string, unknown>} environment
30
+ * @returns {boolean}
31
+ */
32
+ function isHybridEnvironment(environment) {
33
+ return (
34
+ environment.frontendDeploymentType === 'platform' &&
35
+ (environment.backendDeploymentType === 'server' || !!environment.type)
36
+ );
37
+ }
38
+
39
+ /**
40
+ * @param {string} type
41
+ * @param {import('../core/config.js').DeployHubConfig} config
42
+ * @param {string} envName
43
+ * @param {Record<string, string>} [env]
44
+ */
45
+ export function getDeploymentProvider(type, config, envName, env = process.env) {
46
+ const environment = config.environments[envName];
47
+ if (!environment) {
48
+ throw new Error(`Environment "${envName}" not found in config`);
49
+ }
50
+
51
+ if (isPlatformDeployment(environment)) {
52
+ return createPlatformProvider(environment.platform, config, envName, env);
53
+ }
54
+
55
+ const providerType = type || environment.type;
56
+ const factory = PROVIDER_FACTORIES[providerType];
57
+ if (!factory) {
58
+ throw new Error(`Unknown deployment provider: ${providerType}`);
59
+ }
60
+ return factory(config, envName, env);
61
+ }
62
+
63
+ /**
64
+ * @param {import('../core/config.js').DeployHubConfig} config
65
+ * @param {string} artifactDir
66
+ * @param {string[]} [envNames]
67
+ */
68
+ export async function deployToAll(config, artifactDir, envNames) {
69
+ const log = createLogger('deploy');
70
+ const targets = envNames || config.deploy || [];
71
+
72
+ if (targets.length === 0) {
73
+ log.warn('No deployment targets configured, skipping');
74
+ return [];
75
+ }
76
+
77
+ const deployed = [];
78
+ for (const envName of targets) {
79
+ const envConfig = config.environments[envName];
80
+ if (!envConfig) {
81
+ throw new Error(`Environment "${envName}" not found in config`);
82
+ }
83
+
84
+ if (isHybridEnvironment(envConfig)) {
85
+ log.info(`Deploying hybrid target ${envName} (platform frontend + server backend)...`);
86
+
87
+ const platformProvider = createPlatformProvider(
88
+ envConfig.platform,
89
+ config,
90
+ envName
91
+ );
92
+ await platformProvider.deploy(artifactDir);
93
+ log.success(`Frontend deployed to ${envConfig.platform}`);
94
+
95
+ const serverProvider = getDeploymentProvider(envConfig.type, config, envName);
96
+ await serverProvider.deploy(artifactDir);
97
+ log.success(`Backend deployed via ${envConfig.type}`);
98
+
99
+ deployed.push(envName);
100
+ continue;
101
+ }
102
+
103
+ if (isPlatformDeployment(envConfig)) {
104
+ const provider = createPlatformProvider(envConfig.platform, config, envName);
105
+ log.info(`Deploying to ${envName} (${envConfig.platform})...`);
106
+ await provider.deploy(artifactDir);
107
+ deployed.push(envName);
108
+ log.success(`Deployed to ${envName}`);
109
+ continue;
110
+ }
111
+
112
+ const provider = getDeploymentProvider(envConfig.type, config, envName);
113
+ log.info(`Deploying to ${envName} (${envConfig.type})...`);
114
+ await provider.deploy(artifactDir);
115
+ deployed.push(envName);
116
+ log.success(`Deployed to ${envName}`);
117
+ }
118
+
119
+ return deployed;
120
+ }
121
+
122
+ /**
123
+ * @param {import('../core/config.js').DeployHubConfig} config
124
+ * @param {string} artifactDir
125
+ * @param {string[]} [envNames]
126
+ */
127
+ export async function rollbackAll(config, artifactDir, envNames) {
128
+ const targets = envNames || config.deploy || [];
129
+ for (const envName of targets) {
130
+ const envConfig = config.environments[envName];
131
+
132
+ if (isHybridEnvironment(envConfig)) {
133
+ const platformProvider = createPlatformProvider(
134
+ envConfig.platform,
135
+ config,
136
+ envName
137
+ );
138
+ await platformProvider.rollback(artifactDir);
139
+
140
+ const serverProvider = getDeploymentProvider(envConfig.type, config, envName);
141
+ await serverProvider.rollback(artifactDir);
142
+ continue;
143
+ }
144
+
145
+ if (isPlatformDeployment(envConfig)) {
146
+ const provider = createPlatformProvider(envConfig.platform, config, envName);
147
+ await provider.rollback(artifactDir);
148
+ continue;
149
+ }
150
+
151
+ const provider = getDeploymentProvider(envConfig.type, config, envName);
152
+ await provider.rollback(artifactDir);
153
+ }
154
+ }
155
+
156
+ export default { getDeploymentProvider, deployToAll, rollbackAll };
@@ -0,0 +1,7 @@
1
+ import { createSshProvider } from './ssh.js';
2
+
3
+ export function createAzureVmProvider(config, envName, env) {
4
+ return createSshProvider(config, envName, env);
5
+ }
6
+
7
+ export default { createAzureVmProvider };
@@ -0,0 +1,30 @@
1
+ import { execa } from 'execa';
2
+ import { createLogger } from '../../logger/index.js';
3
+
4
+ export function createDockerProvider(config, envName) {
5
+ const log = createLogger('docker');
6
+
7
+ async function deploy(artifactDir) {
8
+ log.info('Deploying via Docker...');
9
+ await execa('docker', ['compose', 'up', '-d', '--build'], {
10
+ cwd: artifactDir,
11
+ stdio: 'inherit',
12
+ });
13
+ }
14
+
15
+ async function rollback(artifactDir) {
16
+ await deploy(artifactDir);
17
+ }
18
+
19
+ async function healthCheck() {
20
+ return true;
21
+ }
22
+
23
+ async function testConnection() {
24
+ await execa('docker', ['info'], { stdio: 'pipe' });
25
+ }
26
+
27
+ return { deploy, rollback, healthCheck, testConnection };
28
+ }
29
+
30
+ export default { createDockerProvider };
@@ -0,0 +1,7 @@
1
+ import { createSshProvider } from './ssh.js';
2
+
3
+ export function createEc2Provider(config, envName, env) {
4
+ return createSshProvider(config, envName, env);
5
+ }
6
+
7
+ export default { createEc2Provider };
@@ -0,0 +1,7 @@
1
+ import { createSshProvider } from './ssh.js';
2
+
3
+ export function createGcpVmProvider(config, envName, env) {
4
+ return createSshProvider(config, envName, env);
5
+ }
6
+
7
+ export default { createGcpVmProvider };
@@ -0,0 +1,30 @@
1
+ import { execa } from 'execa';
2
+ import { createLogger } from '../../logger/index.js';
3
+
4
+ export function createKubernetesProvider(config, envName) {
5
+ const log = createLogger('kubernetes');
6
+
7
+ async function deploy(artifactDir) {
8
+ log.info('Deploying to Kubernetes...');
9
+ await execa('kubectl', ['apply', '-f', '.'], {
10
+ cwd: artifactDir,
11
+ stdio: 'inherit',
12
+ });
13
+ }
14
+
15
+ async function rollback(artifactDir) {
16
+ await deploy(artifactDir);
17
+ }
18
+
19
+ async function healthCheck() {
20
+ return true;
21
+ }
22
+
23
+ async function testConnection() {
24
+ await execa('kubectl', ['cluster-info'], { stdio: 'pipe' });
25
+ }
26
+
27
+ return { deploy, rollback, healthCheck, testConnection };
28
+ }
29
+
30
+ export default { createKubernetesProvider };
@@ -0,0 +1,167 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { execa } from 'execa';
4
+
5
+ /**
6
+ * @param {import('../../../core/config.js').DeployHubConfig} config
7
+ * @returns {string}
8
+ */
9
+ export function getBuildOutputDir(config) {
10
+ if (config.projectType === 'both' && config.frontend?.buildOutput) {
11
+ return config.frontend.buildOutput;
12
+ }
13
+ return config.buildOutput || 'dist';
14
+ }
15
+
16
+ /**
17
+ * @param {import('../../../core/config.js').DeployHubConfig} config
18
+ * @param {string} [cwd]
19
+ * @returns {string}
20
+ */
21
+ export function resolveBuildOutputPath(config, cwd = process.cwd()) {
22
+ return path.join(cwd, getBuildOutputDir(config));
23
+ }
24
+
25
+ /**
26
+ * @param {string} artifactDir
27
+ * @param {Record<string, unknown>} record
28
+ * @param {string} [environmentName]
29
+ */
30
+ export async function saveDeploymentRecord(artifactDir, record, environmentName = '') {
31
+ const deploymentPath = path.join(artifactDir, 'deployment.json');
32
+ let existing = { targets: [], deployedAt: new Date().toISOString(), deployments: [] };
33
+
34
+ if (await fs.pathExists(deploymentPath)) {
35
+ existing = await fs.readJson(deploymentPath);
36
+ }
37
+
38
+ const timestamp = new Date().toISOString();
39
+ const envName = record.environmentName || environmentName || record.envName || '';
40
+
41
+ /** @type {Record<string, unknown>} */
42
+ const entry = {
43
+ ...record,
44
+ platform: record.platform,
45
+ deployId: record.deployId || record.deploymentId || '',
46
+ deployUrl: record.deployUrl || record.deploymentUrl || '',
47
+ deploymentId: record.deploymentId || record.deployId || '',
48
+ deploymentUrl: record.deploymentUrl || record.deployUrl || '',
49
+ timestamp,
50
+ environmentName: envName,
51
+ };
52
+
53
+ const deployments = existing.deployments || existing.platformDeployments || [];
54
+ deployments.push(entry);
55
+
56
+ await fs.writeJson(
57
+ deploymentPath,
58
+ {
59
+ ...existing,
60
+ deployments,
61
+ platformDeployments: deployments,
62
+ lastDeployment: entry,
63
+ deployedAt: timestamp,
64
+ },
65
+ { spaces: 2 }
66
+ );
67
+ }
68
+
69
+ /**
70
+ * @param {string} artifactDir
71
+ * @returns {Promise<Record<string, unknown>|null>}
72
+ */
73
+ export async function readLastPlatformDeployment(artifactDir) {
74
+ const deploymentPath = path.join(artifactDir, 'deployment.json');
75
+ if (!(await fs.pathExists(deploymentPath))) return null;
76
+ const data = await fs.readJson(deploymentPath);
77
+ const deployments = data.deployments || data.platformDeployments || [];
78
+ return data.lastDeployment || deployments.at(-1) || null;
79
+ }
80
+
81
+ /**
82
+ * @param {string} artifactDir
83
+ * @returns {Promise<Record<string, unknown>|null>}
84
+ */
85
+ export async function readPreviousPlatformDeployment(artifactDir) {
86
+ const deploymentPath = path.join(artifactDir, 'deployment.json');
87
+ if (!(await fs.pathExists(deploymentPath))) return null;
88
+ const data = await fs.readJson(deploymentPath);
89
+ const deployments = data.deployments || data.platformDeployments || [];
90
+ if (deployments.length < 2) return null;
91
+ return deployments[deployments.length - 2];
92
+ }
93
+
94
+ /**
95
+ * @param {string} artifactDir
96
+ * @param {string} [environmentName]
97
+ * @returns {Promise<Record<string, unknown>|null>}
98
+ */
99
+ export async function readDeploymentForEnvironment(artifactDir, environmentName) {
100
+ const deploymentPath = path.join(artifactDir, 'deployment.json');
101
+ if (!(await fs.pathExists(deploymentPath))) return null;
102
+ const data = await fs.readJson(deploymentPath);
103
+ const deployments = data.deployments || data.platformDeployments || [];
104
+ if (environmentName) {
105
+ const match = deployments.filter((d) => d.environmentName === environmentName);
106
+ return match.at(-1) || null;
107
+ }
108
+ return data.lastDeployment || deployments.at(-1) || null;
109
+ }
110
+
111
+ /**
112
+ * @param {string} command
113
+ * @param {string} [cwd]
114
+ * @param {Record<string, string>} [env]
115
+ */
116
+ export async function runCli(command, cwd = process.cwd(), env = process.env) {
117
+ const [cmd, ...args] = command.split(' ');
118
+ const result = await execa(cmd, args, {
119
+ cwd,
120
+ env: { ...process.env, ...env },
121
+ shell: true,
122
+ reject: false,
123
+ });
124
+ return result;
125
+ }
126
+
127
+ /**
128
+ * @param {string} binary
129
+ * @returns {Promise<boolean>}
130
+ */
131
+ export async function isCliInstalled(binary) {
132
+ try {
133
+ const result = await execa(binary, ['--version'], { reject: false });
134
+ return result.exitCode === 0;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * @param {string} url
142
+ * @returns {Promise<boolean>}
143
+ */
144
+ export async function checkUrlHealth(url) {
145
+ try {
146
+ const axios = (await import('axios')).default;
147
+ const response = await axios.get(url, {
148
+ timeout: 30000,
149
+ validateStatus: () => true,
150
+ });
151
+ return response.status >= 200 && response.status < 400;
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ export default {
158
+ getBuildOutputDir,
159
+ resolveBuildOutputPath,
160
+ saveDeploymentRecord,
161
+ readLastPlatformDeployment,
162
+ readPreviousPlatformDeployment,
163
+ readDeploymentForEnvironment,
164
+ runCli,
165
+ isCliInstalled,
166
+ checkUrlHealth,
167
+ };