deploy-stack 0.10.0 → 0.11.1
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 +10 -8
- package/bin/cli.js +19 -2
- package/package.json +4 -1
- package/src/commands/init.js +211 -173
- package/src/utils/backup.js +15 -11
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
|
@@ -54,9 +54,9 @@ You retain complete ownership of your infrastructure code without relying on bla
|
|
|
54
54
|
|
|
55
55
|
Run the CLI directly in your project root:
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
```bash
|
|
58
58
|
npx deploy-stack
|
|
59
|
-
|
|
59
|
+
```
|
|
60
60
|
|
|
61
61
|
The interactive wizard will analyze your codebase, detect your framework, estimate your AWS costs, and generate your Terraform and GitHub Actions configurations.
|
|
62
62
|
|
|
@@ -81,13 +81,16 @@ The interactive wizard will analyze your codebase, detect your framework, estima
|
|
|
81
81
|
* **`npx deploy-stack eject`**
|
|
82
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.
|
|
83
83
|
|
|
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`.
|
|
86
|
+
|
|
84
87
|
---
|
|
85
88
|
|
|
86
89
|
## 📁 Generated File Structure
|
|
87
90
|
|
|
88
91
|
Running the CLI seamlessly integrates a modular, DevSecOps-hardened architecture into your repository:
|
|
89
92
|
|
|
90
|
-
|
|
93
|
+
```text
|
|
91
94
|
your-project/
|
|
92
95
|
├── Dockerfile # Multi-stage container preset
|
|
93
96
|
├── .dockerignore # Prevents secret leaks into container builds
|
|
@@ -103,7 +106,7 @@ your-project/
|
|
|
103
106
|
├── secrets.tf # AWS Secrets Manager integration
|
|
104
107
|
├── backend.tf # S3 Remote State backend with native locking
|
|
105
108
|
└── secret_keys.json # Dynamic key map for injected environment variables
|
|
106
|
-
|
|
109
|
+
```
|
|
107
110
|
|
|
108
111
|
---
|
|
109
112
|
|
|
@@ -126,9 +129,9 @@ your-project/
|
|
|
126
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.**
|
|
127
130
|
|
|
128
131
|
To opt out, simply append the flag:
|
|
129
|
-
|
|
132
|
+
```bash
|
|
130
133
|
npx deploy-stack --no-telemetry
|
|
131
|
-
|
|
134
|
+
```
|
|
132
135
|
|
|
133
136
|
---
|
|
134
137
|
|
|
@@ -148,14 +151,13 @@ npx deploy-stack --no-telemetry
|
|
|
148
151
|
|
|
149
152
|
### Phase 5: The Activation Engine (v0.10.0 - Current)
|
|
150
153
|
- [x] **Local Execution Wrapper:** Native `deploy-stack apply` command with terminal-optimized streaming to eliminate Terraform context switching.
|
|
151
|
-
- [
|
|
154
|
+
- [x] **Ecosystem Integrations:** Publishing official plugins to the Astro Integrations directory and backend framework ecosystems.
|
|
152
155
|
- [ ] **Ephemeral PR Previews:** Generating live preview URLs on every GitHub Pull Request, turning single-user tests into team-wide advertisements.
|
|
153
156
|
- [ ] **GitHub Deployments UI Sync:** Wiring up the native GitHub "Environments" tab for instant visual validation that the CLI succeeded in the background.
|
|
154
157
|
|
|
155
158
|
### Phase 6: Workflow Interception (IDE, AI, & Local Bridges)
|
|
156
159
|
- [ ] **AI Agent Rulesets:** Publishing `.cursorrules` and Copilot instructions that teach AI assistants exactly how to utilize the CLI on the user's behalf.
|
|
157
160
|
- [ ] **Docker Compose to ECS Translator:** Automatically converting a familiar local `docker-compose.yml` into production ECS task definitions.
|
|
158
|
-
- [ ] **Framework Registries:** Submitting official plugins to the Astro Integrations directory and Nuxt Modules to secure passive, sustained discovery.
|
|
159
161
|
- [ ] **Dry-Run Visualization:** Generating a local `docker-compose.yml` mirror and a visual architecture map for local infrastructure validation.
|
|
160
162
|
|
|
161
163
|
---
|
package/bin/cli.js
CHANGED
|
@@ -18,7 +18,24 @@ if (hasNoTelemetry) {
|
|
|
18
18
|
// 2. Filter out the telemetry flag from the args so the subcommands don't see it
|
|
19
19
|
const args = rawArgs.filter((arg) => arg !== '--no-telemetry');
|
|
20
20
|
|
|
21
|
-
// 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
|
|
22
39
|
if (args[0] === 'secrets' && args[1] === 'push') {
|
|
23
40
|
const envFile = args[2] || '.env';
|
|
24
41
|
const projectName = path.basename(process.cwd());
|
|
@@ -32,5 +49,5 @@ if (args[0] === 'secrets' && args[1] === 'push') {
|
|
|
32
49
|
} else if (args[0] === 'eject') {
|
|
33
50
|
ejectStack().catch(console.error);
|
|
34
51
|
} else {
|
|
35
|
-
mainStack().catch(console.error);
|
|
52
|
+
mainStack({ isHeadless, headlessOptions }).catch(e => { console.error(e); process.exit(1); });
|
|
36
53
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deploy-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
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"
|
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';
|
|
37
|
+
|
|
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
|
+
});
|
|
72
|
+
|
|
73
|
+
if (typeof projectName === 'symbol') {
|
|
74
|
+
cancel('Operation cancelled.');
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
45
77
|
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
const actualProjectName = projectName === '.' ? path.basename(process.cwd()) : projectName;
|
|
79
|
+
const targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
|
|
48
80
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
81
|
+
// 2.5 Run the scanner
|
|
82
|
+
const detectedFramework = detectFramework(targetDir);
|
|
83
|
+
if (detectedFramework) {
|
|
84
|
+
log.success(`Auto-detected framework: ${detectedFramework.name}`);
|
|
85
|
+
}
|
|
54
86
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
+
},
|
|
183
224
|
},
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
onCancel: () => {
|
|
195
|
-
cancel('Provisioning cancelled.');
|
|
196
|
-
process.exit(0);
|
|
197
|
-
},
|
|
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...');
|
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
|