deploy-stack 0.12.3 → 0.14.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/README.md CHANGED
@@ -30,6 +30,7 @@ You retain complete ownership of your infrastructure code without relying on bla
30
30
  **🚀 Zero-Config Deployments**
31
31
  * **Framework Agnostic:** Tailored container presets for Next.js, Express.js, FastAPI, Go, Django, Rails, Nuxt 3, and Static Sites (React, Vue, SvelteKit, Astro).
32
32
  * **Smart Discovery:** Automatically detects build output directories and generates highly optimized, multi-stage Dockerfiles.
33
+ * **PaaS Migration Engine:** Natively parses Heroku `Procfile` configurations and `vercel.json` files to automatically translate proprietary edge routing (redirects/rewrites) and background workers into standard AWS Fargate and Application Load Balancer architectures.
33
34
  * **Database Scaffolding:** Automatically provisions fully isolated, zero-trust AWS RDS PostgreSQL databases for backend monoliths.
34
35
 
35
36
  **🛡️ DevSecOps & Security**
@@ -83,7 +84,7 @@ The interactive wizard will analyze your codebase, detect your framework, estima
83
84
  Strips all `deploy-stack` metadata and management tags from your project, leaving behind pure, standard Terraform and GitHub Actions files. You retain 100% ownership.
84
85
 
85
86
  * **`npx deploy-stack --headless`**
86
- Bypasses the interactive wizard for fully programmatic execution. Perfect for CI/CD pipelines, custom scripts, or AI agent integration. Accepts flags like `--framework=static`, `--region=us-east-1`, and `--size=micro`.
87
+ Bypasses the interactive wizard for fully programmatic execution. Perfect for CI/CD pipelines, custom scripts, or AI agent integration. Accepts flags like `--framework=static`, `--region=us-east-2`, and `--size=micro`.
87
88
 
88
89
  ---
89
90
 
