@launchframe/cli 0.1.6

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,349 @@
1
+ /**
2
+ * Variant Configuration
3
+ *
4
+ * Defines how to ADD functionality to the base template via variants.
5
+ * Follows open-closed principle: base is minimal, variants ADD features.
6
+ *
7
+ * Two types of additions:
8
+ * - SECTIONS: Code snippets inserted into base template files (via {{MARKERS}})
9
+ * - FILES: Complete files/folders copied to the project
10
+ */
11
+
12
+ const VARIANT_CONFIG = {
13
+ backend: {
14
+ base: 'backend/templates/base',
15
+ sectionsDir: 'backend/templates/sections',
16
+ filesDir: 'backend/templates/files',
17
+
18
+ variants: {
19
+ // Multi-tenant variant: Adds project/workspace support
20
+ 'multi-tenant': {
21
+ // Complete files/folders to copy
22
+ files: [
23
+ 'src/modules/domain/projects', // Entire projects module
24
+ 'src/modules/domain/jwt-auth', // JWT auth module (project-specific JWT issuance)
25
+ 'src/modules/domain/ai/services/project-config.service.ts', // Project config service
26
+ 'src/guards/project-ownership.guard.ts', // Project ownership guard
27
+ 'src/modules/auth/auth.service.ts', // Auth service with multi-tenant support
28
+ 'src/modules/auth/auth.controller.ts', // Auth controller with multi-tenant support
29
+ 'src/modules/users/users.service.ts', // Users service with multi-tenant support
30
+ 'src/modules/users/users.controller.ts', // Users controller with multi-tenant support
31
+ 'src/modules/users/create-user.dto.ts' // CreateUserDto with businessId and recoveryCode
32
+ ],
33
+
34
+ // Code sections to insert into base template files
35
+ sections: {
36
+ 'src/main.ts': [
37
+ 'PROJECT_IMPORTS', // Add project-related imports
38
+ 'PROJECT_CUSTOM_DOMAINS', // Add custom domains query
39
+ 'PROJECT_CUSTOM_DOMAINS_CORS', // Add custom domains to CORS
40
+ 'PROJECT_GUARD' // Add ProjectOwnershipGuard registration
41
+ ],
42
+ 'src/modules/app/app.module.ts': [
43
+ 'PROJECTS_MODULE_IMPORT', // Add ProjectsModule import
44
+ 'PROJECTS_MODULE', // Add ProjectsModule to imports array
45
+ 'JWT_AUTH_MODULE_IMPORT', // Add JwtAuthModule import
46
+ 'JWT_AUTH_MODULE' // Add JwtAuthModule to imports array
47
+ ],
48
+ 'src/modules/auth/auth.module.ts': [
49
+ 'MULTI_TENANT_IMPORTS', // Add Project entity import
50
+ 'MULTI_TENANT_TYPEORM' // Add Project to TypeOrmModule
51
+ ],
52
+ 'src/modules/users/user.entity.ts': [
53
+ 'PROJECTS_RELATIONSHIP_IMPORT', // Add Project entity import
54
+ 'PROJECTS_RELATIONSHIP' // Add projects relationship
55
+ ],
56
+ 'src/modules/users/users.module.ts': [
57
+ 'MULTI_TENANT_IMPORTS', // Add Projects-related imports
58
+ 'MULTI_TENANT_SERVICE_IMPORTS', // Add JWT auth service imports
59
+ 'MULTI_TENANT_ENTITIES', // Add Project entities to TypeORM
60
+ 'MULTI_TENANT_MODULE_IMPORTS', // Add ProjectsModule to imports
61
+ 'MULTI_TENANT_PROVIDERS' // Add multi-tenant providers
62
+ ]
63
+ }
64
+ },
65
+
66
+ // B2B2C variant: Adds regular_user support
67
+ 'b2b2c': {
68
+ // Complete files to copy
69
+ files: [
70
+ 'src/modules/users/user-business.entity.ts' // Business-to-user linking entity
71
+ ],
72
+
73
+ // Code sections to insert
74
+ sections: {
75
+ 'src/modules/users/user.entity.ts': [
76
+ 'B2B2C_IMPORTS', // Add UserBusiness import
77
+ 'B2B2C_USER_ROLE', // Add REGULAR_USER enum value
78
+ 'B2B2C_RELATIONSHIPS' // Add userBusinesses relationship
79
+ ],
80
+ 'src/modules/users/users.module.ts': [
81
+ 'B2B2C_IMPORTS', // Add UserBusiness import
82
+ 'B2B2C_ENTITIES' // Add UserBusiness to TypeORM
83
+ ]
84
+ }
85
+ }
86
+ },
87
+
88
+ // Variant selection prompts
89
+ prompts: {
90
+ tenancy: {
91
+ message: 'Will your application have multiple workspaces/projects per user?',
92
+ choices: [
93
+ {
94
+ name: 'Single-tenant (one instance per user) - Simpler data model',
95
+ value: 'single-tenant',
96
+ description: 'Each user has one instance. Simpler architecture without project isolation.',
97
+ isDefault: true // Base template is single-tenant
98
+ },
99
+ {
100
+ name: 'Multi-tenant (workspaces/projects) - Users can create multiple projects',
101
+ value: 'multi-tenant',
102
+ description: 'Each user can create multiple isolated workspaces. Data is scoped by project context.'
103
+ }
104
+ ],
105
+ default: 'single-tenant'
106
+ },
107
+
108
+ userModel: {
109
+ message: 'Who will use your application?',
110
+ choices: [
111
+ {
112
+ name: 'B2B (Business users only) - Organization employees manage the system',
113
+ value: 'b2b',
114
+ description: 'Only business users (your customers) access the system. No end-user portal.',
115
+ isDefault: true // Base template is B2B
116
+ },
117
+ {
118
+ name: 'B2B2C (Business + End customers) - Business users manage their customers',
119
+ value: 'b2b2c',
120
+ description: 'Business users manage their end customers (regular_user type). Adds customer relationships.'
121
+ }
122
+ ],
123
+ default: 'b2b'
124
+ }
125
+ }
126
+ },
127
+
128
+ // Admin portal inherits tenancy choice from backend
129
+ 'admin-portal': {
130
+ base: 'admin-portal/templates/base',
131
+ sectionsDir: 'admin-portal/templates/sections',
132
+ filesDir: 'admin-portal/templates/files',
133
+
134
+ variants: {
135
+ 'multi-tenant': {
136
+ // Complete files to copy (multi-tenant only components)
137
+ files: [
138
+ 'src/components/projects/ProjectsSelect.tsx', // Project selector dropdown
139
+ 'src/components/projects/NewProject.tsx', // Create project dialog
140
+ 'src/pages/FirstProject.tsx', // Onboarding page
141
+ 'src/api/projectRequests.ts', // Project API calls
142
+ 'src/components/ProjectDetailsOnAuthPage.tsx', // Recovery code flow
143
+ 'src/components/ProjectFrontendLink.tsx', // Project navigation link
144
+ 'src/components/settings/CustomDomain.tsx', // Custom domain settings (multi-tenant only)
145
+ 'src/components/settings/ProjectSettings.tsx' // Project settings (multi-tenant only)
146
+ ],
147
+
148
+ // Code sections to insert into base template files
149
+ sections: {
150
+ 'src/redux/user/userSlice.ts': [
151
+ 'TYPES_IMPORT', // Add Project type to imports
152
+ 'PROJECTS_STATE', // Add projects state to interface
153
+ 'PROJECTS_STATE_INITIAL', // Initialize projects in initial state
154
+ 'PROJECTS_INITIALIZATION', // Initialize projects on profile fetch
155
+ 'PROJECTS_REDUCERS', // Add setSelectedProject reducer
156
+ 'PROJECTS_EXPORTS', // Export setSelectedProject action
157
+ 'PROJECTS_SELECTOR' // Export selectSelectedProject selector
158
+ ],
159
+ 'src/api/client.ts': [
160
+ 'STORE_IMPORT', // Import Redux store
161
+ 'PROJECT_HEADER' // Add X-Project-Id header to requests
162
+ ],
163
+ 'src/components/Layout.tsx': [
164
+ 'PROJECT_STATE', // Extract project state from Redux
165
+ 'FIRST_PROJECT_CHECKS' // Add loading state for missing project
166
+ ],
167
+ 'src/components/ui/SaMenu.tsx': [
168
+ 'PROJECTS_SELECT_IMPORT', // Import ProjectsSelect component
169
+ 'PROJECTS_SELECT_COMPONENT' // Render ProjectsSelect in sidebar
170
+ ],
171
+ 'src/pages/auth/Login.tsx': [
172
+ 'ROUTER_IMPORTS', // Add useNavigate, useSearchParams to imports
173
+ 'PROJECT_DETAILS_IMPORT', // Import ProjectDetailsOnAuthPage
174
+ 'PROJECT_DETAILS_HANDLER', // Handler for project retrieval
175
+ 'PROJECT_DETAILS_COMPONENT' // Render ProjectDetailsOnAuthPage
176
+ ],
177
+ 'src/pages/auth/SignUp.tsx': [
178
+ 'REACT_IMPORTS', // Add useState to React imports
179
+ 'PROJECT_DETAILS_IMPORT', // Import ProjectDetailsOnAuthPage
180
+ 'PROJECT_DETAILS_STATE', // State for recovery code
181
+ 'PROJECT_DETAILS_COMPONENT', // Render ProjectDetailsOnAuthPage
182
+ 'RECOVERY_CODE_PROP_GOOGLE', // Pass recovery code to GoogleLogin
183
+ 'RECOVERY_CODE_PROP_FORM' // Pass recovery code to SignUpForm
184
+ ],
185
+ 'src/pages/Settings.tsx': [
186
+ 'MULTI_TENANT_SETTINGS_IMPORTS', // Import ProjectSettings and CustomDomain
187
+ 'MULTI_TENANT_SETTINGS_STATE', // Set default activeTab to 'project'
188
+ 'MULTI_TENANT_SETTINGS_TABS', // Add Project and Custom Domain tabs
189
+ 'MULTI_TENANT_SETTINGS_CONTENT' // Render ProjectSettings and CustomDomain
190
+ ],
191
+ 'src/App.tsx': [
192
+ 'SET_SELECTED_PROJECT_IMPORT', // Import setSelectedProject action
193
+ 'FIRST_PROJECT_IMPORT', // Import FirstProject component
194
+ 'FIRST_PROJECT_ROUTE_GUARD', // Add FirstProjectRoute guard
195
+ 'USER_ROUTE_VARIABLES', // Add user and location variables for onboarding
196
+ 'USER_ROUTE_ONBOARDING_CHECK', // Redirect to /first-project
197
+ 'PROJECT_QUERY_PARAM_HANDLER', // Handle ?project= query param
198
+ 'FIRST_PROJECT_ROUTE' // Add /first-project route
199
+ ],
200
+ 'src/types/index.ts': [
201
+ 'PROJECTS_FIELD' // Add projects field to User interface
202
+ ]
203
+ }
204
+ },
205
+
206
+ 'b2b2c': {
207
+ files: [
208
+ 'src/pages/Users.tsx' // Users page for managing end customers
209
+ ],
210
+
211
+ sections: {
212
+ 'src/components/Layout.tsx': [
213
+ 'B2B2C_USER_ICON_IMPORT', // Import UserIcon for Users menu
214
+ 'B2B2C_USERS_MENU' // Add Users menu item for customer management
215
+ ],
216
+ 'src/App.tsx': [
217
+ 'B2B2C_USERS_IMPORT', // Import Users page component
218
+ 'B2B2C_USERS_ROUTE' // Add /users route
219
+ ]
220
+ }
221
+ }
222
+ },
223
+
224
+ linkedTo: 'backend.tenancy'
225
+ },
226
+
227
+ // Customers portal (for B2B2C regular_user access)
228
+ 'customers-portal': {
229
+ base: 'customers-portal/templates/base',
230
+ sectionsDir: 'customers-portal/templates/sections',
231
+ filesDir: 'customers-portal/templates/files',
232
+
233
+ variants: {
234
+ 'multi-tenant': {
235
+ files: [],
236
+ sections: {}
237
+ }
238
+ },
239
+
240
+ linkedTo: 'backend.tenancy'
241
+ },
242
+
243
+ // Infrastructure (Docker Compose orchestration)
244
+ infrastructure: {
245
+ base: 'infrastructure',
246
+ sectionsDir: 'infrastructure/templates/sections',
247
+ filesDir: 'infrastructure/templates/files',
248
+
249
+ variants: {
250
+ // B2B2C variant: Adds customers-portal service to docker-compose files
251
+ 'b2b2c': {
252
+ files: [],
253
+
254
+ sections: {
255
+ 'docker-compose.yml': [
256
+ 'B2B2C_CUSTOMERS_PORTAL_SERVICE'
257
+ ],
258
+ 'docker-compose.dev.yml': [
259
+ 'B2B2C_CUSTOMERS_PORTAL_SERVICE',
260
+ 'B2B2C_CUSTOMERS_PORTAL_VOLUMES'
261
+ ],
262
+ 'docker-compose.prod.yml': [
263
+ 'B2B2C_CUSTOMERS_PORTAL_SERVICE'
264
+ ]
265
+ }
266
+ }
267
+ },
268
+
269
+ linkedTo: 'backend.userModel'
270
+ }
271
+ };
272
+
273
+ /**
274
+ * Get variant configuration for a service
275
+ * @param {string} serviceName - Service name (backend, admin-portal, etc.)
276
+ * @returns {object|null} Service variant configuration
277
+ */
278
+ function getVariantConfig(serviceName) {
279
+ return VARIANT_CONFIG[serviceName] || null;
280
+ }
281
+
282
+ /**
283
+ * Get variant prompts for user selection
284
+ * @returns {object} Prompts configuration
285
+ */
286
+ function getVariantPrompts() {
287
+ return VARIANT_CONFIG.backend.prompts;
288
+ }
289
+
290
+ /**
291
+ * Resolve variant choices for all services
292
+ * @param {object} backendChoices - User's choices for backend variants
293
+ * @returns {object} Variant choices for all services
294
+ */
295
+ function resolveVariantChoices(backendChoices) {
296
+ const choices = {
297
+ backend: backendChoices
298
+ };
299
+
300
+ // Resolve linked services
301
+ Object.keys(VARIANT_CONFIG).forEach(serviceName => {
302
+ if (serviceName === 'backend') return;
303
+
304
+ const serviceConfig = VARIANT_CONFIG[serviceName];
305
+ if (serviceConfig.linkedTo) {
306
+ // Parse linkedTo (e.g., "backend.tenancy")
307
+ const [linkedService, linkedDimension] = serviceConfig.linkedTo.split('.');
308
+ choices[serviceName] = {
309
+ [linkedDimension]: backendChoices[linkedDimension]
310
+ };
311
+ }
312
+ });
313
+
314
+ // Special case: admin-portal inherits BOTH tenancy and userModel
315
+ if (choices['admin-portal']) {
316
+ choices['admin-portal'].userModel = backendChoices.userModel;
317
+ }
318
+
319
+ return choices;
320
+ }
321
+
322
+ /**
323
+ * Get variants to apply (exclude defaults)
324
+ * @param {object} choices - User's variant selections
325
+ * @returns {string[]} List of variant names to apply
326
+ */
327
+ function getVariantsToApply(choices) {
328
+ const variantsToApply = [];
329
+
330
+ // Add multi-tenant if selected
331
+ if (choices.tenancy === 'multi-tenant') {
332
+ variantsToApply.push('multi-tenant');
333
+ }
334
+
335
+ // Add b2b2c if selected
336
+ if (choices.userModel === 'b2b2c') {
337
+ variantsToApply.push('b2b2c');
338
+ }
339
+
340
+ return variantsToApply;
341
+ }
342
+
343
+ module.exports = {
344
+ VARIANT_CONFIG,
345
+ getVariantConfig,
346
+ getVariantPrompts,
347
+ resolveVariantChoices,
348
+ getVariantsToApply
349
+ };
@@ -0,0 +1,237 @@
1
+ const { exec } = require('child_process');
2
+ const { promisify } = require('util');
3
+ const ora = require('ora');
4
+ const chalk = require('chalk');
5
+ const fs = require('fs-extra');
6
+ const path = require('path');
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ /**
11
+ * Check if Docker is running
12
+ * @returns {Promise<boolean>}
13
+ */
14
+ async function checkDockerRunning() {
15
+ try {
16
+ await execAsync('docker info', { timeout: 5000 });
17
+ return true;
18
+ } catch (error) {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Login to GitHub Container Registry
25
+ * @param {string} githubOrg - GitHub organization/username
26
+ * @param {string} ghcrToken - GitHub Personal Access Token
27
+ * @returns {Promise<void>}
28
+ */
29
+ async function loginToGHCR(githubOrg, ghcrToken) {
30
+ const spinner = ora('Logging in to GitHub Container Registry...').start();
31
+
32
+ try {
33
+ await execAsync(
34
+ `echo "${ghcrToken}" | docker login ghcr.io -u ${githubOrg} --password-stdin`,
35
+ { timeout: 30000 }
36
+ );
37
+ spinner.succeed('Logged in to GHCR');
38
+ } catch (error) {
39
+ spinner.fail('Failed to login to GHCR');
40
+ throw new Error(`GHCR login failed: ${error.message}`);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Build and push a Docker image
46
+ * @param {string} serviceName - Service name (e.g., 'backend', 'admin-frontend')
47
+ * @param {string} contextDir - Docker build context directory
48
+ * @param {string} registry - Registry URL (e.g., 'ghcr.io/myorg')
49
+ * @param {string} projectName - Project name
50
+ * @param {string[]} buildArgs - Array of build arguments (e.g., ['KEY=value'])
51
+ * @returns {Promise<void>}
52
+ */
53
+ async function buildAndPushImage(serviceName, contextDir, registry, projectName, buildArgs = []) {
54
+ const imageName = `${registry}/${projectName}-${serviceName}:latest`;
55
+
56
+ console.log(chalk.blue(`\n🐳 Building ${serviceName}...\n`));
57
+
58
+ const spinner = ora(`Building ${serviceName} image...`).start();
59
+
60
+ try {
61
+ // Build image
62
+ const buildArgsStr = buildArgs.map(arg => `--build-arg "${arg}"`).join(' ');
63
+ const buildCmd = `docker build --target production --tag ${imageName} ${buildArgsStr} ${contextDir}`;
64
+
65
+ await execAsync(buildCmd, {
66
+ timeout: 600000, // 10 minutes per service
67
+ maxBuffer: 10 * 1024 * 1024 // 10MB buffer for build output
68
+ });
69
+
70
+ spinner.text = `Pushing ${serviceName} to registry...`;
71
+
72
+ // Push image
73
+ await execAsync(`docker push ${imageName}`, {
74
+ timeout: 600000 // 10 minutes for push
75
+ });
76
+
77
+ spinner.succeed(`${serviceName} built and pushed successfully`);
78
+ } catch (error) {
79
+ spinner.fail(`Failed to build ${serviceName}`);
80
+ throw new Error(`Build failed for ${serviceName}: ${error.message}`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Load environment variables from .env.prod
86
+ * @param {string} envFilePath - Path to .env.prod file
87
+ * @returns {Object} Environment variables as key-value pairs
88
+ */
89
+ function loadEnvFile(envFilePath) {
90
+ const envContent = fs.readFileSync(envFilePath, 'utf8');
91
+ const envVars = {};
92
+
93
+ envContent.split('\n').forEach(line => {
94
+ const trimmed = line.trim();
95
+ if (!trimmed || trimmed.startsWith('#')) return;
96
+
97
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
98
+ if (match) {
99
+ const [, key, value] = match;
100
+ // Remove surrounding quotes if present
101
+ envVars[key] = value.replace(/^["']|["']$/g, '');
102
+ }
103
+ });
104
+
105
+ return envVars;
106
+ }
107
+
108
+ /**
109
+ * Build all production images for full app
110
+ * @param {string} projectRoot - Project root directory
111
+ * @param {string} projectName - Project name
112
+ * @param {string} githubOrg - GitHub organization/username
113
+ * @param {string} envFilePath - Path to .env.prod file
114
+ * @returns {Promise<void>}
115
+ */
116
+ async function buildFullAppImages(projectRoot, projectName, githubOrg, envFilePath) {
117
+ const registry = `ghcr.io/${githubOrg}`;
118
+ const envVars = loadEnvFile(envFilePath);
119
+
120
+ console.log(chalk.yellow('\n📦 Building production Docker images...\n'));
121
+ console.log(chalk.gray('This may take 10-20 minutes depending on your system.\n'));
122
+
123
+ // Build backend
124
+ await buildAndPushImage(
125
+ 'backend',
126
+ path.join(projectRoot, 'backend'),
127
+ registry,
128
+ projectName
129
+ );
130
+
131
+ // Build admin-frontend
132
+ await buildAndPushImage(
133
+ 'admin-frontend',
134
+ path.join(projectRoot, 'admin-frontend'),
135
+ registry,
136
+ projectName
137
+ );
138
+
139
+ // Build customers-frontend
140
+ await buildAndPushImage(
141
+ 'customers-frontend',
142
+ path.join(projectRoot, 'frontend'),
143
+ registry,
144
+ projectName
145
+ );
146
+
147
+ // Build website (requires build args)
148
+ const websiteBuildArgs = [
149
+ `APP_NAME=${envVars.APP_NAME || ''}`,
150
+ `DOCS_URL=${envVars.DOCS_URL || ''}`,
151
+ `CONTACT_EMAIL=${envVars.CONTACT_EMAIL || ''}`,
152
+ `CTA_LINK=${envVars.CTA_LINK || ''}`,
153
+ `LIVE_DEMO_URL=${envVars.LIVE_DEMO_URL || ''}`,
154
+ `MIXPANEL_PROJECT_TOKEN=${envVars.MIXPANEL_PROJECT_TOKEN || ''}`,
155
+ `GOOGLE_ANALYTICS_ID=${envVars.GOOGLE_ANALYTICS_ID || ''}`
156
+ ];
157
+
158
+ await buildAndPushImage(
159
+ 'website',
160
+ path.join(projectRoot, 'website'),
161
+ registry,
162
+ projectName,
163
+ websiteBuildArgs
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Build waitlist image
169
+ * @param {string} projectRoot - Project root directory
170
+ * @param {string} projectName - Project name (unused for waitlist, kept for API compatibility)
171
+ * @param {string} githubOrg - GitHub organization/username
172
+ * @returns {Promise<void>}
173
+ */
174
+ async function buildWaitlistImage(projectRoot, projectName, githubOrg) {
175
+ const registry = `ghcr.io/${githubOrg}`;
176
+ const waitlistPath = path.join(projectRoot, 'waitlist');
177
+ const imageName = `${registry}/waitlist:latest`;
178
+
179
+ console.log(chalk.yellow('\n📦 Building waitlist Docker image...\n'));
180
+ console.log(chalk.gray(`Project root: ${projectRoot}`));
181
+ console.log(chalk.gray(`Waitlist path: ${waitlistPath}\n`));
182
+
183
+ // Load environment variables from waitlist .env.prod file (for production build)
184
+ const waitlistEnvProdPath = path.join(waitlistPath, '.env.prod');
185
+ const waitlistEnvPath = path.join(waitlistPath, '.env');
186
+
187
+ // Prefer .env.prod for deployment, fallback to .env
188
+ const envFilePath = fs.existsSync(waitlistEnvProdPath) ? waitlistEnvProdPath : waitlistEnvPath;
189
+ let buildArgs = [];
190
+
191
+ if (fs.existsSync(envFilePath)) {
192
+ const envVars = loadEnvFile(envFilePath);
193
+ buildArgs = [
194
+ `AIRTABLE_PERSONAL_ACCESS_TOKEN=${envVars.AIRTABLE_PERSONAL_ACCESS_TOKEN || ''}`,
195
+ `AIRTABLE_BASE_ID=${envVars.AIRTABLE_BASE_ID || ''}`,
196
+ `AIRTABLE_TABLE_NAME=${envVars.AIRTABLE_TABLE_NAME || 'Waitlist'}`,
197
+ `NEXT_PUBLIC_PROJECT_NAME=${envVars.NEXT_PUBLIC_PROJECT_NAME || envVars.PROJECT_NAME || ''}`,
198
+ `NEXT_PUBLIC_SITE_URL=${envVars.NEXT_PUBLIC_SITE_URL || ''}`,
199
+ `PROJECT_NAME=${envVars.PROJECT_NAME || ''}`,
200
+ `PRIMARY_DOMAIN=${envVars.PRIMARY_DOMAIN || ''}`
201
+ ];
202
+ }
203
+
204
+ console.log(chalk.blue(`\n🐳 Building waitlist...\n`));
205
+
206
+ const spinner = ora(`Building waitlist image...`).start();
207
+
208
+ try {
209
+ // Build image
210
+ const buildArgsStr = buildArgs.map(arg => `--build-arg "${arg}"`).join(' ');
211
+ const buildCmd = `docker build --tag ${imageName} ${buildArgsStr} ${waitlistPath}`;
212
+
213
+ await execAsync(buildCmd, {
214
+ timeout: 600000, // 10 minutes
215
+ maxBuffer: 10 * 1024 * 1024 // 10MB buffer for build output
216
+ });
217
+
218
+ spinner.text = `Pushing waitlist to registry...`;
219
+
220
+ // Push image
221
+ await execAsync(`docker push ${imageName}`, {
222
+ timeout: 600000 // 10 minutes for push
223
+ });
224
+
225
+ spinner.succeed(`waitlist built and pushed successfully`);
226
+ } catch (error) {
227
+ spinner.fail(`Failed to build waitlist`);
228
+ throw new Error(`Build failed for waitlist: ${error.message}`);
229
+ }
230
+ }
231
+
232
+ module.exports = {
233
+ checkDockerRunning,
234
+ loginToGHCR,
235
+ buildFullAppImages,
236
+ buildWaitlistImage
237
+ };
@@ -0,0 +1,88 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * Generate a secure random string
7
+ * @param {number} length - Length of the string
8
+ * @returns {string} Random hex string
9
+ */
10
+ function generateSecret(length = 32) {
11
+ return crypto.randomBytes(length).toString('hex');
12
+ }
13
+
14
+ /**
15
+ * Generate environment file from .env.example
16
+ * @param {string} projectRoot - Root directory of the generated project
17
+ * @param {Object} answers - User answers from prompts
18
+ */
19
+ async function generateEnvFile(projectRoot, answers) {
20
+ const envExamplePath = path.join(projectRoot, 'infrastructure', '.env.example');
21
+ const envPath = path.join(projectRoot, 'infrastructure', '.env');
22
+
23
+ // Read .env.example
24
+ const envTemplate = await fs.readFile(envExamplePath, 'utf8');
25
+
26
+ // Generate secure secrets
27
+ const secrets = {
28
+ JWT_SECRET: generateSecret(32),
29
+ DB_PASSWORD: generateSecret(24),
30
+ BULL_ADMIN_TOKEN: generateSecret(24)
31
+ };
32
+
33
+ // Create variable mappings
34
+ const variables = {
35
+ '{{PROJECT_NAME}}': answers.projectName,
36
+ '{{PROJECT_NAME_UPPER}}': answers.projectNameUpper,
37
+ '{{PRIMARY_DOMAIN}}': answers.primaryDomain,
38
+ '{{ADMIN_EMAIL}}': answers.adminEmail,
39
+
40
+ // Replace placeholder passwords with generated secrets
41
+ 'your_secure_postgres_password': secrets.DB_PASSWORD,
42
+ 'your_jwt_secret_key_change_this_in_production': secrets.JWT_SECRET,
43
+ 'your_bull_admin_token': secrets.BULL_ADMIN_TOKEN
44
+ };
45
+
46
+ // Replace variables in template
47
+ let envContent = envTemplate;
48
+ for (const [placeholder, value] of Object.entries(variables)) {
49
+ envContent = envContent.split(placeholder).join(value);
50
+ }
51
+
52
+ // Write .env file
53
+ await fs.writeFile(envPath, envContent, 'utf8');
54
+
55
+ return {
56
+ envPath,
57
+ secrets
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Update environment file with component-specific variables
63
+ * @param {string} envPath - Path to .env file
64
+ * @param {string} componentName - Name of the component
65
+ * @param {Object} envVarSchema - Schema of env vars (key: description)
66
+ * @param {Object} values - Actual values for the env vars
67
+ */
68
+ async function updateEnvFile(envPath, componentName, envVarSchema, values) {
69
+ // Read existing .env file
70
+ let envContent = await fs.readFile(envPath, 'utf8');
71
+
72
+ // Add section header for component
73
+ envContent += `\n\n# ${componentName.charAt(0).toUpperCase() + componentName.slice(1)} Component\n`;
74
+
75
+ // Add each env var
76
+ for (const [key, description] of Object.entries(envVarSchema)) {
77
+ envContent += `${key}=${values[key]}\n`;
78
+ }
79
+
80
+ // Write back to file
81
+ await fs.writeFile(envPath, envContent, 'utf8');
82
+ }
83
+
84
+ module.exports = {
85
+ generateEnvFile,
86
+ generateSecret,
87
+ updateEnvFile
88
+ };