@launchframe/cli 1.0.0-beta.31 ā 1.0.0-beta.32
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/package.json +1 -1
- package/src/commands/docker-build.js +9 -6
- package/src/commands/help.js +3 -0
- package/src/commands/module.js +146 -0
- package/src/index.js +13 -1
- package/src/services/module-config.js +26 -0
- package/src/services/module-registry.js +12 -0
- package/src/utils/module-installer.js +58 -0
- package/src/utils/project-helpers.js +34 -1
package/package.json
CHANGED
|
@@ -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
|
@@ -49,6 +49,9 @@ function help() {
|
|
|
49
49
|
console.log(chalk.gray(' service:add <name> Add an optional service to your project'));
|
|
50
50
|
console.log(chalk.gray(' service:list List available services'));
|
|
51
51
|
console.log(chalk.gray(' service:remove <name> Remove installed service\n'));
|
|
52
|
+
console.log(chalk.white('Module Management:'));
|
|
53
|
+
console.log(chalk.gray(' module:add <name> Add a module to your project'));
|
|
54
|
+
console.log(chalk.gray(' module:list List available modules\n'));
|
|
52
55
|
console.log(chalk.white('Available Services:'));
|
|
53
56
|
console.log(chalk.gray(' waitlist Coming soon page with email collection\n'));
|
|
54
57
|
console.log(chalk.white('Cache Management:'));
|
|
@@ -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 };
|
package/src/index.js
CHANGED
|
@@ -41,6 +41,7 @@ const {
|
|
|
41
41
|
serviceList,
|
|
42
42
|
serviceRemove
|
|
43
43
|
} = require('./commands/service');
|
|
44
|
+
const { moduleAdd, moduleList } = require('./commands/module');
|
|
44
45
|
const { cacheClear, cacheInfo, cacheUpdate } = require('./commands/cache');
|
|
45
46
|
const { devAddUser } = require('./commands/dev-add-user');
|
|
46
47
|
const { devQueue } = require('./commands/dev-queue');
|
|
@@ -140,7 +141,7 @@ async function main() {
|
|
|
140
141
|
await waitlistLogs();
|
|
141
142
|
break;
|
|
142
143
|
case 'docker:build':
|
|
143
|
-
await dockerBuild();
|
|
144
|
+
await dockerBuild(args[1]); // Optional service name
|
|
144
145
|
break;
|
|
145
146
|
case 'docker:up':
|
|
146
147
|
await dockerUp(args[1]); // Pass optional service name
|
|
@@ -188,6 +189,17 @@ async function main() {
|
|
|
188
189
|
}
|
|
189
190
|
await serviceRemove(args[1]);
|
|
190
191
|
break;
|
|
192
|
+
case 'module:add':
|
|
193
|
+
if (!args[1]) {
|
|
194
|
+
console.error(chalk.red('Error: Module name required'));
|
|
195
|
+
console.log('Usage: launchframe module:add <module-name>');
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
await moduleAdd(args[1]);
|
|
199
|
+
break;
|
|
200
|
+
case 'module:list':
|
|
201
|
+
await moduleList();
|
|
202
|
+
break;
|
|
191
203
|
case 'cache:clear':
|
|
192
204
|
await cacheClear();
|
|
193
205
|
break;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Module configuration - defines files, sections, and dependencies for each module
|
|
2
|
+
const MODULE_CONFIG = {
|
|
3
|
+
blog: {
|
|
4
|
+
website: {
|
|
5
|
+
modulesDir: 'blog',
|
|
6
|
+
files: [
|
|
7
|
+
'src/lib/blog.ts',
|
|
8
|
+
'src/types/blog.ts',
|
|
9
|
+
'src/app/blog',
|
|
10
|
+
'src/components/blog',
|
|
11
|
+
'src/app/sitemap.ts',
|
|
12
|
+
'content/blog',
|
|
13
|
+
],
|
|
14
|
+
sections: {
|
|
15
|
+
'src/components/layout/Navbar.tsx': ['BLOG_NAV_LINK'],
|
|
16
|
+
'src/components/layout/Footer.tsx': ['BLOG_FOOTER_LINK'],
|
|
17
|
+
},
|
|
18
|
+
dependencies: {
|
|
19
|
+
'gray-matter': '^4.0.3',
|
|
20
|
+
'marked': '^12.0.0',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
module.exports = { MODULE_CONFIG };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Module registry - available modules for LaunchFrame services
|
|
2
|
+
const MODULE_REGISTRY = {
|
|
3
|
+
blog: {
|
|
4
|
+
name: 'blog',
|
|
5
|
+
displayName: 'Blog',
|
|
6
|
+
description: 'Markdown-based blog using local .md files with YAML front-matter ā no database required',
|
|
7
|
+
services: ['website'],
|
|
8
|
+
version: '1.0.0'
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
module.exports = { MODULE_REGISTRY };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { replaceSection } = require('./section-replacer');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Install a module into a project
|
|
8
|
+
* @param {string} moduleName - Name of the module to install
|
|
9
|
+
* @param {Object} moduleConfig - Config object from MODULE_CONFIG[moduleName]
|
|
10
|
+
*/
|
|
11
|
+
async function installModule(moduleName, moduleConfig) {
|
|
12
|
+
const templateRoot = path.resolve(__dirname, '../../../services');
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
|
|
15
|
+
for (const [serviceName, config] of Object.entries(moduleConfig)) {
|
|
16
|
+
const moduleFilesDir = path.join(templateRoot, serviceName, 'modules', config.modulesDir, 'files');
|
|
17
|
+
const moduleSectionsDir = path.join(templateRoot, serviceName, 'modules', config.modulesDir, 'sections');
|
|
18
|
+
const serviceDir = path.join(cwd, serviceName);
|
|
19
|
+
|
|
20
|
+
// Copy files
|
|
21
|
+
for (const filePath of config.files) {
|
|
22
|
+
const src = path.join(moduleFilesDir, filePath);
|
|
23
|
+
const dest = path.join(serviceDir, filePath);
|
|
24
|
+
console.log(` Adding ${filePath}`);
|
|
25
|
+
await fs.copy(src, dest, { overwrite: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Inject sections
|
|
29
|
+
for (const [targetFile, markerNames] of Object.entries(config.sections)) {
|
|
30
|
+
const targetFilePath = path.join(serviceDir, targetFile);
|
|
31
|
+
const targetBasename = path.basename(targetFile);
|
|
32
|
+
|
|
33
|
+
for (const markerName of markerNames) {
|
|
34
|
+
const sectionFile = path.join(moduleSectionsDir, `${targetBasename}.${markerName}`);
|
|
35
|
+
console.log(` Injecting ${markerName} into ${targetFile}`);
|
|
36
|
+
const sectionContent = await fs.readFile(sectionFile, 'utf8');
|
|
37
|
+
await replaceSection(targetFilePath, markerName, sectionContent);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Merge dependencies into package.json and sync lockfile
|
|
42
|
+
if (config.dependencies && Object.keys(config.dependencies).length > 0) {
|
|
43
|
+
const packageJsonPath = path.join(serviceDir, 'package.json');
|
|
44
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
45
|
+
packageJson.dependencies = {
|
|
46
|
+
...packageJson.dependencies,
|
|
47
|
+
...config.dependencies,
|
|
48
|
+
};
|
|
49
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
50
|
+
|
|
51
|
+
// Run npm install to sync package-lock.json, otherwise `npm ci` will fail on rebuild
|
|
52
|
+
console.log(`\nRunning npm install in ${serviceName}...`);
|
|
53
|
+
execSync('npm install', { cwd: serviceDir, stdio: 'inherit' });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { installModule };
|
|
@@ -95,6 +95,36 @@ function isWaitlistInstalled(config = null) {
|
|
|
95
95
|
return (config.installedServices || []).includes('waitlist');
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Get list of installed modules
|
|
100
|
+
*/
|
|
101
|
+
function getInstalledModules() {
|
|
102
|
+
const config = getProjectConfig();
|
|
103
|
+
return config.installedModules || [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a module is installed
|
|
108
|
+
*/
|
|
109
|
+
function isModuleInstalled(moduleName) {
|
|
110
|
+
const installedModules = getInstalledModules();
|
|
111
|
+
return installedModules.includes(moduleName);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Add a module to the installed modules list
|
|
116
|
+
*/
|
|
117
|
+
function addInstalledModule(moduleName) {
|
|
118
|
+
const config = getProjectConfig();
|
|
119
|
+
if (!config.installedModules) {
|
|
120
|
+
config.installedModules = [];
|
|
121
|
+
}
|
|
122
|
+
if (!config.installedModules.includes(moduleName)) {
|
|
123
|
+
config.installedModules.push(moduleName);
|
|
124
|
+
updateProjectConfig(config);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
98
128
|
module.exports = {
|
|
99
129
|
isLaunchFrameProject,
|
|
100
130
|
requireProject,
|
|
@@ -104,5 +134,8 @@ module.exports = {
|
|
|
104
134
|
isComponentInstalled,
|
|
105
135
|
addInstalledComponent,
|
|
106
136
|
getPrimaryDomain,
|
|
107
|
-
isWaitlistInstalled
|
|
137
|
+
isWaitlistInstalled,
|
|
138
|
+
getInstalledModules,
|
|
139
|
+
isModuleInstalled,
|
|
140
|
+
addInstalledModule
|
|
108
141
|
};
|