@launchframe/cli 0.1.11 → 1.0.0-beta.2

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/src/generator.js CHANGED
@@ -28,13 +28,14 @@ function initGitRepo(servicePath, serviceName) {
28
28
  * Main project generation function
29
29
  * @param {Object} answers - User answers from prompts
30
30
  * @param {Object} variantChoices - User's variant selections (tenancy, userModel)
31
+ * @param {string} templateRoot - Path to templates (cache or local dev path)
31
32
  */
32
- async function generateProject(answers, variantChoices) {
33
+ async function generateProject(answers, variantChoices, templateRoot) {
33
34
  const { projectName } = answers;
34
35
 
35
36
  // Define source (template) and destination paths
36
- const templateRoot = path.resolve(__dirname, '../../modules');
37
- const projectRoot = path.resolve(__dirname, '../..'); // For root-level files like .github, CLAUDE.md
37
+ // templateRoot is now passed as parameter (cache or local dev path)
38
+ const projectRoot = path.resolve(__dirname, '../..'); // For root-level files like .github, README.md
38
39
  const destinationRoot = path.resolve(process.cwd(), projectName);
39
40
 
40
41
  console.log(`šŸ“ Template source: ${templateRoot}`);
@@ -48,6 +49,7 @@ async function generateProject(answers, variantChoices) {
48
49
  '{{PROJECT_NAME}}': answers.projectName,
49
50
  '{{PROJECT_NAME_UPPER}}': answers.projectNameUpper,
50
51
  '{{PROJECT_DISPLAY_NAME}}': answers.projectDisplayName,
52
+ '{{PROJECT_DESCRIPTION}}': answers.projectDescription,
51
53
  // Leave these as template variables for deploy:configure to replace
52
54
  '{{GITHUB_ORG}}': '{{GITHUB_ORG}}',
53
55
  '{{PRIMARY_DOMAIN}}': '{{PRIMARY_DOMAIN}}',
@@ -144,7 +146,6 @@ async function generateProject(answers, variantChoices) {
144
146
  console.log('šŸ“‹ Copying additional files...');
145
147
  const additionalFiles = [
146
148
  '.github',
147
- 'CLAUDE.md',
148
149
  'README.md',
149
150
  '.gitignore',
150
151
  'LICENSE'
@@ -173,12 +174,20 @@ async function generateProject(answers, variantChoices) {
173
174
  // Step 7: Create .launchframe marker file with variant metadata
174
175
  console.log('šŸ“ Creating LaunchFrame marker file...');
175
176
  const markerPath = path.join(destinationRoot, '.launchframe');
177
+
178
+ // Determine which services were installed
179
+ const installedServices = ['backend', 'admin-portal', 'infrastructure', 'website'];
180
+ if (variantChoices.userModel === 'b2b2c') {
181
+ installedServices.push('customers-portal');
182
+ }
183
+
176
184
  const markerContent = {
177
185
  version: '0.1.0',
178
186
  createdAt: new Date().toISOString(),
179
187
  projectName: answers.projectName,
180
188
  projectDisplayName: answers.projectDisplayName,
181
189
  deployConfigured: false,
190
+ installedServices: installedServices,
182
191
  // Store variant choices for future reference
183
192
  variants: variantChoices
184
193
  };
package/src/index.js CHANGED
@@ -25,6 +25,7 @@ const {
25
25
  serviceList,
26
26
  serviceRemove
27
27
  } = require('./commands/service');
28
+ const { cacheClear, cacheInfo, cacheUpdate } = require('./commands/cache');
28
29
 
29
30
  // Get command and arguments
30
31
  const command = process.argv[2];
@@ -80,7 +81,11 @@ async function main() {
80
81
  // Route commands
81
82
  switch (command) {
82
83
  case 'init':
83
- await init({ projectName: flags['project-name'] });
84
+ await init({
85
+ projectName: flags['project-name'],
86
+ tenancy: flags['tenancy'],
87
+ userModel: flags['user-model']
88
+ });
84
89
  break;
85
90
  case 'deploy:configure':
86
91
  await deployConfigure();
@@ -98,7 +103,7 @@ async function main() {
98
103
  await waitlistDeploy();
99
104
  break;
100
105
  case 'waitlist:up':
101
- await waitlistUp();
106
+ await waitlistUp(flags);
102
107
  break;
103
108
  case 'waitlist:down':
104
109
  await waitlistDown();
@@ -143,6 +148,15 @@ async function main() {
143
148
  }
144
149
  await serviceRemove(args[1]);
145
150
  break;
151
+ case 'cache:clear':
152
+ await cacheClear();
153
+ break;
154
+ case 'cache:info':
155
+ await cacheInfo();
156
+ break;
157
+ case 'cache:update':
158
+ await cacheUpdate();
159
+ break;
146
160
  case 'help':
147
161
  case '--help':
148
162
  case '-h':
package/src/prompts.js CHANGED
@@ -35,6 +35,18 @@ async function runInitPrompts() {
35
35
  }
36
36
  return 'Project display name is required';
37
37
  }
38
+ },
39
+ {
40
+ type: 'input',
41
+ name: 'projectDescription',
42
+ message: 'Project description (for meta tags):',
43
+ default: (answers) => `${answers.projectDisplayName} - Modern SaaS Platform`,
44
+ validate: (input) => {
45
+ if (input.trim().length > 0) {
46
+ return true;
47
+ }
48
+ return 'Project description is required';
49
+ }
38
50
  }
39
51
  ]);
40
52
 
@@ -11,7 +11,9 @@ const SERVICE_REGISTRY = {
11
11
  envVars: {
12
12
  AIRTABLE_PERSONAL_ACCESS_TOKEN: 'Your Airtable personal access token (create at https://airtable.com/create/tokens)',
13
13
  AIRTABLE_BASE_ID: 'Your Airtable base ID (e.g., appHhPeD0hVeiE7dS - found in your base URL: airtable.com/appXXXXXX/...)',
14
- AIRTABLE_TABLE_NAME: 'Your Airtable table name (e.g., "Waitlist Signups" - the exact name of your table, not the table ID)'
14
+ AIRTABLE_TABLE_NAME: 'Your Airtable table name (e.g., "Waitlist Signups" - the exact name of your table, not the table ID)',
15
+ NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: '[OPTIONAL] Your Mixpanel project token (find at mixpanel.com/project/[project-id]/settings - leave blank to disable analytics)',
16
+ NEXT_PUBLIC_MIXPANEL_DATA_RESIDENCY: '[OPTIONAL] Mixpanel data residency (US or EU - leave blank for US default)'
15
17
  },
16
18
  installPath: 'waitlist',
17
19
  devPort: 3002,
@@ -30,16 +32,16 @@ const SERVICE_REGISTRY = {
30
32
  devPort: 5173,
31
33
  version: '1.0.0'
32
34
  },
33
- 'customers-frontend': {
34
- name: 'customers-frontend',
35
- displayName: 'Customer Frontend',
35
+ 'customers-portal': {
36
+ name: 'customers-portal',
37
+ displayName: 'Customers Portal',
36
38
  description: 'Customer-facing portal for B2B2C (React + Vite + Zustand)',
37
- repoUrl: 'https://github.com/{{GITHUB_ORG}}/launchframe-customers-frontend',
39
+ repoUrl: 'https://github.com/{{GITHUB_ORG}}/launchframe-customers-portal',
38
40
  techStack: 'React + Vite + Zustand + TanStack Query',
39
41
  dependencies: ['backend'],
40
42
  standalone: false, // Integrated into main infrastructure/
41
43
  envVars: {}, // Uses standard env vars from infrastructure/.env
42
- installPath: 'customers-frontend',
44
+ installPath: 'customers-portal',
43
45
  devPort: 3000,
44
46
  version: '1.0.0'
45
47
  }
@@ -21,14 +21,14 @@ const VARIANT_CONFIG = {
21
21
  // Complete files/folders to copy
22
22
  files: [
23
23
  'src/modules/domain/projects', // Entire projects module
24
- 'src/modules/domain/jwt-auth', // JWT auth module (project-specific JWT issuance)
25
24
  'src/modules/domain/ai/services/project-config.service.ts', // Project config service
26
- 'src/guards/project-ownership.guard.ts', // Project ownership guard
25
+ 'src/guards/project-ownership.guard.ts', // Project ownership guard (header-based)
26
+ 'src/guards/project-param.guard.ts', // Project param guard (route-based)
27
27
  'src/modules/auth/auth.service.ts', // Auth service with multi-tenant support
28
28
  'src/modules/auth/auth.controller.ts', // Auth controller with multi-tenant support
29
29
  'src/modules/users/users.service.ts', // Users service with multi-tenant support
30
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
31
+ 'src/modules/users/create-user.dto.ts' // CreateUserDto with businessId
32
32
  ],
33
33
 
34
34
  // Code sections to insert into base template files
@@ -41,9 +41,7 @@ const VARIANT_CONFIG = {
41
41
  ],
42
42
  'src/modules/app/app.module.ts': [
43
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
44
+ 'PROJECTS_MODULE' // Add ProjectsModule to imports array
47
45
  ],
48
46
  'src/modules/auth/auth.module.ts': [
49
47
  'MULTI_TENANT_IMPORTS', // Add Project entity import
@@ -55,7 +53,6 @@ const VARIANT_CONFIG = {
55
53
  ],
56
54
  'src/modules/users/users.module.ts': [
57
55
  'MULTI_TENANT_IMPORTS', // Add Projects-related imports
58
- 'MULTI_TENANT_SERVICE_IMPORTS', // Add JWT auth service imports
59
56
  'MULTI_TENANT_ENTITIES', // Add Project entities to TypeORM
60
57
  'MULTI_TENANT_MODULE_IMPORTS', // Add ProjectsModule to imports
61
58
  'MULTI_TENANT_PROVIDERS' // Add multi-tenant providers
@@ -63,11 +60,11 @@ const VARIANT_CONFIG = {
63
60
  }
64
61
  },
65
62
 
66
- // B2B2C variant: Adds regular_user support
63
+ // B2B2C variant: Adds regular_user support (for single-tenant only)
67
64
  'b2b2c': {
68
65
  // Complete files to copy
69
66
  files: [
70
- 'src/modules/users/user-business.entity.ts' // Business-to-user linking entity
67
+ 'src/modules/users/user-business.entity.ts', // Business-to-user linking entity
71
68
  ],
72
69
 
73
70
  // Code sections to insert
@@ -82,6 +79,62 @@ const VARIANT_CONFIG = {
82
79
  'B2B2C_ENTITIES' // Add UserBusiness to TypeORM
83
80
  ]
84
81
  }
82
+ },
83
+
84
+ // B2B2C + Single-tenant combination variant (NEW)
85
+ 'b2b2c_single-tenant': {
86
+ // Complete files to copy (B2B2C features without multi-tenant projects)
87
+ files: [
88
+ 'src/guards/business-scoping.guard.ts', // Business scoping guard
89
+ 'src/modules/domain/business/business.controller.ts', // Business lookup controller
90
+ 'src/modules/domain/business/business.service.ts', // Business lookup service
91
+ 'src/modules/domain/business/business.module.ts', // Business module
92
+ 'src/modules/domain/business/entities/business.entity.ts', // Business entity
93
+ 'src/modules/domain/business/dto/business-response.dto.ts', // Business response DTO
94
+ 'src/modules/domain/business/dto/create-business.dto.ts', // Business create DTO
95
+ 'src/database/migrations/1766688416362-CreateBusinessesTable.ts', // Businesses table migration
96
+ 'src/modules/domain/items/items.controller.ts', // Items with business scoping
97
+ 'src/modules/domain/items/items.service.ts', // Items service with business scoping
98
+ 'src/modules/domain/items/item.entity.ts', // Item entity with businessId
99
+ 'src/modules/domain/items/dto/create-item.dto.ts', // Create item DTO
100
+ 'src/modules/domain/items/dto/update-item.dto.ts', // Update item DTO
101
+ 'src/modules/auth/auth.module.ts', // Auth module with Business entity
102
+ 'src/modules/auth/auth.controller.ts', // Auth controller with magic-link (B2B2C)
103
+ 'src/modules/auth/auth.service.ts', // Auth service with magic-link (B2B2C)
104
+ 'src/modules/auth/jwt-auth.guard.ts', // JWT authentication guard
105
+ 'src/modules/users/user-business.entity.ts', // Business-to-user linking entity
106
+ 'src/modules/users/users.module.ts', // Users module with Business entity
107
+ 'src/modules/users/users.controller.ts', // Users controller (B2B2C)
108
+ 'src/modules/users/users.service.ts', // Users service (B2B2C)
109
+ 'src/modules/users/create-user.dto.ts' // CreateUserDto with businessId
110
+ ],
111
+
112
+ sections: {
113
+ 'src/modules/app/app.module.ts': [
114
+ 'BUSINESS_MODULE_IMPORT', // Import BusinessModule
115
+ 'BUSINESS_MODULE' // Add BusinessModule to imports
116
+ ],
117
+ 'src/modules/users/user.entity.ts': [
118
+ 'BUSINESS_RELATIONSHIP_IMPORT', // Import Business entity
119
+ 'BUSINESS_RELATIONSHIP' // Add business relationship
120
+ ]
121
+ }
122
+ },
123
+
124
+ // B2B2C + Multi-tenant combination variant (RENAMED from multi-tenant_b2b2c)
125
+ 'b2b2c_multi-tenant': {
126
+ // Complete files to copy (has both multi-tenant and B2B2C features)
127
+ files: [
128
+ 'src/modules/users/user-business.entity.ts', // Business-to-user linking entity
129
+ 'src/modules/auth/auth.service.ts', // Combined auth service
130
+ 'src/modules/auth/auth.controller.ts', // Combined auth controller
131
+ 'src/modules/users/users.service.ts', // Combined users service
132
+ 'src/modules/users/users.controller.ts', // Combined users controller
133
+ 'src/modules/domain/projects/projects.module.ts' // Projects module with UserBusiness
134
+ ],
135
+
136
+ // No sections needed - complete files already have all features
137
+ sections: {}
85
138
  }
86
139
  },
87
140
 
@@ -139,10 +192,8 @@ const VARIANT_CONFIG = {
139
192
  'src/components/projects/NewProject.tsx', // Create project dialog
140
193
  'src/pages/FirstProject.tsx', // Onboarding page
141
194
  'src/api/projectRequests.ts', // Project API calls
142
- 'src/components/ProjectDetailsOnAuthPage.tsx', // Recovery code flow
143
195
  '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)
196
+ 'src/components/settings/CustomDomain.tsx' // Custom domain settings (multi-tenant only)
146
197
  ],
147
198
 
148
199
  // Code sections to insert into base template files
@@ -169,24 +220,15 @@ const VARIANT_CONFIG = {
169
220
  'PROJECTS_SELECT_COMPONENT' // Render ProjectsSelect in sidebar
170
221
  ],
171
222
  '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
223
+ 'ROUTER_IMPORTS' // Add useNavigate, useSearchParams to imports
176
224
  ],
177
225
  '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
226
+ 'REACT_IMPORTS' // Add useState to React imports
184
227
  ],
185
228
  '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
229
+ 'MULTI_TENANT_SETTINGS_IMPORTS', // Import CustomDomain
230
+ 'MULTI_TENANT_SETTINGS_TABS', // Add Custom Domain tab
231
+ 'MULTI_TENANT_SETTINGS_CONTENT' // Render CustomDomain
190
232
  ],
191
233
  'src/App.tsx': [
192
234
  'SET_SELECTED_PROJECT_IMPORT', // Import setSelectedProject action
@@ -218,26 +260,63 @@ const VARIANT_CONFIG = {
218
260
  'B2B2C_USERS_ROUTE' // Add /users route
219
261
  ]
220
262
  }
263
+ },
264
+
265
+ 'b2b2c_single-tenant': {
266
+ files: [
267
+ 'src/api/businessRequests.ts', // Business API requests
268
+ 'src/pages/Business.tsx' // Business onboarding page
269
+ ],
270
+
271
+ sections: {
272
+ 'src/App.tsx': [
273
+ 'BUSINESS_IMPORT', // Import Business component
274
+ 'USER_ROUTE_VARIABLES', // Add user and location variables
275
+ 'USER_ROUTE_ONBOARDING_CHECK', // Business onboarding redirect logic
276
+ 'BUSINESS_ROUTE' // Add /business route
277
+ ]
278
+ }
221
279
  }
222
280
  },
223
281
 
224
282
  linkedTo: 'backend.tenancy'
225
283
  },
226
284
 
227
- // Customers portal (for B2B2C regular_user access)
285
+ // Customers portal (B2B2C only - no pure B2B use case)
228
286
  'customers-portal': {
229
- base: 'customers-portal/templates/base',
287
+ base: 'customers-portal/templates/base', // B2B2C + Single-tenant base
230
288
  sectionsDir: 'customers-portal/templates/sections',
231
289
  filesDir: 'customers-portal/templates/files',
232
290
 
233
291
  variants: {
234
- 'multi-tenant': {
292
+ 'single-tenant': {
293
+ // Single-tenant uses section overrides for business endpoints
235
294
  files: [],
295
+ sections: {
296
+ 'src/api/tenantContext.ts': [
297
+ 'TENANT_CONFIG' // Override tenant config for business endpoints
298
+ ]
299
+ }
300
+ },
301
+
302
+ 'multi-tenant': {
303
+ // Complete file replacement for multi-tenant variant
304
+ files: [
305
+ 'src/App.tsx', // Project resolution logic
306
+ 'src/api/config.ts', // x-project-id header
307
+ 'src/api/types.ts', // Project type
308
+ 'src/api/projectRequests.ts', // Project API calls
309
+ 'src/api/hooks/useProject.ts', // Project hook
310
+ 'src/store/useProjectStore.ts' // Project state
311
+ ],
236
312
  sections: {}
237
313
  }
238
314
  },
239
315
 
240
- linkedTo: 'backend.tenancy'
316
+ linkedTo: 'backend.tenancy',
317
+
318
+ // Only deploy customers-portal if B2B2C is selected
319
+ shouldInclude: (choices) => choices.userModel === 'b2b2c'
241
320
  },
242
321
 
243
322
  // Infrastructure (Docker Compose orchestration)
@@ -316,6 +395,11 @@ function resolveVariantChoices(backendChoices) {
316
395
  choices['admin-portal'].userModel = backendChoices.userModel;
317
396
  }
318
397
 
398
+ // Special case: infrastructure needs BOTH tenancy and userModel for proper variant resolution
399
+ if (choices['infrastructure']) {
400
+ choices['infrastructure'].tenancy = backendChoices.tenancy;
401
+ }
402
+
319
403
  return choices;
320
404
  }
321
405
 
@@ -327,15 +411,29 @@ function resolveVariantChoices(backendChoices) {
327
411
  function getVariantsToApply(choices) {
328
412
  const variantsToApply = [];
329
413
 
330
- // Add multi-tenant if selected
331
- if (choices.tenancy === 'multi-tenant') {
414
+ const isMultiTenant = choices.tenancy === 'multi-tenant';
415
+ const isSingleTenant = choices.tenancy === 'single-tenant';
416
+ const isB2B2C = choices.userModel === 'b2b2c';
417
+ const isB2B = choices.userModel === 'b2b';
418
+
419
+ // Handle variant combinations
420
+ if (isMultiTenant && isB2B2C) {
421
+ // B2B2C + Multi-tenant: Apply both variants then combo
422
+ variantsToApply.push('multi-tenant'); // Apply multi-tenant first (for projects module, etc.)
423
+ variantsToApply.push('b2b2c'); // Apply B2B2C sections (REGULAR_USER, userBusinesses)
424
+ variantsToApply.push('b2b2c_multi-tenant'); // Then apply combo files (overwrites with combined versions)
425
+ } else if (isSingleTenant && isB2B2C) {
426
+ // B2B2C + Single-tenant: Apply B2B2C then single-tenant combo
427
+ variantsToApply.push('b2b2c'); // Apply B2B2C sections first
428
+ variantsToApply.push('b2b2c_single-tenant'); // Then apply single-tenant combo
429
+ } else if (isMultiTenant && (isB2B || !choices.userModel)) {
430
+ // B2B + Multi-tenant: Apply only multi-tenant
332
431
  variantsToApply.push('multi-tenant');
432
+ } else if (isSingleTenant && !choices.userModel) {
433
+ // Single-tenant only (e.g., customers-portal): Apply single-tenant variant
434
+ variantsToApply.push('single-tenant');
333
435
  }
334
-
335
- // Add b2b2c if selected
336
- if (choices.userModel === 'b2b2c') {
337
- variantsToApply.push('b2b2c');
338
- }
436
+ // else: B2B + Single-tenant (base template, no variants)
339
437
 
340
438
  return variantsToApply;
341
439
  }
@@ -43,7 +43,7 @@ async function loginToGHCR(githubOrg, ghcrToken) {
43
43
 
44
44
  /**
45
45
  * Build and push a Docker image
46
- * @param {string} serviceName - Service name (e.g., 'backend', 'admin-frontend')
46
+ * @param {string} serviceName - Service name (e.g., 'backend', 'admin-portal')
47
47
  * @param {string} contextDir - Docker build context directory
48
48
  * @param {string} registry - Registry URL (e.g., 'ghcr.io/myorg')
49
49
  * @param {string} projectName - Project name
@@ -111,57 +111,77 @@ function loadEnvFile(envFilePath) {
111
111
  * @param {string} projectName - Project name
112
112
  * @param {string} githubOrg - GitHub organization/username
113
113
  * @param {string} envFilePath - Path to .env.prod file
114
+ * @param {string[]} installedServices - List of installed services from .launchframe
114
115
  * @returns {Promise<void>}
115
116
  */
116
- async function buildFullAppImages(projectRoot, projectName, githubOrg, envFilePath) {
117
+ async function buildFullAppImages(projectRoot, projectName, githubOrg, envFilePath, installedServices = []) {
117
118
  const registry = `ghcr.io/${githubOrg}`;
118
119
  const envVars = loadEnvFile(envFilePath);
119
120
 
120
121
  console.log(chalk.yellow('\nšŸ“¦ Building production Docker images...\n'));
121
122
  console.log(chalk.gray('This may take 10-20 minutes depending on your system.\n'));
123
+ console.log(chalk.gray(`Services to build: ${installedServices.join(', ')}\n`));
124
+
125
+ // Build backend (always required)
126
+ if (installedServices.includes('backend')) {
127
+ await buildAndPushImage(
128
+ 'backend',
129
+ path.join(projectRoot, 'backend'),
130
+ registry,
131
+ projectName
132
+ );
133
+ }
134
+
135
+ // Build admin-portal (always required)
136
+ if (installedServices.includes('admin-portal')) {
137
+ await buildAndPushImage(
138
+ 'admin-portal',
139
+ path.join(projectRoot, 'admin-portal'),
140
+ registry,
141
+ projectName
142
+ );
143
+ }
144
+
145
+ // Build customers-portal (only if installed - B2B2C mode)
146
+ if (installedServices.includes('customers-portal')) {
147
+ await buildAndPushImage(
148
+ 'customers-portal',
149
+ path.join(projectRoot, 'customers-portal'),
150
+ registry,
151
+ projectName
152
+ );
153
+ }
122
154
 
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
- );
155
+ // Build website (always required)
156
+ if (installedServices.includes('website')) {
157
+ const websiteBuildArgs = [
158
+ `APP_NAME=${envVars.APP_NAME || ''}`,
159
+ `DOCS_URL=${envVars.DOCS_URL || ''}`,
160
+ `CONTACT_EMAIL=${envVars.CONTACT_EMAIL || ''}`,
161
+ `CTA_LINK=${envVars.CTA_LINK || ''}`,
162
+ `LIVE_DEMO_URL=${envVars.LIVE_DEMO_URL || ''}`,
163
+ `MIXPANEL_PROJECT_TOKEN=${envVars.MIXPANEL_PROJECT_TOKEN || ''}`,
164
+ `GOOGLE_ANALYTICS_ID=${envVars.GOOGLE_ANALYTICS_ID || ''}`
165
+ ];
166
+
167
+ await buildAndPushImage(
168
+ 'website',
169
+ path.join(projectRoot, 'website'),
170
+ registry,
171
+ projectName,
172
+ websiteBuildArgs
173
+ );
174
+ }
175
+
176
+ // Build docs (optional service - VitePress documentation)
177
+ if (installedServices.includes('docs')) {
178
+ await buildAndPushImage(
179
+ 'docs',
180
+ path.join(projectRoot, 'docs'),
181
+ registry,
182
+ projectName
183
+ );
184
+ }
165
185
  }
166
186
 
167
187
  /**
@@ -196,6 +216,8 @@ async function buildWaitlistImage(projectRoot, projectName, githubOrg) {
196
216
  `AIRTABLE_TABLE_NAME=${envVars.AIRTABLE_TABLE_NAME || 'Waitlist'}`,
197
217
  `NEXT_PUBLIC_PROJECT_NAME=${envVars.NEXT_PUBLIC_PROJECT_NAME || envVars.PROJECT_NAME || ''}`,
198
218
  `NEXT_PUBLIC_SITE_URL=${envVars.NEXT_PUBLIC_SITE_URL || ''}`,
219
+ `NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=${envVars.NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN || ''}`,
220
+ `NEXT_PUBLIC_MIXPANEL_DATA_RESIDENCY=${envVars.NEXT_PUBLIC_MIXPANEL_DATA_RESIDENCY || ''}`,
199
221
  `PROJECT_NAME=${envVars.PROJECT_NAME || ''}`,
200
222
  `PRIMARY_DOMAIN=${envVars.PRIMARY_DOMAIN || ''}`
201
223
  ];
@@ -0,0 +1,67 @@
1
+ const { execSync } = require('child_process');
2
+ const chalk = require('chalk');
3
+
4
+ /**
5
+ * Create a clickable terminal link using OSC 8 escape sequence
6
+ * Works in modern terminals (iTerm2, Hyper, Windows Terminal, VS Code, etc.)
7
+ * Falls back gracefully to plain text in unsupported terminals
8
+ * @param {string} text - Display text
9
+ * @param {string} url - Target URL
10
+ * @returns {string} Formatted link
11
+ */
12
+ function makeClickable(text, url) {
13
+ return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
14
+ }
15
+
16
+ /**
17
+ * Check if user has SSH access to LaunchFrame modules repository
18
+ * @returns {Promise<{hasAccess: boolean, error?: string}>}
19
+ */
20
+ async function checkGitHubAccess() {
21
+ try {
22
+ // Test SSH access by checking if we can list remote refs
23
+ execSync(
24
+ 'git ls-remote git@github.com:launchframe-dev/modules.git HEAD',
25
+ {
26
+ timeout: 15000,
27
+ stdio: 'pipe' // Don't show output
28
+ }
29
+ );
30
+ return { hasAccess: true };
31
+ } catch (error) {
32
+ return {
33
+ hasAccess: false,
34
+ error: error.message
35
+ };
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Display message when user doesn't have access to modules repository
41
+ * Guides them to either purchase or setup SSH keys
42
+ */
43
+ function showAccessDeniedMessage() {
44
+ const purchaseUrl = 'https://buy.polar.sh/polar_cl_Zy4YqEwhoIEdUrAH8vHaWuZtwZuv306sYMnq118MbKi';
45
+ const docsUrl = 'https://docs.launchframe.dev/guide/quick-start#add-ssh-key-to-repo';
46
+
47
+ console.log(chalk.red('\nāŒ Cannot access LaunchFrame modules repository\n'));
48
+
49
+ console.log(chalk.white('This could mean:\n'));
50
+ console.log(chalk.gray(' 1. You haven\'t purchased LaunchFrame yet'));
51
+ console.log(chalk.gray(' 2. You purchased but haven\'t added your SSH key to the repo\n'));
52
+
53
+ console.log(chalk.cyan('→ New customers:'));
54
+ console.log(' ' + chalk.blue.bold.underline(makeClickable('Purchase LaunchFrame', purchaseUrl)));
55
+ console.log(' ' + chalk.cyan(purchaseUrl + '\n'));
56
+
57
+ console.log(chalk.cyan('→ Existing customers:'));
58
+ console.log(' ' + chalk.blue.bold.underline(makeClickable('Setup SSH key (docs)', docsUrl)));
59
+ console.log(' ' + chalk.cyan(docsUrl + '\n'));
60
+
61
+ console.log(chalk.gray('After setup, run: launchframe init\n'));
62
+ }
63
+
64
+ module.exports = {
65
+ checkGitHubAccess,
66
+ showAccessDeniedMessage
67
+ };