@launchframe/cli 1.0.0-beta.27 → 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.27",
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,6 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
+ "bcryptjs": "^2.4.3",
43
44
  "chalk": "^4.1.2",
44
45
  "dotenv": "^17.3.1",
45
46
  "fs-extra": "^11.1.1",
@@ -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 };
@@ -55,6 +55,8 @@ function help() {
55
55
  console.log(chalk.gray(' cache:info Show cache location, size, and cached services'));
56
56
  console.log(chalk.gray(' cache:update Force update cache to latest version'));
57
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'));
58
60
  console.log(chalk.white('Other commands:'));
59
61
  console.log(chalk.gray(' doctor Check project health and configuration'));
60
62
  console.log(chalk.gray(' telemetry Show telemetry status'));
package/src/index.js CHANGED
@@ -42,6 +42,7 @@ const {
42
42
  serviceRemove
43
43
  } = require('./commands/service');
44
44
  const { cacheClear, cacheInfo, cacheUpdate } = require('./commands/cache');
45
+ const { devAddUser } = require('./commands/dev-add-user');
45
46
 
46
47
  // Get command and arguments
47
48
  const command = process.argv[2];
@@ -195,6 +196,9 @@ async function main() {
195
196
  case 'cache:update':
196
197
  await cacheUpdate();
197
198
  break;
199
+ case 'dev:add-user':
200
+ await devAddUser();
201
+ break;
198
202
  case 'telemetry':
199
203
  if (flags.disable) {
200
204
  setTelemetryEnabled(false);