@launchframe/cli 1.0.0-beta.2 → 1.0.0-beta.21

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.
@@ -4,15 +4,9 @@ const chalk = require('chalk');
4
4
  const { runInitPrompts, runVariantPrompts } = require('../prompts');
5
5
  const { generateProject } = require('../generator');
6
6
  const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
7
- const { ensureCacheReady } = require('../utils/module-cache');
8
-
9
- /**
10
- * Check if current directory is a LaunchFrame project
11
- */
12
- function isLaunchFrameProject() {
13
- const markerPath = path.join(process.cwd(), '.launchframe');
14
- return fs.existsSync(markerPath);
15
- }
7
+ const { ensureCacheReady } = require('../utils/service-cache');
8
+ const { isLaunchFrameProject } = require('../utils/project-helpers');
9
+ const logger = require('../utils/logger');
16
10
 
17
11
  /**
18
12
  * Check if running in development mode (local) vs production (npm install)
@@ -30,28 +24,28 @@ function isDevMode() {
30
24
  * @param {string} options.userModel - User model: 'b2b' or 'b2b2c' (skips prompt if provided)
31
25
  */
32
26
  async function init(options = {}) {
33
- console.log(chalk.blue.bold('\nšŸš€ Welcome to LaunchFrame!\n'));
27
+ console.log(chalk.blue.bold('\nLaunchFrame\n'));
34
28
 
35
29
  // Check if in development mode
36
30
  const devMode = isDevMode();
37
-
31
+
38
32
  if (!devMode) {
39
33
  // Production mode: Check GitHub access
40
- console.log(chalk.blue('šŸ” Checking repository access...\n'));
41
-
34
+ console.log(chalk.gray('Checking repository access...'));
35
+
42
36
  const accessCheck = await checkGitHubAccess();
43
-
37
+
44
38
  if (!accessCheck.hasAccess) {
45
- // No access - show purchase/setup message
46
39
  showAccessDeniedMessage();
47
- process.exit(1); // Exit with error code
40
+ process.exit(1);
48
41
  }
49
-
50
- console.log(chalk.green('āœ“ Repository access confirmed\n'));
42
+
43
+ console.log(chalk.green('Repository access confirmed\n'));
51
44
  }
45
+
52
46
  // Check if already in a LaunchFrame project
53
47
  if (isLaunchFrameProject()) {
54
- console.error(chalk.red('\nāŒ Error: Already in a LaunchFrame project'));
48
+ console.error(chalk.red('Error: Already in a LaunchFrame project'));
55
49
  console.log(chalk.gray('Use other commands to manage your project, or run init from outside the project.\n'));
56
50
  process.exit(1);
57
51
  }
@@ -61,12 +55,10 @@ async function init(options = {}) {
61
55
 
62
56
  // If project name provided via flag, skip prompts
63
57
  if (options.projectName) {
64
- // Validate project name format
65
58
  if (!/^[a-z0-9-]+$/.test(options.projectName)) {
66
59
  throw new Error('Project name must contain only lowercase letters, numbers, and hyphens');
67
60
  }
68
61
 
69
- // Auto-generate display name from slug
70
62
  const projectDisplayName = options.projectName
71
63
  .split('-')
72
64
  .map(word => word.charAt(0).toUpperCase() + word.slice(1))
@@ -82,91 +74,74 @@ async function init(options = {}) {
82
74
  projectNameCamel: options.projectName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
83
75
  };
84
76
 
85
- console.log(chalk.gray(`Using project name: ${options.projectName}`));
86
- console.log(chalk.gray(`Using display name: ${projectDisplayName}`));
87
- console.log(chalk.gray(`Using description: ${projectDescription}\n`));
77
+ logger.detail(`Project name: ${options.projectName}`);
78
+ logger.detail(`Display name: ${projectDisplayName}`);
79
+ logger.detail(`Description: ${projectDescription}`);
88
80
  } else {
89
- // Get user inputs via interactive prompts
90
81
  answers = await runInitPrompts();
91
82
  }
92
83
 
93
84
  // Get variant selections (multi-tenancy, B2B vs B2B2C)
94
85
  let variantChoices;
95
-
96
- // If both flags provided, skip variant prompts
86
+
97
87
  if (options.tenancy && options.userModel) {
98
- // Validate tenancy value
99
88
  if (!['single', 'multi'].includes(options.tenancy)) {
100
89
  throw new Error('Invalid --tenancy value. Must be "single" or "multi"');
101
90
  }
102
-
103
- // Validate userModel value
91
+
104
92
  if (!['b2b', 'b2b2c'].includes(options.userModel)) {
105
93
  throw new Error('Invalid --user-model value. Must be "b2b" or "b2b2c"');
106
94
  }
107
-
108
- // Convert short flag values to full variant names
95
+
109
96
  const tenancyMap = {
110
97
  'single': 'single-tenant',
111
98
  'multi': 'multi-tenant'
112
99
  };
113
-
100
+
114
101
  variantChoices = {
115
102
  tenancy: tenancyMap[options.tenancy],
116
103
  userModel: options.userModel
117
104
  };
118
-
119
- console.log(chalk.gray(`Using tenancy: ${options.tenancy}`));
120
- console.log(chalk.gray(`Using user model: ${options.userModel}\n`));
105
+
106
+ logger.detail(`Tenancy: ${options.tenancy}`);
107
+ logger.detail(`User model: ${options.userModel}`);
121
108
  } else {
122
- // Run interactive variant prompts
123
109
  variantChoices = await runVariantPrompts();
124
110
  }
125
111
 
126
- // Determine which modules are needed based on variant choices
127
- const requiredModules = [
128
- 'backend',
129
- 'admin-portal',
130
- 'infrastructure',
131
- 'website'
132
- ];
133
-
134
- // Add customers-portal only if B2B2C mode
112
+ // Determine which services are needed
113
+ const requiredServices = ['backend', 'admin-portal', 'infrastructure', 'website'];
114
+
135
115
  if (variantChoices.userModel === 'b2b2c') {
136
- requiredModules.push('customers-portal');
116
+ requiredServices.push('customers-portal');
137
117
  }
138
118
 
139
- // Determine template source (dev mode = local, production = cache)
119
+ // Determine template source
140
120
  let templateRoot;
141
-
121
+
142
122
  if (devMode) {
143
- // Dev mode: Use local modules directory
144
- templateRoot = path.resolve(__dirname, '../../../modules');
145
- console.log(chalk.gray(`[DEV MODE] Using local modules: ${templateRoot}\n`));
123
+ templateRoot = path.resolve(__dirname, '../../../services');
124
+ logger.detail(`[DEV MODE] Using local services: ${templateRoot}`);
146
125
  } else {
147
- // Production mode: Use cache
148
126
  try {
149
- templateRoot = await ensureCacheReady(requiredModules);
127
+ templateRoot = await ensureCacheReady(requiredServices);
150
128
  } catch (error) {
151
- console.error(chalk.red(`\nāŒ Error: ${error.message}\n`));
129
+ console.error(chalk.red(`Error: ${error.message}\n`));
152
130
  process.exit(1);
153
131
  }
154
132
  }
155
133
 
156
- // Generate project with variant selections
157
- console.log(chalk.yellow('\nāš™ļø Generating project...\n'));
134
+ // Generate project
135
+ console.log(chalk.white('\nGenerating project...\n'));
158
136
  await generateProject(answers, variantChoices, templateRoot);
159
137
 
160
- console.log(chalk.green.bold('\nāœ… Project generated successfully!\n'));
138
+ console.log(chalk.green.bold('\nProject created successfully!\n'));
161
139
  console.log(chalk.white('Next steps:'));
162
- console.log(chalk.white(` cd ${answers.projectName}`));
163
- console.log(chalk.white(' launchframe docker:up # Start all services\n'));
164
- console.log(chalk.gray('Optional:'));
165
- console.log(chalk.gray(' # Review and customize infrastructure/.env if needed'));
166
- console.log(chalk.gray(' launchframe docker:build # Rebuild images after changes\n'));
140
+ console.log(chalk.gray(` cd ${answers.projectName}`));
141
+ console.log(chalk.gray(' launchframe docker:up\n'));
167
142
 
168
143
  } catch (error) {
169
- console.error(chalk.red('\nāŒ Error:'), error.message);
144
+ console.error(chalk.red('Error:'), error.message);
170
145
  process.exit(1);
171
146
  }
172
147
  }
