freeschema 1.0.0 → 1.0.1
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/bin/freeschema.js +413 -92
- package/package.json +1 -1
package/bin/freeschema.js
CHANGED
|
@@ -2,17 +2,78 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
-
const { spawnSync }
|
|
6
|
-
const fs
|
|
7
|
-
const path
|
|
8
|
-
|
|
9
|
-
const
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const readline = require('readline');
|
|
11
|
+
|
|
12
|
+
const VERSION = require('../package.json').version;
|
|
10
13
|
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
11
14
|
|
|
12
|
-
const args
|
|
15
|
+
const args = process.argv.slice(2);
|
|
13
16
|
const command = args[0];
|
|
14
17
|
|
|
15
|
-
// ── helpers
|
|
18
|
+
// ── prompt helpers ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function createRL() {
|
|
21
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ask(rl, question) {
|
|
25
|
+
return new Promise(resolve => rl.question(question, a => resolve(a.trim())));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function prompt(question, defaultVal = '') {
|
|
29
|
+
const rl = createRL();
|
|
30
|
+
const hint = defaultVal ? ` (${defaultVal})` : '';
|
|
31
|
+
const ans = await ask(rl, `${question}${hint}: `);
|
|
32
|
+
rl.close();
|
|
33
|
+
return ans || defaultVal;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function promptSecret(question) {
|
|
37
|
+
return new Promise(resolve => {
|
|
38
|
+
const rl = createRL();
|
|
39
|
+
// hide input on terminals that support it
|
|
40
|
+
if (process.stdout.isTTY) {
|
|
41
|
+
process.stdout.write(`${question}: `);
|
|
42
|
+
const stdin = process.openStdin();
|
|
43
|
+
process.stdin.setRawMode(true);
|
|
44
|
+
process.stdin.resume();
|
|
45
|
+
let val = '';
|
|
46
|
+
process.stdin.on('data', function handler(ch) {
|
|
47
|
+
const c = ch.toString();
|
|
48
|
+
if (c === '\n' || c === '\r' || c === '') {
|
|
49
|
+
process.stdin.setRawMode(false);
|
|
50
|
+
process.stdin.removeListener('data', handler);
|
|
51
|
+
process.stdout.write('\n');
|
|
52
|
+
rl.close();
|
|
53
|
+
resolve(val);
|
|
54
|
+
} else if (c === '') {
|
|
55
|
+
val = val.slice(0, -1);
|
|
56
|
+
} else {
|
|
57
|
+
val += c;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
ask(rl, `${question}: `).then(v => { rl.close(); resolve(v); });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function choose(question, options, defaultIdx = 0) {
|
|
67
|
+
console.log(`\n${question}`);
|
|
68
|
+
options.forEach((o, i) => console.log(` ${i + 1}) ${o}`));
|
|
69
|
+
const rl = createRL();
|
|
70
|
+
const ans = await ask(rl, `Choice (${defaultIdx + 1}): `);
|
|
71
|
+
rl.close();
|
|
72
|
+
const idx = parseInt(ans, 10) - 1;
|
|
73
|
+
return (idx >= 0 && idx < options.length) ? idx : defaultIdx;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── general helpers ───────────────────────────────────────────────────────────
|
|
16
77
|
|
|
17
78
|
function die(msg) {
|
|
18
79
|
console.error(`\nError: ${msg}\n`);
|
|
@@ -30,127 +91,386 @@ function copyRecursive(src, dest) {
|
|
|
30
91
|
}
|
|
31
92
|
}
|
|
32
93
|
|
|
33
|
-
function
|
|
34
|
-
|
|
94
|
+
function setEnvVar(envPath, key, value) {
|
|
95
|
+
let content = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
|
|
96
|
+
const escaped = value.replace(/\$/g, '\\$');
|
|
97
|
+
const re = new RegExp(`^(#\\s*)?${key}=.*`, 'm');
|
|
98
|
+
if (re.test(content)) {
|
|
99
|
+
content = content.replace(re, `${key}="${escaped}"`);
|
|
100
|
+
} else {
|
|
101
|
+
content += `\n${key}="${escaped}"`;
|
|
102
|
+
}
|
|
103
|
+
fs.writeFileSync(envPath, content);
|
|
104
|
+
}
|
|
35
105
|
|
|
106
|
+
function readEnvVar(envPath, key) {
|
|
107
|
+
if (!fs.existsSync(envPath)) return '';
|
|
108
|
+
const m = fs.readFileSync(envPath, 'utf8').match(new RegExp(`^${key}=(.*)`, 'm'));
|
|
109
|
+
return m ? m[1].replace(/^["']|["']$/g, '').trim() : '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function generateJwt() {
|
|
113
|
+
return crypto.randomBytes(32).toString('hex');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function runCompose(composeArgs, opts = {}) {
|
|
117
|
+
const cwd = process.cwd();
|
|
36
118
|
if (!fs.existsSync(path.join(cwd, 'docker-compose.yml'))) {
|
|
37
|
-
die('No docker-compose.yml found
|
|
119
|
+
die('No docker-compose.yml found.\nRun `freeschema init` first.');
|
|
38
120
|
}
|
|
39
|
-
|
|
40
121
|
const result = spawnSync('docker', ['compose', ...composeArgs], {
|
|
41
|
-
stdio: 'inherit',
|
|
42
|
-
cwd,
|
|
122
|
+
stdio: 'inherit', cwd, ...opts,
|
|
43
123
|
});
|
|
44
|
-
|
|
45
124
|
if (result.error) die(`Could not run docker: ${result.error.message}`);
|
|
46
125
|
if (result.status !== 0) process.exit(result.status);
|
|
47
126
|
}
|
|
48
127
|
|
|
49
|
-
|
|
128
|
+
function runComposeOutput(composeArgs) {
|
|
129
|
+
const cwd = process.cwd();
|
|
130
|
+
const result = spawnSync('docker', ['compose', ...composeArgs], { cwd });
|
|
131
|
+
if (result.error) die(`Could not run docker: ${result.error.message}`);
|
|
132
|
+
return (result.stdout || '').toString().trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getMysqlContainer() {
|
|
136
|
+
const out = runComposeOutput(['ps', '--format', 'json']);
|
|
137
|
+
if (!out) die('No containers running. Start with `freeschema start --local-db` first.');
|
|
138
|
+
const containers = out.split('\n').map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
139
|
+
const mysql = containers.find(c => c.Service === 'mysql' || (c.Name || '').includes('mysql-server'));
|
|
140
|
+
if (!mysql) die('MySQL container is not running.\nStart with `freeschema start --local-db`.');
|
|
141
|
+
return mysql.Name || mysql.ID;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readEnv() {
|
|
145
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
146
|
+
if (!fs.existsSync(envPath)) return {};
|
|
147
|
+
const env = {};
|
|
148
|
+
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
|
|
149
|
+
const m = line.match(/^([^#=]+)=(.*)/);
|
|
150
|
+
if (m) env[m[1].trim()] = m[2].trim().replace(/^["']|["']$/g, '');
|
|
151
|
+
}
|
|
152
|
+
return env;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── init (interactive) ────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
async function cmdInit() {
|
|
158
|
+
const target = process.cwd();
|
|
159
|
+
const envPath = path.join(target, '.env');
|
|
160
|
+
const envFrontendPath = path.join(target, '.env.frontend');
|
|
50
161
|
|
|
51
|
-
|
|
52
|
-
const target = process.cwd();
|
|
53
|
-
console.log(`Initializing FreeSchema in ${target}\n`);
|
|
162
|
+
console.log(`\nInitializing FreeSchema in ${target}\n`);
|
|
54
163
|
|
|
164
|
+
// 1. Copy template files
|
|
55
165
|
for (const entry of fs.readdirSync(TEMPLATES_DIR)) {
|
|
56
166
|
const src = path.join(TEMPLATES_DIR, entry);
|
|
57
167
|
const dest = path.join(target, entry);
|
|
58
|
-
|
|
59
|
-
if (fs.existsSync(dest)) {
|
|
60
|
-
console.log(` skip ${entry} (already exists)`);
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
|
|
168
|
+
if (fs.existsSync(dest)) { console.log(` skip ${entry}`); continue; }
|
|
64
169
|
copyRecursive(src, dest);
|
|
65
170
|
console.log(` create ${entry}`);
|
|
66
171
|
}
|
|
67
172
|
|
|
68
|
-
// .env from example if
|
|
69
|
-
const envFile = path.join(target, '.env');
|
|
173
|
+
// Bootstrap .env from example if missing
|
|
70
174
|
const envExample = path.join(target, '.env.example');
|
|
71
|
-
if (!fs.existsSync(
|
|
72
|
-
fs.copyFileSync(envExample,
|
|
73
|
-
console.log(' create .env
|
|
175
|
+
if (!fs.existsSync(envPath) && fs.existsSync(envExample)) {
|
|
176
|
+
fs.copyFileSync(envExample, envPath);
|
|
177
|
+
console.log(' create .env');
|
|
74
178
|
}
|
|
75
|
-
|
|
76
|
-
// .env.frontend from example if not present
|
|
77
|
-
const envFrontend = path.join(target, '.env.frontend');
|
|
78
179
|
const envFrontendExample = path.join(target, '.env.frontend.example');
|
|
79
|
-
if (!fs.existsSync(
|
|
80
|
-
fs.copyFileSync(envFrontendExample,
|
|
81
|
-
console.log(' create .env.frontend
|
|
180
|
+
if (!fs.existsSync(envFrontendPath) && fs.existsSync(envFrontendExample)) {
|
|
181
|
+
fs.copyFileSync(envFrontendExample, envFrontendPath);
|
|
182
|
+
console.log(' create .env.frontend');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log('\n─────────────────────────────────────────');
|
|
186
|
+
console.log(' Configuration Setup');
|
|
187
|
+
console.log('─────────────────────────────────────────\n');
|
|
188
|
+
|
|
189
|
+
// 2. JWT Secret
|
|
190
|
+
const existingJwt = readEnvVar(envPath, 'JWT_SECRET');
|
|
191
|
+
if (!existingJwt || existingJwt === '') {
|
|
192
|
+
const jwt = generateJwt();
|
|
193
|
+
setEnvVar(envPath, 'JWT_SECRET', jwt);
|
|
194
|
+
console.log(' JWT_SECRET auto-generated ✓');
|
|
195
|
+
} else {
|
|
196
|
+
console.log(' JWT_SECRET already set ✓');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 3. Database mode
|
|
200
|
+
const dbChoice = await choose(
|
|
201
|
+
'Database mode:',
|
|
202
|
+
['Local — spin up a MySQL container (recommended for getting started)',
|
|
203
|
+
'External — connect to an existing MySQL server (AWS RDS, self-hosted, etc.)'],
|
|
204
|
+
0
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (dbChoice === 0) {
|
|
208
|
+
// local db
|
|
209
|
+
setEnvVar(envPath, 'DB_MODE', 'local');
|
|
210
|
+
setEnvVar(envPath, 'DB_HOST', 'mysql-server');
|
|
211
|
+
setEnvVar(envPath, 'DB_PORT', '3306');
|
|
212
|
+
|
|
213
|
+
console.log('\n Local MySQL settings (press Enter to keep defaults):');
|
|
214
|
+
const dbUser = await prompt(' DB username', readEnvVar(envPath, 'MYSQL_USER') || 'freeschema');
|
|
215
|
+
const dbPass = await prompt(' DB password', readEnvVar(envPath, 'MYSQL_PASSWORD') || 'Freeschema@123');
|
|
216
|
+
const dbName = await prompt(' DB name', readEnvVar(envPath, 'MYSQL_DATABASE') || 'freeschema_db');
|
|
217
|
+
const dbPort = await prompt(' Exposed host port (MySQL)', readEnvVar(envPath, 'MYSQL_PORT') || '3308');
|
|
218
|
+
|
|
219
|
+
setEnvVar(envPath, 'DB_USER', dbUser);
|
|
220
|
+
setEnvVar(envPath, 'DB_PASSWORD', dbPass);
|
|
221
|
+
setEnvVar(envPath, 'DB_DATABASE', dbName);
|
|
222
|
+
setEnvVar(envPath, 'MYSQL_USER', dbUser);
|
|
223
|
+
setEnvVar(envPath, 'MYSQL_PASSWORD', dbPass);
|
|
224
|
+
setEnvVar(envPath, 'MYSQL_DATABASE', dbName);
|
|
225
|
+
setEnvVar(envPath, 'MYSQL_ROOT_PASSWORD', 'Admin@' + dbPass);
|
|
226
|
+
setEnvVar(envPath, 'MYSQL_PORT', dbPort);
|
|
227
|
+
setEnvVar(envPath, 'DB_SESSION_DATABASE', 'sessions_db');
|
|
228
|
+
console.log(' Database configured (local) ✓');
|
|
229
|
+
} else {
|
|
230
|
+
// external db
|
|
231
|
+
setEnvVar(envPath, 'DB_MODE', 'external');
|
|
232
|
+
console.log('\n External database connection:');
|
|
233
|
+
const dbHost = await prompt(' Host', '');
|
|
234
|
+
const dbPort = await prompt(' Port', '3306');
|
|
235
|
+
const dbUser = await prompt(' Username', '');
|
|
236
|
+
const dbPass = await prompt(' Password', '');
|
|
237
|
+
const dbName = await prompt(' Database name', 'freeschema_db');
|
|
238
|
+
const dbSess = await prompt(' Session database name', 'sessions_db');
|
|
239
|
+
|
|
240
|
+
setEnvVar(envPath, 'DB_HOST', dbHost);
|
|
241
|
+
setEnvVar(envPath, 'DB_PORT', dbPort);
|
|
242
|
+
setEnvVar(envPath, 'DB_USER', dbUser);
|
|
243
|
+
setEnvVar(envPath, 'DB_PASSWORD', dbPass);
|
|
244
|
+
setEnvVar(envPath, 'DB_DATABASE', dbName);
|
|
245
|
+
setEnvVar(envPath, 'DB_SESSION_DATABASE', dbSess);
|
|
246
|
+
console.log(' Database configured (external) ✓');
|
|
82
247
|
}
|
|
83
248
|
|
|
249
|
+
// 4. MQTT
|
|
250
|
+
const mqttChoice = await choose(
|
|
251
|
+
'MQTT broker:',
|
|
252
|
+
['Local — spin up an Eclipse Mosquitto container',
|
|
253
|
+
'External — use an existing MQTT broker'],
|
|
254
|
+
0
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (mqttChoice === 0) {
|
|
258
|
+
setEnvVar(envPath, 'MQTT_HOST', 'mqtt-broker');
|
|
259
|
+
setEnvVar(envPath, 'MQTT_PORT', '1883');
|
|
260
|
+
const mqttUser = await prompt(' MQTT username', readEnvVar(envPath, 'MQTT_USERNAME') || 'freeschema');
|
|
261
|
+
const mqttPass = await prompt(' MQTT password', readEnvVar(envPath, 'MQTT_PASSWORD') || 'freeschema');
|
|
262
|
+
setEnvVar(envPath, 'MQTT_USERNAME', mqttUser);
|
|
263
|
+
setEnvVar(envPath, 'MQTT_PASSWORD', mqttPass);
|
|
264
|
+
console.log(' MQTT configured (local) ✓');
|
|
265
|
+
} else {
|
|
266
|
+
console.log('\n External MQTT broker:');
|
|
267
|
+
const mqttHost = await prompt(' Host', '');
|
|
268
|
+
const mqttPort = await prompt(' Port', '1883');
|
|
269
|
+
const mqttUser = await prompt(' Username', '');
|
|
270
|
+
const mqttPass = await prompt(' Password', '');
|
|
271
|
+
setEnvVar(envPath, 'MQTT_HOST', mqttHost);
|
|
272
|
+
setEnvVar(envPath, 'MQTT_PORT', mqttPort);
|
|
273
|
+
setEnvVar(envPath, 'MQTT_USERNAME', mqttUser);
|
|
274
|
+
setEnvVar(envPath, 'MQTT_PASSWORD', mqttPass);
|
|
275
|
+
console.log(' MQTT configured (external) ✓');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 5. Nginx port
|
|
279
|
+
const nginxPort = await prompt('\nNginx port (the port you open in your browser)', readEnvVar(envPath, 'NGINX_PORT') || '80');
|
|
280
|
+
setEnvVar(envPath, 'NGINX_PORT', nginxPort);
|
|
281
|
+
|
|
282
|
+
// 6. Optional — SMTP
|
|
283
|
+
const smtpChoice = await choose(
|
|
284
|
+
'SMTP (email sending):',
|
|
285
|
+
['Skip for now (can edit .env later)',
|
|
286
|
+
'Configure now'],
|
|
287
|
+
0
|
|
288
|
+
);
|
|
289
|
+
if (smtpChoice === 1) {
|
|
290
|
+
const smtpHost = await prompt(' SMTP host', '');
|
|
291
|
+
const smtpPort = await prompt(' SMTP port', '587');
|
|
292
|
+
const smtpUser = await prompt(' SMTP username / email', '');
|
|
293
|
+
const smtpPass = await prompt(' SMTP password', '');
|
|
294
|
+
const smtpFrom = await prompt(' From address', smtpUser);
|
|
295
|
+
setEnvVar(envPath, 'SMTP_HOST', smtpHost);
|
|
296
|
+
setEnvVar(envPath, 'SMTP_PORT', smtpPort);
|
|
297
|
+
setEnvVar(envPath, 'SMTP_USERNAME', smtpUser);
|
|
298
|
+
setEnvVar(envPath, 'SMTP_PASSWORD', smtpPass);
|
|
299
|
+
setEnvVar(envPath, 'SMTP_FROM_ADDRESS', smtpFrom);
|
|
300
|
+
console.log(' SMTP configured ✓');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 7. Summary
|
|
304
|
+
const localDb = readEnvVar(envPath, 'DB_MODE') === 'local';
|
|
305
|
+
const localMqtt = readEnvVar(envPath, 'MQTT_HOST') === 'mqtt-broker';
|
|
306
|
+
|
|
84
307
|
console.log(`
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
308
|
+
─────────────────────────────────────────
|
|
309
|
+
Setup complete!
|
|
310
|
+
─────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
.env → configured
|
|
313
|
+
.env.frontend → ready (edit if needed)
|
|
314
|
+
JWT_SECRET → generated
|
|
315
|
+
|
|
316
|
+
Next — seed the database then start:
|
|
317
|
+
|
|
318
|
+
freeschema db seed --file /path/to/schema.sql
|
|
319
|
+
|
|
320
|
+
freeschema start${localDb ? ' --local-db' : ''}${localMqtt ? ' --local-mqtt' : ''}
|
|
321
|
+
|
|
322
|
+
Open your browser at: http://localhost:${nginxPort}
|
|
92
323
|
`);
|
|
93
324
|
}
|
|
94
325
|
|
|
326
|
+
// ── start ─────────────────────────────────────────────────────────────────────
|
|
327
|
+
|
|
95
328
|
function cmdStart() {
|
|
329
|
+
const cwd = process.cwd();
|
|
330
|
+
const missing = ['.env', '.env.frontend'].filter(f => !fs.existsSync(path.join(cwd, f)));
|
|
331
|
+
if (missing.length) {
|
|
332
|
+
console.error(`\nMissing required file(s): ${missing.join(', ')}`);
|
|
333
|
+
console.error('Run `freeschema init` first.\n');
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
|
|
96
337
|
const profiles = [];
|
|
97
338
|
const flags = args.slice(1);
|
|
98
|
-
|
|
99
339
|
if (flags.includes('--local-db') || flags.includes('--db')) profiles.push('--profile', 'local-db');
|
|
100
340
|
if (flags.includes('--local-mqtt') || flags.includes('--mqtt')) profiles.push('--profile', 'local-mqtt');
|
|
101
341
|
|
|
102
342
|
console.log('Pulling latest FreeSchema images…');
|
|
103
343
|
runCompose([...profiles, 'pull']);
|
|
104
|
-
|
|
105
344
|
console.log('\nStarting FreeSchema services…');
|
|
106
345
|
runCompose([...profiles, 'up', '-d']);
|
|
107
|
-
|
|
108
346
|
console.log('\nFreeSchema is running.');
|
|
109
|
-
console.log(' Logs: freeschema logs');
|
|
110
347
|
console.log(' Status: freeschema status');
|
|
348
|
+
console.log(' Logs: freeschema logs');
|
|
111
349
|
console.log(' Stop: freeschema stop');
|
|
112
350
|
}
|
|
113
351
|
|
|
114
|
-
|
|
115
|
-
console.log('Stopping FreeSchema services…');
|
|
116
|
-
runCompose(['down']);
|
|
117
|
-
}
|
|
352
|
+
// ── other commands ────────────────────────────────────────────────────────────
|
|
118
353
|
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function cmdStatus() {
|
|
125
|
-
runCompose(['ps']);
|
|
126
|
-
}
|
|
354
|
+
function cmdStop() { console.log('Stopping…'); runCompose(['down']); }
|
|
355
|
+
function cmdRestart() { const s = args[1]; s ? runCompose(['restart', s]) : runCompose(['restart']); }
|
|
356
|
+
function cmdStatus() { runCompose(['ps']); }
|
|
127
357
|
|
|
128
358
|
function cmdLogs() {
|
|
129
|
-
const service
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
if (follow) logArgs.push('--follow');
|
|
359
|
+
const service = args[1];
|
|
360
|
+
const logArgs = ['logs'];
|
|
361
|
+
if (!args.includes('--no-follow')) logArgs.push('--follow');
|
|
133
362
|
if (service) logArgs.push(service);
|
|
134
363
|
runCompose(logArgs);
|
|
135
364
|
}
|
|
136
365
|
|
|
137
366
|
function cmdPull() {
|
|
138
|
-
console.log('Pulling latest
|
|
367
|
+
console.log('Pulling latest images…');
|
|
139
368
|
runCompose(['pull']);
|
|
140
369
|
console.log('\nDone. Run `freeschema restart` to apply.');
|
|
141
370
|
}
|
|
142
371
|
|
|
143
372
|
function cmdDown() {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
373
|
+
const v = args.includes('--volumes') || args.includes('-v');
|
|
374
|
+
console.log(v ? 'Removing containers and volumes…' : 'Removing containers…');
|
|
375
|
+
runCompose(v ? ['down', '--volumes'] : ['down']);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── db subcommands ────────────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
function cmdDb() {
|
|
381
|
+
const sub = args[1];
|
|
382
|
+
switch (sub) {
|
|
383
|
+
case 'seed': cmdDbSeed(); break;
|
|
384
|
+
case 'backup': cmdDbBackup(); break;
|
|
385
|
+
case 'restore': cmdDbRestore(); break;
|
|
386
|
+
default:
|
|
387
|
+
console.log(`
|
|
388
|
+
freeschema db <subcommand>
|
|
389
|
+
|
|
390
|
+
seed --url <sql-url> Download a SQL file and place it in seed-db/
|
|
391
|
+
--file <path> Copy a local SQL file into seed-db/
|
|
392
|
+
backup [file] Dump database to a .sql file (default: backup-<date>.sql)
|
|
393
|
+
restore <file> Restore database from a .sql file
|
|
394
|
+
`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function cmdDbSeed() {
|
|
399
|
+
const urlIdx = args.indexOf('--url');
|
|
400
|
+
const fileIdx = args.indexOf('--file');
|
|
401
|
+
const seedDir = path.join(process.cwd(), 'seed-db');
|
|
402
|
+
if (!fs.existsSync(seedDir)) fs.mkdirSync(seedDir, { recursive: true });
|
|
403
|
+
|
|
404
|
+
if (fileIdx !== -1) {
|
|
405
|
+
const src = args[fileIdx + 1];
|
|
406
|
+
if (!src || !fs.existsSync(src)) die(`File not found: ${src}`);
|
|
407
|
+
const dest = path.join(seedDir, path.basename(src));
|
|
408
|
+
fs.copyFileSync(src, dest);
|
|
409
|
+
console.log(`Copied → seed-db/${path.basename(src)}`);
|
|
410
|
+
console.log('Start with `freeschema start --local-db` — MySQL applies the seed on first boot.');
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (urlIdx !== -1) {
|
|
415
|
+
const url = args[urlIdx + 1];
|
|
416
|
+
if (!url) die('--url requires a URL');
|
|
417
|
+
const name = path.basename(new URL(url).pathname) || 'seed.sql';
|
|
418
|
+
const dest = path.join(seedDir, name);
|
|
419
|
+
console.log(`Downloading → seed-db/${name}…`);
|
|
420
|
+
const file = fs.createWriteStream(dest);
|
|
421
|
+
https.get(url, res => {
|
|
422
|
+
res.pipe(file);
|
|
423
|
+
file.on('finish', () => console.log('Done. Run `freeschema start --local-db` to apply on first boot.'));
|
|
424
|
+
}).on('error', e => { fs.unlinkSync(dest); die(e.message); });
|
|
425
|
+
return;
|
|
151
426
|
}
|
|
427
|
+
|
|
428
|
+
die('Usage:\n freeschema db seed --url <url>\n freeschema db seed --file <path>');
|
|
152
429
|
}
|
|
153
430
|
|
|
431
|
+
function cmdDbBackup() {
|
|
432
|
+
const env = readEnv();
|
|
433
|
+
const dbUser = env.DB_USER || env.MYSQL_USER || 'freeschema';
|
|
434
|
+
const dbPass = env.DB_PASSWORD || env.MYSQL_PASSWORD || 'Freeschema@123';
|
|
435
|
+
const dbName = env.DB_DATABASE || 'freeschema_db';
|
|
436
|
+
const dateStr = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
|
|
437
|
+
const outFile = args[2] || `backup-${dateStr}.sql`;
|
|
438
|
+
const container = getMysqlContainer();
|
|
439
|
+
|
|
440
|
+
console.log(`Backing up ${dbName} → ${outFile}…`);
|
|
441
|
+
const result = spawnSync(
|
|
442
|
+
'docker',
|
|
443
|
+
['exec', container, 'mysqldump', `-u${dbUser}`, `-p${dbPass}`, '--single-transaction', dbName],
|
|
444
|
+
{ stdio: ['inherit', 'pipe', 'inherit'] }
|
|
445
|
+
);
|
|
446
|
+
if (result.error || result.status !== 0) die('mysqldump failed — check container logs.');
|
|
447
|
+
fs.writeFileSync(outFile, result.stdout);
|
|
448
|
+
console.log(`Backup saved: ${outFile}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function cmdDbRestore() {
|
|
452
|
+
const sqlFile = args[2];
|
|
453
|
+
if (!sqlFile) die('Usage: freeschema db restore <file.sql>');
|
|
454
|
+
if (!fs.existsSync(sqlFile)) die(`File not found: ${sqlFile}`);
|
|
455
|
+
|
|
456
|
+
const env = readEnv();
|
|
457
|
+
const dbUser = env.DB_USER || env.MYSQL_USER || 'freeschema';
|
|
458
|
+
const dbPass = env.DB_PASSWORD || env.MYSQL_PASSWORD || 'Freeschema@123';
|
|
459
|
+
const dbName = env.DB_DATABASE || 'freeschema_db';
|
|
460
|
+
const container = getMysqlContainer();
|
|
461
|
+
|
|
462
|
+
console.log(`Restoring ${sqlFile} → ${dbName}…`);
|
|
463
|
+
const result = spawnSync(
|
|
464
|
+
'docker',
|
|
465
|
+
['exec', '-i', container, 'mysql', `-u${dbUser}`, `-p${dbPass}`, dbName],
|
|
466
|
+
{ input: fs.readFileSync(sqlFile), stdio: ['pipe', 'inherit', 'inherit'] }
|
|
467
|
+
);
|
|
468
|
+
if (result.error || result.status !== 0) die('Restore failed — check container logs.');
|
|
469
|
+
console.log('Restore complete.');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ── help ──────────────────────────────────────────────────────────────────────
|
|
473
|
+
|
|
154
474
|
function cmdHelp() {
|
|
155
475
|
console.log(`
|
|
156
476
|
FreeSchema CLI v${VERSION}
|
|
@@ -159,23 +479,30 @@ Usage:
|
|
|
159
479
|
freeschema <command> [options]
|
|
160
480
|
|
|
161
481
|
Commands:
|
|
162
|
-
init
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
--local-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
pull
|
|
172
|
-
down [-v]
|
|
482
|
+
init Interactive setup — creates .env, generates JWT,
|
|
483
|
+
configures DB and MQTT
|
|
484
|
+
start Pull latest images and start all services
|
|
485
|
+
--local-db Spin up the bundled MySQL container
|
|
486
|
+
--local-mqtt Spin up the bundled MQTT broker
|
|
487
|
+
stop Stop all running services
|
|
488
|
+
restart [service] Restart all or one service
|
|
489
|
+
status Show container status
|
|
490
|
+
logs [service] Stream logs (--no-follow to print without streaming)
|
|
491
|
+
pull Pull latest images without restarting
|
|
492
|
+
down [-v] Remove containers (-v also removes data volumes)
|
|
493
|
+
|
|
494
|
+
db seed --url <url> Download SQL seed file into seed-db/
|
|
495
|
+
db seed --file <path> Copy local SQL file into seed-db/
|
|
496
|
+
db backup [file] Dump database to a .sql file
|
|
497
|
+
db restore <file> Restore database from a .sql file
|
|
173
498
|
|
|
174
499
|
Examples:
|
|
175
500
|
freeschema init
|
|
176
501
|
freeschema start --local-db --local-mqtt
|
|
502
|
+
freeschema db seed --file ./schema.sql
|
|
503
|
+
freeschema db backup
|
|
504
|
+
freeschema db restore backup-2026-05-19.sql
|
|
177
505
|
freeschema logs wico-server
|
|
178
|
-
freeschema restart node-server
|
|
179
506
|
freeschema down -v
|
|
180
507
|
`);
|
|
181
508
|
}
|
|
@@ -183,7 +510,7 @@ Examples:
|
|
|
183
510
|
// ── dispatch ──────────────────────────────────────────────────────────────────
|
|
184
511
|
|
|
185
512
|
switch (command) {
|
|
186
|
-
case 'init': cmdInit();
|
|
513
|
+
case 'init': cmdInit().catch(e => { console.error(e); process.exit(1); }); break;
|
|
187
514
|
case 'start': cmdStart(); break;
|
|
188
515
|
case 'stop': cmdStop(); break;
|
|
189
516
|
case 'restart': cmdRestart(); break;
|
|
@@ -191,15 +518,9 @@ switch (command) {
|
|
|
191
518
|
case 'logs': cmdLogs(); break;
|
|
192
519
|
case 'pull': cmdPull(); break;
|
|
193
520
|
case 'down': cmdDown(); break;
|
|
194
|
-
case '
|
|
195
|
-
case '-v':
|
|
196
|
-
|
|
197
|
-
break;
|
|
198
|
-
case '--help':
|
|
199
|
-
case '-h':
|
|
200
|
-
case undefined:
|
|
201
|
-
cmdHelp();
|
|
202
|
-
break;
|
|
521
|
+
case 'db': cmdDb(); break;
|
|
522
|
+
case '--version': case '-v': console.log(VERSION); break;
|
|
523
|
+
case '--help': case '-h': case undefined: cmdHelp(); break;
|
|
203
524
|
default:
|
|
204
525
|
console.error(`Unknown command: ${command}`);
|
|
205
526
|
cmdHelp();
|