@launchframe/cli 0.1.11 ā 1.0.0-beta.10
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 +44 -9
- package/package.json +5 -5
- package/src/commands/cache.js +102 -0
- package/src/commands/deploy-configure.js +30 -4
- package/src/commands/deploy-init.js +38 -56
- 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 +90 -63
- package/src/commands/service.js +71 -41
- 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 +16 -7
- package/src/index.js +18 -11
- package/src/prompts.js +12 -0
- package/src/services/registry.js +8 -6
- package/src/services/variant-config.js +146 -48
- package/src/utils/docker-helper.js +66 -44
- package/src/utils/github-access.js +67 -0
- package/src/utils/project-helpers.js +6 -2
- package/src/utils/section-replacer.js +32 -15
- package/src/utils/service-cache.js +274 -0
- package/src/utils/variable-replacer.js +7 -2
- package/src/utils/variant-processor.js +24 -12
|
@@ -47,36 +47,66 @@ async function dockerDestroy(options = {}) {
|
|
|
47
47
|
|
|
48
48
|
console.log(chalk.yellow('\nšļø Destroying Docker resources...\n'));
|
|
49
49
|
|
|
50
|
-
// Step 1: Stop
|
|
51
|
-
console.log(chalk.gray('Stopping
|
|
50
|
+
// Step 1: Stop all running containers first
|
|
51
|
+
console.log(chalk.gray('Stopping running containers...'));
|
|
52
52
|
try {
|
|
53
|
-
execSync(`docker ps
|
|
53
|
+
const runningContainerIds = execSync(`docker ps --filter "name=${projectName}" -q`, { encoding: 'utf8' }).trim();
|
|
54
|
+
if (runningContainerIds) {
|
|
55
|
+
const ids = runningContainerIds.replace(/\n/g, ' ');
|
|
56
|
+
// Use pipe mode instead of inherit to avoid Windows stdio issues
|
|
57
|
+
const output = execSync(`docker stop ${ids}`, { encoding: 'utf8' });
|
|
58
|
+
if (output) console.log(output);
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Ignore errors if no running containers
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Step 2: Remove all containers (running and stopped)
|
|
65
|
+
console.log(chalk.gray('Removing containers...'));
|
|
66
|
+
try {
|
|
67
|
+
const containerIds = execSync(`docker ps -a --filter "name=${projectName}" -q`, { encoding: 'utf8' }).trim();
|
|
68
|
+
if (containerIds) {
|
|
69
|
+
const ids = containerIds.replace(/\n/g, ' ');
|
|
70
|
+
const output = execSync(`docker rm -f ${ids}`, { encoding: 'utf8' });
|
|
71
|
+
if (output) console.log(output);
|
|
72
|
+
}
|
|
54
73
|
} catch (error) {
|
|
55
74
|
// Ignore errors if no containers found
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
// Step
|
|
59
|
-
console.log(chalk.gray('Removing
|
|
77
|
+
// Step 3: Remove network (must be after containers are removed)
|
|
78
|
+
console.log(chalk.gray('Removing network...'));
|
|
60
79
|
try {
|
|
61
|
-
execSync(`docker
|
|
80
|
+
const output = execSync(`docker network rm ${projectName}-network`, { encoding: 'utf8' });
|
|
81
|
+
if (output) console.log(output);
|
|
62
82
|
} catch (error) {
|
|
63
|
-
// Ignore errors if
|
|
83
|
+
// Ignore errors if network doesn't exist or has active endpoints
|
|
64
84
|
}
|
|
65
85
|
|
|
66
|
-
// Step
|
|
67
|
-
console.log(chalk.gray('Removing
|
|
86
|
+
// Step 4: Remove all volumes (must be after containers are removed)
|
|
87
|
+
console.log(chalk.gray('Removing volumes...'));
|
|
68
88
|
try {
|
|
69
|
-
execSync(`docker
|
|
89
|
+
const volumeIds = execSync(`docker volume ls --filter "name=${projectName}" -q`, { encoding: 'utf8' }).trim();
|
|
90
|
+
if (volumeIds) {
|
|
91
|
+
const ids = volumeIds.replace(/\n/g, ' ');
|
|
92
|
+
const output = execSync(`docker volume rm ${ids}`, { encoding: 'utf8' });
|
|
93
|
+
if (output) console.log(output);
|
|
94
|
+
}
|
|
70
95
|
} catch (error) {
|
|
71
|
-
// Ignore errors if no
|
|
96
|
+
// Ignore errors if no volumes found
|
|
72
97
|
}
|
|
73
98
|
|
|
74
|
-
// Step
|
|
75
|
-
console.log(chalk.gray('Removing
|
|
99
|
+
// Step 5: Remove all images (do this last)
|
|
100
|
+
console.log(chalk.gray('Removing images...'));
|
|
76
101
|
try {
|
|
77
|
-
execSync(`docker
|
|
102
|
+
const imageIds = execSync(`docker images --filter "reference=${projectName}*" -q`, { encoding: 'utf8' }).trim();
|
|
103
|
+
if (imageIds) {
|
|
104
|
+
const ids = imageIds.replace(/\n/g, ' ');
|
|
105
|
+
const output = execSync(`docker rmi -f ${ids}`, { encoding: 'utf8' });
|
|
106
|
+
if (output) console.log(output);
|
|
107
|
+
}
|
|
78
108
|
} catch (error) {
|
|
79
|
-
// Ignore errors if
|
|
109
|
+
// Ignore errors if no images found
|
|
80
110
|
}
|
|
81
111
|
|
|
82
112
|
console.log(chalk.green.bold('\nā
All Docker resources destroyed successfully!\n'));
|
|
@@ -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 services'));
|
|
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 services'));
|
|
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,82 +3,45 @@ const path = require('path');
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { runInitPrompts, runVariantPrompts } = require('../prompts');
|
|
5
5
|
const { generateProject } = require('../generator');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
function isLaunchFrameProject() {
|
|
11
|
-
const markerPath = path.join(process.cwd(), '.launchframe');
|
|
12
|
-
return fs.existsSync(markerPath);
|
|
13
|
-
}
|
|
6
|
+
const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
|
|
7
|
+
const { ensureCacheReady } = require('../utils/service-cache');
|
|
8
|
+
const { isLaunchFrameProject } = require('../utils/project-helpers');
|
|
14
9
|
|
|
15
10
|
/**
|
|
16
11
|
* Check if running in development mode (local) vs production (npm install)
|
|
17
12
|
*/
|
|
18
13
|
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;
|
|
14
|
+
// Only use dev mode if LAUNCHFRAME_DEV is explicitly set to 'true'
|
|
15
|
+
return process.env.LAUNCHFRAME_DEV === 'true';
|
|
44
16
|
}
|
|
45
17
|
|
|
46
18
|
/**
|
|
47
19
|
* Initialize a new LaunchFrame project
|
|
48
20
|
* @param {Object} options - Command options
|
|
49
21
|
* @param {string} options.projectName - Project name (skips prompt if provided)
|
|
22
|
+
* @param {string} options.tenancy - Tenancy model: 'single' or 'multi' (skips prompt if provided)
|
|
23
|
+
* @param {string} options.userModel - User model: 'b2b' or 'b2b2c' (skips prompt if provided)
|
|
50
24
|
*/
|
|
51
25
|
async function init(options = {}) {
|
|
52
26
|
console.log(chalk.blue.bold('\nš Welcome to LaunchFrame!\n'));
|
|
53
27
|
|
|
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;
|
|
28
|
+
// Check if in development mode
|
|
29
|
+
const devMode = isDevMode();
|
|
30
|
+
|
|
31
|
+
if (!devMode) {
|
|
32
|
+
// Production mode: Check GitHub access
|
|
33
|
+
console.log(chalk.blue('š Checking repository access...\n'));
|
|
34
|
+
|
|
35
|
+
const accessCheck = await checkGitHubAccess();
|
|
36
|
+
|
|
37
|
+
if (!accessCheck.hasAccess) {
|
|
38
|
+
// No access - show purchase/setup message
|
|
39
|
+
showAccessDeniedMessage();
|
|
40
|
+
process.exit(1); // Exit with error code
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(chalk.green('ā Repository access confirmed\n'));
|
|
79
44
|
}
|
|
80
|
-
|
|
81
|
-
// Dev mode - continue with normal project generation
|
|
82
45
|
// Check if already in a LaunchFrame project
|
|
83
46
|
if (isLaunchFrameProject()) {
|
|
84
47
|
console.error(chalk.red('\nā Error: Already in a LaunchFrame project'));
|
|
@@ -102,26 +65,90 @@ async function init(options = {}) {
|
|
|
102
65
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
103
66
|
.join(' ');
|
|
104
67
|
|
|
68
|
+
const projectDescription = `${projectDisplayName} - Modern SaaS Platform`;
|
|
69
|
+
|
|
105
70
|
answers = {
|
|
106
71
|
projectName: options.projectName,
|
|
107
72
|
projectDisplayName: projectDisplayName,
|
|
73
|
+
projectDescription: projectDescription,
|
|
108
74
|
projectNameUpper: options.projectName.toUpperCase().replace(/-/g, '_'),
|
|
109
75
|
projectNameCamel: options.projectName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
110
76
|
};
|
|
111
77
|
|
|
112
78
|
console.log(chalk.gray(`Using project name: ${options.projectName}`));
|
|
113
|
-
console.log(chalk.gray(`Using display name: ${projectDisplayName}
|
|
79
|
+
console.log(chalk.gray(`Using display name: ${projectDisplayName}`));
|
|
80
|
+
console.log(chalk.gray(`Using description: ${projectDescription}\n`));
|
|
114
81
|
} else {
|
|
115
82
|
// Get user inputs via interactive prompts
|
|
116
83
|
answers = await runInitPrompts();
|
|
117
84
|
}
|
|
118
85
|
|
|
119
86
|
// Get variant selections (multi-tenancy, B2B vs B2B2C)
|
|
120
|
-
|
|
87
|
+
let variantChoices;
|
|
88
|
+
|
|
89
|
+
// If both flags provided, skip variant prompts
|
|
90
|
+
if (options.tenancy && options.userModel) {
|
|
91
|
+
// Validate tenancy value
|
|
92
|
+
if (!['single', 'multi'].includes(options.tenancy)) {
|
|
93
|
+
throw new Error('Invalid --tenancy value. Must be "single" or "multi"');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate userModel value
|
|
97
|
+
if (!['b2b', 'b2b2c'].includes(options.userModel)) {
|
|
98
|
+
throw new Error('Invalid --user-model value. Must be "b2b" or "b2b2c"');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Convert short flag values to full variant names
|
|
102
|
+
const tenancyMap = {
|
|
103
|
+
'single': 'single-tenant',
|
|
104
|
+
'multi': 'multi-tenant'
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
variantChoices = {
|
|
108
|
+
tenancy: tenancyMap[options.tenancy],
|
|
109
|
+
userModel: options.userModel
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
console.log(chalk.gray(`Using tenancy: ${options.tenancy}`));
|
|
113
|
+
console.log(chalk.gray(`Using user model: ${options.userModel}\n`));
|
|
114
|
+
} else {
|
|
115
|
+
// Run interactive variant prompts
|
|
116
|
+
variantChoices = await runVariantPrompts();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Determine which services are needed based on variant choices
|
|
120
|
+
const requiredServices = [
|
|
121
|
+
'backend',
|
|
122
|
+
'admin-portal',
|
|
123
|
+
'infrastructure',
|
|
124
|
+
'website'
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
// Add customers-portal only if B2B2C mode
|
|
128
|
+
if (variantChoices.userModel === 'b2b2c') {
|
|
129
|
+
requiredServices.push('customers-portal');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Determine template source (dev mode = local, production = cache)
|
|
133
|
+
let templateRoot;
|
|
134
|
+
|
|
135
|
+
if (devMode) {
|
|
136
|
+
// Dev mode: Use local services directory
|
|
137
|
+
templateRoot = path.resolve(__dirname, '../../../services');
|
|
138
|
+
console.log(chalk.gray(`[DEV MODE] Using local services: ${templateRoot}\n`));
|
|
139
|
+
} else {
|
|
140
|
+
// Production mode: Use cache
|
|
141
|
+
try {
|
|
142
|
+
templateRoot = await ensureCacheReady(requiredServices);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error(chalk.red(`\nā Error: ${error.message}\n`));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
121
148
|
|
|
122
149
|
// Generate project with variant selections
|
|
123
150
|
console.log(chalk.yellow('\nāļø Generating project...\n'));
|
|
124
|
-
await generateProject(answers, variantChoices);
|
|
151
|
+
await generateProject(answers, variantChoices, templateRoot);
|
|
125
152
|
|
|
126
153
|
console.log(chalk.green.bold('\nā
Project generated successfully!\n'));
|
|
127
154
|
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, getServicePath } = require('../utils/service-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
|
-
// Local development: copy from launchframe-dev/
|
|
78
|
+
// Local development: copy from launchframe-dev/services directory
|
|
75
79
|
console.log(chalk.blue('\n[DEV MODE] Copying service from local directory...'));
|
|
76
|
-
|
|
80
|
+
sourceDir = path.resolve(__dirname, '../../../services', 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 services 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
|
|
102
|
+
await ensureCacheReady([serviceName]);
|
|
103
|
+
sourceDir = getServicePath(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 = {
|
|
@@ -440,6 +464,12 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
|
|
|
440
464
|
${serviceName}:
|
|
441
465
|
image: ghcr.io/${githubOrg}/${projectName}-${serviceName}:latest
|
|
442
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
|
|
443
473
|
labels:
|
|
444
474
|
- "traefik.enable=true"
|
|
445
475
|
- "traefik.http.routers.${serviceName}.rule=Host(\`${serviceName}.\${PRIMARY_DOMAIN}\`)"
|
|
@@ -449,7 +479,7 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
|
|
|
449
479
|
`,
|
|
450
480
|
volumes: null // No volumes needed in prod config
|
|
451
481
|
};
|
|
452
|
-
} else if (serviceName === 'customers-
|
|
482
|
+
} else if (serviceName === 'customers-portal') {
|
|
453
483
|
definitions.prod = {
|
|
454
484
|
service: `
|
|
455
485
|
${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'));
|