@dtdyq/restbase 1.0.1 → 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 CHANGED
@@ -21,17 +21,27 @@
21
21
  bun install -g @dtdyq/restbase
22
22
  ```
23
23
 
24
- 安装后直接在任意目录使用 `restbase` 命令:
24
+ 安装后在任意目录使用 `restbase` 命令。所有配置通过 `.env` 文件读取:
25
25
 
26
26
  ```bash
27
- # 默认 SQLite 内存模式启动
28
- restbase
27
+ # 生成 .env 配置模板
28
+ restbase env
29
29
 
30
- # 指定数据库和端口
31
- DB_URL=sqlite://./data.db SVR_PORT=8080 restbase
30
+ # 前台启动(读取当前目录 .env)
31
+ restbase run
32
32
 
33
- # 连接 MySQL
34
- DB_URL=mysql://user:pass@localhost/mydb restbase
33
+ # 后台启动(daemon 模式)
34
+ restbase start
35
+
36
+ # 查看运行中的实例(含健康状态)
37
+ restbase status
38
+
39
+ # 查看某个实例的实时日志
40
+ restbase log <pid>
41
+
42
+ # 停止
43
+ restbase stop <pid> # 停止指定实例
44
+ restbase stop all # 停止所有实例
35
45
  ```
36
46
 
37
47
  更新到最新版:
@@ -44,14 +54,15 @@ bun install -g @dtdyq/restbase@latest
44
54
 
45
55
  ```bash
46
56
  bun install
47
- bun run server.ts
57
+ bun run src/server.ts
48
58
  ```
49
59
 
50
60
  ### 验证
51
61
 
52
62
  ```bash
53
- # 健康检查
63
+ # 健康检查(返回实例状态 + 资源占用)
54
64
  curl http://localhost:3333/api/health
65
+ # → { "code": "OK", "data": { "status": "healthy", "port": 3333, "pid": 12345, "uptime": 60, "memory": {...}, "cpu": {...} } }
55
66
 
56
67
  # Basic Auth 查询
57
68
  curl -u admin:admin http://localhost:3333/api/data/products
@@ -78,12 +89,7 @@ const data = await rb.table("products").query().where(gt("price", 100)).data();
78
89
  ## 构建与部署
79
90
 
80
91
  ```bash
81
- bun run build # 当前平台
82
- bun run build:linux # Linux x64
83
- bun run build:linux-arm # Linux ARM64
84
- bun run build:mac # macOS x64
85
- bun run build:mac-arm # macOS ARM64
86
- bun run build:windows # Windows x64
92
+ bun run build # 编译为当前平台独立二进制
87
93
 
88
94
  # 运行二进制(无需 Bun 运行时)
89
95
  ./restbase
@@ -93,6 +99,7 @@ bun run build:windows # Windows x64
93
99
 
94
100
  | 变量 | 说明 | 默认值 |
95
101
  |:--------------------------|:---------------------------|:--------------------|
102
+ | `SVR_NAME` | 实例名称(`status` 中显示) | 空 |
96
103
  | `SVR_PORT` | 服务端口 | `3333` |
97
104
  | `SVR_STATIC` | 静态文件目录(前端托管) | 空 |
98
105
  | `SVR_API_LIMIT` | API 限流(每秒每接口请求数) | `100` |
@@ -112,7 +119,7 @@ bun run build:windows # Windows x64
112
119
  ## 测试
113
120
 
114
121
  ```bash
115
- bun test rest.test.ts # 137+ 用例
122
+ bun test src/rest.test.ts # 137+ 用例
116
123
  ```
117
124
 
118
125
  ## 文档
@@ -124,30 +131,62 @@ bun test rest.test.ts # 137+ 用例
124
131
  | [documents/db_design.md](documents/db_design.md) | 数据库设计指南 — 表结构规范、约束、索引、设计模式与检查清单 |
125
132
  | [documents/design.md](documents/design.md) | 需求与设计文档 — 架构设计、技术规格、完整接口定义 |
126
133
 
134
+ ## CLI 命令
135
+
136
+ ```
137
+ restbase <command> [arguments]
138
+
139
+ Commands:
140
+ run 前台启动服务(读取当前目录 .env)
141
+ start 后台启动(daemon 模式)
142
+ stop <pid|all> 停止后台实例
143
+ status 查看运行中的实例(含健康检查)
144
+ log <pid> 实时查看实例日志
145
+ env 在当前目录生成 .env 配置模板
146
+ version 显示版本号
147
+ help 显示帮助
148
+ ```
149
+
150
+ 所有配置通过 `.env` 文件管理,可在 `.env` 中设置 `SVR_NAME` 为实例命名。
151
+
152
+ 实例元数据和默认日志存放在 `~/.restbase/`:
153
+
154
+ ```
155
+ ~/.restbase/
156
+ ├── instances/ # 实例元数据(name/port/pid/logPath)
157
+ │ ├── 12345.json
158
+ │ └── 23456.json
159
+ └── logs/ # daemon 默认日志目录
160
+ ├── 3333.log
161
+ ├── 3333.out
162
+ └── 8080.log
163
+ ```
164
+
127
165
  ## 文件结构
128
166
 
129
167
  ```
130
168
  restbase/
131
169
  ├── bin/
132
- │ └── restbase.ts # 全局命令入口(bun install -g
133
- ├── server.ts # 服务入口
134
- ├── types.ts # 配置 + 类型 + Zod Schema
135
- ├── db.ts # 数据库
136
- ├── auth.ts # 鉴权
137
- ├── crud.ts # CRUD 路由
138
- ├── query.ts # SQL 生成
139
- ├── logger.ts # 日志
170
+ │ └── restbase.ts # CLI 入口(run/start/stop/status
171
+ ├── src/
172
+ ├── server.ts # 服务启动
173
+ ├── types.ts # 配置 + 类型 + Zod Schema
174
+ ├── db.ts # 数据库
175
+ ├── auth.ts # 鉴权
176
+ ├── crud.ts # CRUD 路由
177
+ ├── query.ts # SQL 生成
178
+ │ ├── logger.ts # 日志
179
+ │ └── rest.test.ts # 集成测试
140
180
  ├── client/
141
- │ ├── restbase-client.ts # 前端客户端(独立 npm 包 @dtdyq/restbase-client)
142
- │ ├── README.md # 客户端文档
181
+ │ ├── restbase-client.ts # 前端客户端(独立 npm 包 @dtdyq/restbase-client)
182
+ │ ├── README.md # 客户端文档
143
183
  │ └── package.json
144
184
  ├── documents/
145
- │ ├── design.md # 需求设计文档
146
- │ ├── server.md # 服务端文档
147
- │ └── db_design.md # 数据库设计指南
148
- ├── init.sql # 初始化 SQL
149
- ├── rest.test.ts # 集成测试
150
- ├── .env / .env.test # 环境配置
185
+ │ ├── design.md # 需求设计文档
186
+ │ ├── server.md # 服务端文档
187
+ │ └── db_design.md # 数据库设计指南
188
+ ├── init.sql # 初始化 SQL
189
+ ├── .env / .env.test # 环境配置
151
190
  └── package.json
152
191
  ```
153
192
 
package/bin/restbase.ts CHANGED
@@ -1,2 +1,541 @@
1
1
  #!/usr/bin/env bun
2
- import "../server.ts";
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
+ }