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.
@@ -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, group, text, select, spinner, cancel, confirm, log } from '@clack/prompts';
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 { detectFramework, parseProcfile } from '../utils/detector.js';
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
- // 0. Silent Pre-flight check
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
- const getFlag = (key, defaultValue) => headlessOptions[key] !== undefined ? headlessOptions[key] : defaultValue;
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
- console.log(color.cyan(`🤖 Running deploy-stack in headless mode [${finalFramework} -> ${project.region}]`));
60
- } else {
61
- // --- INTERACTIVE MODE ---
62
- // 1. Start the CLI
63
- intro(color.bgCyan(color.black(' deploy-stack ☁️ ')));
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
- // 2. Ask for the target directory FIRST
66
- projectName = await text({
67
- message: 'Where should we generate the infrastructure? (Type "." for current directory)',
68
- placeholder: '.',
69
- initialValue: '.',
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
- if (extractedWsgi) {
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
- // 3. Prompt for Setup Mode
149
- setupType = await select({
150
- message: 'Choose your setup mode:',
151
- options: [
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
- if (typeof setupType === 'symbol') {
158
- cancel('Operation cancelled.');
159
- process.exit(0);
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
- // 4. Set intelligent defaults & check current Git branch
163
- let defaultPort = '3000';
164
-
165
- if (finalFramework === 'static') defaultPort = '8080';
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
- // 5. Ask for Managed Database (Only for Backend/Fullstack Frameworks)
178
- needsDatabase = false;
179
- const isBackendFramework = ['node', 'nextjs', 'nuxt', 'python', 'django', 'rails', 'go'].includes(finalFramework);
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
- // 7. Map the user's choices and update the variables
261
- const cpu = project.size === 'small' ? '512' : '256';
262
- const memory = project.size === 'small' ? '1024' : '512';
263
- const computeTier = project.size === 'small' ? 'Small (0.5 vCPU, 1GB RAM)' : 'Micro (0.25 vCPU, 512MB RAM)';
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
- // 7.1 Check for conflicting CI boilerplate (Rails)
274
- if (finalFramework === 'rails') {
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(project.region, actualProjectName);
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
- // 10. Generate all templates and directories
320
- await generateTemplates(targetDir, {
321
- PROJECT_NAME: actualProjectName,
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: deployBranch,
120
+ HEALTH_CHECK_PATH: config.healthCheckPath,
121
+ DESIRED_COUNT: config.desiredCount,
122
+ DEPLOY_BRANCH: config.branch,
333
123
  BUILD_DIR: buildDir,
334
- finalFramework: 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
- // 11. Track the event in telemetry
132
+ // 8. Telemetry
342
133
  trackEvent('project_provisioned', {
343
- projectName: actualProjectName,
344
- framework: finalFramework,
345
- specific_framework: detectedFramework?.name || finalFramework,
346
- region: project.region,
347
- size: project.size,
348
- setup_mode: setupType,
349
- desiredCount: desiredCount,
350
- duration_ms: Date.now() - startTime
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
- // 12. Provide the Outro, Framework Warnings and Next Steps
158
+ // 9. Output
356
159
  let frameworkWarnings = '';
357
- if (!(finalFramework === 'static' && detectedFramework?.buildDir)) {
358
- frameworkWarnings = getFrameworkWarning(finalFramework);
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 needsCd = projectName && projectName !== '.';
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 ${deployBranch} && git remote add origin https://github.com/your-username/your-repo.git && git push -u origin ${deployBranch}`;
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
- const outroMessage = `${color.green('✅ Templates generated!')} ${color.blue('🛡️ DevSecOps scanning enabled.')}
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
  }
@@ -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
  }
@@ -26,6 +26,7 @@ export function trackEvent(eventName, properties) {
26
26
  properties: {
27
27
  os: process.platform,
28
28
  node_version: process.version,
29
+ is_ci: Boolean(process.env.CI || process.env.CONTINUOUS_INTEGRATION),
29
30
  ...properties
30
31
  }
31
32
  };
@@ -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
  }