@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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchframe/cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
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 };
|
package/src/commands/help.js
CHANGED
|
@@ -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);
|