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 +4 -2
- package/package.json +1 -1
- package/release-notes.md +14 -0
- package/src/commands/apply.js +19 -1
- package/src/commands/destroy.js +14 -3
- package/src/commands/doctor.js +6 -0
- package/src/commands/eject.js +7 -0
- package/src/commands/init.js +98 -277
- package/src/commands/secrets.js +15 -0
- package/src/core/telemetry.js +1 -0
- package/src/utils/aws.js +1 -1
- package/src/utils/detector.js +127 -0
- package/src/utils/frameworks.js +55 -0
- package/src/utils/generator.js +71 -6
- package/src/utils/prompts.js +156 -0
- package/src/utils/visualizer.js +17 -9
- package/templates/docker/django.Dockerfile +3 -0
- package/templates/terraform/main.tf +4 -2
- package/templates/terraform/network.tf +3 -1
- package/templates/terraform/worker.tf +62 -0
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-
|
|
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
|
-
- [
|
|
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
package/release-notes.md
ADDED
|
@@ -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.
|
package/src/commands/apply.js
CHANGED
|
@@ -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('
|
|
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
|
}
|
package/src/commands/destroy.js
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
123
|
+
const actualProjectName = path.basename(process.cwd());
|
|
124
|
+
trackEvent('infrastructure_destroyed', {
|
|
125
|
+
projectName: actualProjectName,
|
|
115
126
|
region,
|
|
116
|
-
|
|
127
|
+
retained_state_bucket: !(deleteS3Bucket && typeof deleteS3Bucket !== 'symbol'),
|
|
117
128
|
success: true
|
|
118
129
|
});
|
|
119
130
|
await flushTelemetry();
|
package/src/commands/doctor.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/eject.js
CHANGED
|
@@ -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
|
|
package/src/commands/init.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
89
|
-
finalFramework = detectedFramework ? detectedFramework.id : null;
|
|
52
|
+
if (isHeadless) console.log(color.cyan(`🤖 Running deploy-stack in headless mode`));
|
|
90
53
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
//
|
|
236
|
-
const cpu =
|
|
237
|
-
const memory =
|
|
238
|
-
const computeTier =
|
|
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
|
|
244
|
-
const
|
|
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
|
-
//
|
|
249
|
-
|
|
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(
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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:
|
|
119
|
+
HEALTH_CHECK_PATH: config.healthCheckPath,
|
|
120
|
+
DESIRED_COUNT: config.desiredCount,
|
|
121
|
+
DEPLOY_BRANCH: config.branch,
|
|
308
122
|
BUILD_DIR: buildDir,
|
|
309
|
-
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
|
-
//
|
|
131
|
+
// 8. Telemetry
|
|
316
132
|
trackEvent('project_provisioned', {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
//
|
|
157
|
+
// 9. Output
|
|
330
158
|
let frameworkWarnings = '';
|
|
331
|
-
if (!(
|
|
332
|
-
frameworkWarnings = getFrameworkWarning(
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
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
|
}
|