deploy-stack 0.10.0 → 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 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
- \`\`\`bash
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
- \`\`\`text
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
- \`\`\`bash
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
- - [ ] **Ecosystem Integrations:** Publishing official plugins to the Astro Integrations directory and backend framework ecosystems.
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. Handle commands
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(console.error);
36
53
  }
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "deploy-stack",
3
- "version": "0.10.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"
@@ -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
- // 1. Start the CLI
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
- if (typeof projectName === 'symbol') {
42
- cancel('Operation cancelled.');
43
- process.exit(0);
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
- const actualProjectName = projectName === '.' ? path.basename(process.cwd()) : projectName;
47
- const targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
78
+ const actualProjectName = projectName === '.' ? path.basename(process.cwd()) : projectName;
79
+ const targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
48
80
 
49
- // 2.5 Run the scanner
50
- const detectedFramework = detectFramework(targetDir);
51
- if (detectedFramework) {
52
- log.success(`Auto-detected framework: ${detectedFramework.name}`);
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
- // 2.6 Resolve the framework
56
- let finalFramework = detectedFramework ? detectedFramework.id : null;
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
- if (!finalFramework) {
59
- finalFramework = await select({
60
- message: 'Which framework preset should we configure?',
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: 'node', label: 'Node.js / Express' },
63
- { value: 'nextjs', label: 'Next.js (Standalone)' },
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 finalFramework === 'symbol') {
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
- // 4. Set intelligent defaults & check current Git branch
105
- let defaultPort = '3000';
136
+ // 4. Set intelligent defaults & check current Git branch
137
+ let defaultPort = '3000';
106
138
 
107
- if (finalFramework === 'static') defaultPort = '8080';
108
- if (finalFramework === 'python' || finalFramework === 'django') defaultPort = '8000';
109
- if (finalFramework === 'rails') defaultPort = '3000';
110
- if (finalFramework === 'go') defaultPort = '8080';
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
- let currentGitBranch = 'main';
113
- try {
114
- currentGitBranch = execSync('git symbolic-ref --short HEAD', { cwd: targetDir, stdio: 'pipe' }).toString().trim();
115
- } catch (e) {
116
- // Not a git repo yet, fallback to 'main'
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
- // 5. Ask for Managed Database (Only for Backend/Fullstack Frameworks)
120
- let needsDatabase = false;
121
- const isBackendFramework = ['node', 'nextjs', 'nuxt', 'python', 'django', 'rails', 'go'].includes(finalFramework);
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
- if (isBackendFramework) {
124
- const dbChoice = await confirm({
125
- message: 'Do you need a managed AWS RDS PostgreSQL database? (Adds ~$14/month or uses AWS Free Tier)',
126
- initialValue: false,
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
- if (typeof dbChoice === 'symbol') {
130
- cancel('Provisioning cancelled.')
131
- process.exit(0);
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
- // 6. Prompt Configuration Group
137
- const project = await group(
138
- {
139
- region: () =>
140
- select({
141
- message: 'Which AWS region do you want to deploy to?',
142
- options: [
143
- { value: 'us-east-1', label: 'us-east-1 (N. Virginia)' },
144
- { value: 'us-east-2', label: 'us-east-2 (Ohio)' },
145
- { value: 'eu-west-1', label: 'eu-west-1 (Ireland)' },
146
- { value: 'eu-central-1', label: 'EU (Frankfurt)' },
147
- { value: 'ap-southeast-2', label: 'Asia Pacific (Sydney)' },
148
- ],
149
- }),
150
- port: () =>
151
- text({
152
- message: 'What port does your container expose?',
153
- placeholder: defaultPort,
154
- defaultValue: defaultPort,
155
- }),
156
- size: () =>
157
- select({
158
- message: 'Select your Fargate compute size:',
159
- options: [
160
- { value: 'micro', label: 'Micro (0.25 vCPU, 512MB RAM) - Best for POCs' },
161
- { value: 'small', label: 'Small (0.5 vCPU, 1GB RAM) - Best for small Projects' },
162
- ],
163
- }),
164
- // --- Advanced-Only Prompts (Skipped if setupType === 'quick') ---
165
- healthCheckPath: () => {
166
- if (setupType === 'quick') return undefined;
167
- return text({
168
- message: 'ALB Health Check Path:',
169
- placeholder: '/',
170
- defaultValue: '/',
171
- });
172
- },
173
- desiredCount: () => {
174
- if (setupType === 'quick') return undefined;
175
- return select({
176
- message: 'How many container replicas (tasks) should run?',
177
- options: [
178
- { value: '1', label: '1 Task (Single instance - lowest cost)' },
179
- { value: '2', label: '2 Tasks (High Availability across AZs)' },
180
- ],
181
- defaultValue: '1',
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
- branch: () => {
185
- if (setupType === 'quick') return undefined;
186
- return text({
187
- message: 'Primary Git deployment branch for CI/CD:',
188
- placeholder: currentGitBranch,
189
- defaultValue: currentGitBranch,
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
- console.log('');
225
- const ciContent = await confirm({
226
- 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?'),
227
- initialValue: true,
228
- });
229
- if (typeof ciContent === 'symbol') {
230
- cancel('Provisioning cancelled.');
231
- process.exit(0);
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
- console.log(''); // Add a blank line for visual pacing
240
- const costConsent = await confirm({
241
- message: color.yellow(`⚠️ Pre-Flight Check: This AWS architecture will cost ${estimatedCost}. Proceed with provisioning?`),
242
- initialValue: true,
243
- });
244
-
245
- if (!costConsent || typeof costConsent === 'symbol') {
246
- cancel('Deployment cancelled. No AWS resources were provisioned.');
247
- process.exit(0);
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...');
@@ -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
- const overwriteDecision = await select({
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 (isCancel(overwriteDecision) || overwriteDecision === 'cancel') {
34
- cancel('Operation cancelled to protect existing files.');
35
- process.exit(0);
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