deploy-stack 0.16.0 → 0.17.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/README.md CHANGED
@@ -49,6 +49,7 @@ You retain complete ownership of your infrastructure code without relying on bla
49
49
  * **Native S3 State Locking:** Automatically creates an encrypted S3 state bucket utilizing modern Terraform concurrency locking.
50
50
  * **Safe Iteration:** Idempotent CLI safely backs up existing configurations to `.bak` files to guarantee zero data loss.
51
51
  * **Ephemeral PR Previews (Opt-In):** Automatically spins up completely isolated AWS Fargate environments for every Pull Request and posts the live preview URL to GitHub, accelerating team code reviews.
52
+ * **🤖 IDE AI Integration:** Automatically generates contextual rules for Cursor, Windsurf, Copilot, and Claude to prevent Terraform hallucinations.
52
53
 
53
54
  ---
54
55
 
@@ -97,6 +98,9 @@ The interactive wizard will analyze your codebase, detect your framework, estima
97
98
  * **`npx deploy-stack --headless`**
98
99
  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-2`, and `--size=micro`.
99
100
 
101
+ * **`npx deploy-stack sync-ai`**
102
+ Selectively generates architecture rules for AI coding assistants (Cursor, Copilot, Windsurf, Claude). Automatically extracts your AWS Region and Container Port to prevent Terraform hallucinations.
103
+
100
104
  ---
101
105
 
102
106
  ## 📁 Generated File Structure
@@ -133,14 +137,16 @@ your-project/
133
137
 
134
138
  ---
135
139
 
136
- ## 🤖 AI Agent Integration
140
+ ## 🤖 AI Context Management (Cursor, Copilot, Windsurf, Claude)
141
+
142
+ AI coding assistants are incredible, but they often hallucinate custom Terraform or raw AWS CLI commands that can break your infrastructure state. `deploy-stack` natively intercepts and guides AI agents directly in your IDE by providing strict deployment rules and project-specific context (like your exact AWS Region and Container Port).
137
143
 
138
- Are you using Cursor, Windsurf, or GitHub Copilot? AI coding assistants often hallucinate complex, broken infrastructure code when asked to "deploy to AWS."
144
+ **How it works:**
145
+ * **Quickstart Flow:** The CLI silently auto-detects if you are using AI tools in your repository and safely injects context.
146
+ * **Advanced Flow:** You are explicitly prompted to choose which AI assistants your team uses.
147
+ * **Standalone Command:** You can run `npx deploy-stack sync-ai` at any time to selectively generate these rules later.
139
148
 
140
- To teach your AI to natively use this CLI instead, copy our [Agent Ruleset](./agent-rules.md) into your repository's specific instruction file:
141
- * **Cursor:** Save as `.cursorrules` in your project root.
142
- * **Windsurf:** Save as `.windsurfrules` in your project root.
143
- * **GitHub Copilot:** Save as `.github/copilot-instructions.md`.
149
+ **Safe & Non-Destructive:** We use isolated rule files (like `.cursor/rules/deploy-stack.mdc`) or strictly delimited blocks (``) to ensure your team's existing agent instructions, coding standards, and project prompts are **never overwritten**.
144
150
 
145
151
  ---
146
152
 
@@ -158,7 +164,7 @@ npx deploy-stack --no-telemetry
158
164
 
159
165
  ### Current Focus (Phase 7: Team Workflows & Ecosystem Integrations)
160
166
  - [x] **Ephemeral PR Previews:** Generate GitHub Actions workflows that spin up temporary ECS Fargate tasks and post live preview URLs directly in pull request comments to streamline team code reviews.
161
- - [ ] **AI Context Synchronization:** Implement `deploy-stack sync-ai` to automatically generate `.cursorrules` and AI context files, ensuring coding assistants generate accurate deployment commands tailored to the project.
167
+ - [x] **AI Context Synchronization:** Implement `deploy-stack sync-ai` to automatically generate `.cursorrules` and AI context files, ensuring coding assistants generate accurate deployment commands tailored to the project.
162
168
  - [ ] **Native Ecosystem Integrations:** Publish seamless, push-button plugins across major frameworks. Targets include a `svelte-adapter-deploy-stack`, an official `create-next-app` AWS template, a `vite-plugin-deploy-stack`, a NestJS schematic, and a Django Cookiecutter template.
163
169
  - [ ] **Automated Troubleshooting:** Build `deploy-stack diagnose` (alias: `wtf`) to automatically analyze and troubleshoot common day-2 AWS operational issues (e.g., Fargate OOM kills, ALB 502s) directly from the terminal.
164
170
 
package/bin/cli.js CHANGED
@@ -6,6 +6,7 @@ import { runDoctor } from '../src/commands/doctor.js';
6
6
  import { pushSecrets } from '../src/commands/secrets.js';
7
7
  import { ejectStack } from '../src/commands/eject.js';
8
8
  import { applyStack } from '../src/commands/apply.js';
9
+ import { syncAi } from '../src/commands/sync-ai.js';
9
10
 
10
11
  // 1. Extract the telemetry flag and set the environment variable
11
12
  const rawArgs = process.argv.slice(2);
@@ -40,15 +41,17 @@ const headlessOptions = isHeadless ? {
40
41
  if (args[0] === 'secrets' && args[1] === 'push') {
41
42
  const envFile = args[2] || '.env';
42
43
  const projectName = path.basename(process.cwd());
43
- pushSecrets(envFile, projectName).catch(console.error);
44
+ pushSecrets(envFile, projectName).catch(e => { console.error(e); process.exit(1); });
44
45
  } else if (args[0] === 'apply') {
45
- applyStack({ isDryRun }).catch(console.error);
46
+ applyStack({ isDryRun }).catch(e => { console.error(e); process.exit(1); });
46
47
  } else if (args[0] === 'doctor') {
47
- runDoctor().catch(console.error);
48
+ runDoctor().catch(e => { console.error(e); process.exit(1); });
48
49
  } else if (args[0] === 'destroy') {
49
- destroyStack().catch(console.error);
50
+ destroyStack().catch(e => { console.error(e); process.exit(1); });
50
51
  } else if (args[0] === 'eject') {
51
- ejectStack().catch(console.error);
52
+ ejectStack().catch(e => { console.error(e); process.exit(1); });
53
+ } else if (args[0] === 'sync-ai') {
54
+ syncAi().catch(e => { console.error(e); process.exit(1); });
52
55
  } else {
53
56
  mainStack({ isHeadless, headlessOptions }).catch(e => { console.error(e); process.exit(1); });
54
57
  }
package/docs/ROADMAP.md CHANGED
@@ -25,6 +25,6 @@
25
25
  ### Phase 7: Team Workflows & Ecosystem Integrations (Current)
26
26
  *Focus: Enhance collaborative development and expand native support across major framework ecosystems.*
27
27
  - [x] **Ephemeral PR Previews:** Generate GitHub Actions workflows that spin up temporary ECS Fargate tasks and post live URLs directly in PR comments to streamline team code reviews.
28
- - [ ] **AI Context Synchronization:** Implement `deploy-stack sync-ai` to automatically generate `.cursorrules` and AI context files, ensuring IDE assistants understand the infrastructure.
28
+ - [x] **AI Context Synchronization:** Implement `deploy-stack sync-ai` to automatically generate `.cursorrules` and AI context files, ensuring IDE assistants understand the infrastructure.
29
29
  - [ ] **Native Ecosystem Integrations:** Publish push-button plugins across major frameworks (e.g., `svelte-adapter-deploy-stack`, `create-next-app` AWS template, `vite-plugin-deploy-stack`, NestJS deployment schematic, Django Cookiecutter).
30
30
  - [ ] **Automated Troubleshooting:** Build `deploy-stack diagnose` to auto-diagnose common day-2 AWS failures (e.g., Fargate OOM kills, ALB 502s) to establish immediate technical credibility and simplify maintenance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploy-stack",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -20,7 +20,8 @@ import { handleExistingFiles } from '../utils/backup.js';
20
20
  import { estimateMonthlyCost } from '../utils/visualizer.js';
21
21
  import { getTargetDirectory, getProjectConfig } from '../utils/prompts.js';
22
22
  import { resolveDjangoWsgi, handleRailsCI } from '../utils/frameworks.js';
23
- import { hasDockerCompose, parseDockerCompose } from '../utils/dockerCompose.js';
23
+ import { parseDockerCompose } from '../utils/dockerCompose.js';
24
+ import { getBaseRules, getCursorRules, injectManagedBlock } from '../utils/ai-rules.js';
24
25
 
25
26
  const pkg = JSON.parse(fsSync.readFileSync(new URL('../../package.json', import.meta.url)));
26
27
  const CLI_VERSION = pkg.version;
@@ -171,10 +172,53 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
171
172
  is_vercel_migration: !!vercelRules,
172
173
  is_docker_compose: !!dockerCompose,
173
174
  has_pr_previews: config.enablePrPreviews,
175
+ ai_assistants_configured: config.aiAssistants || [],
174
176
  });
175
177
 
176
178
  s.stop('Infrastructure provisioned successfully!');
177
179
 
180
+ // 8.5 Configure AI Context
181
+ s.start('Configuring AI workspace rules...');
182
+ const aiContext = { region: config.region, port: config.port };
183
+
184
+ if (config.setupType === 'advanced') {
185
+ // --- ADVANCED MODE: Explicitly respect user choices ---
186
+ if (config.aiAssistants.includes('cursor')) {
187
+ const cursorDir = path.join(dirConfig.targetDir, '.cursor', 'rules');
188
+ if (!fsSync.existsSync(cursorDir)) fsSync.mkdirSync(cursorDir, { recursive: true });
189
+ fsSync.writeFileSync(path.join(cursorDir, 'deploy-stack.mdc'), getCursorRules(aiContext));
190
+ }
191
+ if (config.aiAssistants.includes('windsurf')) {
192
+ injectManagedBlock(path.join(dirConfig.targetDir, '.windsurfrules'), getBaseRules(aiContext), false);
193
+ }
194
+ if (config.aiAssistants.includes('claude')) {
195
+ injectManagedBlock(path.join(dirConfig.targetDir, 'CLAUDE.md'), getBaseRules(aiContext), true);
196
+ }
197
+ if (config.aiAssistants.includes('copilot')) {
198
+ const copilotPath = path.join(dirConfig.targetDir, '.github', 'copilot-instructions.md');
199
+ if (!fsSync.existsSync(path.dirname(copilotPath))) fsSync.mkdirSync(path.dirname(copilotPath), { recursive: true });
200
+ injectManagedBlock(copilotPath, getBaseRules(aiContext), true);
201
+ }
202
+ } else {
203
+ // --- QUICKSTART MODE: Silent Auto-Detection ---
204
+ if (fsSync.existsSync(path.join(dirConfig.targetDir, '.cursor'))) {
205
+ const cursorDir = path.join(dirConfig.targetDir, '.cursor', 'rules');
206
+ if (!fsSync.existsSync(cursorDir)) fsSync.mkdirSync(cursorDir, { recursive: true });
207
+ fsSync.writeFileSync(path.join(cursorDir, 'deploy-stack.mdc'), getCursorRules(aiContext));
208
+ }
209
+ if (fsSync.existsSync(path.join(dirConfig.targetDir, '.windsurf')) || fsSync.existsSync(path.join(dirConfig.targetDir, '.windsurfrules'))) {
210
+ injectManagedBlock(path.join(dirConfig.targetDir, '.windsurfrules'), getBaseRules(aiContext), false);
211
+ }
212
+ if (fsSync.existsSync(path.join(dirConfig.targetDir, 'CLAUDE.md'))) {
213
+ injectManagedBlock(path.join(dirConfig.targetDir, 'CLAUDE.md'), getBaseRules(aiContext), true);
214
+ }
215
+ const copilotPath = path.join(dirConfig.targetDir, '.github', 'copilot-instructions.md');
216
+ if (fsSync.existsSync(copilotPath)) {
217
+ injectManagedBlock(copilotPath, getBaseRules(aiContext), true);
218
+ }
219
+ }
220
+ s.stop('AI rules configured successfully!');
221
+
178
222
  // 9. Output
179
223
  let frameworkWarnings = '';
180
224
  if (!(config.framework === 'static' && detectedFramework?.buildDir)) {
@@ -0,0 +1,83 @@
1
+ import fsSync from 'fs';
2
+ import path from 'path';
3
+ import { intro, outro, spinner, log } from '@clack/prompts';
4
+ import color from 'picocolors';
5
+ import { getAiAssistants } from '../utils/prompts.js';
6
+ import { getBaseRules, getCursorRules, injectManagedBlock } from '../utils/ai-rules.js';
7
+ import { trackEvent, flushTelemetry } from '../core/telemetry.js';
8
+
9
+ function getProjectContext(cwd) {
10
+ const context = { region: '', port: '' };
11
+ const mainTfPath = path.join(cwd, 'terraform', 'main.tf');
12
+
13
+ if (fsSync.existsSync(mainTfPath)) {
14
+ const tfContent = fsSync.readFileSync(mainTfPath, 'utf8');
15
+
16
+ // Extract Region
17
+ const regionMatch = tfContent.match(/region\s*=\s*"([^"]+)"/);
18
+ if (regionMatch) context.region = regionMatch[1];
19
+
20
+ // Extract Port
21
+ const portMatch = tfContent.match(/containerPort\s*=\s*(\d+)/);
22
+ if (portMatch) context.port = portMatch[1];
23
+ }
24
+
25
+ return context;
26
+ }
27
+
28
+ export async function syncAi() {
29
+ intro(color.bgCyan(color.black(' deploy-stack sync-ai 🤖 ')));
30
+
31
+ const assistants = await getAiAssistants();
32
+
33
+ if (!assistants || assistants.length === 0) {
34
+ log.warn('No AI assistants selected. Skipping synchronization.');
35
+ process.exit(0);
36
+ }
37
+
38
+ const s = spinner();
39
+ s.start('Writing AI context rules...');
40
+ const cwd = process.cwd();
41
+
42
+ const context = getProjectContext(cwd);
43
+
44
+ try {
45
+ if (assistants.includes('cursor')) {
46
+ const cursorDir = path.join(cwd, '.cursor', 'rules');
47
+ if (!fsSync.existsSync(cursorDir)) fsSync.mkdirSync(cursorDir, { recursive: true });
48
+ fsSync.writeFileSync(path.join(cursorDir, 'deploy-stack.mdc'), getCursorRules(context));
49
+ }
50
+
51
+ if (assistants.includes('copilot')) {
52
+ const githubDir = path.join(cwd, '.github');
53
+ if (!fsSync.existsSync(githubDir)) fsSync.mkdirSync(githubDir, { recursive: true });
54
+ injectManagedBlock(path.join(githubDir, 'copilot-instructions.md'), getBaseRules(context), true);
55
+ }
56
+
57
+ if (assistants.includes('windsurf')) {
58
+ injectManagedBlock(path.join(cwd, '.windsurfrules'), getBaseRules(context), false);
59
+ }
60
+
61
+ if (assistants.includes('claude')) {
62
+ injectManagedBlock(path.join(cwd, 'CLAUDE.md'), getBaseRules(context), true);
63
+ }
64
+
65
+ trackEvent('sync_ai_executed', {
66
+ assistants_selected: assistants,
67
+ has_cursor: assistants.includes('cursor'),
68
+ has_copilot: assistants.includes('copilot'),
69
+ has_windsurf: assistants.includes('windsurf'),
70
+ has_claude: assistants.includes('claude')
71
+ });
72
+ await flushTelemetry();
73
+
74
+ s.stop('AI context synchronized successfully!');
75
+
76
+ outro(`${color.green('✅ AI Assistant Rules generated!')}
77
+ Your AI tools now know exactly how to deploy your app without hallucinating Terraform.`);
78
+ } catch (error) {
79
+ s.stop('❌ Failed to write AI context files.');
80
+ console.error(color.red(error.message));
81
+ process.exit(1);
82
+ }
83
+ }
@@ -0,0 +1,54 @@
1
+ import fsSync from 'fs';
2
+
3
+ export const getBaseRules = (context = {}) => `
4
+ # deploy-stack Infrastructure Context
5
+
6
+ This project's AWS infrastructure is managed strictly by the \`deploy-stack\` CLI.
7
+ **DO NOT** generate custom Terraform, AWS CloudFormation, or raw AWS CLI commands to deploy this application or modify the architecture.
8
+
9
+ ### Standard Operating Procedures
10
+
11
+ 1. **Applying Changes:** If the user asks to deploy, provision, or update the infrastructure, instruct them to run:
12
+ \`npx deploy-stack apply\`
13
+
14
+ 2. **Environment Variables:** If the user adds a new environment variable and needs it available in production, instruct them to run:
15
+ \`npx deploy-stack secrets push <path-to-env-file>\`
16
+
17
+ 3. **Teardown:** If the user wants to destroy the AWS environment, instruct them to run:
18
+ \`npx deploy-stack destroy\`
19
+
20
+ 4. **Architecture Details:**
21
+ - **AWS Region:** \`${context.region || 'Unknown (Check terraform/main.tf)'}\`
22
+ - **Container Port:** \`${context.port || 'Unknown'}\`
23
+ - The infrastructure is an AWS ECS Fargate cluster.
24
+ - It uses an Application Load Balancer (ALB).
25
+ - CI/CD is handled securely via GitHub Actions OIDC (no long-lived IAM keys).
26
+ - Preview environments (Ephemeral PRs) are managed via Terraform Workspaces.
27
+ `;
28
+
29
+ export const getCursorRules = (context = {}) => `---
30
+ description: "Rules for deploying the application and managing AWS infrastructure"
31
+ globs: ["terraform/*.tf", ".github/workflows/*.yml", "Dockerfile"]
32
+ ---${getBaseRules(context)}`;
33
+
34
+ export function injectManagedBlock(filePath, content, isMarkdown = true) {
35
+ const beginMarker = isMarkdown ? '<!-- BEGIN DEPLOY-STACK CONTEXT -->' : '# BEGIN DEPLOY-STACK CONTEXT';
36
+ const endMarker = isMarkdown ? '<!-- END DEPLOY-STACK CONTEXT -->' : '# END DEPLOY-STACK CONTEXT';
37
+ const block = `\n${beginMarker}\n${content.trim()}\n${endMarker}\n`;
38
+
39
+ if (fsSync.existsSync(filePath)) {
40
+ let fileContent = fsSync.readFileSync(filePath, 'utf8');
41
+ // Look for the existing block to replace it
42
+ const regex = new RegExp(`\\n?${beginMarker}[\\s\\S]*?${endMarker}\\n?`);
43
+
44
+ if (regex.test(fileContent)) {
45
+ fileContent = fileContent.replace(regex, block); // Replace our old rules
46
+ } else {
47
+ fileContent = fileContent.trim() + '\n' + block; // Append to bottom
48
+ }
49
+ fsSync.writeFileSync(filePath, fileContent);
50
+ } else {
51
+ // File doesn't exist, create it cleanly
52
+ fsSync.writeFileSync(filePath, block.trim() + '\n');
53
+ }
54
+ }
@@ -2,13 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { load } from 'js-yaml';
4
4
 
