@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.
- package/.claude/settings.local.json +12 -0
- package/CLAUDE.md +27 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/LICENSE +21 -0
- package/README.md +7 -1
- package/package.json +9 -6
- package/src/commands/database-console.js +84 -0
- package/src/commands/deploy-build.js +76 -0
- package/src/commands/deploy-configure.js +10 -3
- package/src/commands/deploy-init.js +24 -57
- package/src/commands/deploy-set-env.js +17 -7
- package/src/commands/deploy-sync-features.js +233 -0
- package/src/commands/deploy-up.js +4 -3
- package/src/commands/dev-add-user.js +165 -0
- package/src/commands/dev-logo.js +160 -0
- package/src/commands/dev-npm-install.js +33 -0
- package/src/commands/dev-queue.js +85 -0
- package/src/commands/docker-build.js +9 -6
- package/src/commands/help.js +33 -9
- package/src/commands/init.js +44 -52
- package/src/commands/migration-create.js +40 -0
- package/src/commands/migration-revert.js +32 -0
- package/src/commands/migration-run.js +32 -0
- package/src/commands/module.js +146 -0
- package/src/commands/waitlist-deploy.js +1 -0
- package/src/generator.js +41 -40
- package/src/index.js +109 -4
- package/src/services/module-config.js +25 -0
- package/src/services/module-registry.js +12 -0
- package/src/services/variant-config.js +24 -13
- 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 +15 -13
- package/src/utils/logger.js +93 -0
- package/src/utils/module-installer.js +58 -0
- package/src/utils/project-helpers.js +34 -1
- package/src/utils/service-cache.js +12 -18
- package/src/utils/ssh-helper.js +51 -1
- package/src/utils/telemetry.js +238 -0
- package/src/utils/variable-replacer.js +18 -23
- package/src/utils/variant-processor.js +35 -42
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { spawnSync, spawn } = require('child_process');
|
|
5
|
+
const { requireProject } = require('../utils/project-helpers');
|
|
6
|
+
|
|
7
|
+
async function devQueue() {
|
|
8
|
+
requireProject();
|
|
9
|
+
|
|
10
|
+
const infrastructurePath = path.join(process.cwd(), 'infrastructure');
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(infrastructurePath)) {
|
|
13
|
+
console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
|
|
14
|
+
console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check backend container is running
|
|
19
|
+
const psResult = spawnSync(
|
|
20
|
+
'docker',
|
|
21
|
+
[
|
|
22
|
+
'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
|
|
23
|
+
'ps', '--status', 'running', '-q', 'backend'
|
|
24
|
+
],
|
|
25
|
+
{ cwd: infrastructurePath, encoding: 'utf8' }
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (!psResult.stdout || psResult.stdout.trim() === '') {
|
|
29
|
+
console.error(chalk.red('\n❌ Backend container is not running.'));
|
|
30
|
+
console.log(chalk.gray('Start local services first:'));
|
|
31
|
+
console.log(chalk.white(' launchframe docker:up\n'));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Read BULL_ADMIN_TOKEN from backend container
|
|
36
|
+
const tokenResult = spawnSync(
|
|
37
|
+
'docker',
|
|
38
|
+
[
|
|
39
|
+
'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
|
|
40
|
+
'exec', '-T', 'backend', 'sh', '-c', 'echo $BULL_ADMIN_TOKEN'
|
|
41
|
+
],
|
|
42
|
+
{ cwd: infrastructurePath, encoding: 'utf8' }
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const token = (tokenResult.stdout || '').trim();
|
|
46
|
+
|
|
47
|
+
if (!token) {
|
|
48
|
+
console.error(chalk.red('\n❌ Could not read BULL_ADMIN_TOKEN from backend container.'));
|
|
49
|
+
console.log(chalk.gray('Make sure the backend is fully started and BULL_ADMIN_TOKEN is set in your .env\n'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Read BACKEND_PORT from backend container (fallback to 4000)
|
|
54
|
+
const portResult = spawnSync(
|
|
55
|
+
'docker',
|
|
56
|
+
[
|
|
57
|
+
'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
|
|
58
|
+
'exec', '-T', 'backend', 'sh', '-c', 'echo $BACKEND_PORT'
|
|
59
|
+
],
|
|
60
|
+
{ cwd: infrastructurePath, encoding: 'utf8' }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const port = (portResult.stdout || '').trim() || '4000';
|
|
64
|
+
|
|
65
|
+
const url = `http://localhost:${port}/admin/queues/${token}`;
|
|
66
|
+
|
|
67
|
+
console.log(chalk.green('\n Opening Bull queue dashboard...'));
|
|
68
|
+
console.log(chalk.gray(` ${url}\n`));
|
|
69
|
+
|
|
70
|
+
// Open URL with platform-appropriate command
|
|
71
|
+
const platform = process.platform;
|
|
72
|
+
let openCmd;
|
|
73
|
+
if (platform === 'darwin') {
|
|
74
|
+
openCmd = 'open';
|
|
75
|
+
} else if (platform === 'win32') {
|
|
76
|
+
openCmd = 'start';
|
|
77
|
+
} else {
|
|
78
|
+
openCmd = 'xdg-open';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const child = spawn(openCmd, [url], { detached: true, stdio: 'ignore' });
|
|
82
|
+
child.unref();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { devQueue };
|
|
@@ -6,8 +6,9 @@ const { requireProject } = require('../utils/project-helpers');
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Build Docker images for the project
|
|
9
|
+
* @param {string} [serviceName] - Optional service to build (builds all if omitted)
|
|
9
10
|
*/
|
|
10
|
-
async function dockerBuild() {
|
|
11
|
+
async function dockerBuild(serviceName) {
|
|
11
12
|
requireProject();
|
|
12
13
|
|
|
13
14
|
const infrastructurePath = path.join(process.cwd(), 'infrastructure');
|
|
@@ -18,11 +19,11 @@ async function dockerBuild() {
|
|
|
18
19
|
process.exit(1);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
console.log(chalk.
|
|
22
|
+
const target = serviceName ? `${serviceName} service` : 'all services';
|
|
23
|
+
console.log(chalk.blue.bold(`\n🔨 Building Docker Images (${target})\n`));
|
|
23
24
|
|
|
24
25
|
try {
|
|
25
|
-
const buildCommand =
|
|
26
|
+
const buildCommand = `docker-compose -f docker-compose.yml -f docker-compose.dev.yml build${serviceName ? ` ${serviceName}` : ''}`;
|
|
26
27
|
|
|
27
28
|
console.log(chalk.gray(`Running: ${buildCommand}\n`));
|
|
28
29
|
|
|
@@ -32,8 +33,10 @@ async function dockerBuild() {
|
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
console.log(chalk.green.bold('\n✅ Docker images built successfully!\n'));
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
if (!serviceName) {
|
|
37
|
+
console.log(chalk.white('Next steps:'));
|
|
38
|
+
console.log(chalk.gray(' launchframe docker:up # Start all services\n'));
|
|
39
|
+
}
|
|
37
40
|
|
|
38
41
|
} catch (error) {
|
|
39
42
|
console.error(chalk.red('\n❌ Error during build:'), error.message);
|
package/src/commands/help.js
CHANGED
|
@@ -7,16 +7,21 @@ const { isLaunchFrameProject, isWaitlistInstalled } = require('../utils/project-
|
|
|
7
7
|
function help() {
|
|
8
8
|
const inProject = isLaunchFrameProject();
|
|
9
9
|
|
|
10
|
-
console.log(chalk.blue.bold('\
|
|
10
|
+
console.log(chalk.blue.bold('\nLaunchFrame CLI\n'));
|
|
11
11
|
console.log(chalk.white('Usage:'));
|
|
12
|
-
console.log(chalk.gray(' launchframe [command]\n'));
|
|
12
|
+
console.log(chalk.gray(' launchframe [command] [options]\n'));
|
|
13
|
+
console.log(chalk.white('Global options:'));
|
|
14
|
+
console.log(chalk.gray(' --verbose, -v Show detailed output'));
|
|
15
|
+
console.log(chalk.gray(' --version Show version number\n'));
|
|
13
16
|
|
|
14
17
|
if (inProject) {
|
|
15
18
|
console.log(chalk.white('Deployment commands:'));
|
|
16
19
|
console.log(chalk.gray(' deploy:configure Configure production deployment settings'));
|
|
17
20
|
console.log(chalk.gray(' deploy:set-env Configure production environment variables'));
|
|
18
21
|
console.log(chalk.gray(' deploy:init Initialize VPS and build Docker images'));
|
|
19
|
-
console.log(chalk.gray(' deploy:up Start services on VPS
|
|
22
|
+
console.log(chalk.gray(' deploy:up Start services on VPS'));
|
|
23
|
+
console.log(chalk.gray(' deploy:build [service] Build, push, and deploy (all or specific service)'));
|
|
24
|
+
console.log(chalk.gray(' deploy:sync-features Sync local features and plan assignments to production\n'));
|
|
20
25
|
|
|
21
26
|
// Conditionally show waitlist commands
|
|
22
27
|
if (isWaitlistInstalled()) {
|
|
@@ -33,18 +38,37 @@ function help() {
|
|
|
33
38
|
console.log(chalk.gray(' docker:logs [service] View logs from all services or specific service'));
|
|
34
39
|
console.log(chalk.gray(' docker:destroy Remove all resources (containers, volumes, images)'));
|
|
35
40
|
console.log(chalk.gray(' --force, -f Skip confirmation prompt\n'));
|
|
41
|
+
console.log(chalk.white('Database Migrations:'));
|
|
42
|
+
console.log(chalk.gray(' migration:run Run pending database migrations'));
|
|
43
|
+
console.log(chalk.gray(' migration:create Create new database migration'));
|
|
44
|
+
console.log(chalk.gray(' migration:revert Revert last database migration\n'));
|
|
45
|
+
console.log(chalk.white('Database:'));
|
|
46
|
+
console.log(chalk.gray(' database:console Open a PostgreSQL console (local)'));
|
|
47
|
+
console.log(chalk.gray(' --remote Connect to the production database\n'));
|
|
48
|
+
|
|
36
49
|
console.log(chalk.white('Service Management:'));
|
|
37
50
|
console.log(chalk.gray(' service:add <name> Add an optional service to your project'));
|
|
38
51
|
console.log(chalk.gray(' service:list List available services'));
|
|
39
52
|
console.log(chalk.gray(' service:remove <name> Remove installed service\n'));
|
|
53
|
+
console.log(chalk.white('Module Management:'));
|
|
54
|
+
console.log(chalk.gray(' module:add <name> Add a module to your project'));
|
|
55
|
+
console.log(chalk.gray(' module:list List available modules\n'));
|
|
40
56
|
console.log(chalk.white('Available Services:'));
|
|
41
57
|
console.log(chalk.gray(' waitlist Coming soon page with email collection\n'));
|
|
42
58
|
console.log(chalk.white('Cache Management:'));
|
|
43
59
|
console.log(chalk.gray(' cache:info Show cache location, size, and cached services'));
|
|
44
60
|
console.log(chalk.gray(' cache:update Force update cache to latest version'));
|
|
45
61
|
console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
|
|
62
|
+
console.log(chalk.white('Dev Helpers:'));
|
|
63
|
+
console.log(chalk.gray(' dev:add-user Generate and insert a random test user into the local database'));
|
|
64
|
+
console.log(chalk.gray(' dev:queue Open the Bull queue dashboard in the browser'));
|
|
65
|
+
console.log(chalk.gray(' dev:logo Generate and inject favicon/logo assets from logo.svg'));
|
|
66
|
+
console.log(chalk.gray(' dev:npm-install <service> [packages...] Install npm packages using node:20-alpine\n'));
|
|
46
67
|
console.log(chalk.white('Other commands:'));
|
|
47
68
|
console.log(chalk.gray(' doctor Check project health and configuration'));
|
|
69
|
+
console.log(chalk.gray(' telemetry Show telemetry status'));
|
|
70
|
+
console.log(chalk.gray(' --enable Enable anonymous usage data collection'));
|
|
71
|
+
console.log(chalk.gray(' --disable Disable anonymous usage data collection'));
|
|
48
72
|
console.log(chalk.gray(' help Show this help message\n'));
|
|
49
73
|
console.log(chalk.white('Examples:'));
|
|
50
74
|
console.log(chalk.gray(' # Deploy full app to production'));
|
|
@@ -70,19 +94,19 @@ function help() {
|
|
|
70
94
|
} else {
|
|
71
95
|
console.log(chalk.white('Available commands:'));
|
|
72
96
|
console.log(chalk.gray(' init Initialize a new LaunchFrame project'));
|
|
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)'));
|
|
76
97
|
console.log(chalk.gray(' help Show this help message\n'));
|
|
98
|
+
console.log(chalk.white('Telemetry:'));
|
|
99
|
+
console.log(chalk.gray(' telemetry Show telemetry status'));
|
|
100
|
+
console.log(chalk.gray(' --enable Enable anonymous usage data collection'));
|
|
101
|
+
console.log(chalk.gray(' --disable Disable anonymous usage data collection\n'));
|
|
77
102
|
console.log(chalk.white('Cache Management:'));
|
|
78
103
|
console.log(chalk.gray(' cache:info Show cache location, size, and cached services'));
|
|
79
104
|
console.log(chalk.gray(' cache:update Force update cache to latest version'));
|
|
80
105
|
console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
|
|
81
106
|
console.log(chalk.white('Examples:'));
|
|
82
|
-
console.log(chalk.gray(' # Interactive mode'));
|
|
83
107
|
console.log(chalk.gray(' launchframe init\n'));
|
|
84
|
-
console.log(chalk.gray(' #
|
|
85
|
-
console.log(chalk.gray(' launchframe init --
|
|
108
|
+
console.log(chalk.gray(' # With verbose output'));
|
|
109
|
+
console.log(chalk.gray(' launchframe init --verbose\n'));
|
|
86
110
|
}
|
|
87
111
|
}
|
|
88
112
|
|
package/src/commands/init.js
CHANGED
|
@@ -6,6 +6,8 @@ const { generateProject } = require('../generator');
|
|
|
6
6
|
const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
|
|
7
7
|
const { ensureCacheReady } = require('../utils/service-cache');
|
|
8
8
|
const { isLaunchFrameProject } = require('../utils/project-helpers');
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
const { trackEvent } = require('../utils/telemetry');
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Check if running in development mode (local) vs production (npm install)
|
|
@@ -23,28 +25,30 @@ function isDevMode() {
|
|
|
23
25
|
* @param {string} options.userModel - User model: 'b2b' or 'b2b2c' (skips prompt if provided)
|
|
24
26
|
*/
|
|
25
27
|
async function init(options = {}) {
|
|
26
|
-
console.log(chalk.blue.bold('\n
|
|
28
|
+
console.log(chalk.blue.bold('\nLaunchFrame\n'));
|
|
27
29
|
|
|
28
30
|
// Check if in development mode
|
|
29
31
|
const devMode = isDevMode();
|
|
30
|
-
|
|
32
|
+
|
|
31
33
|
if (!devMode) {
|
|
32
34
|
// Production mode: Check GitHub access
|
|
33
|
-
console.log(chalk.
|
|
34
|
-
|
|
35
|
+
console.log(chalk.gray('Checking repository access...'));
|
|
36
|
+
|
|
35
37
|
const accessCheck = await checkGitHubAccess();
|
|
36
|
-
|
|
38
|
+
|
|
37
39
|
if (!accessCheck.hasAccess) {
|
|
38
|
-
// No access - show purchase/setup message
|
|
39
40
|
showAccessDeniedMessage();
|
|
40
|
-
|
|
41
|
+
trackEvent('command_executed', { command: 'init', success: false, error_message: 'access_denied' });
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
43
|
+
process.exit(1);
|
|
41
44
|
}
|
|
42
|
-
|
|
43
|
-
console.log(chalk.green('
|
|
45
|
+
|
|
46
|
+
console.log(chalk.green('Repository access confirmed\n'));
|
|
44
47
|
}
|
|
48
|
+
|
|
45
49
|
// Check if already in a LaunchFrame project
|
|
46
50
|
if (isLaunchFrameProject()) {
|
|
47
|
-
console.error(chalk.red('
|
|
51
|
+
console.error(chalk.red('Error: Already in a LaunchFrame project'));
|
|
48
52
|
console.log(chalk.gray('Use other commands to manage your project, or run init from outside the project.\n'));
|
|
49
53
|
process.exit(1);
|
|
50
54
|
}
|
|
@@ -54,12 +58,10 @@ async function init(options = {}) {
|
|
|
54
58
|
|
|
55
59
|
// If project name provided via flag, skip prompts
|
|
56
60
|
if (options.projectName) {
|
|
57
|
-
// Validate project name format
|
|
58
61
|
if (!/^[a-z0-9-]+$/.test(options.projectName)) {
|
|
59
62
|
throw new Error('Project name must contain only lowercase letters, numbers, and hyphens');
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
// Auto-generate display name from slug
|
|
63
65
|
const projectDisplayName = options.projectName
|
|
64
66
|
.split('-')
|
|
65
67
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
@@ -75,91 +77,81 @@ async function init(options = {}) {
|
|
|
75
77
|
projectNameCamel: options.projectName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
76
78
|
};
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
logger.detail(`Project name: ${options.projectName}`);
|
|
81
|
+
logger.detail(`Display name: ${projectDisplayName}`);
|
|
82
|
+
logger.detail(`Description: ${projectDescription}`);
|
|
81
83
|
} else {
|
|
82
|
-
// Get user inputs via interactive prompts
|
|
83
84
|
answers = await runInitPrompts();
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
// Get variant selections (multi-tenancy, B2B vs B2B2C)
|
|
87
88
|
let variantChoices;
|
|
88
|
-
|
|
89
|
-
// If both flags provided, skip variant prompts
|
|
89
|
+
|
|
90
90
|
if (options.tenancy && options.userModel) {
|
|
91
|
-
// Validate tenancy value
|
|
92
91
|
if (!['single', 'multi'].includes(options.tenancy)) {
|
|
93
92
|
throw new Error('Invalid --tenancy value. Must be "single" or "multi"');
|
|
94
93
|
}
|
|
95
|
-
|
|
96
|
-
// Validate userModel value
|
|
94
|
+
|
|
97
95
|
if (!['b2b', 'b2b2c'].includes(options.userModel)) {
|
|
98
96
|
throw new Error('Invalid --user-model value. Must be "b2b" or "b2b2c"');
|
|
99
97
|
}
|
|
100
|
-
|
|
101
|
-
// Convert short flag values to full variant names
|
|
98
|
+
|
|
102
99
|
const tenancyMap = {
|
|
103
100
|
'single': 'single-tenant',
|
|
104
101
|
'multi': 'multi-tenant'
|
|
105
102
|
};
|
|
106
|
-
|
|
103
|
+
|
|
107
104
|
variantChoices = {
|
|
108
105
|
tenancy: tenancyMap[options.tenancy],
|
|
109
106
|
userModel: options.userModel
|
|
110
107
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
|
|
109
|
+
logger.detail(`Tenancy: ${options.tenancy}`);
|
|
110
|
+
logger.detail(`User model: ${options.userModel}`);
|
|
114
111
|
} else {
|
|
115
|
-
// Run interactive variant prompts
|
|
116
112
|
variantChoices = await runVariantPrompts();
|
|
117
113
|
}
|
|
118
114
|
|
|
119
|
-
// Determine which services are needed
|
|
120
|
-
const requiredServices = [
|
|
121
|
-
|
|
122
|
-
'admin-portal',
|
|
123
|
-
'infrastructure',
|
|
124
|
-
'website'
|
|
125
|
-
];
|
|
126
|
-
|
|
127
|
-
// Add customers-portal only if B2B2C mode
|
|
115
|
+
// Determine which services are needed
|
|
116
|
+
const requiredServices = ['backend', 'admin-portal', 'infrastructure', 'website'];
|
|
117
|
+
|
|
128
118
|
if (variantChoices.userModel === 'b2b2c') {
|
|
129
119
|
requiredServices.push('customers-portal');
|
|
130
120
|
}
|
|
131
121
|
|
|
132
|
-
// Determine template source
|
|
122
|
+
// Determine template source
|
|
133
123
|
let templateRoot;
|
|
134
|
-
|
|
124
|
+
|
|
135
125
|
if (devMode) {
|
|
136
|
-
// Dev mode: Use local services directory
|
|
137
126
|
templateRoot = path.resolve(__dirname, '../../../services');
|
|
138
|
-
|
|
127
|
+
logger.detail(`[DEV MODE] Using local services: ${templateRoot}`);
|
|
139
128
|
} else {
|
|
140
|
-
// Production mode: Use cache
|
|
141
129
|
try {
|
|
142
130
|
templateRoot = await ensureCacheReady(requiredServices);
|
|
143
131
|
} catch (error) {
|
|
144
|
-
console.error(chalk.red(
|
|
132
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
145
133
|
process.exit(1);
|
|
146
134
|
}
|
|
147
135
|
}
|
|
148
136
|
|
|
149
|
-
// Generate project
|
|
150
|
-
console.log(chalk.
|
|
137
|
+
// Generate project
|
|
138
|
+
console.log(chalk.white('\nGenerating project...\n'));
|
|
151
139
|
await generateProject(answers, variantChoices, templateRoot);
|
|
152
140
|
|
|
153
|
-
|
|
141
|
+
trackEvent('command_executed', {
|
|
142
|
+
command: 'init',
|
|
143
|
+
success: true,
|
|
144
|
+
tenancy: variantChoices.tenancy,
|
|
145
|
+
user_model: variantChoices.userModel
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
console.log(chalk.green.bold('\nProject created successfully!\n'));
|
|
154
149
|
console.log(chalk.white('Next steps:'));
|
|
155
|
-
console.log(chalk.
|
|
156
|
-
console.log(chalk.
|
|
157
|
-
console.log(chalk.gray('Optional:'));
|
|
158
|
-
console.log(chalk.gray(' # Review and customize infrastructure/.env if needed'));
|
|
159
|
-
console.log(chalk.gray(' launchframe docker:build # Rebuild images after changes\n'));
|
|
150
|
+
console.log(chalk.gray(` cd ${answers.projectName}`));
|
|
151
|
+
console.log(chalk.gray(' launchframe docker:up\n'));
|
|
160
152
|
|
|
161
153
|
} catch (error) {
|
|
162
|
-
console.error(chalk.red('
|
|
154
|
+
console.error(chalk.red('Error:'), error.message);
|
|
163
155
|
process.exit(1);
|
|
164
156
|
}
|
|
165
157
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { requireProject } = require('../utils/project-helpers');
|
|
6
|
+
|
|
7
|
+
async function migrateCreate() {
|
|
8
|
+
requireProject();
|
|
9
|
+
|
|
10
|
+
const infrastructurePath = path.join(process.cwd(), 'infrastructure');
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(infrastructurePath)) {
|
|
13
|
+
console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
|
|
14
|
+
console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const migrationName = process.argv[3];
|
|
19
|
+
|
|
20
|
+
if (!migrationName) {
|
|
21
|
+
console.error(chalk.red('\n❌ Error: migration name is required'));
|
|
22
|
+
console.log(chalk.gray('Usage: launchframe migrate:create <name>\n'));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(chalk.blue.bold(`\n🗄️ Creating migration: ${migrationName}\n`));
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
execSync(`docker compose -f docker-compose.yml -f docker-compose.dev.yml exec backend npm run migration:create -- ${migrationName}`, {
|
|
30
|
+
cwd: infrastructurePath,
|
|
31
|
+
stdio: 'inherit'
|
|
32
|
+
});
|
|
33
|
+
console.log(chalk.green.bold('\n✅ Migration created successfully.\n'));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(chalk.red('\n❌ Error creating migration:'), error.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { migrateCreate };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { requireProject } = require('../utils/project-helpers');
|
|
6
|
+
|
|
7
|
+
async function migrateRevert() {
|
|
8
|
+
requireProject();
|
|
9
|
+
|
|
10
|
+
const infrastructurePath = path.join(process.cwd(), 'infrastructure');
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(infrastructurePath)) {
|
|
13
|
+
console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
|
|
14
|
+
console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(chalk.blue.bold('\n🗄️ Reverting last database migration\n'));
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
execSync('docker compose -f docker-compose.yml -f docker-compose.dev.yml exec backend npm run migration:revert', {
|
|
22
|
+
cwd: infrastructurePath,
|
|
23
|
+
stdio: 'inherit'
|
|
24
|
+
});
|
|
25
|
+
console.log(chalk.green.bold('\n✅ Migration reverted successfully.\n'));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(chalk.red('\n❌ Error reverting migration:'), error.message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { migrateRevert };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { requireProject } = require('../utils/project-helpers');
|
|
6
|
+
|
|
7
|
+
async function migrateRun() {
|
|
8
|
+
requireProject();
|
|
9
|
+
|
|
10
|
+
const infrastructurePath = path.join(process.cwd(), 'infrastructure');
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(infrastructurePath)) {
|
|
13
|
+
console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
|
|
14
|
+
console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(chalk.blue.bold('\n🗄️ Running database migrations\n'));
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
execSync('docker compose -f docker-compose.yml -f docker-compose.dev.yml exec backend npm run migration:run', {
|
|
22
|
+
cwd: infrastructurePath,
|
|
23
|
+
stdio: 'inherit'
|
|
24
|
+
});
|
|
25
|
+
console.log(chalk.green.bold('\n✅ Migrations completed successfully.\n'));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(chalk.red('\n❌ Error running migrations:'), error.message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { migrateRun };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const { MODULE_REGISTRY } = require('../services/module-registry');
|
|
7
|
+
const { MODULE_CONFIG } = require('../services/module-config');
|
|
8
|
+
const { installModule } = require('../utils/module-installer');
|
|
9
|
+
const { dockerBuild } = require('./docker-build');
|
|
10
|
+
const { dockerDown } = require('./docker-down');
|
|
11
|
+
const { dockerUp } = require('./docker-up');
|
|
12
|
+
const {
|
|
13
|
+
requireProject,
|
|
14
|
+
getProjectConfig,
|
|
15
|
+
getInstalledModules,
|
|
16
|
+
isModuleInstalled,
|
|
17
|
+
addInstalledModule
|
|
18
|
+
} = require('../utils/project-helpers');
|
|
19
|
+
|
|
20
|
+
// Core services that can't be added via service:add
|
|
21
|
+
const CORE_SERVICES = ['backend', 'admin-portal', 'infrastructure', 'website'];
|
|
22
|
+
|
|
23
|
+
async function moduleList() {
|
|
24
|
+
requireProject();
|
|
25
|
+
|
|
26
|
+
const installedModules = getInstalledModules();
|
|
27
|
+
const modules = Object.values(MODULE_REGISTRY);
|
|
28
|
+
|
|
29
|
+
console.log(chalk.blue('\nAvailable Modules:\n'));
|
|
30
|
+
|
|
31
|
+
modules.forEach(mod => {
|
|
32
|
+
const installed = installedModules.includes(mod.name);
|
|
33
|
+
const status = installed ? chalk.green(' [installed]') : '';
|
|
34
|
+
console.log(chalk.green(` ${mod.name}`) + status);
|
|
35
|
+
console.log(` ${mod.description}`);
|
|
36
|
+
console.log(` Affects services: ${mod.services.join(', ')}`);
|
|
37
|
+
console.log('');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log('To install a module:');
|
|
41
|
+
console.log(' launchframe module:add <module-name>');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function moduleAdd(moduleName) {
|
|
45
|
+
requireProject();
|
|
46
|
+
|
|
47
|
+
// Validate module exists in registry
|
|
48
|
+
const mod = MODULE_REGISTRY[moduleName];
|
|
49
|
+
if (!mod) {
|
|
50
|
+
console.error(chalk.red(`Error: Module "${moduleName}" not found`));
|
|
51
|
+
console.log('\nAvailable modules:');
|
|
52
|
+
Object.keys(MODULE_REGISTRY).forEach(key => {
|
|
53
|
+
console.log(` - ${key}`);
|
|
54
|
+
});
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check not already installed
|
|
59
|
+
if (isModuleInstalled(moduleName)) {
|
|
60
|
+
console.error(chalk.red(`Error: Module "${moduleName}" is already installed`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate required services
|
|
65
|
+
const config = getProjectConfig();
|
|
66
|
+
const installedServices = config.installedServices || [];
|
|
67
|
+
const errors = [];
|
|
68
|
+
|
|
69
|
+
for (const service of mod.services) {
|
|
70
|
+
// Check if service is in installedServices
|
|
71
|
+
if (!installedServices.includes(service)) {
|
|
72
|
+
if (CORE_SERVICES.includes(service)) {
|
|
73
|
+
errors.push(`Core service '${service}' is missing from your project`);
|
|
74
|
+
} else {
|
|
75
|
+
errors.push(`Service '${service}' is not installed. Install it first with: launchframe service:add ${service}`);
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check if service directory exists on disk
|
|
81
|
+
const serviceDir = path.join(process.cwd(), service);
|
|
82
|
+
if (!fs.existsSync(serviceDir)) {
|
|
83
|
+
errors.push(`Service '${service}' directory not found. Your project structure may be corrupted.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (errors.length > 0) {
|
|
88
|
+
console.error(chalk.red(`\nCannot install module "${moduleName}":\n`));
|
|
89
|
+
errors.forEach(err => {
|
|
90
|
+
console.error(chalk.red(` - ${err}`));
|
|
91
|
+
});
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Display module info and confirm
|
|
96
|
+
console.log(chalk.green(`\n${mod.displayName}`));
|
|
97
|
+
console.log(mod.description);
|
|
98
|
+
console.log(`Affects services: ${mod.services.join(', ')}`);
|
|
99
|
+
|
|
100
|
+
const { confirmed } = await inquirer.prompt([{
|
|
101
|
+
type: 'confirm',
|
|
102
|
+
name: 'confirmed',
|
|
103
|
+
message: `Add module "${mod.displayName}" to your project?`,
|
|
104
|
+
default: true
|
|
105
|
+
}]);
|
|
106
|
+
|
|
107
|
+
if (!confirmed) {
|
|
108
|
+
console.log('Installation cancelled');
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const affectedServices = [...new Set(Object.keys(MODULE_CONFIG[moduleName] || {}))].filter(s => s !== 'infrastructure');
|
|
113
|
+
const infrastructurePath = path.join(process.cwd(), 'infrastructure');
|
|
114
|
+
const composeCmd = 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml';
|
|
115
|
+
|
|
116
|
+
// Bring the stack down and remove affected containers before touching files
|
|
117
|
+
await dockerDown();
|
|
118
|
+
for (const service of affectedServices) {
|
|
119
|
+
try {
|
|
120
|
+
execSync(`${composeCmd} rm -f ${service}`, { cwd: infrastructurePath, stdio: 'inherit' });
|
|
121
|
+
} catch (_) {
|
|
122
|
+
// Container may already be gone — that's fine
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Register module in .launchframe
|
|
127
|
+
addInstalledModule(moduleName);
|
|
128
|
+
|
|
129
|
+
// Install module files, sections, and dependencies
|
|
130
|
+
const moduleServiceConfig = MODULE_CONFIG[moduleName];
|
|
131
|
+
if (moduleServiceConfig) {
|
|
132
|
+
await installModule(moduleName, moduleServiceConfig);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(chalk.green(`\n✓ Module "${moduleName}" installed successfully!`));
|
|
136
|
+
|
|
137
|
+
// Rebuild affected containers
|
|
138
|
+
for (const service of affectedServices) {
|
|
139
|
+
await dockerBuild(service);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Restart the full stack in watch mode
|
|
143
|
+
await dockerUp();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { moduleAdd, moduleList };
|
|
@@ -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
|
|