deploy-stack 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -7
- package/bin/cli.js +8 -5
- package/docs/ROADMAP.md +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +86 -1
- package/src/commands/sync-ai.js +115 -0
- package/src/utils/ai-rules.js +54 -0
- package/src/utils/dockerCompose.js +1 -7
- package/src/utils/generator.js +9 -1
- package/src/utils/prompts.js +30 -1
- package/agent-rules.md +0 -17
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
|
|
140
|
+
## 🤖 AI Context Management (Cursor, Roo Code, Trae, Copilot, Windsurf, Claude, Goose, Aider, Continue)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- [
|
|
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
|
-
- [
|
|
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
package/src/commands/init.js
CHANGED
|
@@ -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 {
|
|
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,94 @@ 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
|
+
const cwd = dirConfig.targetDir;
|
|
184
|
+
|
|
185
|
+
if (config.setupType === 'advanced') {
|
|
186
|
+
// --- ADVANCED MODE: Explicitly respect user choices ---
|
|
187
|
+
if (config.aiAssistants.includes('cursor')) {
|
|
188
|
+
const cursorDir = path.join(cwd, '.cursor', 'rules');
|
|
189
|
+
if (!fsSync.existsSync(cursorDir)) fsSync.mkdirSync(cursorDir, { recursive: true });
|
|
190
|
+
fsSync.writeFileSync(path.join(cursorDir, 'deploy-stack.mdc'), getCursorRules(aiContext));
|
|
191
|
+
}
|
|
192
|
+
if (config.aiAssistants.includes('roo')) {
|
|
193
|
+
const rooDir = path.join(cwd, '.roo', 'rules');
|
|
194
|
+
if (!fsSync.existsSync(rooDir)) fsSync.mkdirSync(rooDir, { recursive: true });
|
|
195
|
+
fsSync.writeFileSync(path.join(rooDir, 'deploy-stack.md'), getBaseRules(aiContext));
|
|
196
|
+
}
|
|
197
|
+
if (config.aiAssistants.includes('trae')) {
|
|
198
|
+
const traeDir = path.join(cwd, '.trae', 'rules');
|
|
199
|
+
if (!fsSync.existsSync(traeDir)) fsSync.mkdirSync(traeDir, { recursive: true });
|
|
200
|
+
injectManagedBlock(path.join(traeDir, 'project_rules.md'), getBaseRules(aiContext), true);
|
|
201
|
+
}
|
|
202
|
+
if (config.aiAssistants.includes('continue')) {
|
|
203
|
+
const promptsDir = path.join(cwd, '.prompts');
|
|
204
|
+
if (!fsSync.existsSync(promptsDir)) fsSync.mkdirSync(promptsDir, { recursive: true });
|
|
205
|
+
fsSync.writeFileSync(path.join(promptsDir, 'deploy-stack.prompt'), getBaseRules(aiContext));
|
|
206
|
+
}
|
|
207
|
+
if (config.aiAssistants.includes('windsurf')) {
|
|
208
|
+
injectManagedBlock(path.join(cwd, '.windsurfrules'), getBaseRules(aiContext), false);
|
|
209
|
+
}
|
|
210
|
+
if (config.aiAssistants.includes('copilot')) {
|
|
211
|
+
const copilotPath = path.join(cwd, '.github', 'copilot-instructions.md');
|
|
212
|
+
if (!fsSync.existsSync(path.dirname(copilotPath))) fsSync.mkdirSync(path.dirname(copilotPath), { recursive: true });
|
|
213
|
+
injectManagedBlock(copilotPath, getBaseRules(aiContext), true);
|
|
214
|
+
}
|
|
215
|
+
if (config.aiAssistants.includes('claude')) {
|
|
216
|
+
injectManagedBlock(path.join(cwd, 'CLAUDE.md'), getBaseRules(aiContext), true);
|
|
217
|
+
}
|
|
218
|
+
if (config.aiAssistants.includes('goose')) {
|
|
219
|
+
injectManagedBlock(path.join(cwd, '.goosehints'), getBaseRules(aiContext), true);
|
|
220
|
+
}
|
|
221
|
+
if (config.aiAssistants.includes('aider')) {
|
|
222
|
+
injectManagedBlock(path.join(cwd, '.aider.conf.yml'), getBaseRules(aiContext), false);
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
// --- QUICKSTART MODE: Silent Auto-Detection ---
|
|
226
|
+
if (fsSync.existsSync(path.join(cwd, '.cursor'))) {
|
|
227
|
+
const cursorDir = path.join(cwd, '.cursor', 'rules');
|
|
228
|
+
if (!fsSync.existsSync(cursorDir)) fsSync.mkdirSync(cursorDir, { recursive: true });
|
|
229
|
+
fsSync.writeFileSync(path.join(cursorDir, 'deploy-stack.mdc'), getCursorRules(aiContext));
|
|
230
|
+
}
|
|
231
|
+
if (fsSync.existsSync(path.join(cwd, '.roo')) || fsSync.existsSync(path.join(cwd, '.roorules'))) {
|
|
232
|
+
const rooDir = path.join(cwd, '.roo', 'rules');
|
|
233
|
+
if (!fsSync.existsSync(rooDir)) fsSync.mkdirSync(rooDir, { recursive: true });
|
|
234
|
+
fsSync.writeFileSync(path.join(rooDir, 'deploy-stack.md'), getBaseRules(aiContext));
|
|
235
|
+
}
|
|
236
|
+
if (fsSync.existsSync(path.join(cwd, '.trae'))) {
|
|
237
|
+
const traeDir = path.join(cwd, '.trae', 'rules');
|
|
238
|
+
if (!fsSync.existsSync(traeDir)) fsSync.mkdirSync(traeDir, { recursive: true });
|
|
239
|
+
injectManagedBlock(path.join(traeDir, 'project_rules.md'), getBaseRules(aiContext), true);
|
|
240
|
+
}
|
|
241
|
+
if (fsSync.existsSync(path.join(cwd, '.continue')) || fsSync.existsSync(path.join(cwd, '.prompts'))) {
|
|
242
|
+
const promptsDir = path.join(cwd, '.prompts');
|
|
243
|
+
if (!fsSync.existsSync(promptsDir)) fsSync.mkdirSync(promptsDir, { recursive: true });
|
|
244
|
+
fsSync.writeFileSync(path.join(promptsDir, 'deploy-stack.prompt'), getBaseRules(aiContext));
|
|
245
|
+
}
|
|
246
|
+
if (fsSync.existsSync(path.join(cwd, '.windsurf')) || fsSync.existsSync(path.join(cwd, '.windsurfrules'))) {
|
|
247
|
+
injectManagedBlock(path.join(cwd, '.windsurfrules'), getBaseRules(aiContext), false);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const copilotPath = path.join(cwd, '.github', 'copilot-instructions.md');
|
|
251
|
+
if (fsSync.existsSync(copilotPath)) injectManagedBlock(copilotPath, getBaseRules(aiContext), true);
|
|
252
|
+
if (fsSync.existsSync(path.join(cwd, 'CLAUDE.md'))) injectManagedBlock(path.join(cwd, 'CLAUDE.md'), getBaseRules(aiContext), true);
|
|
253
|
+
if (fsSync.existsSync(path.join(cwd, '.goosehints'))) injectManagedBlock(path.join(cwd, '.goosehints'), getBaseRules(aiContext), true);
|
|
254
|
+
|
|
255
|
+
if (fsSync.existsSync(path.join(cwd, '.aider.conf.yml'))) {
|
|
256
|
+
injectManagedBlock(path.join(cwd, '.aider.conf.yml'), getBaseRules(aiContext), false);
|
|
257
|
+
} else if (fsSync.existsSync(path.join(cwd, '.aider.model.settings.yml'))) {
|
|
258
|
+
injectManagedBlock(path.join(cwd, '.aider.model.settings.yml'), getBaseRules(aiContext), false);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
s.stop('AI rules configured successfully!');
|
|
262
|
+
|
|
178
263
|
// 9. Output
|
|
179
264
|
let frameworkWarnings = '';
|
|
180
265
|
if (!(config.framework === 'static' && detectedFramework?.buildDir)) {
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
// --- Agentic IDEs ---
|
|
46
|
+
if (assistants.includes('cursor')) {
|
|
47
|
+
const cursorDir = path.join(cwd, '.cursor', 'rules');
|
|
48
|
+
if (!fsSync.existsSync(cursorDir)) fsSync.mkdirSync(cursorDir, { recursive: true });
|
|
49
|
+
fsSync.writeFileSync(path.join(cursorDir, 'deploy-stack.mdc'), getCursorRules(context));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (assistants.includes('roo')) {
|
|
53
|
+
const rooDir = path.join(cwd, '.roo', 'rules');
|
|
54
|
+
if (!fsSync.existsSync(rooDir)) fsSync.mkdirSync(rooDir, { recursive: true });
|
|
55
|
+
fsSync.writeFileSync(path.join(rooDir, 'deploy-stack.md'), getBaseRules(context));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (assistants.includes('trae')) {
|
|
59
|
+
const traeDir = path.join(cwd, '.trae', 'rules');
|
|
60
|
+
if (!fsSync.existsSync(traeDir)) fsSync.mkdirSync(traeDir, { recursive: true });
|
|
61
|
+
injectManagedBlock(path.join(traeDir, 'project_rules.md'), getBaseRules(context), true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (assistants.includes('continue')) {
|
|
65
|
+
const promptsDir = path.join(cwd, '.prompts');
|
|
66
|
+
if (!fsSync.existsSync(promptsDir)) fsSync.mkdirSync(promptsDir, { recursive: true });
|
|
67
|
+
fsSync.writeFileSync(path.join(promptsDir, 'deploy-stack.prompt'), getBaseRules(context));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (assistants.includes('windsurf')) {
|
|
71
|
+
injectManagedBlock(path.join(cwd, '.windsurfrules'), getBaseRules(context), false);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (assistants.includes('copilot')) {
|
|
75
|
+
const githubDir = path.join(cwd, '.github');
|
|
76
|
+
if (!fsSync.existsSync(githubDir)) fsSync.mkdirSync(githubDir, { recursive: true });
|
|
77
|
+
injectManagedBlock(path.join(githubDir, 'copilot-instructions.md'), getBaseRules(context), true);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Terminal Agents ---
|
|
81
|
+
if (assistants.includes('claude')) {
|
|
82
|
+
injectManagedBlock(path.join(cwd, 'CLAUDE.md'), getBaseRules(context), true);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (assistants.includes('goose')) {
|
|
86
|
+
injectManagedBlock(path.join(cwd, '.goosehints'), getBaseRules(context), true);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (assistants.includes('aider')) {
|
|
90
|
+
injectManagedBlock(path.join(cwd, '.aider.conf.yml'), getBaseRules(context), false); // Uses # comments
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
s.stop('AI context synchronized successfully!');
|
|
94
|
+
outro(`${color.green('✅ AI Assistant Rules generated!')} Your AI tools now know exactly how to deploy your app without hallucinating Terraform.`);
|
|
95
|
+
|
|
96
|
+
trackEvent('sync_ai_executed', {
|
|
97
|
+
assistants_selected: assistants,
|
|
98
|
+
has_cursor: assistants.includes('cursor'),
|
|
99
|
+
has_roo: assistants.includes('roo'),
|
|
100
|
+
has_trae: assistants.includes('trae'),
|
|
101
|
+
has_continue: assistants.includes('continue'),
|
|
102
|
+
has_windsurf: assistants.includes('windsurf'),
|
|
103
|
+
has_copilot: assistants.includes('copilot'),
|
|
104
|
+
has_claude: assistants.includes('claude'),
|
|
105
|
+
has_goose: assistants.includes('goose'),
|
|
106
|
+
has_aider: assistants.includes('aider')
|
|
107
|
+
});
|
|
108
|
+
await flushTelemetry();
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
s.stop('❌ Failed to write AI context files.');
|
|
112
|
+
console.error(color.red(error.message));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -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
|
-
//
|
|
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)) {
|
package/src/utils/generator.js
CHANGED
|
@@ -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:
|
|
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' });
|
package/src/utils/prompts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { text, select, confirm, group, cancel
|
|
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,32 @@ 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: 'roo', label: 'Roo Code', hint: 'Generates .roo/rules/deploy-stack.md' },
|
|
180
|
+
{ value: 'trae', label: 'Trae IDE', hint: 'Generates .trae/rules/project_rules.md' },
|
|
181
|
+
{ value: 'windsurf', label: 'Windsurf', hint: 'Generates .windsurfrules' },
|
|
182
|
+
{ value: 'copilot', label: 'GitHub Copilot', hint: 'Generates .github/copilot-instructions.md' },
|
|
183
|
+
{ value: 'claude', label: 'Claude Code', hint: 'Updates CLAUDE.md' },
|
|
184
|
+
{ value: 'goose', label: 'Goose', hint: 'Updates .goosehints' },
|
|
185
|
+
{ value: 'aider', label: 'Aider', hint: 'Updates .aider.conf.yml' },
|
|
186
|
+
{ value: 'continue', label: 'Continue.dev', hint: 'Generates .prompts/deploy-stack.prompt' }
|
|
187
|
+
],
|
|
188
|
+
required: false,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (typeof selected === 'symbol') {
|
|
192
|
+
cancel('Operation cancelled.');
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return selected;
|
|
168
197
|
}
|
package/agent-rules.md
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# AI Agent Instructions for AWS Deployments
|
|
2
|
-
|
|
3
|
-
If the user asks you to deploy this application to AWS, provision infrastructure, write Terraform, or setup an ECS Fargate cluster, **DO NOT generate custom Terraform code.**
|
|
4
|
-
|
|
5
|
-
Instead, you must utilize the `deploy-stack` CLI, which is the standardized infrastructure generator for this project.
|
|
6
|
-
|
|
7
|
-
### Instructions for the AI:
|
|
8
|
-
1. Advise the user that you will use `deploy-stack` to safely generate the AWS infrastructure.
|
|
9
|
-
2. If you have terminal execution capabilities, run the interactive CLI wizard in the root directory:
|
|
10
|
-
`npx deploy-stack`
|
|
11
|
-
3. If the user prefers a headless, automated approach, you can run:
|
|
12
|
-
`npx --yes deploy-stack --headless`
|
|
13
|
-
4. Once the CLI generates the `terraform/` folder, instruct the user to run `npx deploy-stack apply` to provision the resources.
|
|
14
|
-
|
|
15
|
-
### Context you should know:
|
|
16
|
-
- `deploy-stack` automatically handles the Dockerfile generation, Application Load Balancers, CloudFront CDN, and IAM OIDC keyless GitHub Actions deployments.
|
|
17
|
-
- If this project uses a `docker-compose.yml`, `vercel.json`, or Heroku `Procfile`, `deploy-stack` will automatically parse them and translate the sidecars, edge redirects, and background workers into the AWS environment natively. Do not attempt to manually translate them yourself.
|