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 }
|
|
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');
|
|
14
|
+
const DEFAULT_SEED_URL = 'https://apifreeschema%2540boomconcole.com@boomconcole.com/sql/coredb.sql';
|
|
11
15
|
|
|
12
|
-
const args
|
|
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
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
72
|
-
fs.copyFileSync(envExample,
|
|
73
|
-
console.log(' create .env
|
|
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(
|
|
80
|
-
fs.copyFileSync(envFrontendExample,
|
|
81
|
-
console.log(' create .env.frontend
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
125
|
-
|
|
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
|
|
130
|
-
const
|
|
131
|
-
|
|
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
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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('
|
|
150
|
-
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
--local-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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();
|
|
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 '
|
|
195
|
-
case '-v':
|
|
196
|
-
|
|
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();
|