@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 +71 -32
- package/bin/restbase.ts +540 -1
- package/package.json +7 -20
- package/src/rest.test.ts +1465 -0
- package/{server.ts → src/server.ts} +28 -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/README.md
CHANGED
|
@@ -21,17 +21,27 @@
|
|
|
21
21
|
bun install -g @dtdyq/restbase
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
安装后在任意目录使用 `restbase` 命令。所有配置通过 `.env` 文件读取:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
#
|
|
28
|
-
restbase
|
|
27
|
+
# 生成 .env 配置模板
|
|
28
|
+
restbase env
|
|
29
29
|
|
|
30
|
-
#
|
|
31
|
-
|
|
30
|
+
# 前台启动(读取当前目录 .env)
|
|
31
|
+
restbase run
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
|
|
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
|
|
133
|
-
├──
|
|
134
|
-
├──
|
|
135
|
-
├──
|
|
136
|
-
├──
|
|
137
|
-
├──
|
|
138
|
-
├──
|
|
139
|
-
├──
|
|
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
|
|
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
|
|
149
|
-
├──
|
|
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
|
-
|
|
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
|
+
}
|