@solidrt/cli 0.0.25 → 0.0.27
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/package.json +13 -8
- package/scaffold/AGENTS.md +60 -0
- package/scaffold/package.json +5 -5
- package/{src → server}/cache.ts +23 -51
- package/server/control.ts +187 -0
- package/server/main.ts +301 -0
- package/server/proxy.ts +72 -0
- package/server/qr.ts +35 -0
- package/server/rebuild.ts +54 -0
- package/server/remap.ts +47 -0
- package/server/state.ts +62 -0
- package/server/tsconfig.json +11 -0
- package/server/tunnel.ts +53 -0
- package/src/args.ts +2 -0
- package/src/bundle-cli.ts +12 -0
- package/src/bundler.ts +69 -23
- package/src/commands/bundle.ts +4 -7
- package/src/commands/mcp.ts +151 -43
- package/src/commands/server.ts +16 -20
- package/src/dev-client.ts +10 -21
- package/src/dev-server.ts +153 -300
- package/src/repl.ts +67 -45
- package/src/util.ts +30 -16
- package/src/watcher.ts +6 -8
- package/src/control.ts +0 -119
package/src/repl.ts
CHANGED
|
@@ -2,28 +2,41 @@ import { createInterface } from "node:readline"
|
|
|
2
2
|
import { resolve, dirname } from "path"
|
|
3
3
|
import { readdirSync } from "node:fs"
|
|
4
4
|
import { state, print, printErr, shutdown } from "./util"
|
|
5
|
-
import { buildReload,
|
|
6
|
-
import { bundle
|
|
5
|
+
import { buildReload, getClients, sendReload, sendStop, sendStats, showBuildFailure } from "./dev-server"
|
|
6
|
+
import { bundle } from "./bundler"
|
|
7
7
|
import { startWatcher, stopWatcher } from "./watcher"
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// Resolve repl client indexes ("0 2") against the server's client list,
|
|
10
|
+
// printing a complaint for each invalid token. The list order is connect
|
|
11
|
+
// order, matching what `list` shows.
|
|
12
|
+
async function indexesToIds(args: string): Promise<number[]> {
|
|
13
|
+
let clients = await getClients()
|
|
14
|
+
let ids: number[] = []
|
|
15
|
+
for (let token of args.split(/\s+/)) {
|
|
16
|
+
let idx = parseInt(token, 10)
|
|
17
|
+
if (isNaN(idx) || idx < 0 || idx >= clients.length) {
|
|
18
|
+
print(`Invalid client index: ${token}`)
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
ids.push(clients[idx]!.id)
|
|
22
|
+
}
|
|
23
|
+
return ids
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function cmdStop(args: string) {
|
|
10
27
|
if (!args) {
|
|
11
28
|
stopWatcher()
|
|
12
29
|
state.currentCode = null
|
|
30
|
+
state.currentMap = null
|
|
13
31
|
state.source = undefined
|
|
14
|
-
|
|
32
|
+
await sendStop()
|
|
15
33
|
print("[cli] Sent stop to all clients")
|
|
16
34
|
return
|
|
17
35
|
}
|
|
18
|
-
let
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
print(`Invalid client index: ${token}`)
|
|
23
|
-
continue
|
|
24
|
-
}
|
|
25
|
-
clientList[idx].send(JSON.stringify({ type: "stop" }))
|
|
26
|
-
print(`[cli] Sent stop to client ${idx}`)
|
|
36
|
+
let ids = await indexesToIds(args)
|
|
37
|
+
if (ids.length) {
|
|
38
|
+
await sendStop(ids)
|
|
39
|
+
print(`[cli] Sent stop to client(s) ${ids.join(", ")}`)
|
|
27
40
|
}
|
|
28
41
|
}
|
|
29
42
|
|
|
@@ -32,30 +45,26 @@ async function cmdReload(args: string) {
|
|
|
32
45
|
let result = await bundle(state.source)
|
|
33
46
|
if (!result) {
|
|
34
47
|
printErr("[cli] Build failed, reload aborted")
|
|
35
|
-
showBuildFailure()
|
|
48
|
+
await showBuildFailure()
|
|
36
49
|
return
|
|
37
50
|
}
|
|
38
|
-
state.currentCode =
|
|
51
|
+
state.currentCode = result.code
|
|
52
|
+
state.currentMap = result.map
|
|
39
53
|
}
|
|
40
54
|
let msg = buildReload({ code: state.currentCode })
|
|
41
55
|
if (!args) {
|
|
42
|
-
|
|
56
|
+
await sendReload(msg, { latch: true, map: state.currentMap })
|
|
43
57
|
print("[cli] Sent reload to all clients")
|
|
44
58
|
return
|
|
45
59
|
}
|
|
46
|
-
let
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
print(`Invalid client index: ${token}`)
|
|
51
|
-
continue
|
|
52
|
-
}
|
|
53
|
-
clientList[idx].send(msg)
|
|
54
|
-
print(`[cli] Sent reload to client ${idx}`)
|
|
60
|
+
let ids = await indexesToIds(args)
|
|
61
|
+
if (ids.length) {
|
|
62
|
+
await sendReload(msg, { clients: ids, map: state.currentMap })
|
|
63
|
+
print(`[cli] Sent reload to client(s) ${ids.join(", ")}`)
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
function cmdStats(args: string) {
|
|
67
|
+
async function cmdStats(args: string) {
|
|
59
68
|
if (args === "on") {
|
|
60
69
|
state.stats = true
|
|
61
70
|
} else if (args === "off") {
|
|
@@ -66,19 +75,20 @@ function cmdStats(args: string) {
|
|
|
66
75
|
print("Usage: stats [on|off]")
|
|
67
76
|
return
|
|
68
77
|
}
|
|
69
|
-
|
|
78
|
+
await sendStats(state.stats)
|
|
70
79
|
print(`[cli] Stats overlay ${state.stats ? "on" : "off"}`)
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
function cmdList() {
|
|
74
|
-
|
|
82
|
+
async function cmdList() {
|
|
83
|
+
let clients = await getClients()
|
|
84
|
+
if (clients.length === 0) {
|
|
75
85
|
print("No connected clients")
|
|
76
86
|
return
|
|
77
87
|
}
|
|
78
|
-
print(`${
|
|
88
|
+
print(`${clients.length} connected client(s):`)
|
|
79
89
|
let i = 0
|
|
80
|
-
for (let
|
|
81
|
-
print(` ${i++}: ${
|
|
90
|
+
for (let c of clients) {
|
|
91
|
+
print(` ${i++}: ${c.address ?? "unknown"} [${c.platform}, ${c.version}]`)
|
|
82
92
|
}
|
|
83
93
|
}
|
|
84
94
|
|
|
@@ -94,13 +104,15 @@ async function cmdLoad(file: string) {
|
|
|
94
104
|
printErr("[cli] Build failed")
|
|
95
105
|
return
|
|
96
106
|
}
|
|
97
|
-
state.currentCode =
|
|
107
|
+
state.currentCode = result.code
|
|
108
|
+
state.currentMap = result.map
|
|
98
109
|
} else if (file.endsWith(".srt.js")) {
|
|
99
110
|
state.currentCode = await Bun.file(path).text()
|
|
111
|
+
state.currentMap = null
|
|
100
112
|
} else if (file.endsWith(".srt.bin")) {
|
|
101
113
|
let bytes = await Bun.file(path).arrayBuffer()
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
// One-shot: bytecode loads are pushed but not latched for late joiners.
|
|
115
|
+
await sendReload(buildReload({ bytecode: Buffer.from(bytes).toString("base64") }))
|
|
104
116
|
print(`[cli] Loaded ${file} (bytecode, ${bytes.byteLength} bytes)`)
|
|
105
117
|
return
|
|
106
118
|
} else {
|
|
@@ -110,10 +122,14 @@ async function cmdLoad(file: string) {
|
|
|
110
122
|
state.source = path
|
|
111
123
|
state.sourceDir = dirname(path)
|
|
112
124
|
startWatcher()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
// The load also moves the server's file-serving root to the new source dir,
|
|
126
|
+
// and its rebuild entry to the new file (for a later MCP reload).
|
|
127
|
+
await sendReload(buildReload({ code: state.currentCode }), {
|
|
128
|
+
latch: true,
|
|
129
|
+
sourceDir: state.sourceDir,
|
|
130
|
+
entry: file.endsWith(".tsx") ? path : undefined,
|
|
131
|
+
map: state.currentMap,
|
|
132
|
+
})
|
|
117
133
|
print(`[cli] Loaded ${file}`)
|
|
118
134
|
}
|
|
119
135
|
|
|
@@ -146,6 +162,12 @@ function completer(line: string): [string[], string] {
|
|
|
146
162
|
return [matches, line]
|
|
147
163
|
}
|
|
148
164
|
|
|
165
|
+
// Run a repl command, reporting a failed server round-trip instead of leaving
|
|
166
|
+
// an unhandled rejection (e.g. the server process died mid-command).
|
|
167
|
+
function guard(p: Promise<void>) {
|
|
168
|
+
p.catch((e) => printErr(`[cli] ${String(e)}`))
|
|
169
|
+
}
|
|
170
|
+
|
|
149
171
|
export function startRepl() {
|
|
150
172
|
state.rl = createInterface({ input: process.stdin, output: process.stdout, completer })
|
|
151
173
|
state.rl.setPrompt("srt> ")
|
|
@@ -155,15 +177,15 @@ export function startRepl() {
|
|
|
155
177
|
state.rl.on("line", (line) => {
|
|
156
178
|
let cmd = line.trim()
|
|
157
179
|
if (cmd === "stop" || cmd.startsWith("stop ")) {
|
|
158
|
-
cmdStop(cmd.slice(5).trim())
|
|
180
|
+
guard(cmdStop(cmd.slice(5).trim()))
|
|
159
181
|
} else if (cmd === "reload" || cmd.startsWith("reload ")) {
|
|
160
|
-
cmdReload(cmd.slice(7).trim())
|
|
182
|
+
guard(cmdReload(cmd.slice(7).trim()))
|
|
161
183
|
} else if (cmd.startsWith("load ")) {
|
|
162
|
-
cmdLoad(cmd.slice(5).trim())
|
|
184
|
+
guard(cmdLoad(cmd.slice(5).trim()))
|
|
163
185
|
} else if (cmd === "list") {
|
|
164
|
-
cmdList()
|
|
186
|
+
guard(cmdList())
|
|
165
187
|
} else if (cmd === "stats" || cmd.startsWith("stats ")) {
|
|
166
|
-
cmdStats(cmd.slice(6).trim())
|
|
188
|
+
guard(cmdStats(cmd.slice(6).trim()))
|
|
167
189
|
} else if (cmd === "quit" || cmd === "exit") {
|
|
168
190
|
shutdown()
|
|
169
191
|
} else if (cmd.startsWith("!")) {
|
|
@@ -187,4 +209,4 @@ export function startRepl() {
|
|
|
187
209
|
})
|
|
188
210
|
|
|
189
211
|
state.rl.prompt()
|
|
190
|
-
}
|
|
212
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -2,31 +2,29 @@ import { resolveBinary } from "./artifacts"
|
|
|
2
2
|
import { existsSync } from "node:fs"
|
|
3
3
|
import { resolve } from "node:path"
|
|
4
4
|
import type { Interface as ReadlineInterface } from "node:readline"
|
|
5
|
-
import type {
|
|
6
|
-
import type { Bonjour } from "bonjour-service"
|
|
5
|
+
// import type { Bonjour } from "bonjour-service" (mDNS advertise, kept - see dev-server.ts)
|
|
7
6
|
|
|
8
7
|
export let state = {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// What srt believes the current bundle is; the server process keeps its own
|
|
9
|
+
// latched copy for late-joining clients (see packages/cli/server/).
|
|
11
10
|
currentCode: null as string | null,
|
|
11
|
+
// The bundle's composed sourcemap (dev builds), sent to the server alongside
|
|
12
|
+
// reloads so it can remap logged stack traces to .tsx positions.
|
|
13
|
+
currentMap: null as string | null,
|
|
12
14
|
source: undefined as string | undefined,
|
|
13
15
|
sourceDir: process.cwd(),
|
|
14
16
|
child: null as ReturnType<typeof Bun.spawn> | null,
|
|
15
|
-
server
|
|
17
|
+
// The spawned flux dev-server process (see dev-server.ts startServer).
|
|
18
|
+
serverProc: null as ReturnType<typeof Bun.spawn> | null,
|
|
19
|
+
shuttingDown: false,
|
|
16
20
|
serverUrl: null as string | null,
|
|
17
21
|
rl: null as ReadlineInterface | null,
|
|
18
|
-
bonjour: null as Bonjour | null,
|
|
22
|
+
// bonjour: null as Bonjour | null, (mDNS advertise, kept - see dev-server.ts)
|
|
19
23
|
stats: false,
|
|
20
24
|
// --capture <file>: destination for captured key events, or undefined when
|
|
21
|
-
// off
|
|
22
|
-
//
|
|
23
|
-
// several connected clients merge into one coherent timeline, tagged by
|
|
24
|
-
// `device` (see dev-server.ts) so they can be told apart. Streamed to disk
|
|
25
|
-
// as JSON Lines (one event object per line) as each arrives - see
|
|
26
|
-
// dev-server.ts's "capture" message handling.
|
|
25
|
+
// off; the server process owns the capture file and clock (see
|
|
26
|
+
// packages/cli/server/main.ts's "capture" message handling).
|
|
27
27
|
capture: undefined as string | undefined,
|
|
28
|
-
captureStartMs: 0,
|
|
29
|
-
captureLastAt: 0, // ms, same clock as captureStartMs
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
// Build target per binary, for the "not found" hint. Run from the repo root.
|
|
@@ -93,9 +91,25 @@ export function printErr(...args: any[]) {
|
|
|
93
91
|
state.rl?.prompt(true)
|
|
94
92
|
}
|
|
95
93
|
|
|
94
|
+
// Pipe a child stream to `out` without mangling the repl prompt: clear the
|
|
95
|
+
// prompt line, write the chunk, redraw the prompt.
|
|
96
|
+
export function pipeAbovePrompt(stream: ReadableStream<Uint8Array>, out: NodeJS.WriteStream) {
|
|
97
|
+
let reader = stream.getReader()
|
|
98
|
+
;(async () => {
|
|
99
|
+
while (true) {
|
|
100
|
+
let { done, value } = await reader.read()
|
|
101
|
+
if (done || !value) break
|
|
102
|
+
process.stdout.write("\r\x1b[K")
|
|
103
|
+
out.write(value)
|
|
104
|
+
state.rl?.prompt(true)
|
|
105
|
+
}
|
|
106
|
+
})()
|
|
107
|
+
}
|
|
108
|
+
|
|
96
109
|
export function shutdown() {
|
|
110
|
+
state.shuttingDown = true
|
|
97
111
|
if (state.child) state.child.kill()
|
|
98
|
-
if (state.
|
|
99
|
-
if (state.bonjour) state.bonjour.destroy()
|
|
112
|
+
if (state.serverProc) state.serverProc.kill()
|
|
113
|
+
// if (state.bonjour) state.bonjour.destroy() (mDNS advertise, kept - see dev-server.ts)
|
|
100
114
|
process.exit(0)
|
|
101
115
|
}
|
package/src/watcher.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { watch } from "node:fs"
|
|
2
2
|
import { resolve, dirname } from "path"
|
|
3
3
|
import { state, print, printErr } from "./util"
|
|
4
|
-
import { buildReload, showBuildFailure } from "./dev-server"
|
|
5
|
-
import { bundle
|
|
4
|
+
import { buildReload, sendReload, showBuildFailure } from "./dev-server"
|
|
5
|
+
import { bundle } from "./bundler"
|
|
6
6
|
|
|
7
7
|
let currentWatcher: ReturnType<typeof watch> | null = null
|
|
8
8
|
|
|
@@ -28,13 +28,11 @@ export function startWatcher() {
|
|
|
28
28
|
let result = await bundle(state.source)
|
|
29
29
|
if (!result) {
|
|
30
30
|
printErr("[cli] Build failed, waiting for changes...")
|
|
31
|
-
showBuildFailure()
|
|
31
|
+
await showBuildFailure()
|
|
32
32
|
return
|
|
33
33
|
}
|
|
34
|
-
state.currentCode =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
ws.send(msg)
|
|
38
|
-
}
|
|
34
|
+
state.currentCode = result.code
|
|
35
|
+
state.currentMap = result.map
|
|
36
|
+
await sendReload(buildReload({ code: state.currentCode }), { latch: true, map: state.currentMap })
|
|
39
37
|
})
|
|
40
38
|
}
|
package/src/control.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { state } from "./util"
|
|
2
|
-
|
|
3
|
-
// The control API under /__control__/: read-only introspection of connected
|
|
4
|
-
// app clients, served next to the file routes. The MCP bridge (srt mcp) is the
|
|
5
|
-
// primary consumer. Two shapes: server-held data answered directly (clients,
|
|
6
|
-
// logs) and queries forwarded to a client over its websocket and correlated
|
|
7
|
-
// back by id (tree, stats).
|
|
8
|
-
|
|
9
|
-
export type LogEntry = { seq: number; at: number; client: number; level: string; text: string }
|
|
10
|
-
|
|
11
|
-
// Ring buffer of forwarded client logs. Capped so a chatty app cannot grow the
|
|
12
|
-
// server without bound; readers page through it with the `since` cursor.
|
|
13
|
-
const LOG_CAP = 2000
|
|
14
|
-
const QUERY_TIMEOUT_MS = 5000
|
|
15
|
-
const MAX_WAIT_MS = 30000
|
|
16
|
-
|
|
17
|
-
let logs: LogEntry[] = []
|
|
18
|
-
let logSeq = 0
|
|
19
|
-
// Pending long-poll wakeups (see handleLogs). Flushed on every append; a
|
|
20
|
-
// waiter that already timed out resolves again harmlessly.
|
|
21
|
-
let logWaiters: Array<() => void> = []
|
|
22
|
-
|
|
23
|
-
let nextQueryId = 1
|
|
24
|
-
let pendingQueries = new Map<number, (msg: any) => void>()
|
|
25
|
-
|
|
26
|
-
/// A `log` message arrived from a client: buffer it and wake long-polls.
|
|
27
|
-
export function appendLog(client: number, level: string, text: string) {
|
|
28
|
-
logs.push({ seq: ++logSeq, at: Date.now(), client, level, text })
|
|
29
|
-
if (logs.length > LOG_CAP) logs.splice(0, logs.length - LOG_CAP)
|
|
30
|
-
let waiters = logWaiters
|
|
31
|
-
logWaiters = []
|
|
32
|
-
for (let wake of waiters) wake()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/// A `result` message arrived from a client: hand it to the awaiting query.
|
|
36
|
-
export function resolveQuery(msg: { id?: number }) {
|
|
37
|
-
if (typeof msg.id !== "number") return
|
|
38
|
-
let resolve = pendingQueries.get(msg.id)
|
|
39
|
-
if (resolve) {
|
|
40
|
-
pendingQueries.delete(msg.id)
|
|
41
|
-
resolve(msg)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function clientList() {
|
|
46
|
-
return [...state.clients.values()].map((info) => ({
|
|
47
|
-
id: info.id,
|
|
48
|
-
platform: info.platform,
|
|
49
|
-
version: info.version,
|
|
50
|
-
capabilities: info.capabilities,
|
|
51
|
-
}))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Resolve the target client for a query: an explicit ?client=<id>, or the only
|
|
55
|
-
// connected client when the parameter is omitted.
|
|
56
|
-
function findClient(param: string | null): { ws: any } | { error: Response } {
|
|
57
|
-
let entries = [...state.clients.entries()]
|
|
58
|
-
if (param === null) {
|
|
59
|
-
if (entries.length === 1) return { ws: entries[0]![0] }
|
|
60
|
-
if (entries.length === 0) return { error: Response.json({ error: "No connected clients" }, { status: 503 }) }
|
|
61
|
-
return { error: Response.json({ error: "Multiple clients connected; pass ?client=<id>" }, { status: 400 }) }
|
|
62
|
-
}
|
|
63
|
-
let id = parseInt(param, 10)
|
|
64
|
-
let entry = entries.find(([, info]) => info.id === id)
|
|
65
|
-
if (!entry) return { error: Response.json({ error: `No client with id ${param}` }, { status: 404 }) }
|
|
66
|
-
return { ws: entry[0] }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function handleQuery(url: URL, kind: string): Promise<Response> {
|
|
70
|
-
let target = findClient(url.searchParams.get("client"))
|
|
71
|
-
if ("error" in target) return target.error
|
|
72
|
-
let id = nextQueryId++
|
|
73
|
-
let reply = new Promise<any>((resolve) => {
|
|
74
|
-
pendingQueries.set(id, resolve)
|
|
75
|
-
})
|
|
76
|
-
target.ws.send(JSON.stringify({ type: "query", kind, id }))
|
|
77
|
-
let msg = await Promise.race([reply, Bun.sleep(QUERY_TIMEOUT_MS)])
|
|
78
|
-
pendingQueries.delete(id)
|
|
79
|
-
if (!msg) return Response.json({ error: "Query timed out" }, { status: 504 })
|
|
80
|
-
if (msg.error) return Response.json({ error: msg.error }, { status: 502 })
|
|
81
|
-
return Response.json(msg.data)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// GET /__control__/logs?since=N&wait=MS: entries with seq > since, plus the
|
|
85
|
-
// latest seq as the next cursor. With `wait`, holds the response until a new
|
|
86
|
-
// entry arrives or the timeout passes (long-poll), so a caller can follow the
|
|
87
|
-
// stream without tight polling.
|
|
88
|
-
async function handleLogs(url: URL): Promise<Response> {
|
|
89
|
-
let since = parseInt(url.searchParams.get("since") ?? "0", 10) || 0
|
|
90
|
-
let wait = Math.min(parseInt(url.searchParams.get("wait") ?? "0", 10) || 0, MAX_WAIT_MS)
|
|
91
|
-
let entries = logs.filter((e) => e.seq > since)
|
|
92
|
-
if (entries.length === 0 && wait > 0) {
|
|
93
|
-
await new Promise<void>((resolve) => {
|
|
94
|
-
let timer = setTimeout(resolve, wait)
|
|
95
|
-
logWaiters.push(() => {
|
|
96
|
-
clearTimeout(timer)
|
|
97
|
-
resolve()
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
entries = logs.filter((e) => e.seq > since)
|
|
101
|
-
}
|
|
102
|
-
return Response.json({ entries, latest: logSeq })
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export async function handleControl(req: Request, path: string): Promise<Response> {
|
|
106
|
-
let url = new URL(req.url)
|
|
107
|
-
switch (path) {
|
|
108
|
-
case "/__control__/clients":
|
|
109
|
-
return Response.json(clientList())
|
|
110
|
-
case "/__control__/logs":
|
|
111
|
-
return handleLogs(url)
|
|
112
|
-
case "/__control__/tree":
|
|
113
|
-
return handleQuery(url, "tree")
|
|
114
|
-
case "/__control__/stats":
|
|
115
|
-
return handleQuery(url, "stats")
|
|
116
|
-
default:
|
|
117
|
-
return Response.json({ error: "Unknown control endpoint" }, { status: 404 })
|
|
118
|
-
}
|
|
119
|
-
}
|