@launchframe/cli 1.0.0-beta.25 → 1.0.0-beta.27

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LaunchFrame
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@launchframe/cli",
3
- "version": "1.0.0-beta.25",
3
+ "version": "1.0.0-beta.27",
4
4
  "description": "Production-ready B2B SaaS boilerplate with subscriptions, credits, and multi-tenancy",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -41,6 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "chalk": "^4.1.2",
44
+ "dotenv": "^17.3.1",
44
45
  "fs-extra": "^11.1.1",
45
46
  "inquirer": "^8.2.5"
46
47
  }
@@ -0,0 +1,84 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const inquirer = require('inquirer');
5
+ const { spawnSync } = require('child_process');
6
+ const { requireProject, getProjectConfig } = require('../utils/project-helpers');
7
+
8
+ async function databaseConsole({ remote = false } = {}) {
9
+ requireProject();
10
+
11
+ const infrastructurePath = path.join(process.cwd(), 'infrastructure');
12
+
13
+ if (!fs.existsSync(infrastructurePath)) {
14
+ console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
15
+ console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
16
+ process.exit(1);
17
+ }
18
+
19
+ if (remote) {
20
+ // 1. Check deployment is configured
21
+ const config = getProjectConfig();
22
+
23
+ if (!config.deployConfigured || !config.deployment) {
24
+ console.error(chalk.red('\n❌ Deployment is not configured.'));
25
+ console.log(chalk.gray('Run deploy:configure first.\n'));
26
+ process.exit(1);
27
+ }
28
+
29
+ const { vpsUser, vpsHost, vpsAppFolder } = config.deployment;
30
+
31
+ // 2. Warn before connecting to production
32
+ console.log(chalk.yellow.bold('\n⚠️ You are about to connect to the PRODUCTION database.\n'));
33
+ console.log(chalk.gray(` Host: ${vpsHost}`));
34
+ console.log(chalk.gray(` Folder: ${vpsAppFolder}\n`));
35
+
36
+ const { confirmed } = await inquirer.prompt([
37
+ {
38
+ type: 'confirm',
39
+ name: 'confirmed',
40
+ message: 'Are you sure you want to open a console to the production database?',
41
+ default: false
42
+ }
43
+ ]);
44
+
45
+ if (!confirmed) {
46
+ console.log(chalk.gray('\nAborted.\n'));
47
+ process.exit(0);
48
+ }
49
+
50
+ console.log(chalk.blue.bold('\n🔌 Connecting to production database...\n'));
51
+
52
+ // 3. Let the shell inside the container expand $POSTGRES_USER / $POSTGRES_DB.
53
+ // Pass the remote command as a single ssh argument (spawnSync array form)
54
+ // so the local shell never touches it.
55
+ const remoteCmd = `cd ${vpsAppFolder}/infrastructure && docker compose -f docker-compose.yml -f docker-compose.prod.yml exec -it database sh -c 'psql -U $POSTGRES_USER $POSTGRES_DB'`;
56
+
57
+ const result = spawnSync('ssh', ['-t', `${vpsUser}@${vpsHost}`, remoteCmd], { stdio: 'inherit' });
58
+
59
+ if (result.status !== 0) {
60
+ console.error(chalk.red('\n❌ Could not connect to the production database.'));
61
+ console.log(chalk.gray('Check that the VPS is reachable and services are running.\n'));
62
+ process.exit(1);
63
+ }
64
+ } else {
65
+ console.log(chalk.blue.bold('\n🗄️ Opening local database console...\n'));
66
+
67
+ // Let the shell inside the container expand $POSTGRES_USER / $POSTGRES_DB
68
+ const psqlCmd = [
69
+ 'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
70
+ 'exec', 'database', 'sh', '-c', 'psql -U $POSTGRES_USER $POSTGRES_DB'
71
+ ];
72
+
73
+ const result = spawnSync('docker', psqlCmd, { cwd: infrastructurePath, stdio: 'inherit' });
74
+
75
+ if (result.status !== 0) {
76
+ console.error(chalk.red('\n❌ Could not connect to the local database container.'));
77
+ console.log(chalk.gray('Make sure services are running:'));
78
+ console.log(chalk.white(' launchframe docker:up\n'));
79
+ process.exit(1);
80
+ }
81
+ }
82
+ }
83
+
84
+ module.exports = { databaseConsole };
@@ -37,6 +37,14 @@ function help() {
37
37
  console.log(chalk.gray(' docker:logs [service] View logs from all services or specific service'));
38
38
  console.log(chalk.gray(' docker:destroy Remove all resources (containers, volumes, images)'));
39
39
  console.log(chalk.gray(' --force, -f Skip confirmation prompt\n'));
40
+ console.log(chalk.white('Database Migrations:'));
41
+ console.log(chalk.gray(' migration:run Run pending database migrations'));
42
+ console.log(chalk.gray(' migration:create Create new database migration'));
43
+ console.log(chalk.gray(' migration:revert Revert last database migration\n'));
44
+ console.log(chalk.white('Database:'));
45
+ console.log(chalk.gray(' database:console Open a PostgreSQL console (local)'));
46
+ console.log(chalk.gray(' --remote Connect to the production database\n'));
47
+
40
48
  console.log(chalk.white('Service Management:'));
41
49
  console.log(chalk.gray(' service:add <name> Add an optional service to your project'));
42
50
  console.log(chalk.gray(' service:list List available services'));
@@ -77,9 +85,6 @@ function help() {
77
85
  } else {
78
86
  console.log(chalk.white('Available commands:'));
79
87
  console.log(chalk.gray(' init Initialize a new LaunchFrame project'));
80
- console.log(chalk.gray(' --project-name <name> Project name (skips prompt)'));
81
- console.log(chalk.gray(' --tenancy <single|multi> Tenancy model (skips prompt)'));
82
- console.log(chalk.gray(' --user-model <b2b|b2b2c> User model (skips prompt)'));
83
88
  console.log(chalk.gray(' help Show this help message\n'));
84
89
  console.log(chalk.white('Telemetry:'));
85
90
  console.log(chalk.gray(' telemetry Show telemetry status'));
@@ -90,10 +95,7 @@ function help() {
90
95
  console.log(chalk.gray(' cache:update Force update cache to latest version'));
91
96
  console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
92
97
  console.log(chalk.white('Examples:'));
93
- console.log(chalk.gray(' # Interactive mode'));
94
98
  console.log(chalk.gray(' launchframe init\n'));
95
- console.log(chalk.gray(' # Non-interactive mode'));
96
- console.log(chalk.gray(' launchframe init --project-name my-saas --tenancy single --user-model b2b\n'));
97
99
  console.log(chalk.gray(' # With verbose output'));
98
100
  console.log(chalk.gray(' launchframe init --verbose\n'));
99
101
  }
@@ -0,0 +1,40 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { execSync } = require('child_process');
5
+ const { requireProject } = require('../utils/project-helpers');
6
+
7
+ async function migrateCreate() {
8
+ requireProject();
9
+
10
+ const infrastructurePath = path.join(process.cwd(), 'infrastructure');
11
+
12
+ if (!fs.existsSync(infrastructurePath)) {
13
+ console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
14
+ console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
15
+ process.exit(1);
16
+ }
17
+
18
+ const migrationName = process.argv[3];
19
+
20
+ if (!migrationName) {
21
+ console.error(chalk.red('\n❌ Error: migration name is required'));
22
+ console.log(chalk.gray('Usage: launchframe migrate:create <name>\n'));
23
+ process.exit(1);
24
+ }
25
+
26
+ console.log(chalk.blue.bold(`\n🗄️ Creating migration: ${migrationName}\n`));
27
+
28
+ try {
29
+ execSync(`docker compose -f docker-compose.yml -f docker-compose.dev.yml exec backend npm run migration:create -- ${migrationName}`, {
30
+ cwd: infrastructurePath,
31
+ stdio: 'inherit'
32
+ });
33
+ console.log(chalk.green.bold('\n✅ Migration created successfully.\n'));
34
+ } catch (error) {
35
+ console.error(chalk.red('\n❌ Error creating migration:'), error.message);
36
+ process.exit(1);
37
+ }
38
+ }
39
+
40
+ module.exports = { migrateCreate };
@@ -0,0 +1,32 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { execSync } = require('child_process');
5
+ const { requireProject } = require('../utils/project-helpers');
6
+
7
+ async function migrateRevert() {
8
+ requireProject();
9
+
10
+ const infrastructurePath = path.join(process.cwd(), 'infrastructure');
11
+
12
+ if (!fs.existsSync(infrastructurePath)) {
13
+ console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
14
+ console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
15
+ process.exit(1);
16
+ }
17
+
18
+ console.log(chalk.blue.bold('\n🗄️ Reverting last database migration\n'));
19
+
20
+ try {
21
+ execSync('docker compose -f docker-compose.yml -f docker-compose.dev.yml exec backend npm run migration:revert', {
22
+ cwd: infrastructurePath,
23
+ stdio: 'inherit'
24
+ });
25
+ console.log(chalk.green.bold('\n✅ Migration reverted successfully.\n'));
26
+ } catch (error) {
27
+ console.error(chalk.red('\n❌ Error reverting migration:'), error.message);
28
+ process.exit(1);
29
+ }
30
+ }
31
+
32
+ module.exports = { migrateRevert };
@@ -0,0 +1,32 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { execSync } = require('child_process');
5
+ const { requireProject } = require('../utils/project-helpers');
6
+
7
+ async function migrateRun() {
8
+ requireProject();
9
+
10
+ const infrastructurePath = path.join(process.cwd(), 'infrastructure');
11
+
12
+ if (!fs.existsSync(infrastructurePath)) {
13
+ console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
14
+ console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
15
+ process.exit(1);
16
+ }
17
+
18
+ console.log(chalk.blue.bold('\n🗄️ Running database migrations\n'));
19
+
20
+ try {
21
+ execSync('docker compose -f docker-compose.yml -f docker-compose.dev.yml exec backend npm run migration:run', {
22
+ cwd: infrastructurePath,
23
+ stdio: 'inherit'
24
+ });
25
+ console.log(chalk.green.bold('\n✅ Migrations completed successfully.\n'));
26
+ } catch (error) {
27
+ console.error(chalk.red('\n❌ Error running migrations:'), error.message);
28
+ process.exit(1);
29
+ }
30
+ }
31
+
32
+ module.exports = { migrateRun };
package/src/index.js CHANGED
@@ -8,7 +8,8 @@ const { initTelemetry, trackEvent, sanitize, setTelemetryEnabled, showTelemetryS
8
8
  // Detect locally linked version: npm link installs to global node_modules
9
9
  // as a symlink. When running from a real install, __dirname is inside the
10
10
  // global node_modules folder. When linked, it resolves to the source directory.
11
- if (!__dirname.includes('node_modules')) {
11
+ const isDevMode = !__dirname.includes('node_modules');
12
+ if (isDevMode) {
12
13
  const packageJson = require('../package.json');
13
14
  console.log(chalk.yellow(`⚠ Running locally linked CLI v${packageJson.version} (${__dirname})`));
14
15
  }
@@ -28,6 +29,10 @@ const { dockerBuild } = require('./commands/docker-build');
28
29
  const { dockerUp } = require('./commands/docker-up');
29
30
  const { dockerDown } = require('./commands/docker-down');
30
31
  const { dockerLogs } = require('./commands/docker-logs');
32
+ const { migrateRun } = require('./commands/migration-run');
33
+ const { migrateCreate } = require('./commands/migration-create');
34
+ const { migrateRevert } = require('./commands/migration-revert');
35
+ const { databaseConsole } = require('./commands/database-console');
31
36
  const { dockerDestroy } = require('./commands/docker-destroy');
32
37
  const { doctor } = require('./commands/doctor');
33
38
  const { help } = require('./commands/help');
@@ -99,7 +104,7 @@ async function main() {
99
104
  // Route commands
100
105
  switch (command) {
101
106
  case 'init':
102
- await init({
107
+ await init({
103
108
  projectName: flags['project-name'],
104
109
  tenancy: flags['tenancy'],
105
110
  userModel: flags['user-model']
@@ -147,6 +152,18 @@ async function main() {
147
152
  case 'docker:destroy':
148
153
  await dockerDestroy({ force: flags.force || flags.f });
149
154
  break;
155
+ case 'migration:run':
156
+ await migrateRun();
157
+ break;
158
+ case 'migration:create':
159
+ await migrateCreate();
160
+ break;
161
+ case 'migration:revert':
162
+ await migrateRevert();
163
+ break;
164
+ case 'database:console':
165
+ await databaseConsole({ remote: flags.remote });
166
+ break;
150
167
  case 'doctor':
151
168
  await doctor();
152
169
  break;
@@ -63,11 +63,20 @@ function isDisabledByEnv() {
63
63
  return process.env.DO_NOT_TRACK === '1' || process.env.LAUNCHFRAME_TELEMETRY_DISABLED === '1';
64
64
  }
65
65
 
66
+ /**
67
+ * Check if running from a locally linked (dev) version
68
+ * @returns {boolean} True if running via npm link
69
+ */
70
+ function isDevMode() {
71
+ return !__dirname.includes('node_modules');
72
+ }
73
+
66
74
  /**
67
75
  * Check if telemetry is enabled
68
76
  * @returns {boolean} True if telemetry is enabled
69
77
  */
70
78
  function isEnabled() {
79
+ if (isDevMode()) return false;
71
80
  if (isDisabledByEnv()) return false;
72
81
  if (!config || !config.telemetry) return false;
73
82
  return config.telemetry.enabled !== false;
@@ -96,7 +105,7 @@ function initTelemetry() {
96
105
  writeConfig(config);
97
106
  }
98
107
 
99
- if (!config.telemetry.noticeShown && !isDisabledByEnv()) {
108
+ if (!config.telemetry.noticeShown && !isDisabledByEnv() && !isDevMode()) {
100
109
  console.log(
101
110
  chalk.gray(
102
111
  '\nLaunchFrame collects anonymous usage data to improve the CLI.\n' +