@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.
@@ -0,0 +1,75 @@
1
+ const fs = require('fs-extra');
2
+
3
+ /**
4
+ * Check if a file contains any template placeholders ({{VAR}})
5
+ * @param {string} filePath - Path to file to check
6
+ * @returns {Promise<{hasPlaceholders: boolean, placeholders: string[]}>}
7
+ */
8
+ async function checkForPlaceholders(filePath) {
9
+ try {
10
+ const content = await fs.readFile(filePath, 'utf8');
11
+
12
+ // Regex to find {{VAR}} patterns (but not ${{VAR}} which is GitHub Actions)
13
+ const placeholderRegex = /(?<!\$)\{\{([A-Z_][A-Z0-9_]*)\}\}/g;
14
+ const placeholders = [];
15
+ let match;
16
+
17
+ while ((match = placeholderRegex.exec(content)) !== null) {
18
+ const placeholder = match[0]; // Full match including {{ }}
19
+ if (!placeholders.includes(placeholder)) {
20
+ placeholders.push(placeholder);
21
+ }
22
+ }
23
+
24
+ return {
25
+ hasPlaceholders: placeholders.length > 0,
26
+ placeholders
27
+ };
28
+ } catch (error) {
29
+ throw new Error(`Failed to read file ${filePath}: ${error.message}`);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Validate that .env.prod file exists and has no placeholders
35
+ * @param {string} envProdPath - Path to .env.prod file
36
+ * @returns {Promise<{valid: boolean, error?: string, placeholders?: string[]}>}
37
+ */
38
+ async function validateEnvProd(envProdPath) {
39
+ // Check if file exists
40
+ if (!await fs.pathExists(envProdPath)) {
41
+ return {
42
+ valid: false,
43
+ error: 'File does not exist'
44
+ };
45
+ }
46
+
47
+ // Check for placeholders
48
+ const { hasPlaceholders, placeholders } = await checkForPlaceholders(envProdPath);
49
+
50
+ if (hasPlaceholders) {
51
+ return {
52
+ valid: false,
53
+ error: 'File contains placeholder variables',
54
+ placeholders
55
+ };
56
+ }
57
+
58
+ return { valid: true };
59
+ }
60
+
61
+ /**
62
+ * Generate a secure random string for secrets
63
+ * @param {number} length - Length of string to generate
64
+ * @returns {string}
65
+ */
66
+ function generateSecret(length = 32) {
67
+ const crypto = require('crypto');
68
+ return crypto.randomBytes(length).toString('base64').slice(0, length);
69
+ }
70
+
71
+ module.exports = {
72
+ checkForPlaceholders,
73
+ validateEnvProd,
74
+ generateSecret
75
+ };
@@ -0,0 +1,87 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Copy a directory recursively with whitelist or blacklist filtering
6
+ * @param {string} source - Source directory path
7
+ * @param {string} destination - Destination directory path
8
+ * @param {Object} options - Options object
9
+ * @param {string[]} options.exclude - Array of directory names to exclude (blacklist approach)
10
+ * @param {string[]} options.include - Array of directory names to include (whitelist approach, takes precedence)
11
+ */
12
+ async function copyDirectory(source, destination, options = {}) {
13
+ const { exclude = ['node_modules', '.git', '.next', 'dist', 'build', '.env'], include = null } = options;
14
+
15
+ // Ensure destination exists
16
+ await fs.ensureDir(destination);
17
+
18
+ // Read directory contents
19
+ const entries = await fs.readdir(source, { withFileTypes: true });
20
+
21
+ for (const entry of entries) {
22
+ const sourcePath = path.join(source, entry.name);
23
+ const destPath = path.join(destination, entry.name);
24
+
25
+ // Always check exclude list first (applies at all levels)
26
+ if (exclude.includes(entry.name)) {
27
+ continue;
28
+ }
29
+
30
+ // Whitelist approach: if include array is provided, only copy items in the list (only at top level)
31
+ if (include !== null && include.length > 0) {
32
+ if (!include.includes(entry.name)) {
33
+ continue;
34
+ }
35
+ }
36
+
37
+ if (entry.isDirectory()) {
38
+ // Recursively copy directory (pass exclude, remove include for nested levels)
39
+ await copyDirectory(sourcePath, destPath, { exclude });
40
+ } else {
41
+ // Copy file
42
+ await fs.copy(sourcePath, destPath);
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Delete a file
49
+ * @param {string} filePath - Path to file
50
+ */
51
+ async function deleteFile(filePath) {
52
+ await fs.remove(filePath);
53
+ }
54
+
55
+ /**
56
+ * Delete a directory
57
+ * @param {string} dirPath - Path to directory
58
+ */
59
+ async function deleteDirectory(dirPath) {
60
+ await fs.remove(dirPath);
61
+ }
62
+
63
+ /**
64
+ * Copy a single file
65
+ * @param {string} source - Source file path
66
+ * @param {string} destination - Destination file path
67
+ */
68
+ async function copyFile(source, destination) {
69
+ await fs.copy(source, destination);
70
+ }
71
+
72
+ /**
73
+ * Move a file
74
+ * @param {string} source - Source file path
75
+ * @param {string} destination - Destination file path
76
+ */
77
+ async function moveFile(source, destination) {
78
+ await fs.move(source, destination);
79
+ }
80
+
81
+ module.exports = {
82
+ copyDirectory,
83
+ deleteFile,
84
+ deleteDirectory,
85
+ copyFile,
86
+ moveFile
87
+ };
@@ -0,0 +1,104 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ /**
6
+ * Check if current directory is a LaunchFrame project
7
+ */
8
+ function isLaunchFrameProject() {
9
+ const markerPath = path.join(process.cwd(), '.launchframe');
10
+ return fs.existsSync(markerPath);
11
+ }
12
+
13
+ /**
14
+ * Require that the current directory is a LaunchFrame project
15
+ * Exits with error if not
16
+ */
17
+ function requireProject() {
18
+ if (!isLaunchFrameProject()) {
19
+ console.error(chalk.red('\n❌ Error: Not in a LaunchFrame project'));
20
+ console.log(chalk.gray('Run this command from the root of your LaunchFrame project.\n'));
21
+ process.exit(1);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Get project configuration from .launchframe file
27
+ */
28
+ function getProjectConfig() {
29
+ requireProject();
30
+ const markerPath = path.join(process.cwd(), '.launchframe');
31
+ const content = fs.readFileSync(markerPath, 'utf8');
32
+ return JSON.parse(content);
33
+ }
34
+
35
+ /**
36
+ * Update project configuration in .launchframe file
37
+ */
38
+ function updateProjectConfig(config) {
39
+ const markerPath = path.join(process.cwd(), '.launchframe');
40
+ fs.writeFileSync(markerPath, JSON.stringify(config, null, 2), 'utf8');
41
+ }
42
+
43
+ /**
44
+ * Get list of installed services
45
+ */
46
+ function getInstalledComponents() {
47
+ const config = getProjectConfig();
48
+ return config.installedServices || [];
49
+ }
50
+
51
+ /**
52
+ * Check if a service is installed
53
+ */
54
+ function isComponentInstalled(componentName) {
55
+ const installedComponents = getInstalledComponents();
56
+ return installedComponents.includes(componentName);
57
+ }
58
+
59
+ /**
60
+ * Add a service to the installed services list
61
+ */
62
+ function addInstalledComponent(componentName) {
63
+ const config = getProjectConfig();
64
+ if (!config.installedServices) {
65
+ config.installedServices = [];
66
+ }
67
+ if (!config.installedServices.includes(componentName)) {
68
+ config.installedServices.push(componentName);
69
+ updateProjectConfig(config);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Get primary domain from config
75
+ * @param {Object} config - Project config object
76
+ * @returns {string|null} Primary domain
77
+ */
78
+ function getPrimaryDomain(config) {
79
+ return config.primaryDomain || null;
80
+ }
81
+
82
+ /**
83
+ * Check if waitlist service is installed
84
+ * @param {Object} config - Project config object (optional, will fetch if not provided)
85
+ * @returns {boolean}
86
+ */
87
+ function isWaitlistInstalled(config = null) {
88
+ if (!config) {
89
+ config = getProjectConfig();
90
+ }
91
+ return (config.installedServices || []).includes('waitlist');
92
+ }
93
+
94
+ module.exports = {
95
+ isLaunchFrameProject,
96
+ requireProject,
97
+ getProjectConfig,
98
+ updateProjectConfig,
99
+ getInstalledComponents,
100
+ isComponentInstalled,
101
+ addInstalledComponent,
102
+ getPrimaryDomain,
103
+ isWaitlistInstalled
104
+ };
@@ -0,0 +1,71 @@
1
+ const fs = require('fs-extra');
2
+
3
+ /**
4
+ * Replace a marked section in a file
5
+ * @param {string} filePath - Path to file
6
+ * @param {string} sectionName - Name of section (e.g., 'CORS_CONFIG')
7
+ * @param {string} newContent - New content to insert
8
+ */
9
+ async function replaceSection(filePath, sectionName, newContent) {
10
+ const content = await fs.readFile(filePath, 'utf8');
11
+
12
+ // Try both comment formats (// for regular comments, {/* */} for JSX)
13
+ const startMarkerRegular = `// ${sectionName}_START`;
14
+ const endMarkerRegular = `// ${sectionName}_END`;
15
+ const startMarkerJSX = `{/* ${sectionName}_START */}`;
16
+ const endMarkerJSX = `{/* ${sectionName}_END */}`;
17
+
18
+ let startIndex = content.indexOf(startMarkerRegular);
19
+ let endIndex = content.indexOf(endMarkerRegular);
20
+ let isJSX = false;
21
+
22
+ // If not found with regular comments, try JSX comments
23
+ if (startIndex === -1 || endIndex === -1) {
24
+ startIndex = content.indexOf(startMarkerJSX);
25
+ endIndex = content.indexOf(endMarkerJSX);
26
+ isJSX = true;
27
+ }
28
+
29
+ if (startIndex === -1 || endIndex === -1) {
30
+ throw new Error(`Section markers not found: ${sectionName} in ${filePath}`);
31
+ }
32
+
33
+ // Find the end of the end marker line
34
+ const endLineEnd = content.indexOf('\n', endIndex);
35
+
36
+ // Construct new content - exclude both marker lines
37
+ const before = content.substring(0, startIndex);
38
+ const after = content.substring(endLineEnd + 1);
39
+ const replaced = before + newContent + after;
40
+
41
+ await fs.writeFile(filePath, replaced, 'utf8');
42
+ }
43
+
44
+ /**
45
+ * Check if a file contains a specific section marker
46
+ * @param {string} filePath - Path to file
47
+ * @param {string} sectionName - Name of section
48
+ * @returns {Promise<boolean>} - True if section exists
49
+ */
50
+ async function hasSection(filePath, sectionName) {
51
+ try {
52
+ const content = await fs.readFile(filePath, 'utf8');
53
+ const startMarkerRegular = `// ${sectionName}_START`;
54
+ const endMarkerRegular = `// ${sectionName}_END`;
55
+ const startMarkerJSX = `{/* ${sectionName}_START */}`;
56
+ const endMarkerJSX = `{/* ${sectionName}_END */}`;
57
+
58
+ // Check both comment formats
59
+ const hasRegular = content.includes(startMarkerRegular) && content.includes(endMarkerRegular);
60
+ const hasJSX = content.includes(startMarkerJSX) && content.includes(endMarkerJSX);
61
+
62
+ return hasRegular || hasJSX;
63
+ } catch (error) {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ module.exports = {
69
+ replaceSection,
70
+ hasSection
71
+ };
@@ -0,0 +1,220 @@
1
+ const { exec } = require('child_process');
2
+ const { promisify } = require('util');
3
+ const chalk = require('chalk');
4
+ const fs = require('fs-extra');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ /**
11
+ * Test SSH connection to VPS
12
+ * @param {string} vpsUser - SSH username
13
+ * @param {string} vpsHost - VPS hostname or IP
14
+ * @returns {Promise<{success: boolean, error?: string}>}
15
+ */
16
+ async function testSSHConnection(vpsUser, vpsHost) {
17
+ try {
18
+ // Try a simple SSH command (echo test)
19
+ await execAsync(`ssh -o ConnectTimeout=10 -o BatchMode=yes ${vpsUser}@${vpsHost} "echo 'Connection successful'"`, {
20
+ timeout: 15000
21
+ });
22
+ return { success: true };
23
+ } catch (error) {
24
+ return {
25
+ success: false,
26
+ error: error.message
27
+ };
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Check if SSH keys are configured
33
+ * @returns {Promise<{hasKeys: boolean, keyPaths: string[]}>}
34
+ */
35
+ async function checkSSHKeys() {
36
+ const homeDir = os.homedir();
37
+ const sshDir = path.join(homeDir, '.ssh');
38
+
39
+ const commonKeyNames = [
40
+ 'id_rsa',
41
+ 'id_ed25519',
42
+ 'id_ecdsa',
43
+ 'id_dsa'
44
+ ];
45
+
46
+ const keyPaths = [];
47
+
48
+ for (const keyName of commonKeyNames) {
49
+ const keyPath = path.join(sshDir, keyName);
50
+ if (await fs.pathExists(keyPath)) {
51
+ keyPaths.push(keyPath);
52
+ }
53
+ }
54
+
55
+ return {
56
+ hasKeys: keyPaths.length > 0,
57
+ keyPaths
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Execute SSH command on VPS
63
+ * @param {string} vpsUser - SSH username
64
+ * @param {string} vpsHost - VPS hostname or IP
65
+ * @param {string} command - Command to execute
66
+ * @param {Object} options - Execution options
67
+ * @returns {Promise<{stdout: string, stderr: string}>}
68
+ */
69
+ async function executeSSH(vpsUser, vpsHost, command, options = {}) {
70
+ const { timeout = 120000 } = options;
71
+
72
+ try {
73
+ const { stdout, stderr } = await execAsync(
74
+ `ssh -o ConnectTimeout=10 ${vpsUser}@${vpsHost} "${command.replace(/"/g, '\\"')}"`,
75
+ { timeout }
76
+ );
77
+ return { stdout, stderr };
78
+ } catch (error) {
79
+ throw new Error(`SSH command failed: ${error.message}`);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Copy file to VPS via SCP
85
+ * @param {string} localPath - Local file path
86
+ * @param {string} vpsUser - SSH username
87
+ * @param {string} vpsHost - VPS hostname or IP
88
+ * @param {string} remotePath - Remote file path
89
+ * @returns {Promise<void>}
90
+ */
91
+ async function copyFileToVPS(localPath, vpsUser, vpsHost, remotePath) {
92
+ try {
93
+ await execAsync(`scp "${localPath}" ${vpsUser}@${vpsHost}:"${remotePath}"`, {
94
+ timeout: 60000
95
+ });
96
+ } catch (error) {
97
+ throw new Error(`Failed to copy file: ${error.message}`);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Copy entire directory to VPS using tar + scp
103
+ * @param {string} localDir - Local directory path
104
+ * @param {string} vpsUser - SSH username
105
+ * @param {string} vpsHost - VPS hostname or IP
106
+ * @param {string} remoteDir - Remote directory path
107
+ * @returns {Promise<void>}
108
+ */
109
+ async function copyDirectoryToVPS(localDir, vpsUser, vpsHost, remoteDir) {
110
+ const path = require('path');
111
+ const fs = require('fs-extra');
112
+
113
+ try {
114
+ // Create a temporary tarball excluding unnecessary files
115
+ const tarballName = `deploy-${Date.now()}.tar.gz`;
116
+ const tarballPath = path.join('/tmp', tarballName);
117
+
118
+ // Create tarball with exclusions
119
+ await execAsync(
120
+ `tar -czf "${tarballPath}" -C "${localDir}" --exclude='node_modules' --exclude='.next' --exclude='dist' --exclude='build' --exclude='.git' --exclude='*.log' .`,
121
+ { timeout: 60000 }
122
+ );
123
+
124
+ // Ensure remote directory exists
125
+ await execAsync(
126
+ `ssh ${vpsUser}@${vpsHost} "mkdir -p ${remoteDir}"`,
127
+ { timeout: 30000 }
128
+ );
129
+
130
+ // Copy tarball to VPS
131
+ await execAsync(
132
+ `scp "${tarballPath}" ${vpsUser}@${vpsHost}:/tmp/${tarballName}`,
133
+ { timeout: 180000 } // 3 minutes
134
+ );
135
+
136
+ // Extract tarball on VPS
137
+ await execAsync(
138
+ `ssh ${vpsUser}@${vpsHost} "tar -xzf /tmp/${tarballName} -C ${remoteDir} && rm /tmp/${tarballName}"`,
139
+ { timeout: 60000 }
140
+ );
141
+
142
+ // Clean up local tarball
143
+ await fs.remove(tarballPath);
144
+ } catch (error) {
145
+ throw new Error(`Failed to copy directory: ${error.message}`);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Check if repository is private by attempting to clone
151
+ * @param {string} githubOrg - GitHub organization/username
152
+ * @param {string} projectName - Project name
153
+ * @returns {Promise<{isPrivate: boolean, error?: string}>}
154
+ */
155
+ async function checkRepoPrivacy(githubOrg, projectName) {
156
+ try {
157
+ // Try to get repository info via HTTPS (no auth)
158
+ await execAsync(`git ls-remote https://github.com/${githubOrg}/${projectName}.git HEAD`, {
159
+ timeout: 10000
160
+ });
161
+ return { isPrivate: false };
162
+ } catch (error) {
163
+ // If error contains "authentication" or "not found", likely private or doesn't exist
164
+ if (error.message.includes('Authentication') || error.message.includes('not found')) {
165
+ return {
166
+ isPrivate: true,
167
+ error: 'Repository appears to be private or does not exist'
168
+ };
169
+ }
170
+ // Other errors
171
+ return {
172
+ isPrivate: true,
173
+ error: error.message
174
+ };
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Display instructions for setting up deploy keys for private repository
180
+ * @param {string} vpsUser - SSH username
181
+ * @param {string} vpsHost - VPS hostname or IP
182
+ * @param {string} githubOrg - GitHub organization/username
183
+ * @param {string} projectName - Project name
184
+ */
185
+ function showDeployKeyInstructions(vpsUser, vpsHost, githubOrg, projectName) {
186
+ console.log(chalk.yellow('\n📝 Private Repository Detected\n'));
187
+ console.log(chalk.white('Your repository appears to be private. Follow these steps:\n'));
188
+
189
+ console.log(chalk.white('1. SSH to your VPS:'));
190
+ console.log(chalk.gray(` ssh ${vpsUser}@${vpsHost}\n`));
191
+
192
+ console.log(chalk.white('2. Generate SSH key (if not already exists):'));
193
+ console.log(chalk.gray(` ssh-keygen -t ed25519 -C "deploy-key-${projectName}"\n`));
194
+
195
+ console.log(chalk.white('3. Display the public key:'));
196
+ console.log(chalk.gray(' cat ~/.ssh/id_ed25519.pub\n'));
197
+
198
+ console.log(chalk.white('4. Add deploy key to GitHub:'));
199
+ console.log(chalk.gray(` - Go to: https://github.com/${githubOrg}/${projectName}/settings/keys`));
200
+ console.log(chalk.gray(' - Click "Add deploy key"'));
201
+ console.log(chalk.gray(' - Paste the public key'));
202
+ console.log(chalk.gray(' - Title: "VPS Deploy Key"'));
203
+ console.log(chalk.gray(' - Check "Allow write access" if needed\n'));
204
+
205
+ console.log(chalk.white('5. Test the connection:'));
206
+ console.log(chalk.gray(' ssh -T git@github.com\n'));
207
+
208
+ console.log(chalk.white('6. Then retry:'));
209
+ console.log(chalk.gray(' launchframe deploy:init\n'));
210
+ }
211
+
212
+ module.exports = {
213
+ testSSHConnection,
214
+ checkSSHKeys,
215
+ executeSSH,
216
+ copyFileToVPS,
217
+ copyDirectoryToVPS,
218
+ checkRepoPrivacy,
219
+ showDeployKeyInstructions
220
+ };
@@ -0,0 +1,95 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { glob } = require('glob');
4
+
5
+ /**
6
+ * Replace template variables in all files within a directory
7
+ * @param {string} directory - Root directory to search
8
+ * @param {Object} variables - Object with variable mappings (e.g., {'{{VAR}}': 'value'})
9
+ */
10
+ async function replaceVariables(directory, variables) {
11
+ // Find all files (excluding node_modules, .git, binary files)
12
+ const files = await glob('**/*', {
13
+ cwd: directory,
14
+ nodir: true,
15
+ dot: true, // Include hidden files/directories like .vitepress
16
+ ignore: [
17
+ '**/node_modules/**',
18
+ '**/.git/**',
19
+ '**/.next/**',
20
+ '**/dist/**',
21
+ '**/build/**',
22
+ '**/*.png',
23
+ '**/*.jpg',
24
+ '**/*.jpeg',
25
+ '**/*.gif',
26
+ '**/*.ico',
27
+ '**/*.pdf',
28
+ '**/*.woff',
29
+ '**/*.woff2',
30
+ '**/*.ttf',
31
+ '**/*.eot'
32
+ ]
33
+ });
34
+
35
+ for (const file of files) {
36
+ const filePath = path.join(directory, file);
37
+ await replaceVariablesInFile(filePath, variables);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Escape special regex characters in a string
43
+ * @param {string} string - String to escape
44
+ * @returns {string} Escaped string safe for use in regex
45
+ */
46
+ function escapeRegex(string) {
47
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
48
+ }
49
+
50
+ /**
51
+ * Replace template variables in a single file
52
+ * Uses negative lookbehind to avoid replacing GitHub Actions syntax (${{ }})
53
+ * @param {string} filePath - Path to file
54
+ * @param {Object} variables - Object with variable mappings
55
+ */
56
+ async function replaceVariablesInFile(filePath, variables) {
57
+ try {
58
+ let content = await fs.readFile(filePath, 'utf8');
59
+ let modified = false;
60
+
61
+ // Replace each variable using regex with negative lookbehind
62
+ for (const [placeholder, value] of Object.entries(variables)) {
63
+ // Create regex that matches {{VAR}} but NOT ${{VAR}}
64
+ // Negative lookbehind: (?<!\$) means "not preceded by $"
65
+ const escapedPlaceholder = escapeRegex(placeholder);
66
+ const regex = new RegExp(`(?<!\\$)${escapedPlaceholder}`, 'g');
67
+
68
+ if (regex.test(content)) {
69
+ // Reset regex lastIndex after test
70
+ regex.lastIndex = 0;
71
+ content = content.replace(regex, value);
72
+ modified = true;
73
+ }
74
+ }
75
+
76
+ // Only write if changes were made
77
+ if (modified) {
78
+ await fs.writeFile(filePath, content, 'utf8');
79
+ }
80
+
81
+ return modified;
82
+ } catch (error) {
83
+ // Skip binary files or files that can't be read as text
84
+ if (error.code !== 'EISDIR') {
85
+ // Log warning but continue
86
+ console.warn(`Warning: Could not process ${filePath}`);
87
+ }
88
+ return false;
89
+ }
90
+ }
91
+
92
+ module.exports = {
93
+ replaceVariables,
94
+ replaceVariablesInFile
95
+ };