@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.
- package/CLAUDE.md +27 -0
- package/README.md +7 -1
- package/package.json +1 -1
- package/src/commands/cache.js +14 -14
- package/src/commands/deploy-build.js +76 -0
- package/src/commands/deploy-configure.js +15 -6
- package/src/commands/deploy-init.js +40 -57
- package/src/commands/deploy-set-env.js +17 -7
- package/src/commands/deploy-up.js +4 -3
- package/src/commands/help.js +11 -5
- package/src/commands/init.js +39 -64
- package/src/commands/service.js +12 -6
- package/src/commands/waitlist-deploy.js +1 -0
- package/src/generator.js +34 -44
- package/src/index.js +20 -10
- package/src/services/variant-config.js +36 -25
- package/src/utils/docker-helper.js +116 -2
- package/src/utils/env-generator.js +9 -6
- package/src/utils/env-validator.js +4 -2
- package/src/utils/github-access.js +19 -17
- package/src/utils/logger.js +93 -0
- package/src/utils/project-helpers.js +5 -1
- package/src/utils/{module-cache.js ā service-cache.js} +67 -73
- package/src/utils/ssh-helper.js +51 -1
- package/src/utils/variant-processor.js +35 -42
package/src/commands/init.js
CHANGED
|
@@ -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/
|
|
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
|
|
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.
|
|
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);
|
|
40
|
+
process.exit(1);
|
|
48
41
|
}
|
|
49
|
-
|
|
50
|
-
console.log(chalk.green('
|
|
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('
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
|
127
|
-
const
|
|
128
|
-
|
|
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
|
-
|
|
116
|
+
requiredServices.push('customers-portal');
|
|
137
117
|
}
|
|
138
118
|
|
|
139
|
-
// Determine template source
|
|
119
|
+
// Determine template source
|
|
140
120
|
let templateRoot;
|
|
141
|
-
|
|
121
|
+
|
|
142
122
|
if (devMode) {
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
127
|
+
templateRoot = await ensureCacheReady(requiredServices);
|
|
150
128
|
} catch (error) {
|
|
151
|
-
console.error(chalk.red(
|
|
129
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
152
130
|
process.exit(1);
|
|
153
131
|
}
|
|
154
132
|
}
|
|
155
133
|
|
|
156
|
-
// Generate project
|
|
157
|
-
console.log(chalk.
|
|
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('\
|
|
138
|
+
console.log(chalk.green.bold('\nProject created successfully!\n'));
|
|
161
139
|
console.log(chalk.white('Next steps:'));
|
|
162
|
-
console.log(chalk.
|
|
163
|
-
console.log(chalk.
|
|
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('
|
|
144
|
+
console.error(chalk.red('Error:'), error.message);
|
|
170
145
|
process.exit(1);
|
|
171
146
|
}
|
|
172
147
|
}
|
package/src/commands/service.js
CHANGED
|
@@ -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,
|
|
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/
|
|
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, '../../../
|
|
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
|
|
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
|
|
101
|
+
// Ensure cache has this service
|
|
102
102
|
await ensureCacheReady([serviceName]);
|
|
103
|
-
sourceDir =
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
logger.detail(`Git initialized: ${serviceName}`);
|
|
22
24
|
} catch (error) {
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
//
|
|
64
|
-
console.log('
|
|
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
|
-
//
|
|
75
|
-
|
|
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('
|
|
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
|
|
89
|
-
console.log('
|
|
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
|
-
//
|
|
98
|
+
// Process customers-portal (only if B2B2C)
|
|
99
99
|
if (variantChoices.userModel === 'b2b2c') {
|
|
100
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
logger.detail('Skipping customers-portal (B2B mode)');
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
//
|
|
127
|
-
console.log('
|
|
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
|
-
|
|
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
|
-
//
|
|
146
|
-
|
|
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
|
-
//
|
|
170
|
-
console.log('
|
|
163
|
+
// Generate .env file
|
|
164
|
+
console.log(chalk.gray(' Generating environment file...'));
|
|
171
165
|
const { envPath } = await generateEnvFile(destinationRoot, answers);
|
|
172
|
-
|
|
166
|
+
logger.detail(`Environment file: ${envPath}`);
|
|
173
167
|
|
|
174
|
-
//
|
|
175
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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(`\
|
|
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/
|
|
15
|
-
sectionsDir: 'backend/
|
|
16
|
-
filesDir: 'backend/
|
|
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
|
|
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',
|
|
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.
|
|
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/
|
|
184
|
-
sectionsDir: 'admin-portal/
|
|
185
|
-
filesDir: 'admin-portal/
|
|
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/
|
|
288
|
-
sectionsDir: 'customers-portal/
|
|
289
|
-
filesDir: 'customers-portal/
|
|
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/
|
|
326
|
-
filesDir: 'infrastructure/
|
|
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;
|