@launchframe/cli 1.0.0-beta.9 → 1.0.0

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.
Files changed (42) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/CLAUDE.md +27 -0
  3. package/CODE_OF_CONDUCT.md +128 -0
  4. package/LICENSE +21 -0
  5. package/README.md +7 -1
  6. package/package.json +9 -6
  7. package/src/commands/database-console.js +84 -0
  8. package/src/commands/deploy-build.js +76 -0
  9. package/src/commands/deploy-configure.js +10 -3
  10. package/src/commands/deploy-init.js +24 -57
  11. package/src/commands/deploy-set-env.js +17 -7
  12. package/src/commands/deploy-sync-features.js +233 -0
  13. package/src/commands/deploy-up.js +4 -3
  14. package/src/commands/dev-add-user.js +165 -0
  15. package/src/commands/dev-logo.js +160 -0
  16. package/src/commands/dev-npm-install.js +33 -0
  17. package/src/commands/dev-queue.js +85 -0
  18. package/src/commands/docker-build.js +9 -6
  19. package/src/commands/help.js +33 -9
  20. package/src/commands/init.js +44 -52
  21. package/src/commands/migration-create.js +40 -0
  22. package/src/commands/migration-revert.js +32 -0
  23. package/src/commands/migration-run.js +32 -0
  24. package/src/commands/module.js +146 -0
  25. package/src/commands/waitlist-deploy.js +1 -0
  26. package/src/generator.js +41 -40
  27. package/src/index.js +109 -4
  28. package/src/services/module-config.js +25 -0
  29. package/src/services/module-registry.js +12 -0
  30. package/src/services/variant-config.js +24 -13
  31. package/src/utils/docker-helper.js +116 -2
  32. package/src/utils/env-generator.js +9 -6
  33. package/src/utils/env-validator.js +4 -2
  34. package/src/utils/github-access.js +15 -13
  35. package/src/utils/logger.js +93 -0
  36. package/src/utils/module-installer.js +58 -0
  37. package/src/utils/project-helpers.js +34 -1
  38. package/src/utils/service-cache.js +12 -18
  39. package/src/utils/ssh-helper.js +51 -1
  40. package/src/utils/telemetry.js +238 -0
  41. package/src/utils/variable-replacer.js +18 -23
  42. package/src/utils/variant-processor.js +35 -42