@@ -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,7 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const { isLaunchFrameProject } = require('./utils/project-helpers');
5
+ const logger = require('./utils/logger');
5
6
 
6
7
  // Import commands
7
8
  const { init } = require('./commands/init');
@@ -9,6 +10,7 @@ const { deployConfigure } = require('./commands/deploy-configure');
9
10
  const { deploySetEnv } = require('./commands/deploy-set-env');
10
11
  const { deployInit } = require('./commands/deploy-init');
11
12
  const { deployUp } = require('./commands/deploy-up');
13
+ const { deployBuild } = require('./commands/deploy-build');
12
14
  const { waitlistDeploy } = require('./commands/waitlist-deploy');
13
15
  const { waitlistUp } = require('./commands/waitlist-up');
14
16
  const { waitlistDown } = require('./commands/waitlist-down');
@@ -65,17 +67,22 @@ async function main() {
65
67
  const inProject = isLaunchFrameProject();
66
68
  const flags = parseFlags(args);
67
69
 
70
+ // Handle version flag (only as standalone command)
71
+ if (command === '--version') {
72
+ const packageJson = require('../package.json');
73
+ console.log(packageJson.version);
74
+ process.exit(0);
75
+ }
76
+
77
+ // Set verbose mode globally
78
+ if (flags.verbose || flags.v) {
79
+ logger.setVerbose(true);
80
+ }
81
+
68
82
  // No command provided
69
83
  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
- }
84
+ help();
85
+ process.exit(inProject ? 1 : 0);
79
86
  }
80
87
 
81
88
  // Route commands
@@ -99,6 +106,9 @@ async function main() {
99
106
  case 'deploy:up':
100
107
  await deployUp();
101
108
  break;
109
+ case 'deploy:build':
110
+ await deployBuild(args[1]); // Optional service name
111
+ break;
102
112
  case 'waitlist:deploy':
103
113
  await waitlistDeploy();
104
114
  break;
@@ -163,7 +173,7 @@ async function main() {
163
173
  help();
164
174
  break;
165
175
  default:
166
- console.error(chalk.red(`\nāŒ Unknown command: ${command}\n`));
176
+ console.error(chalk.red(`\nUnknown command: ${command}\n`));
167
177
  help();
168
178
  process.exit(1);
169
179
  }
@@ -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;