@dtdyq/restbase 1.0.0 → 2.0.0
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/README.md +114 -24
- package/bin/restbase.ts +540 -1
- package/client/README.md +541 -0
- package/client/package.json +3 -2
- package/client/restbase-client.ts +1 -1
- package/package.json +7 -20
- package/src/rest.test.ts +1465 -0
- package/{server.ts → src/server.ts} +29 -2
- package/{types.ts → src/types.ts} +2 -0
- /package/{auth.ts → src/auth.ts} +0 -0
- /package/{crud.ts → src/crud.ts} +0 -0
- /package/{db.ts → src/db.ts} +0 -0
- /package/{logger.ts → src/logger.ts} +0 -0
- /package/{query.ts → src/query.ts} +0 -0
package/bin/restbase.ts
CHANGED
|
@@ -1,2 +1,541 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* bin/restbase.ts — CLI entry point
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* restbase run Start server in foreground (reads .env)
|
|
7
|
+
* restbase start Start server in background (daemon)
|
|
8
|
+
* restbase stop <pid|all> Stop instance(s)
|
|
9
|
+
* restbase status Show running instances (table + health check)
|
|
10
|
+
* restbase log <pid> Tail log of a background instance
|
|
11
|
+
* restbase env Generate .env template in current directory
|
|
12
|
+
* restbase version Show version
|
|
13
|
+
* restbase help Show help
|
|
14
|
+
*
|
|
15
|
+
* All configuration is read from .env in the current working directory.
|
|
16
|
+
* Instance metadata is stored in ~/.restbase/ for global access.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {spawn} from "child_process";
|
|
20
|
+
import {
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
unlinkSync,
|
|
25
|
+
readdirSync,
|
|
26
|
+
openSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
} from "fs";
|
|
29
|
+
import {resolve, join} from "path";
|
|
30
|
+
import {homedir} from "os";
|
|
31
|
+
|
|
32
|
+
/* ═══════════ Global paths ═══════════ */
|
|
33
|
+
|
|
34
|
+
const RESTBASE_HOME = resolve(homedir(), ".restbase");
|
|
35
|
+
const INSTANCES_DIR = join(RESTBASE_HOME, "instances");
|
|
36
|
+
const LOGS_DIR = join(RESTBASE_HOME, "logs");
|
|
37
|
+
|
|
38
|
+
function ensureDirs() {
|
|
39
|
+
mkdirSync(INSTANCES_DIR, {recursive: true});
|
|
40
|
+
mkdirSync(LOGS_DIR, {recursive: true});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ═══════════ Instance metadata ═══════════ */
|
|
44
|
+
|
|
45
|
+
interface InstanceMeta {
|
|
46
|
+
pid: number;
|
|
47
|
+
name: string;
|
|
48
|
+
port: number;
|
|
49
|
+
logPath: string;
|
|
50
|
+
cwd: string;
|
|
51
|
+
startedAt: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function instanceFile(pid: number) {
|
|
55
|
+
return join(INSTANCES_DIR, `${pid}.json`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function saveInstance(meta: InstanceMeta) {
|
|
59
|
+
writeFileSync(instanceFile(meta.pid), JSON.stringify(meta, null, 2));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function loadInstance(pid: number): InstanceMeta | null {
|
|
63
|
+
const f = instanceFile(pid);
|
|
64
|
+
if (!existsSync(f)) return null;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(readFileSync(f, "utf-8")) as InstanceMeta;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadAllInstances(): InstanceMeta[] {
|
|
73
|
+
ensureDirs();
|
|
74
|
+
const files = readdirSync(INSTANCES_DIR).filter((f) => f.endsWith(".json"));
|
|
75
|
+
const result: InstanceMeta[] = [];
|
|
76
|
+
for (const f of files) {
|
|
77
|
+
try {
|
|
78
|
+
result.push(JSON.parse(readFileSync(join(INSTANCES_DIR, f), "utf-8")));
|
|
79
|
+
} catch { /* corrupted, skip */ }
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function removeInstance(pid: number) {
|
|
85
|
+
const f = instanceFile(pid);
|
|
86
|
+
if (existsSync(f)) unlinkSync(f);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ═══════════ Process helpers ═══════════ */
|
|
90
|
+
|
|
91
|
+
function isAlive(pid: number): boolean {
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid, 0);
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface HealthInfo {
|
|
101
|
+
status: string;
|
|
102
|
+
name?: string;
|
|
103
|
+
port?: number;
|
|
104
|
+
pid?: number;
|
|
105
|
+
logFile?: string;
|
|
106
|
+
startedAt?: string;
|
|
107
|
+
uptime?: number;
|
|
108
|
+
memory?: { rss: number; heapUsed: number; heapTotal: number; external: number };
|
|
109
|
+
cpu?: { user: number; system: number };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function fetchHealth(port: number): Promise<HealthInfo | null> {
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetch(`http://localhost:${port}/api/health`, {
|
|
115
|
+
signal: AbortSignal.timeout(2000),
|
|
116
|
+
});
|
|
117
|
+
const body = (await res.json()) as any;
|
|
118
|
+
return body?.code === "OK" ? (body.data as HealthInfo) : null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Build the command to re-spawn ourselves */
|
|
125
|
+
function getSelfCommand(): string[] {
|
|
126
|
+
if (
|
|
127
|
+
process.argv[1] &&
|
|
128
|
+
(process.argv[1].endsWith(".ts") || process.argv[1].endsWith(".js"))
|
|
129
|
+
) {
|
|
130
|
+
return [process.execPath, process.argv[1]];
|
|
131
|
+
}
|
|
132
|
+
return [process.execPath];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* ═══════════ Commands ═══════════ */
|
|
136
|
+
|
|
137
|
+
/** run — foreground, reads .env as-is */
|
|
138
|
+
async function cmdRun() {
|
|
139
|
+
await import("../src/server.ts");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** start — daemon mode */
|
|
143
|
+
async function cmdStart() {
|
|
144
|
+
ensureDirs();
|
|
145
|
+
|
|
146
|
+
const port = Number(process.env.SVR_PORT) || 3333;
|
|
147
|
+
const name = process.env.SVR_NAME || "";
|
|
148
|
+
|
|
149
|
+
// Reject if this port is already running
|
|
150
|
+
for (const inst of loadAllInstances()) {
|
|
151
|
+
if (inst.port === port && isAlive(inst.pid)) {
|
|
152
|
+
console.log(`RestBase already running on port ${port} (PID: ${inst.pid})`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Determine log path
|
|
158
|
+
const logPath = process.env.LOG_FILE
|
|
159
|
+
? resolve(process.cwd(), process.env.LOG_FILE)
|
|
160
|
+
: join(LOGS_DIR, `${port}.log`);
|
|
161
|
+
|
|
162
|
+
// Build child env: silence console, ensure LOG_FILE
|
|
163
|
+
const env: Record<string, string> = {
|
|
164
|
+
...(process.env as Record<string, string>),
|
|
165
|
+
LOG_CONSOLE: "false",
|
|
166
|
+
};
|
|
167
|
+
if (!process.env.LOG_FILE) {
|
|
168
|
+
env.LOG_FILE = logPath;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// stdout/stderr → separate crash log (safety net for non-pino output)
|
|
172
|
+
const stdoutPath = join(LOGS_DIR, `${port}.out`);
|
|
173
|
+
const out = openSync(stdoutPath, "a");
|
|
174
|
+
const err = openSync(stdoutPath, "a");
|
|
175
|
+
|
|
176
|
+
const self = getSelfCommand();
|
|
177
|
+
const child = spawn(self[0]!, [...self.slice(1), "run"], {
|
|
178
|
+
detached: true,
|
|
179
|
+
stdio: ["ignore", out, err],
|
|
180
|
+
env,
|
|
181
|
+
cwd: process.cwd(),
|
|
182
|
+
});
|
|
183
|
+
child.unref();
|
|
184
|
+
|
|
185
|
+
// Save instance metadata
|
|
186
|
+
saveInstance({
|
|
187
|
+
pid: child.pid!,
|
|
188
|
+
name,
|
|
189
|
+
port,
|
|
190
|
+
logPath,
|
|
191
|
+
cwd: process.cwd(),
|
|
192
|
+
startedAt: new Date().toISOString(),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
console.log(`RestBase started (PID: ${child.pid}, port: ${port})`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** stop — by PID or "all" */
|
|
199
|
+
function cmdStop(target: string) {
|
|
200
|
+
if (target === "all") {
|
|
201
|
+
const instances = loadAllInstances();
|
|
202
|
+
if (instances.length === 0) {
|
|
203
|
+
console.log("No RestBase instances found");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
for (const inst of instances) stopOne(inst.pid);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const pid = parseInt(target, 10);
|
|
211
|
+
if (isNaN(pid)) {
|
|
212
|
+
console.error(`Invalid PID: ${target}`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
stopOne(pid);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function stopOne(pid: number) {
|
|
219
|
+
const meta = loadInstance(pid);
|
|
220
|
+
if (!meta) {
|
|
221
|
+
console.log(`No instance found with PID ${pid}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
if (isAlive(pid)) process.kill(pid, "SIGTERM");
|
|
226
|
+
removeInstance(pid);
|
|
227
|
+
console.log(`Stopped PID ${pid} (port ${meta.port})`);
|
|
228
|
+
} catch {
|
|
229
|
+
removeInstance(pid);
|
|
230
|
+
console.log(`Process ${pid} already gone, cleaned up`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Format seconds into human readable uptime */
|
|
235
|
+
function fmtUptime(sec: number): string {
|
|
236
|
+
if (sec < 60) return `${sec}s`;
|
|
237
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m${sec % 60}s`;
|
|
238
|
+
const h = Math.floor(sec / 3600);
|
|
239
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
240
|
+
if (h < 24) return `${h}h${m}m`;
|
|
241
|
+
const d = Math.floor(h / 24);
|
|
242
|
+
return `${d}d${h % 24}h`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Format bytes into human readable size */
|
|
246
|
+
function fmtBytes(bytes: number): string {
|
|
247
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
248
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
|
|
249
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
250
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Format CPU microseconds into human readable percentage (approx) */
|
|
254
|
+
function fmtCpu(userUs: number, systemUs: number, uptimeSec: number): string {
|
|
255
|
+
if (uptimeSec <= 0) return "-";
|
|
256
|
+
const totalUs = userUs + systemUs;
|
|
257
|
+
const pct = (totalUs / (uptimeSec * 1_000_000)) * 100;
|
|
258
|
+
return `${pct.toFixed(1)}%`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Format ISO timestamp to local short form: MM-DD HH:mm:ss */
|
|
262
|
+
function fmtTime(iso: string): string {
|
|
263
|
+
const d = new Date(iso);
|
|
264
|
+
const MM = String(d.getMonth() + 1).padStart(2, "0");
|
|
265
|
+
const DD = String(d.getDate()).padStart(2, "0");
|
|
266
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
267
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
268
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
269
|
+
return `${MM}-${DD} ${hh}:${mm}:${ss}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** status — table with health check */
|
|
273
|
+
async function cmdStatus() {
|
|
274
|
+
const instances = loadAllInstances();
|
|
275
|
+
if (instances.length === 0) {
|
|
276
|
+
console.log("No RestBase instances found");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Column widths
|
|
281
|
+
const nW = 14, pW = 9, sW = 14, ptW = 7, stW = 17, uW = 10, mW = 10, cW = 8;
|
|
282
|
+
|
|
283
|
+
console.log(
|
|
284
|
+
"NAME".padEnd(nW) +
|
|
285
|
+
"PID".padEnd(pW) +
|
|
286
|
+
"STATE".padEnd(sW) +
|
|
287
|
+
"PORT".padEnd(ptW) +
|
|
288
|
+
"STARTED".padEnd(stW) +
|
|
289
|
+
"UPTIME".padEnd(uW) +
|
|
290
|
+
"MEM".padEnd(mW) +
|
|
291
|
+
"CPU".padEnd(cW) +
|
|
292
|
+
"LOG",
|
|
293
|
+
);
|
|
294
|
+
console.log("─".repeat(nW + pW + sW + ptW + stW + uW + mW + cW + 30));
|
|
295
|
+
|
|
296
|
+
for (const inst of instances) {
|
|
297
|
+
// Dead process → clean up silently
|
|
298
|
+
if (!isAlive(inst.pid)) {
|
|
299
|
+
removeInstance(inst.pid);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Fetch live info from health endpoint
|
|
304
|
+
const health = await fetchHealth(inst.port);
|
|
305
|
+
|
|
306
|
+
const name = health?.name || inst.name || "-";
|
|
307
|
+
const state = health?.status ?? "unreachable";
|
|
308
|
+
const started = health?.startedAt ? fmtTime(health.startedAt) : fmtTime(inst.startedAt);
|
|
309
|
+
const uptime = health?.uptime !== undefined ? fmtUptime(health.uptime) : "-";
|
|
310
|
+
const mem = health?.memory ? fmtBytes(health.memory.rss) : "-";
|
|
311
|
+
const cpu = (health?.cpu && health?.uptime)
|
|
312
|
+
? fmtCpu(health.cpu.user, health.cpu.system, health.uptime)
|
|
313
|
+
: "-";
|
|
314
|
+
const logPath = health?.logFile || inst.logPath;
|
|
315
|
+
|
|
316
|
+
console.log(
|
|
317
|
+
name.padEnd(nW) +
|
|
318
|
+
String(inst.pid).padEnd(pW) +
|
|
319
|
+
state.padEnd(sW) +
|
|
320
|
+
String(inst.port).padEnd(ptW) +
|
|
321
|
+
started.padEnd(stW) +
|
|
322
|
+
uptime.padEnd(uW) +
|
|
323
|
+
mem.padEnd(mW) +
|
|
324
|
+
cpu.padEnd(cW) +
|
|
325
|
+
logPath,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Find the latest log file matching the base path.
|
|
332
|
+
* pino-roll creates files like: app.2026-02-11.1.log (date + sequence inserted before extension)
|
|
333
|
+
* So for base "log/app.log", we look for "app*.log" in "log/".
|
|
334
|
+
*/
|
|
335
|
+
function findLatestLog(basePath: string): string | null {
|
|
336
|
+
// If exact file exists, use it
|
|
337
|
+
if (existsSync(basePath)) return basePath;
|
|
338
|
+
|
|
339
|
+
const dir = resolve(basePath, "..");
|
|
340
|
+
if (!existsSync(dir)) return null;
|
|
341
|
+
|
|
342
|
+
const base = basePath.split("/").pop()!; // "app.log"
|
|
343
|
+
const dot = base.lastIndexOf(".");
|
|
344
|
+
const stem = dot > 0 ? base.slice(0, dot) : base; // "app"
|
|
345
|
+
const ext = dot > 0 ? base.slice(dot) : ""; // ".log"
|
|
346
|
+
|
|
347
|
+
// Find all matching files, sort by mtime descending
|
|
348
|
+
const files = readdirSync(dir)
|
|
349
|
+
.filter((f) => f.startsWith(stem) && f.endsWith(ext) && f !== base)
|
|
350
|
+
.map((f) => ({name: f, mtime: Bun.file(join(dir, f)).lastModified}))
|
|
351
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
352
|
+
|
|
353
|
+
return files.length > 0 ? join(dir, files[0]!.name) : null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** log — tail -f the log file of an instance */
|
|
357
|
+
async function cmdLog(target: string) {
|
|
358
|
+
const pid = parseInt(target, 10);
|
|
359
|
+
if (isNaN(pid)) {
|
|
360
|
+
console.error(`Invalid PID: ${target}`);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const meta = loadInstance(pid);
|
|
365
|
+
if (!meta) {
|
|
366
|
+
console.error(`No instance found with PID ${pid}`);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const logFile = findLatestLog(meta.logPath);
|
|
371
|
+
if (!logFile) {
|
|
372
|
+
console.error(`Log file not found for base path: ${meta.logPath}`);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
console.log(`Tailing ${logFile} (Ctrl+C to stop)\n`);
|
|
377
|
+
|
|
378
|
+
const tail = spawn("tail", ["-f", "-n", "100", logFile], {
|
|
379
|
+
stdio: "inherit",
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
process.on("SIGINT", () => {
|
|
383
|
+
tail.kill();
|
|
384
|
+
process.exit(0);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
await new Promise<void>((resolve) => tail.on("close", () => resolve()));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** env — generate .env template */
|
|
391
|
+
function cmdEnv() {
|
|
392
|
+
const envPath = resolve(process.cwd(), ".env");
|
|
393
|
+
if (existsSync(envPath)) {
|
|
394
|
+
console.log(`.env already exists at ${envPath}`);
|
|
395
|
+
console.log("Remove it first if you want to regenerate.");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const content = `# ═══════════════════════════════════════════════════════════
|
|
400
|
+
# RestBase Configuration
|
|
401
|
+
# ═══════════════════════════════════════════════════════════
|
|
402
|
+
# Uncomment and modify the values you need.
|
|
403
|
+
# All variables have sensible defaults — you can start with
|
|
404
|
+
# an empty .env and only override what you need.
|
|
405
|
+
# ═══════════════════════════════════════════════════════════
|
|
406
|
+
|
|
407
|
+
# ── Server ────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
# SVR_NAME= # Instance name (shown in 'restbase status')
|
|
410
|
+
# SVR_PORT=3333 # Server port
|
|
411
|
+
# SVR_STATIC= # Static file directory for SPA hosting
|
|
412
|
+
# SVR_API_LIMIT=100 # Rate limit: max requests per second per API
|
|
413
|
+
|
|
414
|
+
# ── Database ──────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
# DB_URL=sqlite://:memory: # sqlite://<path> or mysql://user:pass@host/db
|
|
417
|
+
# DB_AUTH_TABLE=users # Auth table name
|
|
418
|
+
# DB_AUTH_FIELD=owner # Owner field name for tenant isolation
|
|
419
|
+
# DB_AUTH_FIELD_NULL_OPEN=false # Treat owner=NULL rows as public data
|
|
420
|
+
# DB_INIT_SQL= # SQL file to run on startup
|
|
421
|
+
|
|
422
|
+
# ── Auth ──────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
# AUTH_JWT_SECRET=restbase # JWT secret — CHANGE THIS IN PRODUCTION!
|
|
425
|
+
# AUTH_JWT_EXP=43200 # JWT expiry in seconds (default: 12 hours)
|
|
426
|
+
# AUTH_BASIC_OPEN=true # Enable Basic Auth
|
|
427
|
+
|
|
428
|
+
# ── Logging ───────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
# LOG_LEVEL=INFO # ERROR / INFO / DEBUG
|
|
431
|
+
# LOG_CONSOLE=true # Console output (auto-disabled in daemon mode)
|
|
432
|
+
# LOG_FILE= # Log file path (auto-configured in daemon mode)
|
|
433
|
+
# LOG_RETAIN_DAYS=7 # Log file retention days
|
|
434
|
+
`;
|
|
435
|
+
|
|
436
|
+
writeFileSync(envPath, content);
|
|
437
|
+
console.log(`Created ${envPath}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** version */
|
|
441
|
+
async function cmdVersion() {
|
|
442
|
+
const pkg = (await import("../package.json")).default;
|
|
443
|
+
console.log(`RestBase v${pkg.version}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** help */
|
|
447
|
+
function printHelp() {
|
|
448
|
+
console.log(`
|
|
449
|
+
RestBase — Zero-code REST API server for SQLite / MySQL
|
|
450
|
+
|
|
451
|
+
Usage:
|
|
452
|
+
restbase <command> [arguments]
|
|
453
|
+
|
|
454
|
+
Commands:
|
|
455
|
+
run Start server in foreground (reads .env)
|
|
456
|
+
start Start server in background (daemon mode)
|
|
457
|
+
stop <pid|all> Stop a background instance by PID, or stop all
|
|
458
|
+
status Show all running background instances
|
|
459
|
+
log <pid> Tail the log of a background instance
|
|
460
|
+
env Generate a .env template in current directory
|
|
461
|
+
version Show version
|
|
462
|
+
help Show this help
|
|
463
|
+
|
|
464
|
+
Examples:
|
|
465
|
+
restbase run Start in foreground
|
|
466
|
+
restbase start Start in background (daemon)
|
|
467
|
+
restbase status List running instances with health status
|
|
468
|
+
restbase log 12345 Tail log for instance with PID 12345
|
|
469
|
+
restbase stop 12345 Stop instance with PID 12345
|
|
470
|
+
restbase stop all Stop all background instances
|
|
471
|
+
restbase env Create a documented .env template
|
|
472
|
+
|
|
473
|
+
Configuration:
|
|
474
|
+
All settings are read from .env in the current working directory.
|
|
475
|
+
Run 'restbase env' to generate a documented template with all options.
|
|
476
|
+
|
|
477
|
+
Instance data is stored in ~/.restbase/ (PID files, logs, metadata).
|
|
478
|
+
`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/* ═══════════ Main ═══════════ */
|
|
482
|
+
|
|
483
|
+
const command = process.argv[2] || "run";
|
|
484
|
+
const arg1 = process.argv[3];
|
|
485
|
+
|
|
486
|
+
switch (command) {
|
|
487
|
+
case "run":
|
|
488
|
+
await cmdRun();
|
|
489
|
+
break;
|
|
490
|
+
|
|
491
|
+
case "start":
|
|
492
|
+
await cmdStart();
|
|
493
|
+
process.exit(0);
|
|
494
|
+
break;
|
|
495
|
+
|
|
496
|
+
case "stop":
|
|
497
|
+
if (!arg1) {
|
|
498
|
+
console.error("Usage: restbase stop <pid|all>");
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
cmdStop(arg1);
|
|
502
|
+
process.exit(0);
|
|
503
|
+
break;
|
|
504
|
+
|
|
505
|
+
case "status":
|
|
506
|
+
await cmdStatus();
|
|
507
|
+
process.exit(0);
|
|
508
|
+
break;
|
|
509
|
+
|
|
510
|
+
case "log":
|
|
511
|
+
if (!arg1) {
|
|
512
|
+
console.error("Usage: restbase log <pid>");
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
await cmdLog(arg1);
|
|
516
|
+
break;
|
|
517
|
+
|
|
518
|
+
case "env":
|
|
519
|
+
cmdEnv();
|
|
520
|
+
process.exit(0);
|
|
521
|
+
break;
|
|
522
|
+
|
|
523
|
+
case "version":
|
|
524
|
+
case "-v":
|
|
525
|
+
case "--version":
|
|
526
|
+
await cmdVersion();
|
|
527
|
+
process.exit(0);
|
|
528
|
+
break;
|
|
529
|
+
|
|
530
|
+
case "help":
|
|
531
|
+
case "-h":
|
|
532
|
+
case "--help":
|
|
533
|
+
printHelp();
|
|
534
|
+
process.exit(0);
|
|
535
|
+
break;
|
|
536
|
+
|
|
537
|
+
default:
|
|
538
|
+
console.error(`Unknown command: ${command}`);
|
|
539
|
+
console.error("Run 'restbase help' for usage.");
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|