deploy-stack 0.1.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.
@@ -0,0 +1,28 @@
1
+ name: Publish CLI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build-and-publish:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - name: Setup Node.js
14
+ uses: actions/setup-node@v4
15
+ with:
16
+ node-version: '20.x'
17
+ registry-url: 'https://registry.npmjs.org'
18
+
19
+ - name: Inject Telemetry Key
20
+ # This securely swaps the placeholder for the real key right before publishing
21
+ run: sed -i "s/___POSTHOG_INGESTION_KEY___/${{ secrets.POSTHOG_KEY }}/g" src/core/telemetry.js
22
+
23
+ - run: npm install
24
+
25
+ - name: Publish to NPM
26
+ run: npm publish
27
+ env:
28
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # deploy-stack ☁️🚀
2
+
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
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ ---
8
+
9
+ ## The Problem
10
+
11
+ Managed platforms like Vercel, Heroku, or Render offer rapid initial deployments, but costs escalate quickly with seat pricing, compute caps, and bandwidth markups.
12
+
13
+ Migrating directly to AWS provides greater cost efficiency and infrastructure control. However, architecting raw Terraform for ECS clusters, Application Load Balancers, CloudFront distributions, and keyless CI/CD pipelines typically requires writing hundreds of lines of complex boilerplate infrastructure code.
14
+
15
+ ## The Solution
16
+
17
+ **`deploy-stack`** is an interactive CLI that streamlines the process. It analyzes your project requirements and generates **clean, readable, and completely ejectable Terraform and GitHub Actions workflows** directly inside your repository.
18
+
19
+ You retain complete ownership of your infrastructure code without relying on black-box platforms.
20
+
21
+ ---
22
+
23
+ ## ✨ Features
24
+
25
+ * **Zero Vendor Lock-In:** Generates standard, clean `.tf` files. Modify, expand, or decouple them at any time.
26
+ * **Framework Agnostic:** Tailored container presets for Next.js (standalone), Express.js, FastAPI, and custom Docker setups.
27
+ * **Production-Grade Defaults:** Automatically provisions an Amazon ECS Fargate cluster fronted by an Application Load Balancer across multiple availability zones.
28
+ * **Global Edge Acceleration:** Includes an integrated AWS CloudFront CDN distribution with SSL termination and optimized caching.
29
+ * **Keyless, Zero-Secret CI/CD:** Uses AWS IAM OpenID Connect (OIDC) for automated GitHub Actions deployments—no long-lived AWS keys stored in GitHub Secrets.
30
+ * **Remote State with Native S3 Locking:** Automatically creates an encrypted S3 state bucket utilizing modern native S3 concurrency locking.
31
+ * **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.
32
+ * **Non-Destructive:** Safely analyzes existing directories and prompts for confirmation before updating any files.
33
+
34
+ ---
35
+
36
+ ## 🚀 Quick Start
37
+
38
+ Run the CLI directly in your project root:
39
+
40
+ ```bash
41
+ npx deploy-stack
42
+ ```
43
+
44
+ The interactive CLI will guide you through the setup:
45
+ 1. **Target Directory / Name:** (Type `.` to bootstrap your current directory)
46
+ 2. **Setup Mode:** (Choose **Quickstart** for sensible defaults, or **Advanced** to customize health checks, scaling, and branch names)
47
+ 3. **AWS Region:** (e.g., `us-east-2`)
48
+ 4. **Compute Tier:** (Choose from Micro, Small, Medium, or Large with live monthly cost estimates)
49
+ 5. **Target Port & Framework:** (Generates an optimized multi-stage `Dockerfile`)
50
+
51
+ ---
52
+
53
+ ## 🔑 Securely Managing Secrets
54
+
55
+ Avoid committing sensitive environment files to version control. Push local variables directly to AWS:
56
+
57
+ ```bash
58
+ npx deploy-stack secrets push .env.production
59
+ ```
60
+ *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.*
61
+
62
+ ---
63
+
64
+ ## 🛠️ Next Steps After Generation
65
+
66
+ 1. **Provision Infrastructure:**
67
+ ```bash
68
+ cd terraform
69
+ terraform init
70
+ terraform apply
71
+ ```
72
+ 2. **Push to GitHub:**
73
+ ```bash
74
+ git add .
75
+ git commit -m "feat: infrastructure and deployment pipeline"
76
+ git push origin main
77
+ ```
78
+ 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.
79
+
80
+ ---
81
+
82
+ ## 📁 Generated File Structure
83
+
84
+ Running the CLI generates a modular architecture tailored to your service:
85
+
86
+ ```text
87
+ your-project/
88
+ ├── Dockerfile # Multi-stage container preset
89
+ ├── .github/
90
+ │ └── workflows/
91
+ │ └── deploy.yml # Keyless OIDC CI/CD deployment pipeline
92
+ └── terraform/
93
+ ├── main.tf # ECR repository, ECS Cluster, and Fargate Task
94
+ ├── network.tf # VPC, Public Subnets, ALB, and Security Groups
95
+ ├── cloudfront.tf # CloudFront CDN edge distribution
96
+ ├── oidc.tf # GitHub Actions keyless IAM OIDC Provider & Roles
97
+ ├── secrets.tf # AWS Secrets Manager integration
98
+ ├── backend.tf # S3 Remote State backend with native locking
99
+ └── secret_keys.json # Dynamic key map for injected environment variables
100
+ ```
101
+
102
+ ---
103
+
104
+ ## 📦 Reference Implementations
105
+
106
+ * **[Next.js Fullstack Reference App](https://github.com/anton-codes-iac/deploy-stack-nextjs-example):** A complete Next.js deployment showcasing the generated Terraform, CloudFront setup, and automated OIDC workflow.
107
+
108
+ ---
109
+
110
+ ### 🛡️ Telemetry & Privacy
111
+ 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.**
112
+
113
+ To opt out, simply append the flag:
114
+ \`\`\`bash
115
+ npx deploy-stack --no-telemetry
116
+ \`\`\`
117
+
118
+ ---
119
+
120
+ ## 🗺️ Roadmap
121
+
122
+ - [x] **POC:** Interactive CLI, ECS Fargate + ALB Terraform generation, GitHub Actions CI/CD, and Secrets sync.
123
+ - [x] **MVP:** CloudFront distribution, Remote S3 State locking, OIDC security, and CLI configuration flows.
124
+ - [ ] **v0.9:** Zero-config framework detection (auto-discovering Next.js, Express, and FastAPI via `package.json` / `requirements.txt`).
125
+ - [ ] **v1.0 (Pro):** Automated RDS PostgreSQL provisioning, custom domain Route 53 / ACM SSL configuration, and environment workspaces (`staging` vs `prod`).
126
+ - [ ] **v2.0 (Enterprise):** Private VPC subnets with NAT gateways, AWS WAF integration, and automated compliance scanning.
127
+
128
+ ---
129
+
130
+ ## 📜 License
131
+
132
+ Distributed under the **MIT License**. See [LICENSE](LICENSE) for more information.
package/bin/cli.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import path from 'path';
3
+ import { runDoctor } from '../src/commands/doctor.js';
4
+ import { pushSecrets } from '../src/commands/secrets.js';
5
+ import { mainStack } from '../src/commands/init.js';
6
+
7
+ // 1. Extract the telemetry flag and set the environment variable
8
+ const rawArgs = process.argv.slice(2);
9
+ const hasNoTelemetry = rawArgs.includes('--no-telemetry');
10
+
11
+ if (hasNoTelemetry) {
12
+ process.env.DO_NOT_TRACK = '1';
13
+ }
14
+
15
+ // 2. Filter out the telemetry flag from the args so the subcommands don't see it
16
+ const args = rawArgs.filter((arg) => arg !== '--no-telemetry');
17
+
18
+ // 3. Handle commands
19
+ if (args[0] === 'secrets' && args[1] === 'push') {
20
+ const envFile = args[2] || '.env';
21
+ const projectName = path.basename(process.cwd());
22
+ pushSecrets(envFile, projectName).catch(console.error);
23
+ } else if (args[0] === 'doctor') {
24
+ runDoctor().catch(console.error);
25
+ } else {
26
+ mainStack().catch(console.error);
27
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "deploy-stack",
3
+ "version": "0.1.0",
4
+ "description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
5
+ "type": "module",
6
+ "bin": {
7
+ "deploy-stack": "bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "author": "anton-codes-iac",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/anton-codes-iac/deploy-stack.git"
16
+ },
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "@aws-sdk/client-s3": "3.1115.0",
20
+ "@aws-sdk/client-secrets-manager": "3.1112.0",
21
+ "@aws-sdk/client-sts": "3.1115.0",
22
+ "@clack/prompts": "1.7.0",
23
+ "dotenv": "17.4.2",
24
+ "picocolors": "1.1.1"
25
+ }
26
+ }
@@ -0,0 +1,39 @@
1
+ import { intro, outro, spinner } from '@clack/prompts';
2
+ import color from 'picocolors';
3
+ import { checkDependency } from '../utils/system.js';
4
+
5
+ export async function runDoctor() {
6
+ intro(color.bgCyan(color.black(' deploy-stack ☁️ ')));
7
+
8
+ const s = spinner();
9
+ s.start('Running pre-flight checks...');
10
+
11
+ const [hasTerraform, hasAws, hasDocker, hasGit] = await Promise.all([
12
+ checkDependency('terraform'),
13
+ checkDependency('aws'),
14
+ checkDependency('docker'),
15
+ checkDependency('git')
16
+ ]);
17
+
18
+ s.stop('Pre-flight checks complete.\n');
19
+
20
+ const printStatus = (ok, label, fix) => {
21
+ const icon = ok ? color.green('✅') : color.red('❌');
22
+ const message = ok ? `${label} (✓)` : `${label} (✗)`;
23
+ console.log(` ${icon} ${message}`);
24
+ if (!ok) {
25
+ console.log(` ┌─ Try: ${color.dim(fix)}`);
26
+ }
27
+ };
28
+
29
+ printStatus(hasTerraform, 'Terraform', 'Install via Homebrew: brew install terraform');
30
+ printStatus(hasAws, 'AWS CLI', 'Install via Homebrew: brew install awscli');
31
+ printStatus(hasDocker, 'Docker', 'Install via Homebrew: brew install docker');
32
+ printStatus(hasGit, 'Git', 'Install via Homebrew: brew install git');
33
+
34
+ if (hasTerraform && hasAws && hasDocker && hasGit) {
35
+ outro(color.green('Your system is 100% ready to provision and deploy! 🚀'));
36
+ } else {
37
+ outro(color.yellow('Please install the missing dependencies before running the provisioning tool.'));
38
+ }
39
+ }
@@ -0,0 +1,331 @@
1
+ import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
2
+ import { S3Client, CreateBucketCommand, PutBucketVersioningCommand } from '@aws-sdk/client-s3';
3
+ import fsSync from 'fs';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import { intro, outro, group, text, select, spinner, cancel, confirm } from '@clack/prompts';
8
+ import color from 'picocolors';
9
+
10
+ import { checkDependency } from '../utils/system.js';
11
+ import { trackEvent } from '../core/telemetry.js';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ export async function mainStack() {
17
+ const startTime = Date.now();
18
+
19
+ // 0. Silent Pre-flight check
20
+ const hasTerraform = await checkDependency('terraform');
21
+ if (!hasTerraform) {
22
+ console.error(color.red('✖ Terraform is not installed.'));
23
+ console.log(color.yellow('Please run "npx deploy-stack doctor" to check your environment.'));
24
+ process.exit(1);
25
+ }
26
+
27
+ // 1. Start the CLI
28
+ intro(color.bgCyan(color.black(' deploy-stack ☁️ ')));
29
+
30
+ // 2. Ask the user for project configuration
31
+ const setupType = await select({
32
+ message: 'Choose your setup mode:',
33
+ options: [
34
+ { value: 'quick', label: '⚡ Quickstart (Recommended)', hint: 'Production defaults, minimal prompts' },
35
+ { value: 'advanced', label: '🛠️ Advanced Configuration', hint: 'Customize health checks, task count, branch, etc.' },
36
+ ],
37
+ });
38
+
39
+ if (typeof setupType === 'symbol') {
40
+ cancel('Operation cancelled.');
41
+ process.exit(0);
42
+ }
43
+
44
+ // 3. Prompt Configuration Group
45
+ const project = await group(
46
+ {
47
+ name: () =>
48
+ text({
49
+ message: 'What is the name of your project? (Type "." to use current directory)',
50
+ placeholder: 'my-web-app',
51
+ validate: (value) => {
52
+ if (!value) return 'Please enter a name.';
53
+ if (value !== '.' && value.includes(' ')) return 'Name cannot contain spaces.';
54
+ },
55
+ }),
56
+ region: () =>
57
+ select({
58
+ message: 'Which AWS region do you want to deploy to?',
59
+ options: [
60
+ { value: 'us-east-1', label: 'us-east-1 (N. Virginia)' },
61
+ { value: 'us-east-2', label: 'us-east-2 (Ohio)' },
62
+ { value: 'eu-west-1', label: 'eu-west-1 (Ireland)' },
63
+ { value: 'eu-central-1', label: 'EU (Frankfurt)' },
64
+ { value: 'ap-southeast-2', label: 'Asia Pacific (Sydney)' },
65
+ ],
66
+ }),
67
+ port: () =>
68
+ text({
69
+ message: 'What port does your container expose?',
70
+ placeholder: '3000',
71
+ defaultValue: '3000',
72
+ }),
73
+ framework: () =>
74
+ select({
75
+ message: 'Which framework preset should we configure?',
76
+ options: [
77
+ { value: 'node', label: 'Node.js / Express' },
78
+ { value: 'nextjs', label: 'Next.js (Standalone)' },
79
+ { value: 'python', label: 'Python FastAPI' },
80
+ { value: 'static', label: 'Static Site (Gatsby, React, plain HTML via Nginx)' },
81
+ ],
82
+ }),
83
+ size: () =>
84
+ select({
85
+ message: 'Select your Fargate compute size:',
86
+ options: [
87
+ { value: 'micro', label: 'Micro (0.25 vCPU, 512MB RAM) - Best for POCs' },
88
+ { value: 'small', label: 'Small (0.5 vCPU, 1GB RAM) - Best for small Projects' },
89
+ ],
90
+ }),
91
+ // --- Advanced-Only Prompts (Skipped if setupType === 'quick') ---
92
+ healthCheckPath: () => {
93
+ if (setupType === 'quick') return undefined;
94
+ return text({
95
+ message: 'ALB Health Check Path:',
96
+ placeholder: '/',
97
+ defaultValue: '/',
98
+ });
99
+ },
100
+ desiredCount: () => {
101
+ if (setupType === 'quick') return undefined;
102
+ return select({
103
+ message: 'How many container replicas (tasks) should run?',
104
+ options: [
105
+ { value: '1', label: '1 Task (Single instance - lowest cost)' },
106
+ { value: '2', label: '2 Tasks (High Availability across AZs)' },
107
+ ],
108
+ defaultValue: '1',
109
+ });
110
+ },
111
+ branch: () => {
112
+ if (setupType === 'quick') return undefined;
113
+ return text({
114
+ message: 'Primary Git deployment branch for CI/CD:',
115
+ placeholder: 'main',
116
+ defaultValue: 'main',
117
+ });
118
+ },
119
+ },
120
+ {
121
+ onCancel: () => {
122
+ cancel('Provisioning cancelled.');
123
+ process.exit(0);
124
+ },
125
+ }
126
+ );
127
+
128
+ // 4. Map the user's choices and update the variables
129
+ const actualProjectName = project.name === '.' ? path.basename(process.cwd()) : project.name;
130
+ const targetDir = project.name === '.' ? process.cwd() : path.join(process.cwd(), project.name);
131
+
132
+ const cpu = project.size === 'small' ? '512' : '256';
133
+ const memory = project.size === 'small' ? '1024' : '512';
134
+
135
+ const computeTier = project.size === 'small' ? 'Small (0.5 vCPU, 1GB RAM)' : 'Micro (0.25 vCPU, 512MB RAM)';
136
+ const estimatedCost = project.size === 'small' ? '~$35.00 / month' : '~$25.00 / month';
137
+
138
+ const healthCheckPath = project.healthCheckPath || '/';
139
+ const desiredCount = project.desiredCount || '1';
140
+ const deployBranch = project.branch || 'main';
141
+
142
+ // 5. Check for existing files that might be overwritten
143
+ const dockerfilePath = path.join(targetDir, 'Dockerfile');
144
+ const tfDirPath = path.join(targetDir, 'terraform');
145
+
146
+ if (fsSync.existsSync(dockerfilePath) || fsSync.existsSync(tfDirPath)) {
147
+ const overwrite = await confirm({
148
+ message: color.yellow('⚠️ A Dockerfile or terraform/ folder already exists here. Overwrite them?'),
149
+ initialValue: false,
150
+ });
151
+
152
+ if (!overwrite) {
153
+ cancel('Provisioning cancelled to protect existing files.');
154
+ process.exit(0);
155
+ }
156
+ }
157
+
158
+ const s = spinner();
159
+ s.start('Provisioning infrastructure...');
160
+
161
+ // 6. Provision S3 bucket for Terraform state & enable versioning
162
+ const stsClient = new STSClient({ region: project.region });
163
+ let awsAccountId;
164
+ try {
165
+ const { Account } = await stsClient.send(new GetCallerIdentityCommand({}));
166
+ awsAccountId = Account;
167
+ } catch (error) {
168
+ s.stop('❌ Failed to authenticate with AWS.');
169
+ console.error(color.red(`AWS Error: Ensure your credentials are valid. (${error.name})`));
170
+
171
+ trackEvent('cli-error', { step: 'aws_sts_auth', error_code: error.name });
172
+ process.exit(1);
173
+ }
174
+
175
+ let stateBucketName = `${actualProjectName}-tfstate-${awsAccountId}`.toLowerCase().replace(/[^a-z0-9-]/g, '-');
176
+ if (stateBucketName.length > 63) {
177
+ stateBucketName = stateBucketName.substring(0, 63).replace(/-$/, '');
178
+ }
179
+
180
+ const s3Client = new S3Client({ region: project.region });
181
+ try {
182
+ await s3Client.send(new CreateBucketCommand({
183
+ Bucket: stateBucketName,
184
+ CreateBucketConfiguration: project.region === 'us-east-1' ? undefined : { LocationConstraint: project.region }
185
+ }));
186
+
187
+ await s3Client.send(new PutBucketVersioningCommand({
188
+ Bucket: stateBucketName,
189
+ VersioningConfiguration: { Status: 'Enabled' }
190
+ }));
191
+ } catch (error) {
192
+ if (error.name !== 'BucketAlreadyOwnedByYou') {
193
+ s.stop('❌ Failed to provision remote state.');
194
+ console.error(color.red(`AWS S3 Error: ${error.message}`));
195
+
196
+ trackEvent('cli-error', { step: 's3_bucket_creation', error_code: error.name });
197
+ process.exit(1);
198
+ }
199
+ }
200
+
201
+ s.message('Synthesizing Terraform templates...');
202
+
203
+ // 7. Create project directories
204
+ await fs.mkdir(targetDir, { recursive: true });
205
+ await fs.mkdir(path.join(targetDir, 'terraform'), { recursive: true });
206
+ await fs.mkdir(path.join(targetDir, '.github', 'workflows'), { recursive: true });
207
+
208
+ // 8. Read the template files
209
+ const tfMainPath = path.join(__dirname, '../../templates', 'terraform', 'main.tf');
210
+ const tfNetworkPath = path.join(__dirname, '../../templates', 'terraform', 'network.tf');
211
+ const tfSecretsPath = path.join(__dirname, '../../templates', 'terraform', 'secrets.tf');
212
+ const tfOidcPath = path.join(__dirname, '../../templates', 'terraform', 'oidc.tf');
213
+ const tfBackendPath = path.join(__dirname, '../../templates', 'terraform', 'backend.tf');
214
+ const tfCloudfrontPath = path.join(__dirname, '../../templates', 'terraform', 'cloudfront.tf');
215
+ const dockerTemplatePath = path.join(__dirname, '../../templates', 'docker', `${project.framework}.Dockerfile`);
216
+ const githubActionPath = path.join(__dirname, '../../templates', 'github', 'deploy.yml');
217
+ const readmePath = path.join(__dirname, '../../templates', 'README.md');
218
+ const gitignorePath = path.join(__dirname, '../../templates', '_gitignore');
219
+
220
+ let tfMain = await fs.readFile(tfMainPath, 'utf-8');
221
+ let tfNetwork = await fs.readFile(tfNetworkPath, 'utf-8');
222
+ let tfSecrets = await fs.readFile(tfSecretsPath, 'utf-8');
223
+ let tfOidc = await fs.readFile(tfOidcPath, 'utf-8');
224
+ let tfBackend = await fs.readFile(tfBackendPath, 'utf-8');
225
+ let tfCloudfront = await fs.readFile(tfCloudfrontPath, 'utf-8');
226
+ let dockerContent = await fs.readFile(dockerTemplatePath, 'utf-8');
227
+ let githubAction = await fs.readFile(githubActionPath, 'utf-8');
228
+ let readmeContent = await fs.readFile(readmePath, 'utf-8');
229
+ let gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
230
+
231
+ // 9. Update the variables
232
+ const injectVariables = (content) => {
233
+ return content
234
+ .replace(/{{PROJECT_NAME}}/g, actualProjectName)
235
+ .replace(/{{REGION}}/g, project.region)
236
+ .replace(/{{PORT}}/g, project.port)
237
+ .replace(/{{CPU}}/g, cpu)
238
+ .replace(/{{MEMORY}}/g, memory)
239
+ .replace(/{{COMPUTE_TIER}}/g, computeTier)
240
+ .replace(/{{ESTIMATED_COST}}/g, estimatedCost)
241
+ .replace(/{{STATE_BUCKET}}/g, stateBucketName)
242
+ .replace(/{{AWS_ACCOUNT_ID}}/g, awsAccountId)
243
+ .replace(/{{HEALTH_CHECK_PATH}}/g, healthCheckPath)
244
+ .replace(/{{DESIRED_COUNT}}/g, desiredCount)
245
+ .replace(/{{DEPLOY_BRANCH}}/g, deployBranch);
246
+ };
247
+
248
+ tfMain = injectVariables(tfMain);
249
+ tfNetwork = injectVariables(tfNetwork);
250
+ tfSecrets = injectVariables(tfSecrets);
251
+ tfOidc = injectVariables(tfOidc);
252
+ tfBackend = injectVariables(tfBackend);
253
+ tfCloudfront = injectVariables(tfCloudfront);
254
+ dockerContent = injectVariables(dockerContent);
255
+ githubAction = injectVariables(githubAction);
256
+ readmeContent = injectVariables(readmeContent);
257
+
258
+ // 10. Write the finalized files
259
+ await fs.writeFile(path.join(targetDir, 'terraform', 'main.tf'), tfMain);
260
+ await fs.writeFile(path.join(targetDir, 'terraform', 'network.tf'), tfNetwork);
261
+ await fs.writeFile(path.join(targetDir, 'terraform', 'secrets.tf'), tfSecrets);
262
+ await fs.writeFile(path.join(targetDir, 'terraform', 'oidc.tf'), tfOidc);
263
+ await fs.writeFile(path.join(targetDir, 'terraform', 'backend.tf'), tfBackend);
264
+ await fs.writeFile(path.join(targetDir, 'terraform', 'cloudfront.tf'), tfCloudfront);
265
+ await fs.writeFile(path.join(targetDir, 'Dockerfile'), dockerContent);
266
+ await fs.writeFile(path.join(targetDir, '.github', 'workflows', 'deploy.yml'), githubAction);
267
+ await fs.writeFile(path.join(targetDir, 'README.md'), readmeContent);
268
+ await fs.writeFile(path.join(targetDir, 'terraform', 'secret_keys.json'), "[]");
269
+
270
+ // 10.5. Ensure .gitignore exists and contains necessary Terraform ignores
271
+ const targetGitignore = path.join(targetDir, '.gitignore');
272
+ if (!fsSync.existsSync(targetGitignore)) {
273
+ // No gitignore exists? Give them the full template (Terraform + Node + Python)
274
+ await fs.writeFile(targetGitignore, gitignoreContent);
275
+ } else {
276
+ // File exists? Only inject the Terraform rules to prevent duplicates
277
+ const existingGitignore = await fs.readFile(targetGitignore, 'utf-8');
278
+
279
+ if (!existingGitignore.includes('terraform/.terraform/')) {
280
+ const terraformIgnores = `
281
+ # Added by deploy-stack (Terraform)
282
+ terraform/.terraform/
283
+ terraform/*.tfstate
284
+ terraform/*.tfstate.backup
285
+ terraform/.terraform.lock.hcl
286
+ terraform/secret_keys.json
287
+ terraform/.terraform.*
288
+ `;
289
+ await fs.appendFile(targetGitignore, '\n' + terraformIgnores);
290
+ }
291
+ }
292
+
293
+ // 11. Track the event in telemetry
294
+ trackEvent('project_provisioned', {
295
+ projectName: actualProjectName,
296
+ framework: project.framework,
297
+ region: project.region,
298
+ size: project.size,
299
+ setup_mode: setupType,
300
+ desiredCount: desiredCount,
301
+ duration_ms: Date.now() - startTime
302
+ });
303
+
304
+ s.stop('Infrastructure provisioned successfully!');
305
+
306
+ // 12. Provide the Outro, Framework Warnings and Next Steps
307
+ let frameworkWarnings = '';
308
+
309
+ if (project.framework === 'nextjs') {
310
+ frameworkWarnings =
311
+ color.bgYellow(color.black(' ⚠️ IMPORTANT: NEXT.JS SETUP REQUIRED ')) +
312
+ color.yellow('\n You must modify your next.config file and create a health check route before deploying.') +
313
+ color.yellow('\n See the "Critical Application Prerequisites" section in your README.md for copy-paste code.\n\n');
314
+ }
315
+
316
+ outro(`
317
+ ${color.green('✅ Project provisioned successfully!')}
318
+
319
+ Next steps:
320
+ 1. cd ${project.name}
321
+ 2. Deploy infrastructure:
322
+ cd terraform && terraform init && terraform apply
323
+ 3. Push to GitHub:
324
+ git init && git add . && git commit -m "Initial commit"
325
+
326
+ ${color.cyan('Once deployed, Terraform will output your new https://*.cloudfront.net URL.')}
327
+
328
+ ${color.magenta('🚀 Infrastructure ready! Need help or have feedback? Grab 15 mins with Anton:')}
329
+ ${color.underline('https://calendly.com/anton-codes-iac/15min')}
330
+ `);
331
+ }
@@ -0,0 +1,48 @@
1
+ import { SecretsManagerClient, UpdateSecretCommand } from "@aws-sdk/client-secrets-manager";
2
+ import dotenv from "dotenv";
3
+ import fs from 'fs/promises';
4
+ import { spinner } from '@clack/prompts';
5
+ import color from 'picocolors';
6
+ import path from 'path';
7
+
8
+ export async function pushSecrets(envFilePath, projectName) {
9
+ const s = spinner();
10
+ s.start(`Reading ${envFilePath} and pushing to AWS Secrets Manager...`);
11
+
12
+ try {
13
+ // 1. Read and parse the local .env file
14
+ const envPath = path.resolve(process.cwd(), envFilePath);
15
+ const envContent = await fs.readFile(envPath, 'utf-8');
16
+ const parsedSecrets = dotenv.parse(envContent);
17
+
18
+ if (Object.keys(parsedSecrets).length === 0) {
19
+ s.stop('No secrets found in file.');
20
+ return;
21
+ }
22
+
23
+ // 2. Initialize the AWS Client
24
+ // It automatically uses the AWS credentials the user set in their terminal
25
+ const client = new SecretsManagerClient({});
26
+
27
+ // 3. Update the secret string in AWS
28
+ // The SecretId matches the name we generated in secrets.tf
29
+ const command = new UpdateSecretCommand({
30
+ SecretId: `${projectName}-secrets`,
31
+ SecretString: JSON.stringify(parsedSecrets),
32
+ });
33
+
34
+ await client.send(command);
35
+
36
+ const keys = Object.keys(parsedSecrets);
37
+ const keysFilePath = path.join(process.cwd(), 'terraform', 'secret_keys.json');
38
+
39
+ await fs.writeFile(keysFilePath, JSON.stringify(keys, null, 2));
40
+
41
+ s.stop(`✅ Successfully pushed ${Object.keys(parsedSecrets).length} secrets to AWS!`);
42
+ console.log(color.cyan(`\nUpdated ${keysFilePath}`));
43
+ console.log(color.green('Commit this file and push to GitHub to trigger a deployment with your new variables.'));
44
+
45
+ } catch (error) {
46
+ s.stop(`❌ Failed to push secrets: ${error.message}`);
47
+ }
48
+ }
@@ -0,0 +1,39 @@
1
+ import crypto from 'crypto';
2
+
3
+ const TELEMETRY_ENDPOINT = 'https://app.posthog.com/capture';
4
+
5
+ const POSTHOG_API_KEY = '___POSTHOG_INGESTION_KEY___';
6
+
7
+ export function trackEvent(eventName, properties) {
8
+ // 1. Respect privacy standards
9
+ if (process.env.DO_NOT_TRACK === '1' || process.env.DO_NOT_TRACK === 'true') {
10
+ return;
11
+ }
12
+
13
+ // 2. Hash the project name so it is completely anonymous
14
+ const rawProjectName = properties.projectName || 'unknown';
15
+ const anonymousProjectId = crypto.createHash('sha256').update(rawProjectName).digest('hex').substring(0, 16);
16
+
17
+ // 3. Strip the raw name out of the payload
18
+ delete properties.projectName;
19
+
20
+ const payload = {
21
+ api_key: POSTHOG_API_KEY,
22
+ event: eventName,
23
+ properties: {
24
+ distinct_id: anonymousProjectId, // Used to count unique projects, not identify them
25
+ os: process.platform,
26
+ node_version: process.version,
27
+ ...properties
28
+ }
29
+ };
30
+
31
+ // 4. Fire and forget (No 'await' so we don't block the user's terminal)
32
+ fetch(TELEMETRY_ENDPOINT, {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: JSON.stringify(payload),
36
+ }).catch(() => {
37
+ // Silently swallow network errors (e.g., user is offline)
38
+ });
39
+ }