deploy-stack 0.9.15 → 0.11.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/LICENSE +21 -0
- package/README.md +46 -57
- package/bin/cli.js +22 -2
- package/package.json +4 -1
- package/src/commands/apply.js +133 -0
- package/src/commands/destroy.js +56 -11
- package/src/commands/init.js +221 -199
- package/src/utils/backup.js +15 -11
- package/src/utils/generator.js +11 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 anton-codes-iac (https://github.com/anton-codes-iac)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> The zero-lock-in cloud generator. Eject your containerized web app from expensive PaaS platforms to production-ready, highly available AWS infrastructure in 60 seconds.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/deploy-stack)
|
|
6
|
+
[](https://www.npmjs.com/package/deploy-stack)
|
|
7
|
+
[](https://trivy.dev/)
|
|
5
8
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://trivy.dev/)
|
|
7
9
|
|
|
8
10
|
<!--  -->
|
|
9
11
|
|
|
@@ -25,22 +27,26 @@ You retain complete ownership of your infrastructure code without relying on bla
|
|
|
25
27
|
|
|
26
28
|
## ✨ Features
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
* **
|
|
30
|
-
* **
|
|
31
|
-
* **Cost & Observability Baselines:** Prevents runaway AWS bills with explicit 14-day CloudWatch log retention policies and auto-generates 5XX error alerting.
|
|
32
|
-
* **Safe Overwrite Flow:** Idempotent CLI safely backs up existing configurations to timestamped `.bak` files and updates `.gitignore` to guarantee zero data loss during rapid iteration.
|
|
33
|
-
* **Framework Agnostic:** Tailored container presets for Next.js, Express.js, FastAPI, Go, Django, Ruby on Rails, Nuxt 3, and **Zero-Config Static Sites** (React, Vue, SvelteKit, Astro, Vite).
|
|
34
|
-
* **Smart Git Integration:** Automatically detects your working branch (`main`, `master`, `develop`) and binds it directly to the generated GitHub Actions pipeline.
|
|
35
|
-
* **Production-Grade Defaults:** Automatically provisions an Amazon ECS Fargate cluster fronted by an Application Load Balancer across multiple availability zones.
|
|
36
|
-
* **Global Edge Acceleration:** Includes an integrated AWS CloudFront CDN distribution with SSL termination and optimized caching.
|
|
37
|
-
* **Keyless, Zero-Secret CI/CD:** Uses AWS IAM OpenID Connect (OIDC) for automated deployments—no long-lived AWS keys stored in GitHub Secrets.
|
|
38
|
-
* **Remote State with Native S3 Locking:** Automatically creates an encrypted S3 state bucket utilizing modern Terraform (1.7+) native S3 concurrency locking.
|
|
39
|
-
* **Built-in Secrets Sync:** Provides a dedicated CLI workflow to securely push local `.env` variables into AWS Secrets Manager and map them directly into containers at runtime.
|
|
40
|
-
* **Ecosystem Ready:** Integrated directly with the [deploy-stack GitHub Action](https://github.com/marketplace/actions/deploy-stack-aws-fargate-terraform-deploy) for a secure, boilerplate-free continuous deployment pipeline.
|
|
30
|
+
**🚀 Zero-Config Deployments**
|
|
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
|
+
* **Smart Discovery:** Automatically detects build output directories and generates highly optimized, multi-stage Dockerfiles.
|
|
41
33
|
* **Database Scaffolding:** Automatically provisions fully isolated, zero-trust AWS RDS PostgreSQL databases for backend monoliths.
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
|
|
35
|
+
**🛡️ DevSecOps & Security**
|
|
36
|
+
* **Automated Trivy Scanning:** Integrated IaC and container vulnerability scanning on every GitHub Actions run.
|
|
37
|
+
* **Hardened Containers:** Explicitly drops root privileges using `nginx-unprivileged` and distroless bases for strict Fargate security compliance.
|
|
38
|
+
* **Zero-Secret CI/CD:** Utilizes AWS IAM OpenID Connect (OIDC) for automated deployments—no long-lived AWS keys in GitHub.
|
|
39
|
+
* **Built-in Secrets Manager:** Push local `.env` variables directly into encrypted AWS Secrets Manager vaults with a single CLI command.
|
|
40
|
+
|
|
41
|
+
**☁️ AWS Native Architecture**
|
|
42
|
+
* **Production Defaults:** Provisions an Amazon ECS Fargate cluster fronted by an Application Load Balancer across multiple availability zones.
|
|
43
|
+
* **Global Edge Acceleration:** Integrated AWS CloudFront CDN distribution with SSL termination and edge caching.
|
|
44
|
+
* **Cost & Observability:** Prevents runaway AWS bills with explicit 14-day CloudWatch log retention and auto-generates 5XX error alerting.
|
|
45
|
+
|
|
46
|
+
**🛠️ Developer Experience**
|
|
47
|
+
* **Zero Vendor Lock-In:** Generates standard, readable Terraform (`.tf`) files. You own the infrastructure.
|
|
48
|
+
* **Native S3 State Locking:** Automatically creates an encrypted S3 state bucket utilizing modern Terraform concurrency locking.
|
|
49
|
+
* **Safe Iteration:** Idempotent CLI safely backs up existing configurations to `.bak` files to guarantee zero data loss.
|
|
44
50
|
|
|
45
51
|
---
|
|
46
52
|
|
|
@@ -52,58 +58,43 @@ Run the CLI directly in your project root:
|
|
|
52
58
|
npx deploy-stack
|
|
53
59
|
```
|
|
54
60
|
|
|
55
|
-
The interactive
|
|
56
|
-
1. **Target Directory / Name:** (Type `.` to bootstrap your current directory)
|
|
57
|
-
2. **Setup Mode:** (Choose **Quickstart** for sensible defaults, or **Advanced** for custom scaling and branch names)
|
|
58
|
-
3. **Zero-Config Detection:** The CLI automatically scans your project to detect your framework. For static sites, it intelligently discovers your build output directory (`dist`, `build`, `.output`, etc.) and dynamically configures the hardened Nginx container.
|
|
59
|
-
4. **AWS Region & Compute Tier:** (Select your target region and Fargate size with live cost estimates)
|
|
61
|
+
The interactive wizard will analyze your codebase, detect your framework, estimate your AWS costs, and generate your Terraform and GitHub Actions configurations.
|
|
60
62
|
|
|
61
63
|
---
|
|
62
64
|
|
|
63
|
-
##
|
|
65
|
+
## 🧰 CLI Command Reference
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
npx deploy-stack secrets push .env.production
|
|
69
|
-
```
|
|
70
|
-
*This command encrypts your values in AWS Secrets Manager and updates `terraform/secret_keys.json` to expose those variables inside your ECS tasks at boot.*
|
|
67
|
+
`deploy-stack` manages the entire lifecycle of your infrastructure.
|
|
71
68
|
|
|
72
|
-
*
|
|
69
|
+
* **`npx deploy-stack apply`**
|
|
70
|
+
Wraps Terraform execution in a beautiful, terminal-friendly UI. Automatically provisions your AWS infrastructure and outputs your live CDN and Load Balancer URLs.
|
|
71
|
+
|
|
72
|
+
* **`npx deploy-stack secrets push <file>`**
|
|
73
|
+
Securely encrypts your local environment variables (e.g., `.env.production`) into AWS Secrets Manager and maps them to your ECS container at runtime.
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
* **`npx deploy-stack doctor`**
|
|
76
|
+
Scans your local environment and generated files to ensure all required dependencies (Docker, Terraform, AWS CLI) are installed and configured correctly.
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
1. **Provision Infrastructure:**
|
|
79
|
-
```bash
|
|
80
|
-
cd terraform
|
|
81
|
-
terraform init
|
|
82
|
-
terraform apply
|
|
83
|
-
```
|
|
84
|
-
2. **Push to GitHub:**
|
|
85
|
-
```bash
|
|
86
|
-
git add .
|
|
87
|
-
git commit -m "feat: infrastructure and deployment pipeline"
|
|
88
|
-
git push origin main
|
|
89
|
-
```
|
|
90
|
-
3. **Automated Deployment:** GitHub Actions securely authenticates via OIDC, builds your container, pushes to Amazon ECR, and executes a zero-downtime rolling deployment to ECS.
|
|
78
|
+
* **`npx deploy-stack destroy`**
|
|
79
|
+
Safely tears down your ECS cluster, Load Balancers, and networking resources to stop AWS billing. Includes an interactive prompt to optionally retain or delete your S3 remote state bucket.
|
|
91
80
|
|
|
92
|
-
|
|
81
|
+
* **`npx deploy-stack eject`**
|
|
82
|
+
Strips all `deploy-stack` metadata and management tags from your project, leaving behind pure, standard Terraform and GitHub Actions files. You retain 100% ownership.
|
|
93
83
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`npx deploy-stack destroy`
|
|
84
|
+
* **`npx deploy-stack --headless`**
|
|
85
|
+
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`.
|
|
97
86
|
|
|
98
87
|
---
|
|
99
88
|
|
|
100
89
|
## 📁 Generated File Structure
|
|
101
90
|
|
|
102
|
-
Running the CLI
|
|
91
|
+
Running the CLI seamlessly integrates a modular, DevSecOps-hardened architecture into your repository:
|
|
103
92
|
|
|
104
93
|
```text
|
|
105
94
|
your-project/
|
|
106
95
|
├── Dockerfile # Multi-stage container preset
|
|
96
|
+
├── .dockerignore # Prevents secret leaks into container builds
|
|
97
|
+
├── .gitignore # Automatically updated to ignore tfstate and .bak files
|
|
107
98
|
├── .github/
|
|
108
99
|
│ └── workflows/
|
|
109
100
|
│ └── deploy.yml # Keyless OIDC CI/CD deployment pipeline
|
|
@@ -138,16 +129,14 @@ your-project/
|
|
|
138
129
|
By default, `deploy-stack` collects anonymous, hashed usage data to help improve the CLI (e.g., framework presets used, deployment success rates). **No codebase files, AWS credentials, or personal data are ever collected.**
|
|
139
130
|
|
|
140
131
|
To opt out, simply append the flag:
|
|
141
|
-
|
|
132
|
+
```bash
|
|
142
133
|
npx deploy-stack --no-telemetry
|
|
143
|
-
|
|
134
|
+
```
|
|
144
135
|
|
|
145
136
|
---
|
|
146
137
|
|
|
147
138
|
## 🗺️ Roadmap
|
|
148
139
|
|
|
149
|
-
## 🗺️ Roadmap
|
|
150
|
-
|
|
151
140
|
### Phase 1–3: The Core Engine (Completed)
|
|
152
141
|
- [x] **Core MVP:** Interactive CLI, ECS Fargate + ALB generation, CI/CD, and Secrets sync.
|
|
153
142
|
- [x] **Production Readiness:** CloudFront CDN edge distribution, native S3 state locking, and secure OIDC integration.
|
|
@@ -160,15 +149,15 @@ npx deploy-stack --no-telemetry
|
|
|
160
149
|
- [x] **Zero Vendor Lock-In:** Explicit `npx deploy-stack eject` command to safely strip `ManagedBy` tags and CLI metadata, leaving behind pure IaC.
|
|
161
150
|
- [x] **Heavy Backend Monoliths:** Hardened, unprivileged container adapters for Go, Nuxt.js, Django, and Rails, complete with automated zero-trust RDS PostgreSQL provisioning.
|
|
162
151
|
|
|
163
|
-
### Phase 5: The Activation Engine (
|
|
152
|
+
### Phase 5: The Activation Engine (v0.10.0 - Current)
|
|
153
|
+
- [x] **Local Execution Wrapper:** Native `deploy-stack apply` command with terminal-optimized streaming to eliminate Terraform context switching.
|
|
154
|
+
- [x] **Ecosystem Integrations:** Publishing official plugins to the Astro Integrations directory and backend framework ecosystems.
|
|
164
155
|
- [ ] **Ephemeral PR Previews:** Generating live preview URLs on every GitHub Pull Request, turning single-user tests into team-wide advertisements.
|
|
165
|
-
- [ ] **Next.js Image Optimization Handler:** A drop-in AWS Serverless handler (Lambda + CloudFront) to process images affordably, removing a massive Vercel lock-in hurdle.
|
|
166
156
|
- [ ] **GitHub Deployments UI Sync:** Wiring up the native GitHub "Environments" tab for instant visual validation that the CLI succeeded in the background.
|
|
167
157
|
|
|
168
158
|
### Phase 6: Workflow Interception (IDE, AI, & Local Bridges)
|
|
169
159
|
- [ ] **AI Agent Rulesets:** Publishing `.cursorrules` and Copilot instructions that teach AI assistants exactly how to utilize the CLI on the user's behalf.
|
|
170
160
|
- [ ] **Docker Compose to ECS Translator:** Automatically converting a familiar local `docker-compose.yml` into production ECS task definitions.
|
|
171
|
-
- [ ] **Framework Registries:** Submitting official plugins to the Astro Integrations directory and Nuxt Modules to secure passive, sustained discovery.
|
|
172
161
|
- [ ] **Dry-Run Visualization:** Generating a local `docker-compose.yml` mirror and a visual architecture map for local infrastructure validation.
|
|
173
162
|
|
|
174
163
|
---
|
package/bin/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { destroyStack } from '../src/commands/destroy.js';
|
|
|
5
5
|
import { runDoctor } from '../src/commands/doctor.js';
|
|
6
6
|
import { pushSecrets } from '../src/commands/secrets.js';
|
|
7
7
|
import { ejectStack } from '../src/commands/eject.js';
|
|
8
|
+
import { applyStack } from '../src/commands/apply.js';
|
|
8
9
|
|
|
9
10
|
// 1. Extract the telemetry flag and set the environment variable
|
|
10
11
|
const rawArgs = process.argv.slice(2);
|
|
@@ -17,11 +18,30 @@ if (hasNoTelemetry) {
|
|
|
17
18
|
// 2. Filter out the telemetry flag from the args so the subcommands don't see it
|
|
18
19
|
const args = rawArgs.filter((arg) => arg !== '--no-telemetry');
|
|
19
20
|
|
|
20
|
-
// 3.
|
|
21
|
+
// 3. Parse headless flags
|
|
22
|
+
const isHeadless = args.includes('--headless');
|
|
23
|
+
const getFlag = (flagName) => {
|
|
24
|
+
const match = args.find(a => a.startsWith(`--${flagName}=`));
|
|
25
|
+
return match ? match.split('=')[1] : undefined;
|
|
26
|
+
};
|
|
27
|
+
const headlessOptions = isHeadless ? {
|
|
28
|
+
dir: getFlag('dir'),
|
|
29
|
+
framework: getFlag('framework'),
|
|
30
|
+
region: getFlag('region'),
|
|
31
|
+
port: getFlag('port'),
|
|
32
|
+
size: getFlag('size'),
|
|
33
|
+
healthCheckPath: getFlag('healthCheckPath'),
|
|
34
|
+
desiredCount: getFlag('desiredCount'),
|
|
35
|
+
branch: getFlag('branch')
|
|
36
|
+
} : {};
|
|
37
|
+
|
|
38
|
+
// 4. Handle commands
|
|
21
39
|
if (args[0] === 'secrets' && args[1] === 'push') {
|
|
22
40
|
const envFile = args[2] || '.env';
|
|
23
41
|
const projectName = path.basename(process.cwd());
|
|
24
42
|
pushSecrets(envFile, projectName).catch(console.error);
|
|
43
|
+
} else if (args[0] === 'apply') {
|
|
44
|
+
applyStack().catch(console.error);
|
|
25
45
|
} else if (args[0] === 'doctor') {
|
|
26
46
|
runDoctor().catch(console.error);
|
|
27
47
|
} else if (args[0] === 'destroy') {
|
|
@@ -29,5 +49,5 @@ if (args[0] === 'secrets' && args[1] === 'push') {
|
|
|
29
49
|
} else if (args[0] === 'eject') {
|
|
30
50
|
ejectStack().catch(console.error);
|
|
31
51
|
} else {
|
|
32
|
-
mainStack().catch(console.error);
|
|
52
|
+
mainStack({ isHeadless, headlessOptions }).catch(console.error);
|
|
33
53
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deploy-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=18.0.0"
|
|
7
|
+
},
|
|
5
8
|
"type": "module",
|
|
6
9
|
"bin": {
|
|
7
10
|
"deploy-stack": "bin/cli.js"
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { intro, outro, spinner, log, cancel } from '@clack/prompts';
|
|
5
|
+
import color from 'picocolors';
|
|
6
|
+
|
|
7
|
+
// Helper to run a command while piping the latest stdout line into a @clack spinner
|
|
8
|
+
function runTerraformCommand(args, cwd, spin, loadingPrefix) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const child = spawn('terraform', args, { cwd });
|
|
11
|
+
let errorOutput = '';
|
|
12
|
+
let isDone = false;
|
|
13
|
+
|
|
14
|
+
child.stdout.on('data', (data) => {
|
|
15
|
+
if (isDone) return;
|
|
16
|
+
const lines = data.toString().split('\n').filter(line => line.trim() !== '');
|
|
17
|
+
if (lines.length > 0) {
|
|
18
|
+
const latestLine = lines[lines.length - 1].trim();
|
|
19
|
+
const display = latestLine.length > 120 ? latestLine.substring(0, 117) + '...' : latestLine;
|
|
20
|
+
spin.message(`${loadingPrefix} - ${color.dim(display)}`);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
child.stderr.on('data', (data) => {
|
|
25
|
+
errorOutput += data.toString();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
child.on('close', (code) => {
|
|
29
|
+
isDone = true;
|
|
30
|
+
if (code === 0) {
|
|
31
|
+
resolve();
|
|
32
|
+
} else {
|
|
33
|
+
reject(new Error(errorOutput || `Terraform exited with code ${code}`));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
child.on('error', (err) => {
|
|
38
|
+
isDone = true;
|
|
39
|
+
reject(err);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Helper to pull JSON outputs from Terraform
|
|
45
|
+
function getTerraformOutputs(cwd) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const child = spawn('terraform', ['output', '-json'], { cwd });
|
|
48
|
+
let outputData = '';
|
|
49
|
+
|
|
50
|
+
child.stdout.on('data', (data) => {
|
|
51
|
+
outputData += data.toString();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
child.on('close', (code) => {
|
|
55
|
+
if (code === 0) {
|
|
56
|
+
try {
|
|
57
|
+
resolve(JSON.parse(outputData));
|
|
58
|
+
} catch {
|
|
59
|
+
resolve({});
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
resolve({});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
child.on('error', () => {
|
|
67
|
+
resolve({});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function applyStack() {
|
|
73
|
+
intro(color.bgCyan(color.black(' deploy-stack apply ☁️ ')));
|
|
74
|
+
|
|
75
|
+
const targetDir = process.cwd();
|
|
76
|
+
const tfDir = path.join(targetDir, 'terraform');
|
|
77
|
+
|
|
78
|
+
// 1. Verify that the user is inside a project generated by deploy-stack
|
|
79
|
+
if (!fs.existsSync(tfDir) || !fs.existsSync(path.join(tfDir, 'main.tf'))) {
|
|
80
|
+
log.error(color.red('✖ No terraform directory found in current path.'));
|
|
81
|
+
log.message('Please run "npx deploy-stack" first to generate your infrastructure templates.');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const s = spinner();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// 2. Run terraform init
|
|
89
|
+
s.start('Initializing Terraform plugins...');
|
|
90
|
+
await runTerraformCommand(['init', '-upgrade'], tfDir, s, 'Initializing');
|
|
91
|
+
log.success('Terraform initialized.');
|
|
92
|
+
|
|
93
|
+
// 3. Run terraform apply
|
|
94
|
+
s.start('Provisioning AWS infrastructure (this may take 3–5 minutes)...');
|
|
95
|
+
await runTerraformCommand(['apply', '-auto-approve'], tfDir, s, 'Provisioning');
|
|
96
|
+
s.stop('AWS infrastructure provisioned successfully!');
|
|
97
|
+
|
|
98
|
+
// 4. Extract and print the outputs
|
|
99
|
+
const outputs = await getTerraformOutputs(tfDir);
|
|
100
|
+
|
|
101
|
+
const cfUrl = outputs.cloudfront_url?.value;
|
|
102
|
+
const albUrl = outputs.alb_direct_url?.value;
|
|
103
|
+
|
|
104
|
+
let finalMessage = color.green('✅ Infrastructure is live!');
|
|
105
|
+
if (cfUrl) finalMessage += `\n 🌍 CDN: ${color.cyan(cfUrl)}`;
|
|
106
|
+
if (albUrl) finalMessage += `\n 🚦 ALB: ${color.gray(albUrl)}`;
|
|
107
|
+
|
|
108
|
+
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')}`);
|
|
109
|
+
|
|
110
|
+
process.exit(0);
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
s.stop(color.red('❌ Terraform apply failed.'));
|
|
114
|
+
|
|
115
|
+
const errString = error.message || "";
|
|
116
|
+
|
|
117
|
+
if (errString.includes('EntityAlreadyExists') && errString.includes('token.actions.githubusercontent.com')) {
|
|
118
|
+
log.error(color.red('GitHub OIDC Provider Conflict Detected'));
|
|
119
|
+
log.message('AWS only allows one GitHub Actions provider per account, and you already have one configured.');
|
|
120
|
+
log.message('');
|
|
121
|
+
log.message(`${color.bold('How to fix this:')}`);
|
|
122
|
+
log.message(`1. Open ${color.cyan('terraform/oidc.tf')}`);
|
|
123
|
+
log.message(`2. Change ${color.green('create_oidc_provider = true')} to ${color.green('create_oidc_provider = false')}`);
|
|
124
|
+
log.message(`3. Run ${color.cyan('npx deploy-stack apply')} again.`);
|
|
125
|
+
} else {
|
|
126
|
+
// Generic fallback for unknown errors
|
|
127
|
+
log.error(color.red(errString));
|
|
128
|
+
log.message(`${color.bold('To debug manually, navigate to your terraform folder:')}`);
|
|
129
|
+
log.message(color.cyan('cd terraform && terraform apply'));
|
|
130
|
+
}
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/commands/destroy.js
CHANGED
|
@@ -2,11 +2,45 @@ import fsSync 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 { execSync } from 'child_process';
|
|
5
|
+
import { execSync, spawn } from 'child_process';
|
|
6
6
|
import { teardownStateBucket } from '../utils/aws.js';
|
|
7
7
|
import { checkDependency } from '../utils/system.js';
|
|
8
8
|
import { trackEvent, flushTelemetry } from '../core/telemetry.js';
|
|
9
9
|
|
|
10
|
+
// Helper to run a command while piping the latest stdout line into a @clack spinner
|
|
11
|
+
function runTerraformCommand(args, cwd, spin, loadingPrefix) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const child = spawn('terraform', args, { cwd });
|
|
14
|
+
let errorOutput = '';
|
|
15
|
+
let isDone = false;
|
|
16
|
+
|
|
17
|
+
child.stdout.on('data', (data) => {
|
|
18
|
+
if (isDone) return;
|
|
19
|
+
const lines = data.toString().split('\n').filter(line => line.trim() !== '');
|
|
20
|
+
if (lines.length > 0) {
|
|
21
|
+
const latestLine = lines[lines.length - 1].trim();
|
|
22
|
+
const display = latestLine.length > 120 ? latestLine.substring(0, 117) + '...' : latestLine;
|
|
23
|
+
spin.message(`${loadingPrefix} - ${color.dim(display)}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
child.stderr.on('data', (data) => {
|
|
28
|
+
errorOutput += data.toString();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.on('close', (code) => {
|
|
32
|
+
isDone = true;
|
|
33
|
+
if (code === 0) resolve();
|
|
34
|
+
else reject(new Error(errorOutput || `Terraform exited with code ${code}`));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
child.on('error', (err) => {
|
|
38
|
+
isDone = true;
|
|
39
|
+
reject(err);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
10
44
|
export async function destroyStack() {
|
|
11
45
|
intro(color.bgRed(color.white(' deploy-stack destroy 🗑️ ')));
|
|
12
46
|
|
|
@@ -46,23 +80,34 @@ export async function destroyStack() {
|
|
|
46
80
|
const region = regionMatch ? regionMatch[1] : 'us-east-1';
|
|
47
81
|
|
|
48
82
|
// 2. Execute Terraform Destroy
|
|
49
|
-
|
|
83
|
+
s.start('Destroying AWS compute resources (this takes a few minutes)...');
|
|
50
84
|
try {
|
|
51
|
-
|
|
85
|
+
await runTerraformCommand(['destroy', '-auto-approve'], tfDirPath, s, 'Destroying');
|
|
86
|
+
s.stop('AWS compute resources destroyed.');
|
|
52
87
|
} catch (error) {
|
|
53
|
-
|
|
88
|
+
s.stop(color.red('❌ Terraform destroy failed.'));
|
|
89
|
+
console.error(color.red(error.message));
|
|
54
90
|
process.exit(1);
|
|
55
91
|
}
|
|
56
92
|
|
|
57
93
|
// 3. Clean up the S3 State Bucket
|
|
58
94
|
if (bucketName) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
95
|
+
const deleteS3Bucket = await confirm({
|
|
96
|
+
message: color.yellow(`AWS compute resources destroyed. Do you also want to permanently delete the S3 state bucket?\n (Select 'No' if you plan to run 'deploy-stack apply' later to spin this back up.)`),
|
|
97
|
+
initialValue: false,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (deleteS3Bucket && typeof deleteS3Bucket !== 'symbol') {
|
|
101
|
+
s.start(`Emptying and deleting S3 state bucket: ${bucketName}...`);
|
|
102
|
+
try {
|
|
103
|
+
await teardownStateBucket(region, bucketName);
|
|
104
|
+
s.stop(`S3 bucket ${bucketName} successfully deleted.`);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
s.stop(`❌ Failed to delete S3 bucket. You may need to delete it manually in the AWS Console.`);
|
|
107
|
+
console.error(color.red(`AWS Error: ${error.message}`));
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
console.log(color.cyan(`\n S3 bucket retained. You can run 'npx deploy-stack apply' anytime to restore your infrastructure.`));
|
|
66
111
|
}
|
|
67
112
|
}
|
|
68
113
|
|
package/src/commands/init.js
CHANGED
|
@@ -13,7 +13,7 @@ import { provisionStateBucket } from '../utils/aws.js';
|
|
|
13
13
|
import { generateTemplates } from '../utils/generator.js';
|
|
14
14
|
import { handleExistingFiles } from '../utils/backup.js';
|
|
15
15
|
|
|
16
|
-
export async function mainStack() {
|
|
16
|
+
export async function mainStack({ isHeadless = false, headlessOptions = {} } = {}) {
|
|
17
17
|
const startTime = Date.now();
|
|
18
18
|
|
|
19
19
|
// 0. Silent Pre-flight check
|
|
@@ -24,179 +24,212 @@ export async function mainStack() {
|
|
|
24
24
|
process.exit(1);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
intro(color.bgCyan(color.black(' deploy-stack ☁️ ')));
|
|
29
|
-
|
|
30
|
-
// 2. Ask for the target directory FIRST
|
|
31
|
-
const projectName = await text({
|
|
32
|
-
message: 'Where should we generate the infrastructure? (Type "." for current directory)',
|
|
33
|
-
placeholder: '.',
|
|
34
|
-
initialValue: '.',
|
|
35
|
-
validate: (value) => {
|
|
36
|
-
if (!value) return 'Please enter a name or directory.';
|
|
37
|
-
if (value !== '.' && value.includes(' ')) return 'Name cannot contain spaces.';
|
|
38
|
-
},
|
|
39
|
-
});
|
|
27
|
+
const getFlag = (key, defaultValue) => headlessOptions[key] !== undefined ? headlessOptions[key] : defaultValue;
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
let projectName, actualProjectName, targetDir;
|
|
30
|
+
let finalFramework, detectedFramework;
|
|
31
|
+
let djangoWsgi = 'core.wsgi';
|
|
32
|
+
let setupType = 'quick';
|
|
33
|
+
let needsDatabase = false;
|
|
34
|
+
let disableDefaultCI = false;
|
|
35
|
+
let project = {};
|
|
36
|
+
let currentGitBranch = 'main';
|
|
45
37
|
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
if (isHeadless) {
|
|
39
|
+
// --- HEADLESS MODE ---
|
|
40
|
+
projectName = getFlag('dir', '.');
|
|
41
|
+
actualProjectName = projectName === '.' ? path.basename(process.cwd()) : projectName;
|
|
42
|
+
targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
|
|
43
|
+
|
|
44
|
+
detectedFramework = detectFramework(targetDir);
|
|
45
|
+
finalFramework = getFlag('framework', detectedFramework ? detectedFramework.id : 'static');
|
|
46
|
+
|
|
47
|
+
project = {
|
|
48
|
+
region: getFlag('region', 'us-east-1'),
|
|
49
|
+
port: getFlag('port', finalFramework === 'static' ? '8080' : '3000'),
|
|
50
|
+
size: getFlag('size', 'micro'),
|
|
51
|
+
healthCheckPath: getFlag('healthCheckPath', '/'),
|
|
52
|
+
desiredCount: getFlag('desiredCount', '1'),
|
|
53
|
+
branch: getFlag('branch', 'main')
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
console.log(color.cyan(`🤖 Running deploy-stack in headless mode [${finalFramework} -> ${project.region}]`));
|
|
57
|
+
} else {
|
|
58
|
+
// --- INTERACTIVE MODE ---
|
|
59
|
+
// 1. Start the CLI
|
|
60
|
+
intro(color.bgCyan(color.black(' deploy-stack ☁️ ')));
|
|
61
|
+
|
|
62
|
+
// 2. Ask for the target directory FIRST
|
|
63
|
+
const projectName = await text({
|
|
64
|
+
message: 'Where should we generate the infrastructure? (Type "." for current directory)',
|
|
65
|
+
placeholder: '.',
|
|
66
|
+
initialValue: '.',
|
|
67
|
+
validate: (value) => {
|
|
68
|
+
if (!value) return 'Please enter a name or directory.';
|
|
69
|
+
if (value !== '.' && value.includes(' ')) return 'Name cannot contain spaces.';
|
|
70
|
+
},
|
|
71
|
+
});
|
|
48
72
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
if (typeof projectName === 'symbol') {
|
|
74
|
+
cancel('Operation cancelled.');
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const actualProjectName = projectName === '.' ? path.basename(process.cwd()) : projectName;
|
|
79
|
+
const targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
|
|
54
80
|
|
|
55
|
-
|
|
56
|
-
|
|
81
|
+
// 2.5 Run the scanner
|
|
82
|
+
const detectedFramework = detectFramework(targetDir);
|
|
83
|
+
if (detectedFramework) {
|
|
84
|
+
log.success(`Auto-detected framework: ${detectedFramework.name}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2.6 Resolve the framework
|
|
88
|
+
let finalFramework = detectedFramework ? detectedFramework.id : null;
|
|
89
|
+
|
|
90
|
+
if (!finalFramework) {
|
|
91
|
+
finalFramework = await select({
|
|
92
|
+
message: 'Which framework preset should we configure?',
|
|
93
|
+
options: [
|
|
94
|
+
{ value: 'node', label: 'Node.js / Express' },
|
|
95
|
+
{ value: 'nextjs', label: 'Next.js (Standalone)' },
|
|
96
|
+
{ value: 'nuxt', label: 'Nuxt 3 (SSR)' },
|
|
97
|
+
{ value: 'python', label: 'Python FastAPI' },
|
|
98
|
+
{ value: 'django', label: 'Django (Python)' },
|
|
99
|
+
{ value: 'rails', label: 'Ruby on Rails' },
|
|
100
|
+
{ value: 'go', label: 'Go (Golang)' },
|
|
101
|
+
{ value: 'static', label: 'Static Site (Gatsby, React, plain HTML via Nginx)' },
|
|
102
|
+
],
|
|
103
|
+
});
|
|
57
104
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
105
|
+
if (typeof finalFramework === 'symbol') {
|
|
106
|
+
cancel('Operation cancelled.');
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- DJANGO SPECIFIC PROMPT ---
|
|
112
|
+
let djangoWsgi = 'core.wsgi';
|
|
113
|
+
if (finalFramework === 'django') {
|
|
114
|
+
djangoWsgi = await text({
|
|
115
|
+
message: 'What is the Python module path to your Django wsgi.py?',
|
|
116
|
+
placeholder: 'core.wsgi',
|
|
117
|
+
initialValue: 'core.wsgi',
|
|
118
|
+
});
|
|
119
|
+
if (typeof djangoWsgi === 'symbol') process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. Prompt for Setup Mode
|
|
123
|
+
const setupType = await select({
|
|
124
|
+
message: 'Choose your setup mode:',
|
|
61
125
|
options: [
|
|
62
|
-
{ value: '
|
|
63
|
-
{ value: '
|
|
64
|
-
{ value: 'nuxt', label: 'Nuxt 3 (SSR)' },
|
|
65
|
-
{ value: 'python', label: 'Python FastAPI' },
|
|
66
|
-
{ value: 'django', label: 'Django (Python)' },
|
|
67
|
-
{ value: 'rails', label: 'Ruby on Rails' },
|
|
68
|
-
{ value: 'go', label: 'Go (Golang)' },
|
|
69
|
-
{ value: 'static', label: 'Static Site (Gatsby, React, plain HTML via Nginx)' },
|
|
126
|
+
{ value: 'quick', label: '⚡ Quickstart (Recommended)', hint: 'Production defaults, minimal prompts' },
|
|
127
|
+
{ value: 'advanced', label: '🛠️ Advanced Configuration', hint: 'Customize health checks, task count, branch, etc.' },
|
|
70
128
|
],
|
|
71
129
|
});
|
|
72
130
|
|
|
73
|
-
if (typeof
|
|
131
|
+
if (typeof setupType === 'symbol') {
|
|
74
132
|
cancel('Operation cancelled.');
|
|
75
133
|
process.exit(0);
|
|
76
134
|
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// --- DJANGO SPECIFIC PROMPT ---
|
|
80
|
-
let djangoWsgi = 'core.wsgi';
|
|
81
|
-
if (finalFramework === 'django') {
|
|
82
|
-
djangoWsgi = await text({
|
|
83
|
-
message: 'What is the Python module path to your Django wsgi.py?',
|
|
84
|
-
placeholder: 'core.wsgi',
|
|
85
|
-
initialValue: 'core.wsgi',
|
|
86
|
-
});
|
|
87
|
-
if (typeof djangoWsgi === 'symbol') process.exit(0);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 3. Prompt for Setup Mode
|
|
91
|
-
const setupType = await select({
|
|
92
|
-
message: 'Choose your setup mode:',
|
|
93
|
-
options: [
|
|
94
|
-
{ value: 'quick', label: '⚡ Quickstart (Recommended)', hint: 'Production defaults, minimal prompts' },
|
|
95
|
-
{ value: 'advanced', label: '🛠️ Advanced Configuration', hint: 'Customize health checks, task count, branch, etc.' },
|
|
96
|
-
],
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
if (typeof setupType === 'symbol') {
|
|
100
|
-
cancel('Operation cancelled.');
|
|
101
|
-
process.exit(0);
|
|
102
|
-
}
|
|
103
135
|
|
|
104
|
-
|
|
105
|
-
|
|
136
|
+
// 4. Set intelligent defaults & check current Git branch
|
|
137
|
+
let defaultPort = '3000';
|
|
106
138
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
if (finalFramework === 'static') defaultPort = '8080';
|
|
140
|
+
if (finalFramework === 'python' || finalFramework === 'django') defaultPort = '8000';
|
|
141
|
+
if (finalFramework === 'rails') defaultPort = '3000';
|
|
142
|
+
if (finalFramework === 'go') defaultPort = '8080';
|
|
111
143
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
let currentGitBranch = 'main';
|
|
145
|
+
try {
|
|
146
|
+
currentGitBranch = execSync('git symbolic-ref --short HEAD', { cwd: targetDir, stdio: 'pipe' }).toString().trim();
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// Not a git repo yet, fallback to 'main'
|
|
149
|
+
}
|
|
118
150
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
151
|
+
// 5. Ask for Managed Database (Only for Backend/Fullstack Frameworks)
|
|
152
|
+
let needsDatabase = false;
|
|
153
|
+
const isBackendFramework = ['node', 'nextjs', 'nuxt', 'python', 'django', 'rails', 'go'].includes(finalFramework);
|
|
122
154
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
155
|
+
if (isBackendFramework) {
|
|
156
|
+
const dbChoice = await confirm({
|
|
157
|
+
message: 'Do you need a managed AWS RDS PostgreSQL database? (Adds ~$14/month or uses AWS Free Tier)',
|
|
158
|
+
initialValue: false,
|
|
159
|
+
});
|
|
128
160
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
if (typeof dbChoice === 'symbol') {
|
|
162
|
+
cancel('Provisioning cancelled.')
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
needsDatabase = dbChoice;
|
|
132
166
|
}
|
|
133
|
-
needsDatabase = dbChoice;
|
|
134
|
-
}
|
|
135
167
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
onCancel: () => {
|
|
195
|
-
cancel('Provisioning cancelled.');
|
|
196
|
-
process.exit(0);
|
|
168
|
+
// 6. Prompt Configuration Group
|
|
169
|
+
const project = await group(
|
|
170
|
+
{
|
|
171
|
+
region: () =>
|
|
172
|
+
select({
|
|
173
|
+
message: 'Which AWS region do you want to deploy to?',
|
|
174
|
+
options: [
|
|
175
|
+
{ value: 'us-east-1', label: 'us-east-1 (N. Virginia)' },
|
|
176
|
+
{ value: 'us-east-2', label: 'us-east-2 (Ohio)' },
|
|
177
|
+
{ value: 'eu-west-1', label: 'eu-west-1 (Ireland)' },
|
|
178
|
+
{ value: 'eu-central-1', label: 'EU (Frankfurt)' },
|
|
179
|
+
{ value: 'ap-southeast-2', label: 'Asia Pacific (Sydney)' },
|
|
180
|
+
],
|
|
181
|
+
}),
|
|
182
|
+
port: () =>
|
|
183
|
+
text({
|
|
184
|
+
message: 'What port does your container expose?',
|
|
185
|
+
placeholder: defaultPort,
|
|
186
|
+
defaultValue: defaultPort,
|
|
187
|
+
}),
|
|
188
|
+
size: () =>
|
|
189
|
+
select({
|
|
190
|
+
message: 'Select your Fargate compute size:',
|
|
191
|
+
options: [
|
|
192
|
+
{ value: 'micro', label: 'Micro (0.25 vCPU, 512MB RAM) - Best for POCs' },
|
|
193
|
+
{ value: 'small', label: 'Small (0.5 vCPU, 1GB RAM) - Best for small Projects' },
|
|
194
|
+
],
|
|
195
|
+
}),
|
|
196
|
+
// --- Advanced-Only Prompts (Skipped if setupType === 'quick') ---
|
|
197
|
+
healthCheckPath: () => {
|
|
198
|
+
if (setupType === 'quick') return undefined;
|
|
199
|
+
return text({
|
|
200
|
+
message: 'ALB Health Check Path:',
|
|
201
|
+
placeholder: '/',
|
|
202
|
+
defaultValue: '/',
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
desiredCount: () => {
|
|
206
|
+
if (setupType === 'quick') return undefined;
|
|
207
|
+
return select({
|
|
208
|
+
message: 'How many container replicas (tasks) should run?',
|
|
209
|
+
options: [
|
|
210
|
+
{ value: '1', label: '1 Task (Single instance - lowest cost)' },
|
|
211
|
+
{ value: '2', label: '2 Tasks (High Availability across AZs)' },
|
|
212
|
+
],
|
|
213
|
+
defaultValue: '1',
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
branch: () => {
|
|
217
|
+
if (setupType === 'quick') return undefined;
|
|
218
|
+
return text({
|
|
219
|
+
message: 'Primary Git deployment branch for CI/CD:',
|
|
220
|
+
placeholder: currentGitBranch,
|
|
221
|
+
defaultValue: currentGitBranch,
|
|
222
|
+
});
|
|
223
|
+
},
|
|
197
224
|
},
|
|
198
|
-
|
|
199
|
-
|
|
225
|
+
{
|
|
226
|
+
onCancel: () => {
|
|
227
|
+
cancel('Provisioning cancelled.');
|
|
228
|
+
process.exit(0);
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
}
|
|
200
233
|
|
|
201
234
|
// 7. Map the user's choices and update the variables
|
|
202
235
|
const cpu = project.size === 'small' ? '512' : '256';
|
|
@@ -215,40 +248,45 @@ export async function mainStack() {
|
|
|
215
248
|
const buildDir = detectedFramework?.buildDir || 'dist';
|
|
216
249
|
|
|
217
250
|
// 7.4 Check for conflicting CI boilerplate (Rails)
|
|
218
|
-
let disableDefaultCI = false;
|
|
219
251
|
if (finalFramework === 'rails') {
|
|
220
252
|
const ciPath = path.join(targetDir, '.github', 'workflows', 'ci.yml');
|
|
221
253
|
const dependabotPath = path.join(targetDir, '.github', 'dependabot.yml');
|
|
222
254
|
|
|
223
255
|
if (fsSync.existsSync(ciPath) || fsSync.existsSync(dependabotPath)) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
256
|
+
if (!isHeadless) {
|
|
257
|
+
console.log('');
|
|
258
|
+
const ciContent = await confirm({
|
|
259
|
+
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?'),
|
|
260
|
+
initialValue: true,
|
|
261
|
+
});
|
|
262
|
+
if (typeof ciContent === 'symbol') {
|
|
263
|
+
cancel('Provisioning cancelled.');
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
disableDefaultCI = ciContent;
|
|
267
|
+
} else {
|
|
268
|
+
// Headless: automatically disable conflicting CI
|
|
269
|
+
disableDefaultCI = true;
|
|
232
270
|
}
|
|
233
|
-
disableDefaultCI = ciContent;
|
|
234
271
|
}
|
|
235
272
|
}
|
|
236
273
|
|
|
237
274
|
// 7.5. The Pre-Flight Cost Estimator
|
|
238
275
|
// We explicitly ask for financial consent to eliminate AWS billing anxiety.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
276
|
+
if (!isHeadless) {
|
|
277
|
+
console.log(''); // Add a blank line for visual pacing
|
|
278
|
+
const costConsent = await confirm({
|
|
279
|
+
message: color.yellow(`⚠️ Pre-Flight Check: This AWS architecture will cost ${estimatedCost}. Proceed with provisioning?`),
|
|
280
|
+
initialValue: true,
|
|
281
|
+
});
|
|
282
|
+
if (!costConsent || typeof costConsent === 'symbol') {
|
|
283
|
+
cancel('Deployment cancelled. No AWS resources were provisioned.');
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
248
286
|
}
|
|
249
287
|
|
|
250
288
|
// 8. Safely handle existing files (Backup and auto-prune)
|
|
251
|
-
await handleExistingFiles(targetDir);
|
|
289
|
+
await handleExistingFiles(targetDir, isHeadless);
|
|
252
290
|
|
|
253
291
|
const s = spinner();
|
|
254
292
|
s.start('Provisioning infrastructure...');
|
|
@@ -313,36 +351,20 @@ export async function mainStack() {
|
|
|
313
351
|
const isGitInitialized = fsSync.existsSync(path.join(targetDir, '.git'));
|
|
314
352
|
|
|
315
353
|
const cdStep = projectName === '.' ? '' : `1. cd ${projectName}\n `;
|
|
316
|
-
const
|
|
317
|
-
const gitStepNum = projectName === '.' ? '2' : '3';
|
|
354
|
+
const applyStep = `${cdStep}npx deploy-stack apply`;
|
|
318
355
|
|
|
319
356
|
const gitInstructions = isGitInitialized
|
|
320
|
-
? `git add
|
|
321
|
-
: `git init
|
|
322
|
-
|
|
323
|
-
outro(`
|
|
324
|
-
${color.green('✅ Project provisioned successfully!')}
|
|
325
|
-
|
|
326
|
-
${color.blue('🛡️ DevSecOps Enabled:')}
|
|
327
|
-
Automated Trivy vulnerability scanning for your Docker container
|
|
328
|
-
and Terraform has been added to your CI/CD pipeline.
|
|
329
|
-
Check the 'Summary' page of your GitHub Actions runs for reports.
|
|
330
|
-
|
|
331
|
-
${frameworkWarnings}
|
|
332
|
-
|
|
333
|
-
Next steps:
|
|
334
|
-
${cdStep}${deployStepNum}. Deploy infrastructure:
|
|
335
|
-
cd terraform && terraform init && terraform apply
|
|
336
|
-
|
|
337
|
-
${gitStepNum}. Push to GitHub to trigger CI/CD:
|
|
338
|
-
cd ..
|
|
339
|
-
${gitInstructions}
|
|
357
|
+
? `git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git push`
|
|
358
|
+
: `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}`;
|
|
340
359
|
|
|
341
|
-
|
|
360
|
+
const outroMessage = `${color.green('✅ Templates generated!')} ${color.blue('🛡️ DevSecOps scanning enabled.')}
|
|
361
|
+
${frameworkWarnings ? `\n ${frameworkWarnings}` : ''}
|
|
362
|
+
${color.yellow('Next steps:')}
|
|
363
|
+
1. ${color.cyan(applyStep)}
|
|
364
|
+
2. ${color.cyan(gitInstructions)}
|
|
365
|
+
${color.magenta('🚀 Need help?')} ${color.underline('https://calendly.com/anton-codes-iac/15min')}`;
|
|
342
366
|
|
|
343
|
-
|
|
344
|
-
${color.underline('https://calendly.com/anton-codes-iac/15min')}
|
|
345
|
-
`);
|
|
367
|
+
outro(outroMessage);
|
|
346
368
|
|
|
347
369
|
// 13.Ensure all analytics are sent before the CLI terminates
|
|
348
370
|
await flushTelemetry();
|
package/src/utils/backup.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { select, isCancel, cancel } from '@clack/prompts';
|
|
4
4
|
import color from 'picocolors';
|
|
5
5
|
|
|
6
|
-
export async function handleExistingFiles(targetDir) {
|
|
6
|
+
export async function handleExistingFiles(targetDir, isHeadless = false) {
|
|
7
7
|
const tfPath = path.join(targetDir, 'terraform');
|
|
8
8
|
const dockerfilePath = path.join(targetDir, 'Dockerfile');
|
|
9
9
|
const wfDir = path.join(targetDir, '.github', 'workflows');
|
|
@@ -22,17 +22,21 @@ export async function handleExistingFiles(targetDir) {
|
|
|
22
22
|
if (dockerfileExists) foundFiles.push('Dockerfile');
|
|
23
23
|
if (wfExists) foundFiles.push('.github/workflows/deploy.yml');
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
message: color.yellow(`⚠️ Conflicting files found (${foundFiles.join(', ')}). To guarantee a secure, 0-CVE deployment, we must use our optimized configurations.`),
|
|
27
|
-
options: [
|
|
28
|
-
{ value: 'backup', label: 'Backup & Regenerate', hint: 'Move old configs to .bak and generate secure templates' },
|
|
29
|
-
{ value: 'cancel', label: 'Cancel', hint: 'Exit without making changes' }
|
|
30
|
-
]
|
|
31
|
-
});
|
|
25
|
+
let overwriteDecision = 'backup';
|
|
32
26
|
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
if (!isHeadless) {
|
|
28
|
+
overwriteDecision = await select({
|
|
29
|
+
message: color.yellow(`⚠️ Conflicting files found (${foundFiles.join(', ')}). To guarantee a secure, 0-CVE deployment, we must use our optimized configurations.`),
|
|
30
|
+
options: [
|
|
31
|
+
{ value: 'backup', label: 'Backup & Regenerate', hint: 'Move old configs to .bak and generate secure templates' },
|
|
32
|
+
{ value: 'cancel', label: 'Cancel', hint: 'Exit without making changes' }
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (isCancel(overwriteDecision) || overwriteDecision === 'cancel') {
|
|
37
|
+
cancel('Operation cancelled to protect existing files.');
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
// Execute the safe backup
|
package/src/utils/generator.js
CHANGED
|
@@ -152,13 +152,17 @@ Thumbs.db
|
|
|
152
152
|
const railsCiPath = path.join(targetDir, '.github', 'workflows', 'ci.yml');
|
|
153
153
|
const dependabotPath = path.join(targetDir, '.github', 'dependabot.yml');
|
|
154
154
|
|
|
155
|
-
if (
|
|
156
|
-
fsSync.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
fsSync.
|
|
161
|
-
|
|
155
|
+
if (config.DISABLE_DEFAULT_CI) {
|
|
156
|
+
if (fsSync.existsSync(railsCiPath)) {
|
|
157
|
+
fsSync.renameSync(railsCiPath, `${railsCiPath}.bak`);
|
|
158
|
+
console.log(' ✅ Safely disabled default Rails ci.yml (renamed to .bak).');
|
|
159
|
+
}
|
|
160
|
+
if (fsSync.existsSync(dependabotPath)) {
|
|
161
|
+
fsSync.renameSync(dependabotPath, `${dependabotPath}.bak`);
|
|
162
|
+
console.log(' ✅ Safely disabled default dependabot.yml (renamed to .bak).');
|
|
163
|
+
}
|
|
164
|
+
} else if (fsSync.existsSync(railsCiPath) || fsSync.existsSync(dependabotPath)) {
|
|
165
|
+
console.log(' ⚠️ You chose to keep your existing GitHub Actions. Please note that default tests may fail until you configure a database service in your CI runner.');
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
}
|