@launchframe/cli 0.1.11 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -11
- package/package.json +5 -5
- package/src/commands/cache.js +102 -0
- package/src/commands/deploy-configure.js +21 -4
- package/src/commands/deploy-init.js +24 -58
- package/src/commands/deploy-set-env.js +68 -91
- package/src/commands/docker-destroy.js +45 -15
- package/src/commands/docker-up.js +42 -16
- package/src/commands/help.js +11 -1
- package/src/commands/init.js +89 -55
- package/src/commands/service.js +64 -40
- package/src/commands/waitlist-deploy.js +2 -2
- package/src/commands/waitlist-logs.js +1 -2
- package/src/commands/waitlist-up.js +50 -15
- package/src/generator.js +13 -4
- package/src/index.js +16 -2
- package/src/prompts.js +12 -0
- package/src/services/registry.js +8 -6
- package/src/services/variant-config.js +135 -37
- package/src/utils/docker-helper.js +56 -44
- package/src/utils/github-access.js +67 -0
- package/src/utils/module-cache.js +274 -0
- package/src/utils/project-helpers.js +1 -1
- package/src/utils/section-replacer.js +32 -15
- package/src/utils/variable-replacer.js +7 -2
- package/src/utils/variant-processor.js +24 -12
|
@@ -19,18 +19,38 @@ async function dockerUp(serviceName) {
|
|
|
19
19
|
process.exit(1);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Check Docker Compose version for watch support
|
|
23
|
+
try {
|
|
24
|
+
const composeVersion = execSync('docker compose version', { encoding: 'utf8' });
|
|
25
|
+
const versionMatch = composeVersion.match(/v?(\d+)\.(\d+)\.(\d+)/);
|
|
26
|
+
|
|
27
|
+
if (versionMatch) {
|
|
28
|
+
const [, major, minor] = versionMatch.map(Number);
|
|
29
|
+
|
|
30
|
+
if (major < 2 || (major === 2 && minor < 22)) {
|
|
31
|
+
console.error(chalk.red('\n❌ Error: Docker Compose v2.22+ is required for watch support'));
|
|
32
|
+
console.log(chalk.yellow(`Current version: Docker Compose v${major}.${minor}`));
|
|
33
|
+
console.log(chalk.gray('\nPlease upgrade Docker Compose:'));
|
|
34
|
+
console.log(chalk.white(' https://docs.docker.com/compose/install/\n'));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.warn(chalk.yellow('⚠️ Could not detect Docker Compose version'));
|
|
40
|
+
}
|
|
41
|
+
|
|
22
42
|
if (serviceName) {
|
|
23
|
-
console.log(chalk.blue.bold(`\n🚀 Starting ${serviceName} service\n`));
|
|
24
|
-
console.log(chalk.gray(`Starting ${serviceName}
|
|
43
|
+
console.log(chalk.blue.bold(`\n🚀 Starting ${serviceName} service with watch\n`));
|
|
44
|
+
console.log(chalk.gray(`Starting ${serviceName} with file watching enabled...\n`));
|
|
25
45
|
} else {
|
|
26
|
-
console.log(chalk.blue.bold('\n🚀 Starting Docker Services\n'));
|
|
27
|
-
console.log(chalk.gray('Starting all services
|
|
46
|
+
console.log(chalk.blue.bold('\n🚀 Starting Docker Services with Watch\n'));
|
|
47
|
+
console.log(chalk.gray('Starting all services with file watching enabled...\n'));
|
|
28
48
|
}
|
|
29
49
|
|
|
30
50
|
try {
|
|
31
51
|
const upCommand = serviceName
|
|
32
|
-
? `docker-compose -f docker-compose.yml -f docker-compose.dev.yml
|
|
33
|
-
: 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml
|
|
52
|
+
? `docker-compose -f docker-compose.yml -f docker-compose.dev.yml watch ${serviceName}`
|
|
53
|
+
: 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml watch';
|
|
34
54
|
|
|
35
55
|
console.log(chalk.gray(`Running: ${upCommand}\n`));
|
|
36
56
|
|
|
@@ -40,12 +60,19 @@ async function dockerUp(serviceName) {
|
|
|
40
60
|
});
|
|
41
61
|
|
|
42
62
|
if (serviceName) {
|
|
43
|
-
console.log(chalk.green.bold(`\n✅ ${serviceName} service started
|
|
44
|
-
console.log(chalk.
|
|
45
|
-
console.log(chalk.
|
|
46
|
-
console.log(chalk.gray(
|
|
63
|
+
console.log(chalk.green.bold(`\n✅ ${serviceName} service started with watch!\n`));
|
|
64
|
+
console.log(chalk.yellow('📺 Watching for file changes (press Ctrl+C to stop)...\n'));
|
|
65
|
+
console.log(chalk.white('Watch behavior:'));
|
|
66
|
+
console.log(chalk.gray(' • Code changes → Auto-sync to container'));
|
|
67
|
+
console.log(chalk.gray(' • package.json → Auto-rebuild & restart\n'));
|
|
68
|
+
console.log(chalk.white('To stop:'));
|
|
69
|
+
console.log(chalk.gray(' Press Ctrl+C in this terminal\n'));
|
|
47
70
|
} else {
|
|
48
|
-
console.log(chalk.green.bold('\n✅ All services started
|
|
71
|
+
console.log(chalk.green.bold('\n✅ All services started with watch!\n'));
|
|
72
|
+
console.log(chalk.yellow('📺 Watching for file changes (press Ctrl+C to stop)...\n'));
|
|
73
|
+
console.log(chalk.white('Watch behavior:'));
|
|
74
|
+
console.log(chalk.gray(' • Code changes → Auto-sync to containers'));
|
|
75
|
+
console.log(chalk.gray(' • package.json → Auto-rebuild & restart\n'));
|
|
49
76
|
console.log(chalk.white('Services running at:'));
|
|
50
77
|
console.log(chalk.gray(' Backend API: http://localhost:4000'));
|
|
51
78
|
console.log(chalk.gray(' Admin Panel: http://localhost:3001'));
|
|
@@ -57,11 +84,10 @@ async function dockerUp(serviceName) {
|
|
|
57
84
|
}
|
|
58
85
|
|
|
59
86
|
console.log(chalk.gray(' Marketing Site: http://localhost:8080\n'));
|
|
60
|
-
console.log(chalk.white('
|
|
61
|
-
console.log(chalk.gray('
|
|
62
|
-
console.log(chalk.gray(' launchframe docker:
|
|
63
|
-
console.log(chalk.
|
|
64
|
-
console.log(chalk.gray(' launchframe docker:destroy # Remove all resources\n'));
|
|
87
|
+
console.log(chalk.white('To stop all services:'));
|
|
88
|
+
console.log(chalk.gray(' Press Ctrl+C in this terminal'));
|
|
89
|
+
console.log(chalk.gray(' Or run: launchframe docker:down\n'));
|
|
90
|
+
console.log(chalk.cyan('💡 Tip (Linux/Mac): Add & at the end to run in background\n'));
|
|
65
91
|
}
|
|
66
92
|
|
|
67
93
|
} catch (error) {
|
package/src/commands/help.js
CHANGED
|
@@ -39,6 +39,10 @@ function help() {
|
|
|
39
39
|
console.log(chalk.gray(' service:remove <name> Remove installed service\n'));
|
|
40
40
|
console.log(chalk.white('Available Services:'));
|
|
41
41
|
console.log(chalk.gray(' waitlist Coming soon page with email collection\n'));
|
|
42
|
+
console.log(chalk.white('Cache Management:'));
|
|
43
|
+
console.log(chalk.gray(' cache:info Show cache location, size, and cached modules'));
|
|
44
|
+
console.log(chalk.gray(' cache:update Force update cache to latest version'));
|
|
45
|
+
console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
|
|
42
46
|
console.log(chalk.white('Other commands:'));
|
|
43
47
|
console.log(chalk.gray(' doctor Check project health and configuration'));
|
|
44
48
|
console.log(chalk.gray(' help Show this help message\n'));
|
|
@@ -67,12 +71,18 @@ function help() {
|
|
|
67
71
|
console.log(chalk.white('Available commands:'));
|
|
68
72
|
console.log(chalk.gray(' init Initialize a new LaunchFrame project'));
|
|
69
73
|
console.log(chalk.gray(' --project-name <name> Project name (skips prompt)'));
|
|
74
|
+
console.log(chalk.gray(' --tenancy <single|multi> Tenancy model (skips prompt)'));
|
|
75
|
+
console.log(chalk.gray(' --user-model <b2b|b2b2c> User model (skips prompt)'));
|
|
70
76
|
console.log(chalk.gray(' help Show this help message\n'));
|
|
77
|
+
console.log(chalk.white('Cache Management:'));
|
|
78
|
+
console.log(chalk.gray(' cache:info Show cache location, size, and cached modules'));
|
|
79
|
+
console.log(chalk.gray(' cache:update Force update cache to latest version'));
|
|
80
|
+
console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
|
|
71
81
|
console.log(chalk.white('Examples:'));
|
|
72
82
|
console.log(chalk.gray(' # Interactive mode'));
|
|
73
83
|
console.log(chalk.gray(' launchframe init\n'));
|
|
74
84
|
console.log(chalk.gray(' # Non-interactive mode'));
|
|
75
|
-
console.log(chalk.gray(' launchframe init --project-name my-saas\n'));
|
|
85
|
+
console.log(chalk.gray(' launchframe init --project-name my-saas --tenancy single --user-model b2b\n'));
|
|
76
86
|
}
|
|
77
87
|
}
|
|
78
88
|
|
package/src/commands/init.js
CHANGED
|
@@ -3,6 +3,8 @@ const path = require('path');
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { runInitPrompts, runVariantPrompts } = require('../prompts');
|
|
5
5
|
const { generateProject } = require('../generator');
|
|
6
|
+
const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
|
|
7
|
+
const { ensureCacheReady } = require('../utils/module-cache');
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Check if current directory is a LaunchFrame project
|
|
@@ -16,69 +18,37 @@ function isLaunchFrameProject() {
|
|
|
16
18
|
* Check if running in development mode (local) vs production (npm install)
|
|
17
19
|
*/
|
|
18
20
|
function isDevMode() {
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
if (process.env.LAUNCHFRAME_DEV === 'false') {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Check if running from node_modules (production) or local directory (dev)
|
|
28
|
-
const scriptPath = __dirname;
|
|
29
|
-
|
|
30
|
-
// Check multiple indicators that we're in production:
|
|
31
|
-
// 1. Running from node_modules
|
|
32
|
-
// 2. Running from npm cache (_npx or .npm)
|
|
33
|
-
// 3. Package name in path
|
|
34
|
-
const isInNodeModules = scriptPath.includes('node_modules');
|
|
35
|
-
const isInNpmCache = scriptPath.includes('_npx') || scriptPath.includes('.npm');
|
|
36
|
-
const isInPackage = scriptPath.includes('@launchframe');
|
|
37
|
-
|
|
38
|
-
// If any production indicator is found, we're NOT in dev mode
|
|
39
|
-
if (isInNodeModules || isInNpmCache || isInPackage) {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return true;
|
|
21
|
+
// Only use dev mode if LAUNCHFRAME_DEV is explicitly set to 'true'
|
|
22
|
+
return process.env.LAUNCHFRAME_DEV === 'true';
|
|
44
23
|
}
|
|
45
24
|
|
|
46
25
|
/**
|
|
47
26
|
* Initialize a new LaunchFrame project
|
|
48
27
|
* @param {Object} options - Command options
|
|
49
28
|
* @param {string} options.projectName - Project name (skips prompt if provided)
|
|
29
|
+
* @param {string} options.tenancy - Tenancy model: 'single' or 'multi' (skips prompt if provided)
|
|
30
|
+
* @param {string} options.userModel - User model: 'b2b' or 'b2b2c' (skips prompt if provided)
|
|
50
31
|
*/
|
|
51
32
|
async function init(options = {}) {
|
|
52
33
|
console.log(chalk.blue.bold('\n🚀 Welcome to LaunchFrame!\n'));
|
|
53
34
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
console.log(chalk.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
console.log(chalk.
|
|
70
|
-
|
|
71
|
-
console.log(chalk.gray('Founding members get:'));
|
|
72
|
-
console.log(chalk.gray(' • Exclusive early access'));
|
|
73
|
-
console.log(chalk.gray(' • Lifetime updates'));
|
|
74
|
-
console.log(chalk.gray(' • Priority support'));
|
|
75
|
-
console.log(chalk.gray(' • Special launch pricing\n'));
|
|
76
|
-
|
|
77
|
-
console.log(chalk.white('Questions? Email support@launchframe.dev\n'));
|
|
78
|
-
return;
|
|
35
|
+
// Check if in development mode
|
|
36
|
+
const devMode = isDevMode();
|
|
37
|
+
|
|
38
|
+
if (!devMode) {
|
|
39
|
+
// Production mode: Check GitHub access
|
|
40
|
+
console.log(chalk.blue('🔍 Checking repository access...\n'));
|
|
41
|
+
|
|
42
|
+
const accessCheck = await checkGitHubAccess();
|
|
43
|
+
|
|
44
|
+
if (!accessCheck.hasAccess) {
|
|
45
|
+
// No access - show purchase/setup message
|
|
46
|
+
showAccessDeniedMessage();
|
|
47
|
+
process.exit(1); // Exit with error code
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(chalk.green('✓ Repository access confirmed\n'));
|
|
79
51
|
}
|
|
80
|
-
|
|
81
|
-
// Dev mode - continue with normal project generation
|
|
82
52
|
// Check if already in a LaunchFrame project
|
|
83
53
|
if (isLaunchFrameProject()) {
|
|
84
54
|
console.error(chalk.red('\n❌ Error: Already in a LaunchFrame project'));
|
|
@@ -102,26 +72,90 @@ async function init(options = {}) {
|
|
|
102
72
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
103
73
|
.join(' ');
|
|
104
74
|
|
|
75
|
+
const projectDescription = `${projectDisplayName} - Modern SaaS Platform`;
|
|
76
|
+
|
|
105
77
|
answers = {
|
|
106
78
|
projectName: options.projectName,
|
|
107
79
|
projectDisplayName: projectDisplayName,
|
|
80
|
+
projectDescription: projectDescription,
|
|
108
81
|
projectNameUpper: options.projectName.toUpperCase().replace(/-/g, '_'),
|
|
109
82
|
projectNameCamel: options.projectName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
110
83
|
};
|
|
111
84
|
|
|
112
85
|
console.log(chalk.gray(`Using project name: ${options.projectName}`));
|
|
113
|
-
console.log(chalk.gray(`Using display name: ${projectDisplayName}
|
|
86
|
+
console.log(chalk.gray(`Using display name: ${projectDisplayName}`));
|
|
87
|
+
console.log(chalk.gray(`Using description: ${projectDescription}\n`));
|
|
114
88
|
} else {
|
|
115
89
|
// Get user inputs via interactive prompts
|
|
116
90
|
answers = await runInitPrompts();
|
|
117
91
|
}
|
|
118
92
|
|
|
119
93
|
// Get variant selections (multi-tenancy, B2B vs B2B2C)
|
|
120
|
-
|
|
94
|
+
let variantChoices;
|
|
95
|
+
|
|
96
|
+
// If both flags provided, skip variant prompts
|
|
97
|
+
if (options.tenancy && options.userModel) {
|
|
98
|
+
// Validate tenancy value
|
|
99
|
+
if (!['single', 'multi'].includes(options.tenancy)) {
|
|
100
|
+
throw new Error('Invalid --tenancy value. Must be "single" or "multi"');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Validate userModel value
|
|
104
|
+
if (!['b2b', 'b2b2c'].includes(options.userModel)) {
|
|
105
|
+
throw new Error('Invalid --user-model value. Must be "b2b" or "b2b2c"');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Convert short flag values to full variant names
|
|
109
|
+
const tenancyMap = {
|
|
110
|
+
'single': 'single-tenant',
|
|
111
|
+
'multi': 'multi-tenant'
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
variantChoices = {
|
|
115
|
+
tenancy: tenancyMap[options.tenancy],
|
|
116
|
+
userModel: options.userModel
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
console.log(chalk.gray(`Using tenancy: ${options.tenancy}`));
|
|
120
|
+
console.log(chalk.gray(`Using user model: ${options.userModel}\n`));
|
|
121
|
+
} else {
|
|
122
|
+
// Run interactive variant prompts
|
|
123
|
+
variantChoices = await runVariantPrompts();
|
|
124
|
+
}
|
|
125
|
+
|
|
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
|
|
135
|
+
if (variantChoices.userModel === 'b2b2c') {
|
|
136
|
+
requiredModules.push('customers-portal');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Determine template source (dev mode = local, production = cache)
|
|
140
|
+
let templateRoot;
|
|
141
|
+
|
|
142
|
+
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`));
|
|
146
|
+
} else {
|
|
147
|
+
// Production mode: Use cache
|
|
148
|
+
try {
|
|
149
|
+
templateRoot = await ensureCacheReady(requiredModules);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
121
155
|
|
|
122
156
|
// Generate project with variant selections
|
|
123
157
|
console.log(chalk.yellow('\n⚙️ Generating project...\n'));
|
|
124
|
-
await generateProject(answers, variantChoices);
|
|
158
|
+
await generateProject(answers, variantChoices, templateRoot);
|
|
125
159
|
|
|
126
160
|
console.log(chalk.green.bold('\n✅ Project generated successfully!\n'));
|
|
127
161
|
console.log(chalk.white('Next steps:'));
|
package/src/commands/service.js
CHANGED
|
@@ -7,6 +7,8 @@ const { SERVICE_REGISTRY } = require('../services/registry');
|
|
|
7
7
|
const { isLaunchFrameProject, getProjectConfig, updateProjectConfig, getPrimaryDomain } = require('../utils/project-helpers');
|
|
8
8
|
const { replaceVariables } = require('../utils/variable-replacer');
|
|
9
9
|
const { updateEnvFile } = require('../utils/env-generator');
|
|
10
|
+
const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
|
|
11
|
+
const { ensureCacheReady, getModulePath } = require('../utils/module-cache');
|
|
10
12
|
|
|
11
13
|
async function serviceAdd(serviceName) {
|
|
12
14
|
// STEP 1: Validation
|
|
@@ -59,7 +61,7 @@ async function serviceAdd(serviceName) {
|
|
|
59
61
|
process.exit(0);
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
// STEP 3:
|
|
64
|
+
// STEP 3: Get service files (from cache in production, local in dev)
|
|
63
65
|
const installPath = path.resolve(process.cwd(), serviceName);
|
|
64
66
|
|
|
65
67
|
if (fs.existsSync(installPath)) {
|
|
@@ -67,50 +69,59 @@ async function serviceAdd(serviceName) {
|
|
|
67
69
|
process.exit(1);
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
// Check if in development mode
|
|
71
|
-
const isDevMode = process.env.
|
|
72
|
+
// Check if in development mode
|
|
73
|
+
const isDevMode = process.env.LAUNCHFRAME_DEV === 'true';
|
|
74
|
+
|
|
75
|
+
let sourceDir;
|
|
72
76
|
|
|
73
77
|
if (isDevMode) {
|
|
74
78
|
// Local development: copy from launchframe-dev/modules directory
|
|
75
79
|
console.log(chalk.blue('\n[DEV MODE] Copying service from local directory...'));
|
|
76
|
-
|
|
80
|
+
sourceDir = path.resolve(__dirname, '../../../modules', serviceName);
|
|
77
81
|
|
|
78
82
|
if (!fs.existsSync(sourceDir)) {
|
|
79
83
|
console.error(chalk.red(`Error: Local service directory not found: ${sourceDir}`));
|
|
80
|
-
console.log('Make sure the service exists in the
|
|
84
|
+
console.log('Make sure the service exists in the modules directory');
|
|
81
85
|
process.exit(1);
|
|
82
86
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
console.log(chalk.green('✓ Service copied successfully'));
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.error(chalk.red('Failed to copy service directory'));
|
|
95
|
-
console.error(error.message);
|
|
87
|
+
} else {
|
|
88
|
+
// Production mode: Check access and use cache
|
|
89
|
+
console.log(chalk.blue('\n🔍 Checking repository access...'));
|
|
90
|
+
|
|
91
|
+
const accessCheck = await checkGitHubAccess();
|
|
92
|
+
|
|
93
|
+
if (!accessCheck.hasAccess) {
|
|
94
|
+
showAccessDeniedMessage();
|
|
96
95
|
process.exit(1);
|
|
97
96
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Replace {{GITHUB_ORG}} in repo URL with actual org from project config
|
|
103
|
-
const repoUrl = service.repoUrl.replace('{{GITHUB_ORG}}', projectConfig.githubOrg || 'launchframe');
|
|
104
|
-
|
|
97
|
+
|
|
98
|
+
console.log(chalk.green('✓ Repository access confirmed'));
|
|
99
|
+
|
|
105
100
|
try {
|
|
106
|
-
|
|
101
|
+
// Ensure cache has this service module
|
|
102
|
+
await ensureCacheReady([serviceName]);
|
|
103
|
+
sourceDir = getModulePath(serviceName);
|
|
107
104
|
} catch (error) {
|
|
108
|
-
console.error(chalk.red(
|
|
109
|
-
console.log('Make sure you have access to the repository');
|
|
105
|
+
console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
110
106
|
process.exit(1);
|
|
111
107
|
}
|
|
112
108
|
}
|
|
113
109
|
|
|
110
|
+
// Copy service from source to installation path
|
|
111
|
+
try {
|
|
112
|
+
await fs.copy(sourceDir, installPath, {
|
|
113
|
+
filter: (src) => {
|
|
114
|
+
const basename = path.basename(src);
|
|
115
|
+
return !['node_modules', '.git', '.next', 'dist', 'build', '.env'].includes(basename);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
console.log(chalk.green('✓ Service files copied successfully'));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(chalk.red('Failed to copy service directory'));
|
|
121
|
+
console.error(error.message);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
// STEP 4: Service-specific prompts (e.g., Airtable credentials)
|
|
115
126
|
console.log(chalk.blue('\nConfiguring service...'));
|
|
116
127
|
const envValues = await runServicePrompts(service);
|
|
@@ -124,7 +135,7 @@ async function serviceAdd(serviceName) {
|
|
|
124
135
|
'{{PROJECT_NAME_UPPER}}': projectName.toUpperCase().replace(/-/g, '_'),
|
|
125
136
|
'{{PROJECT_DISPLAY_NAME}}': projectConfig.projectDisplayName || projectName,
|
|
126
137
|
'{{PRIMARY_DOMAIN}}': getPrimaryDomain(projectConfig) || 'example.com',
|
|
127
|
-
'{{GITHUB_ORG}}': projectConfig.githubOrg || 'launchframe'
|
|
138
|
+
'{{GITHUB_ORG}}': projectConfig.deployment?.githubOrg || projectConfig.githubOrg || 'launchframe'
|
|
128
139
|
};
|
|
129
140
|
|
|
130
141
|
// Add service-specific env var values as {{VAR_NAME}} (with double curly braces)
|
|
@@ -335,7 +346,7 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
|
|
|
335
346
|
const definitions = {};
|
|
336
347
|
|
|
337
348
|
// Base configuration (docker-compose.yml)
|
|
338
|
-
if (serviceName === 'customers-
|
|
349
|
+
if (serviceName === 'customers-portal') {
|
|
339
350
|
definitions.base = {
|
|
340
351
|
service: `
|
|
341
352
|
# ---------------------------------------------------------------------------
|
|
@@ -380,12 +391,25 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
|
|
|
380
391
|
target: development
|
|
381
392
|
image: ${projectName}-${serviceName}:dev
|
|
382
393
|
restart: "no"
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
394
|
+
# File watching for automatic sync and rebuild
|
|
395
|
+
# - Code changes are synced automatically
|
|
396
|
+
# - package.json changes trigger rebuild + restart
|
|
397
|
+
develop:
|
|
398
|
+
watch:
|
|
399
|
+
- action: sync
|
|
400
|
+
path: ../${serviceName}
|
|
401
|
+
target: /app
|
|
402
|
+
ignore:
|
|
403
|
+
- node_modules/
|
|
404
|
+
- dist/
|
|
405
|
+
- .vitepress/dist/
|
|
406
|
+
- coverage/
|
|
407
|
+
- '*.log'
|
|
408
|
+
- .git/
|
|
409
|
+
- action: rebuild
|
|
410
|
+
path: ../${serviceName}/package.json
|
|
411
|
+
- action: rebuild
|
|
412
|
+
path: ../${serviceName}/package-lock.json
|
|
389
413
|
environment:
|
|
390
414
|
- NODE_ENV=development
|
|
391
415
|
ports:
|
|
@@ -395,7 +419,7 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
|
|
|
395
419
|
name: ${projectName}-${serviceName}-node-modules
|
|
396
420
|
`
|
|
397
421
|
};
|
|
398
|
-
} else if (serviceName === 'customers-
|
|
422
|
+
} else if (serviceName === 'customers-portal') {
|
|
399
423
|
definitions.dev = {
|
|
400
424
|
service: `
|
|
401
425
|
${serviceName}:
|
|
@@ -431,8 +455,8 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
|
|
|
431
455
|
}
|
|
432
456
|
|
|
433
457
|
// Production configuration (docker-compose.prod.yml)
|
|
434
|
-
const githubOrg = projectConfig.githubOrg || 'launchframe';
|
|
435
|
-
const primaryDomain = projectConfig.primaryDomain || 'example.com';
|
|
458
|
+
const githubOrg = projectConfig.deployment?.githubOrg || 'launchframe';
|
|
459
|
+
const primaryDomain = projectConfig.deployment?.primaryDomain || 'example.com';
|
|
436
460
|
|
|
437
461
|
if (serviceName === 'docs') {
|
|
438
462
|
definitions.prod = {
|
|
@@ -449,7 +473,7 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
|
|
|
449
473
|
`,
|
|
450
474
|
volumes: null // No volumes needed in prod config
|
|
451
475
|
};
|
|
452
|
-
} else if (serviceName === 'customers-
|
|
476
|
+
} else if (serviceName === 'customers-portal') {
|
|
453
477
|
definitions.prod = {
|
|
454
478
|
service: `
|
|
455
479
|
${serviceName}:
|
|
@@ -44,8 +44,8 @@ async function waitlistDeploy() {
|
|
|
44
44
|
process.exit(1);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const { vpsHost, vpsUser, ghcrToken, adminEmail } = config.deployment;
|
|
48
|
-
const { projectName
|
|
47
|
+
const { vpsHost, vpsUser, vpsAppFolder, ghcrToken, adminEmail, githubOrg } = config.deployment;
|
|
48
|
+
const { projectName } = config;
|
|
49
49
|
const projectRoot = process.cwd();
|
|
50
50
|
const waitlistPath = path.join(projectRoot, 'waitlist');
|
|
51
51
|
|
|
@@ -26,8 +26,7 @@ async function waitlistLogs() {
|
|
|
26
26
|
process.exit(1);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const { vpsHost, vpsUser } = config.deployment;
|
|
30
|
-
const { vpsAppFolder } = config;
|
|
29
|
+
const { vpsHost, vpsUser, vpsAppFolder } = config.deployment;
|
|
31
30
|
|
|
32
31
|
console.log(chalk.gray('Connecting to VPS and streaming logs...\n'));
|
|
33
32
|
console.log(chalk.gray('Press Ctrl+C to exit\n'));
|
|
@@ -9,11 +9,13 @@ const execAsync = promisify(exec);
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Start waitlist service locally
|
|
12
|
+
* @param {Object} flags - Command line flags
|
|
13
|
+
* @param {boolean} flags.build - Force rebuild of containers
|
|
12
14
|
*/
|
|
13
|
-
async function waitlistUp() {
|
|
15
|
+
async function waitlistUp(flags = {}) {
|
|
14
16
|
requireProject();
|
|
15
17
|
|
|
16
|
-
console.log(chalk.blue.bold('\n🚀 Starting Waitlist Service (Local)\n'));
|
|
18
|
+
console.log(chalk.blue.bold('\n🚀 Starting Waitlist Service with Watch (Local)\n'));
|
|
17
19
|
|
|
18
20
|
const config = getProjectConfig();
|
|
19
21
|
|
|
@@ -42,18 +44,46 @@ async function waitlistUp() {
|
|
|
42
44
|
process.exit(1);
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
// STEP
|
|
46
|
-
console.log(chalk.yellow('\n
|
|
47
|
+
// STEP 1b: Check Docker Compose version for watch support
|
|
48
|
+
console.log(chalk.yellow('\n🔍 Checking Docker Compose version...\n'));
|
|
47
49
|
|
|
48
|
-
const
|
|
50
|
+
const composeSpinner = ora('Verifying Docker Compose v2.22+...').start();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const { stdout: composeVersion } = await execAsync('docker compose version');
|
|
54
|
+
const versionMatch = composeVersion.match(/v?(\d+)\.(\d+)\.(\d+)/);
|
|
55
|
+
|
|
56
|
+
if (versionMatch) {
|
|
57
|
+
const [, major, minor] = versionMatch.map(Number);
|
|
58
|
+
|
|
59
|
+
if (major < 2 || (major === 2 && minor < 22)) {
|
|
60
|
+
composeSpinner.fail(`Docker Compose v${major}.${minor} is too old`);
|
|
61
|
+
console.log(chalk.red('\n❌ Docker Compose v2.22+ is required for watch support\n'));
|
|
62
|
+
console.log(chalk.gray('Please upgrade Docker Compose:'));
|
|
63
|
+
console.log(chalk.white(' https://docs.docker.com/compose/install/\n'));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
composeSpinner.succeed(`Docker Compose v${major}.${minor} (compatible)`);
|
|
67
|
+
} else {
|
|
68
|
+
composeSpinner.warn('Could not parse version, proceeding anyway...');
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
composeSpinner.warn('Could not detect Docker Compose version');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// STEP 2: Start waitlist with watch
|
|
75
|
+
console.log(chalk.yellow('\n🚀 Step 2: Starting waitlist with watch...\n'));
|
|
76
|
+
|
|
77
|
+
const buildFlag = flags.build ? '--build' : '';
|
|
78
|
+
const deploySpinner = ora('Starting waitlist with watch...').start();
|
|
49
79
|
|
|
50
80
|
try {
|
|
51
81
|
await execAsync(
|
|
52
|
-
`cd ${waitlistPath} && docker-compose -f docker-compose.waitlist.yml -f docker-compose.waitlist.dev.yml
|
|
82
|
+
`cd ${waitlistPath} && docker-compose -f docker-compose.waitlist.yml -f docker-compose.waitlist.dev.yml watch ${buildFlag}`.trim(),
|
|
53
83
|
{ timeout: 180000 } // 3 minutes
|
|
54
84
|
);
|
|
55
85
|
|
|
56
|
-
deploySpinner.succeed('Waitlist started
|
|
86
|
+
deploySpinner.succeed('Waitlist started with watch');
|
|
57
87
|
} catch (error) {
|
|
58
88
|
deploySpinner.fail('Failed to start waitlist');
|
|
59
89
|
console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
@@ -78,18 +108,23 @@ async function waitlistUp() {
|
|
|
78
108
|
}
|
|
79
109
|
|
|
80
110
|
// Success!
|
|
81
|
-
console.log(chalk.green.bold('\n✅ Waitlist
|
|
111
|
+
console.log(chalk.green.bold('\n✅ Waitlist started with watch mode!\n'));
|
|
112
|
+
|
|
113
|
+
console.log(chalk.yellow('📺 Watching for file changes (press Ctrl+C to stop)...\n'));
|
|
114
|
+
|
|
115
|
+
console.log(chalk.white('Watch behavior:'));
|
|
116
|
+
console.log(chalk.gray(' • Code changes → Auto-sync to container'));
|
|
117
|
+
console.log(chalk.gray(' • package.json → Auto-rebuild & restart\n'));
|
|
82
118
|
|
|
83
119
|
console.log(chalk.white('Your waitlist landing page is available at:\n'));
|
|
84
120
|
console.log(chalk.cyan(` 🌍 Waitlist: http://localhost:3002`));
|
|
85
|
-
console.log(chalk.gray(` ✓ Running in development mode with
|
|
86
|
-
console.log(chalk.gray(` ✓ Source code mounted from ./waitlist/src\n`));
|
|
87
|
-
|
|
88
|
-
console.log(chalk.white('Monitor waitlist:'));
|
|
89
|
-
console.log(chalk.gray(` launchframe waitlist:logs\n`));
|
|
121
|
+
console.log(chalk.gray(` ✓ Running in development mode with file watching\n`));
|
|
90
122
|
|
|
91
|
-
console.log(chalk.white('
|
|
92
|
-
console.log(chalk.gray(
|
|
123
|
+
console.log(chalk.white('To stop:'));
|
|
124
|
+
console.log(chalk.gray(' Press Ctrl+C in this terminal'));
|
|
125
|
+
console.log(chalk.gray(' Or run: launchframe waitlist:down\n'));
|
|
126
|
+
|
|
127
|
+
console.log(chalk.cyan('💡 Tip (Linux/Mac): Add & at the end to run in background\n'));
|
|
93
128
|
}
|
|
94
129
|
|
|
95
130
|
module.exports = { waitlistUp };
|