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.
@@ -0,0 +1,55 @@
1
+ import fsSync from 'fs';
2
+ import path from 'path';
3
+ import { text, confirm, cancel, log } from '@clack/prompts';
4
+ import color from 'picocolors';
5
+
6
+ export async function resolveDjangoWsgi(targetDir, procfile, framework, isHeadless) {
7
+ if (framework !== 'django') return 'core.wsgi';
8
+
9
+ let extractedWsgi = null;
10
+ if (procfile && procfile.web) {
11
+ const webCommand = procfile.web.join(' ');
12
+ const wsgiMatch = webCommand.match(/([a-zA-Z0-9_]+)\.wsgi/);
13
+ if (wsgiMatch) extractedWsgi = `${wsgiMatch[1]}.wsgi`;
14
+ }
15
+
16
+ if (extractedWsgi) {
17
+ if (!isHeadless) log.success(`Auto-detected Django WSGI from Procfile: ${color.cyan(extractedWsgi)}`);
18
+ return extractedWsgi;
19
+ }
20
+
21
+ if (isHeadless) return 'core.wsgi';
22
+
23
+ const djangoWsgi = await text({
24
+ message: 'What is the Python module path to your Django wsgi.py?',
25
+ placeholder: 'core.wsgi',
26
+ initialValue: 'core.wsgi',
27
+ });
28
+
29
+ if (typeof djangoWsgi === 'symbol') process.exit(0);
30
+ return djangoWsgi;
31
+ }
32
+
33
+ export async function handleRailsCI(targetDir, framework, isHeadless) {
34
+ if (framework !== 'rails') return false;
35
+
36
+ const ciPath = path.join(targetDir, '.github', 'workflows', 'ci.yml');
37
+ const dependabotPath = path.join(targetDir, '.github', 'dependabot.yml');
38
+
39
+ if (fsSync.existsSync(ciPath) || fsSync.existsSync(dependabotPath)) {
40
+ if (isHeadless) return true;
41
+
42
+ console.log('');
43
+ const disable = await confirm({
44
+ 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?'),
45
+ initialValue: true,
46
+ });
47
+
48
+ if (typeof disable === 'symbol') {
49
+ cancel('Provisioning cancelled.');
50
+ process.exit(0);
51
+ }
52
+ return disable;
53
+ }
54
+ return false;
55
+ }
@@ -89,6 +89,46 @@ export async function generateTemplates(targetDir, config) {
89
89
  ? config.PROJECT_NAME.substring(0, 27).replace(/-$/, '') // Remove trailing hyphens
90
90
  : config.PROJECT_NAME;
91
91
 
92
+ // 4.5. Translate Vercel Rules to AWS ALB Listener Rules
93
+ let vercelTerraformRules = '';
94
+
95
+ if (config.VERCEL_RULES && config.VERCEL_RULES.redirects) {
96
+ config.VERCEL_RULES.redirects.forEach((rule, index) => {
97
+ // Vercel uses 'permanent: true' for 301, false for 302
98
+ const statusCode = rule.permanent === false ? "HTTP_302" : "HTTP_301";
99
+
100
+ // Map Vercel's source path to AWS ALB Path Pattern
101
+ let sourcePath = rule.source;
102
+ // Vercel sometimes uses regex syntax like '/blog/(.*)'. ALB uses '/blog/*'
103
+ sourcePath = sourcePath.replace(/\(\.\*\)/g, '*');
104
+
105
+ vercelTerraformRules += `
106
+ # Auto-generated from vercel.json redirect
107
+ resource "aws_lb_listener_rule" "vercel_redirect_${index}" {
108
+ listener_arn = aws_lb_listener.http.arn
109
+ priority = ${100 + index} # Start at 100 to avoid conflicts
110
+
111
+ action {
112
+ type = "redirect"
113
+ redirect {
114
+ status_code = "${statusCode}"
115
+ path = "${rule.destination}"
116
+ }
117
+ }
118
+
119
+ condition {
120
+ path_pattern {
121
+ values = ["${sourcePath}"]
122
+ }
123
+ }
124
+ }
125
+ `;
126
+ });
127
+ }
128
+
129
+ // Assign the compiled HCL to the config object so the template engine can inject it
130
+ config.VERCEL_EDGE_ROUTING = vercelTerraformRules;
131
+
92
132
  // 5. Process standard files
93
133
  for (const file of filesToProcess) {
94
134
  let content = await fs.readFile(path.join(templatesDir, file.src), 'utf-8');
@@ -0,0 +1,156 @@
1
+ import path from 'path';
2
+ import { text, select, confirm, group, cancel, log } from '@clack/prompts';
3
+ import color from 'picocolors';
4
+ import { execSync } from 'child_process';
5
+
6
+ export async function getTargetDirectory(isHeadless, headlessOptions) {
7
+ if (isHeadless) {
8
+ const projectName = headlessOptions.dir || '.';
9
+ return {
10
+ projectName,
11
+ actualProjectName: projectName === '.' ? path.basename(process.cwd()) : projectName,
12
+ targetDir: projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName)
13
+ };
14
+ }
15
+
16
+ const projectName = await text({
17
+ message: 'Where should we generate the infrastructure? (Type "." for current directory)',
18
+ placeholder: '.',
19
+ initialValue: '.',
20
+ validate: (value) => {
21
+ if (!value) return 'Please enter a name or directory.';
22
+ if (value !== '.' && value.includes(' ')) return 'Name cannot contain spaces.';
23
+ },
24
+ });
25
+
26
+ if (typeof projectName === 'symbol') {
27
+ cancel('Operation cancelled.');
28
+ process.exit(0);
29
+ }
30
+
31
+ return {
32
+ projectName,
33
+ actualProjectName: projectName === '.' ? path.basename(process.cwd()) : projectName,
34
+ targetDir: projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName)
35
+ };
36
+ }
37
+
38
+ export async function getProjectConfig(isHeadless, headlessOptions, targetDir, detectedFramework) {
39
+ if (isHeadless) {
40
+ return {
41
+ framework: headlessOptions.framework || (detectedFramework ? detectedFramework.id : 'static'),
42
+ region: headlessOptions.region || 'us-east-2',
43
+ port: headlessOptions.port || (headlessOptions.framework === 'static' ? '8080' : '3000'),
44
+ size: headlessOptions.size || 'micro',
45
+ healthCheckPath: headlessOptions.healthCheckPath || '/',
46
+ desiredCount: headlessOptions.desiredCount || '1',
47
+ branch: headlessOptions.branch || 'main',
48
+ needsDatabase: false,
49
+ setupType: 'headless'
50
+ };
51
+ }
52
+
53
+ let finalFramework = detectedFramework ? detectedFramework.id : null;
54
+
55
+ if (!finalFramework) {
56
+ finalFramework = await select({
57
+ message: 'Which framework preset should we configure?',
58
+ options: [
59
+ { value: 'node', label: 'Node.js / Express' },
60
+ { value: 'nextjs', label: 'Next.js (Standalone)' },
61
+ { value: 'nuxt', label: 'Nuxt 3 (SSR)' },
62
+ { value: 'python', label: 'Python FastAPI' },
63
+ { value: 'django', label: 'Django (Python)' },
64
+ { value: 'rails', label: 'Ruby on Rails' },
65
+ { value: 'go', label: 'Go (Golang)' },
66
+ { value: 'static', label: 'Static Site (Gatsby, React, plain HTML via Nginx)' },
67
+ ],
68
+ });
69
+ if (typeof finalFramework === 'symbol') process.exit(0);
70
+ }
71
+
72
+ const setupType = await select({
73
+ message: 'Choose your setup mode:',
74
+ options: [
75
+ { value: 'quick', label: '⚡ Quickstart (Recommended)', hint: 'Production defaults, minimal prompts' },
76
+ { value: 'advanced', label: '🛠️ Advanced Configuration', hint: 'Customize health checks, task count, branch, etc.' },
77
+ ],
78
+ });
79
+ if (typeof setupType === 'symbol') process.exit(0);
80
+
81
+ let defaultPort = '3000';
82
+ if (finalFramework === 'static' || finalFramework === 'go') defaultPort = '8080';
83
+ if (finalFramework === 'python' || finalFramework === 'django') defaultPort = '8000';
84
+
85
+ let currentGitBranch = 'main';
86
+ try {
87
+ currentGitBranch = execSync('git symbolic-ref --short HEAD', { cwd: targetDir, stdio: 'pipe' }).toString().trim();
88
+ } catch (e) { }
89
+
90
+ let needsDatabase = false;
91
+ const isBackendFramework = ['node', 'nextjs', 'nuxt', 'python', 'django', 'rails', 'go'].includes(finalFramework);
92
+
93
+ if (isBackendFramework) {
94
+ const dbChoice = await confirm({
95
+ message: 'Do you need a managed AWS RDS PostgreSQL database? (Adds ~$14/month or uses AWS Free Tier)',
96
+ initialValue: false,
97
+ });
98
+ if (typeof dbChoice === 'symbol') process.exit(0);
99
+ needsDatabase = dbChoice;
100
+ }
101
+
102
+ const project = await group({
103
+ region: () => select({
104
+ message: 'Which AWS region do you want to deploy to?',
105
+ options: [
106
+ { value: 'us-east-1', label: 'us-east-1 (N. Virginia)' },
107
+ { value: 'us-east-2', label: 'us-east-2 (Ohio)' },
108
+ { value: 'eu-west-1', label: 'eu-west-1 (Ireland)' },
109
+ { value: 'eu-central-1', label: 'EU (Frankfurt)' },
110
+ { value: 'ap-southeast-2', label: 'Asia Pacific (Sydney)' },
111
+ ],
112
+ }),
113
+ port: () => text({
114
+ message: 'What port does your container expose?',
115
+ placeholder: defaultPort,
116
+ defaultValue: defaultPort,
117
+ }),
118
+ size: () => select({
119
+ message: 'Select your Fargate compute size:',
120
+ options: [
121
+ { value: 'micro', label: 'Micro (0.25 vCPU, 512MB RAM) - Best for POCs' },
122
+ { value: 'small', label: 'Small (0.5 vCPU, 1GB RAM) - Best for small Projects' },
123
+ ],
124
+ }),
125
+ healthCheckPath: () => setupType === 'quick' ? undefined : text({
126
+ message: 'ALB Health Check Path:',
127
+ placeholder: '/',
128
+ defaultValue: '/',
129
+ }),
130
+ desiredCount: () => setupType === 'quick' ? undefined : select({
131
+ message: 'How many container replicas (tasks) should run?',
132
+ options: [
133
+ { value: '1', label: '1 Task (Single instance - lowest cost)' },
134
+ { value: '2', label: '2 Tasks (High Availability across AZs)' },
135
+ ],
136
+ defaultValue: '1',
137
+ }),
138
+ branch: () => setupType === 'quick' ? undefined : text({
139
+ message: 'Primary Git deployment branch for CI/CD:',
140
+ placeholder: currentGitBranch,
141
+ defaultValue: currentGitBranch,
142
+ }),
143
+ }, { onCancel: () => process.exit(0) });
144
+
145
+ return {
146
+ framework: finalFramework,
147
+ region: project.region,
148
+ port: project.port,
149
+ size: project.size,
150
+ healthCheckPath: project.healthCheckPath || '/',
151
+ desiredCount: project.desiredCount || '1',
152
+ branch: project.branch || currentGitBranch,
153
+ needsDatabase,
154
+ setupType
155
+ };
156
+ }
@@ -90,4 +90,6 @@ resource "aws_security_group" "ecs_tasks" {
90
90
  # trivy:ignore:AVD-AWS-0104 - Allow containers to pull images and hit external APIs
91
91
  cidr_blocks = ["0.0.0.0/0"]
92
92
  }
93
- }
93
+ }
94
+
95
+ {{VERCEL_EDGE_ROUTING}}