@launchframe/cli 1.0.0-beta.14 โ 1.0.0-beta.17
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/deploy-build.js +137 -0
- package/src/commands/deploy-set-env.js +1 -0
- package/src/commands/deploy-up.js +2 -2
- package/src/commands/help.js +8 -3
- package/src/commands/init.js +34 -52
- package/src/generator.js +30 -40
- package/src/index.js +11 -1
- package/src/utils/docker-helper.js +1 -0
- package/src/utils/logger.js +93 -0
- package/src/utils/service-cache.js +12 -18
- package/src/utils/variant-processor.js +33 -42
package/package.json
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { exec } = require('child_process');
|
|
3
|
+
const { promisify } = require('util');
|
|
4
|
+
const ora = require('ora');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { requireProject, getProjectConfig } = require('../utils/project-helpers');
|
|
7
|
+
const { checkDockerRunning, loginToGHCR, buildFullAppImages, buildAndPushImage } = require('../utils/docker-helper');
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build, push, and deploy Docker images
|
|
13
|
+
* @param {string} [serviceName] - Optional specific service to build (e.g., 'backend', 'admin-portal')
|
|
14
|
+
*/
|
|
15
|
+
async function deployBuild(serviceName) {
|
|
16
|
+
requireProject();
|
|
17
|
+
|
|
18
|
+
const serviceLabel = serviceName ? `(${serviceName})` : '(all services)';
|
|
19
|
+
console.log(chalk.blue.bold(`\n๐จ LaunchFrame Build & Deploy ${serviceLabel}\n`));
|
|
20
|
+
|
|
21
|
+
const config = getProjectConfig();
|
|
22
|
+
const projectRoot = process.cwd();
|
|
23
|
+
|
|
24
|
+
// Validate deployment is configured
|
|
25
|
+
if (!config.deployConfigured || !config.deployment) {
|
|
26
|
+
console.log(chalk.red('โ Error: Deployment not configured yet\n'));
|
|
27
|
+
console.log(chalk.gray('Run this command first:'));
|
|
28
|
+
console.log(chalk.white(' launchframe deploy:configure\n'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { vpsHost, vpsUser, vpsAppFolder, githubOrg } = config.deployment;
|
|
33
|
+
const { projectName, installedServices } = config;
|
|
34
|
+
const envProdPath = path.join(projectRoot, 'infrastructure', '.env.prod');
|
|
35
|
+
|
|
36
|
+
// Step 1: Check Docker is running
|
|
37
|
+
console.log(chalk.yellow('๐ณ Step 1: Checking Docker...\n'));
|
|
38
|
+
|
|
39
|
+
const dockerSpinner = ora('Checking Docker...').start();
|
|
40
|
+
|
|
41
|
+
const dockerRunning = await checkDockerRunning();
|
|
42
|
+
if (!dockerRunning) {
|
|
43
|
+
dockerSpinner.fail('Docker is not running');
|
|
44
|
+
console.log(chalk.red('\nโ Please start Docker and try again.\n'));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
dockerSpinner.succeed('Docker is running');
|
|
49
|
+
|
|
50
|
+
// Step 2: Login to GHCR
|
|
51
|
+
console.log(chalk.yellow('\n๐ Step 2: Logging in to GitHub Container Registry...\n'));
|
|
52
|
+
|
|
53
|
+
const ghcrToken = config.deployment.ghcrToken;
|
|
54
|
+
if (!ghcrToken) {
|
|
55
|
+
console.log(chalk.red('โ Error: GHCR token not found in .launchframe config\n'));
|
|
56
|
+
console.log(chalk.gray('Run deploy:configure to set up your GitHub token.\n'));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await loginToGHCR(githubOrg, ghcrToken);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.log(chalk.red(`\nโ ${error.message}\n`));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Step 3: Build and push images
|
|
68
|
+
console.log(chalk.yellow('\n๐ฆ Step 3: Building and pushing images...\n'));
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (serviceName) {
|
|
72
|
+
// Build specific service
|
|
73
|
+
if (!installedServices.includes(serviceName)) {
|
|
74
|
+
console.log(chalk.red(`โ Service "${serviceName}" not found in installed services.\n`));
|
|
75
|
+
console.log(chalk.gray(`Available services: ${installedServices.join(', ')}\n`));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const registry = `ghcr.io/${githubOrg}`;
|
|
80
|
+
await buildAndPushImage(
|
|
81
|
+
serviceName,
|
|
82
|
+
path.join(projectRoot, serviceName),
|
|
83
|
+
registry,
|
|
84
|
+
projectName
|
|
85
|
+
);
|
|
86
|
+
console.log(chalk.green.bold(`\nโ
${serviceName} built and pushed to GHCR!\n`));
|
|
87
|
+
} else {
|
|
88
|
+
// Build all services
|
|
89
|
+
await buildFullAppImages(projectRoot, projectName, githubOrg, envProdPath, installedServices);
|
|
90
|
+
console.log(chalk.green.bold('\nโ
All images built and pushed to GHCR!\n'));
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.log(chalk.red(`\nโ Build failed: ${error.message}\n`));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Step 4: Pull images on VPS
|
|
98
|
+
console.log(chalk.yellow('\n๐ Step 4: Pulling images on VPS...\n'));
|
|
99
|
+
|
|
100
|
+
const pullSpinner = ora('Pulling images on VPS...').start();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await execAsync(
|
|
104
|
+
`ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull"`,
|
|
105
|
+
{ timeout: 600000 } // 10 minutes
|
|
106
|
+
);
|
|
107
|
+
pullSpinner.succeed('Images pulled on VPS');
|
|
108
|
+
} catch (error) {
|
|
109
|
+
pullSpinner.fail('Failed to pull images');
|
|
110
|
+
console.log(chalk.red(`\nโ Error: ${error.message}\n`));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Step 5: Restart services
|
|
115
|
+
console.log(chalk.yellow('\n๐ Step 5: Restarting services...\n'));
|
|
116
|
+
|
|
117
|
+
const restartSpinner = ora('Restarting services...').start();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await execAsync(
|
|
121
|
+
`ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d"`,
|
|
122
|
+
{ timeout: 300000 } // 5 minutes
|
|
123
|
+
);
|
|
124
|
+
restartSpinner.succeed('Services restarted');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
restartSpinner.fail('Failed to restart services');
|
|
127
|
+
console.log(chalk.red(`\nโ Error: ${error.message}\n`));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Success!
|
|
132
|
+
console.log(chalk.green.bold('\nโ
Build and deploy complete!\n'));
|
|
133
|
+
|
|
134
|
+
console.log(chalk.gray('Your updated application is now running.\n'));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { deployBuild };
|
|
@@ -164,6 +164,7 @@ async function deploySetEnv() {
|
|
|
164
164
|
if (config.deployment?.primaryDomain) {
|
|
165
165
|
const domain = config.deployment.primaryDomain;
|
|
166
166
|
const urlReplacements = {
|
|
167
|
+
'PRIMARY_DOMAIN': domain,
|
|
167
168
|
'API_BASE_URL': `https://api.${domain}`,
|
|
168
169
|
'ADMIN_BASE_URL': `https://admin.${domain}`,
|
|
169
170
|
'FRONTEND_BASE_URL': `https://app.${domain}`,
|
|
@@ -106,7 +106,7 @@ async function deployUp() {
|
|
|
106
106
|
|
|
107
107
|
try {
|
|
108
108
|
const { stdout: psOutput } = await execAsync(
|
|
109
|
-
`ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml ps"`,
|
|
109
|
+
`ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml ps"`,
|
|
110
110
|
{ timeout: 30000 }
|
|
111
111
|
);
|
|
112
112
|
|
|
@@ -139,7 +139,7 @@ async function deployUp() {
|
|
|
139
139
|
console.log(chalk.gray(' Just push to GitHub - CI/CD will handle deployment automatically!\n'));
|
|
140
140
|
|
|
141
141
|
console.log(chalk.white('3. Monitor services:'));
|
|
142
|
-
console.log(chalk.gray(` Run: ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose logs -f"\n`));
|
|
142
|
+
console.log(chalk.gray(` Run: ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml logs -f"\n`));
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
module.exports = { deployUp };
|
package/src/commands/help.js
CHANGED
|
@@ -7,16 +7,19 @@ const { isLaunchFrameProject, isWaitlistInstalled } = require('../utils/project-
|
|
|
7
7
|
function help() {
|
|
8
8
|
const inProject = isLaunchFrameProject();
|
|
9
9
|
|
|
10
|
-
console.log(chalk.blue.bold('\
|
|
10
|
+
console.log(chalk.blue.bold('\nLaunchFrame CLI\n'));
|
|
11
11
|
console.log(chalk.white('Usage:'));
|
|
12
|
-
console.log(chalk.gray(' launchframe [command]\n'));
|
|
12
|
+
console.log(chalk.gray(' launchframe [command] [options]\n'));
|
|
13
|
+
console.log(chalk.white('Global options:'));
|
|
14
|
+
console.log(chalk.gray(' --verbose, -v Show detailed output\n'));
|
|
13
15
|
|
|
14
16
|
if (inProject) {
|
|
15
17
|
console.log(chalk.white('Deployment commands:'));
|
|
16
18
|
console.log(chalk.gray(' deploy:configure Configure production deployment settings'));
|
|
17
19
|
console.log(chalk.gray(' deploy:set-env Configure production environment variables'));
|
|
18
20
|
console.log(chalk.gray(' deploy:init Initialize VPS and build Docker images'));
|
|
19
|
-
console.log(chalk.gray(' deploy:up Start services on VPS
|
|
21
|
+
console.log(chalk.gray(' deploy:up Start services on VPS'));
|
|
22
|
+
console.log(chalk.gray(' deploy:build [service] Build, push, and deploy (all or specific service)\n'));
|
|
20
23
|
|
|
21
24
|
// Conditionally show waitlist commands
|
|
22
25
|
if (isWaitlistInstalled()) {
|
|
@@ -83,6 +86,8 @@ function help() {
|
|
|
83
86
|
console.log(chalk.gray(' launchframe init\n'));
|
|
84
87
|
console.log(chalk.gray(' # Non-interactive mode'));
|
|
85
88
|
console.log(chalk.gray(' launchframe init --project-name my-saas --tenancy single --user-model b2b\n'));
|
|
89
|
+
console.log(chalk.gray(' # With verbose output'));
|
|
90
|
+
console.log(chalk.gray(' launchframe init --verbose\n'));
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|
package/src/commands/init.js
CHANGED
|
@@ -6,6 +6,7 @@ const { generateProject } = require('../generator');
|
|
|
6
6
|
const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
|
|
7
7
|
const { ensureCacheReady } = require('../utils/service-cache');
|
|
8
8
|
const { isLaunchFrameProject } = require('../utils/project-helpers');
|
|
9
|
+
const logger = require('../utils/logger');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Check if running in development mode (local) vs production (npm install)
|
|
@@ -23,28 +24,28 @@ function isDevMode() {
|
|
|
23
24
|
* @param {string} options.userModel - User model: 'b2b' or 'b2b2c' (skips prompt if provided)
|
|
24
25
|
*/
|
|
25
26
|
async function init(options = {}) {
|
|
26
|
-
console.log(chalk.blue.bold('\n
|
|
27
|
+
console.log(chalk.blue.bold('\nLaunchFrame\n'));
|
|
27
28
|
|
|
28
29
|
// Check if in development mode
|
|
29
30
|
const devMode = isDevMode();
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
if (!devMode) {
|
|
32
33
|
// Production mode: Check GitHub access
|
|
33
|
-
console.log(chalk.
|
|
34
|
-
|
|
34
|
+
console.log(chalk.gray('Checking repository access...'));
|
|
35
|
+
|
|
35
36
|
const accessCheck = await checkGitHubAccess();
|
|
36
|
-
|
|
37
|
+
|
|
37
38
|
if (!accessCheck.hasAccess) {
|
|
38
|
-
// No access - show purchase/setup message
|
|
39
39
|
showAccessDeniedMessage();
|
|
40
|
-
process.exit(1);
|
|
40
|
+
process.exit(1);
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
console.log(chalk.green('
|
|
42
|
+
|
|
43
|
+
console.log(chalk.green('Repository access confirmed\n'));
|
|
44
44
|
}
|
|
45
|
+
|
|
45
46
|
// Check if already in a LaunchFrame project
|
|
46
47
|
if (isLaunchFrameProject()) {
|
|
47
|
-
console.error(chalk.red('
|
|
48
|
+
console.error(chalk.red('Error: Already in a LaunchFrame project'));
|
|
48
49
|
console.log(chalk.gray('Use other commands to manage your project, or run init from outside the project.\n'));
|
|
49
50
|
process.exit(1);
|
|
50
51
|
}
|
|
@@ -54,12 +55,10 @@ async function init(options = {}) {
|
|
|
54
55
|
|
|
55
56
|
// If project name provided via flag, skip prompts
|
|
56
57
|
if (options.projectName) {
|
|
57
|
-
// Validate project name format
|
|
58
58
|
if (!/^[a-z0-9-]+$/.test(options.projectName)) {
|
|
59
59
|
throw new Error('Project name must contain only lowercase letters, numbers, and hyphens');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// Auto-generate display name from slug
|
|
63
62
|
const projectDisplayName = options.projectName
|
|
64
63
|
.split('-')
|
|
65
64
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
@@ -75,91 +74,74 @@ async function init(options = {}) {
|
|
|
75
74
|
projectNameCamel: options.projectName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
76
75
|
};
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
logger.detail(`Project name: ${options.projectName}`);
|
|
78
|
+
logger.detail(`Display name: ${projectDisplayName}`);
|
|
79
|
+
logger.detail(`Description: ${projectDescription}`);
|
|
81
80
|
} else {
|
|
82
|
-
// Get user inputs via interactive prompts
|
|
83
81
|
answers = await runInitPrompts();
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
// Get variant selections (multi-tenancy, B2B vs B2B2C)
|
|
87
85
|
let variantChoices;
|
|
88
|
-
|
|
89
|
-
// If both flags provided, skip variant prompts
|
|
86
|
+
|
|
90
87
|
if (options.tenancy && options.userModel) {
|
|
91
|
-
// Validate tenancy value
|
|
92
88
|
if (!['single', 'multi'].includes(options.tenancy)) {
|
|
93
89
|
throw new Error('Invalid --tenancy value. Must be "single" or "multi"');
|
|
94
90
|
}
|
|
95
|
-
|
|
96
|
-
// Validate userModel value
|
|
91
|
+
|
|
97
92
|
if (!['b2b', 'b2b2c'].includes(options.userModel)) {
|
|
98
93
|
throw new Error('Invalid --user-model value. Must be "b2b" or "b2b2c"');
|
|
99
94
|
}
|
|
100
|
-
|
|
101
|
-
// Convert short flag values to full variant names
|
|
95
|
+
|
|
102
96
|
const tenancyMap = {
|
|
103
97
|
'single': 'single-tenant',
|
|
104
98
|
'multi': 'multi-tenant'
|
|
105
99
|
};
|
|
106
|
-
|
|
100
|
+
|
|
107
101
|
variantChoices = {
|
|
108
102
|
tenancy: tenancyMap[options.tenancy],
|
|
109
103
|
userModel: options.userModel
|
|
110
104
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
|
|
106
|
+
logger.detail(`Tenancy: ${options.tenancy}`);
|
|
107
|
+
logger.detail(`User model: ${options.userModel}`);
|
|
114
108
|
} else {
|
|
115
|
-
// Run interactive variant prompts
|
|
116
109
|
variantChoices = await runVariantPrompts();
|
|
117
110
|
}
|
|
118
111
|
|
|
119
|
-
// Determine which services are needed
|
|
120
|
-
const requiredServices = [
|
|
121
|
-
|
|
122
|
-
'admin-portal',
|
|
123
|
-
'infrastructure',
|
|
124
|
-
'website'
|
|
125
|
-
];
|
|
126
|
-
|
|
127
|
-
// Add customers-portal only if B2B2C mode
|
|
112
|
+
// Determine which services are needed
|
|
113
|
+
const requiredServices = ['backend', 'admin-portal', 'infrastructure', 'website'];
|
|
114
|
+
|
|
128
115
|
if (variantChoices.userModel === 'b2b2c') {
|
|
129
116
|
requiredServices.push('customers-portal');
|
|
130
117
|
}
|
|
131
118
|
|
|
132
|
-
// Determine template source
|
|
119
|
+
// Determine template source
|
|
133
120
|
let templateRoot;
|
|
134
|
-
|
|
121
|
+
|
|
135
122
|
if (devMode) {
|
|
136
|
-
// Dev mode: Use local services directory
|
|
137
123
|
templateRoot = path.resolve(__dirname, '../../../services');
|
|
138
|
-
|
|
124
|
+
logger.detail(`[DEV MODE] Using local services: ${templateRoot}`);
|
|
139
125
|
} else {
|
|
140
|
-
// Production mode: Use cache
|
|
141
126
|
try {
|
|
142
127
|
templateRoot = await ensureCacheReady(requiredServices);
|
|
143
128
|
} catch (error) {
|
|
144
|
-
console.error(chalk.red(
|
|
129
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
145
130
|
process.exit(1);
|
|
146
131
|
}
|
|
147
132
|
}
|
|
148
133
|
|
|
149
|
-
// Generate project
|
|
150
|
-
console.log(chalk.
|
|
134
|
+
// Generate project
|
|
135
|
+
console.log(chalk.white('\nGenerating project...\n'));
|
|
151
136
|
await generateProject(answers, variantChoices, templateRoot);
|
|
152
137
|
|
|
153
|
-
console.log(chalk.green.bold('\
|
|
138
|
+
console.log(chalk.green.bold('\nProject created successfully!\n'));
|
|
154
139
|
console.log(chalk.white('Next steps:'));
|
|
155
|
-
console.log(chalk.
|
|
156
|
-
console.log(chalk.
|
|
157
|
-
console.log(chalk.gray('Optional:'));
|
|
158
|
-
console.log(chalk.gray(' # Review and customize infrastructure/.env if needed'));
|
|
159
|
-
console.log(chalk.gray(' launchframe docker:build # Rebuild images after changes\n'));
|
|
140
|
+
console.log(chalk.gray(` cd ${answers.projectName}`));
|
|
141
|
+
console.log(chalk.gray(' launchframe docker:up\n'));
|
|
160
142
|
|
|
161
143
|
} catch (error) {
|
|
162
|
-
console.error(chalk.red('
|
|
144
|
+
console.error(chalk.red('Error:'), error.message);
|
|
163
145
|
process.exit(1);
|
|
164
146
|
}
|
|
165
147
|
}
|
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,15 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
166
160
|
}
|
|
167
161
|
}
|
|
168
162
|
|
|
169
|
-
//
|
|
170
|
-
console.log('
|
|
163
|
+
// Generate .env file
|
|
164
|
+
console.log(chalk.gray(' Generating environment file...'));
|
|
171
165
|
const { envPath } = await generateEnvFile(destinationRoot, answers);
|
|
172
|
-
|
|
166
|
+
logger.detail(`Environment file: ${envPath}`);
|
|
173
167
|
|
|
174
|
-
//
|
|
175
|
-
|
|
168
|
+
// Create .launchframe marker file
|
|
169
|
+
logger.detail('Creating project marker file...');
|
|
176
170
|
const markerPath = path.join(destinationRoot, '.launchframe');
|
|
177
171
|
|
|
178
|
-
// Determine which services were installed
|
|
179
172
|
const installedServices = ['backend', 'admin-portal', 'infrastructure', 'website'];
|
|
180
173
|
if (variantChoices.userModel === 'b2b2c') {
|
|
181
174
|
installedServices.push('customers-portal');
|
|
@@ -188,12 +181,9 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
188
181
|
projectDisplayName: answers.projectDisplayName,
|
|
189
182
|
deployConfigured: false,
|
|
190
183
|
installedServices: installedServices,
|
|
191
|
-
// Store variant choices for future reference
|
|
192
184
|
variants: variantChoices
|
|
193
185
|
};
|
|
194
186
|
await fs.writeJson(markerPath, markerContent, { spaces: 2 });
|
|
195
|
-
|
|
196
|
-
console.log('โ
Base project generated with variants applied');
|
|
197
187
|
}
|
|
198
188
|
|
|
199
189
|
module.exports = { generateProject };
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { isLaunchFrameProject } = require('./utils/project-helpers');
|
|
5
|
+
const logger = require('./utils/logger');
|
|
5
6
|
|
|
6
7
|
// Import commands
|
|
7
8
|
const { init } = require('./commands/init');
|
|
@@ -9,6 +10,7 @@ const { deployConfigure } = require('./commands/deploy-configure');
|
|
|
9
10
|
const { deploySetEnv } = require('./commands/deploy-set-env');
|
|
10
11
|
const { deployInit } = require('./commands/deploy-init');
|
|
11
12
|
const { deployUp } = require('./commands/deploy-up');
|
|
13
|
+
const { deployBuild } = require('./commands/deploy-build');
|
|
12
14
|
const { waitlistDeploy } = require('./commands/waitlist-deploy');
|
|
13
15
|
const { waitlistUp } = require('./commands/waitlist-up');
|
|
14
16
|
const { waitlistDown } = require('./commands/waitlist-down');
|
|
@@ -65,6 +67,11 @@ async function main() {
|
|
|
65
67
|
const inProject = isLaunchFrameProject();
|
|
66
68
|
const flags = parseFlags(args);
|
|
67
69
|
|
|
70
|
+
// Set verbose mode globally
|
|
71
|
+
if (flags.verbose || flags.v) {
|
|
72
|
+
logger.setVerbose(true);
|
|
73
|
+
}
|
|
74
|
+
|
|
68
75
|
// No command provided
|
|
69
76
|
if (!command) {
|
|
70
77
|
help();
|
|
@@ -92,6 +99,9 @@ async function main() {
|
|
|
92
99
|
case 'deploy:up':
|
|
93
100
|
await deployUp();
|
|
94
101
|
break;
|
|
102
|
+
case 'deploy:build':
|
|
103
|
+
await deployBuild(args[1]); // Optional service name
|
|
104
|
+
break;
|
|
95
105
|
case 'waitlist:deploy':
|
|
96
106
|
await waitlistDeploy();
|
|
97
107
|
break;
|
|
@@ -156,7 +166,7 @@ async function main() {
|
|
|
156
166
|
help();
|
|
157
167
|
break;
|
|
158
168
|
default:
|
|
159
|
-
console.error(chalk.red(`\
|
|
169
|
+
console.error(chalk.red(`\nUnknown command: ${command}\n`));
|
|
160
170
|
help();
|
|
161
171
|
process.exit(1);
|
|
162
172
|
}
|
|
@@ -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
|
+
};
|
|
@@ -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
|
}
|
|
@@ -15,6 +15,7 @@ const { replaceVariables } = require('./variable-replacer');
|
|
|
15
15
|
const { getVariantConfig, getVariantsToApply } = require('../services/variant-config');
|
|
16
16
|
const fs = require('fs-extra');
|
|
17
17
|
const path = require('path');
|
|
18
|
+
const logger = require('./logger');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Process service with variant modifications
|
|
@@ -32,7 +33,7 @@ async function processServiceVariant(
|
|
|
32
33
|
replacements,
|
|
33
34
|
templateRoot
|
|
34
35
|
) {
|
|
35
|
-
|
|
36
|
+
logger.detail(`Processing ${serviceName} with choices: ${JSON.stringify(variantChoices)}`, 2);
|
|
36
37
|
|
|
37
38
|
const serviceConfig = getVariantConfig(serviceName);
|
|
38
39
|
if (!serviceConfig) {
|
|
@@ -41,33 +42,33 @@ async function processServiceVariant(
|
|
|
41
42
|
|
|
42
43
|
const basePath = path.join(templateRoot, serviceConfig.base);
|
|
43
44
|
|
|
44
|
-
//
|
|
45
|
-
|
|
45
|
+
// Copy base template
|
|
46
|
+
logger.detail(`Copying base template from ${serviceConfig.base}`, 2);
|
|
46
47
|
await copyDirectory(basePath, destination, {
|
|
47
48
|
exclude: ['node_modules', '.git', 'dist', '.env']
|
|
48
49
|
});
|
|
49
50
|
|
|
50
|
-
//
|
|
51
|
+
// Determine which variants to apply
|
|
51
52
|
const variantsToApply = getVariantsToApply(variantChoices);
|
|
52
53
|
|
|
53
54
|
if (variantsToApply.length === 0) {
|
|
54
|
-
|
|
55
|
+
logger.detail('Using base template (no variants)', 2);
|
|
55
56
|
} else {
|
|
56
|
-
|
|
57
|
+
logger.detail(`Applying variants: ${variantsToApply.join(', ')}`, 2);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
//
|
|
60
|
+
// Apply each variant
|
|
60
61
|
for (const variantName of variantsToApply) {
|
|
61
62
|
const variantConfig = serviceConfig.variants[variantName];
|
|
62
63
|
|
|
63
64
|
if (!variantConfig) {
|
|
64
|
-
|
|
65
|
+
logger.warn(`No configuration found for variant: ${variantName}, skipping`);
|
|
65
66
|
continue;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
logger.detail(`Applying ${variantName} variant`, 2);
|
|
69
70
|
|
|
70
|
-
//
|
|
71
|
+
// Copy variant FILES
|
|
71
72
|
await copyVariantFiles(
|
|
72
73
|
variantName,
|
|
73
74
|
variantConfig.files || [],
|
|
@@ -76,7 +77,7 @@ async function processServiceVariant(
|
|
|
76
77
|
templateRoot
|
|
77
78
|
);
|
|
78
79
|
|
|
79
|
-
//
|
|
80
|
+
// Insert variant SECTIONS
|
|
80
81
|
await insertVariantSections(
|
|
81
82
|
variantName,
|
|
82
83
|
variantConfig.sections || {},
|
|
@@ -86,15 +87,15 @@ async function processServiceVariant(
|
|
|
86
87
|
);
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
//
|
|
90
|
-
|
|
90
|
+
// Clean up unused section markers
|
|
91
|
+
logger.detail('Cleaning up unused section markers', 2);
|
|
91
92
|
await cleanupSectionMarkers(serviceName, serviceConfig, variantsToApply, destination);
|
|
92
93
|
|
|
93
|
-
//
|
|
94
|
-
|
|
94
|
+
// Replace template variables
|
|
95
|
+
logger.detail('Replacing template variables', 2);
|
|
95
96
|
await replaceVariables(destination, replacements);
|
|
96
97
|
|
|
97
|
-
|
|
98
|
+
logger.detail(`${serviceName} complete`, 2);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
@@ -110,7 +111,7 @@ async function cleanupSectionMarkers(serviceName, serviceConfig, appliedVariants
|
|
|
110
111
|
const unappliedVariants = allVariants.filter(v => !appliedVariants.includes(v));
|
|
111
112
|
|
|
112
113
|
if (unappliedVariants.length === 0) {
|
|
113
|
-
|
|
114
|
+
logger.detail('No unused section markers to clean', 3);
|
|
114
115
|
return;
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -146,8 +147,6 @@ async function cleanupSectionMarkers(serviceName, serviceConfig, appliedVariants
|
|
|
146
147
|
// Remove each unused section marker (keep content, remove only marker comments)
|
|
147
148
|
for (const sectionName of sectionNames) {
|
|
148
149
|
// Try all comment formats (// for JS/TS, {/* */} for JSX, # for YAML/Shell)
|
|
149
|
-
// Capture: START marker, content, END marker - replace with just content
|
|
150
|
-
// Include leading whitespace before markers to prevent indentation issues
|
|
151
150
|
const slashPattern = new RegExp(
|
|
152
151
|
`^[ \\t]*\\/\\/ ${sectionName}_START\\n([\\s\\S]*?)^[ \\t]*\\/\\/ ${sectionName}_END\\n?`,
|
|
153
152
|
'gm'
|
|
@@ -185,14 +184,14 @@ async function cleanupSectionMarkers(serviceName, serviceConfig, appliedVariants
|
|
|
185
184
|
totalCleaned++;
|
|
186
185
|
}
|
|
187
186
|
} catch (error) {
|
|
188
|
-
|
|
187
|
+
logger.warn(`Could not clean markers in ${filePath}: ${error.message}`);
|
|
189
188
|
}
|
|
190
189
|
}
|
|
191
190
|
|
|
192
191
|
if (totalCleaned > 0) {
|
|
193
|
-
|
|
192
|
+
logger.detail(`Cleaned section markers in ${totalCleaned} file(s)`, 3);
|
|
194
193
|
} else {
|
|
195
|
-
|
|
194
|
+
logger.detail('No unused section markers found', 3);
|
|
196
195
|
}
|
|
197
196
|
}
|
|
198
197
|
|
|
@@ -206,11 +205,11 @@ async function cleanupSectionMarkers(serviceName, serviceConfig, appliedVariants
|
|
|
206
205
|
*/
|
|
207
206
|
async function copyVariantFiles(variantName, files, filesDir, destination, templateRoot) {
|
|
208
207
|
if (!files || files.length === 0) {
|
|
209
|
-
|
|
208
|
+
logger.detail(`No files to copy for ${variantName}`, 3);
|
|
210
209
|
return;
|
|
211
210
|
}
|
|
212
211
|
|
|
213
|
-
|
|
212
|
+
logger.detail(`Copying ${files.length} file(s) for ${variantName}`, 3);
|
|
214
213
|
|
|
215
214
|
const variantFilesPath = path.join(templateRoot, filesDir, variantName);
|
|
216
215
|
|
|
@@ -219,22 +218,18 @@ async function copyVariantFiles(variantName, files, filesDir, destination, templ
|
|
|
219
218
|
const destPath = path.join(destination, filePath);
|
|
220
219
|
|
|
221
220
|
try {
|
|
222
|
-
// Check if source exists
|
|
223
221
|
if (!await fs.pathExists(sourcePath)) {
|
|
224
|
-
|
|
222
|
+
logger.warn(`Source not found: ${filePath}, skipping`);
|
|
225
223
|
continue;
|
|
226
224
|
}
|
|
227
225
|
|
|
228
|
-
// Create parent directory if needed
|
|
229
226
|
await fs.ensureDir(path.dirname(destPath));
|
|
230
|
-
|
|
231
|
-
// Copy file or directory
|
|
232
227
|
await fs.copy(sourcePath, destPath, { overwrite: true });
|
|
233
228
|
|
|
234
229
|
const isDir = (await fs.stat(sourcePath)).isDirectory();
|
|
235
|
-
|
|
230
|
+
logger.detail(`Copied ${isDir ? 'folder' : 'file'}: ${filePath}`, 4);
|
|
236
231
|
} catch (error) {
|
|
237
|
-
|
|
232
|
+
logger.warn(`Could not copy ${filePath}: ${error.message}`);
|
|
238
233
|
}
|
|
239
234
|
}
|
|
240
235
|
}
|
|
@@ -249,45 +244,41 @@ async function copyVariantFiles(variantName, files, filesDir, destination, templ
|
|
|
249
244
|
*/
|
|
250
245
|
async function insertVariantSections(variantName, sections, sectionsDir, destination, templateRoot) {
|
|
251
246
|
if (!sections || Object.keys(sections).length === 0) {
|
|
252
|
-
|
|
247
|
+
logger.detail(`No sections to insert for ${variantName}`, 3);
|
|
253
248
|
return;
|
|
254
249
|
}
|
|
255
250
|
|
|
256
251
|
const sectionFiles = Object.keys(sections);
|
|
257
|
-
|
|
252
|
+
logger.detail(`Inserting sections into ${sectionFiles.length} file(s)`, 3);
|
|
258
253
|
|
|
259
254
|
const variantSectionsPath = path.join(templateRoot, sectionsDir, variantName);
|
|
260
255
|
|
|
261
256
|
for (const [filePath, sectionNames] of Object.entries(sections)) {
|
|
262
257
|
const targetFilePath = path.join(destination, filePath);
|
|
263
258
|
|
|
264
|
-
// Check if target file exists
|
|
265
259
|
if (!await fs.pathExists(targetFilePath)) {
|
|
266
|
-
|
|
260
|
+
logger.warn(`Target file not found: ${filePath}, skipping sections`);
|
|
267
261
|
continue;
|
|
268
262
|
}
|
|
269
263
|
|
|
270
|
-
|
|
264
|
+
logger.detail(`Processing ${filePath}`, 4);
|
|
271
265
|
|
|
272
266
|
for (const sectionName of sectionNames) {
|
|
273
267
|
try {
|
|
274
|
-
// Read section content from file
|
|
275
268
|
const fileName = path.basename(filePath);
|
|
276
269
|
const sectionFileName = `${fileName}.${sectionName}`;
|
|
277
270
|
const sectionFilePath = path.join(variantSectionsPath, sectionFileName);
|
|
278
271
|
|
|
279
272
|
if (!await fs.pathExists(sectionFilePath)) {
|
|
280
|
-
|
|
273
|
+
logger.warn(`Section file not found: ${sectionFileName}, skipping`);
|
|
281
274
|
continue;
|
|
282
275
|
}
|
|
283
276
|
|
|
284
277
|
const sectionContent = await fs.readFile(sectionFilePath, 'utf-8');
|
|
285
|
-
|
|
286
|
-
// Insert section content into target file
|
|
287
278
|
await replaceSection(targetFilePath, sectionName, sectionContent);
|
|
288
|
-
|
|
279
|
+
logger.detail(`Inserted [${sectionName}]`, 5);
|
|
289
280
|
} catch (error) {
|
|
290
|
-
|
|
281
|
+
logger.warn(`Could not insert section ${sectionName}: ${error.message}`);
|
|
291
282
|
}
|
|
292
283
|
}
|
|
293
284
|
}
|