@anna-ai/cli 0.1.4 → 0.1.11

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.
@@ -0,0 +1,678 @@
1
+ import { canonicalHost, getAccount } from "./credentials-ggdaz_-7.js";
2
+ import { dirname, join, normalize, resolve } from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { createReadStream, existsSync, readFileSync, statSync, watch } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { readFile } from "node:fs/promises";
7
+ import { createServer } from "node:http";
8
+ import { WebSocketServer } from "ws";
9
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
10
+
11
+ //#region src/harness/llm-bridge.ts
12
+ var LlmBridge = class {
13
+ mintedAuto = new Map();
14
+ mintedAgent = new Map();
15
+ mocks = [];
16
+ streamCounter = 0;
17
+ constructor(opts) {
18
+ this.opts = opts;
19
+ if (opts.mode === "mock" && opts.mockFile) {
20
+ const path = resolve(opts.mockFile);
21
+ if (existsSync(path)) {
22
+ const lines = readFileSync(path, "utf8").split(/\r?\n/);
23
+ for (const line of lines) {
24
+ if (!line.trim() || line.startsWith("#")) continue;
25
+ try {
26
+ this.mocks.push(JSON.parse(line));
27
+ } catch {}
28
+ }
29
+ }
30
+ }
31
+ }
32
+ /** Returns true iff this bridge handles `(ns, method)` (i.e. the harness
33
+ * should NOT forward it to the in-process Python dispatcher). */
34
+ static handles(ns, method) {
35
+ if (ns === "llm" && method === "complete") return true;
36
+ if (ns === "agent" && method.startsWith("session.")) return true;
37
+ return false;
38
+ }
39
+ /** Resolve the active account or throw with a friendly message. */
40
+ account() {
41
+ const acc = getAccount(this.opts.account);
42
+ if (!acc) throw new Error("no PAT on disk — run `anna-app login --host <nexus-url>` first (or use `--no-llm` / `--mock-llm <fixture>` to develop offline)");
43
+ if (acc.expires_at && acc.expires_at < Math.floor(Date.now() / 1e3)) throw new Error("PAT expired — run `anna-app login` again");
44
+ return acc;
45
+ }
46
+ /** Mint (or reuse) an `app_session_token` for kind=complete (per window). */
47
+ async mintComplete(windowUuid) {
48
+ const cached = this.mintedAuto.get(windowUuid);
49
+ if (cached && cached.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return cached;
50
+ const acc = this.account();
51
+ const body = {
52
+ pat: acc.pat,
53
+ kind: "complete",
54
+ app_id: this.opts.appId ?? null
55
+ };
56
+ if (!body.app_id) body.app_id = 1;
57
+ const minted = await this.callMint(acc.host, body);
58
+ const ms = {
59
+ appSessionToken: minted.app_session_token,
60
+ appSessionUuid: minted.app_session_uuid,
61
+ expiresAt: Math.floor(Date.now() / 1e3) + (minted.expires_in || 600),
62
+ isAgent: false,
63
+ submode: null
64
+ };
65
+ this.mintedAuto.set(windowUuid, ms);
66
+ return ms;
67
+ }
68
+ /** Mint a kind=agent session — called from `agent.session.create`. */
69
+ async mintAgent(args) {
70
+ const acc = this.account();
71
+ const submode = args.submode ?? "auto";
72
+ const body = {
73
+ pat: acc.pat,
74
+ kind: "agent",
75
+ submode,
76
+ fixed_client_id: args.fixed_client_id ?? args.fixedClientId ?? null,
77
+ app_id: this.opts.appId ?? 1,
78
+ label: args.label ?? "anna-app dev",
79
+ quota_caps: args.quotaCaps ?? null
80
+ };
81
+ const minted = await this.callMint(acc.host, body);
82
+ const ms = {
83
+ appSessionToken: minted.app_session_token,
84
+ appSessionUuid: minted.app_session_uuid,
85
+ expiresAt: Math.floor(Date.now() / 1e3) + (minted.expires_in || 600),
86
+ isAgent: true,
87
+ submode
88
+ };
89
+ this.mintedAgent.set(ms.appSessionUuid, ms);
90
+ return ms;
91
+ }
92
+ async callMint(host, body) {
93
+ const url = `${canonicalHost(host)}/api/v1/anna-apps/dev/session/mint`;
94
+ const res = await fetch(url, {
95
+ method: "POST",
96
+ headers: { "content-type": "application/json" },
97
+ body: JSON.stringify(body)
98
+ });
99
+ if (!res.ok) {
100
+ const text = await res.text().catch(() => "");
101
+ throw new Error(`session.mint failed: HTTP ${res.status} ${text}`);
102
+ }
103
+ return await res.json();
104
+ }
105
+ /** Public entry — invoked by harness `proxyCall` for llm.* / agent.*. */
106
+ async dispatch(args) {
107
+ if (this.opts.mode === "off") return {
108
+ ok: false,
109
+ error: {
110
+ code: "llm_disabled",
111
+ message: "harness started with --no-llm"
112
+ }
113
+ };
114
+ if (this.opts.mode === "mock") return this.dispatchMock(args);
115
+ try {
116
+ return await this.dispatchReal(args);
117
+ } catch (e) {
118
+ return {
119
+ ok: false,
120
+ error: {
121
+ code: "transport",
122
+ message: e.message
123
+ }
124
+ };
125
+ }
126
+ }
127
+ async dispatchMock(args) {
128
+ const content = String(args.args?.content ?? args.args?.messages ?? "");
129
+ const entry = this.mocks.find((m) => m.ns === args.ns && m.method === args.method && (!m.match?.contentIncludes || content.includes(m.match.contentIncludes))) ?? this.mocks.find((m) => m.ns === args.ns && m.method === args.method);
130
+ if (!entry) return {
131
+ ok: true,
132
+ result: args.ns === "llm" ? {
133
+ role: "assistant",
134
+ content: {
135
+ type: "text",
136
+ text: "(mock) no fixture matched"
137
+ },
138
+ model: "mock-model",
139
+ stopReason: "endTurn"
140
+ } : {
141
+ app_session_uuid: "aps_mock",
142
+ expires_in: 600,
143
+ submode: "auto",
144
+ granted_tools: []
145
+ }
146
+ };
147
+ if (entry.events && entry.events.length > 0) {
148
+ const sid = `mock-${++this.streamCounter}`;
149
+ (async () => {
150
+ let seq = 0;
151
+ for (const ev of entry.events ?? []) {
152
+ if (ev.delay_ms) await setTimeout$1(ev.delay_ms);
153
+ seq += 1;
154
+ args.onEvent("rpc.stream", {
155
+ stream_id: sid,
156
+ seq,
157
+ payload: ev.payload,
158
+ done: false
159
+ });
160
+ }
161
+ args.onEvent("rpc.stream", {
162
+ stream_id: sid,
163
+ seq: seq + 1,
164
+ payload: { event: "end" },
165
+ done: true
166
+ });
167
+ })();
168
+ return {
169
+ ok: true,
170
+ result: {
171
+ stream_id: sid,
172
+ run_id: "mock-run"
173
+ }
174
+ };
175
+ }
176
+ return {
177
+ ok: true,
178
+ result: entry.result ?? {}
179
+ };
180
+ }
181
+ async dispatchReal(args) {
182
+ const acc = this.account();
183
+ if (args.ns === "llm" && args.method === "complete") {
184
+ const ms = await this.mintComplete(args.windowUuid);
185
+ const result = await this.postJson(`${canonicalHost(acc.host)}/api/v1/copilot/app/complete`, ms.appSessionToken, args.args);
186
+ return {
187
+ ok: true,
188
+ result
189
+ };
190
+ }
191
+ if (args.ns === "agent") switch (args.method) {
192
+ case "session.create": {
193
+ const ms = await this.mintAgent(args.args);
194
+ return {
195
+ ok: true,
196
+ result: {
197
+ app_session_uuid: ms.appSessionUuid,
198
+ expires_in: Math.max(0, ms.expiresAt - Math.floor(Date.now() / 1e3)),
199
+ submode: ms.submode,
200
+ fixed_client_id: args.args.fixed_client_id ?? null,
201
+ granted_tools: []
202
+ }
203
+ };
204
+ }
205
+ case "session.run": {
206
+ const apsUuid = String(args.args.app_session_uuid ?? "");
207
+ const ms = this.mintedAgent.get(apsUuid);
208
+ if (!ms) return {
209
+ ok: false,
210
+ error: {
211
+ code: "session_expired",
212
+ message: "no cached session"
213
+ }
214
+ };
215
+ const sid = `dev-${++this.streamCounter}`;
216
+ this.pumpAgentRun({
217
+ host: acc.host,
218
+ token: ms.appSessionToken,
219
+ body: {
220
+ ...args.args,
221
+ stream: true
222
+ },
223
+ streamId: sid,
224
+ windowUuid: args.windowUuid,
225
+ onEvent: args.onEvent
226
+ });
227
+ return {
228
+ ok: true,
229
+ result: {
230
+ stream_id: sid,
231
+ run_id: String(args.args.run_id ?? sid)
232
+ }
233
+ };
234
+ }
235
+ case "session.cancel": {
236
+ const apsUuid = String(args.args.app_session_uuid ?? "");
237
+ const ms = this.mintedAgent.get(apsUuid);
238
+ if (!ms) return {
239
+ ok: false,
240
+ error: {
241
+ code: "session_expired",
242
+ message: "no cached session"
243
+ }
244
+ };
245
+ const out = await this.postJson(`${canonicalHost(acc.host)}/api/v1/copilot/app/agent/cancel`, ms.appSessionToken, args.args);
246
+ return {
247
+ ok: true,
248
+ result: out
249
+ };
250
+ }
251
+ case "session.history": return {
252
+ ok: false,
253
+ error: {
254
+ code: "not_supported",
255
+ message: "agent.session.history is not exposed over HTTP; available only via in-process store"
256
+ }
257
+ };
258
+ case "session.delete": {
259
+ const apsUuid = String(args.args.app_session_uuid ?? "");
260
+ const ms = this.mintedAgent.get(apsUuid);
261
+ this.mintedAgent.delete(apsUuid);
262
+ if (!ms) return {
263
+ ok: true,
264
+ result: { deleted: true }
265
+ };
266
+ const url = `${canonicalHost(acc.host)}/api/v1/copilot/app/sessions/${encodeURIComponent(apsUuid)}`;
267
+ const res = await fetch(url, {
268
+ method: "DELETE",
269
+ headers: { authorization: `Bearer ${ms.appSessionToken}` }
270
+ });
271
+ if (!res.ok) {
272
+ const text = await res.text().catch(() => "");
273
+ return {
274
+ ok: false,
275
+ error: {
276
+ code: "transport",
277
+ message: `HTTP ${res.status}: ${text}`
278
+ }
279
+ };
280
+ }
281
+ return {
282
+ ok: true,
283
+ result: { deleted: true }
284
+ };
285
+ }
286
+ }
287
+ return {
288
+ ok: false,
289
+ error: {
290
+ code: "unknown_method",
291
+ message: `${args.ns}.${args.method}`
292
+ }
293
+ };
294
+ }
295
+ async postJson(url, token, body) {
296
+ const res = await fetch(url, {
297
+ method: "POST",
298
+ headers: {
299
+ "content-type": "application/json",
300
+ authorization: `Bearer ${token}`
301
+ },
302
+ body: JSON.stringify(body)
303
+ });
304
+ if (!res.ok) {
305
+ const text = await res.text().catch(() => "");
306
+ throw new Error(`HTTP ${res.status}: ${text}`);
307
+ }
308
+ return res.json();
309
+ }
310
+ /** Consume an SSE response and forward each frame as `rpc.stream`. */
311
+ async pumpAgentRun(args) {
312
+ const url = `${canonicalHost(args.host)}/api/v1/copilot/app/agent`;
313
+ let seq = 0;
314
+ const emit = (payload, done) => {
315
+ seq += 1;
316
+ args.onEvent("rpc.stream", {
317
+ stream_id: args.streamId,
318
+ window_uuid: args.windowUuid,
319
+ seq,
320
+ payload,
321
+ done
322
+ });
323
+ };
324
+ try {
325
+ const res = await fetch(url, {
326
+ method: "POST",
327
+ headers: {
328
+ "content-type": "application/json",
329
+ authorization: `Bearer ${args.token}`,
330
+ accept: "text/event-stream"
331
+ },
332
+ body: JSON.stringify(args.body)
333
+ });
334
+ if (!res.ok || !res.body) {
335
+ const text = await res.text().catch(() => "");
336
+ emit({
337
+ event: "error",
338
+ code: "http",
339
+ message: `HTTP ${res.status}: ${text}`
340
+ }, true);
341
+ return;
342
+ }
343
+ const reader = res.body.getReader();
344
+ const decoder = new TextDecoder();
345
+ let buf = "";
346
+ while (true) {
347
+ const { value, done } = await reader.read();
348
+ if (done) break;
349
+ buf += decoder.decode(value, { stream: true });
350
+ let idx;
351
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
352
+ const frame = buf.slice(0, idx);
353
+ buf = buf.slice(idx + 2);
354
+ const payload = parseSseFrame(frame);
355
+ if (payload != null) emit(payload, false);
356
+ }
357
+ }
358
+ if (buf.trim().length > 0) {
359
+ const payload = parseSseFrame(buf);
360
+ if (payload != null) emit(payload, false);
361
+ }
362
+ emit({ event: "end" }, true);
363
+ } catch (e) {
364
+ emit({
365
+ event: "error",
366
+ code: "transport",
367
+ message: e.message
368
+ }, true);
369
+ }
370
+ }
371
+ };
372
+ function parseSseFrame(frame) {
373
+ const lines = frame.split(/\r?\n/);
374
+ let dataLines = [];
375
+ let eventName = null;
376
+ for (const raw of lines) {
377
+ if (!raw || raw.startsWith(":")) continue;
378
+ if (raw.startsWith("data:")) dataLines.push(raw.slice(5).trimStart());
379
+ else if (raw.startsWith("event:")) eventName = raw.slice(6).trim();
380
+ }
381
+ if (dataLines.length === 0) return null;
382
+ const data = dataLines.join("\n");
383
+ try {
384
+ const obj = JSON.parse(data);
385
+ if (eventName && obj && typeof obj === "object" && !("event" in obj)) return {
386
+ ...obj,
387
+ event: eventName
388
+ };
389
+ return obj;
390
+ } catch {
391
+ return {
392
+ event: eventName ?? "raw",
393
+ text: data
394
+ };
395
+ }
396
+ }
397
+
398
+ //#endregion
399
+ //#region src/harness/server.ts
400
+ const __filename = fileURLToPath(import.meta.url);
401
+ const __dirname = dirname(__filename);
402
+ const MIME = {
403
+ ".html": "text/html; charset=utf-8",
404
+ ".js": "application/javascript; charset=utf-8",
405
+ ".mjs": "application/javascript; charset=utf-8",
406
+ ".css": "text/css; charset=utf-8",
407
+ ".json": "application/json; charset=utf-8",
408
+ ".svg": "image/svg+xml",
409
+ ".png": "image/png",
410
+ ".jpg": "image/jpeg",
411
+ ".jpeg": "image/jpeg",
412
+ ".gif": "image/gif",
413
+ ".woff": "font/woff",
414
+ ".woff2": "font/woff2",
415
+ ".map": "application/json"
416
+ };
417
+ var HarnessServer = class {
418
+ server = createServer((req, res) => this.handle(req, res));
419
+ wss = null;
420
+ sessionId = null;
421
+ liveSockets = new Set();
422
+ watchers = [];
423
+ reloadDebounce = null;
424
+ /** Pending events queued by the LLM bridge — drained alongside Python events. */
425
+ llmEventQueue = [];
426
+ llmBridge;
427
+ constructor(cfg, bridge) {
428
+ this.cfg = cfg;
429
+ this.bridge = bridge;
430
+ this.llmBridge = cfg.llm ? new LlmBridge(cfg.llm) : null;
431
+ }
432
+ async listen() {
433
+ if (this.cfg.executas && this.cfg.executas.length > 0) await this.bridge.call("executas.register", { executas: this.cfg.executas.map((e) => ({
434
+ tool_id: e.tool_id,
435
+ project_dir: e.project_dir,
436
+ command: e.command ?? null
437
+ })) });
438
+ await new Promise((res, rej) => this.server.listen(this.cfg.port, () => res()).once("error", rej));
439
+ this.wss = new WebSocketServer({ noServer: true });
440
+ this.server.on("upgrade", (req, socket, head) => {
441
+ const url = new URL(req.url ?? "/", "http://localhost");
442
+ if (url.pathname !== "/ws") {
443
+ socket.destroy();
444
+ return;
445
+ }
446
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
447
+ const sid = url.searchParams.get("session_id");
448
+ if (!sid || sid !== this.sessionId) {
449
+ ws.close(1008, "unknown session_id");
450
+ return;
451
+ }
452
+ this.liveSockets.add(ws);
453
+ const timer = setInterval(async () => {
454
+ try {
455
+ const out = await this.bridge.call("session.drain_events", { session_id: sid });
456
+ for (const ev of out.events) ws.send(JSON.stringify({
457
+ kind: "event",
458
+ ...ev
459
+ }));
460
+ if (this.llmEventQueue.length > 0) {
461
+ const drained = this.llmEventQueue.splice(0);
462
+ for (const ev of drained) ws.send(JSON.stringify({
463
+ kind: "event",
464
+ ...ev
465
+ }));
466
+ }
467
+ } catch (e) {
468
+ ws.close(1011, `drain failed: ${e.message}`);
469
+ clearInterval(timer);
470
+ }
471
+ }, 200);
472
+ ws.on("close", () => {
473
+ this.liveSockets.delete(ws);
474
+ clearInterval(timer);
475
+ });
476
+ });
477
+ });
478
+ if (this.cfg.watch) this.startWatcher();
479
+ }
480
+ async close() {
481
+ for (const w of this.watchers) w.close();
482
+ this.watchers = [];
483
+ if (this.reloadDebounce) clearTimeout(this.reloadDebounce);
484
+ if (this.wss) {
485
+ for (const c of this.wss.clients) c.terminate();
486
+ this.wss.close();
487
+ }
488
+ await new Promise((res) => this.server.close(() => res()));
489
+ }
490
+ startWatcher() {
491
+ const broadcastReload = (path) => {
492
+ if (this.reloadDebounce) clearTimeout(this.reloadDebounce);
493
+ this.reloadDebounce = setTimeout(() => {
494
+ const env = JSON.stringify({
495
+ kind: "reload",
496
+ path
497
+ });
498
+ for (const ws of this.liveSockets) if (ws.readyState === ws.OPEN) ws.send(env);
499
+ }, 100);
500
+ };
501
+ try {
502
+ this.watchers.push(watch(this.cfg.bundleDir, { recursive: true }, (_evt, filename) => {
503
+ if (filename) broadcastReload(`bundle/${filename}`);
504
+ }));
505
+ } catch (e) {
506
+ process.stderr.write(`[harness] watcher failed to attach to ${this.cfg.bundleDir}: ${e.message}\n`);
507
+ }
508
+ }
509
+ async handle(req, res) {
510
+ try {
511
+ const url = new URL(req.url ?? "/", "http://localhost");
512
+ const method = req.method ?? "GET";
513
+ if (method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) return await this.serveDashboard(res);
514
+ if (method === "GET" && url.pathname === "/api/config") return this.json(res, 200, {
515
+ app_slug: this.cfg.slug,
516
+ view: this.cfg.view ?? null,
517
+ bundle_base: `/anna-apps/${this.cfg.slug}/dev/${this.cfg.bundleEntry}`,
518
+ executas: (this.cfg.executas ?? []).map((e) => e.tool_id),
519
+ watch: !!this.cfg.watch
520
+ });
521
+ if (method === "POST" && url.pathname === "/api/session/create") return await this.createSession(res);
522
+ if (method === "POST" && url.pathname === "/api/session/call") return await this.proxyCall(req, res);
523
+ if (method === "POST" && url.pathname === "/api/session/refresh-token") return await this.refreshToken(res);
524
+ if (method === "GET" && url.pathname.startsWith("/static/anna-apps/_sdk/")) return await this.serveSdk(url.pathname, res);
525
+ if (method === "GET" && url.pathname.startsWith(`/anna-apps/${this.cfg.slug}/dev/`)) {
526
+ const rel = url.pathname.replace(`/anna-apps/${this.cfg.slug}/dev/`, "");
527
+ return await this.serveBundleAsset(rel, res);
528
+ }
529
+ this.text(res, 404, `not found: ${url.pathname}`);
530
+ } catch (e) {
531
+ this.text(res, 500, `harness error: ${e.message}`);
532
+ }
533
+ }
534
+ async serveDashboard(res) {
535
+ const file = join(__dirname, "dashboard.html");
536
+ const html = await readFile(file, "utf-8");
537
+ res.writeHead(200, {
538
+ "content-type": MIME[".html"],
539
+ "cache-control": "no-store"
540
+ });
541
+ res.end(html);
542
+ }
543
+ async createSession(res) {
544
+ if (this.sessionId) {
545
+ try {
546
+ await this.bridge.call("session.close", { session_id: this.sessionId });
547
+ } catch {}
548
+ this.sessionId = null;
549
+ }
550
+ const out = await this.bridge.call("session.create", {
551
+ user_id: this.cfg.userId,
552
+ manifest: this.cfg.manifest,
553
+ view: this.cfg.view,
554
+ entry_payload: this.cfg.entryPayload ?? {},
555
+ app_slug: this.cfg.slug
556
+ });
557
+ this.sessionId = out.session_id;
558
+ this.json(res, 200, out);
559
+ }
560
+ async proxyCall(req, res) {
561
+ const body = await readBody(req);
562
+ let parsed;
563
+ try {
564
+ parsed = JSON.parse(body);
565
+ } catch {
566
+ return this.json(res, 400, {
567
+ ok: false,
568
+ error: {
569
+ code: "bad_request",
570
+ message: "invalid json body"
571
+ }
572
+ });
573
+ }
574
+ if (this.llmBridge != null && LlmBridge.handles(parsed.ns, parsed.method)) {
575
+ const out = await this.llmBridge.dispatch({
576
+ windowUuid: this.sessionId ?? "harness",
577
+ ns: parsed.ns,
578
+ method: parsed.method,
579
+ args: parsed.args ?? {},
580
+ onEvent: (kind, payload) => {
581
+ this.llmEventQueue.push({
582
+ event: kind,
583
+ payload
584
+ });
585
+ }
586
+ });
587
+ this.json(res, 200, out);
588
+ return;
589
+ }
590
+ try {
591
+ const out = await this.bridge.call("session.call", {
592
+ session_id: parsed.session_id,
593
+ ns: parsed.ns,
594
+ method: parsed.method,
595
+ args: parsed.args ?? {}
596
+ });
597
+ this.json(res, 200, out);
598
+ } catch (e) {
599
+ this.json(res, 200, {
600
+ ok: false,
601
+ error: {
602
+ code: "transport",
603
+ message: e.message
604
+ }
605
+ });
606
+ }
607
+ }
608
+ async refreshToken(res) {
609
+ if (!this.sessionId) return this.json(res, 400, { error: "no active session" });
610
+ try {
611
+ const out = await this.bridge.call("session.refresh_token", { session_id: this.sessionId });
612
+ this.json(res, 200, out);
613
+ } catch (e) {
614
+ this.json(res, 500, { error: e.message });
615
+ }
616
+ }
617
+ async serveSdk(pathname, res) {
618
+ const sdkRel = pathname.replace(/^\/static\/anna-apps\/_sdk\/[^/]+\//, "");
619
+ let distRoot;
620
+ try {
621
+ const req = createRequire(import.meta.url);
622
+ distRoot = dirname(req.resolve("@anna-ai/app-runtime"));
623
+ } catch (e) {
624
+ return this.text(res, 500, `@anna-ai/app-runtime is not installed: ${e.message}`);
625
+ }
626
+ const abs = resolve(distRoot, sdkRel);
627
+ if (!abs.startsWith(distRoot)) return this.text(res, 403, "forbidden");
628
+ return this.serveFile(abs, res);
629
+ }
630
+ async serveBundleAsset(rel, res) {
631
+ const abs = resolve(this.cfg.bundleDir, normalize(rel));
632
+ if (!abs.startsWith(resolve(this.cfg.bundleDir))) return this.text(res, 403, "forbidden");
633
+ return this.serveFile(abs, res);
634
+ }
635
+ async serveFile(abs, res) {
636
+ let stat;
637
+ try {
638
+ stat = statSync(abs);
639
+ } catch {
640
+ return this.text(res, 404, `not found: ${abs}`);
641
+ }
642
+ if (!stat.isFile()) return this.text(res, 404, "not a file");
643
+ const ext = abs.slice(abs.lastIndexOf("."));
644
+ res.writeHead(200, {
645
+ "content-type": MIME[ext] ?? "application/octet-stream",
646
+ "cache-control": "no-store",
647
+ "content-length": String(stat.size)
648
+ });
649
+ createReadStream(abs).pipe(res);
650
+ }
651
+ json(res, status, body) {
652
+ const text = JSON.stringify(body);
653
+ res.writeHead(status, {
654
+ "content-type": MIME[".json"],
655
+ "content-length": String(Buffer.byteLength(text)),
656
+ "cache-control": "no-store"
657
+ });
658
+ res.end(text);
659
+ }
660
+ text(res, status, body) {
661
+ res.writeHead(status, {
662
+ "content-type": "text/plain; charset=utf-8",
663
+ "content-length": String(Buffer.byteLength(body))
664
+ });
665
+ res.end(body);
666
+ }
667
+ };
668
+ function readBody(req) {
669
+ return new Promise((resolve$1, reject) => {
670
+ const chunks = [];
671
+ req.on("data", (c) => chunks.push(c));
672
+ req.on("end", () => resolve$1(Buffer.concat(chunks).toString("utf-8")));
673
+ req.on("error", reject);
674
+ });
675
+ }
676
+
677
+ //#endregion
678
+ export { HarnessServer };