@@ -123,6 +124,7 @@ your-project/
123
124
  * **[Ruby on Rails](https://github.com/anton-codes-iac/deploy-stack-rails-example):** A production Rails 7+ setup featuring an auto-provisioned PostgreSQL database and secure `.auto.tfvars` Master Key injection.
124
125
  * **[Nuxt 3 (SSR)](https://github.com/anton-codes-iac/deploy-stack-nuxt-example):** Demonstrates a fully server-side rendered Nuxt application using Nitro's optimized Node output.
125
126
  * **[Django / Python](https://github.com/anton-codes-iac/deploy-stack-django-example):** A secure Gunicorn/WSGI implementation with PostgreSQL and unprivileged container adapters.
127
+ * **[Heroku to AWS Migration (Django)](https://github.com/anton-codes-iac/deploy-stack-heroku-django-example):** A classic Heroku-style monolith migrated via the Procfile Importer, demonstrating a multi-container Web and Celery Worker architecture deployed from a single codebase.
126
128
  * **[Go / Fiber](https://github.com/anton-codes-iac/deploy-stack-go-example):** A distroless, compiled Go binary deployment demonstrating ultra-low memory footprints and instant boot times.
127
129
  ---
128
130
 
@@ -156,7 +158,7 @@ npx deploy-stack --no-telemetry
156
158
 
157
159
  ### Phase 6: Migration & Trust Engine (Current)
158
160
  - [x] **Dry-Run Visualization:** Interactive pre-flight terminal UI with ASCII topology maps and precise, dynamic AWS cost estimation.
159
- - [ ] **PaaS Importers:** Auto-parse `vercel.json` or Heroku `Procfile` configurations to map build commands and environment variables automatically.
161
+ - [x] **PaaS Importers:** Auto-parse `vercel.json` and Heroku `Procfile` configurations to map routing rules, web commands, and background workers automatically.
160
162
  - [ ] **Docker Compose to ECS Translator:** Automatically converting a familiar local `docker-compose.yml` into production ECS task definitions.
161
163
  - [ ] **AI Agent Rulesets:** Publishing `.cursorrules` and Copilot instructions that teach AI assistants exactly how to utilize the CLI on the user's behalf.
162
164
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploy-stack",
3
- "version": "0.12.3",
3
+ "version": "0.14.0",
4
4
  "description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -0,0 +1,14 @@
1
+ # 🚀 Vercel Escape Hatch, Orchestrator Refactor & Advanced Telemetry
2
+
3
+ This release introduces the second half of our PaaS Migration Engine, focusing on seamlessly escaping Vercel's proprietary edge network, alongside an internal architectural cleanup.
4
+
5
+ ### ✨ What's New
6
+ * **Vercel Edge Routing Migration:** `deploy-stack` now automatically parses `vercel.json` files. It natively translates Vercel edge redirects (301/302) and regex path matching directly into standard AWS Application Load Balancer Listener Rules.
7
+ * **Vendor Lock-In Detection:** The CLI now proactively analyzes framework configuration files (`next.config.mjs`, `svelte.config.js`, `astro.config.mjs`) during the pre-flight checks. It warns users if they are locked into Vercel-specific adapters and provides exact instructions on how to switch to standard Node/Standalone outputs for AWS containerization.
8
+
9
+ ### 🛠️ Architecture & Telemetry
10
+ * **Core Orchestrator Refactor:** Stripped over 250 lines of business logic and UI prompting out of `init.js`, establishing a clean, strictly isolated `src/utils/` toolbox pattern for future PaaS parsers.
11
+ * **Wide Telemetry Payloads:** Upgraded the analytics engine to capture detailed execution context (CLI version, headless status, desired task counts, and specific migration vectors like Heroku/Vercel) to better map the user deployment funnel.
12
+
13
+ ### 🐛 Bug Fixes
14
+ * **Next.js Standalone Enforcement:** Explicit warnings are now surfaced if a Next.js project is missing the critical `output: 'standalone'` directive before attempting to provision cloud resources.
@@ -5,6 +5,7 @@ import { intro, outro, spinner, log, cancel } from '@clack/prompts';
5
5
  import color from 'picocolors';
6
6
  import { renderDryRunPreview, parseTerraformConfig } from '../utils/visualizer.js';
7
7
  import { detectFramework } from '../utils/detector.js';
8
+ import { trackEvent, flushTelemetry } from '../core/telemetry.js';
8
9
 
9
10
  // Helper to run a command while piping the latest stdout line into a @clack spinner
10
11
  function runTerraformCommand(args, cwd, spin, loadingPrefix) {
@@ -111,7 +112,7 @@ export async function applyStack(options = {}) {
111
112
  // 6. Run terraform apply
112
113
  s.start('Provisioning AWS infrastructure (this may take 3–5 minutes)...');
113
114
  await runTerraformCommand(['apply', '-auto-approve'], tfDir, s, 'Provisioning');
114
- s.stop('AWS infrastructure provisioned successfully!');
115
+ s.stop('Cloud resources provisioned.');
115
116
 
116
117
  // 7. Extract and print the outputs
117
118
  const outputs = await getTerraformOutputs(tfDir);
@@ -125,6 +126,14 @@ export async function applyStack(options = {}) {
125
126
 
126
127
  outro(`${finalMessage}\n\n ${color.yellow('Push code to deploy your app and clear the 503 error:')}\n ${color.cyan('git add . && git commit -m "ci: infra" && git push origin main')}`);
127
128
 
129
+ const actualProjectName = path.basename(process.cwd());
130
+ trackEvent('infrastructure_applied', {
131
+ projectName: actualProjectName,
132
+ framework: detectedConfig.framework,
133
+ success: true
134
+ });
135
+ await flushTelemetry();
136
+
128
137
  process.exit(0);
129
138
 
130
139
  } catch (error) {
@@ -146,6 +155,15 @@ export async function applyStack(options = {}) {
146
155
  log.message(`${color.bold('To debug manually, navigate to your terraform folder:')}`);
147
156
  log.message(color.cyan('cd terraform && terraform apply'));
148
157
  }
158
+
159
+ const actualProjectName = path.basename(process.cwd());
160
+ trackEvent('infrastructure_applied', {
161
+ projectName: actualProjectName,
162
+ success: false,
163
+ error_code: error.code || 'UNKNOWN'
164
+ });
165
+ await flushTelemetry();
166
+
149
167
  process.exit(1);
150
168
  }
151
169
  }
@@ -77,7 +77,7 @@ export async function destroyStack() {
77
77
  const regionMatch = backendContent.match(/region\s*=\s*"([^"]+)"/);
78
78
 
79
79
  const bucketName = bucketMatch ? bucketMatch[1] : null;
80
- const region = regionMatch ? regionMatch[1] : 'us-east-1';
80
+ const region = regionMatch ? regionMatch[1] : 'us-east-2';
81
81
 
82
82
  // 2. Execute Terraform Destroy
83
83
  s.start('Destroying AWS compute resources (this takes a few minutes)...');
@@ -87,6 +87,15 @@ export async function destroyStack() {
87
87
  } catch (error) {
88
88
  s.stop(color.red('❌ Terraform destroy failed.'));
89
89
  console.error(color.red(error.message));
90
+
91
+ const actualProjectName = path.basename(process.cwd());
92
+ trackEvent('infrastructure_destroyed', {
93
+ projectName: actualProjectName,
94
+ success: false,
95
+ error_code: error.code || 'UNKNOWN'
96
+ });
97
+ await flushTelemetry();
98
+
90
99
  process.exit(1);
91
100
  }
92
101
 
@@ -111,9 +120,11 @@ export async function destroyStack() {
111
120
  }
112
121
  }
113
122
 
114
- trackEvent('project_destroyed', {
123
+ const actualProjectName = path.basename(process.cwd());
124
+ trackEvent('infrastructure_destroyed', {
125
+ projectName: actualProjectName,
115
126
  region,
116
- bucket: bucketName,
127
+ retained_state_bucket: !(deleteS3Bucket && typeof deleteS3Bucket !== 'symbol'),
117
128
  success: true
118
129
  });
119
130
  await flushTelemetry();
@@ -1,6 +1,7 @@
1
1
  import { intro, outro, spinner } from '@clack/prompts';
2
2
  import color from 'picocolors';
3
3
  import { checkDependency } from '../utils/system.js';
4
+ import { trackEvent, flushTelemetry } from '../core/telemetry.js';
4
5
 
5
6
  export async function runDoctor() {
6
7
  intro(color.bgCyan(color.black(' deploy-stack ☁️ ')));
@@ -36,4 +37,9 @@ export async function runDoctor() {
36
37
  } else {
37
38
  outro(color.yellow('Please install the missing dependencies before running the provisioning tool.'));
38
39
  }
40
+
41
+ trackEvent('doctor_run', {
42
+ success: hasTerraform && hasAws && hasDocker && hasGit
43
+ });
44
+ await flushTelemetry();
39
45
  }
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { intro, outro, confirm, spinner, cancel } from '@clack/prompts';
4
4
  import color from 'picocolors';
5
+ import { trackEvent, flushTelemetry } from '../core/telemetry.js';
5
6
 
6
7
  export async function ejectStack() {
7
8
  intro(color.bgRed(color.white(' deploy-stack eject ⏏️ ')));
@@ -68,6 +69,12 @@ export async function ejectStack() {
68
69
 
69
70
  s.stop('Ejection complete.');
70
71
 
72
+ const actualProjectName = path.basename(process.cwd());
73
+ trackEvent('project_ejected', {
74
+ projectName: actualProjectName
75
+ });
76
+ await flushTelemetry();
77
+
71
78
  outro(`
72
79
  ${color.green('✅ Successfully ejected from deploy-stack!')}
73
80
 
@@ -1,23 +1,33 @@
1
1
  import fsSync from 'fs';
2
- import fs from 'fs/promises';
3
2
  import path from 'path';
4
- import { intro, outro, group, text, select, spinner, cancel, confirm, log } from '@clack/prompts';
3
+ import { intro, outro, spinner, log } from '@clack/prompts';
5
4
  import color from 'picocolors';
6
- import { execSync } from 'child_process';
7
5
 
8
6
  import { checkDependency } from '../utils/system.js';
9
- import { detectFramework } from '../utils/detector.js';
7
+ import {
8
+ detectFramework,
9
+ parseProcfile,
10
+ parseVercelConfig,
11
+ analyzeNextConfig,
12
+ analyzeSvelteConfig,
13
+ analyzeAstroConfig
14
+ } from '../utils/detector.js';
10
15
  import { trackEvent, flushTelemetry } from '../core/telemetry.js';
11
16
  import { getFrameworkWarning } from '../utils/warnings.js';
12
17
  import { provisionStateBucket } from '../utils/aws.js';
13
18
  import { generateTemplates } from '../utils/generator.js';
14
19
  import { handleExistingFiles } from '../utils/backup.js';
15
20
  import { estimateMonthlyCost } from '../utils/visualizer.js';
21
+ import { getTargetDirectory, getProjectConfig } from '../utils/prompts.js';
22
+ import { resolveDjangoWsgi, handleRailsCI } from '../utils/frameworks.js';
23
+
24
+ const pkg = JSON.parse(fsSync.readFileSync(new URL('../../package.json', import.meta.url)));
25
+ const CLI_VERSION = pkg.version;
16
26
 
17
27
  export async function mainStack({ isHeadless = false, headlessOptions = {} } = {}) {
18
28
  const startTime = Date.now();
19
29
 
20
- // 0. Silent Pre-flight check
30
+ // 1. Silent Pre-flight check
21
31
  const hasTerraform = await checkDependency('terraform');
22
32
  if (!hasTerraform) {
23
33
  console.error(color.red('✖ Terraform is not installed.'));
@@ -25,260 +35,65 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
25
35
  process.exit(1);
26
36
  }
27
37
 
28
- const getFlag = (key, defaultValue) => headlessOptions[key] !== undefined ? headlessOptions[key] : defaultValue;
29
-
30
- let projectName, actualProjectName, targetDir;
31
- let finalFramework, detectedFramework;
32
- let djangoWsgi = 'core.wsgi';
33
- let setupType = 'quick';
34
- let needsDatabase = false;
35
- let disableDefaultCI = false;
36
- let project = {};
37
- let currentGitBranch = 'main';
38
-
39
- if (isHeadless) {
40
- // --- HEADLESS MODE ---
41
- projectName = getFlag('dir', '.');
42
- actualProjectName = projectName === '.' ? path.basename(process.cwd()) : projectName;
43
- targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
44
-
45
- detectedFramework = detectFramework(targetDir);
46
- finalFramework = getFlag('framework', detectedFramework ? detectedFramework.id : 'static');
47
-
48
- project = {
49
- region: getFlag('region', 'us-east-1'),
50
- port: getFlag('port', finalFramework === 'static' ? '8080' : '3000'),
51
- size: getFlag('size', 'micro'),
52
- healthCheckPath: getFlag('healthCheckPath', '/'),
53
- desiredCount: getFlag('desiredCount', '1'),
54
- branch: getFlag('branch', 'main')
55
- };
38
+ if (!isHeadless) intro(color.bgCyan(color.black(' deploy-stack ☁️ ')));
56
39
 
57
- console.log(color.cyan(`🤖 Running deploy-stack in headless mode [${finalFramework} -> ${project.region}]`));
58
- } else {
59
- // --- INTERACTIVE MODE ---
60
- // 1. Start the CLI
61
- intro(color.bgCyan(color.black(' deploy-stack ☁️ ')));
40
+ // 2. Resolve Target & Scan Codebase
41
+ const dirConfig = await getTargetDirectory(isHeadless, headlessOptions);
42
+ const detectedFramework = detectFramework(dirConfig.targetDir);
43
+ const procfile = parseProcfile(dirConfig.targetDir);
44
+ const vercelRules = parseVercelConfig(dirConfig.targetDir);
62
45
 
63
- // 2. Ask for the target directory FIRST
64
- projectName = await text({
65
- message: 'Where should we generate the infrastructure? (Type "." for current directory)',
66
- placeholder: '.',
67
- initialValue: '.',
68
- validate: (value) => {
69
- if (!value) return 'Please enter a name or directory.';
70
- if (value !== '.' && value.includes(' ')) return 'Name cannot contain spaces.';
71
- },
72
- });
73
-
74
- if (typeof projectName === 'symbol') {
75
- cancel('Operation cancelled.');
76
- process.exit(0);
77
- }
78
-
79
- actualProjectName = projectName === '.' ? path.basename(process.cwd()) : projectName;
80
- targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
81
-
82
- // 2.5 Run the scanner
83
- const detectedFramework = detectFramework(targetDir);
84
- if (detectedFramework) {
85
- log.success(`Auto-detected framework: ${detectedFramework.name}`);
86
- }
46
+ if (!isHeadless) {
47
+ if (detectedFramework) log.success(`Auto-detected framework: ${detectedFramework.name}`);
48
+ if (procfile && procfile.web) log.success(`Auto-detected Procfile (web command: ${procfile.web.join(' ')})`);
49
+ if (vercelRules) log.success(`Auto-detected vercel.json (Migrating edge network rules)`);
50
+ }
87
51
 
88
- // 2.6 Resolve the framework
89
- finalFramework = detectedFramework ? detectedFramework.id : null;
52
+ if (isHeadless) console.log(color.cyan(`🤖 Running deploy-stack in headless mode`));
90
53
 
91
- if (!finalFramework) {
92
- finalFramework = await select({
93
- message: 'Which framework preset should we configure?',
94
- options: [
95
- { value: 'node', label: 'Node.js / Express' },
96
- { value: 'nextjs', label: 'Next.js (Standalone)' },
97
- { value: 'nuxt', label: 'Nuxt 3 (SSR)' },
98
- { value: 'python', label: 'Python FastAPI' },
99
- { value: 'django', label: 'Django (Python)' },
100
- { value: 'rails', label: 'Ruby on Rails' },
101
- { value: 'go', label: 'Go (Golang)' },
102
- { value: 'static', label: 'Static Site (Gatsby, React, plain HTML via Nginx)' },
103
- ],
104
- });
54
+ // 3. Gather Configuration & Framework Quirks
55
+ const config = await getProjectConfig(isHeadless, headlessOptions, dirConfig.targetDir, detectedFramework);
56
+ const djangoWsgi = await resolveDjangoWsgi(dirConfig.targetDir, procfile, config.framework, isHeadless);
57
+ const disableDefaultCI = await handleRailsCI(dirConfig.targetDir, config.framework, isHeadless);
105
58
 
106
- if (typeof finalFramework === 'symbol') {
107
- cancel('Operation cancelled.');
108
- process.exit(0);
59
+ // 4. Framework Migration Checks (Vercel Escape Hatch)
60
+ if (!isHeadless) {
61
+ if (config.framework === 'nextjs') {
62
+ const nextConfig = analyzeNextConfig(dirConfig.targetDir);
63
+ if (nextConfig.hasConfig && !nextConfig.isStandalone) {
64
+ log.warn(color.yellow('⚠️ Next.js config is missing "output: \'standalone\'". Your CI/CD Docker build will crash until you add it!'));
109
65
  }
110
- }
111
-
112
- // --- DJANGO SPECIFIC PROMPT ---
113
- djangoWsgi = 'core.wsgi';
114
- if (finalFramework === 'django') {
115
- djangoWsgi = await text({
116
- message: 'What is the Python module path to your Django wsgi.py?',
117
- placeholder: 'core.wsgi',
118
- initialValue: 'core.wsgi',
119
- });
120
- if (typeof djangoWsgi === 'symbol') process.exit(0);
121
- }
122
-
123
- // 3. Prompt for Setup Mode
124
- setupType = await select({
125
- message: 'Choose your setup mode:',
126
- options: [
127
- { value: 'quick', label: '⚡ Quickstart (Recommended)', hint: 'Production defaults, minimal prompts' },
128
- { value: 'advanced', label: '🛠️ Advanced Configuration', hint: 'Customize health checks, task count, branch, etc.' },
129
- ],
130
- });
131
-
132
- if (typeof setupType === 'symbol') {
133
- cancel('Operation cancelled.');
134
- process.exit(0);
135
- }
136
-
137
- // 4. Set intelligent defaults & check current Git branch
138
- let defaultPort = '3000';
139
-
140
- if (finalFramework === 'static') defaultPort = '8080';
141
- if (finalFramework === 'python' || finalFramework === 'django') defaultPort = '8000';
142
- if (finalFramework === 'rails') defaultPort = '3000';
143
- if (finalFramework === 'go') defaultPort = '8080';
144
-
145
- currentGitBranch = 'main';
146
- try {
147
- currentGitBranch = execSync('git symbolic-ref --short HEAD', { cwd: targetDir, stdio: 'pipe' }).toString().trim();
148
- } catch (e) {
149
- // Not a git repo yet, fallback to 'main'
150
- }
151
-
152
- // 5. Ask for Managed Database (Only for Backend/Fullstack Frameworks)
153
- needsDatabase = false;
154
- const isBackendFramework = ['node', 'nextjs', 'nuxt', 'python', 'django', 'rails', 'go'].includes(finalFramework);
155
-
156
- if (isBackendFramework) {
157
- const dbChoice = await confirm({
158
- message: 'Do you need a managed AWS RDS PostgreSQL database? (Adds ~$14/month or uses AWS Free Tier)',
159
- initialValue: false,
160
- });
161
-
162
- if (typeof dbChoice === 'symbol') {
163
- cancel('Provisioning cancelled.')
164
- process.exit(0);
66
+ } else if (detectedFramework?.name === 'SvelteKit') {
67
+ const svelteConfig = analyzeSvelteConfig(dirConfig.targetDir);
68
+ if (svelteConfig.adapter === 'vercel' || svelteConfig.adapter === 'auto') {
69
+ log.warn(color.yellow('⚠️ SvelteKit is locked into the Vercel/Auto adapter. Switch to @sveltejs/adapter-node (for SSR) to deploy on AWS.'));
165
70
  }
166
- needsDatabase = dbChoice;
167
- }
168
-
169
- // 6. Prompt Configuration Group
170
- project = await group(
171
- {
172
- region: () =>
173
- select({
174
- message: 'Which AWS region do you want to deploy to?',
175
- options: [
176
- { value: 'us-east-1', label: 'us-east-1 (N. Virginia)' },
177
- { value: 'us-east-2', label: 'us-east-2 (Ohio)' },
178
- { value: 'eu-west-1', label: 'eu-west-1 (Ireland)' },
179
- { value: 'eu-central-1', label: 'EU (Frankfurt)' },
180
- { value: 'ap-southeast-2', label: 'Asia Pacific (Sydney)' },
181
- ],
182
- }),
183
- port: () =>
184
- text({
185
- message: 'What port does your container expose?',
186
- placeholder: defaultPort,
187
- defaultValue: defaultPort,
188
- }),
189
- size: () =>
190
- select({
191
- message: 'Select your Fargate compute size:',
192
- options: [
193
- { value: 'micro', label: 'Micro (0.25 vCPU, 512MB RAM) - Best for POCs' },
194
- { value: 'small', label: 'Small (0.5 vCPU, 1GB RAM) - Best for small Projects' },
195
- ],
196
- }),
197
- // --- Advanced-Only Prompts (Skipped if setupType === 'quick') ---
198
- healthCheckPath: () => {
199
- if (setupType === 'quick') return undefined;
200
- return text({
201
- message: 'ALB Health Check Path:',
202
- placeholder: '/',
203
- defaultValue: '/',
204
- });
205
- },
206
- desiredCount: () => {
207
- if (setupType === 'quick') return undefined;
208
- return select({
209
- message: 'How many container replicas (tasks) should run?',
210
- options: [
211
- { value: '1', label: '1 Task (Single instance - lowest cost)' },
212
- { value: '2', label: '2 Tasks (High Availability across AZs)' },
213
- ],
214
- defaultValue: '1',
215
- });
216
- },
217
- branch: () => {
218
- if (setupType === 'quick') return undefined;
219
- return text({
220
- message: 'Primary Git deployment branch for CI/CD:',
221
- placeholder: currentGitBranch,
222
- defaultValue: currentGitBranch,
223
- });
224
- },
225
- },
226
- {
227
- onCancel: () => {
228
- cancel('Provisioning cancelled.');
229
- process.exit(0);
230
- },
71
+ } else if (detectedFramework?.name === 'Astro') {
72
+ const astroConfig = analyzeAstroConfig(dirConfig.targetDir);
73
+ if (astroConfig.adapter === 'vercel') {
74
+ log.warn(color.yellow('⚠️ Astro is locked into the Vercel adapter. Switch to @astrojs/node (for SSR) to deploy on AWS.'));
231
75
  }
232
- );
76
+ }
233
77
  }
234
78
 
235
- // 7. Map the user's choices and update the variables
236
- const cpu = project.size === 'small' ? '512' : '256';
237
- const memory = project.size === 'small' ? '1024' : '512';
238
- const computeTier = project.size === 'small' ? 'Small (0.5 vCPU, 1GB RAM)' : 'Micro (0.25 vCPU, 512MB RAM)';
239
-
240
- const costs = estimateMonthlyCost({ cpu: parseInt(cpu), memory: parseInt(memory), hasDb: needsDatabase });
241
- const estimatedCost = `~$${costs.totalMonthly} / month${needsDatabase ? ' (Includes Fargate + RDS PostgreSQL)' : ''}`;
79
+ // 5. Calculate Derived Values
80
+ const cpu = config.size === 'small' ? '512' : '256';
81
+ const memory = config.size === 'small' ? '1024' : '512';
82
+ const computeTier = config.size === 'small' ? 'Small (0.5 vCPU, 1GB RAM)' : 'Micro (0.25 vCPU, 512MB RAM)';
242
83
 
243
- const healthCheckPath = project.healthCheckPath || '/';
244
- const desiredCount = project.desiredCount || '1';
245
- const deployBranch = project.branch || currentGitBranch;
84
+ const costs = estimateMonthlyCost({ cpu: parseInt(cpu), memory: parseInt(memory), hasDb: config.needsDatabase });
85
+ const estimatedCost = `~$${costs.totalMonthly} / month${config.needsDatabase ? ' (Includes Fargate + RDS PostgreSQL)' : ''}`;
246
86
  const buildDir = detectedFramework?.buildDir || 'dist';
247
87
 
248
- // 7.1 Check for conflicting CI boilerplate (Rails)
249
- if (finalFramework === 'rails') {
250
- const ciPath = path.join(targetDir, '.github', 'workflows', 'ci.yml');
251
- const dependabotPath = path.join(targetDir, '.github', 'dependabot.yml');
252
-
253
- if (fsSync.existsSync(ciPath) || fsSync.existsSync(dependabotPath)) {
254
- if (!isHeadless) {
255
- console.log('');
256
- const ciContent = await confirm({
257
- message: color.yellow('We detected default Rails GitHub Actions (ci.yml, dependabot.yml) that usually crash in isolated CI environments without a database. Would you like deploy-stack to safely disable them by renaming them to .bak?'),
258
- initialValue: true,
259
- });
260
- if (typeof ciContent === 'symbol') {
261
- cancel('Provisioning cancelled.');
262
- process.exit(0);
263
- }
264
- disableDefaultCI = ciContent;
265
- } else {
266
- // Headless: automatically disable conflicting CI
267
- disableDefaultCI = true;
268
- }
269
- }
270
- }
271
-
272
- // 8. Safely handle existing files (Backup and auto-prune)
273
- await handleExistingFiles(targetDir, isHeadless);
88
+ // 6. Handle Backups & Provision Remote State
89
+ await handleExistingFiles(dirConfig.targetDir, isHeadless);
274
90
 
275
91
  const s = spinner();
276
92
  s.start('Provisioning infrastructure...');
277
93
 
278
- // 9. Provision S3 bucket for Terraform state & enable versioning
279
94
  let awsAccountId, stateBucketName;
280
95
  try {
281
- const bucketData = await provisionStateBucket(project.region, actualProjectName);
96
+ const bucketData = await provisionStateBucket(config.region, dirConfig.actualProjectName);
282
97
  awsAccountId = bucketData.awsAccountId;
283
98
  stateBucketName = bucketData.stateBucketName;
284
99
  } catch (error) {
@@ -289,69 +104,75 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
289
104
  process.exit(1);
290
105
  }
291
106
 
107
+ // 7. Synthesize Templates
292
108
  s.message('Synthesizing Terraform templates...');
293
-
294
- // 10. Generate all templates and directories
295
- await generateTemplates(targetDir, {
296
- PROJECT_NAME: actualProjectName,
297
- REGION: project.region,
298
- PORT: project.port,
109
+ await generateTemplates(dirConfig.targetDir, {
110
+ PROJECT_NAME: dirConfig.actualProjectName,
111
+ REGION: config.region,
112
+ PORT: config.port,
299
113
  CPU: cpu,
300
114
  MEMORY: memory,
301
115
  COMPUTE_TIER: computeTier,
302
116
  ESTIMATED_COST: estimatedCost,
303
117
  STATE_BUCKET: stateBucketName,
304
118
  AWS_ACCOUNT_ID: awsAccountId,
305
- HEALTH_CHECK_PATH: healthCheckPath,
306
- DESIRED_COUNT: desiredCount,
307
- DEPLOY_BRANCH: deployBranch,
119
+ HEALTH_CHECK_PATH: config.healthCheckPath,
120
+ DESIRED_COUNT: config.desiredCount,
121
+ DEPLOY_BRANCH: config.branch,
308
122
  BUILD_DIR: buildDir,
309
- finalFramework: finalFramework,
310
- NEEDS_DATABASE: needsDatabase,
123
+ finalFramework: config.framework,
124
+ NEEDS_DATABASE: config.needsDatabase,
311
125
  DJANGO_WSGI: djangoWsgi,
312
- DISABLE_DEFAULT_CI: disableDefaultCI
126
+ DISABLE_DEFAULT_CI: disableDefaultCI,
127
+ PROCFILE: procfile,
128
+ VERCEL_RULES: vercelRules
313
129
  });
314
130
 
315
- // 11. Track the event in telemetry
131
+ // 8. Telemetry
316
132
  trackEvent('project_provisioned', {
317
- projectName: actualProjectName,
318
- framework: finalFramework,
319
- specific_framework: detectedFramework?.name || finalFramework,
320
- region: project.region,
321
- size: project.size,
322
- setup_mode: setupType,
323
- desiredCount: desiredCount,
324
- duration_ms: Date.now() - startTime
133
+ // 1. Core & Context
134
+ projectName: dirConfig.actualProjectName,
135
+ cli_version: CLI_VERSION,
136
+ is_headless: isHeadless,
137
+ setup_mode: config.setupType,
138
+ duration_ms: Date.now() - startTime,
139
+
140
+ // 2. Infrastructure Shape
141
+ framework: config.framework,
142
+ specific_framework: detectedFramework?.name || config.framework,
143
+ region: config.region,
144
+ size: config.size,
145
+ desired_count: parseInt(config.desiredCount),
146
+ has_database: config.needsDatabase,
147
+ has_custom_health_check: config.healthCheckPath !== '/',
148
+
149
+ // 3. Advanced Features & PaaS Context
150
+ has_worker: !!(procfile && procfile.worker),
151
+ is_heroku_migration: !!procfile,
152
+ is_vercel_migration: !!vercelRules,
325
153
  });
326
154
 
327
155
  s.stop('Infrastructure provisioned successfully!');
328
156
 
329
- // 12. Provide the Outro, Framework Warnings and Next Steps
157
+ // 9. Output
330
158
  let frameworkWarnings = '';
331
- if (!(finalFramework === 'static' && detectedFramework?.buildDir)) {
332
- frameworkWarnings = getFrameworkWarning(finalFramework);
159
+ if (!(config.framework === 'static' && detectedFramework?.buildDir)) {
160
+ frameworkWarnings = getFrameworkWarning(config.framework);
333
161
  }
334
162
 
335
- const isGitInitialized = fsSync.existsSync(path.join(targetDir, '.git'));
336
-
337
- const needsCd = projectName && projectName !== '.';
338
- const applyStep = needsCd
339
- ? `cd ${projectName} && npx deploy-stack apply --yes`
340
- : 'npx deploy-stack apply';
341
-
163
+ const isGitInitialized = fsSync.existsSync(path.join(dirConfig.targetDir, '.git'));
164
+ const needsCd = dirConfig.projectName && dirConfig.projectName !== '.';
165
+ const applyStep = needsCd ? `cd ${dirConfig.projectName} && npx --yes deploy-stack apply` : 'npx --yes deploy-stack apply';
342
166
  const gitInstructions = isGitInitialized
343
167
  ? `git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git push`
344
- : `git init && git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git branch -M ${deployBranch} && git remote add origin https://github.com/your-username/your-repo.git && git push -u origin ${deployBranch}`;
168
+ : `git init && git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git branch -M ${config.branch} && git remote add origin https://github.com/your-username/your-repo.git && git push -u origin ${config.branch}`;
345
169
 
346
- const outroMessage = `${color.green('✅ Templates generated!')} ${color.blue('🛡️ DevSecOps scanning enabled.')}
170
+ outro(`${color.green('✅ Templates generated!')} ${color.blue('🛡️ DevSecOps scanning enabled.')}
347
171
  ${frameworkWarnings ? `\n ${frameworkWarnings}` : ''}
348
172
  ${color.yellow('Next steps:')}
349
173
  1. ${color.cyan(applyStep)}
350
174
  2. ${color.cyan(gitInstructions)}
351
- ${color.magenta('🚀 Need help?')} ${color.underline('https://calendly.com/anton-codes-iac/15min')}`;
352
-
353
- outro(outroMessage);
175
+ ${color.magenta('🚀 Need help?')} ${color.underline('https://calendly.com/anton-codes-iac/15min')}`);
354
176
 
355
- // 13.Ensure all analytics are sent before the CLI terminates
356
177
  await flushTelemetry();
357
178
  }