@launchframe/cli 0.1.6
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/README.md +59 -0
- package/package.json +45 -0
- package/src/commands/deploy-configure.js +219 -0
- package/src/commands/deploy-init.js +277 -0
- package/src/commands/deploy-set-env.js +232 -0
- package/src/commands/deploy-up.js +144 -0
- package/src/commands/docker-build.js +44 -0
- package/src/commands/docker-destroy.js +93 -0
- package/src/commands/docker-down.js +44 -0
- package/src/commands/docker-logs.js +69 -0
- package/src/commands/docker-up.js +73 -0
- package/src/commands/doctor.js +20 -0
- package/src/commands/help.js +79 -0
- package/src/commands/init.js +126 -0
- package/src/commands/service.js +569 -0
- package/src/commands/waitlist-deploy.js +231 -0
- package/src/commands/waitlist-down.js +50 -0
- package/src/commands/waitlist-logs.js +55 -0
- package/src/commands/waitlist-up.js +95 -0
- package/src/generator.js +190 -0
- package/src/index.js +158 -0
- package/src/prompts.js +200 -0
- package/src/services/registry.js +48 -0
- package/src/services/variant-config.js +349 -0
- package/src/utils/docker-helper.js +237 -0
- package/src/utils/env-generator.js +88 -0
- package/src/utils/env-validator.js +75 -0
- package/src/utils/file-ops.js +87 -0
- package/src/utils/project-helpers.js +104 -0
- package/src/utils/section-replacer.js +71 -0
- package/src/utils/ssh-helper.js +220 -0
- package/src/utils/variable-replacer.js +95 -0
- package/src/utils/variant-processor.js +313 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variant Processor
|
|
3
|
+
*
|
|
4
|
+
* Applies variant modifications to generated projects.
|
|
5
|
+
* Strategy: Copy base โ Copy variant FILES โ Insert variant SECTIONS
|
|
6
|
+
*
|
|
7
|
+
* Clear distinction:
|
|
8
|
+
* - SECTIONS: Code snippets inserted into base template files
|
|
9
|
+
* - FILES: Complete files/folders copied to the project
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { replaceSection } = require('./section-replacer');
|
|
13
|
+
const { copyDirectory } = require('./file-ops');
|
|
14
|
+
const { replaceVariables } = require('./variable-replacer');
|
|
15
|
+
const { getVariantConfig, getVariantsToApply } = require('../services/variant-config');
|
|
16
|
+
const fs = require('fs-extra');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Process service with variant modifications
|
|
21
|
+
*
|
|
22
|
+
* @param {string} serviceName - Service to process (backend, admin-portal, etc.)
|
|
23
|
+
* @param {object} variantChoices - User's variant selections (e.g., { tenancy: 'multi-tenant', userModel: 'b2b' })
|
|
24
|
+
* @param {string} destination - Destination directory for generated code
|
|
25
|
+
* @param {object} replacements - Template variable replacements (e.g., {{PROJECT_NAME}})
|
|
26
|
+
* @param {string} templateRoot - Root directory of templates
|
|
27
|
+
*/
|
|
28
|
+
async function processServiceVariant(
|
|
29
|
+
serviceName,
|
|
30
|
+
variantChoices,
|
|
31
|
+
destination,
|
|
32
|
+
replacements,
|
|
33
|
+
templateRoot
|
|
34
|
+
) {
|
|
35
|
+
console.log(`\n๐ฆ Processing ${serviceName} with choices:`, variantChoices);
|
|
36
|
+
|
|
37
|
+
const serviceConfig = getVariantConfig(serviceName);
|
|
38
|
+
if (!serviceConfig) {
|
|
39
|
+
throw new Error(`No variant configuration found for service: ${serviceName}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const basePath = path.join(templateRoot, serviceConfig.base);
|
|
43
|
+
|
|
44
|
+
// Step 1: Copy base template (minimal - B2B + single-tenant)
|
|
45
|
+
console.log(` ๐ Copying base template from ${serviceConfig.base}`);
|
|
46
|
+
await copyDirectory(basePath, destination, {
|
|
47
|
+
exclude: ['node_modules', '.git', 'dist', '.env', 'templates']
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Step 2: Determine which variants to apply
|
|
51
|
+
const variantsToApply = getVariantsToApply(variantChoices);
|
|
52
|
+
|
|
53
|
+
if (variantsToApply.length === 0) {
|
|
54
|
+
console.log(` โ
Using base template (no variants to apply)`);
|
|
55
|
+
} else {
|
|
56
|
+
console.log(` ๐ง Applying variants: ${variantsToApply.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Step 3: Apply each variant
|
|
60
|
+
for (const variantName of variantsToApply) {
|
|
61
|
+
const variantConfig = serviceConfig.variants[variantName];
|
|
62
|
+
|
|
63
|
+
if (!variantConfig) {
|
|
64
|
+
console.warn(` โ ๏ธ No configuration found for variant: ${variantName}, skipping`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`\n โจ Applying ${variantName} variant:`);
|
|
69
|
+
|
|
70
|
+
// Step 3a: Copy variant FILES
|
|
71
|
+
await copyVariantFiles(
|
|
72
|
+
variantName,
|
|
73
|
+
variantConfig.files || [],
|
|
74
|
+
serviceConfig.filesDir,
|
|
75
|
+
destination,
|
|
76
|
+
templateRoot
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Step 3b: Insert variant SECTIONS
|
|
80
|
+
await insertVariantSections(
|
|
81
|
+
variantName,
|
|
82
|
+
variantConfig.sections || {},
|
|
83
|
+
serviceConfig.sectionsDir,
|
|
84
|
+
destination,
|
|
85
|
+
templateRoot
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Step 4: Clean up unused section markers
|
|
90
|
+
console.log(`\n ๐งน Cleaning up unused section markers`);
|
|
91
|
+
await cleanupSectionMarkers(serviceName, serviceConfig, variantsToApply, destination);
|
|
92
|
+
|
|
93
|
+
// Step 5: Replace template variables ({{PROJECT_NAME}}, etc.)
|
|
94
|
+
console.log(`\n ๐ค Replacing template variables`);
|
|
95
|
+
await replaceVariables(destination, replacements);
|
|
96
|
+
|
|
97
|
+
console.log(` โ
${serviceName} processing complete\n`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clean up unused section markers from generated files
|
|
102
|
+
* Uses variant configuration to only process files with known sections
|
|
103
|
+
* @param {string} serviceName - Service name (backend, admin-portal, etc.)
|
|
104
|
+
* @param {object} serviceConfig - Service variant configuration
|
|
105
|
+
* @param {string[]} appliedVariants - Variants that were applied
|
|
106
|
+
* @param {string} destination - Destination directory
|
|
107
|
+
*/
|
|
108
|
+
async function cleanupSectionMarkers(serviceName, serviceConfig, appliedVariants, destination) {
|
|
109
|
+
const allVariants = Object.keys(serviceConfig.variants || {});
|
|
110
|
+
const unappliedVariants = allVariants.filter(v => !appliedVariants.includes(v));
|
|
111
|
+
|
|
112
|
+
if (unappliedVariants.length === 0) {
|
|
113
|
+
console.log(` โ No unused section markers to clean`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Collect all sections from unapplied variants
|
|
118
|
+
const sectionsToRemove = new Map(); // filePath -> Set of section names
|
|
119
|
+
|
|
120
|
+
for (const variantName of unappliedVariants) {
|
|
121
|
+
const variantConfig = serviceConfig.variants[variantName];
|
|
122
|
+
const sections = variantConfig?.sections || {};
|
|
123
|
+
|
|
124
|
+
for (const [filePath, sectionNames] of Object.entries(sections)) {
|
|
125
|
+
if (!sectionsToRemove.has(filePath)) {
|
|
126
|
+
sectionsToRemove.set(filePath, new Set());
|
|
127
|
+
}
|
|
128
|
+
sectionNames.forEach(name => sectionsToRemove.get(filePath).add(name));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let totalCleaned = 0;
|
|
133
|
+
|
|
134
|
+
// Process each file that has sections to remove
|
|
135
|
+
for (const [filePath, sectionNames] of sectionsToRemove.entries()) {
|
|
136
|
+
const targetFilePath = path.join(destination, filePath);
|
|
137
|
+
|
|
138
|
+
if (!await fs.pathExists(targetFilePath)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
let content = await fs.readFile(targetFilePath, 'utf-8');
|
|
144
|
+
let modified = false;
|
|
145
|
+
|
|
146
|
+
// Remove each unused section marker
|
|
147
|
+
for (const sectionName of sectionNames) {
|
|
148
|
+
// Try both comment formats
|
|
149
|
+
const regularPattern = new RegExp(
|
|
150
|
+
`\\/\\/ ${sectionName}_START\\n[\\s\\S]*?\\/\\/ ${sectionName}_END\\n?`,
|
|
151
|
+
'g'
|
|
152
|
+
);
|
|
153
|
+
const jsxPattern = new RegExp(
|
|
154
|
+
`\\{\\/\\* ${sectionName}_START \\*\\/\\}\\n[\\s\\S]*?\\{\\/\\* ${sectionName}_END \\*\\/\\}\\n?`,
|
|
155
|
+
'g'
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const beforeRegular = content;
|
|
159
|
+
content = content.replace(regularPattern, '');
|
|
160
|
+
if (content !== beforeRegular) {
|
|
161
|
+
modified = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const beforeJsx = content;
|
|
165
|
+
content = content.replace(jsxPattern, '');
|
|
166
|
+
if (content !== beforeJsx) {
|
|
167
|
+
modified = true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (modified) {
|
|
172
|
+
await fs.writeFile(targetFilePath, content, 'utf-8');
|
|
173
|
+
totalCleaned++;
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.warn(` โ ๏ธ Could not clean markers in ${filePath}:`, error.message);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (totalCleaned > 0) {
|
|
181
|
+
console.log(` โ Cleaned up section markers in ${totalCleaned} file(s)`);
|
|
182
|
+
} else {
|
|
183
|
+
console.log(` โ No unused section markers found`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Copy variant files to destination
|
|
189
|
+
* @param {string} variantName - Variant name (e.g., 'multi-tenant')
|
|
190
|
+
* @param {string[]} files - List of files/folders to copy
|
|
191
|
+
* @param {string} filesDir - Variant files directory
|
|
192
|
+
* @param {string} destination - Destination directory
|
|
193
|
+
* @param {string} templateRoot - Template root directory
|
|
194
|
+
*/
|
|
195
|
+
async function copyVariantFiles(variantName, files, filesDir, destination, templateRoot) {
|
|
196
|
+
if (!files || files.length === 0) {
|
|
197
|
+
console.log(` ๐ No files to copy for ${variantName}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(` ๐ Copying ${files.length} file(s)/folder(s):`);
|
|
202
|
+
|
|
203
|
+
const variantFilesPath = path.join(templateRoot, filesDir, variantName);
|
|
204
|
+
|
|
205
|
+
for (const filePath of files) {
|
|
206
|
+
const sourcePath = path.join(variantFilesPath, filePath);
|
|
207
|
+
const destPath = path.join(destination, filePath);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
// Check if source exists
|
|
211
|
+
if (!await fs.pathExists(sourcePath)) {
|
|
212
|
+
console.warn(` โ ๏ธ Source not found: ${filePath}, skipping`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Create parent directory if needed
|
|
217
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
218
|
+
|
|
219
|
+
// Copy file or directory
|
|
220
|
+
await fs.copy(sourcePath, destPath, { overwrite: true });
|
|
221
|
+
|
|
222
|
+
const isDir = (await fs.stat(sourcePath)).isDirectory();
|
|
223
|
+
console.log(` โ Copied ${isDir ? 'folder' : 'file'}: ${filePath}`);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.warn(` โ ๏ธ Could not copy ${filePath}:`, error.message);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Insert variant section content into base template files
|
|
232
|
+
* @param {string} variantName - Variant name (e.g., 'multi-tenant')
|
|
233
|
+
* @param {object} sections - Section configuration { 'file.ts': ['SECTION1', 'SECTION2'] }
|
|
234
|
+
* @param {string} sectionsDir - Variant sections directory
|
|
235
|
+
* @param {string} destination - Destination directory
|
|
236
|
+
* @param {string} templateRoot - Template root directory
|
|
237
|
+
*/
|
|
238
|
+
async function insertVariantSections(variantName, sections, sectionsDir, destination, templateRoot) {
|
|
239
|
+
if (!sections || Object.keys(sections).length === 0) {
|
|
240
|
+
console.log(` โ๏ธ No sections to insert for ${variantName}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const sectionFiles = Object.keys(sections);
|
|
245
|
+
console.log(` โ๏ธ Inserting sections into ${sectionFiles.length} file(s):`);
|
|
246
|
+
|
|
247
|
+
const variantSectionsPath = path.join(templateRoot, sectionsDir, variantName);
|
|
248
|
+
|
|
249
|
+
for (const [filePath, sectionNames] of Object.entries(sections)) {
|
|
250
|
+
const targetFilePath = path.join(destination, filePath);
|
|
251
|
+
|
|
252
|
+
// Check if target file exists
|
|
253
|
+
if (!await fs.pathExists(targetFilePath)) {
|
|
254
|
+
console.warn(` โ ๏ธ Target file not found: ${filePath}, skipping sections`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
console.log(` ๐ ${filePath}:`);
|
|
259
|
+
|
|
260
|
+
for (const sectionName of sectionNames) {
|
|
261
|
+
try {
|
|
262
|
+
// Read section content from file
|
|
263
|
+
const fileName = path.basename(filePath);
|
|
264
|
+
const sectionFileName = `${fileName}.${sectionName}`;
|
|
265
|
+
const sectionFilePath = path.join(variantSectionsPath, sectionFileName);
|
|
266
|
+
|
|
267
|
+
if (!await fs.pathExists(sectionFilePath)) {
|
|
268
|
+
console.warn(` โ ๏ธ Section file not found: ${sectionFileName}, skipping`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const sectionContent = await fs.readFile(sectionFilePath, 'utf-8');
|
|
273
|
+
|
|
274
|
+
// Insert section content into target file
|
|
275
|
+
await replaceSection(targetFilePath, sectionName, sectionContent);
|
|
276
|
+
console.log(` โ Inserted [${sectionName}]`);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.warn(` โ ๏ธ Could not insert section ${sectionName}:`, error.message);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Validate variant choices
|
|
286
|
+
* @param {object} variantChoices - User's variant selections
|
|
287
|
+
* @returns {object} Validation result { valid: boolean, errors: string[] }
|
|
288
|
+
*/
|
|
289
|
+
function validateVariantChoices(variantChoices) {
|
|
290
|
+
const errors = [];
|
|
291
|
+
|
|
292
|
+
// Validate tenancy choice
|
|
293
|
+
const tenancy = variantChoices.tenancy;
|
|
294
|
+
if (tenancy && !['multi-tenant', 'single-tenant'].includes(tenancy)) {
|
|
295
|
+
errors.push(`Invalid tenancy choice: ${tenancy}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Validate user model choice
|
|
299
|
+
const userModel = variantChoices.userModel;
|
|
300
|
+
if (userModel && !['b2b', 'b2b2c'].includes(userModel)) {
|
|
301
|
+
errors.push(`Invalid user model choice: ${userModel}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
valid: errors.length === 0,
|
|
306
|
+
errors
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = {
|
|
311
|
+
processServiceVariant,
|
|
312
|
+
validateVariantChoices
|
|
313
|
+
};
|