@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/README.md +40 -11
- package/package.json +5 -5
- package/src/commands/cache.js +102 -0
- package/src/commands/deploy-configure.js +21 -4
- package/src/commands/deploy-init.js +24 -58
- package/src/commands/deploy-set-env.js +68 -91
- package/src/commands/docker-destroy.js +45 -15
- package/src/commands/docker-up.js +42 -16
- package/src/commands/help.js +11 -1
- package/src/commands/init.js +89 -55
- package/src/commands/service.js +64 -40
- package/src/commands/waitlist-deploy.js +2 -2
- package/src/commands/waitlist-logs.js +1 -2
- package/src/commands/waitlist-up.js +50 -15
- package/src/generator.js +13 -4
- package/src/index.js +16 -2
- package/src/prompts.js +12 -0
- package/src/services/registry.js +8 -6
- package/src/services/variant-config.js +135 -37
- package/src/utils/docker-helper.js +66 -44
- package/src/utils/github-access.js +67 -0
- package/src/utils/module-cache.js +274 -0
- package/src/utils/project-helpers.js +1 -1
- package/src/utils/section-replacer.js +32 -15
- package/src/utils/variable-replacer.js +7 -2
- package/src/utils/variant-processor.js +24 -12
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
|
-
|
|
37
|
-
const projectRoot = path.resolve(__dirname, '../..'); // For root-level files like .github,
|
|
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({
|
|
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
|
|
package/src/services/registry.js
CHANGED
|
@@ -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-
|
|
34
|
-
name: 'customers-
|
|
35
|
-
displayName: '
|
|
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-
|
|
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-
|
|
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
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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
|
|
187
|
-
'
|
|
188
|
-
'
|
|
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 (
|
|
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
|
-
'
|
|
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
|
-
|
|
331
|
-
|
|
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-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
};
|