@launchframe/cli 1.0.0-beta.9 → 1.0.0

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.
Files changed (42) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/CLAUDE.md +27 -0
  3. package/CODE_OF_CONDUCT.md +128 -0
  4. package/LICENSE +21 -0
  5. package/README.md +7 -1
  6. package/package.json +9 -6
  7. package/src/commands/database-console.js +84 -0
  8. package/src/commands/deploy-build.js +76 -0
  9. package/src/commands/deploy-configure.js +10 -3
  10. package/src/commands/deploy-init.js +24 -57
  11. package/src/commands/deploy-set-env.js +17 -7
  12. package/src/commands/deploy-sync-features.js +233 -0
  13. package/src/commands/deploy-up.js +4 -3
  14. package/src/commands/dev-add-user.js +165 -0
  15. package/src/commands/dev-logo.js +160 -0
  16. package/src/commands/dev-npm-install.js +33 -0
  17. package/src/commands/dev-queue.js +85 -0
  18. package/src/commands/docker-build.js +9 -6
  19. package/src/commands/help.js +33 -9
  20. package/src/commands/init.js +44 -52
  21. package/src/commands/migration-create.js +40 -0
  22. package/src/commands/migration-revert.js +32 -0
  23. package/src/commands/migration-run.js +32 -0
  24. package/src/commands/module.js +146 -0
  25. package/src/commands/waitlist-deploy.js +1 -0
  26. package/src/generator.js +41 -40
  27. package/src/index.js +109 -4
  28. package/src/services/module-config.js +25 -0
  29. package/src/services/module-registry.js +12 -0
  30. package/src/services/variant-config.js +24 -13
  31. package/src/utils/docker-helper.js +116 -2
  32. package/src/utils/env-generator.js +9 -6
  33. package/src/utils/env-validator.js +4 -2
  34. package/src/utils/github-access.js +15 -13
  35. package/src/utils/logger.js +93 -0
  36. package/src/utils/module-installer.js +58 -0
  37. package/src/utils/project-helpers.js +34 -1
  38. package/src/utils/service-cache.js +12 -18
  39. package/src/utils/ssh-helper.js +51 -1
  40. package/src/utils/telemetry.js +238 -0
  41. package/src/utils/variable-replacer.js +18 -23
  42. package/src/utils/variant-processor.js +35 -42
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
75
+ // Process admin-portal
76
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
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 services/)
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,16 +160,26 @@ 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
+ // Copy MCP configuration files from template root
164
+ logger.detail('Copying MCP configuration files...');
165
+ const mcpFiles = ['CLAUDE.md', '.mcp.json'];
166
+ for (const file of mcpFiles) {
167
+ const sourcePath = path.join(templateRoot, file);
168
+ const destPath = path.join(destinationRoot, file);
169
+ if (await fs.pathExists(sourcePath)) {
170
+ await fs.copy(sourcePath, destPath);
171
+ }
172
+ }
173
+
174
+ // Generate .env file
175
+ console.log(chalk.gray(' Generating environment file...'));
171
176
  const { envPath } = await generateEnvFile(destinationRoot, answers);
172
- console.log(`āœ… Environment file created: ${envPath}`);
177
+ logger.detail(`Environment file: ${envPath}`);
173
178
 
174
- // Step 7: Create .launchframe marker file with variant metadata
175
- console.log('šŸ“ Creating LaunchFrame marker file...');
179
+ // Create .launchframe marker file
180
+ logger.detail('Creating project marker file...');
176
181
  const markerPath = path.join(destinationRoot, '.launchframe');
177
182
 
178
- // Determine which services were installed
179
183
  const installedServices = ['backend', 'admin-portal', 'infrastructure', 'website'];
180
184
  if (variantChoices.userModel === 'b2b2c') {
181
185
  installedServices.push('customers-portal');
@@ -188,12 +192,9 @@ async function generateProject(answers, variantChoices, templateRoot) {
188
192
  projectDisplayName: answers.projectDisplayName,
189
193
  deployConfigured: false,
190
194
  installedServices: installedServices,
191
- // Store variant choices for future reference
192
195
  variants: variantChoices
193
196
  };
194
197
  await fs.writeJson(markerPath, markerContent, { spaces: 2 });
195
-
196
- console.log('āœ… Base project generated with variants applied');
197
198
  }
198
199
 
199
200
  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');
@@ -25,7 +41,13 @@ const {
25
41
  serviceList,
26
42
  serviceRemove
27
43
  } = require('./commands/service');
