deploy-stack 0.13.0 → 0.14.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 +11 -2
- package/docs/guides/database-connections.md +27 -0
- package/docs/guides/secrets-management.md +24 -0
- package/docs/migration/astro-vercel-to-aws.md +47 -0
- package/docs/migration/heroku-procfile-to-aws.md +32 -0
- package/docs/migration/nextjs-vercel-to-aws.md +43 -0
- package/docs/migration/sveltekit-vercel-to-aws.md +55 -0
- package/package.json +1 -1
- package/release_notes.md +20 -0
- package/src/commands/apply.js +18 -0
- package/src/commands/destroy.js +13 -2
- package/src/commands/doctor.js +6 -0
- package/src/commands/eject.js +7 -0
- package/src/commands/init.js +105 -302
- package/src/commands/secrets.js +16 -0
- package/src/core/telemetry.js +1 -0
- package/src/utils/detector.js +95 -0
- package/src/utils/frameworks.js +55 -0
- package/src/utils/generator.js +40 -0
- package/src/utils/prompts.js +156 -0
- package/templates/terraform/network.tf +3 -1
package/src/commands/init.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import fsSync from 'fs';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
2
|
import path from 'path';
|
|
4
|
-
import { intro, outro,
|
|
3
|
+
import { intro, outro, spinner, log } from '@clack/prompts';
|
|
5
4
|
import color from 'picocolors';
|
|
6
|
-
import { execSync } from 'child_process';
|
|
7
5
|
|
|
8
6
|
import { checkDependency } from '../utils/system.js';
|
|
9
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
detectFramework,
|
|
9
|
+
parseProcfile,
|
|
10
|
+
parseVercelConfig,
|
|
11
|
+
analyzeNextConfig,
|
|
12
|
+
analyzeSvelteConfig,
|
|
13
|
+
analyzeAstroConfig
|
|
14
|
+
} from '../utils/detector.js';
|
|
10
15
|
import { trackEvent, flushTelemetry } from '../core/telemetry.js';
|
|
11
16
|
import { getFrameworkWarning } from '../utils/warnings.js';
|
|
12
17
|
import { provisionStateBucket } from '../utils/aws.js';
|
|
13
18
|
import { generateTemplates } from '../utils/generator.js';
|
|
14
19
|
import { handleExistingFiles } from '../utils/backup.js';
|
|
15
20
|
import { estimateMonthlyCost } from '../utils/visualizer.js';
|
|
21
|
+
import { getTargetDirectory, getProjectConfig } from '../utils/prompts.js';
|
|
22
|
+
import { resolveDjangoWsgi, handleRailsCI } from '../utils/frameworks.js';
|
|
23
|
+
|
|
24
|
+
const pkg = JSON.parse(fsSync.readFileSync(new URL('../../package.json', import.meta.url)));
|
|
25
|
+
const CLI_VERSION = pkg.version;
|
|
16
26
|
|
|
17
27
|
export async function mainStack({ isHeadless = false, headlessOptions = {} } = {}) {
|
|
18
28
|
const startTime = Date.now();
|
|
19
29
|
|
|
20
|
-
//
|
|
30
|
+
// 1. Silent Pre-flight check
|
|
21
31
|
const hasTerraform = await checkDependency('terraform');
|
|
22
32
|
if (!hasTerraform) {
|
|
23
33
|
console.error(color.red('✖ Terraform is not installed.'));
|
|
@@ -25,285 +35,66 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
25
35
|
process.exit(1);
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
let projectName, actualProjectName, targetDir;
|
|
31
|
-
let finalFramework, detectedFramework;
|
|
32
|
-
let djangoWsgi = 'core.wsgi';
|
|
33
|
-
let setupType = 'quick';
|
|
34
|
-
let needsDatabase = false;
|
|
35
|
-
let disableDefaultCI = false;
|
|
36
|
-
let project = {};
|
|
37
|
-
let currentGitBranch = 'main';
|
|
38
|
-
let procfile = null;
|
|
39
|
-
|
|
40
|
-
if (isHeadless) {
|
|
41
|
-
// --- HEADLESS MODE ---
|
|
42
|
-
projectName = getFlag('dir', '.');
|
|
43
|
-
actualProjectName = projectName === '.' ? path.basename(process.cwd()) : projectName;
|
|
44
|
-
targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
|
|
45
|
-
|
|
46
|
-
detectedFramework = detectFramework(targetDir);
|
|
47
|
-
procfile = parseProcfile(targetDir);
|
|
48
|
-
finalFramework = getFlag('framework', detectedFramework ? detectedFramework.id : 'static');
|
|
49
|
-
|
|
50
|
-
project = {
|
|
51
|
-
region: getFlag('region', 'us-east-2'),
|
|
52
|
-
port: getFlag('port', finalFramework === 'static' ? '8080' : '3000'),
|
|
53
|
-
size: getFlag('size', 'micro'),
|
|
54
|
-
healthCheckPath: getFlag('healthCheckPath', '/'),
|
|
55
|
-
desiredCount: getFlag('desiredCount', '1'),
|
|
56
|
-
branch: getFlag('branch', 'main')
|
|
57
|
-
};
|
|
38
|
+
if (!isHeadless) intro(color.bgCyan(color.black(' deploy-stack ☁️ ')));
|
|
58
39
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
40
|
+
// 2. Resolve Target & Scan Codebase
|
|
41
|
+
const dirConfig = await getTargetDirectory(isHeadless, headlessOptions);
|
|
42
|
+
const detectedFramework = detectFramework(dirConfig.targetDir);
|
|
43
|
+
const procfile = parseProcfile(dirConfig.targetDir);
|
|
44
|
+
const vercelRules = parseVercelConfig(dirConfig.targetDir);
|
|
64
45
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
validate: (value) => {
|
|
71
|
-
if (!value) return 'Please enter a name or directory.';
|
|
72
|
-
if (value !== '.' && value.includes(' ')) return 'Name cannot contain spaces.';
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (typeof projectName === 'symbol') {
|
|
77
|
-
cancel('Operation cancelled.');
|
|
78
|
-
process.exit(0);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
actualProjectName = projectName === '.' ? path.basename(process.cwd()) : projectName;
|
|
82
|
-
targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
|
|
83
|
-
|
|
84
|
-
// 2.5 Run the scanner
|
|
85
|
-
const detectedFramework = detectFramework(targetDir);
|
|
86
|
-
if (detectedFramework) {
|
|
87
|
-
log.success(`Auto-detected framework: ${detectedFramework.name}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 2.6 Run the Procfile Parser
|
|
91
|
-
procfile = parseProcfile(targetDir);
|
|
92
|
-
if (procfile && procfile.web) {
|
|
93
|
-
log.success(`Auto-detected Procfile (web command: ${procfile.web.join(' ')})`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// 2.7 Resolve the framework
|
|
97
|
-
finalFramework = detectedFramework ? detectedFramework.id : null;
|
|
98
|
-
|
|
99
|
-
if (!finalFramework) {
|
|
100
|
-
finalFramework = await select({
|
|
101
|
-
message: 'Which framework preset should we configure?',
|
|
102
|
-
options: [
|
|
103
|
-
{ value: 'node', label: 'Node.js / Express' },
|
|
104
|
-
{ value: 'nextjs', label: 'Next.js (Standalone)' },
|
|
105
|
-
{ value: 'nuxt', label: 'Nuxt 3 (SSR)' },
|
|
106
|
-
{ value: 'python', label: 'Python FastAPI' },
|
|
107
|
-
{ value: 'django', label: 'Django (Python)' },
|
|
108
|
-
{ value: 'rails', label: 'Ruby on Rails' },
|
|
109
|
-
{ value: 'go', label: 'Go (Golang)' },
|
|
110
|
-
{ value: 'static', label: 'Static Site (Gatsby, React, plain HTML via Nginx)' },
|
|
111
|
-
],
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if (typeof finalFramework === 'symbol') {
|
|
115
|
-
cancel('Operation cancelled.');
|
|
116
|
-
process.exit(0);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 2.8 Check if framework is Django and resolve wsgi.py path
|
|
121
|
-
djangoWsgi = 'core.wsgi';
|
|
122
|
-
if (finalFramework === 'django') {
|
|
123
|
-
let extractedWsgi = null;
|
|
124
|
-
|
|
125
|
-
// Check if Procfile already specifies the WSGI module
|
|
126
|
-
if (procfile && procfile.web) {
|
|
127
|
-
const webCommand = procfile.web.join(' ');
|
|
128
|
-
// Matches patterns like "gunicorn my_app.wsgi" or "my_app.wsgi:application"
|
|
129
|
-
const wsgiMatch = webCommand.match(/([a-zA-Z0-9_]+)\.wsgi/);
|
|
130
|
-
if (wsgiMatch) {
|
|
131
|
-
extractedWsgi = `${wsgiMatch[1]}.wsgi`;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
46
|
+
if (!isHeadless) {
|
|
47
|
+
if (detectedFramework) log.success(`Auto-detected framework: ${detectedFramework.name}`);
|
|
48
|
+
if (procfile && procfile.web) log.success(`Auto-detected Procfile (web command: ${procfile.web.join(' ')})`);
|
|
49
|
+
if (vercelRules) log.success(`Auto-detected vercel.json (Migrating edge network rules)`);
|
|
50
|
+
}
|
|
134
51
|
|
|
135
|
-
|
|
136
|
-
djangoWsgi = extractedWsgi;
|
|
137
|
-
log.success(`Auto-detected Django WSGI from Procfile: ${color.cyan(djangoWsgi)}`);
|
|
138
|
-
} else {
|
|
139
|
-
djangoWsgi = await text({
|
|
140
|
-
message: 'What is the Python module path to your Django wsgi.py?',
|
|
141
|
-
placeholder: 'core.wsgi',
|
|
142
|
-
initialValue: 'core.wsgi',
|
|
143
|
-
});
|
|
144
|
-
if (typeof djangoWsgi === 'symbol') process.exit(0);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
52
|
+
if (isHeadless) console.log(color.cyan(`🤖 Running deploy-stack in headless mode`));
|
|
147
53
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
{ value: 'quick', label: '⚡ Quickstart (Recommended)', hint: 'Production defaults, minimal prompts' },
|
|
153
|
-
{ value: 'advanced', label: '🛠️ Advanced Configuration', hint: 'Customize health checks, task count, branch, etc.' },
|
|
154
|
-
],
|
|
155
|
-
});
|
|
54
|
+
// 3. Gather Configuration & Framework Quirks
|
|
55
|
+
const config = await getProjectConfig(isHeadless, headlessOptions, dirConfig.targetDir, detectedFramework);
|
|
56
|
+
const djangoWsgi = await resolveDjangoWsgi(dirConfig.targetDir, procfile, config.framework, isHeadless);
|
|
57
|
+
const disableDefaultCI = await handleRailsCI(dirConfig.targetDir, config.framework, isHeadless);
|
|
156
58
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
59
|
+
// 4. Framework Migration Checks (Vercel Escape Hatch)
|
|
60
|
+
if (config.framework === 'nextjs') {
|
|
61
|
+
const nextConfig = analyzeNextConfig(dirConfig.targetDir);
|
|
62
|
+
if (nextConfig.hasConfig && !nextConfig.isStandalone) {
|
|
63
|
+
log.warn(color.yellow('⚠️ Next.js config is missing "output: \'standalone\'".'));
|
|
64
|
+
console.log(color.cyan(' Fix it here: https://github.com/anton-codes-iac/deploy-stack/blob/main/docs/migrations/nextjs-vercel-to-aws.md'));
|
|
160
65
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (finalFramework === 'python' || finalFramework === 'django') defaultPort = '8000';
|
|
167
|
-
if (finalFramework === 'rails') defaultPort = '3000';
|
|
168
|
-
if (finalFramework === 'go') defaultPort = '8080';
|
|
169
|
-
|
|
170
|
-
currentGitBranch = 'main';
|
|
171
|
-
try {
|
|
172
|
-
currentGitBranch = execSync('git symbolic-ref --short HEAD', { cwd: targetDir, stdio: 'pipe' }).toString().trim();
|
|
173
|
-
} catch (e) {
|
|
174
|
-
// Not a git repo yet, fallback to 'main'
|
|
66
|
+
} else if (detectedFramework?.name === 'SvelteKit') {
|
|
67
|
+
const svelteConfig = analyzeSvelteConfig(dirConfig.targetDir);
|
|
68
|
+
if (svelteConfig.adapter === 'vercel' || svelteConfig.adapter === 'auto') {
|
|
69
|
+
log.warn(color.yellow('⚠️ SvelteKit is locked into the Vercel/Auto adapter.'));
|
|
70
|
+
console.log(color.cyan(' Fix it here: https://github.com/anton-codes-iac/deploy-stack/blob/main/docs/migrations/sveltekit-vercel-to-aws.md'));
|
|
175
71
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (isBackendFramework) {
|
|
182
|
-
const dbChoice = await confirm({
|
|
183
|
-
message: 'Do you need a managed AWS RDS PostgreSQL database? (Adds ~$14/month or uses AWS Free Tier)',
|
|
184
|
-
initialValue: false,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
if (typeof dbChoice === 'symbol') {
|
|
188
|
-
cancel('Provisioning cancelled.')
|
|
189
|
-
process.exit(0);
|
|
190
|
-
}
|
|
191
|
-
needsDatabase = dbChoice;
|
|
72
|
+
} else if (detectedFramework?.name === 'Astro') {
|
|
73
|
+
const astroConfig = analyzeAstroConfig(dirConfig.targetDir);
|
|
74
|
+
if (astroConfig.adapter === 'vercel') {
|
|
75
|
+
log.warn(color.yellow('⚠️ Astro is locked into the Vercel adapter.'));
|
|
76
|
+
console.log(color.cyan(' Fix it here: https://github.com/anton-codes-iac/deploy-stack/blob/main/docs/migrations/astro-vercel-to-aws.md'));
|
|
192
77
|
}
|
|
193
|
-
|
|
194
|
-
// 6. Prompt Configuration Group
|
|
195
|
-
project = await group(
|
|
196
|
-
{
|
|
197
|
-
region: () =>
|
|
198
|
-
select({
|
|
199
|
-
message: 'Which AWS region do you want to deploy to?',
|
|
200
|
-
options: [
|
|
201
|
-
{ value: 'us-east-1', label: 'us-east-1 (N. Virginia)' },
|
|
202
|
-
{ value: 'us-east-2', label: 'us-east-2 (Ohio)' },
|
|
203
|
-
{ value: 'eu-west-1', label: 'eu-west-1 (Ireland)' },
|
|
204
|
-
{ value: 'eu-central-1', label: 'EU (Frankfurt)' },
|
|
205
|
-
{ value: 'ap-southeast-2', label: 'Asia Pacific (Sydney)' },
|
|
206
|
-
],
|
|
207
|
-
}),
|
|
208
|
-
port: () =>
|
|
209
|
-
text({
|
|
210
|
-
message: 'What port does your container expose?',
|
|
211
|
-
placeholder: defaultPort,
|
|
212
|
-
defaultValue: defaultPort,
|
|
213
|
-
}),
|
|
214
|
-
size: () =>
|
|
215
|
-
select({
|
|
216
|
-
message: 'Select your Fargate compute size:',
|
|
217
|
-
options: [
|
|
218
|
-
{ value: 'micro', label: 'Micro (0.25 vCPU, 512MB RAM) - Best for POCs' },
|
|
219
|
-
{ value: 'small', label: 'Small (0.5 vCPU, 1GB RAM) - Best for small Projects' },
|
|
220
|
-
],
|
|
221
|
-
}),
|
|
222
|
-
// --- Advanced-Only Prompts (Skipped if setupType === 'quick') ---
|
|
223
|
-
healthCheckPath: () => {
|
|
224
|
-
if (setupType === 'quick') return undefined;
|
|
225
|
-
return text({
|
|
226
|
-
message: 'ALB Health Check Path:',
|
|
227
|
-
placeholder: '/',
|
|
228
|
-
defaultValue: '/',
|
|
229
|
-
});
|
|
230
|
-
},
|
|
231
|
-
desiredCount: () => {
|
|
232
|
-
if (setupType === 'quick') return undefined;
|
|
233
|
-
return select({
|
|
234
|
-
message: 'How many container replicas (tasks) should run?',
|
|
235
|
-
options: [
|
|
236
|
-
{ value: '1', label: '1 Task (Single instance - lowest cost)' },
|
|
237
|
-
{ value: '2', label: '2 Tasks (High Availability across AZs)' },
|
|
238
|
-
],
|
|
239
|
-
defaultValue: '1',
|
|
240
|
-
});
|
|
241
|
-
},
|
|
242
|
-
branch: () => {
|
|
243
|
-
if (setupType === 'quick') return undefined;
|
|
244
|
-
return text({
|
|
245
|
-
message: 'Primary Git deployment branch for CI/CD:',
|
|
246
|
-
placeholder: currentGitBranch,
|
|
247
|
-
defaultValue: currentGitBranch,
|
|
248
|
-
});
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
onCancel: () => {
|
|
253
|
-
cancel('Provisioning cancelled.');
|
|
254
|
-
process.exit(0);
|
|
255
|
-
},
|
|
256
|
-
}
|
|
257
|
-
);
|
|
258
78
|
}
|
|
259
79
|
|
|
260
|
-
//
|
|
261
|
-
const cpu =
|
|
262
|
-
const memory =
|
|
263
|
-
const computeTier =
|
|
80
|
+
// 5. Calculate Derived Values
|
|
81
|
+
const cpu = config.size === 'small' ? '512' : '256';
|
|
82
|
+
const memory = config.size === 'small' ? '1024' : '512';
|
|
83
|
+
const computeTier = config.size === 'small' ? 'Small (0.5 vCPU, 1GB RAM)' : 'Micro (0.25 vCPU, 512MB RAM)';
|
|
264
84
|
|
|
265
|
-
const costs = estimateMonthlyCost({ cpu: parseInt(cpu), memory: parseInt(memory), hasDb: needsDatabase });
|
|
266
|
-
const estimatedCost = `~$${costs.totalMonthly} / month${needsDatabase ? ' (Includes Fargate + RDS PostgreSQL)' : ''}`;
|
|
267
|
-
|
|
268
|
-
const healthCheckPath = project.healthCheckPath || '/';
|
|
269
|
-
const desiredCount = project.desiredCount || '1';
|
|
270
|
-
const deployBranch = project.branch || currentGitBranch;
|
|
85
|
+
const costs = estimateMonthlyCost({ cpu: parseInt(cpu), memory: parseInt(memory), hasDb: config.needsDatabase });
|
|
86
|
+
const estimatedCost = `~$${costs.totalMonthly} / month${config.needsDatabase ? ' (Includes Fargate + RDS PostgreSQL)' : ''}`;
|
|
271
87
|
const buildDir = detectedFramework?.buildDir || 'dist';
|
|
272
88
|
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
const ciPath = path.join(targetDir, '.github', 'workflows', 'ci.yml');
|
|
276
|
-
const dependabotPath = path.join(targetDir, '.github', 'dependabot.yml');
|
|
277
|
-
|
|
278
|
-
if (fsSync.existsSync(ciPath) || fsSync.existsSync(dependabotPath)) {
|
|
279
|
-
if (!isHeadless) {
|
|
280
|
-
console.log('');
|
|
281
|
-
const ciContent = await confirm({
|
|
282
|
-
message: color.yellow('We detected default Rails GitHub Actions (ci.yml, dependabot.yml) that usually crash in isolated CI environments without a database. Would you like deploy-stack to safely disable them by renaming them to .bak?'),
|
|
283
|
-
initialValue: true,
|
|
284
|
-
});
|
|
285
|
-
if (typeof ciContent === 'symbol') {
|
|
286
|
-
cancel('Provisioning cancelled.');
|
|
287
|
-
process.exit(0);
|
|
288
|
-
}
|
|
289
|
-
disableDefaultCI = ciContent;
|
|
290
|
-
} else {
|
|
291
|
-
// Headless: automatically disable conflicting CI
|
|
292
|
-
disableDefaultCI = true;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// 8. Safely handle existing files (Backup and auto-prune)
|
|
298
|
-
await handleExistingFiles(targetDir, isHeadless);
|
|
89
|
+
// 6. Handle Backups & Provision Remote State
|
|
90
|
+
await handleExistingFiles(dirConfig.targetDir, isHeadless);
|
|
299
91
|
|
|
300
92
|
const s = spinner();
|
|
301
93
|
s.start('Provisioning infrastructure...');
|
|
302
94
|
|
|
303
|
-
// 9. Provision S3 bucket for Terraform state & enable versioning
|
|
304
95
|
let awsAccountId, stateBucketName;
|
|
305
96
|
try {
|
|
306
|
-
const bucketData = await provisionStateBucket(
|
|
97
|
+
const bucketData = await provisionStateBucket(config.region, dirConfig.actualProjectName);
|
|
307
98
|
awsAccountId = bucketData.awsAccountId;
|
|
308
99
|
stateBucketName = bucketData.stateBucketName;
|
|
309
100
|
} catch (error) {
|
|
@@ -314,70 +105,82 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
|
|
|
314
105
|
process.exit(1);
|
|
315
106
|
}
|
|
316
107
|
|
|
108
|
+
// 7. Synthesize Templates
|
|
317
109
|
s.message('Synthesizing Terraform templates...');
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
REGION: project.region,
|
|
323
|
-
PORT: project.port,
|
|
110
|
+
await generateTemplates(dirConfig.targetDir, {
|
|
111
|
+
PROJECT_NAME: dirConfig.actualProjectName,
|
|
112
|
+
REGION: config.region,
|
|
113
|
+
PORT: config.port,
|
|
324
114
|
CPU: cpu,
|
|
325
115
|
MEMORY: memory,
|
|
326
116
|
COMPUTE_TIER: computeTier,
|
|
327
117
|
ESTIMATED_COST: estimatedCost,
|
|
328
118
|
STATE_BUCKET: stateBucketName,
|
|
329
119
|
AWS_ACCOUNT_ID: awsAccountId,
|
|
330
|
-
HEALTH_CHECK_PATH: healthCheckPath,
|
|
331
|
-
DESIRED_COUNT: desiredCount,
|
|
332
|
-
DEPLOY_BRANCH:
|
|
120
|
+
HEALTH_CHECK_PATH: config.healthCheckPath,
|
|
121
|
+
DESIRED_COUNT: config.desiredCount,
|
|
122
|
+
DEPLOY_BRANCH: config.branch,
|
|
333
123
|
BUILD_DIR: buildDir,
|
|
334
|
-
finalFramework:
|
|
335
|
-
NEEDS_DATABASE: needsDatabase,
|
|
124
|
+
finalFramework: config.framework,
|
|
125
|
+
NEEDS_DATABASE: config.needsDatabase,
|
|
336
126
|
DJANGO_WSGI: djangoWsgi,
|
|
337
127
|
DISABLE_DEFAULT_CI: disableDefaultCI,
|
|
338
|
-
PROCFILE: procfile
|
|
128
|
+
PROCFILE: procfile,
|
|
129
|
+
VERCEL_RULES: vercelRules
|
|
339
130
|
});
|
|
340
131
|
|
|
341
|
-
//
|
|
132
|
+
// 8. Telemetry
|
|
342
133
|
trackEvent('project_provisioned', {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
134
|
+
// 1. Core & Context
|
|
135
|
+
projectName: dirConfig.actualProjectName,
|
|
136
|
+
cli_version: CLI_VERSION,
|
|
137
|
+
is_headless: isHeadless,
|
|
138
|
+
setup_mode: config.setupType,
|
|
139
|
+
duration_ms: Date.now() - startTime,
|
|
140
|
+
|
|
141
|
+
// 2. Infrastructure Shape
|
|
142
|
+
framework: config.framework,
|
|
143
|
+
specific_framework: detectedFramework?.name || config.framework,
|
|
144
|
+
region: config.region,
|
|
145
|
+
size: config.size,
|
|
146
|
+
desired_count: parseInt(config.desiredCount),
|
|
147
|
+
has_database: config.needsDatabase,
|
|
148
|
+
has_custom_health_check: config.healthCheckPath !== '/',
|
|
149
|
+
|
|
150
|
+
// 3. Advanced Features & PaaS Context
|
|
151
|
+
has_worker: !!(procfile && procfile.worker),
|
|
152
|
+
is_heroku_migration: !!procfile,
|
|
153
|
+
is_vercel_migration: !!vercelRules,
|
|
351
154
|
});
|
|
352
155
|
|
|
353
156
|
s.stop('Infrastructure provisioned successfully!');
|
|
354
157
|
|
|
355
|
-
//
|
|
158
|
+
// 9. Output
|
|
356
159
|
let frameworkWarnings = '';
|
|
357
|
-
if (!(
|
|
358
|
-
frameworkWarnings = getFrameworkWarning(
|
|
160
|
+
if (!(config.framework === 'static' && detectedFramework?.buildDir)) {
|
|
161
|
+
frameworkWarnings = getFrameworkWarning(config.framework);
|
|
359
162
|
}
|
|
360
163
|
|
|
361
|
-
const isGitInitialized = fsSync.existsSync(path.join(targetDir, '.git'));
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
const applyStep = needsCd
|
|
365
|
-
? `cd ${projectName} && npx --yes deploy-stack apply`
|
|
366
|
-
: 'npx --yes deploy-stack apply';
|
|
367
|
-
|
|
164
|
+
const isGitInitialized = fsSync.existsSync(path.join(dirConfig.targetDir, '.git'));
|
|
165
|
+
const needsCd = dirConfig.projectName && dirConfig.projectName !== '.';
|
|
166
|
+
const applyStep = needsCd ? `cd ${dirConfig.projectName} && npx --yes deploy-stack apply` : 'npx --yes deploy-stack apply';
|
|
368
167
|
const gitInstructions = isGitInitialized
|
|
369
168
|
? `git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git push`
|
|
370
|
-
: `git init && git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git branch -M ${
|
|
169
|
+
: `git init && git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git branch -M ${config.branch} && git remote add origin https://github.com/your-username/your-repo.git && git push -u origin ${config.branch}`;
|
|
170
|
+
|
|
171
|
+
let docsTip = '';
|
|
172
|
+
if (procfile) {
|
|
173
|
+
docsTip = `\n ${color.blue('📘 Read the Heroku Migration Guide:')} ${color.underline('https://github.com/anton-codes-iac/deploy-stack/blob/main/docs/migrations/heroku-procfile-to-aws.md')}`;
|
|
174
|
+
} else if (config.needsDatabase) {
|
|
175
|
+
docsTip = `\n ${color.blue('📘 Read the Database Connections Guide:')} ${color.underline('https://github.com/anton-codes-iac/deploy-stack/blob/main/docs/guides/database-connections.md')}`;
|
|
176
|
+
}
|
|
371
177
|
|
|
372
|
-
|
|
178
|
+
outro(`${color.green('✅ Templates generated!')} ${color.blue('🛡️ DevSecOps scanning enabled.')}
|
|
373
179
|
${frameworkWarnings ? `\n ${frameworkWarnings}` : ''}
|
|
374
180
|
${color.yellow('Next steps:')}
|
|
375
181
|
1. ${color.cyan(applyStep)}
|
|
376
182
|
2. ${color.cyan(gitInstructions)}
|
|
377
|
-
${color.magenta('🚀 Need help?')} ${color.underline('https://calendly.com/anton-codes-iac/15min')}
|
|
378
|
-
|
|
379
|
-
outro(outroMessage);
|
|
183
|
+
${color.magenta('🚀 Need help?')} ${color.underline('https://calendly.com/anton-codes-iac/15min')}`);
|
|
380
184
|
|
|
381
|
-
// 13.Ensure all analytics are sent before the CLI terminates
|
|
382
185
|
await flushTelemetry();
|
|
383
186
|
}
|
package/src/commands/secrets.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'fs/promises';
|
|
|
4
4
|
import { spinner } from '@clack/prompts';
|
|
5
5
|
import color from 'picocolors';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import { trackEvent, flushTelemetry } from '../core/telemetry.js';
|
|
7
8
|
|
|
8
9
|
export async function pushSecrets(envFilePath, projectName) {
|
|
9
10
|
const s = spinner();
|
|
@@ -41,8 +42,23 @@ export async function pushSecrets(envFilePath, projectName) {
|
|
|
41
42
|
s.stop(`✅ Successfully pushed ${Object.keys(parsedSecrets).length} secrets to AWS!`);
|
|
42
43
|
console.log(color.cyan(`\nUpdated ${keysFilePath}`));
|
|
43
44
|
console.log(color.green('Commit this file and push to GitHub to trigger a deployment with your new variables.'));
|
|
45
|
+
console.log(color.blue(`\n📘 Learn how secrets reach your app: ${color.underline('https://github.com/anton-codes-iac/deploy-stack/blob/main/docs/guides/secrets-management.md')}`));
|
|
46
|
+
|
|
47
|
+
trackEvent('secrets_pushed', {
|
|
48
|
+
projectName,
|
|
49
|
+
secret_count: Object.keys(parsedSecrets).length,
|
|
50
|
+
success: true
|
|
51
|
+
});
|
|
52
|
+
await flushTelemetry();
|
|
44
53
|
|
|
45
54
|
} catch (error) {
|
|
46
55
|
s.stop(`❌ Failed to push secrets: ${error.message}`);
|
|
56
|
+
|
|
57
|
+
trackEvent('secrets_pushed', {
|
|
58
|
+
projectName,
|
|
59
|
+
success: false,
|
|
60
|
+
error_code: error.name || 'UNKNOWN'
|
|
61
|
+
});
|
|
62
|
+
await flushTelemetry();
|
|
47
63
|
}
|
|
48
64
|
}
|
package/src/core/telemetry.js
CHANGED
package/src/utils/detector.js
CHANGED
|
@@ -94,4 +94,99 @@ export function parseProcfile(targetDir) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
return Object.keys(processes).length > 0 ? processes : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Parses a vercel.json file to extract routing and edge rules
|
|
100
|
+
export function parseVercelConfig(targetDir) {
|
|
101
|
+
const vercelConfigPath = path.join(targetDir, 'vercel.json');
|
|
102
|
+
if (!fsSync.existsSync(vercelConfigPath)) return null;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const content = fsSync.readFileSync(vercelConfigPath, 'utf-8');
|
|
106
|
+
const vercelJson = JSON.parse(content);
|
|
107
|
+
|
|
108
|
+
// We only care about network-level edge rules that AWS needs to handle
|
|
109
|
+
const rules = {
|
|
110
|
+
redirects: vercelJson.redirects || null,
|
|
111
|
+
headers: vercelJson.headers || null,
|
|
112
|
+
rewrites: vercelJson.rewrites || null
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// If it's just an empty vercel.json, return null
|
|
116
|
+
if (!rules.redirects && !rules.headers && !rules.rewrites) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return rules;
|
|
121
|
+
} catch (e) {
|
|
122
|
+
// Silently fail on malformed JSON
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Checks if Next.js is configured for 'standalone' output
|
|
128
|
+
export function analyzeNextConfig(targetDir) {
|
|
129
|
+
const extensions = ['js', 'mjs', 'cjs', 'ts'];
|
|
130
|
+
let configPath = null;
|
|
131
|
+
let configContent = '';
|
|
132
|
+
|
|
133
|
+
for (const ext of extensions) {
|
|
134
|
+
const tempPath = path.join(targetDir, `next.config.${ext}`);
|
|
135
|
+
if (fsSync.existsSync(tempPath)) {
|
|
136
|
+
configPath = tempPath;
|
|
137
|
+
configContent = fsSync.readFileSync(tempPath, 'utf-8');
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!configPath) return { hasConfig: false, isStandalone: false };
|
|
143
|
+
|
|
144
|
+
// Regex looks for output: 'standalone' or output: "standalone" (handling spacing)
|
|
145
|
+
const isStandalone = /output\s*:\s*['"`]standalone['"`]/.test(configContent);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
hasConfig: true,
|
|
149
|
+
isStandalone: isStandalone,
|
|
150
|
+
configPath: configPath
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Checks if SvelteKit is locked into Vercel
|
|
155
|
+
export function analyzeSvelteConfig(targetDir) {
|
|
156
|
+
const configPath = path.join(targetDir, 'svelte.config.js');
|
|
157
|
+
if (!fsSync.existsSync(configPath)) return { hasConfig: false, adapter: 'unknown' };
|
|
158
|
+
|
|
159
|
+
const content = fsSync.readFileSync(configPath, 'utf-8');
|
|
160
|
+
|
|
161
|
+
let adapter = 'unknown';
|
|
162
|
+
if (content.includes('@sveltejs/adapter-vercel')) adapter = 'vercel';
|
|
163
|
+
else if (content.includes('@sveltejs/adapter-node')) adapter = 'node';
|
|
164
|
+
else if (content.includes('@sveltejs/adapter-static')) adapter = 'static';
|
|
165
|
+
else if (content.includes('@sveltejs/adapter-auto')) adapter = 'auto'; // Vercel's default
|
|
166
|
+
|
|
167
|
+
return { hasConfig: true, adapter };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Checks if Astro is locked into Vercel
|
|
171
|
+
export function analyzeAstroConfig(targetDir) {
|
|
172
|
+
const extensions = ['mjs', 'js', 'ts', 'cjs'];
|
|
173
|
+
let configPath = null;
|
|
174
|
+
let content = '';
|
|
175
|
+
|
|
176
|
+
for (const ext of extensions) {
|
|
177
|
+
const tempPath = path.join(targetDir, `astro.config.${ext}`);
|
|
178
|
+
if (fsSync.existsSync(tempPath)) {
|
|
179
|
+
configPath = tempPath;
|
|
180
|
+
content = fsSync.readFileSync(tempPath, 'utf-8');
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!configPath) return { hasConfig: false, adapter: 'unknown' };
|
|
186
|
+
|
|
187
|
+
let adapter = 'unknown';
|
|
188
|
+
if (content.includes('@astrojs/vercel')) adapter = 'vercel';
|
|
189
|
+
else if (content.includes('@astrojs/node')) adapter = 'node';
|
|
190
|
+
|
|
191
|
+
return { hasConfig: true, adapter };
|
|
97
192
|
}
|