orez 0.0.46 → 0.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +4 -8
  2. package/dist/admin/http-proxy.d.ts +31 -0
  3. package/dist/admin/http-proxy.d.ts.map +1 -0
  4. package/dist/admin/http-proxy.js +140 -0
  5. package/dist/admin/http-proxy.js.map +1 -0
  6. package/dist/admin/log-store.d.ts +22 -0
  7. package/dist/admin/log-store.d.ts.map +1 -0
  8. package/dist/admin/log-store.js +86 -0
  9. package/dist/admin/log-store.js.map +1 -0
  10. package/dist/admin/server.d.ts +19 -0
  11. package/dist/admin/server.d.ts.map +1 -0
  12. package/dist/admin/server.js +110 -0
  13. package/dist/admin/server.js.map +1 -0
  14. package/dist/admin/ui.d.ts +2 -0
  15. package/dist/admin/ui.d.ts.map +1 -0
  16. package/dist/admin/ui.js +683 -0
  17. package/dist/admin/ui.js.map +1 -0
  18. package/dist/cli.js +48 -1
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config.d.ts +4 -0
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +4 -0
  23. package/dist/config.js.map +1 -1
  24. package/dist/index.d.ts +9 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +180 -20
  27. package/dist/index.js.map +1 -1
  28. package/dist/log.d.ts +9 -0
  29. package/dist/log.d.ts.map +1 -1
  30. package/dist/log.js +24 -1
  31. package/dist/log.js.map +1 -1
  32. package/dist/pg-proxy.d.ts.map +1 -1
  33. package/dist/pg-proxy.js +19 -4
  34. package/dist/pg-proxy.js.map +1 -1
  35. package/dist/pglite-manager.d.ts +1 -0
  36. package/dist/pglite-manager.d.ts.map +1 -1
  37. package/dist/pglite-manager.js +1 -1
  38. package/dist/pglite-manager.js.map +1 -1
  39. package/dist/replication/handler.d.ts.map +1 -1
  40. package/dist/replication/handler.js +20 -2
  41. package/dist/replication/handler.js.map +1 -1
  42. package/dist/vite-plugin.d.ts +3 -0
  43. package/dist/vite-plugin.d.ts.map +1 -1
  44. package/dist/vite-plugin.js +24 -0
  45. package/dist/vite-plugin.js.map +1 -1
  46. package/package.json +4 -2
  47. package/src/admin/http-proxy.ts +186 -0
  48. package/src/admin/log-store.ts +111 -0
  49. package/src/admin/server.ts +148 -0
  50. package/src/admin/ui.ts +682 -0
  51. package/src/cli.ts +49 -1
  52. package/src/config.ts +8 -0
  53. package/src/index.ts +192 -20
  54. package/src/log.ts +25 -1
  55. package/src/pg-proxy.ts +26 -6
  56. package/src/pglite-manager.ts +1 -1
  57. package/src/replication/handler.ts +21 -2
  58. package/src/vite-plugin.ts +28 -0
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAW1C,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,OAA2B;IACtD,IAAI,IAAI,GAAiC,IAAI,CAAA;IAC7C,IAAI,QAAQ,GAAkB,IAAI,CAAA;IAElC,OAAO;QACL,IAAI,EAAE,MAAM;QAEZ,KAAK,CAAC,eAAe,CAAC,MAAM;YAC1B,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAA;YAC3C,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;YAElB,IAAI,OAAO,EAAE,EAAE,EAAE,CAAC;gBAChB,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAA;gBACtD,QAAQ,GAAG,MAAM,YAAY,CAAC;oBAC5B,IAAI,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI;oBAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO;iBAC/B,CAAC,CAAA;YACJ,CAAC;YAED,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBACxC,QAAQ,EAAE,KAAK,EAAE,CAAA;gBACjB,IAAI,IAAI,EAAE,CAAC;oBACT,MAAM,IAAI,EAAE,CAAA;oBACZ,IAAI,GAAG,IAAI,CAAA;gBACb,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;KACF,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAc1C,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,OAA2B;IACtD,IAAI,IAAI,GAAiC,IAAI,CAAA;IAC7C,IAAI,QAAQ,GAAkB,IAAI,CAAA;IAClC,IAAI,WAAW,GAAkB,IAAI,CAAA;IAErC,OAAO;QACL,IAAI,EAAE,MAAM;QAEZ,KAAK,CAAC,eAAe,CAAC,MAAM;YAC1B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YAC5B,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAA;YAC3C,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;YAElB,IAAI,OAAO,EAAE,EAAE,EAAE,CAAC;gBAChB,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAA;gBACtD,QAAQ,GAAG,MAAM,YAAY,CAAC;oBAC5B,IAAI,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI;oBAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO;iBAC/B,CAAC,CAAA;YACJ,CAAC;YAED,IAAI,OAAO,EAAE,KAAK,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;gBACtC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAA;gBAC9C,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAA;gBACxC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAA;gBACnE,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,CAAA;gBAC9C,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAA;gBAC9D,WAAW,GAAG,MAAM,gBAAgB,CAAC;oBACnC,IAAI,EAAE,YAAY;oBAClB,QAAQ,EAAE,MAAM,CAAC,QAAQ;oBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,OAAO,EAAE,MAAM,CAAC,OAAO;oBACvB,OAAO,EAAE,MAAM,CAAC,OAAO;oBACvB,SAAS;oBACT,OAAO,EAAE,MAAM,CAAC,YAAY,IAAI,SAAS;iBAC1C,CAAC,CAAA;gBACF,GAAG,CAAC,IAAI,CAAC,2BAA2B,YAAY,EAAE,CAAC,CAAA;gBACnD,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;oBAC5B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAA;oBAC7C,GAAG,CAAC,IAAI,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;gBACzE,CAAC;YACH,CAAC;YAED,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBACxC,WAAW,EAAE,KAAK,EAAE,CAAA;gBACpB,QAAQ,EAAE,KAAK,EAAE,CAAA;gBACjB,IAAI,IAAI,EAAE,CAAC;oBACT,MAAM,IAAI,EAAE,CAAA;oBACZ,IAAI,GAAG,IAAI,CAAA;gBACb,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;KACF,CAAA;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orez",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
4
4
  "description": "PGlite-powered zero-sync development backend. No Docker required.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,13 +40,15 @@
