@launchframe/cli 1.0.0-beta.26 → 1.0.0-beta.28

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.
@@ -5,7 +5,8 @@
5
5
  "WebFetch(domain:developer.mixpanel.com)",
6
6
  "Bash(node -c:*)",
7
7
  "Bash(node:*)",
8
- "Bash(git checkout:*)"
8
+ "Bash(git checkout:*)",
9
+ "Bash(grep:*)"
9
10
  ]
10
11
  }
11
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@launchframe/cli",
3
- "version": "1.0.0-beta.26",
3
+ "version": "1.0.0-beta.28",
4
4
  "description": "Production-ready B2B SaaS boilerplate with subscriptions, credits, and multi-tenancy",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -40,7 +40,9 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
+ "bcryptjs": "^2.4.3",
43
44
  "chalk": "^4.1.2",
45
+ "dotenv": "^17.3.1",
44
46
  "fs-extra": "^11.1.1",
45
47
  "inquirer": "^8.2.5"
46
48
  }
@@ -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 };
@@ -0,0 +1,165 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const chalk = require('chalk');
5
+ const bcrypt = require('bcryptjs');
6
+ const { spawnSync } = require('child_process');
7
+ const { requireProject, getProjectConfig } = require('../utils/project-helpers');
8
+
9
+ const FIRST_NAMES = [
10
+ 'Alice', 'Bob', 'Carol', 'David', 'Eve', 'Frank', 'Grace', 'Henry',
11
+ 'Iris', 'Jack', 'Karen', 'Leo', 'Mia', 'Nathan', 'Olivia'
12
+ ];
13
+
14
+ const LAST_NAMES = [
15
+ 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller',
16
+ 'Davis', 'Wilson', 'Moore', 'Taylor', 'Anderson', 'Thomas', 'Jackson', 'White'
17
+ ];
18
+
19
+ function randomElement(arr) {
20
+ return arr[Math.floor(Math.random() * arr.length)];
21
+ }
22
+
23
+ function generateDummyUser() {
24
+ const firstName = randomElement(FIRST_NAMES);
25
+ const lastName = randomElement(LAST_NAMES);
26
+ const suffix = String(Math.floor(1000 + Math.random() * 9000));
27
+ const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}${suffix}@example.com`;
28
+ return { firstName, lastName, name: `${firstName} ${lastName}`, email, suffix };
29
+ }
30
+
31
+ function checkEmailExists(infrastructurePath, email) {
32
+ const composeArgs = [
33
+ 'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
34
+ 'exec', '-T', 'database', 'sh', '-c', 'psql -U $POSTGRES_USER $POSTGRES_DB -t -c "SELECT COUNT(*) FROM users WHERE email = \'__EMAIL__\'"'
35
+ .replace('__EMAIL__', email.replace(/'/g, "''"))
36
+ ];
37
+
38
+ const result = spawnSync('docker', composeArgs, { cwd: infrastructurePath, encoding: 'utf8' });
39
+ if (result.status !== 0) return false;
40
+ const count = parseInt((result.stdout || '').trim(), 10);
41
+ return count > 0;
42
+ }
43
+
44
+ async function devAddUser() {
45
+ requireProject();
46
+
47
+ const infrastructurePath = path.join(process.cwd(), 'infrastructure');
48
+
49
+ if (!fs.existsSync(infrastructurePath)) {
50
+ console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
51
+ console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
52
+ process.exit(1);
53
+ }
54
+
55
+ // Check database container is running
56
+ const psResult = spawnSync(
57
+ 'docker',
58
+ [
59
+ 'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
60
+ 'ps', '--status', 'running', '-q', 'database'
61
+ ],
62
+ { cwd: infrastructurePath, encoding: 'utf8' }
63
+ );
64
+
65
+ if (!psResult.stdout || psResult.stdout.trim() === '') {
66
+ console.error(chalk.red('\n❌ Database container is not running.'));
67
+ console.log(chalk.gray('Start local services first:'));
68
+ console.log(chalk.white(' launchframe docker:up\n'));
69
+ process.exit(1);
70
+ }
71
+
72
+ // Generate user with unique email (up to 5 attempts)
73
+ let user = null;
74
+ for (let attempt = 0; attempt < 5; attempt++) {
75
+ const candidate = generateDummyUser();
76
+ if (!checkEmailExists(infrastructurePath, candidate.email)) {
77
+ user = candidate;
78
+ break;
79
+ }
80
+ }
81
+
82
+ if (!user) {
83
+ console.error(chalk.red('\n❌ Could not generate a unique email after 5 attempts.'));
84
+ process.exit(1);
85
+ }
86
+
87
+ const config = getProjectConfig();
88
+ const isMultiTenant = config.variants?.tenancy === 'multi-tenant';
89
+
90
+ const passwordHash = await bcrypt.hash('test123', 10);
91
+ const accountId = crypto.randomUUID();
92
+
93
+ const projectTitle = `Demo Project`;
94
+ const projectSlug = `demo-project-${user.suffix}`;
95
+
96
+ const projectInsert = isMultiTenant ? `
97
+ INSERT INTO projects (user_id, title, slug, description, created_at, updated_at)
98
+ VALUES (
99
+ new_user_id,
100
+ '${projectTitle}',
101
+ '${projectSlug}',
102
+ 'Auto-generated demo project',
103
+ NOW(),
104
+ NOW()
105
+ );` : '';
106
+
107
+ const sqlScript = `
108
+ DO $$
109
+ DECLARE
110
+ new_user_id INT;
111
+ BEGIN
112
+ INSERT INTO users (email, name, role, email_verified, is_active, created_at, updated_at)
113
+ VALUES (
114
+ '${user.email.replace(/'/g, "''")}',
115
+ '${user.name.replace(/'/g, "''")}',
116
+ 'business_user',
117
+ true,
118
+ true,
119
+ NOW(),
120
+ NOW()
121
+ )
122
+ RETURNING id INTO new_user_id;
123
+
124
+ INSERT INTO accounts (id, user_id, account_id, provider_id, password, created_at, updated_at)
125
+ VALUES (
126
+ '${accountId}',
127
+ new_user_id,
128
+ new_user_id::text,
129
+ 'credential',
130
+ '${passwordHash.replace(/'/g, "''")}',
131
+ NOW(),
132
+ NOW()
133
+ );
134
+ ${projectInsert}
135
+ END $$;
136
+ `;
137
+
138
+ const execResult = spawnSync(
139
+ 'docker',
140
+ [
141
+ 'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
142
+ 'exec', '-T', 'database', 'sh', '-c', 'psql -v ON_ERROR_STOP=1 -U $POSTGRES_USER $POSTGRES_DB'
143
+ ],
144
+ { cwd: infrastructurePath, input: sqlScript, encoding: 'utf8' }
145
+ );
146
+
147
+ if (execResult.status !== 0) {
148
+ console.error(chalk.red('\n❌ Failed to insert user into database.'));
149
+ if (execResult.stderr) {
150
+ console.error(chalk.gray(execResult.stderr));
151
+ }
152
+ process.exit(1);
153
+ }
154
+
155
+ console.log(chalk.green('\n✅ User created!'));
156
+ console.log(chalk.gray(` Name: ${user.name}`));
157
+ console.log(chalk.gray(` Email: ${user.email}`));
158
+ console.log(chalk.gray(` Password: test123`));
159
+ if (isMultiTenant) {
160
+ console.log(chalk.gray(` Project: ${projectTitle} (${projectSlug})`));
161
+ }
162
+ console.log();
163
+ }
164
+
165
+ module.exports = { devAddUser };
@@ -41,6 +41,9 @@ function help() {
41
41
  console.log(chalk.gray(' migration:run Run pending database migrations'));
42
42
  console.log(chalk.gray(' migration:create Create new database migration'));
43
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'));
44
47
 
45
48
  console.log(chalk.white('Service Management:'));
46
49
  console.log(chalk.gray(' service:add <name> Add an optional service to your project'));
@@ -52,6 +55,8 @@ function help() {
52
55
  console.log(chalk.gray(' cache:info Show cache location, size, and cached services'));
53
56
  console.log(chalk.gray(' cache:update Force update cache to latest version'));
54
57
  console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
58
+ console.log(chalk.white('Dev Helpers:'));
59
+ console.log(chalk.gray(' dev:add-user Generate and insert a random test user into the local database\n'));
55
60
  console.log(chalk.white('Other commands:'));
56
61
  console.log(chalk.gray(' doctor Check project health and configuration'));
57
62
  console.log(chalk.gray(' telemetry Show telemetry status'));
package/src/index.js CHANGED
@@ -32,6 +32,7 @@ const { dockerLogs } = require('./commands/docker-logs');
32
32
  const { migrateRun } = require('./commands/migration-run');
33
33
  const { migrateCreate } = require('./commands/migration-create');
34
34
  const { migrateRevert } = require('./commands/migration-revert');
35
+ const { databaseConsole } = require('./commands/database-console');
35
36
  const { dockerDestroy } = require('./commands/docker-destroy');
36
37
  const { doctor } = require('./commands/doctor');
37
38
  const { help } = require('./commands/help');
@@ -41,6 +42,7 @@ const {
41
42
  serviceRemove
42
43
  } = require('./commands/service');
43
44
  const { cacheClear, cacheInfo, cacheUpdate } = require('./commands/cache');
45
+ const { devAddUser } = require('./commands/dev-add-user');
44
46
 
45
47
  // Get command and arguments
46
48
  const command = process.argv[2];
@@ -160,6 +162,9 @@ async function main() {
160
162
  case 'migration:revert':
161
163
  await migrateRevert();
162
164
  break;
165
+ case 'database:console':
166
+ await databaseConsole({ remote: flags.remote });
167
+ break;
163
168
  case 'doctor':
164
169
  await doctor();
165
170
  break;
@@ -191,6 +196,9 @@ async function main() {
191
196
  case 'cache:update':
192
197
  await cacheUpdate();
193
198
  break;
199
+ case 'dev:add-user':
200
+ await devAddUser();
201
+ break;
194
202
  case 'telemetry':
195
203
  if (flags.disable) {
196
204
  setTelemetryEnabled(false);