@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.
- package/README.md +44 -9
- package/package.json +5 -5
- package/src/commands/cache.js +102 -0
- package/src/commands/deploy-configure.js +30 -4
- package/src/commands/deploy-init.js +38 -56
- package/src/commands/deploy-set-env.js +68 -91
- package/src/commands/docker-destroy.js +45 -15
- package/src/commands/docker-up.js +42 -16
- package/src/commands/help.js +11 -1
- package/src/commands/init.js +90 -63
- package/src/commands/service.js +71 -41
- package/src/commands/waitlist-deploy.js +2 -2
- package/src/commands/waitlist-logs.js +1 -2
- package/src/commands/waitlist-up.js +50 -15
- package/src/generator.js +16 -7
- package/src/index.js +18 -11
- package/src/prompts.js +12 -0
- package/src/services/registry.js +8 -6
- package/src/services/variant-config.js +146 -48
- package/src/utils/docker-helper.js +66 -44
- package/src/utils/github-access.js +67 -0
- package/src/utils/project-helpers.js +6 -2
- package/src/utils/section-replacer.js +32 -15
- package/src/utils/service-cache.js +274 -0
- package/src/utils/variable-replacer.js +7 -2
- package/src/utils/variant-processor.js +24 -12
|
@@ -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-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
|
13
|
-
const
|
|
14
|
-
const
|
|
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(
|
|
19
|
-
let endIndex = content.indexOf(
|
|
20
|
-
let isJSX = false;
|
|
20
|
+
let startIndex = content.indexOf(startMarkerSlash);
|
|
21
|
+
let endIndex = content.indexOf(endMarkerSlash);
|
|
21
22
|
|
|
22
|
-
// If not found with
|
|
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
|
-
|
|
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,
|
|
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
|
|
54
|
-
const
|
|
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
|
|
59
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
'
|
|
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
|
|
159
|
-
content = content.replace(
|
|
160
|
-
if (content !==
|
|
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) {
|