deploy-stack 0.11.1 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/bin/cli.js +2 -1
- package/package.json +1 -1
- package/src/commands/apply.js +22 -4
- package/src/commands/init.js +18 -32
- package/src/utils/visualizer.js +107 -0
package/README.md
CHANGED
|
@@ -68,6 +68,7 @@ The interactive wizard will analyze your codebase, detect your framework, estima
|
|
|
68
68
|
|
|
69
69
|
* **`npx deploy-stack apply`**
|
|
70
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
|
+
*Tip: Append `--dry-run` to preview the architecture topology and estimated cost without provisioning anything.*
|
|
71
72
|
|
|
72
73
|
* **`npx deploy-stack secrets push <file>`**
|
|
73
74
|
Securely encrypts your local environment variables (e.g., `.env.production`) into AWS Secrets Manager and maps them to your ECS container at runtime.
|
|
@@ -143,22 +144,21 @@ npx deploy-stack --no-telemetry
|
|
|
143
144
|
- [x] **Smart Experience:** Zero-config framework auto-discovery for static output directories.
|
|
144
145
|
- [x] **Trust & Observability:** DevSecOps Trivy scanning, automated 5XX alarms, 14-day log retention, and safe local overwrite protections.
|
|
145
146
|
|
|
146
|
-
### Phase 4: Trust Anchors & TAM Expansion (
|
|
147
|
+
### Phase 4: Trust Anchors & TAM Expansion (Completed)
|
|
147
148
|
- [x] **Ecosystem Distribution:** Native GitHub Marketplace Action for rapid discovery.
|
|
148
149
|
- [x] **Cost Transparency:** Pre-flight AWS cost estimator injected directly into the CLI wizard.
|
|
149
150
|
- [x] **Zero Vendor Lock-In:** Explicit `npx deploy-stack eject` command to safely strip `ManagedBy` tags and CLI metadata, leaving behind pure IaC.
|
|
150
151
|
- [x] **Heavy Backend Monoliths:** Hardened, unprivileged container adapters for Go, Nuxt.js, Django, and Rails, complete with automated zero-trust RDS PostgreSQL provisioning.
|
|
151
152
|
|
|
152
|
-
### Phase 5: The Activation Engine (
|
|
153
|
+
### Phase 5: The Activation Engine (Completed)
|
|
153
154
|
- [x] **Local Execution Wrapper:** Native `deploy-stack apply` command with terminal-optimized streaming to eliminate Terraform context switching.
|
|
154
|
-
- [x] **Ecosystem Integrations:**
|
|
155
|
-
- [ ] **Ephemeral PR Previews:** Generating live preview URLs on every GitHub Pull Request, turning single-user tests into team-wide advertisements.
|
|
156
|
-
- [ ] **GitHub Deployments UI Sync:** Wiring up the native GitHub "Environments" tab for instant visual validation that the CLI succeeded in the background.
|
|
155
|
+
- [x] **Ecosystem Integrations:** Official plugins published to the Astro Integrations directory (`astro-deploy-stack`) and Nuxt module registry (`nuxt-deploy-stack`).
|
|
157
156
|
|
|
158
|
-
### Phase 6:
|
|
159
|
-
- [
|
|
157
|
+
### Phase 6: Migration & Trust Engine (Current)
|
|
158
|
+
- [x] **Dry-Run Visualization:** Interactive pre-flight terminal UI with ASCII topology maps and precise, dynamic AWS cost estimation.
|
|
159
|
+
- [ ] **PaaS Importers:** Auto-parse `vercel.json` or Heroku `Procfile` configurations to map build commands and environment variables automatically.
|
|
160
160
|
- [ ] **Docker Compose to ECS Translator:** Automatically converting a familiar local `docker-compose.yml` into production ECS task definitions.
|
|
161
|
-
- [ ] **
|
|
161
|
+
- [ ] **AI Agent Rulesets:** Publishing `.cursorrules` and Copilot instructions that teach AI assistants exactly how to utilize the CLI on the user's behalf.
|
|
162
162
|
|
|
163
163
|
---
|
|
164
164
|
|
package/bin/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ const args = rawArgs.filter((arg) => arg !== '--no-telemetry');
|
|
|
20
20
|
|
|
21
21
|
// 3. Parse headless flags
|
|
22
22
|
const isHeadless = args.includes('--headless');
|
|
23
|
+
const isDryRun = args.includes('--dry-run');
|
|
23
24
|
const getFlag = (flagName) => {
|
|
24
25
|
const match = args.find(a => a.startsWith(`--${flagName}=`));
|
|
25
26
|
return match ? match.split('=')[1] : undefined;
|
|
@@ -41,7 +42,7 @@ if (args[0] === 'secrets' && args[1] === 'push') {
|
|
|
41
42
|
const projectName = path.basename(process.cwd());
|
|
42
43
|
pushSecrets(envFile, projectName).catch(console.error);
|
|
43
44
|
} else if (args[0] === 'apply') {
|
|
44
|
-
applyStack().catch(console.error);
|
|
45
|
+
applyStack({ isDryRun }).catch(console.error);
|
|
45
46
|
} else if (args[0] === 'doctor') {
|
|
46
47
|
runDoctor().catch(console.error);
|
|
47
48
|
} else if (args[0] === 'destroy') {
|
package/package.json
CHANGED
package/src/commands/apply.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { intro, outro, spinner, log, cancel } from '@clack/prompts';
|
|
5
5
|
import color from 'picocolors';
|
|
6
|
+
import { renderDryRunPreview, parseTerraformConfig } from '../utils/visualizer.js';
|
|
7
|
+
import { detectFramework } from '../utils/detector.js';
|
|
6
8
|
|
|
7
9
|
// Helper to run a command while piping the latest stdout line into a @clack spinner
|
|
8
10
|
function runTerraformCommand(args, cwd, spin, loadingPrefix) {
|
|
@@ -69,7 +71,7 @@ function getTerraformOutputs(cwd) {
|
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
export async function applyStack() {
|
|
74
|
+
export async function applyStack(options = {}) {
|
|
73
75
|
intro(color.bgCyan(color.black(' deploy-stack apply ☁️ ')));
|
|
74
76
|
|
|
75
77
|
const targetDir = process.cwd();
|
|
@@ -82,20 +84,36 @@ export async function applyStack() {
|
|
|
82
84
|
process.exit(1);
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
// 2. Read the actual AWS configuration from disk (CPU, Memory, Region, Database)
|
|
88
|
+
const detectedConfig = parseTerraformConfig(tfDir);
|
|
89
|
+
|
|
90
|
+
// 3. Detect the framework name using your existing detector
|
|
91
|
+
const detectedFw = detectFramework(targetDir);
|
|
92
|
+
detectedConfig.framework = detectedFw ? detectedFw.name : 'Container';
|
|
93
|
+
|
|
94
|
+
// 4. Run the visualizer (passing the flag so it knows whether to prompt)
|
|
95
|
+
if (options.isDryRun) {
|
|
96
|
+
await renderDryRunPreview(detectedConfig, true);
|
|
97
|
+
outro(color.green('Dry run complete. No infrastructure was provisioned.'));
|
|
98
|
+
process.exit(0);
|
|
99
|
+
} else {
|
|
100
|
+
await renderDryRunPreview(detectedConfig, false);
|
|
101
|
+
}
|
|
102
|
+
|
|
85
103
|
const s = spinner();
|
|
86
104
|
|
|
87
105
|
try {
|
|
88
|
-
//
|
|
106
|
+
// 5. Run terraform init
|
|
89
107
|
s.start('Initializing Terraform plugins...');
|
|
90
108
|
await runTerraformCommand(['init', '-upgrade'], tfDir, s, 'Initializing');
|
|
91
109
|
log.success('Terraform initialized.');
|
|
92
110
|
|
|
93
|
-
//
|
|
111
|
+
// 6. Run terraform apply
|
|
94
112
|
s.start('Provisioning AWS infrastructure (this may take 3–5 minutes)...');
|
|
95
113
|
await runTerraformCommand(['apply', '-auto-approve'], tfDir, s, 'Provisioning');
|
|
96
114
|
s.stop('AWS infrastructure provisioned successfully!');
|
|
97
115
|
|
|
98
|
-
//
|
|
116
|
+
// 7. Extract and print the outputs
|
|
99
117
|
const outputs = await getTerraformOutputs(tfDir);
|
|
100
118
|
|
|
101
119
|
const cfUrl = outputs.cloudfront_url?.value;
|
package/src/commands/init.js
CHANGED
|
@@ -12,6 +12,7 @@ import { getFrameworkWarning } from '../utils/warnings.js';
|
|
|
12
12
|
import { provisionStateBucket } from '../utils/aws.js';
|
|
13
13
|
import { generateTemplates } from '../utils/generator.js';
|
|
14
14
|
import { handleExistingFiles } from '../utils/backup.js';
|
|
15
|
+
import { estimateMonthlyCost } from '../utils/visualizer.js';
|
|
15
16
|
|
|
16
17
|
export async function mainStack({ isHeadless = false, headlessOptions = {} } = {}) {
|
|
17
18
|
const startTime = Date.now();
|
|
@@ -60,7 +61,7 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
60
61
|
intro(color.bgCyan(color.black(' deploy-stack ☁️ ')));
|
|
61
62
|
|
|
62
63
|
// 2. Ask for the target directory FIRST
|
|
63
|
-
|
|
64
|
+
projectName = await text({
|
|
64
65
|
message: 'Where should we generate the infrastructure? (Type "." for current directory)',
|
|
65
66
|
placeholder: '.',
|
|
66
67
|
initialValue: '.',
|
|
@@ -75,8 +76,8 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
75
76
|
process.exit(0);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
actualProjectName = projectName === '.' ? path.basename(process.cwd()) : projectName;
|
|
80
|
+
targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
|
|
80
81
|
|
|
81
82
|
// 2.5 Run the scanner
|
|
82
83
|
const detectedFramework = detectFramework(targetDir);
|
|
@@ -85,7 +86,7 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
// 2.6 Resolve the framework
|
|
88
|
-
|
|
89
|
+
finalFramework = detectedFramework ? detectedFramework.id : null;
|
|
89
90
|
|
|
90
91
|
if (!finalFramework) {
|
|
91
92
|
finalFramework = await select({
|
|
@@ -109,7 +110,7 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
// --- DJANGO SPECIFIC PROMPT ---
|
|
112
|
-
|
|
113
|
+
djangoWsgi = 'core.wsgi';
|
|
113
114
|
if (finalFramework === 'django') {
|
|
114
115
|
djangoWsgi = await text({
|
|
115
116
|
message: 'What is the Python module path to your Django wsgi.py?',
|
|
@@ -120,7 +121,7 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
// 3. Prompt for Setup Mode
|
|
123
|
-
|
|
124
|
+
setupType = await select({
|
|
124
125
|
message: 'Choose your setup mode:',
|
|
125
126
|
options: [
|
|
126
127
|
{ value: 'quick', label: '⚡ Quickstart (Recommended)', hint: 'Production defaults, minimal prompts' },
|
|
@@ -141,7 +142,7 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
141
142
|
if (finalFramework === 'rails') defaultPort = '3000';
|
|
142
143
|
if (finalFramework === 'go') defaultPort = '8080';
|
|
143
144
|
|
|
144
|
-
|
|
145
|
+
currentGitBranch = 'main';
|
|
145
146
|
try {
|
|
146
147
|
currentGitBranch = execSync('git symbolic-ref --short HEAD', { cwd: targetDir, stdio: 'pipe' }).toString().trim();
|
|
147
148
|
} catch (e) {
|
|
@@ -149,7 +150,7 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
// 5. Ask for Managed Database (Only for Backend/Fullstack Frameworks)
|
|
152
|
-
|
|
153
|
+
needsDatabase = false;
|
|
153
154
|
const isBackendFramework = ['node', 'nextjs', 'nuxt', 'python', 'django', 'rails', 'go'].includes(finalFramework);
|
|
154
155
|
|
|
155
156
|
if (isBackendFramework) {
|
|
@@ -166,7 +167,7 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
// 6. Prompt Configuration Group
|
|
169
|
-
|
|
170
|
+
project = await group(
|
|
170
171
|
{
|
|
171
172
|
region: () =>
|
|
172
173
|
select({
|
|
@@ -234,20 +235,17 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
234
235
|
// 7. Map the user's choices and update the variables
|
|
235
236
|
const cpu = project.size === 'small' ? '512' : '256';
|
|
236
237
|
const memory = project.size === 'small' ? '1024' : '512';
|
|
237
|
-
|
|
238
|
-
const baseCost = project.size === 'small' ? 35 : 25;
|
|
239
|
-
const dbCost = needsDatabase ? 14 : 0;
|
|
240
|
-
const totalCost = baseCost + dbCost;
|
|
241
|
-
|
|
242
238
|
const computeTier = project.size === 'small' ? 'Small (0.5 vCPU, 1GB RAM)' : 'Micro (0.25 vCPU, 512MB RAM)';
|
|
243
|
-
|
|
239
|
+
|
|
240
|
+
const costs = estimateMonthlyCost({ cpu: parseInt(cpu), memory: parseInt(memory), hasDb: needsDatabase });
|
|
241
|
+
const estimatedCost = `~$${costs.totalMonthly} / month${needsDatabase ? ' (Includes Fargate + RDS PostgreSQL)' : ''}`;
|
|
244
242
|
|
|
245
243
|
const healthCheckPath = project.healthCheckPath || '/';
|
|
246
244
|
const desiredCount = project.desiredCount || '1';
|
|
247
245
|
const deployBranch = project.branch || currentGitBranch;
|
|
248
246
|
const buildDir = detectedFramework?.buildDir || 'dist';
|
|
249
247
|
|
|
250
|
-
// 7.
|
|
248
|
+
// 7.1 Check for conflicting CI boilerplate (Rails)
|
|
251
249
|
if (finalFramework === 'rails') {
|
|
252
250
|
const ciPath = path.join(targetDir, '.github', 'workflows', 'ci.yml');
|
|
253
251
|
const dependabotPath = path.join(targetDir, '.github', 'dependabot.yml');
|
|
@@ -271,20 +269,6 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
271
269
|
}
|
|
272
270
|
}
|
|
273
271
|
|
|
274
|
-
// 7.5. The Pre-Flight Cost Estimator
|
|
275
|
-
// We explicitly ask for financial consent to eliminate AWS billing anxiety.
|
|
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
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
272
|
// 8. Safely handle existing files (Backup and auto-prune)
|
|
289
273
|
await handleExistingFiles(targetDir, isHeadless);
|
|
290
274
|
|
|
@@ -350,8 +334,10 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
350
334
|
|
|
351
335
|
const isGitInitialized = fsSync.existsSync(path.join(targetDir, '.git'));
|
|
352
336
|
|
|
353
|
-
const
|
|
354
|
-
const applyStep =
|
|
337
|
+
const needsCd = projectName && projectName !== '.';
|
|
338
|
+
const applyStep = needsCd
|
|
339
|
+
? `cd ${projectName} && npx deploy-stack apply`
|
|
340
|
+
: 'npx deploy-stack apply';
|
|
355
341
|
|
|
356
342
|
const gitInstructions = isGitInitialized
|
|
357
343
|
? `git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git push`
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { note, confirm, isCancel, cancel } from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
// Cost benchmarks for AWS us-east-1 baseline (Fargate + ALB)
|
|
7
|
+
const PRICING_TABLE = {
|
|
8
|
+
fargate: {
|
|
9
|
+
cpuPerHour: 0.04048, // per vCPU hour
|
|
10
|
+
memoryPerHour: 0.004445 // per GB hour
|
|
11
|
+
},
|
|
12
|
+
alb: {
|
|
13
|
+
basePerHour: 0.0225, // ~$16.20/month
|
|
14
|
+
lcuPerHour: 0.008 // Baseline ~1 LCU (~$5.76/month)
|
|
15
|
+
},
|
|
16
|
+
rds: {
|
|
17
|
+
microPerHour: 0.016, // ~$11.68/mo for db.t4g.micro
|
|
18
|
+
storagePerMonth: 2.30 // 20GB gp3 storage baseline
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// 1. Parse the local terraform files to extract the actual configuration
|
|
23
|
+
export function parseTerraformConfig(tfDir) {
|
|
24
|
+
const tfvarsPath = path.join(tfDir, 'terraform.tfvars');
|
|
25
|
+
let region = 'us-east-1';
|
|
26
|
+
let cpu = 256;
|
|
27
|
+
let memory = 512;
|
|
28
|
+
let framework = 'Application';
|
|
29
|
+
|
|
30
|
+
if (fs.existsSync(tfvarsPath)) {
|
|
31
|
+
const content = fs.readFileSync(tfvarsPath, 'utf-8');
|
|
32
|
+
|
|
33
|
+
// Use regex to pull values out of the HCL format
|
|
34
|
+
const regionMatch = content.match(/aws_region\s*=\s*"([^"]+)"/);
|
|
35
|
+
if (regionMatch) region = regionMatch[1];
|
|
36
|
+
|
|
37
|
+
const cpuMatch = content.match(/container_cpu\s*=\s*(\d+)/);
|
|
38
|
+
if (cpuMatch) cpu = parseInt(cpuMatch[1], 10);
|
|
39
|
+
|
|
40
|
+
const memoryMatch = content.match(/container_memory\s*=\s*(\d+)/);
|
|
41
|
+
if (memoryMatch) memory = parseInt(memoryMatch[1], 10);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if database files exist
|
|
45
|
+
const hasDb = fs.existsSync(path.join(tfDir, 'rds.tf')) || fs.existsSync(path.join(tfDir, 'database.tf'));
|
|
46
|
+
|
|
47
|
+
return { framework, region, cpu, memory, hasDb };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Calculate itemized monthly costs based on task definition settings
|
|
51
|
+
export function estimateMonthlyCost({ cpu = 256, memory = 512, hasDb = false }) {
|
|
52
|
+
const vCpu = cpu / 1024;
|
|
53
|
+
const memGb = memory / 1024;
|
|
54
|
+
const hoursInMonth = 730;
|
|
55
|
+
|
|
56
|
+
const fargateCost = ((vCpu * PRICING_TABLE.fargate.cpuPerHour) +
|
|
57
|
+
(memGb * PRICING_TABLE.fargate.memoryPerHour)) * hoursInMonth;
|
|
58
|
+
const albCost = (PRICING_TABLE.alb.basePerHour + PRICING_TABLE.alb.lcuPerHour) * hoursInMonth;
|
|
59
|
+
const dbCost = hasDb ? (PRICING_TABLE.rds.microPerHour * hoursInMonth) + PRICING_TABLE.rds.storagePerMonth : 0;
|
|
60
|
+
|
|
61
|
+
const total = fargateCost + albCost + dbCost;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
fargateMonthly: fargateCost.toFixed(2),
|
|
65
|
+
albMonthly: albCost.toFixed(2),
|
|
66
|
+
dbMonthly: dbCost.toFixed(2),
|
|
67
|
+
totalMonthly: total.toFixed(2)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Render the terminal architecture visualization and requests confirmation
|
|
72
|
+
export async function renderDryRunPreview(config, isDryRunFlag = false) {
|
|
73
|
+
const { framework = 'Node.js', region = 'us-east-1', cpu = 256, memory = 512, hasDb = false } = config;
|
|
74
|
+
const cost = estimateMonthlyCost({ cpu, memory, hasDb });
|
|
75
|
+
|
|
76
|
+
const hourlyRate = (cost.totalMonthly / 730).toFixed(3); // 730 hours in a month
|
|
77
|
+
|
|
78
|
+
const treeOutput = [
|
|
79
|
+
`${pc.bold('Topology')} (${pc.cyan(region)}):`,
|
|
80
|
+
` ${pc.gray('├──')} 🌐 ${pc.bold('ALB')} (Public Entry & Health: ${pc.green('200 OK')})`,
|
|
81
|
+
` ${pc.gray('├──')} 🔒 ${pc.bold('IAM OIDC')} (GitHub Auth) & 🐳 ${pc.bold('ECR')} (Registry)`,
|
|
82
|
+
` ${pc.gray('└──')} 📦 ${pc.bold('ECS Fargate Cluster')} 🟢 ${pc.green(framework)} [${cpu} CPU / ${memory} MB]`,
|
|
83
|
+
hasDb ? ` └── 🛢️ ${pc.yellow('Amazon RDS')} (PostgreSQL managed instance)` : '',
|
|
84
|
+
'',
|
|
85
|
+
`${pc.bold('Est. Monthly Cost:')} ${pc.green(pc.bold(`~$${cost.totalMonthly}`))} ${pc.dim(`(ALB: $${cost.albMonthly}, Fargate: $${cost.fargateMonthly}${hasDb ? `, RDS: $${cost.dbMonthly}` : ''})`)}`,
|
|
86
|
+
` ${pc.dim(`* Hourly billing: ~$${hourlyRate}/hr. Destroy anytime with "npx deploy-stack destroy --yes"`)}`
|
|
87
|
+
].filter(Boolean).join('\n');
|
|
88
|
+
|
|
89
|
+
note(treeOutput, 'Cloud Infrastructure Pre-Flight Inspection');
|
|
90
|
+
|
|
91
|
+
// 4. Check if this is a dry run (print & exit) or full apply (prompt & proceed)
|
|
92
|
+
if (isDryRunFlag) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const shouldProceed = await confirm({
|
|
97
|
+
message: 'Review completed. Provision this infrastructure to AWS now?',
|
|
98
|
+
initialValue: true
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (isCancel(shouldProceed) || !shouldProceed) {
|
|
102
|
+
cancel('Operation canceled. No infrastructure was created.');
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return true;
|
|
107
|
+
}
|