@launchframe/cli 1.0.0-beta.9 → 1.0.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/.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/cache.js +19 -14
- package/src/commands/database-console.js +124 -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 +235 -0
- package/src/commands/deploy-up.js +4 -3
- package/src/commands/dev-add-user.js +165 -0
- package/src/commands/dev-logo.js +161 -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/docker-logs.js +26 -7
- package/src/commands/docker-up.js +17 -3
- package/src/commands/help.js +33 -9
- package/src/commands/init.js +44 -52
- package/src/commands/migration-create.js +38 -0
- package/src/commands/migration-revert.js +32 -0
- package/src/commands/migration-run.js +32 -0
- package/src/commands/module.js +148 -0
- package/src/commands/service.js +22 -12
- package/src/commands/waitlist-deploy.js +1 -0
- package/src/commands/waitlist-logs.js +20 -3
- package/src/generator.js +41 -40
- package/src/index.js +115 -10
- 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,148 @@
|
|
|
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, flags = {}) {
|
|
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
|
+
if (!(flags.yes || flags.y)) {
|
|
101
|
+
const { confirmed } = await inquirer.prompt([{
|
|
102
|
+
type: 'confirm',
|
|
103
|
+
name: 'confirmed',
|
|
104
|
+
message: `Add module "${mod.displayName}" to your project?`,
|
|
105
|
+
default: true
|
|
106
|
+
}]);
|
|
107
|
+
|
|
108
|
+
if (!confirmed) {
|
|
109
|
+
console.log('Installation cancelled');
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const affectedServices = [...new Set(Object.keys(MODULE_CONFIG[moduleName] || {}))].filter(s => s !== 'infrastructure');
|
|
115
|
+
const infrastructurePath = path.join(process.cwd(), 'infrastructure');
|
|
116
|
+
const composeCmd = 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml';
|
|
117
|
+
|
|
118
|
+
// Bring the stack down and remove affected containers before touching files
|
|
119
|
+
await dockerDown();
|
|
120
|
+
for (const service of affectedServices) {
|
|
121
|
+
try {
|
|
122
|
+
execSync(`${composeCmd} rm -f ${service}`, { cwd: infrastructurePath, stdio: 'inherit' });
|
|
123
|
+
} catch (_) {
|
|
124
|
+
// Container may already be gone — that's fine
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Register module in .launchframe
|
|
129
|
+
addInstalledModule(moduleName);
|
|
130
|
+
|
|
131
|
+
// Install module files, sections, and dependencies
|
|
132
|
+
const moduleServiceConfig = MODULE_CONFIG[moduleName];
|
|
133
|
+
if (moduleServiceConfig) {
|
|
134
|
+
await installModule(moduleName, moduleServiceConfig);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(chalk.green(`\n✓ Module "${moduleName}" installed successfully!`));
|
|
138
|
+
|
|
139
|
+
// Rebuild affected containers
|
|
140
|
+
for (const service of affectedServices) {
|
|
141
|
+
await dockerBuild(service);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Restart the full stack (detached when running non-interactively, watch otherwise)
|
|
145
|
+
await dockerUp(undefined, flags.yes || flags.y ? { detach: true } : {});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { moduleAdd, moduleList };
|
package/src/commands/service.js
CHANGED
|
@@ -10,7 +10,7 @@ const { updateEnvFile } = require('../utils/env-generator');
|
|
|
10
10
|
const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
|
|
11
11
|
const { ensureCacheReady, getServicePath } = require('../utils/service-cache');
|
|
12
12
|
|
|
13
|
-
async function serviceAdd(serviceName) {
|
|
13
|
+
async function serviceAdd(serviceName, flags = {}) {
|
|
14
14
|
// STEP 1: Validation
|
|
15
15
|
console.log(chalk.blue(`Installing ${serviceName} service...`));
|
|
16
16
|
|
|
@@ -49,16 +49,18 @@ async function serviceAdd(serviceName) {
|
|
|
49
49
|
console.log(`Tech stack: ${service.techStack}`);
|
|
50
50
|
console.log(`Dependencies: ${service.dependencies.join(', ')}`);
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
if (!(flags.yes || flags.y)) {
|
|
53
|
+
const { confirmed } = await inquirer.prompt([{
|
|
54
|
+
type: 'confirm',
|
|
55
|
+
name: 'confirmed',
|
|
56
|
+
message: 'Continue with installation?',
|
|
57
|
+
default: true
|
|
58
|
+
}]);
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
if (!confirmed) {
|
|
61
|
+
console.log('Installation cancelled');
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// STEP 3: Get service files (from cache in production, local in dev)
|
|
@@ -124,7 +126,7 @@ async function serviceAdd(serviceName) {
|
|
|
124
126
|
|
|
125
127
|
// STEP 4: Service-specific prompts (e.g., Airtable credentials)
|
|
126
128
|
console.log(chalk.blue('\nConfiguring service...'));
|
|
127
|
-
const envValues = await runServicePrompts(service);
|
|
129
|
+
const envValues = await runServicePrompts(service, flags);
|
|
128
130
|
|
|
129
131
|
// STEP 5: Replace template variables
|
|
130
132
|
console.log(chalk.blue('\nCustomizing service for your project...'));
|
|
@@ -265,9 +267,17 @@ async function serviceAdd(serviceName) {
|
|
|
265
267
|
console.log(`\n📖 See README.md in ${serviceName}/ for more details.`);
|
|
266
268
|
}
|
|
267
269
|
|
|
268
|
-
async function runServicePrompts(service) {
|
|
270
|
+
async function runServicePrompts(service, flags = {}) {
|
|
269
271
|
const envValues = {};
|
|
270
272
|
|
|
273
|
+
if (flags.yes || flags.y) {
|
|
274
|
+
// Non-interactive mode — use empty values; configure manually after install
|
|
275
|
+
for (const key of Object.keys(service.envVars)) {
|
|
276
|
+
envValues[key] = '';
|
|
277
|
+
}
|
|
278
|
+
return envValues;
|
|
279
|
+
}
|
|
280
|
+
|
|
271
281
|
// Prompt for each required env var
|
|
272
282
|
for (const [key, description] of Object.entries(service.envVars)) {
|
|
273
283
|
const { value } = await inquirer.prompt([{
|
|
@@ -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
|
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
|
-
const { spawn } = require('child_process');
|
|
2
|
+
const { spawn, spawnSync } = require('child_process');
|
|
3
3
|
const { requireProject, getProjectConfig, isWaitlistInstalled } = require('../utils/project-helpers');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* View waitlist logs from VPS
|
|
6
|
+
* View waitlist logs from VPS
|
|
7
|
+
* @param {Object} flags - Optional flags
|
|
8
|
+
* @param {boolean} flags['no-follow'] - Snapshot mode: print tail and exit (non-interactive)
|
|
9
|
+
* @param {number} flags.tail - Number of lines to show (default 100, used with --no-follow)
|
|
7
10
|
*/
|
|
8
|
-
async function waitlistLogs() {
|
|
11
|
+
async function waitlistLogs(flags = {}) {
|
|
9
12
|
requireProject();
|
|
10
13
|
|
|
11
14
|
console.log(chalk.blue.bold('\n📋 Waitlist Logs\n'));
|
|
@@ -28,6 +31,20 @@ async function waitlistLogs() {
|
|
|
28
31
|
|
|
29
32
|
const { vpsHost, vpsUser, vpsAppFolder } = config.deployment;
|
|
30
33
|
|
|
34
|
+
if (flags['no-follow']) {
|
|
35
|
+
// Snapshot mode — print tail and exit (non-interactive, suitable for MCP)
|
|
36
|
+
const tail = flags.tail || 100;
|
|
37
|
+
const sshCmd = `cd ${vpsAppFolder}/waitlist && docker-compose -f docker-compose.waitlist.yml logs --no-follow --tail=${tail}`;
|
|
38
|
+
|
|
39
|
+
const result = spawnSync('ssh', [`${vpsUser}@${vpsHost}`, sshCmd], { stdio: 'inherit' });
|
|
40
|
+
|
|
41
|
+
if (result.status !== 0 && result.status !== null) {
|
|
42
|
+
console.log(chalk.yellow(`\n⚠️ Process exited with code ${result.status}\n`));
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Streaming mode (interactive)
|
|
31
48
|
console.log(chalk.gray('Connecting to VPS and streaming logs...\n'));
|
|
32
49
|
console.log(chalk.gray('Press Ctrl+C to exit\n'));
|
|
33
50
|
|
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
|
-
// Note: admin-portal folder might not exist yet in templates, skip if missing
|
|
75
|
+
// Process admin-portal
|
|
76
76
|
const adminPortalTemplatePath = path.join(templateRoot, 'admin-portal/base');
|
|
77
77
|
if (await fs.pathExists(adminPortalTemplatePath)) {
|
|
78
|
-
console.log('
|
|
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
|
-
// Note: customers-portal folder might not exist yet in templates, skip if missing
|
|
101
100
|
const customersPortalTemplatePath = path.join(templateRoot, 'customers-portal/base');
|
|
102
101
|
if (await fs.pathExists(customersPortalTemplatePath)) {
|
|
103
|
-
console.log('
|
|
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,16 +160,26 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
166
160
|
}
|
|
167
161
|
}
|
|
168
162
|
|
|
169
|
-
//
|
|
170
|
-
|
|
163
|
+
// Copy MCP configuration files from template root
|
|
164
|
+
logger.detail('Copying MCP configuration files...');
|
|
165
|
+
const mcpFiles = ['CLAUDE.md', '.mcp.json'];
|
|
166
|
+
for (const file of mcpFiles) {
|
|
167
|
+
const sourcePath = path.join(templateRoot, file);
|
|
168
|
+
const destPath = path.join(destinationRoot, file);
|
|
169
|
+
if (await fs.pathExists(sourcePath)) {
|
|
170
|
+
await fs.copy(sourcePath, destPath);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Generate .env file
|
|
175
|
+
console.log(chalk.gray(' Generating environment file...'));
|
|
171
176
|
const { envPath } = await generateEnvFile(destinationRoot, answers);
|
|
172
|
-
|
|
177
|
+
logger.detail(`Environment file: ${envPath}`);
|
|
173
178
|
|
|
174
|
-
//
|
|
175
|
-
|
|
179
|
+
// Create .launchframe marker file
|
|
180
|
+
logger.detail('Creating project marker file...');
|
|
176
181
|
const markerPath = path.join(destinationRoot, '.launchframe');
|
|
177
182
|
|
|
178
|
-
// Determine which services were installed
|
|
179
183
|
const installedServices = ['backend', 'admin-portal', 'infrastructure', 'website'];
|
|
180
184
|
if (variantChoices.userModel === 'b2b2c') {
|
|
181
185
|
installedServices.push('customers-portal');
|
|
@@ -188,12 +192,9 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
188
192
|
projectDisplayName: answers.projectDisplayName,
|
|
189
193
|
deployConfigured: false,
|
|
190
194
|
installedServices: installedServices,
|
|
191
|
-
// Store variant choices for future reference
|
|
192
195
|
variants: variantChoices
|
|
193
196
|
};
|
|
194
197
|
await fs.writeJson(markerPath, markerContent, { spaces: 2 });
|
|
195
|
-
|
|
196
|
-
console.log('✅ Base project generated with variants applied');
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
module.exports = { generateProject };
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { isLaunchFrameProject } = require('./utils/project-helpers');
|
|
5
|
+
const logger = require('./utils/logger');
|
|
6
|
+
const { initTelemetry, trackEvent, sanitize, setTelemetryEnabled, showTelemetryStatus } = require('./utils/telemetry');
|
|
7
|
+
|
|
8
|
+
// Detect locally linked version: npm link installs to global node_modules
|
|
9
|
+
// as a symlink. When running from a real install, __dirname is inside the
|
|
10
|
+
// global node_modules folder. When linked, it resolves to the source directory.
|
|
11
|
+
const isDevMode = !__dirname.includes('node_modules');
|
|
12
|
+
if (isDevMode) {
|
|
13
|
+
const packageJson = require('../package.json');
|
|
14
|
+
console.log(chalk.yellow(`⚠ Running locally linked CLI v${packageJson.version} (${__dirname})`));
|
|
15
|
+
}
|
|
5
16
|
|
|
6
17
|
// Import commands
|
|
7
18
|
const { init } = require('./commands/init');
|
|
@@ -9,6 +20,7 @@ const { deployConfigure } = require('./commands/deploy-configure');
|
|
|
9
20
|
const { deploySetEnv } = require('./commands/deploy-set-env');
|
|
10
21
|
const { deployInit } = require('./commands/deploy-init');
|
|
11
22
|
const { deployUp } = require('./commands/deploy-up');
|
|
23
|
+
const { deployBuild } = require('./commands/deploy-build');
|
|
12
24
|
const { waitlistDeploy } = require('./commands/waitlist-deploy');
|
|
13
25
|
const { waitlistUp } = require('./commands/waitlist-up');
|
|
14
26
|
const { waitlistDown } = require('./commands/waitlist-down');
|
|
@@ -17,6 +29,10 @@ const { dockerBuild } = require('./commands/docker-build');
|
|
|
17
29
|
const { dockerUp } = require('./commands/docker-up');
|
|
18
30
|
const { dockerDown } = require('./commands/docker-down');
|
|
19
31
|
const { dockerLogs } = require('./commands/docker-logs');
|
|
32
|
+
const { migrateRun } = require('./commands/migration-run');
|
|
33
|
+
const { migrateCreate } = require('./commands/migration-create');
|
|
34
|
+
const { migrateRevert } = require('./commands/migration-revert');
|
|
35
|
+
const { databaseConsole } = require('./commands/database-console');
|
|
20
36
|
const { dockerDestroy } = require('./commands/docker-destroy');
|
|
21
37
|
const { doctor } = require('./commands/doctor');
|
|
22
38
|
const { help } = require('./commands/help');
|
|
@@ -25,7 +41,13 @@ const {
|
|
|
25
41
|
serviceList,
|
|
26
42
|
serviceRemove
|
|
27
43
|
} = require('./commands/service');
|
|
44
|
+
const { moduleAdd, moduleList } = require('./commands/module');
|
|
28
45
|
const { cacheClear, cacheInfo, cacheUpdate } = require('./commands/cache');
|
|
46
|
+
const { devAddUser } = require('./commands/dev-add-user');
|
|
47
|
+
const { devQueue } = require('./commands/dev-queue');
|
|
48
|
+
const { devLogo } = require('./commands/dev-logo');
|
|
49
|
+
const { devNpmInstall } = require('./commands/dev-npm-install');
|
|
50
|
+
const { deploySyncFeatures } = require('./commands/deploy-sync-features');
|
|
29
51
|
|
|
30
52
|
// Get command and arguments
|
|
31
53
|
const command = process.argv[2];
|
|
@@ -62,9 +84,23 @@ function parseFlags(args) {
|
|
|
62
84
|
* Main CLI router
|
|
63
85
|
*/
|
|
64
86
|
async function main() {
|
|
87
|
+
initTelemetry();
|
|
88
|
+
|
|
65
89
|
const inProject = isLaunchFrameProject();
|
|
66
90
|
const flags = parseFlags(args);
|
|
67
91
|
|
|
92
|
+
// Handle version flag (only as standalone command)
|
|
93
|
+
if (command === '--version') {
|
|
94
|
+
const packageJson = require('../package.json');
|
|
95
|
+
console.log(packageJson.version);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Set verbose mode globally
|
|
100
|
+
if (flags.verbose || flags.v) {
|
|
101
|
+
logger.setVerbose(true);
|
|
102
|
+
}
|
|
103
|
+
|
|
68
104
|
// No command provided
|
|
69
105
|
if (!command) {
|
|
70
106
|
help();
|
|
@@ -74,7 +110,7 @@ async function main() {
|
|
|
74
110
|
// Route commands
|
|
75
111
|
switch (command) {
|
|
76
112
|
case 'init':
|
|
77
|
-
await init({
|
|
113
|
+
await init({
|
|
78
114
|
projectName: flags['project-name'],
|
|
79
115
|
tenancy: flags['tenancy'],
|
|
80
116
|
userModel: flags['user-model']
|
|
@@ -92,6 +128,12 @@ async function main() {
|
|
|
92
128
|
case 'deploy:up':
|
|
93
129
|
await deployUp();
|
|
94
130
|
break;
|
|
131
|
+
case 'deploy:build':
|
|
132
|
+
await deployBuild(args[1]); // Optional service name
|
|
133
|
+
break;
|
|
134
|
+
case 'deploy:sync-features':
|
|
135
|
+
await deploySyncFeatures(flags);
|
|
136
|
+
break;
|
|
95
137
|
case 'waitlist:deploy':
|
|
96
138
|
await waitlistDeploy();
|
|
97
139
|
break;
|
|
@@ -102,33 +144,45 @@ async function main() {
|
|
|
102
144
|
await waitlistDown();
|
|
103
145
|
break;
|
|
104
146
|
case 'waitlist:logs':
|
|
105
|
-
await waitlistLogs();
|
|
147
|
+
await waitlistLogs(flags);
|
|
106
148
|
break;
|
|
107
149
|
case 'docker:build':
|
|
108
|
-
await dockerBuild();
|
|
150
|
+
await dockerBuild(args[1]); // Optional service name
|
|
109
151
|
break;
|
|
110
152
|
case 'docker:up':
|
|
111
|
-
await dockerUp(args[1]
|
|
153
|
+
await dockerUp(args[1] || flags.service, flags);
|
|
112
154
|
break;
|
|
113
155
|
case 'docker:down':
|
|
114
156
|
await dockerDown();
|
|
115
157
|
break;
|
|
116
158
|
case 'docker:logs':
|
|
117
|
-
await dockerLogs();
|
|
159
|
+
await dockerLogs(args[1] || flags.service, flags);
|
|
118
160
|
break;
|
|
119
161
|
case 'docker:destroy':
|
|
120
162
|
await dockerDestroy({ force: flags.force || flags.f });
|
|
121
163
|
break;
|
|
164
|
+
case 'migration:run':
|
|
165
|
+
await migrateRun();
|
|
166
|
+
break;
|
|
167
|
+
case 'migration:create':
|
|
168
|
+
await migrateCreate(args[1] || flags.name);
|
|
169
|
+
break;
|
|
170
|
+
case 'migration:revert':
|
|
171
|
+
await migrateRevert();
|
|
172
|
+
break;
|
|
173
|
+
case 'database:console':
|
|
174
|
+
await databaseConsole({ remote: flags.remote, query: flags.query });
|
|
175
|
+
break;
|
|
122
176
|
case 'doctor':
|
|
123
177
|
await doctor();
|
|
124
178
|
break;
|
|
125
179
|
case 'service:add':
|
|
126
|
-
if (!args[1]) {
|
|
180
|
+
if (!args[1] && !flags.service) {
|
|
127
181
|
console.error(chalk.red('Error: Service name required'));
|
|
128
182
|
console.log('Usage: launchframe service:add <service-name>');
|
|
129
183
|
process.exit(1);
|
|
130
184
|
}
|
|
131
|
-
await serviceAdd(args[1]);
|
|
185
|
+
await serviceAdd(args[1] || flags.service, flags);
|
|
132
186
|
break;
|
|
133
187
|
case 'service:list':
|
|
134
188
|
await serviceList();
|
|
@@ -141,8 +195,19 @@ async function main() {
|
|
|
141
195
|
}
|
|
142
196
|
await serviceRemove(args[1]);
|
|
143
197
|
break;
|
|
198
|
+
case 'module:add':
|
|
199
|
+
if (!args[1] && !flags.name) {
|
|
200
|
+
console.error(chalk.red('Error: Module name required'));
|
|
201
|
+
console.log('Usage: launchframe module:add <module-name>');
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
await moduleAdd(args[1] || flags.name, flags);
|
|
205
|
+
break;
|
|
206
|
+
case 'module:list':
|
|
207
|
+
await moduleList();
|
|
208
|
+
break;
|
|
144
209
|
case 'cache:clear':
|
|
145
|
-
await cacheClear();
|
|
210
|
+
await cacheClear(flags);
|
|
146
211
|
break;
|
|
147
212
|
case 'cache:info':
|
|
148
213
|
await cacheInfo();
|
|
@@ -150,16 +215,56 @@ async function main() {
|
|
|
150
215
|
case 'cache:update':
|
|
151
216
|
await cacheUpdate();
|
|
152
217
|
break;
|
|
218
|
+
case 'dev:add-user':
|
|
219
|
+
await devAddUser();
|
|
220
|
+
break;
|
|
221
|
+
case 'dev:queue':
|
|
222
|
+
await devQueue();
|
|
223
|
+
break;
|
|
224
|
+
case 'dev:logo':
|
|
225
|
+
await devLogo(args[1] || flags.svg);
|
|
226
|
+
break;
|
|
227
|
+
case 'dev:npm-install':
|
|
228
|
+
if (!args[1]) {
|
|
229
|
+
console.error(chalk.red('Error: Service name required'));
|
|
230
|
+
console.log('Usage: launchframe dev:npm-install <service> [packages...]');
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
await devNpmInstall(args[1], args.slice(2));
|
|
234
|
+
break;
|
|
235
|
+
case 'telemetry':
|
|
236
|
+
if (flags.disable) {
|
|
237
|
+
setTelemetryEnabled(false);
|
|
238
|
+
} else if (flags.enable) {
|
|
239
|
+
setTelemetryEnabled(true);
|
|
240
|
+
} else {
|
|
241
|
+
showTelemetryStatus();
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
153
244
|
case 'help':
|
|
154
245
|
case '--help':
|
|
155
246
|
case '-h':
|
|
156
247
|
help();
|
|
157
248
|
break;
|
|
158
249
|
default:
|
|
159
|
-
console.error(chalk.red(`\
|
|
250
|
+
console.error(chalk.red(`\nUnknown command: ${command}\n`));
|
|
160
251
|
help();
|
|
161
252
|
process.exit(1);
|
|
162
253
|
}
|
|
163
254
|
}
|
|
164
255
|
|
|
165
|
-
main()
|
|
256
|
+
main()
|
|
257
|
+
.then(() => {
|
|
258
|
+
if (command && command !== 'help' && command !== '--help' && command !== '-h' && command !== '--version') {
|
|
259
|
+
trackEvent('command_executed', { command, success: true });
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
.catch((error) => {
|
|
263
|
+
trackEvent('command_executed', {
|
|
264
|
+
command,
|
|
265
|
+
success: false,
|
|
266
|
+
error_message: sanitize(error.message)
|
|
267
|
+
});
|
|
268
|
+
console.error(chalk.red(error.message));
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|