5
- // 1. Check if a Docker Compose file exists in the target directory.
6
- export function hasDockerCompose(targetDir) {
7
- return fs.existsSync(path.join(targetDir, 'docker-compose.yml')) ||
8
- fs.existsSync(path.join(targetDir, 'docker-compose.yaml'));
9
- }
10
-
11
- // 2. Parse docker-compose.yml and normalize it into an array of services.
5
+ // Parse docker-compose.yml and normalize it into an array of services.
12
6
  export function parseDockerCompose(targetDir) {
13
7
  let composePath = path.join(targetDir, 'docker-compose.yml');
14
8
  if (!fs.existsSync(composePath)) {
@@ -14,6 +14,8 @@ export async function generateTemplates(targetDir, config) {
14
14
 
15
15
  // 2. Define paths
16
16
  const templatesDir = path.join(__dirname, '../../templates');
17
+ const readmeExists = fsSync.existsSync(path.join(targetDir, 'README.md'));
18
+ const readmeDest = readmeExists ? 'DEPLOYMENT.md' : 'README.md';
17
19
  const filesToProcess = [
18
20
  { src: 'terraform/main.tf', dest: 'terraform/main.tf' },
19
21
  { src: 'terraform/network.tf', dest: 'terraform/network.tf' },
@@ -23,9 +25,15 @@ export async function generateTemplates(targetDir, config) {
23
25
  { src: 'terraform/cloudfront.tf', dest: 'terraform/cloudfront.tf' },
24
26
  { src: `docker/${config.finalFramework}.Dockerfile`, dest: 'Dockerfile' },
25
27
  { src: 'github/deploy.yml', dest: '.github/workflows/deploy.yml' },
26
- { src: 'README.md', dest: 'README.md' }
28
+ { src: 'README.md', dest: readmeDest }
27
29
  ];
28
30
 
31
+ // 2.1 Check if README.md exists (append DEPLOYMENT.md note if yes, generate new if no)
32
+ if (readmeExists) {
33
+ const readmeNotice = `\n## 🚀 Deployment\nAWS Fargate infrastructure is managed via [deploy-stack](./DEPLOYMENT.md).\n`;
34
+ fsSync.appendFileSync(path.join(targetDir, 'README.md'), readmeNotice);
35
+ }
36
+
29
37
  // 2.1 Add Ephemeral PR workflows only if opted in
30
38
  if (config.ENABLE_PR_PREVIEWS) {
31
39
  filesToProcess.push({ src: 'github/preview.yml', dest: '.github/workflows/preview.yml' });
@@ -1,5 +1,5 @@
1
1
  import path from 'path';
2
- import { text, select, confirm, group, cancel, log } from '@clack/prompts';
2
+ import { text, select, multiselect, confirm, group, cancel } from '@clack/prompts';
3
3
  import color from 'picocolors';
4
4
  import { execSync } from 'child_process';
5
5
 
@@ -101,6 +101,7 @@ export async function getProjectConfig(isHeadless, headlessOptions, targetDir, d
101
101
  }
102
102
 
103
103
  let enablePrPreviews = false;
104
+ let aiAssistants = [];
104
105
  if (setupType === 'advanced') {
105
106
  const prChoice = await confirm({
106
107
  message: `Enable Ephemeral PR Previews? (Spins up isolated, temporary AWS environments for PRs)\n ${color.gray('📖 Learn more: https://github.com/anton-codes-iac/deploy-stack/blob/main/docs/guides/ephemeral-pr-previews.md')}`,
@@ -108,6 +109,8 @@ export async function getProjectConfig(isHeadless, headlessOptions, targetDir, d
108
109
  });
109
110
  if (typeof prChoice === 'symbol') process.exit(0);
110
111
  enablePrPreviews = prChoice;
112
+
113
+ aiAssistants = await getAiAssistants();
111
114
  }
112
115
 
113
116
  const project = await group({
@@ -163,6 +166,27 @@ export async function getProjectConfig(isHeadless, headlessOptions, targetDir, d
163
166
  branch: project.branch || currentGitBranch,
164
167
  needsDatabase,
165
168
  enablePrPreviews,
169
+ aiAssistants,
166
170
  setupType
167
171
  };
172
+ }
173
+
174
+ export async function getAiAssistants() {
175
+ const selected = await multiselect({
176
+ message: 'Which AI coding assistants does your team use?',
177
+ options: [
178
+ { value: 'cursor', label: 'Cursor', hint: 'Generates .cursor/rules/deploy-stack.mdc' },
179
+ { value: 'copilot', label: 'GitHub Copilot', hint: 'Generates .github/copilot-instructions.md' },
180
+ { value: 'windsurf', label: 'Windsurf', hint: 'Generates .windsurfrules' },
181
+ { value: 'claude', label: 'Claude Code / CLI Agents', hint: 'Generates CLAUDE.md' }
182
+ ],
183
+ required: false,
184
+ });
185
+
186
+ if (typeof selected === 'symbol') {
187
+ cancel('Operation cancelled.');
188
+ process.exit(0);
189
+ }
190
+
191
+ return selected;
168
192
  }