@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
package/src/utils/ssh-helper.js
CHANGED
|
@@ -209,6 +209,54 @@ function showDeployKeyInstructions(vpsUser, vpsHost, githubOrg, projectName) {
|
|
|
209
209
|
console.log(chalk.gray(' launchframe deploy:init\n'));
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Pull Docker images on VPS
|
|
214
|
+
* @param {string} vpsUser - SSH username
|
|
215
|
+
* @param {string} vpsHost - VPS hostname or IP
|
|
216
|
+
* @param {string} vpsAppFolder - App folder path on VPS
|
|
217
|
+
* @returns {Promise<void>}
|
|
218
|
+
*/
|
|
219
|
+
async function pullImagesOnVPS(vpsUser, vpsHost, vpsAppFolder) {
|
|
220
|
+
const ora = require('ora');
|
|
221
|
+
|
|
222
|
+
const spinner = ora('Pulling images on VPS...').start();
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
await execAsync(
|
|
226
|
+
`ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull"`,
|
|
227
|
+
{ timeout: 600000 } // 10 minutes
|
|
228
|
+
);
|
|
229
|
+
spinner.succeed('Images pulled on VPS');
|
|
230
|
+
} catch (error) {
|
|
231
|
+
spinner.fail('Failed to pull images on VPS');
|
|
232
|
+
throw new Error(`Failed to pull images: ${error.message}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Restart services on VPS
|
|
238
|
+
* @param {string} vpsUser - SSH username
|
|
239
|
+
* @param {string} vpsHost - VPS hostname or IP
|
|
240
|
+
* @param {string} vpsAppFolder - App folder path on VPS
|
|
241
|
+
* @returns {Promise<void>}
|
|
242
|
+
*/
|
|
243
|
+
async function restartServicesOnVPS(vpsUser, vpsHost, vpsAppFolder) {
|
|
244
|
+
const ora = require('ora');
|
|
245
|
+
|
|
246
|
+
const spinner = ora('Restarting services...').start();
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await execAsync(
|
|
250
|
+
`ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d"`,
|
|
251
|
+
{ timeout: 300000 } // 5 minutes
|
|
252
|
+
);
|
|
253
|
+
spinner.succeed('Services restarted');
|
|
254
|
+
} catch (error) {
|
|
255
|
+
spinner.fail('Failed to restart services');
|
|
256
|
+
throw new Error(`Failed to restart services: ${error.message}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
212
260
|
module.exports = {
|
|
213
261
|
testSSHConnection,
|
|
214
262
|
checkSSHKeys,
|
|
@@ -216,5 +264,7 @@ module.exports = {
|
|
|
216
264
|
copyFileToVPS,
|
|
217
265
|
copyDirectoryToVPS,
|
|
218
266
|
checkRepoPrivacy,
|
|
219
|
-
showDeployKeyInstructions
|
|
267
|
+
showDeployKeyInstructions,
|
|
268
|
+
pullImagesOnVPS,
|
|
269
|
+
restartServicesOnVPS
|
|
220
270
|
};
|
|
@@ -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,35 @@ 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
|
+
// Silently skip - not every service needs every variant combination
|
|
66
|
+
// (e.g., b2b2c_multi-tenant may only apply to backend, not admin-portal)
|
|
67
|
+
logger.detail(`Skipping ${variantName} (not applicable to this service)`, 3);
|
|
65
68
|
continue;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
logger.detail(`Applying ${variantName} variant`, 2);
|
|
69
72
|
|
|
70
|
-
//
|
|
73
|
+
// Copy variant FILES
|
|
71
74
|
await copyVariantFiles(
|
|
72
75
|
variantName,
|
|
73
76
|
variantConfig.files || [],
|
|
@@ -76,7 +79,7 @@ async function processServiceVariant(
|
|
|
76
79
|
templateRoot
|
|
77
80
|
);
|
|
78
81
|
|
|
79
|
-
//
|
|
82
|
+
// Insert variant SECTIONS
|
|
80
83
|
await insertVariantSections(
|
|
81
84
|
variantName,
|
|
82
85
|
variantConfig.sections || {},
|
|
@@ -86,15 +89,15 @@ async function processServiceVariant(
|
|
|
86
89
|
);
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
//
|
|
90
|
-
|
|
92
|
+
// Clean up unused section markers
|
|
93
|
+
logger.detail('Cleaning up unused section markers', 2);
|
|
91
94
|
await cleanupSectionMarkers(serviceName, serviceConfig, variantsToApply, destination);
|
|
92
95
|
|
|
93
|
-
//
|
|
94
|
-
|
|
96
|
+
// Replace template variables
|
|
97
|
+
logger.detail('Replacing template variables', 2);
|
|
95
98
|
await replaceVariables(destination, replacements);
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
logger.detail(`${serviceName} complete`, 2);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/**
|
|
@@ -110,7 +113,7 @@ async function cleanupSectionMarkers(serviceName, serviceConfig, appliedVariants
|
|
|
110
113
|
const unappliedVariants = allVariants.filter(v => !appliedVariants.includes(v));
|
|
111
114
|
|
|
112
115
|
if (unappliedVariants.length === 0) {
|
|
113
|
-
|
|
116
|
+
logger.detail('No unused section markers to clean', 3);
|
|
114
117
|
return;
|
|
115
118
|
}
|
|
116
119
|
|
|
@@ -146,8 +149,6 @@ async function cleanupSectionMarkers(serviceName, serviceConfig, appliedVariants
|
|
|
146
149
|
// Remove each unused section marker (keep content, remove only marker comments)
|
|
147
150
|
for (const sectionName of sectionNames) {
|
|
148
151
|
// 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
152
|
const slashPattern = new RegExp(
|
|
152
153
|
`^[ \\t]*\\/\\/ ${sectionName}_START\\n([\\s\\S]*?)^[ \\t]*\\/\\/ ${sectionName}_END\\n?`,
|
|
153
154
|
'gm'
|
|
@@ -185,14 +186,14 @@ async function cleanupSectionMarkers(serviceName, serviceConfig, appliedVariants
|
|
|
185
186
|
totalCleaned++;
|
|
186
187
|
}
|
|
187
188
|
} catch (error) {
|
|
188
|
-
|
|
189
|
+
logger.warn(`Could not clean markers in ${filePath}: ${error.message}`);
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
if (totalCleaned > 0) {
|
|
193
|
-
|
|
194
|
+
logger.detail(`Cleaned section markers in ${totalCleaned} file(s)`, 3);
|
|
194
195
|
} else {
|
|
195
|
-
|
|
196
|
+
logger.detail('No unused section markers found', 3);
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
|
|
@@ -206,11 +207,11 @@ async function cleanupSectionMarkers(serviceName, serviceConfig, appliedVariants
|
|
|
206
207
|
*/
|
|
207
208
|
async function copyVariantFiles(variantName, files, filesDir, destination, templateRoot) {
|
|
208
209
|
if (!files || files.length === 0) {
|
|
209
|
-
|
|
210
|
+
logger.detail(`No files to copy for ${variantName}`, 3);
|
|
210
211
|
return;
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
|
|
214
|
+
logger.detail(`Copying ${files.length} file(s) for ${variantName}`, 3);
|
|
214
215
|
|
|
215
216
|
const variantFilesPath = path.join(templateRoot, filesDir, variantName);
|
|
216
217
|
|
|
@@ -219,22 +220,18 @@ async function copyVariantFiles(variantName, files, filesDir, destination, templ
|
|
|
219
220
|
const destPath = path.join(destination, filePath);
|
|
220
221
|
|
|
221
222
|
try {
|
|
222
|
-
// Check if source exists
|
|
223
223
|
if (!await fs.pathExists(sourcePath)) {
|
|
224
|
-
|
|
224
|
+
logger.warn(`Source not found: ${filePath}, skipping`);
|
|
225
225
|
continue;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
// Create parent directory if needed
|
|
229
228
|
await fs.ensureDir(path.dirname(destPath));
|
|
230
|
-
|
|
231
|
-
// Copy file or directory
|
|
232
229
|
await fs.copy(sourcePath, destPath, { overwrite: true });
|
|
233
230
|
|
|
234
231
|
const isDir = (await fs.stat(sourcePath)).isDirectory();
|
|
235
|
-
|
|
232
|
+
logger.detail(`Copied ${isDir ? 'folder' : 'file'}: ${filePath}`, 4);
|
|
236
233
|
} catch (error) {
|
|
237
|
-
|
|
234
|
+
logger.warn(`Could not copy ${filePath}: ${error.message}`);
|
|
238
235
|
}
|
|
239
236
|
}
|
|
240
237
|
}
|
|
@@ -249,45 +246,41 @@ async function copyVariantFiles(variantName, files, filesDir, destination, templ
|
|
|
249
246
|
*/
|
|
250
247
|
async function insertVariantSections(variantName, sections, sectionsDir, destination, templateRoot) {
|
|
251
248
|
if (!sections || Object.keys(sections).length === 0) {
|
|
252
|
-
|
|
249
|
+
logger.detail(`No sections to insert for ${variantName}`, 3);
|
|
253
250
|
return;
|
|
254
251
|
}
|
|
255
252
|
|
|
256
253
|
const sectionFiles = Object.keys(sections);
|
|
257
|
-
|
|
254
|
+
logger.detail(`Inserting sections into ${sectionFiles.length} file(s)`, 3);
|
|
258
255
|
|
|
259
256
|
const variantSectionsPath = path.join(templateRoot, sectionsDir, variantName);
|
|
260
257
|
|
|
261
258
|
for (const [filePath, sectionNames] of Object.entries(sections)) {
|
|
262
259
|
const targetFilePath = path.join(destination, filePath);
|
|
263
260
|
|
|
264
|
-
// Check if target file exists
|
|
265
261
|
if (!await fs.pathExists(targetFilePath)) {
|
|
266
|
-
|
|
262
|
+
logger.warn(`Target file not found: ${filePath}, skipping sections`);
|
|
267
263
|
continue;
|
|
268
264
|
}
|
|
269
265
|
|
|
270
|
-
|
|
266
|
+
logger.detail(`Processing ${filePath}`, 4);
|
|
271
267
|
|
|
272
268
|
for (const sectionName of sectionNames) {
|
|
273
269
|
try {
|
|
274
|
-
// Read section content from file
|
|
275
270
|
const fileName = path.basename(filePath);
|
|
276
271
|
const sectionFileName = `${fileName}.${sectionName}`;
|
|
277
272
|
const sectionFilePath = path.join(variantSectionsPath, sectionFileName);
|
|
278
273
|
|
|
279
274
|
if (!await fs.pathExists(sectionFilePath)) {
|
|
280
|
-
|
|
275
|
+
logger.warn(`Section file not found: ${sectionFileName}, skipping`);
|
|
281
276
|
continue;
|
|
282
277
|
}
|
|
283
278
|
|
|
284
279
|
const sectionContent = await fs.readFile(sectionFilePath, 'utf-8');
|
|
285
|
-
|
|
286
|
-
// Insert section content into target file
|
|
287
280
|
await replaceSection(targetFilePath, sectionName, sectionContent);
|
|
288
|
-
|
|
281
|
+
logger.detail(`Inserted [${sectionName}]`, 5);
|
|
289
282
|
} catch (error) {
|
|
290
|
-
|
|
283
|
+
logger.warn(`Could not insert section ${sectionName}: ${error.message}`);
|
|
291
284
|
}
|
|
292
285
|
}
|
|
293
286
|
}
|