freeschema 1.0.0 → 1.0.2

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