@launchframe/cli 0.1.11 → 1.0.0-beta.10

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.
@@ -43,7 +43,7 @@ async function loginToGHCR(githubOrg, ghcrToken) {
43
43
 
44
44
  /**
45
45
  * Build and push a Docker image
46
- * @param {string} serviceName - Service name (e.g., 'backend', 'admin-frontend')
46
+ * @param {string} serviceName - Service name (e.g., 'backend', 'admin-portal')
47
47
  * @param {string} contextDir - Docker build context directory
48
48
  * @param {string} registry - Registry URL (e.g., 'ghcr.io/myorg')
49
49
  * @param {string} projectName - Project name
@@ -111,57 +111,77 @@ function loadEnvFile(envFilePath) {
111
111
  * @param {string} projectName - Project name
112
112
  * @param {string} githubOrg - GitHub organization/username
113
113
  * @param {string} envFilePath - Path to .env.prod file
114
+ * @param {string[]} installedServices - List of installed services from .launchframe
114
115
  * @returns {Promise<void>}
115
116
  */
116
- async function buildFullAppImages(projectRoot, projectName, githubOrg, envFilePath) {
117
+ async function buildFullAppImages(projectRoot, projectName, githubOrg, envFilePath, installedServices = []) {
117
118
  const registry = `ghcr.io/${githubOrg}`;
118
119
  const envVars = loadEnvFile(envFilePath);
119
120
 
120
121
  console.log(chalk.yellow('\nšŸ“¦ Building production Docker images...\n'));
121
122
  console.log(chalk.gray('This may take 10-20 minutes depending on your system.\n'));
123
+ console.log(chalk.gray(`Services to build: ${installedServices.join(', ')}\n`));
124
+
125
+ // Build backend (always required)
126
+ if (installedServices.includes('backend')) {
127
+ await buildAndPushImage(
128
+ 'backend',
129
+ path.join(projectRoot, 'backend'),
130
+ registry,
131
+ projectName
132
+ );
133
+ }
134
+
135
+ // Build admin-portal (always required)
136
+ if (installedServices.includes('admin-portal')) {
137
+ await buildAndPushImage(
138
+ 'admin-portal',
139
+ path.join(projectRoot, 'admin-portal'),
140
+ registry,
141
+ projectName
142
+ );
143
+ }
144
+
145
+ // Build customers-portal (only if installed - B2B2C mode)
146
+ if (installedServices.includes('customers-portal')) {
147
+ await buildAndPushImage(
148
+ 'customers-portal',
149
+ path.join(projectRoot, 'customers-portal'),
150
+ registry,
151
+ projectName
152
+ );
153
+ }
122
154
 
123
- // Build backend
124
- await buildAndPushImage(
125
- 'backend',
126
- path.join(projectRoot, 'backend'),
127
- registry,
128
- projectName
129
- );
130
-
131
- // Build admin-frontend
132
- await buildAndPushImage(
133
- 'admin-frontend',
134
- path.join(projectRoot, 'admin-frontend'),
135
- registry,
136
- projectName
137
- );
138
-
139
- // Build customers-frontend
140
- await buildAndPushImage(
141
- 'customers-frontend',
142
- path.join(projectRoot, 'frontend'),
143
- registry,
144
- projectName
145
- );
146
-
147
- // Build website (requires build args)
148
- const websiteBuildArgs = [
149
- `APP_NAME=${envVars.APP_NAME || ''}`,
150
- `DOCS_URL=${envVars.DOCS_URL || ''}`,
151
- `CONTACT_EMAIL=${envVars.CONTACT_EMAIL || ''}`,
152
- `CTA_LINK=${envVars.CTA_LINK || ''}`,
153
- `LIVE_DEMO_URL=${envVars.LIVE_DEMO_URL || ''}`,
154
- `MIXPANEL_PROJECT_TOKEN=${envVars.MIXPANEL_PROJECT_TOKEN || ''}`,
155
- `GOOGLE_ANALYTICS_ID=${envVars.GOOGLE_ANALYTICS_ID || ''}`
156
- ];
157
-
158
- await buildAndPushImage(
159
- 'website',
160
- path.join(projectRoot, 'website'),
161
- registry,
162
- projectName,
163
- websiteBuildArgs
164
- );
155
+ // Build website (always required)
156
+ if (installedServices.includes('website')) {
157
+ const websiteBuildArgs = [
158
+ `APP_NAME=${envVars.APP_NAME || ''}`,
159
+ `DOCS_URL=${envVars.DOCS_URL || ''}`,
160
+ `CONTACT_EMAIL=${envVars.CONTACT_EMAIL || ''}`,
161
+ `CTA_LINK=${envVars.CTA_LINK || ''}`,
162
+ `LIVE_DEMO_URL=${envVars.LIVE_DEMO_URL || ''}`,
163
+ `MIXPANEL_PROJECT_TOKEN=${envVars.MIXPANEL_PROJECT_TOKEN || ''}`,
164
+ `GOOGLE_ANALYTICS_ID=${envVars.GOOGLE_ANALYTICS_ID || ''}`
165
+ ];
166
+
167
+ await buildAndPushImage(
168
+ 'website',
169
+ path.join(projectRoot, 'website'),
170
+ registry,
171
+ projectName,
172
+ websiteBuildArgs
173
+ );
174
+ }
175
+
176
+ // Build docs (optional service - VitePress documentation)
177
+ if (installedServices.includes('docs')) {
178
+ await buildAndPushImage(
179
+ 'docs',
180
+ path.join(projectRoot, 'docs'),
181
+ registry,
182
+ projectName
183
+ );
184
+ }
165
185
  }
166
186
 
167
187
  /**
@@ -196,6 +216,8 @@ async function buildWaitlistImage(projectRoot, projectName, githubOrg) {
196
216
  `AIRTABLE_TABLE_NAME=${envVars.AIRTABLE_TABLE_NAME || 'Waitlist'}`,
197
217
  `NEXT_PUBLIC_PROJECT_NAME=${envVars.NEXT_PUBLIC_PROJECT_NAME || envVars.PROJECT_NAME || ''}`,
198
218
  `NEXT_PUBLIC_SITE_URL=${envVars.NEXT_PUBLIC_SITE_URL || ''}`,
219
+ `NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=${envVars.NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN || ''}`,
220
+ `NEXT_PUBLIC_MIXPANEL_DATA_RESIDENCY=${envVars.NEXT_PUBLIC_MIXPANEL_DATA_RESIDENCY || ''}`,
199
221
  `PROJECT_NAME=${envVars.PROJECT_NAME || ''}`,
200
222
  `PRIMARY_DOMAIN=${envVars.PRIMARY_DOMAIN || ''}`
201
223
  ];
@@ -0,0 +1,67 @@
1
+ const { execSync } = require('child_process');
2
+ const chalk = require('chalk');
3
+
4
+ /**
5
+ * Create a clickable terminal link using OSC 8 escape sequence
6
+ * Works in modern terminals (iTerm2, Hyper, Windows Terminal, VS Code, etc.)
7
+ * Falls back gracefully to plain text in unsupported terminals
8
+ * @param {string} text - Display text
9
+ * @param {string} url - Target URL
10
+ * @returns {string} Formatted link
11
+ */
12
+ function makeClickable(text, url) {
13
+ return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
14
+ }
15
+
16
+ /**
17
+ * Check if user has SSH access to LaunchFrame services repository
18
+ * @returns {Promise<{hasAccess: boolean, error?: string}>}
19
+ */
20
+ async function checkGitHubAccess() {
21
+ try {
22
+ // Test SSH access by checking if we can list remote refs
23
+ execSync(
24
+ 'git ls-remote git@github.com:launchframe-dev/services.git HEAD',
25
+ {
26
+ timeout: 15000,
27
+ stdio: 'pipe' // Don't show output
28
+ }
29
+ );
30
+ return { hasAccess: true };
31
+ } catch (error) {
32
+ return {
33
+ hasAccess: false,
34
+ error: error.message
35
+ };
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Display message when user doesn't have access to services repository
41
+ * Guides them to either purchase or setup SSH keys
42
+ */
43
+ function showAccessDeniedMessage() {
44
+ const purchaseUrl = 'https://buy.polar.sh/polar_cl_Zy4YqEwhoIEdUrAH8vHaWuZtwZuv306sYMnq118MbKi';
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 services repository\n'));
48
+
49
+ console.log(chalk.white('This could mean:\n'));
50
+ console.log(chalk.gray(' 1. You haven\'t purchased LaunchFrame yet'));
51
+ console.log(chalk.gray(' 2. You purchased but haven\'t added your SSH key to the repo\n'));
52
+
53
+ console.log(chalk.cyan('→ New customers:'));
54
+ console.log(' ' + chalk.blue.bold.underline(makeClickable('Purchase LaunchFrame', purchaseUrl)));
55
+ console.log(' ' + chalk.cyan(purchaseUrl + '\n'));
56
+
57
+ console.log(chalk.cyan('→ Existing customers:'));
58
+ console.log(' ' + chalk.blue.bold.underline(makeClickable('Setup SSH key (docs)', docsUrl)));
59
+ console.log(' ' + chalk.cyan(docsUrl + '\n'));
60
+
61
+ console.log(chalk.gray('After setup, run: launchframe init\n'));
62
+ }
63
+
64
+ module.exports = {
65
+ checkGitHubAccess,
66
+ showAccessDeniedMessage
67
+ };
@@ -7,7 +7,11 @@ const chalk = require('chalk');
7
7
  */
8
8
  function isLaunchFrameProject() {
9
9
  const markerPath = path.join(process.cwd(), '.launchframe');
10
- return fs.existsSync(markerPath);
10
+ try {
11
+ return fs.existsSync(markerPath) && fs.statSync(markerPath).isFile();
12
+ } catch (error) {
13
+ return false;
14
+ }
11
15
  }
12
16
 
13
17
  /**
@@ -76,7 +80,7 @@ function addInstalledComponent(componentName) {
76
80
  * @returns {string|null} Primary domain
77
81
  */
78
82
  function getPrimaryDomain(config) {
79
- return config.primaryDomain || null;
83
+ return config.deployment?.primaryDomain || null;
80
84
  }
81
85
 
82
86
  /**
@@ -9,32 +9,46 @@ const fs = require('fs-extra');
9
9
  async function replaceSection(filePath, sectionName, newContent) {
10
10
  const content = await fs.readFile(filePath, 'utf8');
11
11
 
12
- // Try both comment formats (// for regular comments, {/* */} for JSX)
13
- const startMarkerRegular = `// ${sectionName}_START`;
14
- const endMarkerRegular = `// ${sectionName}_END`;
12
+ // Try all comment formats (// for JS/TS, {/* */} for JSX, # for YAML/Shell)
13
+ const startMarkerSlash = `// ${sectionName}_START`;
14
+ const endMarkerSlash = `// ${sectionName}_END`;
15
15
  const startMarkerJSX = `{/* ${sectionName}_START */}`;
16
16
  const endMarkerJSX = `{/* ${sectionName}_END */}`;
17
+ const startMarkerHash = `# ${sectionName}_START`;
18
+ const endMarkerHash = `# ${sectionName}_END`;
17
19
 
18
- let startIndex = content.indexOf(startMarkerRegular);
19
- let endIndex = content.indexOf(endMarkerRegular);
20
- let isJSX = false;
20
+ let startIndex = content.indexOf(startMarkerSlash);
21
+ let endIndex = content.indexOf(endMarkerSlash);
21
22
 
22
- // If not found with regular comments, try JSX comments
23
+ // If not found with // comments, try JSX comments
23
24
  if (startIndex === -1 || endIndex === -1) {
24
25
  startIndex = content.indexOf(startMarkerJSX);
25
26
  endIndex = content.indexOf(endMarkerJSX);
26
- isJSX = true;
27
+ }
28
+
29
+ // If not found with JSX comments, try # comments (YAML/Shell)
30
+ if (startIndex === -1 || endIndex === -1) {
31
+ startIndex = content.indexOf(startMarkerHash);
32
+ endIndex = content.indexOf(endMarkerHash);
27
33
  }
28
34
 
29
35
  if (startIndex === -1 || endIndex === -1) {
30
36
  throw new Error(`Section markers not found: ${sectionName} in ${filePath}`);
31
37
  }
32
38
 
39
+ // Find the start of the start marker line (beginning of line, not just the marker)
40
+ let lineStart = content.lastIndexOf('\n', startIndex - 1);
41
+ if (lineStart === -1) {
42
+ lineStart = 0; // Marker is on first line
43
+ } else {
44
+ lineStart += 1; // Move past the newline to the start of the line
45
+ }
46
+
33
47
  // Find the end of the end marker line
34
48
  const endLineEnd = content.indexOf('\n', endIndex);
35
49
 
36
- // Construct new content - exclude both marker lines
37
- const before = content.substring(0, startIndex);
50
+ // Construct new content - exclude both marker lines (including leading whitespace)
51
+ const before = content.substring(0, lineStart);
38
52
  const after = content.substring(endLineEnd + 1);
39
53
  const replaced = before + newContent + after;
40
54
 
@@ -50,16 +64,19 @@ async function replaceSection(filePath, sectionName, newContent) {
50
64
  async function hasSection(filePath, sectionName) {
51
65
  try {
52
66
  const content = await fs.readFile(filePath, 'utf8');
53
- const startMarkerRegular = `// ${sectionName}_START`;
54
- const endMarkerRegular = `// ${sectionName}_END`;
67
+ const startMarkerSlash = `// ${sectionName}_START`;
68
+ const endMarkerSlash = `// ${sectionName}_END`;
55
69
  const startMarkerJSX = `{/* ${sectionName}_START */}`;
56
70
  const endMarkerJSX = `{/* ${sectionName}_END */}`;
71
+ const startMarkerHash = `# ${sectionName}_START`;
72
+ const endMarkerHash = `# ${sectionName}_END`;
57
73
 
58
- // Check both comment formats
59
- const hasRegular = content.includes(startMarkerRegular) && content.includes(endMarkerRegular);
74
+ // Check all comment formats
75
+ const hasSlash = content.includes(startMarkerSlash) && content.includes(endMarkerSlash);
60
76
  const hasJSX = content.includes(startMarkerJSX) && content.includes(endMarkerJSX);
77
+ const hasHash = content.includes(startMarkerHash) && content.includes(endMarkerHash);
61
78
 
62
- return hasRegular || hasJSX;
79
+ return hasSlash || hasJSX || hasHash;
63
80
  } catch (error) {
64
81
  return false;
65
82
  }
@@ -0,0 +1,274 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+ const chalk = require('chalk');
6
+
7
+ const SERVICES_REPO = 'git@github.com:launchframe-dev/services.git';
8
+ const BRANCH = 'main';
9
+
10
+ /**
11
+ * Get the cache directory path
12
+ * Works cross-platform (Linux, Mac, Windows)
13
+ * @returns {string} Cache directory path
14
+ */
15
+ function getCacheDir() {
16
+ const homeDir = os.homedir();
17
+ // Use same path structure on all platforms
18
+ // Windows: C:\Users\username\.launchframe\cache\services
19
+ // Mac/Linux: /home/username/.launchframe/cache/services
20
+ return path.join(homeDir, '.launchframe', 'cache', 'services');
21
+ }
22
+
23
+ /**
24
+ * Check if cache exists and is valid
25
+ * @returns {boolean} True if cache exists
26
+ */
27
+ async function cacheExists() {
28
+ const cacheDir = getCacheDir();
29
+ const gitDir = path.join(cacheDir, '.git');
30
+ return await fs.pathExists(gitDir);
31
+ }
32
+
33
+ /**
34
+ * Initialize cache with sparse checkout
35
+ * Clones only the repository structure, no services yet
36
+ * @returns {Promise<void>}
37
+ */
38
+ async function initializeCache() {
39
+ const cacheDir = getCacheDir();
40
+
41
+ console.log(chalk.blue('šŸ”„ Initializing services cache...'));
42
+
43
+ try {
44
+ // Ensure parent directory exists
45
+ await fs.ensureDir(path.dirname(cacheDir));
46
+
47
+ // Sparse clone (only root files, no services)
48
+ execSync(
49
+ `git clone --sparse --depth 1 --branch ${BRANCH} ${SERVICES_REPO} "${cacheDir}"`,
50
+ {
51
+ stdio: 'pipe', // Hide output
52
+ timeout: 60000 // 1 minute timeout
53
+ }
54
+ );
55
+
56
+ // Configure sparse checkout (starts with empty set)
57
+ execSync('git sparse-checkout init --cone', {
58
+ cwd: cacheDir,
59
+ stdio: 'pipe'
60
+ });
61
+
62
+ console.log(chalk.green('āœ“ Cache initialized'));
63
+ } catch (error) {
64
+ // Clean up partial clone on failure
65
+ await fs.remove(cacheDir);
66
+ throw new Error(`Failed to initialize cache: ${error.message}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Update cache to latest version from main branch
72
+ * Requires internet connection
73
+ * @returns {Promise<void>}
74
+ */
75
+ async function updateCache() {
76
+ const cacheDir = getCacheDir();
77
+
78
+ console.log(chalk.blue('šŸ”„ Updating service cache...'));
79
+
80
+ try {
81
+ execSync('git pull origin main', {
82
+ cwd: cacheDir,
83
+ stdio: 'pipe',
84
+ timeout: 30000 // 30 seconds
85
+ });
86
+
87
+ console.log(chalk.green('āœ“ Cache updated'));
88
+ } catch (error) {
89
+ throw new Error(`Failed to update cache: ${error.message}`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Expand sparse checkout to include specific services
95
+ * @param {string[]} serviceNames - Array of services names to expand
96
+ * @returns {Promise<void>}
97
+ */
98
+ async function expandServices(serviceNames) {
99
+ const cacheDir = getCacheDir();
100
+
101
+ console.log(chalk.blue(`šŸ“¦ Loading services: ${serviceNames.join(', ')}...`));
102
+
103
+ try {
104
+ // Get current sparse checkout list
105
+ let currentServices = [];
106
+ try {
107
+ const output = execSync('git sparse-checkout list', {
108
+ cwd: cacheDir,
109
+ stdio: 'pipe',
110
+ encoding: 'utf8'
111
+ });
112
+ currentServices = output.trim().split('\n').filter(Boolean);
113
+ } catch (error) {
114
+ // No services yet, that's fine
115
+ }
116
+
117
+ // Add new services to the list
118
+ const allServices = [...new Set([...currentServices, ...serviceNames])];
119
+
120
+ // Set sparse checkout to include all services
121
+ execSync(`git sparse-checkout set ${allServices.join(' ')}`, {
122
+ cwd: cacheDir,
123
+ stdio: 'pipe',
124
+ timeout: 60000 // 1 minute (may need to download files)
125
+ });
126
+
127
+ console.log(chalk.green('āœ“ Services loaded'));
128
+ } catch (error) {
129
+ throw new Error(`Failed to expand services: ${error.message}`);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Get path to a specific service in the cache
135
+ * @param {string} serviceName - Service name (e.g., 'backend', 'admin-portal')
136
+ * @returns {string} Absolute path to service
137
+ */
138
+ function getServicePath(serviceName) {
139
+ const cacheDir = getCacheDir();
140
+ return path.join(cacheDir, serviceName);
141
+ }
142
+
143
+ /**
144
+ * Get cache root path
145
+ * @returns {string} Absolute path to cache root
146
+ */
147
+ function getCachePath() {
148
+ return getCacheDir();
149
+ }
150
+
151
+ /**
152
+ * Clear the entire service cache
153
+ * Useful for troubleshooting or forcing fresh download
154
+ * @returns {Promise<void>}
155
+ */
156
+ async function clearCache() {
157
+ const cacheDir = getCacheDir();
158
+
159
+ if (await fs.pathExists(cacheDir)) {
160
+ await fs.remove(cacheDir);
161
+ console.log(chalk.green('āœ“ Cache cleared'));
162
+ } else {
163
+ console.log(chalk.gray('Cache is already empty'));
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Get cache information (size, last update, services)
169
+ * @returns {Promise<{exists: boolean, path: string, size?: number, services?: string[], lastUpdate?: Date}>}
170
+ */
171
+ async function getCacheInfo() {
172
+ const cacheDir = getCacheDir();
173
+ const info = {
174
+ exists: false,
175
+ path: cacheDir
176
+ };
177
+
178
+ if (!(await cacheExists())) {
179
+ return info;
180
+ }
181
+
182
+ info.exists = true;
183
+
184
+ try {
185
+ // Get cache size (du command works on Unix/Mac, different on Windows)
186
+ if (process.platform === 'win32') {
187
+ // Windows: use powershell to get size
188
+ const output = execSync(
189
+ `powershell -command "(Get-ChildItem -Path '${cacheDir}' -Recurse | Measure-Object -Property Length -Sum).Sum"`,
190
+ { encoding: 'utf8', stdio: 'pipe' }
191
+ );
192
+ info.size = parseInt(output.trim());
193
+ } else {
194
+ // Unix/Mac: use du
195
+ const output = execSync(`du -sb "${cacheDir}"`, {
196
+ encoding: 'utf8',
197
+ stdio: 'pipe'
198
+ });
199
+ info.size = parseInt(output.split('\t')[0]);
200
+ }
201
+ } catch (error) {
202
+ // Size calculation failed, not critical
203
+ }
204
+
205
+ try {
206
+ // Get list of expanded services
207
+ const output = execSync('git sparse-checkout list', {
208
+ cwd: cacheDir,
209
+ encoding: 'utf8',
210
+ stdio: 'pipe'
211
+ });
212
+ info.services = output.trim().split('\n').filter(Boolean);
213
+ } catch (error) {
214
+ info.services = [];
215
+ }
216
+
217
+ try {
218
+ // Get last update time from git log
219
+ const output = execSync('git log -1 --format=%cd --date=iso', {
220
+ cwd: cacheDir,
221
+ encoding: 'utf8',
222
+ stdio: 'pipe'
223
+ });
224
+ info.lastUpdate = new Date(output.trim());
225
+ } catch (error) {
226
+ // Last update time failed, not critical
227
+ }
228
+
229
+ return info;
230
+ }
231
+
232
+ /**
233
+ * Ensure cache is ready (initialize if needed, update if exists)
234
+ * This is the main entry point for cache management
235
+ * @param {string[]} requiredServices - Services needed for the operation
236
+ * @returns {Promise<string>} Path to cache root
237
+ */
238
+ async function ensureCacheReady(requiredServices) {
239
+ try {
240
+ if (!(await cacheExists())) {
241
+ // Cache doesn't exist, initialize it
242
+ await initializeCache();
243
+ } else {
244
+ // Cache exists, update it
245
+ await updateCache();
246
+ }
247
+
248
+ // Expand sparse checkout to include required services
249
+ await expandServices(requiredServices);
250
+
251
+ return getCachePath();
252
+ } catch (error) {
253
+ // If we fail and it's a network error, provide helpful message
254
+ if (error.message.includes('Connection') || error.message.includes('timed out')) {
255
+ throw new Error(
256
+ 'Cannot connect to GitHub. Please check your internet connection and try again.'
257
+ );
258
+ }
259
+ throw error;
260
+ }
261
+ }
262
+
263
+ module.exports = {
264
+ getCacheDir,
265
+ cacheExists,
266
+ initializeCache,
267
+ updateCache,
268
+ expandServices,
269
+ getServicePath,
270
+ getCachePath,
271
+ clearCache,
272
+ getCacheInfo,
273
+ ensureCacheReady
274
+ };
@@ -55,7 +55,10 @@ function escapeRegex(string) {
55
55
  */
56
56
  async function replaceVariablesInFile(filePath, variables) {
57
57
  try {
58
- let content = await fs.readFile(filePath, 'utf8');
58
+ // Read file content - preserve line endings for shell scripts
59
+ // Use binary mode to avoid Node.js line ending normalization on Windows
60
+ const buffer = await fs.readFile(filePath);
61
+ let content = buffer.toString('utf8');
59
62
  let modified = false;
60
63
 
61
64
  // Replace each variable using regex with negative lookbehind
@@ -74,8 +77,10 @@ async function replaceVariablesInFile(filePath, variables) {
74
77
  }
75
78
 
76
79
  // Only write if changes were made
80
+ // Write as Buffer to preserve original line endings
77
81
  if (modified) {
78
- await fs.writeFile(filePath, content, 'utf8');
82
+ const outputBuffer = Buffer.from(content, 'utf8');
83
+ await fs.writeFile(filePath, outputBuffer);
79
84
  }
80
85
 
81
86
  return modified;
@@ -44,7 +44,7 @@ async function processServiceVariant(
44
44
  // Step 1: Copy base template (minimal - B2B + single-tenant)
45
45
  console.log(` šŸ“ Copying base template from ${serviceConfig.base}`);
46
46
  await copyDirectory(basePath, destination, {
47
- exclude: ['node_modules', '.git', 'dist', '.env', 'templates']
47
+ exclude: ['node_modules', '.git', 'dist', '.env']
48
48
  });
49
49
 
50
50
  // Step 2: Determine which variants to apply
@@ -143,29 +143,41 @@ async function cleanupSectionMarkers(serviceName, serviceConfig, appliedVariants
143
143
  let content = await fs.readFile(targetFilePath, 'utf-8');
144
144
  let modified = false;
145
145
 
146
- // Remove each unused section marker
146
+ // Remove each unused section marker (keep content, remove only marker comments)
147
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'
148
+ // 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
+ const slashPattern = new RegExp(
152
+ `^[ \\t]*\\/\\/ ${sectionName}_START\\n([\\s\\S]*?)^[ \\t]*\\/\\/ ${sectionName}_END\\n?`,
153
+ 'gm'
152
154
  );
153
155
  const jsxPattern = new RegExp(
154
- `\\{\\/\\* ${sectionName}_START \\*\\/\\}\\n[\\s\\S]*?\\{\\/\\* ${sectionName}_END \\*\\/\\}\\n?`,
155
- 'g'
156
+ `^[ \\t]*\\{\\/\\* ${sectionName}_START \\*\\/\\}\\n([\\s\\S]*?)^[ \\t]*\\{\\/\\* ${sectionName}_END \\*\\/\\}\\n?`,
157
+ 'gm'
158
+ );
159
+ const hashPattern = new RegExp(
160
+ `^[ \\t]*# ${sectionName}_START\\n([\\s\\S]*?)^[ \\t]*# ${sectionName}_END\\n?`,
161
+ 'gm'
156
162
  );
157
163
 
158
- const beforeRegular = content;
159
- content = content.replace(regularPattern, '');
160
- if (content !== beforeRegular) {
164
+ const beforeSlash = content;
165
+ content = content.replace(slashPattern, '$1');
166
+ if (content !== beforeSlash) {
161
167
  modified = true;
162
168
  }
163
169
 
164
170
  const beforeJsx = content;
165
- content = content.replace(jsxPattern, '');
171
+ content = content.replace(jsxPattern, '$1');
166
172
  if (content !== beforeJsx) {
167
173
  modified = true;
168
174
  }
175
+
176
+ const beforeHash = content;
177
+ content = content.replace(hashPattern, '$1');
178
+ if (content !== beforeHash) {
179
+ modified = true;
180
+ }
169
181
  }
170
182
 
171
183
  if (modified) {