@launchframe/cli 1.0.0-beta.8 → 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/cache.js +14 -14
- 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 +35 -11
- package/src/commands/init.js +48 -56
- 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/service.js +6 -6
- package/src/commands/waitlist-deploy.js +1 -0
- package/src/generator.js +43 -42
- 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 +19 -17
- 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/{module-cache.js → service-cache.js} +67 -73
- 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
|
@@ -159,7 +159,6 @@ async function buildFullAppImages(projectRoot, projectName, githubOrg, envFilePa
|
|
|
159
159
|
`DOCS_URL=${envVars.DOCS_URL || ''}`,
|
|
160
160
|
`CONTACT_EMAIL=${envVars.CONTACT_EMAIL || ''}`,
|
|
161
161
|
`CTA_LINK=${envVars.CTA_LINK || ''}`,
|
|
162
|
-
`LIVE_DEMO_URL=${envVars.LIVE_DEMO_URL || ''}`,
|
|
163
162
|
`MIXPANEL_PROJECT_TOKEN=${envVars.MIXPANEL_PROJECT_TOKEN || ''}`,
|
|
164
163
|
`GOOGLE_ANALYTICS_ID=${envVars.GOOGLE_ANALYTICS_ID || ''}`
|
|
165
164
|
];
|
|
@@ -245,15 +244,130 @@ async function buildWaitlistImage(projectRoot, projectName, githubOrg) {
|
|
|
245
244
|
});
|
|
246
245
|
|
|
247
246
|
spinner.succeed(`waitlist built and pushed successfully`);
|
|
247
|
+
|
|
248
|
+
// Clean up local image after push
|
|
249
|
+
const cleanupSpinner = ora('Cleaning up local waitlist image...').start();
|
|
250
|
+
try {
|
|
251
|
+
await execAsync(`docker rmi ${imageName}`, { timeout: 30000 });
|
|
252
|
+
cleanupSpinner.succeed('Cleaned up local waitlist image');
|
|
253
|
+
} catch (error) {
|
|
254
|
+
cleanupSpinner.info('Could not remove local waitlist image (may be in use)');
|
|
255
|
+
}
|
|
248
256
|
} catch (error) {
|
|
249
257
|
spinner.fail(`Failed to build waitlist`);
|
|
250
258
|
throw new Error(`Build failed for waitlist: ${error.message}`);
|
|
251
259
|
}
|
|
252
260
|
}
|
|
253
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Clean up local Docker images after push
|
|
264
|
+
* @param {string} registry - Registry URL (e.g., 'ghcr.io/myorg')
|
|
265
|
+
* @param {string} projectName - Project name
|
|
266
|
+
* @param {string[]} services - List of services to clean up
|
|
267
|
+
* @returns {Promise<void>}
|
|
268
|
+
*/
|
|
269
|
+
async function cleanupLocalImages(registry, projectName, services) {
|
|
270
|
+
const spinner = ora('Cleaning up local Docker images...').start();
|
|
271
|
+
|
|
272
|
+
const imagesToRemove = services.map(service => `${registry}/${projectName}-${service}:latest`);
|
|
273
|
+
let removedCount = 0;
|
|
274
|
+
|
|
275
|
+
for (const imageName of imagesToRemove) {
|
|
276
|
+
try {
|
|
277
|
+
await execAsync(`docker rmi ${imageName}`, { timeout: 30000 });
|
|
278
|
+
removedCount++;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
// Image might not exist or be in use, continue
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (removedCount > 0) {
|
|
285
|
+
spinner.succeed(`Cleaned up ${removedCount} local Docker image(s)`);
|
|
286
|
+
} else {
|
|
287
|
+
spinner.info('No local images to clean up');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Complete build and push workflow - checks Docker, logs in to GHCR, builds and pushes images
|
|
293
|
+
* @param {Object} options - Workflow options
|
|
294
|
+
* @param {string} options.projectRoot - Project root directory
|
|
295
|
+
* @param {string} options.projectName - Project name
|
|
296
|
+
* @param {string} options.githubOrg - GitHub organization/username
|
|
297
|
+
* @param {string} options.ghcrToken - GitHub Container Registry token
|
|
298
|
+
* @param {string} options.envProdPath - Path to .env.prod file
|
|
299
|
+
* @param {string[]} options.installedServices - List of installed services
|
|
300
|
+
* @param {string} [options.serviceName] - Optional specific service to build (if not provided, builds all)
|
|
301
|
+
* @returns {Promise<void>}
|
|
302
|
+
*/
|
|
303
|
+
async function buildAndPushWorkflow(options) {
|
|
304
|
+
const {
|
|
305
|
+
projectRoot,
|
|
306
|
+
projectName,
|
|
307
|
+
githubOrg,
|
|
308
|
+
ghcrToken,
|
|
309
|
+
envProdPath,
|
|
310
|
+
installedServices,
|
|
311
|
+
serviceName
|
|
312
|
+
} = options;
|
|
313
|
+
|
|
314
|
+
// Step 1: Check Docker is running
|
|
315
|
+
const dockerSpinner = ora('Checking Docker...').start();
|
|
316
|
+
|
|
317
|
+
const dockerRunning = await checkDockerRunning();
|
|
318
|
+
if (!dockerRunning) {
|
|
319
|
+
dockerSpinner.fail('Docker is not running');
|
|
320
|
+
throw new Error('Docker is not running. Please start Docker and try again.');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
dockerSpinner.succeed('Docker is running');
|
|
324
|
+
|
|
325
|
+
// Step 2: Login to GHCR
|
|
326
|
+
if (!ghcrToken) {
|
|
327
|
+
throw new Error('GHCR token not found. Run deploy:configure to set up your GitHub token.');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await loginToGHCR(githubOrg, ghcrToken);
|
|
331
|
+
|
|
332
|
+
// Step 3: Build and push images
|
|
333
|
+
console.log(chalk.yellow('\n📦 Building and pushing images...\n'));
|
|
334
|
+
|
|
335
|
+
const registry = `ghcr.io/${githubOrg}`;
|
|
336
|
+
|
|
337
|
+
if (serviceName) {
|
|
338
|
+
// Build specific service
|
|
339
|
+
if (!installedServices.includes(serviceName)) {
|
|
340
|
+
throw new Error(`Service "${serviceName}" not found in installed services. Available: ${installedServices.join(', ')}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
await buildAndPushImage(
|
|
344
|
+
serviceName,
|
|
345
|
+
path.join(projectRoot, serviceName),
|
|
346
|
+
registry,
|
|
347
|
+
projectName
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Clean up local image after push
|
|
351
|
+
await cleanupLocalImages(registry, projectName, [serviceName]);
|
|
352
|
+
|
|
353
|
+
console.log(chalk.green.bold(`\n✅ ${serviceName} built and pushed to GHCR!\n`));
|
|
354
|
+
} else {
|
|
355
|
+
// Build all services
|
|
356
|
+
await buildFullAppImages(projectRoot, projectName, githubOrg, envProdPath, installedServices);
|
|
357
|
+
|
|
358
|
+
// Clean up local images after push
|
|
359
|
+
await cleanupLocalImages(registry, projectName, installedServices);
|
|
360
|
+
|
|
361
|
+
console.log(chalk.green.bold('\n✅ All images built and pushed to GHCR!\n'));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
254
365
|
module.exports = {
|
|
255
366
|
checkDockerRunning,
|
|
256
367
|
loginToGHCR,
|
|
368
|
+
buildAndPushImage,
|
|
257
369
|
buildFullAppImages,
|
|
258
|
-
buildWaitlistImage
|
|
370
|
+
buildWaitlistImage,
|
|
371
|
+
buildAndPushWorkflow,
|
|
372
|
+
cleanupLocalImages
|
|
259
373
|
};
|
|
@@ -25,28 +25,31 @@ async function generateEnvFile(projectRoot, answers) {
|
|
|
25
25
|
|
|
26
26
|
// Generate secure secrets
|
|
27
27
|
const secrets = {
|
|
28
|
-
|
|
28
|
+
BETTER_AUTH_SECRET: generateSecret(32),
|
|
29
29
|
DB_PASSWORD: generateSecret(24),
|
|
30
30
|
BULL_ADMIN_TOKEN: generateSecret(24)
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
// Create variable mappings
|
|
34
|
+
// Note: PRIMARY_DOMAIN and ADMIN_EMAIL are NOT replaced here - they stay as placeholders
|
|
35
|
+
// until deploy:configure and deploy:set-env are run
|
|
34
36
|
const variables = {
|
|
35
37
|
'{{PROJECT_NAME}}': answers.projectName,
|
|
36
38
|
'{{PROJECT_NAME_UPPER}}': answers.projectNameUpper,
|
|
37
|
-
'{{
|
|
38
|
-
'{{ADMIN_EMAIL}}': answers.adminEmail,
|
|
39
|
+
'{{PROJECT_DISPLAY_NAME}}': answers.projectDisplayName,
|
|
39
40
|
|
|
40
41
|
// Replace placeholder passwords with generated secrets
|
|
41
42
|
'your_secure_postgres_password': secrets.DB_PASSWORD,
|
|
42
|
-
'
|
|
43
|
+
'your_better_auth_secret_minimum_32_chars': secrets.BETTER_AUTH_SECRET,
|
|
43
44
|
'your_bull_admin_token': secrets.BULL_ADMIN_TOKEN
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
// Replace variables in template
|
|
47
|
+
// Replace variables in template (only those with defined values)
|
|
47
48
|
let envContent = envTemplate;
|
|
48
49
|
for (const [placeholder, value] of Object.entries(variables)) {
|
|
49
|
-
|
|
50
|
+
if (value !== undefined && value !== null) {
|
|
51
|
+
envContent = envContent.split(placeholder).join(value);
|
|
52
|
+
}
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
// Write .env file
|
|
@@ -59,13 +59,15 @@ async function validateEnvProd(envProdPath) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* Generate a secure random string for secrets
|
|
62
|
+
* Generate a secure random string for secrets (URL-safe)
|
|
63
63
|
* @param {number} length - Length of string to generate
|
|
64
64
|
* @returns {string}
|
|
65
65
|
*/
|
|
66
66
|
function generateSecret(length = 32) {
|
|
67
67
|
const crypto = require('crypto');
|
|
68
|
-
|
|
68
|
+
// Use hex encoding to avoid URL-unsafe characters (+, /, =)
|
|
69
|
+
// Hex produces 2 chars per byte, so divide by 2
|
|
70
|
+
return crypto.randomBytes(Math.ceil(length / 2)).toString('hex').slice(0, length);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
module.exports = {
|
|
@@ -14,14 +14,14 @@ function makeClickable(text, url) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Check if user has SSH access to LaunchFrame
|
|
17
|
+
* Check if user has SSH access to LaunchFrame services repository
|
|
18
18
|
* @returns {Promise<{hasAccess: boolean, error?: string}>}
|
|
19
19
|
*/
|
|
20
20
|
async function checkGitHubAccess() {
|
|
21
21
|
try {
|
|
22
22
|
// Test SSH access by checking if we can list remote refs
|
|
23
23
|
execSync(
|
|
24
|
-
'git ls-remote git@github.com:launchframe-dev/
|
|
24
|
+
'git ls-remote git@github.com:launchframe-dev/services.git HEAD',
|
|
25
25
|
{
|
|
26
26
|
timeout: 15000,
|
|
27
27
|
stdio: 'pipe' // Don't show output
|
|
@@ -37,27 +37,29 @@ async function checkGitHubAccess() {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Display message when user doesn't have access to
|
|
41
|
-
* Guides them to either
|
|
40
|
+
* Display message when user doesn't have access to services repository
|
|
41
|
+
* Guides them to either get beta access or setup SSH keys
|
|
42
42
|
*/
|
|
43
43
|
function showAccessDeniedMessage() {
|
|
44
|
-
const
|
|
44
|
+
const betaUrl = 'https://launchframe.dev/';
|
|
45
45
|
const docsUrl = 'https://docs.launchframe.dev/guide/quick-start#add-ssh-key-to-repo';
|
|
46
|
-
|
|
47
|
-
console.log(chalk.red('\n❌ Cannot access LaunchFrame
|
|
48
|
-
|
|
46
|
+
|
|
47
|
+
console.log(chalk.red('\n❌ Cannot access LaunchFrame services repository\n'));
|
|
48
|
+
|
|
49
49
|
console.log(chalk.white('This could mean:\n'));
|
|
50
|
-
console.log(chalk.gray(' 1. You
|
|
51
|
-
console.log(chalk.gray(' 2. You
|
|
52
|
-
|
|
53
|
-
console.log(chalk.cyan('→
|
|
54
|
-
console.log('
|
|
55
|
-
console.log('
|
|
56
|
-
|
|
57
|
-
console.log(chalk.cyan(
|
|
50
|
+
console.log(chalk.gray(' 1. You don\'t have beta access yet'));
|
|
51
|
+
console.log(chalk.gray(' 2. You have access but haven\'t added your SSH key to the repo\n'));
|
|
52
|
+
|
|
53
|
+
console.log(chalk.cyan('→ Get beta access:'));
|
|
54
|
+
console.log(chalk.white(' LaunchFrame is in open beta for 100 users.'));
|
|
55
|
+
console.log(chalk.white(' Get free lifetime access at:'));
|
|
56
|
+
console.log(' ' + chalk.blue.bold.underline(makeClickable('launchframe.dev', betaUrl)));
|
|
57
|
+
console.log(' ' + chalk.cyan(betaUrl + '\n'));
|
|
58
|
+
|
|
59
|
+
console.log(chalk.cyan('→ Already have access?'));
|
|
58
60
|
console.log(' ' + chalk.blue.bold.underline(makeClickable('Setup SSH key (docs)', docsUrl)));
|
|
59
61
|
console.log(' ' + chalk.cyan(docsUrl + '\n'));
|
|
60
|
-
|
|
62
|
+
|
|
61
63
|
console.log(chalk.gray('After setup, run: launchframe init\n'));
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple logger with verbose mode support
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* logger.setVerbose(true);
|
|
8
|
+
* logger.info('Main message'); // Always shown
|
|
9
|
+
* logger.detail('Nested detail'); // Only shown in verbose mode
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
let verboseMode = false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Enable or disable verbose mode
|
|
16
|
+
* @param {boolean} enabled
|
|
17
|
+
*/
|
|
18
|
+
function setVerbose(enabled) {
|
|
19
|
+
verboseMode = enabled;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if verbose mode is enabled
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function isVerbose() {
|
|
27
|
+
return verboseMode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Log a main info message (always shown)
|
|
32
|
+
* @param {string} message
|
|
33
|
+
*/
|
|
34
|
+
function info(message) {
|
|
35
|
+
console.log(message);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Log a success message (always shown)
|
|
40
|
+
* @param {string} message
|
|
41
|
+
*/
|
|
42
|
+
function success(message) {
|
|
43
|
+
console.log(chalk.green(message));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Log an error message (always shown)
|
|
48
|
+
* @param {string} message
|
|
49
|
+
*/
|
|
50
|
+
function error(message) {
|
|
51
|
+
console.error(chalk.red(message));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Log a warning message (always shown)
|
|
56
|
+
* @param {string} message
|
|
57
|
+
*/
|
|
58
|
+
function warn(message) {
|
|
59
|
+
console.warn(chalk.yellow(message));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Log a detail/nested message (only in verbose mode)
|
|
64
|
+
* @param {string} message
|
|
65
|
+
* @param {number} indent - Indentation level (default 1)
|
|
66
|
+
*/
|
|
67
|
+
function detail(message, indent = 1) {
|
|
68
|
+
if (verboseMode) {
|
|
69
|
+
const prefix = ' '.repeat(indent);
|
|
70
|
+
console.log(chalk.gray(`${prefix}${message}`));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Log a step within an operation (only in verbose mode)
|
|
76
|
+
* @param {string} message
|
|
77
|
+
*/
|
|
78
|
+
function step(message) {
|
|
79
|
+
if (verboseMode) {
|
|
80
|
+
console.log(chalk.gray(` - ${message}`));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
setVerbose,
|
|
86
|
+
isVerbose,
|
|
87
|
+
info,
|
|
88
|
+
success,
|
|
89
|
+
error,
|
|
90
|
+
warn,
|
|
91
|
+
detail,
|
|
92
|
+
step
|
|
93
|
+
};
|
|
@@ -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, 'modules', moduleName, serviceName, 'files');
|
|
17
|
+
const moduleSectionsDir = path.join(templateRoot, 'modules', moduleName, serviceName, '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
|
};
|