@launchframe/cli 1.0.0-beta.2 ā 1.0.0-beta.21
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.md +27 -0
- package/README.md +7 -1
- package/package.json +1 -1
- package/src/commands/cache.js +14 -14
- package/src/commands/deploy-build.js +76 -0
- package/src/commands/deploy-configure.js +15 -6
- package/src/commands/deploy-init.js +40 -57
- package/src/commands/deploy-set-env.js +17 -7
- package/src/commands/deploy-up.js +4 -3
- package/src/commands/help.js +11 -5
- package/src/commands/init.js +39 -64
- package/src/commands/service.js +12 -6
- package/src/commands/waitlist-deploy.js +1 -0
- package/src/generator.js +34 -44
- package/src/index.js +20 -10
- package/src/services/variant-config.js +36 -25
- 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/project-helpers.js +5 -1
- package/src/utils/{module-cache.js ā service-cache.js} +67 -73
- package/src/utils/ssh-helper.js +51 -1
- 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
|
+
};
|
|
@@ -7,7 +7,11 @@ const chalk = require('chalk');
|
|
|
7
7
|
*/
|
|
8
8
|
function isLaunchFrameProject() {
|
|
9
9
|
const markerPath = path.join(process.cwd(), '.launchframe');
|
|
10
|
-
|
|
10
|
+
try {
|
|
11
|
+
return fs.existsSync(markerPath) && fs.statSync(markerPath).isFile();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
/**
|
|
@@ -3,8 +3,9 @@ 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
|
-
const
|
|
8
|
+
const SERVICES_REPO = 'git@github.com:launchframe-dev/services.git';
|
|
8
9
|
const BRANCH = 'main';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -15,9 +16,9 @@ const BRANCH = 'main';
|
|
|
15
16
|
function getCacheDir() {
|
|
16
17
|
const homeDir = os.homedir();
|
|
17
18
|
// Use same path structure on all platforms
|
|
18
|
-
// Windows: C:\Users\username\.launchframe\cache\
|
|
19
|
-
// Mac/Linux: /home/username/.launchframe/cache/
|
|
20
|
-
return path.join(homeDir, '.launchframe', 'cache', '
|
|
19
|
+
// Windows: C:\Users\username\.launchframe\cache\services
|
|
20
|
+
// Mac/Linux: /home/username/.launchframe/cache/services
|
|
21
|
+
return path.join(homeDir, '.launchframe', 'cache', 'services');
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -32,36 +33,32 @@ async function cacheExists() {
|
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Initialize cache with sparse checkout
|
|
35
|
-
* Clones only the repository structure, no
|
|
36
|
+
* Clones only the repository structure, no services yet
|
|
36
37
|
* @returns {Promise<void>}
|
|
37
38
|
*/
|
|
38
39
|
async function initializeCache() {
|
|
39
40
|
const cacheDir = getCacheDir();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
|
|
42
|
+
logger.detail('Initializing services cache...');
|
|
43
|
+
|
|
43
44
|
try {
|
|
44
|
-
// Ensure parent directory exists
|
|
45
45
|
await fs.ensureDir(path.dirname(cacheDir));
|
|
46
|
-
|
|
47
|
-
// Sparse clone (only root files, no modules)
|
|
46
|
+
|
|
48
47
|
execSync(
|
|
49
|
-
`git clone --sparse --depth 1 --branch ${BRANCH} ${
|
|
50
|
-
{
|
|
51
|
-
stdio: 'pipe',
|
|
52
|
-
timeout: 60000
|
|
48
|
+
`git clone --sparse --depth 1 --branch ${BRANCH} ${SERVICES_REPO} "${cacheDir}"`,
|
|
49
|
+
{
|
|
50
|
+
stdio: 'pipe',
|
|
51
|
+
timeout: 60000
|
|
53
52
|
}
|
|
54
53
|
);
|
|
55
|
-
|
|
56
|
-
// Configure sparse checkout (starts with empty set)
|
|
54
|
+
|
|
57
55
|
execSync('git sparse-checkout init --cone', {
|
|
58
56
|
cwd: cacheDir,
|
|
59
57
|
stdio: 'pipe'
|
|
60
58
|
});
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
|
|
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
|
}
|
|
@@ -74,70 +71,67 @@ async function initializeCache() {
|
|
|
74
71
|
*/
|
|
75
72
|
async function updateCache() {
|
|
76
73
|
const cacheDir = getCacheDir();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
|
|
75
|
+
logger.detail('Updating service cache...');
|
|
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
|
-
|
|
87
|
-
|
|
83
|
+
|
|
84
|
+
logger.detail('Cache updated');
|
|
88
85
|
} catch (error) {
|
|
89
86
|
throw new Error(`Failed to update cache: ${error.message}`);
|
|
90
87
|
}
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
/**
|
|
94
|
-
* Expand sparse checkout to include specific
|
|
95
|
-
* @param {string[]}
|
|
91
|
+
* Expand sparse checkout to include specific services
|
|
92
|
+
* @param {string[]} serviceNames - Array of services names to expand
|
|
96
93
|
* @returns {Promise<void>}
|
|
97
94
|
*/
|
|
98
|
-
async function
|
|
95
|
+
async function expandServices(serviceNames) {
|
|
99
96
|
const cacheDir = getCacheDir();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
|
|
98
|
+
logger.detail(`Loading services: ${serviceNames.join(', ')}...`);
|
|
99
|
+
|
|
103
100
|
try {
|
|
104
|
-
|
|
105
|
-
let currentModules = [];
|
|
101
|
+
let currentServices = [];
|
|
106
102
|
try {
|
|
107
103
|
const output = execSync('git sparse-checkout list', {
|
|
108
104
|
cwd: cacheDir,
|
|
109
105
|
stdio: 'pipe',
|
|
110
106
|
encoding: 'utf8'
|
|
111
107
|
});
|
|
112
|
-
|
|
108
|
+
currentServices = output.trim().split('\n').filter(Boolean);
|
|
113
109
|
} catch (error) {
|
|
114
|
-
// No
|
|
110
|
+
// No services yet, that's fine
|
|
115
111
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Set sparse checkout to include all modules
|
|
121
|
-
execSync(`git sparse-checkout set ${allModules.join(' ')}`, {
|
|
112
|
+
|
|
113
|
+
const allServices = [...new Set([...currentServices, ...serviceNames])];
|
|
114
|
+
|
|
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
|
-
|
|
127
|
-
|
|
120
|
+
|
|
121
|
+
logger.detail('Services loaded');
|
|
128
122
|
} catch (error) {
|
|
129
|
-
throw new Error(`Failed to expand
|
|
123
|
+
throw new Error(`Failed to expand services: ${error.message}`);
|
|
130
124
|
}
|
|
131
125
|
}
|
|
132
126
|
|
|
133
127
|
/**
|
|
134
|
-
* Get path to a specific
|
|
135
|
-
* @param {string}
|
|
136
|
-
* @returns {string} Absolute path to
|
|
128
|
+
* Get path to a specific service in the cache
|
|
129
|
+
* @param {string} serviceName - Service name (e.g., 'backend', 'admin-portal')
|
|
130
|
+
* @returns {string} Absolute path to service
|
|
137
131
|
*/
|
|
138
|
-
function
|
|
132
|
+
function getServicePath(serviceName) {
|
|
139
133
|
const cacheDir = getCacheDir();
|
|
140
|
-
return path.join(cacheDir,
|
|
134
|
+
return path.join(cacheDir, serviceName);
|
|
141
135
|
}
|
|
142
136
|
|
|
143
137
|
/**
|
|
@@ -149,24 +143,24 @@ function getCachePath() {
|
|
|
149
143
|
}
|
|
150
144
|
|
|
151
145
|
/**
|
|
152
|
-
* Clear the entire
|
|
146
|
+
* Clear the entire service cache
|
|
153
147
|
* Useful for troubleshooting or forcing fresh download
|
|
154
148
|
* @returns {Promise<void>}
|
|
155
149
|
*/
|
|
156
150
|
async function clearCache() {
|
|
157
151
|
const cacheDir = getCacheDir();
|
|
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
|
}
|
|
165
159
|
}
|
|
166
160
|
|
|
167
161
|
/**
|
|
168
|
-
* Get cache information (size, last update,
|
|
169
|
-
* @returns {Promise<{exists: boolean, path: string, size?: number,
|
|
162
|
+
* Get cache information (size, last update, services)
|
|
163
|
+
* @returns {Promise<{exists: boolean, path: string, size?: number, services?: string[], lastUpdate?: Date}>}
|
|
170
164
|
*/
|
|
171
165
|
async function getCacheInfo() {
|
|
172
166
|
const cacheDir = getCacheDir();
|
|
@@ -174,13 +168,13 @@ async function getCacheInfo() {
|
|
|
174
168
|
exists: false,
|
|
175
169
|
path: cacheDir
|
|
176
170
|
};
|
|
177
|
-
|
|
171
|
+
|
|
178
172
|
if (!(await cacheExists())) {
|
|
179
173
|
return info;
|
|
180
174
|
}
|
|
181
|
-
|
|
175
|
+
|
|
182
176
|
info.exists = true;
|
|
183
|
-
|
|
177
|
+
|
|
184
178
|
try {
|
|
185
179
|
// Get cache size (du command works on Unix/Mac, different on Windows)
|
|
186
180
|
if (process.platform === 'win32') {
|
|
@@ -201,19 +195,19 @@ async function getCacheInfo() {
|
|
|
201
195
|
} catch (error) {
|
|
202
196
|
// Size calculation failed, not critical
|
|
203
197
|
}
|
|
204
|
-
|
|
198
|
+
|
|
205
199
|
try {
|
|
206
|
-
// Get list of expanded
|
|
200
|
+
// Get list of expanded services
|
|
207
201
|
const output = execSync('git sparse-checkout list', {
|
|
208
202
|
cwd: cacheDir,
|
|
209
203
|
encoding: 'utf8',
|
|
210
204
|
stdio: 'pipe'
|
|
211
205
|
});
|
|
212
|
-
info.
|
|
206
|
+
info.services = output.trim().split('\n').filter(Boolean);
|
|
213
207
|
} catch (error) {
|
|
214
|
-
info.
|
|
208
|
+
info.services = [];
|
|
215
209
|
}
|
|
216
|
-
|
|
210
|
+
|
|
217
211
|
try {
|
|
218
212
|
// Get last update time from git log
|
|
219
213
|
const output = execSync('git log -1 --format=%cd --date=iso', {
|
|
@@ -225,17 +219,17 @@ async function getCacheInfo() {
|
|
|
225
219
|
} catch (error) {
|
|
226
220
|
// Last update time failed, not critical
|
|
227
221
|
}
|
|
228
|
-
|
|
222
|
+
|
|
229
223
|
return info;
|
|
230
224
|
}
|
|
231
225
|
|
|
232
226
|
/**
|
|
233
227
|
* Ensure cache is ready (initialize if needed, update if exists)
|
|
234
228
|
* This is the main entry point for cache management
|
|
235
|
-
* @param {string[]}
|
|
229
|
+
* @param {string[]} requiredServices - Services needed for the operation
|
|
236
230
|
* @returns {Promise<string>} Path to cache root
|
|
237
231
|
*/
|
|
238
|
-
async function ensureCacheReady(
|
|
232
|
+
async function ensureCacheReady(requiredServices) {
|
|
239
233
|
try {
|
|
240
234
|
if (!(await cacheExists())) {
|
|
241
235
|
// Cache doesn't exist, initialize it
|
|
@@ -244,10 +238,10 @@ async function ensureCacheReady(requiredModules) {
|
|
|
244
238
|
// Cache exists, update it
|
|
245
239
|
await updateCache();
|
|
246
240
|
}
|
|
247
|
-
|
|
248
|
-
// Expand sparse checkout to include required
|
|
249
|
-
await
|
|
250
|
-
|
|
241
|
+
|
|
242
|
+
// Expand sparse checkout to include required services
|
|
243
|
+
await expandServices(requiredServices);
|
|
244
|
+
|
|
251
245
|
return getCachePath();
|
|
252
246
|
} catch (error) {
|
|
253
247
|
// If we fail and it's a network error, provide helpful message
|
|
@@ -265,8 +259,8 @@ module.exports = {
|
|
|
265
259
|
cacheExists,
|
|
266
260
|
initializeCache,
|
|
267
261
|
updateCache,
|
|
268
|
-
|
|
269
|
-
|
|
262
|
+
expandServices,
|
|
263
|
+
getServicePath,
|
|
270
264
|
getCachePath,
|
|
271
265
|
clearCache,
|
|
272
266
|
getCacheInfo,
|