@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,25 @@
|
|
|
1
|
+
// Module configuration - defines files, sections, and dependencies for each module
|
|
2
|
+
const MODULE_CONFIG = {
|
|
3
|
+
blog: {
|
|
4
|
+
website: {
|
|
5
|
+
files: [
|
|
6
|
+
'src/lib/blog.ts',
|
|
7
|
+
'src/types/blog.ts',
|
|
8
|
+
'src/app/blog',
|
|
9
|
+
'src/components/blog',
|
|
10
|
+
'src/app/sitemap.ts',
|
|
11
|
+
'content/blog',
|
|
12
|
+
],
|
|
13
|
+
sections: {
|
|
14
|
+
'src/components/layout/Navbar.tsx': ['BLOG_NAV_LINK'],
|
|
15
|
+
'src/components/layout/Footer.tsx': ['BLOG_FOOTER_LINK'],
|
|
16
|
+
},
|
|
17
|
+
dependencies: {
|
|
18
|
+
'gray-matter': '^4.0.3',
|
|
19
|
+
'marked': '^12.0.0',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
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 };
|
|
@@ -21,24 +21,16 @@ const VARIANT_CONFIG = {
|
|
|
21
21
|
// Complete files/folders to copy
|
|
22
22
|
files: [
|
|
23
23
|
'src/modules/domain/projects', // Entire projects module
|
|
24
|
-
'src/modules/domain/ai/services/project-config.service.ts', // Project config service
|
|
25
24
|
'src/guards/project-ownership.guard.ts', // Project ownership guard (header-based)
|
|
26
25
|
'src/guards/project-param.guard.ts', // Project param guard (route-based)
|
|
27
|
-
'src/modules/auth/auth.service.ts', // Auth service with multi-tenant support
|
|
28
|
-
'src/modules/auth/auth.controller.ts', // Auth controller with multi-tenant support
|
|
29
26
|
'src/modules/users/users.service.ts', // Users service with multi-tenant support
|
|
30
27
|
'src/modules/users/users.controller.ts', // Users controller with multi-tenant support
|
|
31
28
|
'src/modules/users/create-user.dto.ts' // CreateUserDto with businessId
|
|
32
29
|
],
|
|
33
30
|
|
|
34
31
|
// Code sections to insert into base template files
|
|
32
|
+
// Note: main.ts uses PRIMARY_DOMAIN env var for dynamic CORS - no sections needed
|
|
35
33
|
sections: {
|
|
36
|
-
'src/main.ts': [
|
|
37
|
-
'PROJECT_IMPORTS', // Add project-related imports
|
|
38
|
-
'PROJECT_CUSTOM_DOMAINS', // Add custom domains query
|
|
39
|
-
'PROJECT_CUSTOM_DOMAINS_CORS', // Add custom domains to CORS
|
|
40
|
-
'PROJECT_GUARD' // Add ProjectOwnershipGuard registration
|
|
41
|
-
],
|
|
42
34
|
'src/modules/app/app.module.ts': [
|
|
43
35
|
'PROJECTS_MODULE_IMPORT', // Add ProjectsModule import
|
|
44
36
|
'PROJECTS_MODULE' // Add ProjectsModule to imports array
|
|
@@ -60,11 +52,15 @@ const VARIANT_CONFIG = {
|
|
|
60
52
|
}
|
|
61
53
|
},
|
|
62
54
|
|
|
63
|
-
// B2B2C variant: Adds regular_user support
|
|
55
|
+
// B2B2C variant: Adds regular_user support with separate customer auth
|
|
64
56
|
'b2b2c': {
|
|
65
57
|
// Complete files to copy
|
|
66
58
|
files: [
|
|
67
|
-
'src/modules/users/user-business.entity.ts',
|
|
59
|
+
'src/modules/users/user-business.entity.ts', // Business-to-user linking entity
|
|
60
|
+
'src/modules/auth/auth-customer.ts', // Customer auth config (regular_user, customer_ cookie)
|
|
61
|
+
'src/modules/auth/better-auth-customer.controller.ts', // Customer auth controller (/api/auth/customer)
|
|
62
|
+
'src/modules/auth/auth.module.ts', // Auth module with customer controller
|
|
63
|
+
'src/modules/auth/better-auth.guard.ts', // Guard handling both auth instances
|
|
68
64
|
],
|
|
69
65
|
|
|
70
66
|
// Code sections to insert
|
|
@@ -77,6 +73,9 @@ const VARIANT_CONFIG = {
|
|
|
77
73
|
'src/modules/users/users.module.ts': [
|
|
78
74
|
'B2B2C_IMPORTS', // Add UserBusiness import
|
|
79
75
|
'B2B2C_ENTITIES' // Add UserBusiness to TypeORM
|
|
76
|
+
],
|
|
77
|
+
'src/database/migrations/1764300000001-CreateSessionsTable.ts': [
|
|
78
|
+
'B2B2C_TENANT_COLUMN' // Add tenant_id column for session scoping
|
|
80
79
|
]
|
|
81
80
|
}
|
|
82
81
|
},
|
|
@@ -126,8 +125,7 @@ const VARIANT_CONFIG = {
|
|
|
126
125
|
// Complete files to copy (has both multi-tenant and B2B2C features)
|
|
127
126
|
files: [
|
|
128
127
|
'src/modules/users/user-business.entity.ts', // Business-to-user linking entity
|
|
129
|
-
'src/modules/auth/auth.
|
|
130
|
-
'src/modules/auth/auth.controller.ts', // Combined auth controller
|
|
128
|
+
'src/modules/auth/auth.ts', // Combined Better Auth config
|
|
131
129
|
'src/modules/users/users.service.ts', // Combined users service
|
|
132
130
|
'src/modules/users/users.controller.ts', // Combined users controller
|
|
133
131
|
'src/modules/domain/projects/projects.module.ts' // Projects module with UserBusiness
|
|
@@ -310,6 +308,14 @@ const VARIANT_CONFIG = {
|
|
|
310
308
|
'src/store/useProjectStore.ts' // Project state
|
|
311
309
|
],
|
|
312
310
|
sections: {}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
'b2b2c': {
|
|
314
|
+
// B2B2C uses separate auth endpoint for customer sessions
|
|
315
|
+
files: [
|
|
316
|
+
'src/lib/auth-client.ts' // Auth client with /api/auth/customer basePath
|
|
317
|
+
],
|
|
318
|
+
sections: {}
|
|
313
319
|
}
|
|
314
320
|
},
|
|
315
321
|
|
|
@@ -395,6 +401,11 @@ function resolveVariantChoices(backendChoices) {
|
|
|
395
401
|
choices['admin-portal'].userModel = backendChoices.userModel;
|
|
396
402
|
}
|
|
397
403
|
|
|
404
|
+
// Special case: customers-portal inherits BOTH tenancy and userModel
|
|
405
|
+
if (choices['customers-portal']) {
|
|
406
|
+
choices['customers-portal'].userModel = backendChoices.userModel;
|
|
407
|
+
}
|
|
408
|
+
|
|
398
409
|
// Special case: infrastructure needs BOTH tenancy and userModel for proper variant resolution
|
|
399
410
|
if (choices['infrastructure']) {
|
|
400
411
|
choices['infrastructure'].tenancy = backendChoices.tenancy;
|
|
@@ -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 = {
|
|
@@ -38,26 +38,28 @@ async function checkGitHubAccess() {
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Display message when user doesn't have access to services repository
|
|
41
|
-
* Guides them to either
|
|
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
|
-
|
|
46
|
+
|
|
47
47
|
console.log(chalk.red('\n❌ Cannot access LaunchFrame services repository\n'));
|
|
48
|
-
|
|
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
|
};
|
|
@@ -3,6 +3,7 @@ const fs = require('fs-extra');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
5
|
const chalk = require('chalk');
|
|
6
|
+
const logger = require('./logger');
|
|
6
7
|
|
|
7
8
|
const SERVICES_REPO = 'git@github.com:launchframe-dev/services.git';
|
|
8
9
|
const BRANCH = 'main';
|
|
@@ -38,30 +39,26 @@ async function cacheExists() {
|
|
|
38
39
|
async function initializeCache() {
|
|
39
40
|
const cacheDir = getCacheDir();
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
logger.detail('Initializing services cache...');
|
|
42
43
|
|
|
43
44
|
try {
|
|
44
|
-
// Ensure parent directory exists
|
|
45
45
|
await fs.ensureDir(path.dirname(cacheDir));
|
|
46
46
|
|
|
47
|
-
// Sparse clone (only root files, no services)
|
|
48
47
|
execSync(
|
|
49
48
|
`git clone --sparse --depth 1 --branch ${BRANCH} ${SERVICES_REPO} "${cacheDir}"`,
|
|
50
49
|
{
|
|
51
|
-
stdio: 'pipe',
|
|
52
|
-
timeout: 60000
|
|
50
|
+
stdio: 'pipe',
|
|
51
|
+
timeout: 60000
|
|
53
52
|
}
|
|
54
53
|
);
|
|
55
54
|
|
|
56
|
-
// Configure sparse checkout (starts with empty set)
|
|
57
55
|
execSync('git sparse-checkout init --cone', {
|
|
58
56
|
cwd: cacheDir,
|
|
59
57
|
stdio: 'pipe'
|
|
60
58
|
});
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
logger.detail('Cache initialized');
|
|
63
61
|
} catch (error) {
|
|
64
|
-
// Clean up partial clone on failure
|
|
65
62
|
await fs.remove(cacheDir);
|
|
66
63
|
throw new Error(`Failed to initialize cache: ${error.message}`);
|
|
67
64
|
}
|
|
@@ -75,16 +72,16 @@ async function initializeCache() {
|
|
|
75
72
|
async function updateCache() {
|
|
76
73
|
const cacheDir = getCacheDir();
|
|
77
74
|
|
|
78
|
-
|
|
75
|
+
logger.detail('Updating service cache...');
|
|
79
76
|
|
|
80
77
|
try {
|
|
81
78
|
execSync('git pull origin main', {
|
|
82
79
|
cwd: cacheDir,
|
|
83
80
|
stdio: 'pipe',
|
|
84
|
-
timeout: 30000
|
|
81
|
+
timeout: 30000
|
|
85
82
|
});
|
|
86
83
|
|
|
87
|
-
|
|
84
|
+
logger.detail('Cache updated');
|
|
88
85
|
} catch (error) {
|
|
89
86
|
throw new Error(`Failed to update cache: ${error.message}`);
|
|
90
87
|
}
|
|
@@ -98,10 +95,9 @@ async function updateCache() {
|
|
|
98
95
|
async function expandServices(serviceNames) {
|
|
99
96
|
const cacheDir = getCacheDir();
|
|
100
97
|
|
|
101
|
-
|
|
98
|
+
logger.detail(`Loading services: ${serviceNames.join(', ')}...`);
|
|
102
99
|
|
|
103
100
|
try {
|
|
104
|
-
// Get current sparse checkout list
|
|
105
101
|
let currentServices = [];
|
|
106
102
|
try {
|
|
107
103
|
const output = execSync('git sparse-checkout list', {
|
|
@@ -114,17 +110,15 @@ async function expandServices(serviceNames) {
|
|
|
114
110
|
// No services yet, that's fine
|
|
115
111
|
}
|
|
116
112
|
|
|
117
|
-
// Add new services to the list
|
|
118
113
|
const allServices = [...new Set([...currentServices, ...serviceNames])];
|
|
119
114
|
|
|
120
|
-
// Set sparse checkout to include all services
|
|
121
115
|
execSync(`git sparse-checkout set ${allServices.join(' ')}`, {
|
|
122
116
|
cwd: cacheDir,
|
|
123
117
|
stdio: 'pipe',
|
|
124
|
-
timeout: 60000
|
|
118
|
+
timeout: 60000
|
|
125
119
|
});
|
|
126
120
|
|
|
127
|
-
|
|
121
|
+
logger.detail('Services loaded');
|
|
128
122
|
} catch (error) {
|
|
129
123
|
throw new Error(`Failed to expand services: ${error.message}`);
|
|
130
124
|
}
|
|
@@ -158,7 +152,7 @@ async function clearCache() {
|
|
|
158
152
|
|
|
159
153
|
if (await fs.pathExists(cacheDir)) {
|
|
160
154
|
await fs.remove(cacheDir);
|
|
161
|
-
console.log(chalk.green('
|
|
155
|
+
console.log(chalk.green('Cache cleared'));
|
|
162
156
|
} else {
|
|
163
157
|
console.log(chalk.gray('Cache is already empty'));
|
|
164
158
|
}
|