44
+ const { moduleAdd, moduleList } = require('./commands/module');
28
45
  const { cacheClear, cacheInfo, cacheUpdate } = require('./commands/cache');
46
+ const { devAddUser } = require('./commands/dev-add-user');
47
+ const { devQueue } = require('./commands/dev-queue');
48
+ const { devLogo } = require('./commands/dev-logo');
49
+ const { devNpmInstall } = require('./commands/dev-npm-install');
50
+ const { deploySyncFeatures } = require('./commands/deploy-sync-features');
29
51
 
30
52
  // Get command and arguments
31
53
  const command = process.argv[2];
@@ -62,9 +84,23 @@ function parseFlags(args) {
62
84
  * Main CLI router
63
85
  */
64
86
  async function main() {
87
+ initTelemetry();
88
+
65
89
  const inProject = isLaunchFrameProject();
66
90
  const flags = parseFlags(args);
67
91
 
92
+ // Handle version flag (only as standalone command)
93
+ if (command === '--version') {
94
+ const packageJson = require('../package.json');
95
+ console.log(packageJson.version);
96
+ process.exit(0);
97
+ }
98
+
99
+ // Set verbose mode globally
100
+ if (flags.verbose || flags.v) {
101
+ logger.setVerbose(true);
102
+ }
103
+
68
104
  // No command provided
69
105
  if (!command) {
70
106
  help();
@@ -74,7 +110,7 @@ async function main() {
74
110
  // Route commands
75
111
  switch (command) {
76
112
  case 'init':
77
- await init({
113
+ await init({
78
114
  projectName: flags['project-name'],
79
115
  tenancy: flags['tenancy'],
80
116
  userModel: flags['user-model']
@@ -92,6 +128,12 @@ async function main() {
92
128
  case 'deploy:up':
93
129
  await deployUp();
94
130
  break;
131
+ case 'deploy:build':
132
+ await deployBuild(args[1]); // Optional service name
133
+ break;
134
+ case 'deploy:sync-features':
135
+ await deploySyncFeatures();
136
+ break;
95
137
  case 'waitlist:deploy':
96
138
  await waitlistDeploy();
97
139
  break;
@@ -105,7 +147,7 @@ async function main() {
105
147
  await waitlistLogs();
106
148
  break;
107
149
  case 'docker:build':
108
- await dockerBuild();
150
+ await dockerBuild(args[1]); // Optional service name
109
151
  break;
110
152
  case 'docker:up':
111
153
  await dockerUp(args[1]); // Pass optional service name
@@ -119,6 +161,18 @@ async function main() {
119
161
  case 'docker:destroy':
120
162
  await dockerDestroy({ force: flags.force || flags.f });
121
163
  break;
164
+ case 'migration:run':
165
+ await migrateRun();
166
+ break;
167
+ case 'migration:create':
168
+ await migrateCreate();
169
+ break;
170
+ case 'migration:revert':
171
+ await migrateRevert();
172
+ break;
173
+ case 'database:console':
174
+ await databaseConsole({ remote: flags.remote });
175
+ break;
122
176
  case 'doctor':
123
177
  await doctor();
124
178
  break;
@@ -141,6 +195,17 @@ async function main() {
141
195
  }
142
196
  await serviceRemove(args[1]);
143
197
  break;
198
+ case 'module:add':
199
+ if (!args[1]) {
200
+ console.error(chalk.red('Error: Module name required'));
201
+ console.log('Usage: launchframe module:add <module-name>');
202
+ process.exit(1);
203
+ }
204
+ await moduleAdd(args[1]);
205
+ break;
206
+ case 'module:list':
207
+ await moduleList();
208
+ break;
144
209
  case 'cache:clear':
145
210
  await cacheClear();
146
211
  break;
@@ -150,16 +215,56 @@ async function main() {
150
215
  case 'cache:update':
151
216
  await cacheUpdate();
152
217
  break;
218
+ case 'dev:add-user':
219
+ await devAddUser();
220
+ break;
221
+ case 'dev:queue':
222
+ await devQueue();
223
+ break;
224
+ case 'dev:logo':
225
+ await devLogo();
226
+ break;
227
+ case 'dev:npm-install':
228
+ if (!args[1]) {
229
+ console.error(chalk.red('Error: Service name required'));
230
+ console.log('Usage: launchframe dev:npm-install <service> [packages...]');
231
+ process.exit(1);
232
+ }
233
+ await devNpmInstall(args[1], args.slice(2));
234
+ break;
235
+ case 'telemetry':
236
+ if (flags.disable) {
237
+ setTelemetryEnabled(false);
238
+ } else if (flags.enable) {
239
+ setTelemetryEnabled(true);
240
+ } else {
241
+ showTelemetryStatus();
242
+ }
243
+ break;
153
244
  case 'help':
154
245
  case '--help':
155
246
  case '-h':
156
247
  help();
157
248
  break;
158
249
  default:
159
- console.error(chalk.red(`\nāŒ Unknown command: ${command}\n`));
250
+ console.error(chalk.red(`\nUnknown command: ${command}\n`));
160
251
  help();
161
252
  process.exit(1);
162
253
  }
163
254
  }
164
255
 
165
- main();
256
+ main()
257
+ .then(() => {
258
+ if (command && command !== 'help' && command !== '--help' && command !== '-h' && command !== '--version') {
259
+ trackEvent('command_executed', { command, success: true });
260
+ }
261
+ })
262
+ .catch((error) => {
263
+ trackEvent('command_executed', {
264
+ command,
265
+ success: false,
266
+ error_message: sanitize(error.message)
267
+ });
268
+ console.error(chalk.red(error.message));
269
+ process.exit(1);
270
+ });
@@ -0,0 +1,25 @@
1
+ // Module configuration - defines files, sections, and dependencies for each module
2
+ const MODULE_CONFIG = {
3
+ blog: {
4
+ website: {
5
+ files: [
6
+ 'src/lib/blog.ts',
7
+ 'src/types/blog.ts',
8
+ 'src/app/blog',
9
+ 'src/components/blog',
10
+ 'src/app/sitemap.ts',
11
+ 'content/blog',
12
+ ],
13
+ sections: {
14
+ 'src/components/layout/Navbar.tsx': ['BLOG_NAV_LINK'],
15
+ 'src/components/layout/Footer.tsx': ['BLOG_FOOTER_LINK'],
16
+ },
17
+ dependencies: {
18
+ 'gray-matter': '^4.0.3',
19
+ 'marked': '^12.0.0',
20
+ },
21
+ },
22
+ },
23
+ };
24
+
25
+ module.exports = { MODULE_CONFIG };
@@ -0,0 +1,12 @@
1
+ // Module registry - available modules for LaunchFrame services
2
+ const MODULE_REGISTRY = {
3
+ blog: {
4
+ name: 'blog',
5
+ displayName: 'Blog',
6
+ description: 'Markdown-based blog using local .md files with YAML front-matter — no database required',
7
+ services: ['website'],
8
+ version: '1.0.0'
9
+ }
10
+ };
11
+
12
+ module.exports = { MODULE_REGISTRY };
@@ -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
@@ -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
 
@@ -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;
@@ -159,7 +159,6 @@ async function buildFullAppImages(projectRoot, projectName, githubOrg, envFilePa
159
159
  `DOCS_URL=${envVars.DOCS_URL || ''}`,
160
160
  `CONTACT_EMAIL=${envVars.CONTACT_EMAIL || ''}`,
161
161
  `CTA_LINK=${envVars.CTA_LINK || ''}`,
162
- `LIVE_DEMO_URL=${envVars.LIVE_DEMO_URL || ''}`,
163
162
  `MIXPANEL_PROJECT_TOKEN=${envVars.MIXPANEL_PROJECT_TOKEN || ''}`,
164
163
  `GOOGLE_ANALYTICS_ID=${envVars.GOOGLE_ANALYTICS_ID || ''}`
165
164
  ];
@@ -245,15 +244,130 @@ async function buildWaitlistImage(projectRoot, projectName, githubOrg) {
245
244
  });
246
245
 
247
246
  spinner.succeed(`waitlist built and pushed successfully`);
247
+
248
+ // Clean up local image after push
249
+ const cleanupSpinner = ora('Cleaning up local waitlist image...').start();
250
+ try {
251
+ await execAsync(`docker rmi ${imageName}`, { timeout: 30000 });
252
+ cleanupSpinner.succeed('Cleaned up local waitlist image');
253
+ } catch (error) {
254
+ cleanupSpinner.info('Could not remove local waitlist image (may be in use)');
255
+ }
248
256
  } catch (error) {
249
257
  spinner.fail(`Failed to build waitlist`);
250
258
  throw new Error(`Build failed for waitlist: ${error.message}`);
251
259
  }
252
260
  }
253
261
 
262
+ /**
263
+ * Clean up local Docker images after push
264
+ * @param {string} registry - Registry URL (e.g., 'ghcr.io/myorg')
265
+ * @param {string} projectName - Project name
266
+ * @param {string[]} services - List of services to clean up
267
+ * @returns {Promise<void>}
268
+ */
269
+ async function cleanupLocalImages(registry, projectName, services) {
270
+ const spinner = ora('Cleaning up local Docker images...').start();
271
+
272
+ const imagesToRemove = services.map(service => `${registry}/${projectName}-${service}:latest`);
273
+ let removedCount = 0;
274
+
275
+ for (const imageName of imagesToRemove) {
276
+ try {
277
+ await execAsync(`docker rmi ${imageName}`, { timeout: 30000 });
278
+ removedCount++;
279
+ } catch (error) {
280
+ // Image might not exist or be in use, continue
281
+ }
282
+ }
283
+
284
+ if (removedCount > 0) {
285
+ spinner.succeed(`Cleaned up ${removedCount} local Docker image(s)`);
286
+ } else {
287
+ spinner.info('No local images to clean up');
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Complete build and push workflow - checks Docker, logs in to GHCR, builds and pushes images
293
+ * @param {Object} options - Workflow options
294
+ * @param {string} options.projectRoot - Project root directory
295
+ * @param {string} options.projectName - Project name
296
+ * @param {string} options.githubOrg - GitHub organization/username
297
+ * @param {string} options.ghcrToken - GitHub Container Registry token
298
+ * @param {string} options.envProdPath - Path to .env.prod file
299
+ * @param {string[]} options.installedServices - List of installed services
300
+ * @param {string} [options.serviceName] - Optional specific service to build (if not provided, builds all)
301
+ * @returns {Promise<void>}
302
+ */
303
+ async function buildAndPushWorkflow(options) {
304
+ const {
305
+ projectRoot,
306
+ projectName,
307
+ githubOrg,
308
+ ghcrToken,
309
+ envProdPath,
310
+ installedServices,
311
+ serviceName
312
+ } = options;
313
+
314
+ // Step 1: Check Docker is running
315
+ const dockerSpinner = ora('Checking Docker...').start();
316
+
317
+ const dockerRunning = await checkDockerRunning();
318
+ if (!dockerRunning) {
319
+ dockerSpinner.fail('Docker is not running');
320
+ throw new Error('Docker is not running. Please start Docker and try again.');
321
+ }
322
+
323
+ dockerSpinner.succeed('Docker is running');
324
+
325
+ // Step 2: Login to GHCR
326
+ if (!ghcrToken) {
327
+ throw new Error('GHCR token not found. Run deploy:configure to set up your GitHub token.');
328
+ }
329
+
330
+ await loginToGHCR(githubOrg, ghcrToken);
331
+
332
+ // Step 3: Build and push images
333
+ console.log(chalk.yellow('\nšŸ“¦ Building and pushing images...\n'));
334
+
335
+ const registry = `ghcr.io/${githubOrg}`;
336
+
337
+ if (serviceName) {
338
+ // Build specific service
339
+ if (!installedServices.includes(serviceName)) {
340
+ throw new Error(`Service "${serviceName}" not found in installed services. Available: ${installedServices.join(', ')}`);
341
+ }
342
+
343
+ await buildAndPushImage(
344
+ serviceName,
345
+ path.join(projectRoot, serviceName),
346
+ registry,
347
+ projectName
348
+ );
349
+
350
+ // Clean up local image after push
351
+ await cleanupLocalImages(registry, projectName, [serviceName]);
352
+
353
+ console.log(chalk.green.bold(`\nāœ… ${serviceName} built and pushed to GHCR!\n`));
354
+ } else {
355
+ // Build all services
356
+ await buildFullAppImages(projectRoot, projectName, githubOrg, envProdPath, installedServices);
357
+
358
+ // Clean up local images after push
359
+ await cleanupLocalImages(registry, projectName, installedServices);
360
+
361
+ console.log(chalk.green.bold('\nāœ… All images built and pushed to GHCR!\n'));
362
+ }
363
+ }
364
+
254
365
  module.exports = {
255
366
  checkDockerRunning,
256
367
  loginToGHCR,
368
+ buildAndPushImage,
257
369
  buildFullAppImages,
258
- buildWaitlistImage
370
+ buildWaitlistImage,
371
+ buildAndPushWorkflow,
372
+ cleanupLocalImages
259
373
  };