@launchframe/cli 1.0.0-beta.3 → 1.0.0-beta.31

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.
@@ -8,7 +8,7 @@ const { isLaunchFrameProject, getProjectConfig, updateProjectConfig, getPrimaryD
8
8
  const { replaceVariables } = require('../utils/variable-replacer');
9
9
  const { updateEnvFile } = require('../utils/env-generator');
10
10
  const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
11
- const { ensureCacheReady, getModulePath } = require('../utils/module-cache');
11
+ const { ensureCacheReady, getServicePath } = require('../utils/service-cache');
12
12
 
13
13
  async function serviceAdd(serviceName) {
14
14
  // STEP 1: Validation
@@ -75,13 +75,13 @@ async function serviceAdd(serviceName) {
75
75
  let sourceDir;
76
76
 
77
77
  if (isDevMode) {
78
- // Local development: copy from launchframe-dev/modules directory
78
+ // Local development: copy from launchframe-dev/services directory
79
79
  console.log(chalk.blue('\n[DEV MODE] Copying service from local directory...'));
80
- sourceDir = path.resolve(__dirname, '../../../modules', serviceName);
80
+ sourceDir = path.resolve(__dirname, '../../../services', serviceName);
81
81
 
82
82
  if (!fs.existsSync(sourceDir)) {
83
83
  console.error(chalk.red(`Error: Local service directory not found: ${sourceDir}`));
84
- console.log('Make sure the service exists in the modules directory');
84
+ console.log('Make sure the service exists in the services directory');
85
85
  process.exit(1);
86
86
  }
87
87
  } else {
@@ -98,9 +98,9 @@ async function serviceAdd(serviceName) {
98
98
  console.log(chalk.green('✓ Repository access confirmed'));
99
99
 
100
100
  try {
101
- // Ensure cache has this service module
101
+ // Ensure cache has this service
102
102
  await ensureCacheReady([serviceName]);
103
- sourceDir = getModulePath(serviceName);
103
+ sourceDir = getServicePath(serviceName);
104
104
  } catch (error) {
105
105
  console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
106
106
  process.exit(1);
@@ -464,6 +464,12 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
464
464
  ${serviceName}:
465
465
  image: ghcr.io/${githubOrg}/${projectName}-${serviceName}:latest
466
466
  restart: unless-stopped
467
+ healthcheck:
468
+ test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/"]
469
+ interval: 30s
470
+ timeout: 10s
471
+ start_period: 40s
472
+ retries: 3
467
473
  labels:
468
474
  - "traefik.enable=true"
469
475
  - "traefik.http.routers.${serviceName}.rule=Host(\`${serviceName}.\${PRIMARY_DOMAIN}\`)"
@@ -205,6 +205,7 @@ async function waitlistDeploy() {
205
205
  verifySpinner.succeed('Services verified');
206
206
  console.log(chalk.gray('\n' + psOutput));
207
207
  } catch (error) {
208
+ console.error(chalk.yellow(`\n⚠️ Error: ${error.message}\n`));
208
209
  verifySpinner.warn('Could not verify services');
209
210
  }
210
211
 
package/src/generator.js CHANGED
@@ -1,11 +1,13 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs-extra');
3
3
  const { execSync } = require('child_process');
4
+ const chalk = require('chalk');
4
5
  const { replaceVariables } = require('./utils/variable-replacer');
5
6
  const { copyDirectory } = require('./utils/file-ops');
6
7
  const { generateEnvFile } = require('./utils/env-generator');
7
8
  const { processServiceVariant } = require('./utils/variant-processor');
8
9
  const { resolveVariantChoices } = require('./services/variant-config');
10
+ const logger = require('./utils/logger');
9
11
 
10
12
  /**
11
13
  * Initialize git repository in a service directory
@@ -14,13 +16,13 @@ const { resolveVariantChoices } = require('./services/variant-config');
14
16
  */
15
17
  function initGitRepo(servicePath, serviceName) {
16
18
  try {
17
- console.log(`🔧 Initializing git repository for ${serviceName}...`);
19
+ logger.detail(`Initializing git repository for ${serviceName}`);
18
20
  execSync('git init', { cwd: servicePath, stdio: 'ignore' });
19
21
  execSync('git add .', { cwd: servicePath, stdio: 'ignore' });
20
22
  execSync('git commit -m "Initial commit"', { cwd: servicePath, stdio: 'ignore' });
21
- console.log(`✅ Git repository initialized for ${serviceName}`);
23
+ logger.detail(`Git initialized: ${serviceName}`);
22
24
  } catch (error) {
23
- console.warn(`⚠️ Could not initialize git repository for ${serviceName}: ${error.message}`);
25
+ logger.warn(`Could not initialize git for ${serviceName}: ${error.message}`);
24
26
  }
25
27
  }
26
28
 
@@ -34,12 +36,11 @@ async function generateProject(answers, variantChoices, templateRoot) {
34
36
  const { projectName } = answers;
35
37
 
36
38
  // Define source (template) and destination paths
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
39
+ const projectRoot = path.resolve(__dirname, '../..'); // For root-level files
39
40
  const destinationRoot = path.resolve(process.cwd(), projectName);
40
41
 
41
- console.log(`📁 Template source: ${templateRoot}`);
42
- console.log(`📁 Destination: ${destinationRoot}\n`);
42
+ logger.detail(`Template source: ${templateRoot}`);
43
+ logger.detail(`Destination: ${destinationRoot}`);
43
44
 
44
45
  // Ensure destination directory exists
45
46
  await fs.ensureDir(destinationRoot);
@@ -60,8 +61,8 @@ async function generateProject(answers, variantChoices, templateRoot) {
60
61
  // Resolve variant choices for all services
61
62
  const allServiceVariants = resolveVariantChoices(variantChoices);
62
63
 
63
- // Step 1: Process backend service with variants
64
- console.log('🔧 Processing backend service...');
64
+ // Process backend
65
+ console.log(chalk.gray(' Processing backend...'));
65
66
  await processServiceVariant(
66
67
  'backend',
67
68
  allServiceVariants.backend,
@@ -71,11 +72,10 @@ async function generateProject(answers, variantChoices, templateRoot) {
71
72
  );
72
73
  initGitRepo(path.join(destinationRoot, 'backend'), 'backend');
73
74
 
74
- // Step 2: Process admin-portal service with variants
75
- // Note: admin-portal folder might not exist yet in templates, skip if missing
76
- const adminPortalTemplatePath = path.join(templateRoot, 'admin-portal/templates/base');
75
+ // Process admin-portal
76
+ const adminPortalTemplatePath = path.join(templateRoot, 'admin-portal/base');
77
77
  if (await fs.pathExists(adminPortalTemplatePath)) {
78
- console.log('🔧 Processing admin-portal service...');
78
+ console.log(chalk.gray(' Processing admin-portal...'));
79
79
  await processServiceVariant(
80
80
  'admin-portal',
81
81
  allServiceVariants['admin-portal'],
@@ -85,8 +85,8 @@ async function generateProject(answers, variantChoices, templateRoot) {
85
85
  );
86
86
  initGitRepo(path.join(destinationRoot, 'admin-portal'), 'admin-portal');
87
87
  } else {
88
- // Fallback: Copy admin-portal directly without variants (for now)
89
- console.log('📋 Copying admin-portal service (no variants yet)...');
88
+ // Fallback: Copy admin-portal directly without variants
89
+ console.log(chalk.gray(' Copying admin-portal...'));
90
90
  const adminPortalSource = path.join(templateRoot, 'admin-portal');
91
91
  if (await fs.pathExists(adminPortalSource)) {
92
92
  await copyDirectory(adminPortalSource, path.join(destinationRoot, 'admin-portal'));
@@ -95,12 +95,11 @@ async function generateProject(answers, variantChoices, templateRoot) {
95
95
  }
96
96
  }
97
97
 
98
- // Step 3: Process customers-portal service (ONLY if B2B2C selected)
98
+ // Process customers-portal (only if B2B2C)
99
99
  if (variantChoices.userModel === 'b2b2c') {
100
- // Note: customers-portal folder might not exist yet in templates, skip if missing
101
- const customersPortalTemplatePath = path.join(templateRoot, 'customers-portal/templates/base');
100
+ const customersPortalTemplatePath = path.join(templateRoot, 'customers-portal/base');
102
101
  if (await fs.pathExists(customersPortalTemplatePath)) {
103
- console.log('🔧 Processing customers-portal service...');
102
+ console.log(chalk.gray(' Processing customers-portal...'));
104
103
  await processServiceVariant(
105
104
  'customers-portal',
106
105
  allServiceVariants['customers-portal'],
@@ -110,8 +109,7 @@ async function generateProject(answers, variantChoices, templateRoot) {
110
109
  );
111
110
  initGitRepo(path.join(destinationRoot, 'customers-portal'), 'customers-portal');
112
111
  } else {
113
- // Fallback: Copy customers-portal directly without variants (for now)
114
- console.log('📋 Copying customers-portal service (B2B2C mode)...');
112
+ console.log(chalk.gray(' Copying customers-portal...'));
115
113
  const customersPortalSource = path.join(templateRoot, 'customers-portal');
116
114
  if (await fs.pathExists(customersPortalSource)) {
117
115
  await copyDirectory(customersPortalSource, path.join(destinationRoot, 'customers-portal'));
@@ -120,11 +118,11 @@ async function generateProject(answers, variantChoices, templateRoot) {
120
118
  }
121
119
  }
122
120
  } else {
123
- console.log('📋 Skipping customers-portal (B2B mode - admin users only)');
121
+ logger.detail('Skipping customers-portal (B2B mode)');
124
122
  }
125
123
 
126
- // Step 4: Process infrastructure with variants (docker-compose files conditionally include customers-portal)
127
- console.log('🔧 Processing infrastructure...');
124
+ // Process infrastructure
125
+ console.log(chalk.gray(' Processing infrastructure...'));
128
126
  await processServiceVariant(
129
127
  'infrastructure',
130
128
  allServiceVariants.infrastructure,
@@ -134,7 +132,8 @@ async function generateProject(answers, variantChoices, templateRoot) {
134
132
  );
135
133
  initGitRepo(path.join(destinationRoot, 'infrastructure'), 'infrastructure');
136
134
 
137
- console.log('📋 Copying website...');
135
+ // Process website
136
+ console.log(chalk.gray(' Processing website...'));
138
137
  await copyDirectory(
139
138
  path.join(templateRoot, 'website'),
140
139
  path.join(destinationRoot, 'website')
@@ -142,14 +141,9 @@ async function generateProject(answers, variantChoices, templateRoot) {
142
141
  await replaceVariables(path.join(destinationRoot, 'website'), variables);
143
142
  initGitRepo(path.join(destinationRoot, 'website'), 'website');
144
143
 
145
- // Step 5: Copy additional files (from project root, not modules/)
146
- console.log('📋 Copying additional files...');
147
- const additionalFiles = [
148
- '.github',
149
- 'README.md',
150
- '.gitignore',
151
- 'LICENSE'
152
- ];
144
+ // Copy additional files
145
+ logger.detail('Copying additional files...');
146
+ const additionalFiles = ['.github', 'README.md', '.gitignore', 'LICENSE'];
153
147
 
154
148
  for (const file of additionalFiles) {
155
149
  const sourcePath = path.join(projectRoot, file);
@@ -166,21 +160,20 @@ async function generateProject(answers, variantChoices, templateRoot) {
166
160
  }
167
161
  }
168
162
 
169
- // Step 6: Generate .env file with localhost defaults
170
- console.log('\n🔐 Generating .env file with secure secrets...');
163
+ // Generate .env file
164
+ console.log(chalk.gray(' Generating environment file...'));
171
165
  const { envPath } = await generateEnvFile(destinationRoot, answers);
172
- console.log(`✅ Environment file created: ${envPath}`);
166
+ logger.detail(`Environment file: ${envPath}`);
173
167
 
174
- // Step 7: Create .launchframe marker file with variant metadata
175
- console.log('📝 Creating LaunchFrame marker file...');
168
+ // Create .launchframe marker file
169
+ logger.detail('Creating project marker file...');
176
170
  const markerPath = path.join(destinationRoot, '.launchframe');
177
-
178
- // Determine which services were installed
171
+
179
172
  const installedServices = ['backend', 'admin-portal', 'infrastructure', 'website'];
180
173
  if (variantChoices.userModel === 'b2b2c') {
181
174
  installedServices.push('customers-portal');
182
175
  }
183
-
176
+
184
177
  const markerContent = {
185
178
  version: '0.1.0',
186
179
  createdAt: new Date().toISOString(),
@@ -188,12 +181,9 @@ async function generateProject(answers, variantChoices, templateRoot) {
188
181
  projectDisplayName: answers.projectDisplayName,
189
182
  deployConfigured: false,
190
183
  installedServices: installedServices,
191
- // Store variant choices for future reference
192
184
  variants: variantChoices
193
185
  };
194
186
  await fs.writeJson(markerPath, markerContent, { spaces: 2 });
195
-
196
- console.log('✅ Base project generated with variants applied');
197
187
  }
198
188
 
199
189
  module.exports = { generateProject };
package/src/index.js CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const { isLaunchFrameProject } = require('./utils/project-helpers');
5
+ const logger = require('./utils/logger');
6
+ const { initTelemetry, trackEvent, sanitize, setTelemetryEnabled, showTelemetryStatus } = require('./utils/telemetry');
7
+
8
+ // Detect locally linked version: npm link installs to global node_modules
9
+ // as a symlink. When running from a real install, __dirname is inside the
10
+ // global node_modules folder. When linked, it resolves to the source directory.
11
+ const isDevMode = !__dirname.includes('node_modules');
12
+ if (isDevMode) {
13
+ const packageJson = require('../package.json');
14
+ console.log(chalk.yellow(`⚠ Running locally linked CLI v${packageJson.version} (${__dirname})`));
15
+ }
5
16
 
6
17
  // Import commands
7
18
  const { init } = require('./commands/init');
@@ -9,6 +20,7 @@ const { deployConfigure } = require('./commands/deploy-configure');
9
20
  const { deploySetEnv } = require('./commands/deploy-set-env');
10
21
  const { deployInit } = require('./commands/deploy-init');
11
22
  const { deployUp } = require('./commands/deploy-up');
23
+ const { deployBuild } = require('./commands/deploy-build');
12
24
  const { waitlistDeploy } = require('./commands/waitlist-deploy');
13
25
  const { waitlistUp } = require('./commands/waitlist-up');
14
26
  const { waitlistDown } = require('./commands/waitlist-down');
@@ -17,6 +29,10 @@ const { dockerBuild } = require('./commands/docker-build');
17
29
  const { dockerUp } = require('./commands/docker-up');
18
30
  const { dockerDown } = require('./commands/docker-down');
19
31
  const { dockerLogs } = require('./commands/docker-logs');
32
+ const { migrateRun } = require('./commands/migration-run');
33
+ const { migrateCreate } = require('./commands/migration-create');
34
+ const { migrateRevert } = require('./commands/migration-revert');
35
+ const { databaseConsole } = require('./commands/database-console');
20
36
  const { dockerDestroy } = require('./commands/docker-destroy');
21
37
  const { doctor } = require('./commands/doctor');
22
38
  const { help } = require('./commands/help');
@@ -26,6 +42,8 @@ const {
26
42
  serviceRemove
27
43
  } = require('./commands/service');
28
44
  const { cacheClear, cacheInfo, cacheUpdate } = require('./commands/cache');
45
+ const { devAddUser } = require('./commands/dev-add-user');
46
+ const { devQueue } = require('./commands/dev-queue');
29
47
 
30
48
  // Get command and arguments
31
49
  const command = process.argv[2];
@@ -62,26 +80,33 @@ function parseFlags(args) {
62
80
  * Main CLI router
63
81
  */
64
82
  async function main() {
83
+ initTelemetry();
84
+
65
85
  const inProject = isLaunchFrameProject();
66
86
  const flags = parseFlags(args);
67
87
 
88
+ // Handle version flag (only as standalone command)
89
+ if (command === '--version') {
90
+ const packageJson = require('../package.json');
91
+ console.log(packageJson.version);
92
+ process.exit(0);
93
+ }
94
+
95
+ // Set verbose mode globally
96
+ if (flags.verbose || flags.v) {
97
+ logger.setVerbose(true);
98
+ }
99
+
68
100
  // No command provided
69
101
  if (!command) {
70
- if (inProject) {
71
- console.error(chalk.red('\n❌ Error: No command specified'));
72
- help();
73
- process.exit(1);
74
- } else {
75
- // Outside project, default to init
76
- await init(flags);
77
- return;
78
- }
102
+ help();
103
+ process.exit(inProject ? 1 : 0);
79
104
  }
80
105
 
81
106
  // Route commands
82
107
  switch (command) {
83
108
  case 'init':
84
- await init({
109
+ await init({
85
110
  projectName: flags['project-name'],
86
111
  tenancy: flags['tenancy'],
87
112
  userModel: flags['user-model']
@@ -99,6 +124,9 @@ async function main() {
99
124
  case 'deploy:up':
100
125
  await deployUp();
101
126
  break;
127
+ case 'deploy:build':
128
+ await deployBuild(args[1]); // Optional service name
129
+ break;
102
130
  case 'waitlist:deploy':
103
131
  await waitlistDeploy();
104
132
  break;
@@ -126,6 +154,18 @@ async function main() {
126
154
  case 'docker:destroy':
127
155
  await dockerDestroy({ force: flags.force || flags.f });
128
156
  break;
157
+ case 'migration:run':
158
+ await migrateRun();
159
+ break;
160
+ case 'migration:create':
161
+ await migrateCreate();
162
+ break;
163
+ case 'migration:revert':
164
+ await migrateRevert();
165
+ break;
166
+ case 'database:console':
167
+ await databaseConsole({ remote: flags.remote });
168
+ break;
129
169
  case 'doctor':
130
170
  await doctor();
131
171
  break;
@@ -157,16 +197,45 @@ async function main() {
157
197
  case 'cache:update':
158
198
  await cacheUpdate();
159
199
  break;
200
+ case 'dev:add-user':
201
+ await devAddUser();
202
+ break;
203
+ case 'dev:queue':
204
+ await devQueue();
205
+ break;
206
+ case 'telemetry':
207
+ if (flags.disable) {
208
+ setTelemetryEnabled(false);
209
+ } else if (flags.enable) {
210
+ setTelemetryEnabled(true);
211
+ } else {
212
+ showTelemetryStatus();
213
+ }
214
+ break;
160
215
  case 'help':
161
216
  case '--help':
162
217
  case '-h':
163
218
  help();
164
219
  break;
165
220
  default:
166
- console.error(chalk.red(`\n❌ Unknown command: ${command}\n`));
221
+ console.error(chalk.red(`\nUnknown command: ${command}\n`));
167
222
  help();
168
223
  process.exit(1);
169
224
  }
170
225
  }
171
226
 
172
- main();
227
+ main()
228
+ .then(() => {
229
+ if (command && command !== 'help' && command !== '--help' && command !== '-h' && command !== '--version') {
230
+ trackEvent('command_executed', { command, success: true });
231
+ }
232
+ })
233
+ .catch((error) => {
234
+ trackEvent('command_executed', {
235
+ command,
236
+ success: false,
237
+ error_message: sanitize(error.message)
238
+ });
239
+ console.error(chalk.red(error.message));
240
+ process.exit(1);
241
+ });
@@ -11,9 +11,9 @@
11
11
 
12
12
  const VARIANT_CONFIG = {
13
13
  backend: {
14
- base: 'backend/templates/base',
15
- sectionsDir: 'backend/templates/sections',
16
- filesDir: 'backend/templates/files',
14
+ base: 'backend/base',
15
+ sectionsDir: 'backend/variants/sections',
16
+ filesDir: 'backend/variants/files',
17
17
 
18
18
  variants: {
19
19
  // Multi-tenant variant: Adds project/workspace support
@@ -21,24 +21,16 @@ 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/ai/services/project-config.service.ts', // Project config service
25
24
  'src/guards/project-ownership.guard.ts', // Project ownership guard (header-based)
26
25
  'src/guards/project-param.guard.ts', // Project param guard (route-based)
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
26
  'src/modules/users/users.service.ts', // Users service with multi-tenant support
30
27
  'src/modules/users/users.controller.ts', // Users controller with multi-tenant support
31
28
  'src/modules/users/create-user.dto.ts' // CreateUserDto with businessId
32
29
  ],
33
30
 
34
31
  // Code sections to insert into base template files
32
+ // Note: main.ts uses PRIMARY_DOMAIN env var for dynamic CORS - no sections needed
35
33
  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
34
  'src/modules/app/app.module.ts': [
43
35
  'PROJECTS_MODULE_IMPORT', // Add ProjectsModule import
44
36
  'PROJECTS_MODULE' // Add ProjectsModule to imports array
@@ -60,11 +52,15 @@ const VARIANT_CONFIG = {
60
52
  }
61
53
  },
62
54
 
63
- // B2B2C variant: Adds regular_user support (for single-tenant only)
55
+ // B2B2C variant: Adds regular_user support with separate customer auth
64
56
  'b2b2c': {
65
57
  // Complete files to copy
66
58
  files: [
67
- 'src/modules/users/user-business.entity.ts', // Business-to-user linking entity
59
+ 'src/modules/users/user-business.entity.ts', // Business-to-user linking entity
60
+ 'src/modules/auth/auth-customer.ts', // Customer auth config (regular_user, customer_ cookie)
61
+ 'src/modules/auth/better-auth-customer.controller.ts', // Customer auth controller (/api/auth/customer)
62
+ 'src/modules/auth/auth.module.ts', // Auth module with customer controller
63
+ 'src/modules/auth/better-auth.guard.ts', // Guard handling both auth instances
68
64
  ],
69
65
 
70
66
  // Code sections to insert
@@ -77,6 +73,9 @@ const VARIANT_CONFIG = {
77
73
  'src/modules/users/users.module.ts': [
78
74
  'B2B2C_IMPORTS', // Add UserBusiness import
79
75
  'B2B2C_ENTITIES' // Add UserBusiness to TypeORM
76
+ ],
77
+ 'src/database/migrations/1764300000001-CreateSessionsTable.ts': [
78
+ 'B2B2C_TENANT_COLUMN' // Add tenant_id column for session scoping
80
79
  ]
81
80
  }
82
81
  },
@@ -126,8 +125,7 @@ const VARIANT_CONFIG = {
126
125
  // Complete files to copy (has both multi-tenant and B2B2C features)
127
126
  files: [
128
127
  '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
128
+ 'src/modules/auth/auth.ts', // Combined Better Auth config
131
129
  'src/modules/users/users.service.ts', // Combined users service
132
130
  'src/modules/users/users.controller.ts', // Combined users controller
133
131
  'src/modules/domain/projects/projects.module.ts' // Projects module with UserBusiness
@@ -180,9 +178,9 @@ const VARIANT_CONFIG = {
180
178
 
181
179
  // Admin portal inherits tenancy choice from backend
182
180
  'admin-portal': {
183
- base: 'admin-portal/templates/base',
184
- sectionsDir: 'admin-portal/templates/sections',
185
- filesDir: 'admin-portal/templates/files',
181
+ base: 'admin-portal/base',
182
+ sectionsDir: 'admin-portal/variants/sections',
183
+ filesDir: 'admin-portal/variants/files',
186
184
 
187
185
  variants: {
188
186
  'multi-tenant': {
@@ -284,9 +282,9 @@ const VARIANT_CONFIG = {
284
282
 
285
283
  // Customers portal (B2B2C only - no pure B2B use case)
286
284
  'customers-portal': {
287
- base: 'customers-portal/templates/base', // B2B2C + Single-tenant base
288
- sectionsDir: 'customers-portal/templates/sections',
289
- filesDir: 'customers-portal/templates/files',
285
+ base: 'customers-portal/base', // B2B2C + Single-tenant base
286
+ sectionsDir: 'customers-portal/variants/sections',
287
+ filesDir: 'customers-portal/variants/files',
290
288
 
291
289
  variants: {
292
290
  'single-tenant': {
@@ -310,6 +308,14 @@ const VARIANT_CONFIG = {
310
308
  'src/store/useProjectStore.ts' // Project state
311
309
  ],
312
310
  sections: {}
311
+ },
312
+
313
+ 'b2b2c': {
314
+ // B2B2C uses separate auth endpoint for customer sessions
315
+ files: [
316
+ 'src/lib/auth-client.ts' // Auth client with /api/auth/customer basePath
317
+ ],
318
+ sections: {}
313
319
  }
314
320
  },
315
321
 
@@ -321,9 +327,9 @@ const VARIANT_CONFIG = {
321
327
 
322
328
  // Infrastructure (Docker Compose orchestration)
323
329
  infrastructure: {
324
- base: 'infrastructure',
325
- sectionsDir: 'infrastructure/templates/sections',
326
- filesDir: 'infrastructure/templates/files',
330
+ base: 'infrastructure/base',
331
+ sectionsDir: 'infrastructure/variants/sections',
332
+ filesDir: 'infrastructure/variants/files',
327
333
 
328
334
  variants: {
329
335
  // B2B2C variant: Adds customers-portal service to docker-compose files
@@ -395,6 +401,11 @@ function resolveVariantChoices(backendChoices) {
395
401
  choices['admin-portal'].userModel = backendChoices.userModel;
396
402
  }
397
403
 
404
+ // Special case: customers-portal inherits BOTH tenancy and userModel
405
+ if (choices['customers-portal']) {
406
+ choices['customers-portal'].userModel = backendChoices.userModel;
407
+ }
408
+
398
409
  // Special case: infrastructure needs BOTH tenancy and userModel for proper variant resolution
399
410
  if (choices['infrastructure']) {
400
411
  choices['infrastructure'].tenancy = backendChoices.tenancy;