@@ -25,28 +25,31 @@ async function generateEnvFile(projectRoot, answers) {
25
25
 
26
26
  // Generate secure secrets
27
27
  const secrets = {
28
- JWT_SECRET: generateSecret(32),
28
+ BETTER_AUTH_SECRET: generateSecret(32),
29
29
  DB_PASSWORD: generateSecret(24),
30
30
  BULL_ADMIN_TOKEN: generateSecret(24)
31
31
  };
32
32
 
33
33
  // Create variable mappings
34
+ // Note: PRIMARY_DOMAIN and ADMIN_EMAIL are NOT replaced here - they stay as placeholders
35
+ // until deploy:configure and deploy:set-env are run
34
36
  const variables = {
35
37
  '{{PROJECT_NAME}}': answers.projectName,
36
38
  '{{PROJECT_NAME_UPPER}}': answers.projectNameUpper,
37
- '{{PRIMARY_DOMAIN}}': answers.primaryDomain,
38
- '{{ADMIN_EMAIL}}': answers.adminEmail,
39
+ '{{PROJECT_DISPLAY_NAME}}': answers.projectDisplayName,
39
40
 
40
41
  // Replace placeholder passwords with generated secrets
41
42
  'your_secure_postgres_password': secrets.DB_PASSWORD,
42
- 'your_jwt_secret_key_change_this_in_production': secrets.JWT_SECRET,
43
+ 'your_better_auth_secret_minimum_32_chars': secrets.BETTER_AUTH_SECRET,
43
44
  'your_bull_admin_token': secrets.BULL_ADMIN_TOKEN
44
45
  };
45
46
 
46
- // Replace variables in template
47
+ // Replace variables in template (only those with defined values)
47
48
  let envContent = envTemplate;
48
49
  for (const [placeholder, value] of Object.entries(variables)) {
49
- envContent = envContent.split(placeholder).join(value);
50
+ if (value !== undefined && value !== null) {
51
+ envContent = envContent.split(placeholder).join(value);
52
+ }
50
53
  }
51
54
 
52
55
  // Write .env file
@@ -59,13 +59,15 @@ async function validateEnvProd(envProdPath) {
59
59
  }
60
60
 
61
61
  /**
62
- * Generate a secure random string for secrets
62
+ * Generate a secure random string for secrets (URL-safe)
63
63
  * @param {number} length - Length of string to generate
64
64
  * @returns {string}
65
65
  */
66
66
  function generateSecret(length = 32) {
67
67
  const crypto = require('crypto');
68
- return crypto.randomBytes(length).toString('base64').slice(0, length);
68
+ // Use hex encoding to avoid URL-unsafe characters (+, /, =)
69
+ // Hex produces 2 chars per byte, so divide by 2
70
+ return crypto.randomBytes(Math.ceil(length / 2)).toString('hex').slice(0, length);
69
71
  }
70
72
 
71
73
  module.exports = {
@@ -38,26 +38,28 @@ async function checkGitHubAccess() {
38
38
 
39
39
  /**
40
40
  * Display message when user doesn't have access to services repository
41
- * Guides them to either purchase or setup SSH keys
41
+ * Guides them to either get beta access or setup SSH keys
42
42
  */
43
43
  function showAccessDeniedMessage() {
44
- const purchaseUrl = 'https://buy.polar.sh/polar_cl_Zy4YqEwhoIEdUrAH8vHaWuZtwZuv306sYMnq118MbKi';
44
+ const betaUrl = 'https://launchframe.dev/';
45
45
  const docsUrl = 'https://docs.launchframe.dev/guide/quick-start#add-ssh-key-to-repo';
46
-
46
+
47
47
  console.log(chalk.red('\n❌ Cannot access LaunchFrame services repository\n'));
48
-
48
+
49
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:'));
50
+ console.log(chalk.gray(' 1. You don\'t have beta access yet'));
51
+ console.log(chalk.gray(' 2. You have access but haven\'t added your SSH key to the repo\n'));
52
+
53
+ console.log(chalk.cyan('→ Get beta access:'));
54
+ console.log(chalk.white(' LaunchFrame is in open beta for 100 users.'));
55
+ console.log(chalk.white(' Get free lifetime access at:'));
56
+ console.log(' ' + chalk.blue.bold.underline(makeClickable('launchframe.dev', betaUrl)));
57
+ console.log(' ' + chalk.cyan(betaUrl + '\n'));
58
+
59
+ console.log(chalk.cyan('→ Already have access?'));
58
60
  console.log(' ' + chalk.blue.bold.underline(makeClickable('Setup SSH key (docs)', docsUrl)));
59
61
  console.log(' ' + chalk.cyan(docsUrl + '\n'));
60
-
62
+
61
63
  console.log(chalk.gray('After setup, run: launchframe init\n'));
62
64
  }
63
65
 
@@ -0,0 +1,93 @@
1
+ const chalk = require('chalk');
2
+
3
+ /**
4
+ * Simple logger with verbose mode support
5
+ *
6
+ * Usage:
7
+ * logger.setVerbose(true);
8
+ * logger.info('Main message'); // Always shown
9
+ * logger.detail('Nested detail'); // Only shown in verbose mode
10
+ */
11
+
12
+ let verboseMode = false;
13
+
14
+ /**
15
+ * Enable or disable verbose mode
16
+ * @param {boolean} enabled
17
+ */
18
+ function setVerbose(enabled) {
19
+ verboseMode = enabled;
20
+ }
21
+
22
+ /**
23
+ * Check if verbose mode is enabled
24
+ * @returns {boolean}
25
+ */
26
+ function isVerbose() {
27
+ return verboseMode;
28
+ }
29
+
30
+ /**
31
+ * Log a main info message (always shown)
32
+ * @param {string} message
33
+ */
34
+ function info(message) {
35
+ console.log(message);
36
+ }
37
+
38
+ /**
39
+ * Log a success message (always shown)
40
+ * @param {string} message
41
+ */
42
+ function success(message) {
43
+ console.log(chalk.green(message));
44
+ }
45
+
46
+ /**
47
+ * Log an error message (always shown)
48
+ * @param {string} message
49
+ */
50
+ function error(message) {
51
+ console.error(chalk.red(message));
52
+ }
53
+
54
+ /**
55
+ * Log a warning message (always shown)
56
+ * @param {string} message
57
+ */
58
+ function warn(message) {
59
+ console.warn(chalk.yellow(message));
60
+ }
61
+
62
+ /**
63
+ * Log a detail/nested message (only in verbose mode)
64
+ * @param {string} message
65
+ * @param {number} indent - Indentation level (default 1)
66
+ */
67
+ function detail(message, indent = 1) {
68
+ if (verboseMode) {
69
+ const prefix = ' '.repeat(indent);
70
+ console.log(chalk.gray(`${prefix}${message}`));
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Log a step within an operation (only in verbose mode)
76
+ * @param {string} message
77
+ */
78
+ function step(message) {
79
+ if (verboseMode) {
80
+ console.log(chalk.gray(` - ${message}`));
81
+ }
82
+ }
83
+
84
+ module.exports = {
85
+ setVerbose,
86
+ isVerbose,
87
+ info,
88
+ success,
89
+ error,
90
+ warn,
91
+ detail,
92
+ step
93
+ };
@@ -0,0 +1,58 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const { replaceSection } = require('./section-replacer');
5
+
6
+ /**
7
+ * Install a module into a project
8
+ * @param {string} moduleName - Name of the module to install
9
+ * @param {Object} moduleConfig - Config object from MODULE_CONFIG[moduleName]
10
+ */
11
+ async function installModule(moduleName, moduleConfig) {
12
+ const templateRoot = path.resolve(__dirname, '../../../services');
13
+ const cwd = process.cwd();
14
+
15
+ for (const [serviceName, config] of Object.entries(moduleConfig)) {
16
+ const moduleFilesDir = path.join(templateRoot, 'modules', moduleName, serviceName, 'files');
17
+ const moduleSectionsDir = path.join(templateRoot, 'modules', moduleName, serviceName, 'sections');
18
+ const serviceDir = path.join(cwd, serviceName);
19
+
20
+ // Copy files
21
+ for (const filePath of config.files) {
22
+ const src = path.join(moduleFilesDir, filePath);
23
+ const dest = path.join(serviceDir, filePath);
24
+ console.log(` Adding ${filePath}`);
25
+ await fs.copy(src, dest, { overwrite: true });
26
+ }
27
+
28
+ // Inject sections
29
+ for (const [targetFile, markerNames] of Object.entries(config.sections)) {
30
+ const targetFilePath = path.join(serviceDir, targetFile);
31
+ const targetBasename = path.basename(targetFile);
32
+
33
+ for (const markerName of markerNames) {
34
+ const sectionFile = path.join(moduleSectionsDir, `${targetBasename}.${markerName}`);
35
+ console.log(` Injecting ${markerName} into ${targetFile}`);
36
+ const sectionContent = await fs.readFile(sectionFile, 'utf8');
37
+ await replaceSection(targetFilePath, markerName, sectionContent);
38
+ }
39
+ }
40
+
41
+ // Merge dependencies into package.json and sync lockfile
42
+ if (config.dependencies && Object.keys(config.dependencies).length > 0) {
43
+ const packageJsonPath = path.join(serviceDir, 'package.json');
44
+ const packageJson = await fs.readJson(packageJsonPath);
45
+ packageJson.dependencies = {
46
+ ...packageJson.dependencies,
47
+ ...config.dependencies,
48
+ };
49
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
50
+
51
+ // Run npm install to sync package-lock.json, otherwise `npm ci` will fail on rebuild
52
+ console.log(`\nRunning npm install in ${serviceName}...`);
53
+ execSync('npm install', { cwd: serviceDir, stdio: 'inherit' });
54
+ }
55
+ }
56
+ }
57
+
58
+ module.exports = { installModule };
@@ -95,6 +95,36 @@ function isWaitlistInstalled(config = null) {
95
95
  return (config.installedServices || []).includes('waitlist');
96
96
  }
97
97
 
98
+ /**
99
+ * Get list of installed modules
100
+ */
101
+ function getInstalledModules() {
102
+ const config = getProjectConfig();
103
+ return config.installedModules || [];
104
+ }
105
+
106
+ /**
107
+ * Check if a module is installed
108
+ */
109
+ function isModuleInstalled(moduleName) {
110
+ const installedModules = getInstalledModules();
111
+ return installedModules.includes(moduleName);
112
+ }
113
+
114
+ /**
115
+ * Add a module to the installed modules list
116
+ */
117
+ function addInstalledModule(moduleName) {
118
+ const config = getProjectConfig();
119
+ if (!config.installedModules) {
120
+ config.installedModules = [];
121
+ }
122
+ if (!config.installedModules.includes(moduleName)) {
123
+ config.installedModules.push(moduleName);
124
+ updateProjectConfig(config);
125
+ }
126
+ }
127
+
98
128
  module.exports = {
99
129
  isLaunchFrameProject,
100
130
  requireProject,
@@ -104,5 +134,8 @@ module.exports = {
104
134
  isComponentInstalled,
105
135
  addInstalledComponent,
106
136
  getPrimaryDomain,
107
- isWaitlistInstalled
137
+ isWaitlistInstalled,
138
+ getInstalledModules,
139
+ isModuleInstalled,
140
+ addInstalledModule
108
141
  };
@@ -3,6 +3,7 @@ const fs = require('fs-extra');
3
3
  const os = require('os');
4
4
  const { execSync } = require('child_process');
5
5
  const chalk = require('chalk');
6
+ const logger = require('./logger');
6
7
 
7
8
  const SERVICES_REPO = 'git@github.com:launchframe-dev/services.git';
8
9
  const BRANCH = 'main';
@@ -38,30 +39,26 @@ async function cacheExists() {
38
39
  async function initializeCache() {
39
40
  const cacheDir = getCacheDir();
40
41
 
41
- console.log(chalk.blue('🔄 Initializing services cache...'));
42
+ logger.detail('Initializing services cache...');
42
43
 
43
44
  try {
44
- // Ensure parent directory exists
45
45
  await fs.ensureDir(path.dirname(cacheDir));
46
46
 
47
- // Sparse clone (only root files, no services)
48
47
  execSync(
49
48
  `git clone --sparse --depth 1 --branch ${BRANCH} ${SERVICES_REPO} "${cacheDir}"`,
50
49
  {
51
- stdio: 'pipe', // Hide output
52
- timeout: 60000 // 1 minute timeout
50
+ stdio: 'pipe',
51
+ timeout: 60000
53
52
  }
54
53
  );
55
54
 
56
- // Configure sparse checkout (starts with empty set)
57
55
  execSync('git sparse-checkout init --cone', {
58
56
  cwd: cacheDir,
59
57
  stdio: 'pipe'
60
58
  });
61
59
 
62
- console.log(chalk.green('Cache initialized'));
60
+ logger.detail('Cache initialized');
63
61
  } catch (error) {
64
- // Clean up partial clone on failure
65
62
  await fs.remove(cacheDir);
66
63
  throw new Error(`Failed to initialize cache: ${error.message}`);
67
64
  }
@@ -75,16 +72,16 @@ async function initializeCache() {
75
72
  async function updateCache() {
76
73
  const cacheDir = getCacheDir();
77
74
 
78
- console.log(chalk.blue('🔄 Updating service cache...'));
75
+ logger.detail('Updating service cache...');
79
76
 
80
77
  try {
81
78
  execSync('git pull origin main', {
82
79
  cwd: cacheDir,
83
80
  stdio: 'pipe',
84
- timeout: 30000 // 30 seconds
81
+ timeout: 30000
85
82
  });
86
83
 
87
- console.log(chalk.green('Cache updated'));
84
+ logger.detail('Cache updated');
88
85
  } catch (error) {
89
86
  throw new Error(`Failed to update cache: ${error.message}`);
90
87
  }
@@ -98,10 +95,9 @@ async function updateCache() {
98
95
  async function expandServices(serviceNames) {
99
96
  const cacheDir = getCacheDir();
100
97
 
101
- console.log(chalk.blue(`📦 Loading services: ${serviceNames.join(', ')}...`));
98
+ logger.detail(`Loading services: ${serviceNames.join(', ')}...`);
102
99
 
103
100
  try {
104
- // Get current sparse checkout list
105
101
  let currentServices = [];
106
102
  try {
107
103
  const output = execSync('git sparse-checkout list', {
@@ -114,17 +110,15 @@ async function expandServices(serviceNames) {
114
110
  // No services yet, that's fine
115
111
  }
116
112
 
117
- // Add new services to the list
118
113
  const allServices = [...new Set([...currentServices, ...serviceNames])];
119
114
 
120
- // Set sparse checkout to include all services
121
115
  execSync(`git sparse-checkout set ${allServices.join(' ')}`, {
122
116
  cwd: cacheDir,
123
117
  stdio: 'pipe',
124
- timeout: 60000 // 1 minute (may need to download files)
118
+ timeout: 60000
125
119
  });
126
120
 
127
- console.log(chalk.green('Services loaded'));
121
+ logger.detail('Services loaded');
128
122
  } catch (error) {
129
123
  throw new Error(`Failed to expand services: ${error.message}`);
130
124
  }
@@ -158,7 +152,7 @@ async function clearCache() {
158
152
 
159
153
  if (await fs.pathExists(cacheDir)) {
160
154
  await fs.remove(cacheDir);
161
- console.log(chalk.green('Cache cleared'));
155
+ console.log(chalk.green('Cache cleared'));
162
156
  } else {
163
157
  console.log(chalk.gray('Cache is already empty'));
164
158
  }
@@ -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
  };