40
40
  "check": "tsc --noEmit",
41
41
  "demo:test": "bun demo/src/test/run-e2e.ts",
42
42
  "check:all": "bun run lint && bun run format:check && bun run check && bun run test",
43
+ "test:chat": "bun scripts/test-chat-integration.ts",
44
+ "test:chat:smoke": "bun scripts/test-chat-integration.ts --smoke",
43
45
  "release": "bun scripts/release.ts"
44
46
  },
45
47
  "dependencies": {
46
48
  "@electric-sql/pglite": "^0.2.17",
47
49
  "@electric-sql/pglite-tools": "0.2.4",
48
50
  "@rocicorp/zero": ">=0.1.0",
49
- "bedrock-sqlite": "0.0.39",
51
+ "bedrock-sqlite": "0.0.40",
50
52
  "citty": "^0.2.0",
51
53
  "postgres": "^3.4.5",
52
54
  "pgsql-parser": "^17.9.11"
@@ -0,0 +1,186 @@
1
+ import {
2
+ createServer,
3
+ request as httpRequest,
4
+ type Server,
5
+ type IncomingMessage,
6
+ type ServerResponse,
7
+ } from 'node:http'
8
+ import type { Socket } from 'node:net'
9
+
10
+ export interface HttpLogEntry {
11
+ id: number
12
+ ts: number
13
+ method: string
14
+ path: string
15
+ status: number
16
+ duration: number
17
+ reqSize: number
18
+ resSize: number
19
+ reqHeaders: Record<string, string>
20
+ resHeaders: Record<string, string>
21
+ }
22
+
23
+ export interface HttpLogStore {
24
+ push(entry: Omit<HttpLogEntry, 'id'>): void
25
+ query(opts?: { since?: number; path?: string }): { entries: HttpLogEntry[]; cursor: number }
26
+ clear(): void
27
+ }
28
+
29
+ const MAX_ENTRIES = 10_000
30
+
31
+ export function createHttpLogStore(): HttpLogStore {
32
+ const entries: HttpLogEntry[] = []
33
+ let nextId = 1
34
+
35
+ function push(entry: Omit<HttpLogEntry, 'id'>) {
36
+ const full: HttpLogEntry = { ...entry, id: nextId++ }
37
+ entries.push(full)
38
+ if (entries.length > MAX_ENTRIES) entries.splice(0, entries.length - MAX_ENTRIES)
39
+ }
40
+
41
+ function query(opts?: { since?: number; path?: string }) {
42
+ let result: HttpLogEntry[] = entries
43
+ if (opts?.since) {
44
+ const since = opts.since
45
+ let lo = 0
46
+ let hi = result.length
47
+ while (lo < hi) {
48
+ const mid = (lo + hi) >>> 1
49
+ if (result[mid].id <= since) lo = mid + 1
50
+ else hi = mid
51
+ }
52
+ result = result.slice(lo)
53
+ }
54
+ if (opts?.path) {
55
+ const p = opts.path
56
+ result = result.filter((e) => e.path.includes(p))
57
+ }
58
+ return {
59
+ entries: result,
60
+ cursor: entries.length > 0 ? entries[entries.length - 1].id : 0,
61
+ }
62
+ }
63
+
64
+ function clear() {
65
+ entries.length = 0
66
+ }
67
+
68
+ return { push, query, clear }
69
+ }
70
+
71
+ function flatHeaders(headers: Record<string, any>): Record<string, string> {
72
+ const out: Record<string, string> = {}
73
+ for (const [k, v] of Object.entries(headers)) {
74
+ out[k] = Array.isArray(v) ? v.join(', ') : String(v ?? '')
75
+ }
76
+ return out
77
+ }
78
+
79
+ export function startHttpProxy(opts: {
80
+ listenPort: number
81
+ targetPort: number
82
+ httpLog: HttpLogStore
83
+ }): Promise<Server> {
84
+ const { listenPort, targetPort, httpLog } = opts
85
+
86
+ const server = createServer((req: IncomingMessage, res: ServerResponse) => {
87
+ const start = Date.now()
88
+ let reqSize = 0
89
+ const reqChunks: Buffer[] = []
90
+
91
+ req.on('data', (chunk: Buffer) => {
92
+ reqSize += chunk.length
93
+ reqChunks.push(chunk)
94
+ })
95
+
96
+ req.on('end', () => {
97
+ const proxyReq = httpRequest(
98
+ {
99
+ hostname: '127.0.0.1',
100
+ port: targetPort,
101
+ path: req.url,
102
+ method: req.method,
103
+ headers: req.headers,
104
+ },
105
+ (proxyRes) => {
106
+ let resSize = 0
107
+ proxyRes.on('data', (chunk: Buffer) => {
108
+ resSize += chunk.length
109
+ })
110
+ proxyRes.on('end', () => {
111
+ httpLog.push({
112
+ ts: start,
113
+ method: req.method || 'GET',
114
+ path: req.url || '/',
115
+ status: proxyRes.statusCode || 0,
116
+ duration: Date.now() - start,
117
+ reqSize,
118
+ resSize,
119
+ reqHeaders: flatHeaders(req.headers),
120
+ resHeaders: flatHeaders(proxyRes.headers),
121
+ })
122
+ })
123
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers)
124
+ proxyRes.pipe(res)
125
+ }
126
+ )
127
+
128
+ proxyReq.on('error', (err) => {
129
+ res.writeHead(502)
130
+ res.end('proxy error: ' + err.message)
131
+ })
132
+
133
+ for (const chunk of reqChunks) proxyReq.write(chunk)
134
+ proxyReq.end()
135
+ })
136
+ })
137
+
138
+ // websocket upgrade passthrough
139
+ server.on('upgrade', (req: IncomingMessage, socket: Socket, head: Buffer) => {
140
+ const start = Date.now()
141
+ const proxyReq = httpRequest({
142
+ hostname: '127.0.0.1',
143
+ port: targetPort,
144
+ path: req.url,
145
+ method: req.method,
146
+ headers: req.headers,
147
+ })
148
+
149
+ proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
150
+ httpLog.push({
151
+ ts: start,
152
+ method: 'WS',
153
+ path: req.url || '/',
154
+ status: 101,
155
+ duration: Date.now() - start,
156
+ reqSize: 0,
157
+ resSize: 0,
158
+ reqHeaders: flatHeaders(req.headers),
159
+ resHeaders: flatHeaders(proxyRes.headers),
160
+ })
161
+
162
+ // forward the 101 response
163
+ let rawHeaders = 'HTTP/1.1 101 Switching Protocols\r\n'
164
+ for (const [k, v] of Object.entries(proxyRes.headers)) {
165
+ rawHeaders += `${k}: ${Array.isArray(v) ? v.join(', ') : v}\r\n`
166
+ }
167
+ rawHeaders += '\r\n'
168
+ socket.write(rawHeaders)
169
+ if (proxyHead.length) socket.write(proxyHead)
170
+
171
+ proxySocket.pipe(socket)
172
+ socket.pipe(proxySocket)
173
+ proxySocket.on('error', () => socket.destroy())
174
+ socket.on('error', () => proxySocket.destroy())
175
+ })
176
+
177
+ proxyReq.on('error', () => socket.destroy())
178
+ proxyReq.write(head)
179
+ proxyReq.end()
180
+ })
181
+
182
+ return new Promise((resolve, reject) => {
183
+ server.listen(listenPort, '127.0.0.1', () => resolve(server))
184
+ server.on('error', reject)
185
+ })
186
+ }
@@ -0,0 +1,111 @@
1
+ import { existsSync, mkdirSync, appendFileSync, statSync, renameSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export interface LogEntry {
5
+ id: number
6
+ ts: number
7
+ source: string
8
+ level: string
9
+ msg: string
10
+ }
11
+
12
+ export interface LogStore {
13
+ push(source: string, level: string, msg: string): void
14
+ query(opts?: { source?: string; level?: string; since?: number }): {
15
+ entries: LogEntry[]
16
+ cursor: number
17
+ }
18
+ getAll(): LogEntry[]
19
+ clear(): void
20
+ }
21
+
22
+ const ANSI_RE = /\x1b\[[0-9;]*m/g
23
+ const MAX_ENTRIES = 50_000
24
+ const MAX_FILE_SIZE = 5 * 1024 * 1024
25
+ const LEVEL_PRIORITY: Record<string, number> = { error: 0, warn: 1, info: 2, debug: 3 }
26
+
27
+ export function createLogStore(dataDir: string, writeToDisk = true): LogStore {
28
+ const entries: LogEntry[] = []
29
+ let nextId = 1
30
+
31
+ const logsDir = join(dataDir, 'logs')
32
+ const logFile = join(logsDir, 'orez.log')
33
+ const backupFile = join(logsDir, 'orez.log.1')
34
+
35
+ if (writeToDisk) {
36
+ mkdirSync(logsDir, { recursive: true })
37
+ }
38
+
39
+ function rotateIfNeeded() {
40
+ if (!writeToDisk) return
41
+ try {
42
+ if (!existsSync(logFile)) return
43
+ const stat = statSync(logFile)
44
+ if (stat.size > MAX_FILE_SIZE) {
45
+ renameSync(logFile, backupFile)
46
+ }
47
+ } catch {}
48
+ }
49
+
50
+ function push(source: string, level: string, msg: string) {
51
+ const entry: LogEntry = {
52
+ id: nextId++,
53
+ ts: Date.now(),
54
+ source,
55
+ level,
56
+ msg: msg.replace(ANSI_RE, ''),
57
+ }
58
+ entries.push(entry)
59
+ if (entries.length > MAX_ENTRIES) {
60
+ entries.splice(0, entries.length - MAX_ENTRIES)
61
+ }
62
+ if (writeToDisk) {
63
+ try {
64
+ const ts = new Date(entry.ts).toISOString()
65
+ appendFileSync(logFile, '[' + ts + '] [' + source + '] [' + level + '] ' + entry.msg + '\n')
66
+ rotateIfNeeded()
67
+ } catch {}
68
+ }
69
+ }
70
+
71
+ function query(opts?: { source?: string; level?: string; since?: number }) {
72
+ let result = entries
73
+
74
+ if (opts?.since) {
75
+ const since = opts.since
76
+ let lo = 0
77
+ let hi = result.length
78
+ while (lo < hi) {
79
+ const mid = (lo + hi) >>> 1
80
+ if (result[mid].id <= since) lo = mid + 1
81
+ else hi = mid
82
+ }
83
+ result = result.slice(lo)
84
+ }
85
+
86
+ if (opts?.source) {
87
+ const source = opts.source
88
+ result = result.filter((e) => e.source === source)
89
+ }
90
+
91
+ if (opts?.level) {
92
+ const maxPriority = LEVEL_PRIORITY[opts.level] ?? 3
93
+ result = result.filter((e) => (LEVEL_PRIORITY[e.level] ?? 3) <= maxPriority)
94
+ }
95
+
96
+ return {
97
+ entries: result,
98
+ cursor: entries.length > 0 ? entries[entries.length - 1].id : 0,
99
+ }
100
+ }
101
+
102
+ function getAll() {
103
+ return [...entries]
104
+ }
105
+
106
+ function clear() {
107
+ entries.length = 0
108
+ }
109
+
110
+ return { push, query, getAll, clear }
111
+ }
@@ -0,0 +1,148 @@
1
+ import {
2
+ createServer,
3
+ type Server,
4
+ type IncomingMessage,
5
+ type ServerResponse,
6
+ } from 'node:http'
7
+ import { log } from '../log.js'
8
+ import { getAdminHtml } from './ui.js'
9
+ import type { LogStore } from './log-store.js'
10
+ import type { HttpLogStore } from './http-proxy.js'
11
+ import type { ZeroLiteConfig } from '../config.js'
12
+
13
+ export interface AdminActions {
14
+ restartZero?: () => Promise<void>
15
+ resetZero?: () => Promise<void>
16
+ }
17
+
18
+ export interface AdminServerOpts {
19
+ port: number
20
+ logStore: LogStore
21
+ config: ZeroLiteConfig
22
+ zeroEnv: Record<string, string>
23
+ actions?: AdminActions
24
+ startTime: number
25
+ httpLog?: HttpLogStore
26
+ }
27
+
28
+ function corsHeaders(): Record<string, string> {
29
+ return {
30
+ 'Access-Control-Allow-Origin': '*',
31
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
32
+ 'Access-Control-Allow-Headers': '*',
33
+ }
34
+ }
35
+
36
+ function json(res: ServerResponse, data: unknown, status = 200) {
37
+ const headers = { ...corsHeaders(), 'Content-Type': 'application/json' }
38
+ res.writeHead(status, headers)
39
+ res.end(JSON.stringify(data))
40
+ }
41
+
42
+ export function startAdminServer(opts: AdminServerOpts): Promise<Server> {
43
+ const { logStore, config, zeroEnv, actions, startTime } = opts
44
+ const html = getAdminHtml()
45
+
46
+ const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
47
+ const headers = corsHeaders()
48
+
49
+ if (req.method === 'OPTIONS') {
50
+ res.writeHead(200, headers)
51
+ res.end()
52
+ return
53
+ }
54
+
55
+ const url = new URL(req.url || '/', 'http://localhost:' + opts.port)
56
+
57
+ try {
58
+ if (req.method === 'GET' && url.pathname === '/') {
59
+ res.writeHead(200, { ...headers, 'Content-Type': 'text/html' })
60
+ res.end(html)
61
+ return
62
+ }
63
+
64
+ if (req.method === 'GET' && url.pathname === '/api/logs') {
65
+ const source = url.searchParams.get('source') || undefined
66
+ const level = url.searchParams.get('level') || undefined
67
+ const sinceStr = url.searchParams.get('since')
68
+ const since = sinceStr ? Number(sinceStr) : undefined
69
+ json(res, logStore.query({ source, level, since }))
70
+ return
71
+ }
72
+
73
+ if (req.method === 'GET' && url.pathname === '/api/env') {
74
+ const filtered = Object.entries(zeroEnv)
75
+ .filter(([k]) => k.startsWith('ZERO_') || k === 'NODE_ENV' || k === 'NODE_OPTIONS')
76
+ .sort(([a], [b]) => a.localeCompare(b))
77
+ json(res, { env: Object.fromEntries(filtered) })
78
+ return
79
+ }
80
+
81
+ if (req.method === 'GET' && url.pathname === '/api/status') {
82
+ json(res, {
83
+ pgPort: config.pgPort,
84
+ zeroPort: config.zeroPort,
85
+ adminPort: opts.port,
86
+ uptime: Math.floor((Date.now() - startTime) / 1000),
87
+ logLevel: config.logLevel,
88
+ skipZeroCache: config.skipZeroCache,
89
+ })
90
+ return
91
+ }
92
+
93
+ if (req.method === 'POST' && url.pathname === '/api/actions/restart-zero') {
94
+ if (!actions?.restartZero) {
95
+ json(res, { ok: false, message: 'zero-cache not running' }, 400)
96
+ return
97
+ }
98
+ log.orez('admin: restarting zero-cache')
99
+ await actions.restartZero()
100
+ json(res, { ok: true, message: 'zero-cache restarted' })
101
+ return
102
+ }
103
+
104
+ if (req.method === 'POST' && url.pathname === '/api/actions/reset-zero') {
105
+ if (!actions?.resetZero) {
106
+ json(res, { ok: false, message: 'zero-cache not running' }, 400)
107
+ return
108
+ }
109
+ log.orez('admin: resetting zero-cache')
110
+ await actions.resetZero()
111
+ json(res, { ok: true, message: 'zero-cache reset and restarted' })
112
+ return
113
+ }
114
+
115
+ if (req.method === 'POST' && url.pathname === '/api/actions/clear-logs') {
116
+ logStore.clear()
117
+ json(res, { ok: true, message: 'logs cleared' })
118
+ return
119
+ }
120
+
121
+ if (req.method === 'GET' && url.pathname === '/api/http-log') {
122
+ const sinceStr = url.searchParams.get('since')
123
+ const path = url.searchParams.get('path') || undefined
124
+ const since = sinceStr ? Number(sinceStr) : undefined
125
+ json(res, opts.httpLog?.query({ since, path }) || { entries: [], cursor: 0 })
126
+ return
127
+ }
128
+
129
+ if (req.method === 'POST' && url.pathname === '/api/actions/clear-http') {
130
+ opts.httpLog?.clear()
131
+ json(res, { ok: true, message: 'http log cleared' })
132
+ return
133
+ }
134
+
135
+ res.writeHead(404, headers)
136
+ res.end('not found')
137
+ } catch (err: any) {
138
+ json(res, { error: err?.message ?? 'internal error' }, 500)
139
+ }
140
+ })
141
+
142
+ return new Promise((resolve, reject) => {
143
+ server.listen(opts.port, '127.0.0.1', () => {
144
+ resolve(server)
145
+ })
146
+ server.on('error', reject)
147
+ })
148
+ }