@cleocode/runtime 2026.6.14 → 2026.6.15

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.
@@ -258,6 +258,195 @@ function resolveStreamSource(domain, operation) {
258
258
  return STREAM_SOURCES.get(streamSourceKey(domain, operation));
259
259
  }
260
260
 
261
+ // src/gateway/http/studio-static.ts
262
+ import { createReadStream, existsSync } from "fs";
263
+ import { stat } from "fs/promises";
264
+ import { extname, join, resolve } from "path";
265
+ var log = {
266
+ info: (obj, msg) => {
267
+ if (process.env["CLEO_DEBUG_STUDIO"]) console.info("[studio-static]", msg, obj);
268
+ },
269
+ warn: (obj, msg) => {
270
+ if (process.env["NODE_ENV"] !== "test") {
271
+ console.warn("[studio-static]", msg, obj);
272
+ }
273
+ },
274
+ debug: (_obj, _msg) => {
275
+ if (process.env["CLEO_DEBUG_STUDIO"]) console.debug("[studio-static]", _msg, _obj);
276
+ }
277
+ };
278
+ var MIME_TYPES = /* @__PURE__ */ new Map([
279
+ [".html", "text/html; charset=utf-8"],
280
+ [".js", "application/javascript; charset=utf-8"],
281
+ [".mjs", "application/javascript; charset=utf-8"],
282
+ [".css", "text/css; charset=utf-8"],
283
+ [".json", "application/json; charset=utf-8"],
284
+ [".png", "image/png"],
285
+ [".jpg", "image/jpeg"],
286
+ [".jpeg", "image/jpeg"],
287
+ [".svg", "image/svg+xml"],
288
+ [".ico", "image/x-icon"],
289
+ [".woff", "font/woff"],
290
+ [".woff2", "font/woff2"],
291
+ [".ttf", "font/ttf"],
292
+ [".map", "application/json; charset=utf-8"],
293
+ [".txt", "text/plain; charset=utf-8"],
294
+ [".webmanifest", "application/manifest+json; charset=utf-8"]
295
+ ]);
296
+ var MIME_FALLBACK = "application/octet-stream";
297
+ var STUDIO_PREFIX = "/studio";
298
+ function isStudioPath(pathname) {
299
+ return pathname === STUDIO_PREFIX || pathname.startsWith(`${STUDIO_PREFIX}/`);
300
+ }
301
+ function resolveStudioAssetPath(staticDir, pathname) {
302
+ const base = resolve(staticDir);
303
+ const stripped = pathname.slice(STUDIO_PREFIX.length) || "/";
304
+ const relative = stripped.replace(/^[\\/]+/, "") || ".";
305
+ const candidate = resolve(base, relative);
306
+ const safeBase = base.endsWith("/") ? base : `${base}/`;
307
+ if (candidate !== base && !candidate.startsWith(safeBase)) {
308
+ return void 0;
309
+ }
310
+ return candidate;
311
+ }
312
+ function mimeType(filePath) {
313
+ return MIME_TYPES.get(extname(filePath).toLowerCase()) ?? MIME_FALLBACK;
314
+ }
315
+ function writeBundleAbsentError(res, hint) {
316
+ const body = JSON.stringify({
317
+ success: false,
318
+ error: {
319
+ code: "E_STUDIO_BUNDLE_ABSENT",
320
+ message: "CLEO Studio assets are not available. Run `pnpm --filter @cleocode/studio run build` to build them, or install from npm (npm i -g @cleocode/cleo) for a batteries-included release.",
321
+ hint: hint ?? null
322
+ }
323
+ });
324
+ res.writeHead(503, { "Content-Type": "application/json; charset=utf-8" });
325
+ res.end(body);
326
+ }
327
+ async function serveStudioStatic(req, res, opts) {
328
+ const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
329
+ if (req.method !== "GET" && req.method !== "HEAD") {
330
+ res.writeHead(405, {
331
+ "Content-Type": "application/json; charset=utf-8",
332
+ Allow: "GET, HEAD"
333
+ });
334
+ res.end(
335
+ JSON.stringify({
336
+ success: false,
337
+ error: {
338
+ code: "E_METHOD_NOT_ALLOWED",
339
+ message: `Method ${req.method ?? "<none>"} not allowed for /studio routes`
340
+ }
341
+ })
342
+ );
343
+ return;
344
+ }
345
+ if (pathname === STUDIO_PREFIX) {
346
+ res.writeHead(302, { Location: `${STUDIO_PREFIX}/` });
347
+ res.end();
348
+ return;
349
+ }
350
+ const staticDir = opts.staticDir;
351
+ if (staticDir === void 0 || !existsSync(staticDir)) {
352
+ log.warn({ staticDir }, "studio static dir absent");
353
+ writeBundleAbsentError(res, staticDir);
354
+ return;
355
+ }
356
+ const assetPath = resolveStudioAssetPath(staticDir, pathname);
357
+ if (assetPath === void 0) {
358
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
359
+ res.end(
360
+ JSON.stringify({
361
+ success: false,
362
+ error: { code: "E_BAD_REQUEST", message: "Invalid path" }
363
+ })
364
+ );
365
+ return;
366
+ }
367
+ let targetPath = assetPath;
368
+ let isSpaFallback = false;
369
+ try {
370
+ const st = await stat(targetPath);
371
+ if (st.isDirectory()) {
372
+ const dirIndex = join(targetPath, "index.html");
373
+ if (existsSync(dirIndex)) {
374
+ targetPath = dirIndex;
375
+ } else {
376
+ targetPath = join(staticDir, "index.html");
377
+ isSpaFallback = true;
378
+ }
379
+ }
380
+ } catch {
381
+ targetPath = join(staticDir, "index.html");
382
+ isSpaFallback = true;
383
+ }
384
+ if (!existsSync(targetPath)) {
385
+ res.writeHead(503, { "Content-Type": "application/json; charset=utf-8" });
386
+ res.end(
387
+ JSON.stringify({
388
+ success: false,
389
+ error: {
390
+ code: "E_STUDIO_BUNDLE_CORRUPT",
391
+ message: "Studio index.html not found in the static bundle."
392
+ }
393
+ })
394
+ );
395
+ return;
396
+ }
397
+ const ct = mimeType(targetPath);
398
+ if (req.method === "HEAD") {
399
+ try {
400
+ const st = await stat(targetPath);
401
+ res.writeHead(200, {
402
+ "Content-Type": ct,
403
+ "Content-Length": String(st.size),
404
+ ...isSpaFallback ? {} : { "Cache-Control": "public, max-age=31536000, immutable" }
405
+ });
406
+ res.end();
407
+ } catch {
408
+ res.writeHead(404);
409
+ res.end();
410
+ }
411
+ return;
412
+ }
413
+ try {
414
+ const st = await stat(targetPath);
415
+ res.writeHead(200, {
416
+ "Content-Type": ct,
417
+ "Content-Length": String(st.size),
418
+ // Immutable cache for fingerprinted assets; no cache for SPA fallback.
419
+ ...isSpaFallback ? { "Cache-Control": "no-cache" } : {
420
+ "Cache-Control": "public, max-age=31536000, immutable"
421
+ }
422
+ });
423
+ const stream = createReadStream(targetPath);
424
+ stream.on("error", (err) => {
425
+ log.warn({ err, targetPath }, "studio static read error");
426
+ if (!res.writableEnded) res.end();
427
+ });
428
+ stream.pipe(res);
429
+ log.debug({ pathname, targetPath, isSpaFallback, ct }, "studio static served");
430
+ } catch (err) {
431
+ log.warn({ err, targetPath }, "studio static stat error");
432
+ if (!res.headersSent) {
433
+ res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
434
+ res.end(
435
+ JSON.stringify({
436
+ success: false,
437
+ error: { code: "E_INTERNAL", message: "Failed to read Studio asset" }
438
+ })
439
+ );
440
+ } else if (!res.writableEnded) {
441
+ res.end();
442
+ }
443
+ }
444
+ }
445
+ function resolveStudioStaticDir(cleoPackageDir) {
446
+ const candidate = join(cleoPackageDir, "studio-dist", "client");
447
+ return existsSync(candidate) ? candidate : void 0;
448
+ }
449
+
261
450
  // src/gateway/http/ws-pty.ts
