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.
Files changed (2) hide show
  1. package/bin/freeschema.js +413 -92
  2. 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 } = 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');
11
14
 
12
- const args = process.argv.slice(2);
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 runCompose(composeArgs) {
34
- const cwd = process.cwd();
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 in the current directory.\nRun `freeschema init` first.');
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
- // ── commands ──────────────────────────────────────────────────────────────────
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
- function cmdInit() {
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 not present
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(envFile) && fs.existsSync(envExample)) {
72
- fs.copyFileSync(envExample, envFile);
73
- console.log(' create .env (copied from .env.example)');
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(envFrontend) && fs.existsSync(envFrontendExample)) {
80
- fs.copyFileSync(envFrontendExample, envFrontend);
81
- console.log(' create .env.frontend (copied from .env.frontend.example)');
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
- 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
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
- function cmdStop() {
115
- console.log('Stopping FreeSchema services…');
116
- runCompose(['down']);
117
- }
352
+ // ── other commands ────────────────────────────────────────────────────────────
118
353
 
119
- function cmdRestart() {
120
- const service = args[1];
121
- service ? runCompose(['restart', service]) : runCompose(['restart']);
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 = args[1];
130
- const follow = !args.includes('--no-follow');
131
- const logArgs = ['logs'];
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 FreeSchema images…');
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 withVolumes = args.includes('--volumes') || args.includes('-v');
145
- if (withVolumes) {
146
- console.log('Removing containers and volumes');
147
- runCompose(['down', '--volumes']);
148
- } else {
149
- console.log('Removing containers…');
150
- runCompose(['down']);
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 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)
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(); break;
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 '--version':
195
- case '-v':
196
- console.log(VERSION);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freeschema",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "FreeSchema — self-hosted deployment CLI",
5
5
  "keywords": ["freeschema", "self-hosted", "docker"],
6
6
  "homepage": "https://freeschema.com",