@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,33 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { listLocalArtifacts } from '../artifact/engine.js';
5
+
6
+ /**
7
+ * @param {import('commander').Command} program
8
+ */
9
+ export function registerLogsCommand(program) {
10
+ program
11
+ .command('logs')
12
+ .description('Show logs from the last deployment')
13
+ .action(async () => {
14
+ const artifacts = await listLocalArtifacts();
15
+ if (artifacts.length === 0) {
16
+ console.log(chalk.yellow('No deployment logs found.'));
17
+ return;
18
+ }
19
+
20
+ const latest = artifacts[0];
21
+ const logsPath = path.join(latest.path, 'logs.txt');
22
+
23
+ if (await fs.pathExists(logsPath)) {
24
+ const content = await fs.readFile(logsPath, 'utf-8');
25
+ console.log(chalk.bold(`\nDeployment logs (v${latest.version}):\n`));
26
+ console.log(content);
27
+ } else {
28
+ console.log(chalk.yellow('No logs.txt found in latest artifact.'));
29
+ }
30
+ });
31
+ }
32
+
33
+ export default { registerLogsCommand };
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig, loadEnv } from '../core/config.js';
3
+ import { rollbackToVersion } from '../rollback/engine.js';
4
+ import axios from 'axios';
5
+
6
+ /**
7
+ * @param {import('commander').Command} program
8
+ */
9
+ export function registerRollbackCommand(program) {
10
+ program
11
+ .command('rollback [version]')
12
+ .description('Rollback to a previous artifact version')
13
+ .action(async (version) => {
14
+ loadEnv();
15
+ const config = await loadConfig();
16
+
17
+ if (!version) {
18
+ const { listLocalArtifacts } = await import('../artifact/engine.js');
19
+ const artifacts = await listLocalArtifacts();
20
+ if (artifacts.length < 2) {
21
+ console.error(chalk.red('No previous version available for rollback'));
22
+ process.exit(1);
23
+ }
24
+ version = artifacts[1].version;
25
+ console.log(chalk.gray(`Rolling back to previous version: v${version}`));
26
+ }
27
+
28
+ await rollbackToVersion(config, version);
29
+
30
+ if (config.healthCheck?.url) {
31
+ try {
32
+ const response = await axios.get(config.healthCheck.url, {
33
+ timeout: (config.healthCheck.timeout || 30) * 1000,
34
+ validateStatus: () => true,
35
+ });
36
+ if (response.status >= 200 && response.status < 400) {
37
+ console.log(chalk.green(`Health check passed: HTTP ${response.status}`));
38
+ } else {
39
+ console.log(chalk.yellow(`Health check returned HTTP ${response.status}`));
40
+ }
41
+ } catch (err) {
42
+ console.log(chalk.yellow(`Health check failed: ${err instanceof Error ? err.message : String(err)}`));
43
+ }
44
+ }
45
+
46
+ console.log(chalk.green(`✓ Rolled back to v${version}`));
47
+ });
48
+ }
49
+
50
+ export default { registerRollbackCommand };
@@ -0,0 +1,116 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { appendEnv, loadEnv } from '../core/config.js';
4
+ import { testProvider, testAllProviders } from '../storage/index.js';
5
+
6
+ const PROVIDER_PROMPTS = {
7
+ aws: [
8
+ { key: 'AWS_ACCESS_KEY_ID', message: 'AWS Access Key ID:' },
9
+ { key: 'AWS_SECRET_ACCESS_KEY', message: 'AWS Secret Access Key:', type: 'password' },
10
+ { key: 'AWS_BUCKET', message: 'AWS Bucket name:' },
11
+ { key: 'AWS_REGION', message: 'AWS Region:', default: 'us-east-1' },
12
+ ],
13
+ azure: [
14
+ { key: 'AZURE_CONNECTION_STRING', message: 'Azure Connection String:', type: 'password' },
15
+ { key: 'AZURE_CONTAINER', message: 'Azure Container name:' },
16
+ ],
17
+ gcp: [
18
+ { key: 'GCP_PROJECT_ID', message: 'GCP Project ID:' },
19
+ { key: 'GCP_KEY_FILE', message: 'Path to GCP key file:' },
20
+ { key: 'GCP_BUCKET', message: 'GCP Bucket name:' },
21
+ ],
22
+ gdrive: [
23
+ { key: 'GDRIVE_CLIENT_ID', message: 'Google Drive Client ID:' },
24
+ { key: 'GDRIVE_CLIENT_SECRET', message: 'Google Drive Client Secret:', type: 'password' },
25
+ { key: 'GDRIVE_REFRESH_TOKEN', message: 'Google Drive Refresh Token:', type: 'password' },
26
+ { key: 'GDRIVE_FOLDER_ID', message: 'Google Drive Folder ID (optional):' },
27
+ ],
28
+ dropbox: [
29
+ { key: 'DROPBOX_ACCESS_TOKEN', message: 'Dropbox Access Token:', type: 'password' },
30
+ ],
31
+ ftp: [
32
+ { key: 'FTP_HOST', message: 'FTP Host:' },
33
+ { key: 'FTP_USER', message: 'FTP User:' },
34
+ { key: 'FTP_PASSWORD', message: 'FTP Password:', type: 'password' },
35
+ { key: 'FTP_PORT', message: 'FTP Port:', default: '21' },
36
+ ],
37
+ local: [],
38
+ };
39
+
40
+ /**
41
+ * @param {import('commander').Command} program
42
+ */
43
+ export function registerStorageCommand(program) {
44
+ const storage = program
45
+ .command('storage')
46
+ .description('Manage storage providers');
47
+
48
+ storage
49
+ .command('add <provider>')
50
+ .description('Add credentials for a storage provider')
51
+ .action(async (provider) => {
52
+ loadEnv();
53
+ const prompts = PROVIDER_PROMPTS[provider];
54
+ if (!prompts) {
55
+ console.error(chalk.red(`Unknown provider: ${provider}`));
56
+ console.log('Available: aws, azure, gcp, gdrive, dropbox, local, ftp');
57
+ process.exit(1);
58
+ }
59
+
60
+ if (prompts.length === 0) {
61
+ console.log(chalk.green('Local storage requires no credentials.'));
62
+ return;
63
+ }
64
+
65
+ const questions = prompts.map((p) => ({
66
+ type: p.type || 'input',
67
+ name: p.key,
68
+ message: p.message,
69
+ default: p.default,
70
+ }));
71
+
72
+ const answers = await inquirer.prompt(questions);
73
+ await appendEnv(answers);
74
+
75
+ for (const [key, value] of Object.entries(answers)) {
76
+ process.env[key] = value;
77
+ }
78
+
79
+ console.log('Testing connection...');
80
+ try {
81
+ await testProvider(provider);
82
+ console.log(chalk.green(`✓ ${provider} connected successfully`));
83
+ } catch (err) {
84
+ console.error(
85
+ chalk.red(`✗ Connection failed: ${err instanceof Error ? err.message : String(err)}`)
86
+ );
87
+ process.exit(1);
88
+ }
89
+ });
90
+
91
+ storage
92
+ .command('list')
93
+ .description('List configured storage providers and status')
94
+ .action(async () => {
95
+ loadEnv();
96
+ let config;
97
+ try {
98
+ const { loadConfig } = await import('../core/config.js');
99
+ config = await loadConfig();
100
+ } catch {
101
+ console.error(chalk.red('Run deployhub init first'));
102
+ process.exit(1);
103
+ }
104
+
105
+ const results = await testAllProviders(config.storage);
106
+ console.log(chalk.bold('\nStorage Providers:\n'));
107
+ for (const r of results) {
108
+ const icon = r.status === 'connected' ? chalk.green('✓') : chalk.red('✗');
109
+ const extra = r.error ? chalk.gray(` (${r.error})`) : '';
110
+ console.log(` ${icon} ${r.name}${extra}`);
111
+ }
112
+ console.log('');
113
+ });
114
+ }
115
+
116
+ export default { registerStorageCommand };
@@ -0,0 +1,63 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { execa } from 'execa';
4
+ import semver from 'semver';
5
+
6
+ const PACKAGE_NAME = 'deployhub';
7
+
8
+ /**
9
+ * @param {import('commander').Command} program
10
+ */
11
+ export function registerUpdateCommand(program) {
12
+ program
13
+ .command('update')
14
+ .description('Check for and install updates to DeployHub')
15
+ .action(async () => {
16
+ console.log(chalk.gray('Checking for updates...'));
17
+
18
+ let latest;
19
+ try {
20
+ const { stdout } = await execa('npm', ['view', PACKAGE_NAME, 'version'], {
21
+ stdio: 'pipe',
22
+ });
23
+ latest = stdout.trim();
24
+ } catch {
25
+ console.log(
26
+ chalk.yellow(
27
+ 'Could not check npm registry. DeployHub may not be published yet.'
28
+ )
29
+ );
30
+ return;
31
+ }
32
+
33
+ const current = '1.0.0';
34
+
35
+ if (!semver.gt(latest, current)) {
36
+ console.log(chalk.green(`✓ DeployHub is up to date (v${current})`));
37
+ return;
38
+ }
39
+
40
+ console.log(chalk.yellow(`Update available: v${current} → v${latest}`));
41
+
42
+ const { confirm } = await inquirer.prompt([
43
+ {
44
+ type: 'confirm',
45
+ name: 'confirm',
46
+ message: `Install deployhub@${latest}?`,
47
+ default: true,
48
+ },
49
+ ]);
50
+
51
+ if (!confirm) {
52
+ console.log(chalk.gray('Update cancelled.'));
53
+ return;
54
+ }
55
+
56
+ await execa('npm', ['install', '-g', `${PACKAGE_NAME}@${latest}`], {
57
+ stdio: 'inherit',
58
+ });
59
+ console.log(chalk.green(`✓ Updated to v${latest}`));
60
+ });
61
+ }
62
+
63
+ export default { registerUpdateCommand };
@@ -0,0 +1,55 @@
1
+ import chalk from 'chalk';
2
+ import axios from 'axios';
3
+ import { loadConfig, loadEnv } from '../core/config.js';
4
+
5
+ /**
6
+ * @param {import('commander').Command} program
7
+ */
8
+ export function registerVerifyCommand(program) {
9
+ program
10
+ .command('verify')
11
+ .description('Run health check on configured endpoint')
12
+ .action(async () => {
13
+ loadEnv();
14
+ const config = await loadConfig();
15
+ const url = config.healthCheck?.url;
16
+
17
+ if (!url) {
18
+ console.error(chalk.red('No health check URL configured in deployhub.config.json'));
19
+ process.exit(1);
20
+ }
21
+
22
+ const timeout = (config.healthCheck.timeout || 30) * 1000;
23
+ const start = Date.now();
24
+
25
+ try {
26
+ const response = await axios.get(url, {
27
+ timeout,
28
+ validateStatus: () => true,
29
+ });
30
+ const elapsed = Date.now() - start;
31
+ const ok = response.status >= 200 && response.status < 400;
32
+
33
+ if (ok) {
34
+ console.log(
35
+ chalk.green(`✓ Health check passed: HTTP ${response.status} (${elapsed}ms)`)
36
+ );
37
+ } else {
38
+ console.log(
39
+ chalk.red(`✗ Health check failed: HTTP ${response.status} (${elapsed}ms)`)
40
+ );
41
+ process.exit(1);
42
+ }
43
+ } catch (err) {
44
+ const elapsed = Date.now() - start;
45
+ console.error(
46
+ chalk.red(
47
+ `✗ Health check failed: ${err instanceof Error ? err.message : String(err)} (${elapsed}ms)`
48
+ )
49
+ );
50
+ process.exit(1);
51
+ }
52
+ });
53
+ }
54
+
55
+ export default { registerVerifyCommand };
@@ -0,0 +1,168 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { z } from 'zod';
4
+ import dotenv from 'dotenv';
5
+
6
+ const SideConfigSchema = z.object({
7
+ framework: z.string(),
8
+ language: z.string().optional(),
9
+ buildCommand: z.string().nullable().optional(),
10
+ startCommand: z.string().nullable().optional(),
11
+ buildOutput: z.string().optional(),
12
+ port: z.number().optional(),
13
+ });
14
+
15
+ const EnvironmentSchema = z.object({
16
+ type: z.string().optional(),
17
+ deploymentType: z.enum(['platform', 'server']).optional(),
18
+ platform: z.string().optional(),
19
+ projectName: z.string().optional(),
20
+ siteId: z.string().optional(),
21
+ accountId: z.string().optional(),
22
+ appId: z.string().optional(),
23
+ region: z.string().optional(),
24
+ githubConnected: z.boolean().optional(),
25
+ resourceName: z.string().optional(),
26
+ projectId: z.string().optional(),
27
+ backendName: z.string().optional(),
28
+ frontendDeploymentType: z.enum(['platform', 'server']).optional(),
29
+ backendDeploymentType: z.enum(['server']).optional(),
30
+ host: z.string().optional(),
31
+ user: z.string().optional(),
32
+ path: z.string().optional(),
33
+ deployPath: z.string().optional(),
34
+ keyPath: z.string().optional(),
35
+ appName: z.string().optional(),
36
+ framework: z.string().optional(),
37
+ port: z.number().optional(),
38
+ frontendDeployPath: z.string().optional(),
39
+ backendDeployPath: z.string().optional(),
40
+ });
41
+
42
+ const ConfigSchema = z.object({
43
+ project: z.string(),
44
+ version: z.string().optional(),
45
+ projectType: z.enum(['frontend', 'backend', 'both']).default('frontend'),
46
+ framework: z.string().optional(),
47
+ language: z.string().optional(),
48
+ buildCommand: z.string().nullable().optional(),
49
+ startCommand: z.string().nullable().optional(),
50
+ buildOutput: z.string().optional(),
51
+ port: z.number().optional(),
52
+ frontend: SideConfigSchema.optional(),
53
+ backend: SideConfigSchema.optional(),
54
+ docker: z.boolean().default(false),
55
+ artifact: z.boolean().default(true),
56
+ storage: z.array(z.string()).default(['local']),
57
+ deploy: z.array(z.string()).default([]),
58
+ environments: z.record(EnvironmentSchema).default({}),
59
+ healthCheck: z
60
+ .object({
61
+ url: z.string().default(''),
62
+ timeout: z.number().default(30),
63
+ })
64
+ .default({}),
65
+ notifications: z
66
+ .object({
67
+ slack: z.boolean().default(false),
68
+ email: z.boolean().default(false),
69
+ webhook: z.boolean().default(false),
70
+ })
71
+ .default({}),
72
+ pipeline: z
73
+ .object({
74
+ test: z.boolean().default(true),
75
+ docker: z.boolean().default(false),
76
+ deploy: z.boolean().default(false),
77
+ verify: z.boolean().default(true),
78
+ notify: z.boolean().default(false),
79
+ })
80
+ .default({}),
81
+ artifactRetention: z.number().default(10),
82
+ cli: z
83
+ .object({
84
+ source: z.string().default('npm:deployhub'),
85
+ })
86
+ .default({}),
87
+ });
88
+
89
+ /** @typedef {z.infer<typeof ConfigSchema>} DeployHubConfig */
90
+
91
+ const CONFIG_FILENAME = 'deployhub.config.json';
92
+
93
+ /**
94
+ * @param {string} [cwd]
95
+ * @returns {string}
96
+ */
97
+ export function getConfigPath(cwd = process.cwd()) {
98
+ return path.join(cwd, CONFIG_FILENAME);
99
+ }
100
+
101
+ /**
102
+ * @param {string} [cwd]
103
+ * @returns {Promise<DeployHubConfig>}
104
+ */
105
+ export async function loadConfig(cwd = process.cwd()) {
106
+ const configPath = getConfigPath(cwd);
107
+ if (!(await fs.pathExists(configPath))) {
108
+ throw new Error(
109
+ `Config not found at ${configPath}. Run "deployhub init" first.`
110
+ );
111
+ }
112
+ const raw = await fs.readJson(configPath);
113
+ const parsed = ConfigSchema.parse(raw);
114
+
115
+ if (!parsed.framework && parsed.projectType === 'frontend') {
116
+ parsed.framework = 'node';
117
+ }
118
+ if (parsed.buildCommand === undefined && parsed.projectType !== 'backend') {
119
+ parsed.buildCommand = 'npm run build';
120
+ }
121
+ if (!parsed.buildOutput) {
122
+ parsed.buildOutput = parsed.projectType === 'backend' ? '.' : 'dist';
123
+ }
124
+ if (!parsed.version) {
125
+ parsed.version = '0.0.0';
126
+ }
127
+
128
+ return parsed;
129
+ }
130
+
131
+ /**
132
+ * @param {DeployHubConfig} config
133
+ * @param {string} [cwd]
134
+ */
135
+ export async function saveConfig(config, cwd = process.cwd()) {
136
+ const configPath = getConfigPath(cwd);
137
+ await fs.writeJson(configPath, config, { spaces: 2 });
138
+ }
139
+
140
+ /**
141
+ * @param {string} [cwd]
142
+ */
143
+ export function loadEnv(cwd = process.cwd()) {
144
+ dotenv.config({ path: path.join(cwd, '.env') });
145
+ }
146
+
147
+ /**
148
+ * @param {Record<string, string>} vars
149
+ * @param {string} [cwd]
150
+ */
151
+ export async function appendEnv(vars, cwd = process.cwd()) {
152
+ const envPath = path.join(cwd, '.env');
153
+ const lines = [];
154
+ for (const [key, value] of Object.entries(vars)) {
155
+ if (value) {
156
+ lines.push(`${key}=${value}`);
157
+ }
158
+ }
159
+ if (lines.length === 0) return;
160
+
161
+ const existing = (await fs.pathExists(envPath))
162
+ ? await fs.readFile(envPath, 'utf-8')
163
+ : '';
164
+ const separator = existing && !existing.endsWith('\n') ? '\n' : '';
165
+ await fs.appendFile(envPath, `${separator}${lines.join('\n')}\n`);
166
+ }
167
+
168
+ export { ConfigSchema };
@@ -0,0 +1,77 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import { createLogger } from '../logger/index.js';
4
+
5
+ /**
6
+ * @typedef {Object} PipelineContext
7
+ * @property {import('./config.js').DeployHubConfig} config
8
+ * @property {string} cwd
9
+ * @property {Record<string, unknown>} state
10
+ */
11
+
12
+ /**
13
+ * @typedef {Object} PipelineStage
14
+ * @property {string} name
15
+ * @property {function(PipelineContext): Promise<void>} run
16
+ * @property {function(PipelineContext): boolean} [enabled]
17
+ */
18
+
19
+ /** @type {string[]} */
20
+ export const ALL_STAGES = [
21
+ 'detect',
22
+ 'install',
23
+ 'test',
24
+ 'build',
25
+ 'docker',
26
+ 'artifact',
27
+ 'storage',
28
+ 'deploy',
29
+ 'verify',
30
+ 'notify',
31
+ ];
32
+
33
+ /**
34
+ * @param {PipelineStage[]} stages
35
+ * @param {PipelineContext} context
36
+ */
37
+ export async function runPipeline(stages, context) {
38
+ const log = createLogger('pipeline');
39
+ /** @type {string[]} */
40
+ const completed = [];
41
+ /** @type {Error|null} */
42
+ let failure = null;
43
+
44
+ for (const stage of stages) {
45
+ if (stage.enabled && !stage.enabled(context)) {
46
+ log.info(`Skipping stage: ${stage.name} (disabled)`);
47
+ continue;
48
+ }
49
+
50
+ if (stage.name === 'deploy' && !completed.includes('storage')) {
51
+ throw new Error(
52
+ 'Deploy requires storage upload to complete first. Configure at least one storage provider.'
53
+ );
54
+ }
55
+
56
+ const spinner = ora({
57
+ text: `Running ${stage.name}...`,
58
+ color: 'cyan',
59
+ }).start();
60
+
61
+ try {
62
+ await stage.run(context);
63
+ spinner.succeed(chalk.green(`${stage.name} complete`));
64
+ completed.push(stage.name);
65
+ } catch (err) {
66
+ const message = err instanceof Error ? err.message : String(err);
67
+ spinner.fail(chalk.red(`${stage.name} failed: ${message}`));
68
+ failure = err instanceof Error ? err : new Error(message);
69
+ log.error(`Pipeline stopped at stage: ${stage.name}`);
70
+ break;
71
+ }
72
+ }
73
+
74
+ return { completed, failure };
75
+ }
76
+
77
+ export default { runPipeline, ALL_STAGES };