262
451
  import { createHash } from "crypto";
263
452
  import { delimiter } from "path";
@@ -428,7 +617,7 @@ Content-Length: ${Buffer.byteLength(reason)}\r
428
617
  }
429
618
  function swallowLateSocketError() {
430
619
  }
431
- async function bridgePty(req, socket, opts, log, onTeardown) {
620
+ async function bridgePty(req, socket, opts, log2, onTeardown) {
432
621
  const key = req.headers["sec-websocket-key"];
433
622
  socket.write(
434
623
  `HTTP/1.1 101 Switching Protocols\r
@@ -480,7 +669,7 @@ Sec-WebSocket-Accept: ${computeAcceptKey(typeof key === "string" ? key : "")}\r
480
669
  onTeardown();
481
670
  } catch {
482
671
  }
483
- log.debug({ code, reason }, "ws-pty session torn down");
672
+ log2.debug({ code, reason }, "ws-pty session torn down");
484
673
  };
485
674
  const onFrame = (frame) => {
486
675
  switch (frame.opcode) {
@@ -538,7 +727,7 @@ Sec-WebSocket-Accept: ${computeAcceptKey(typeof key === "string" ? key : "")}\r
538
727
  env: scrubPtyEnv()
539
728
  });
540
729
  } catch (err) {
541
- log.warn({ err }, "ws-pty spawn failed");
730
+ log2.warn({ err }, "ws-pty spawn failed");
542
731
  teardown(1011, "PTY spawn failed");
543
732
  return { teardown };
544
733
  }
@@ -546,7 +735,7 @@ Sec-WebSocket-Accept: ${computeAcceptKey(typeof key === "string" ? key : "")}\r
546
735
  if (dataSub && typeof dataSub.dispose === "function") disposables.push(dataSub);
547
736
  const exitSub = pty.onExit(({ exitCode }) => teardown(1e3, `pty exited (${exitCode})`));
548
737
  if (exitSub && typeof exitSub.dispose === "function") disposables.push(exitSub);
549
- log.debug({ shell }, "ws-pty session established");
738
+ log2.debug({ shell }, "ws-pty session established");
550
739
  return { teardown };
551
740
  }
552
741
  function tryParseControl(text) {
@@ -562,13 +751,13 @@ function tryParseControl(text) {
562
751
  return void 0;
563
752
  }
564
753
  function attachWsPtyEndpoint(server, opts = {}) {
565
- const log = getLogger("gateway-ws-pty");
754
+ const log2 = getLogger("gateway-ws-pty");
566
755
  const sessions = /* @__PURE__ */ new Set();
567
756
  const onUpgrade = (req, socket, _head) => {
568
757
  if (!isWsPtyPath(req.url)) return;
569
758
  const decision = authorizeUpgrade(req, req.socket.remoteAddress, opts);
570
759
  if (!decision.ok) {
571
- log.debug({ status: decision.status, reason: decision.reason }, "ws-pty upgrade rejected");
760
+ log2.debug({ status: decision.status, reason: decision.reason }, "ws-pty upgrade rejected");
572
761
  rejectUpgrade(socket, decision.status, decision.reason);
573
762
  return;
574
763
  }
@@ -579,11 +768,11 @@ function attachWsPtyEndpoint(server, opts = {}) {
579
768
  deregistered = true;
580
769
  if (session !== void 0) sessions.delete(session);
581
770
  };
582
- bridgePty(req, socket, opts, log, deregister).then((s) => {
771
+ bridgePty(req, socket, opts, log2, deregister).then((s) => {
583
772
  session = s;
584
773
  if (!deregistered) sessions.add(s);
585
774
  }).catch((err) => {
586
- log.warn({ err }, "ws-pty bridge failed");
775
+ log2.warn({ err }, "ws-pty bridge failed");
587
776
  deregister();
588
777
  if (!socket.destroyed) socket.destroy();
589
778
  });
@@ -670,7 +859,7 @@ function parseHttpRoute(method, url) {
670
859
  return { ok: true, kind: "unary", gateway, domain, operation };
671
860
  }
672
861
  function readBody(req) {
673
- return new Promise((resolve, reject) => {
862
+ return new Promise((resolve2, reject) => {
674
863
  const chunks = [];
675
864
  let size = 0;
676
865
  req.on("data", (chunk) => {
@@ -682,7 +871,7 @@ function readBody(req) {
682
871
  }
683
872
  chunks.push(chunk);
684
873
  });
685
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
874
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
686
875
  req.on("error", reject);
687
876
  });
688
877
  }
@@ -742,12 +931,12 @@ function writeStreamError(res, domain, operation, requestId) {
742
931
  res.writeHead(200, { ...SSE_HEADERS });
743
932
  res.end(encodeStreamEvent(frame));
744
933
  }
745
- async function handleStreamRequest(req, res, domain, operation, log) {
934
+ async function handleStreamRequest(req, res, domain, operation, log2) {
746
935
  const requestId = randomUUID2();
747
936
  const source = resolveStreamSource(domain, operation);
748
937
  if (source === void 0) {
749
938
  writeStreamError(res, domain, operation, requestId);
750
- log.warn({ domain, operation }, "sse stream has no registered source");
939
+ log2.warn({ domain, operation }, "sse stream has no registered source");
751
940
  return;
752
941
  }
753
942
  const controller = new AbortController();
@@ -768,20 +957,25 @@ async function handleStreamRequest(req, res, domain, operation, log) {
768
957
  if (done) break;
769
958
  if (res.writableEnded) break;
770
959
  if (!res.write(value)) {
771
- await new Promise((resolve) => res.once("drain", resolve));
960
+ await new Promise((resolve2) => res.once("drain", resolve2));
772
961
  }
773
962
  }
774
963
  } catch (err) {
775
- log.warn({ err, domain, operation }, "sse stream pipe failed");
964
+ log2.warn({ err, domain, operation }, "sse stream pipe failed");
776
965
  } finally {
777
966
  reader.releaseLock();
778
967
  req.removeListener("close", onClose);
779
968
  if (!controller.signal.aborted) controller.abort();
780
969
  if (!res.writableEnded) res.end();
781
- log.debug({ domain, operation, requestId }, "sse stream closed");
970
+ log2.debug({ domain, operation, requestId }, "sse stream closed");
782
971
  }
783
972
  }
784
- async function handleHttpRequest(handler, req, res, log) {
973
+ async function handleHttpRequest(handler, req, res, log2, studioOpts) {
974
+ const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
975
+ if (studioOpts !== void 0 && isStudioPath(pathname)) {
976
+ await serveStudioStatic(req, res, studioOpts);
977
+ return;
978
+ }
785
979
  const route = parseHttpRoute(req.method, req.url);
786
980
  if (!route.ok) {
787
981
  writeEdgeError(res, route.status, route.code, route.message);
@@ -789,11 +983,11 @@ async function handleHttpRequest(handler, req, res, log) {
789
983
  }
790
984
  if (route.kind === "health") {
791
985
  writeHealth(res);
792
- log.debug({ route: "health" }, "http health probe served");
986
+ log2.debug({ route: "health" }, "http health probe served");
793
987
  return;
794
988
  }
795
989
  if (route.kind === "stream") {
796
- await handleStreamRequest(req, res, route.domain, route.operation, log);
990
+ await handleStreamRequest(req, res, route.domain, route.operation, log2);
797
991
  return;
798
992
  }
799
993
  let params;
@@ -821,7 +1015,7 @@ async function handleHttpRequest(handler, req, res, log) {
821
1015
  const result = await routeUnary(handler, unary);
822
1016
  res.writeHead(result.status, { "Content-Type": "application/json" });
823
1017
  res.end(JSON.stringify(result.body));
824
- log.debug(
1018
+ log2.debug(
825
1019
  {
826
1020
  gateway: route.gateway,
827
1021
  domain: route.domain,
@@ -832,12 +1026,16 @@ async function handleHttpRequest(handler, req, res, log) {
832
1026
  );
833
1027
  }
834
1028
  function startHttpServer(handler, opts) {
835
- const log = getLogger2("gateway-http");
1029
+ const log2 = getLogger2("gateway-http");
836
1030
  const host = opts.host ?? "127.0.0.1";
1031
+ const studioOpts = opts.studioStaticDir !== void 0 ? { staticDir: opts.studioStaticDir } : void 0;
1032
+ if (studioOpts !== void 0) {
1033
+ log2.info({ studioStaticDir: opts.studioStaticDir }, "studio static serving enabled at /studio");
1034
+ }
837
1035
  const server = createServer((req, res) => {
838
- handleHttpRequest(handler, req, res, log).catch((err) => {
1036
+ handleHttpRequest(handler, req, res, log2, studioOpts).catch((err) => {
839
1037
  const message = err instanceof Error ? err.message : String(err);
840
- log.warn({ err }, "http request handling failed");
1038
+ log2.warn({ err }, "http request handling failed");
841
1039
  if (!res.headersSent) {
842
1040
  writeEdgeError(res, 500, "E_HTTP_INTERNAL", message);
843
1041
  } else if (!res.writableEnded) {
@@ -846,13 +1044,13 @@ function startHttpServer(handler, opts) {
846
1044
  });
847
1045
  });
848
1046
  const wsPty = opts.wsPty !== void 0 ? attachWsPtyEndpoint(server, opts.wsPty) : void 0;
849
- return new Promise((resolve, reject) => {
1047
+ return new Promise((resolve2, reject) => {
850
1048
  server.once("error", reject);
851
1049
  server.listen(opts.port, host, () => {
852
1050
  server.removeListener("error", reject);
853
1051
  const address = server.address();
854
1052
  const boundPort = typeof address === "object" && address !== null ? address.port : opts.port;
855
- log.info(
1053
+ log2.info(
856
1054
  { host, port: boundPort, wsPty: wsPty !== void 0 },
857
1055
  "http gateway server listening"
858
1056
  );
@@ -871,7 +1069,7 @@ function startHttpServer(handler, opts) {
871
1069
  });
872
1070
  }
873
1071
  };
874
- resolve(handle);
1072
+ resolve2(handle);
875
1073
  });
876
1074
  });
877
1075
  }
@@ -886,6 +1084,13 @@ export {
886
1084
  tickStreamSource,
887
1085
  registerStreamSource,
888
1086
  resolveStreamSource,
1087
+ STUDIO_PREFIX,
1088
+ isStudioPath,
1089
+ resolveStudioAssetPath,
1090
+ mimeType,
1091
+ writeBundleAbsentError,
1092
+ serveStudioStatic,
1093
+ resolveStudioStaticDir,
889
1094
  WS_PTY_PATH,
890
1095
  NODE_PTY_INSTALL_HINT,
891
1096
  loadNodePty,
@@ -899,4 +1104,4 @@ export {
899
1104
  parseHttpRoute,
900
1105
  startHttpServer
901
1106
  };
902
- //# sourceMappingURL=chunk-Q7X4XH2V.js.map
1107
+ //# sourceMappingURL=chunk-U2ESNIJ5.js.map