@ggterm/core 0.3.0 → 0.3.1

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/dist/cli-plot.js CHANGED
@@ -14601,6 +14601,9 @@ __export(exports_serve, {
14601
14601
  });
14602
14602
  import { watch, writeFileSync as writeFileSync3, unlinkSync } from "fs";
14603
14603
  import { join as join3 } from "path";
14604
+ import { createServer } from "http";
14605
+ import { createHash } from "crypto";
14606
+ import { spawn } from "child_process";
14604
14607
  function plotToVegaLite(plot) {
14605
14608
  const geomTypes = plot._provenance.geomTypes;
14606
14609
  const hasCompositeMark = geomTypes.some((t) => COMPOSITE_MARKS.has(t));
@@ -14617,6 +14620,32 @@ function getLatestPayload() {
14617
14620
  const { spec, provenance } = plotToVegaLite(plot);
14618
14621
  return JSON.stringify({ type: "plot", spec, provenance });
14619
14622
  }
14623
+ function encodeWebSocketFrame(data) {
14624
+ const payload = Buffer.from(data, "utf-8");
14625
+ const len = payload.length;
14626
+ let header;
14627
+ if (len < 126) {
14628
+ header = Buffer.alloc(2);
14629
+ header[0] = 129;
14630
+ header[1] = len;
14631
+ } else if (len < 65536) {
14632
+ header = Buffer.alloc(4);
14633
+ header[0] = 129;
14634
+ header[1] = 126;
14635
+ header.writeUInt16BE(len, 2);
14636
+ } else {
14637
+ header = Buffer.alloc(10);
14638
+ header[0] = 129;
14639
+ header[1] = 127;
14640
+ header.writeBigUInt64BE(BigInt(len), 2);
14641
+ }
14642
+ return Buffer.concat([header, payload]);
14643
+ }
14644
+ function jsonResponse(res, data, status = 200) {
14645
+ const body = JSON.stringify(data);
14646
+ res.writeHead(status, { "content-type": "application/json" });
14647
+ res.end(body);
14648
+ }
14620
14649
  function handleServe(port) {
14621
14650
  const p = port || 4242;
14622
14651
  ensureHistoryDirs();
@@ -14632,85 +14661,119 @@ function handleServe(port) {
14632
14661
  const payload = getLatestPayload();
14633
14662
  if (!payload)
14634
14663
  return;
14664
+ const frame = encodeWebSocketFrame(payload);
14635
14665
  for (const client of clients) {
14636
14666
  try {
14637
- client.send(payload);
14667
+ client.write(frame);
14638
14668
  } catch {
14639
14669
  clients.delete(client);
14640
14670
  }
14641
14671
  }
14642
14672
  }, 150);
14643
14673
  });
14644
- const server = Bun.serve({
14645
- port: p,
14646
- fetch(req, server2) {
14647
- const url2 = new URL(req.url);
14648
- if (url2.pathname === "/ws") {
14649
- if (server2.upgrade(req))
14650
- return;
14651
- return new Response("WebSocket upgrade failed", { status: 400 });
14652
- }
14653
- if (url2.pathname === "/api/latest") {
14654
- const payload = getLatestPayload();
14655
- if (!payload)
14656
- return Response.json({ type: "empty" });
14657
- return new Response(payload, { headers: { "content-type": "application/json" } });
14658
- }
14659
- if (url2.pathname === "/api/history") {
14660
- const entries = getHistory().slice(-50);
14661
- return Response.json(entries);
14662
- }
14663
- if (url2.pathname.startsWith("/api/plot/")) {
14664
- const id = url2.pathname.slice("/api/plot/".length);
14665
- const plot = loadPlotFromHistory(id);
14666
- if (!plot)
14667
- return Response.json({ error: "not found" }, { status: 404 });
14668
- const { spec, provenance } = plotToVegaLite(plot);
14669
- return Response.json({ type: "plot", spec, provenance });
14670
- }
14671
- return new Response(CLIENT_HTML, {
14672
- headers: { "content-type": "text/html; charset=utf-8" }
14673
- });
14674
- },
14675
- websocket: {
14676
- open(ws) {
14677
- clients.add(ws);
14678
- const payload = getLatestPayload();
14679
- if (payload)
14680
- ws.send(payload);
14681
- },
14682
- close(ws) {
14683
- clients.delete(ws);
14684
- },
14685
- message() {}
14674
+ const server = createServer((req, res) => {
14675
+ const url = new URL(req.url || "/", `http://localhost:${p}`);
14676
+ if (url.pathname === "/api/latest") {
14677
+ const payload = getLatestPayload();
14678
+ if (!payload)
14679
+ return jsonResponse(res, { type: "empty" });
14680
+ res.writeHead(200, { "content-type": "application/json" });
14681
+ res.end(payload);
14682
+ return;
14686
14683
  }
14684
+ if (url.pathname === "/api/history") {
14685
+ const entries = getHistory().slice(-50);
14686
+ jsonResponse(res, entries);
14687
+ return;
14688
+ }
14689
+ if (url.pathname.startsWith("/api/plot/")) {
14690
+ const id = url.pathname.slice("/api/plot/".length);
14691
+ const plot = loadPlotFromHistory(id);
14692
+ if (!plot)
14693
+ return jsonResponse(res, { error: "not found" }, 404);
14694
+ const { spec, provenance } = plotToVegaLite(plot);
14695
+ jsonResponse(res, { type: "plot", spec, provenance });
14696
+ return;
14697
+ }
14698
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
14699
+ res.end(CLIENT_HTML);
14687
14700
  });
14688
- const url = `http://localhost:${server.port}`;
14689
- console.log(`ggterm live viewer running at ${url}`);
14690
- const markerPath = join3(getGGTermDir(), "serve.json");
14691
- writeFileSync3(markerPath, JSON.stringify({ port: server.port, pid: process.pid }));
14692
- const cleanup = () => {
14693
- try {
14694
- unlinkSync(markerPath);
14695
- } catch {}
14696
- };
14697
- process.on("SIGINT", () => {
14698
- cleanup();
14699
- process.exit(0);
14701
+ server.on("upgrade", (req, socket, _head) => {
14702
+ const url = new URL(req.url || "/", `http://localhost:${p}`);
14703
+ if (url.pathname !== "/ws") {
14704
+ socket.destroy();
14705
+ return;
14706
+ }
14707
+ const key = req.headers["sec-websocket-key"];
14708
+ if (!key) {
14709
+ socket.destroy();
14710
+ return;
14711
+ }
14712
+ const accept = createHash("sha1").update(key + "258EAFA5-E914-47DA-95CA-5AB5DC85B7A8").digest("base64");
14713
+ socket.write(`HTTP/1.1 101 Switching Protocols\r
14714
+ ` + `Upgrade: websocket\r
14715
+ ` + `Connection: Upgrade\r
14716
+ ` + `Sec-WebSocket-Accept: ${accept}\r
14717
+ ` + `\r
14718
+ `);
14719
+ clients.add(socket);
14720
+ const payload = getLatestPayload();
14721
+ if (payload)
14722
+ socket.write(encodeWebSocketFrame(payload));
14723
+ socket.on("close", () => clients.delete(socket));
14724
+ socket.on("error", () => clients.delete(socket));
14725
+ socket.on("data", (data) => {
14726
+ if (data.length < 2)
14727
+ return;
14728
+ const opcode = data[0] & 15;
14729
+ if (opcode === 8) {
14730
+ const closeFrame = Buffer.alloc(2);
14731
+ closeFrame[0] = 136;
14732
+ closeFrame[1] = 0;
14733
+ try {
14734
+ socket.write(closeFrame);
14735
+ } catch {}
14736
+ socket.end();
14737
+ clients.delete(socket);
14738
+ return;
14739
+ }
14740
+ if (opcode === 9) {
14741
+ const pong = Buffer.from(data);
14742
+ pong[0] = pong[0] & 240 | 10;
14743
+ try {
14744
+ socket.write(pong);
14745
+ } catch {}
14746
+ }
14747
+ });
14700
14748
  });
14701
- process.on("SIGTERM", () => {
14702
- cleanup();
14703
- process.exit(0);
14749
+ server.listen(p, () => {
14750
+ const url = `http://localhost:${p}`;
14751
+ console.log(`ggterm live viewer running at ${url}`);
14752
+ const markerPath = join3(getGGTermDir(), "serve.json");
14753
+ writeFileSync3(markerPath, JSON.stringify({ port: p, pid: process.pid }));
14754
+ const cleanup = () => {
14755
+ try {
14756
+ unlinkSync(markerPath);
14757
+ } catch {}
14758
+ };
14759
+ process.on("SIGINT", () => {
14760
+ cleanup();
14761
+ process.exit(0);
14762
+ });
14763
+ process.on("SIGTERM", () => {
14764
+ cleanup();
14765
+ process.exit(0);
14766
+ });
14767
+ process.on("exit", cleanup);
14768
+ if (process.env.TERM_PROGRAM === "waveterm") {
14769
+ spawn("wsh", ["web", "open", url], { stdio: "ignore", detached: true }).unref();
14770
+ console.log(`Opened Wave panel`);
14771
+ } else {
14772
+ console.log(`Open in browser or Wave panel: wsh web open ${url}`);
14773
+ }
14774
+ console.log(`Watching ${plotsDir} for new plots...`);
14775
+ console.log(`Press Ctrl+C to stop`);
14704
14776
  });
14705
- process.on("exit", cleanup);
14706
- if (process.env.TERM_PROGRAM === "waveterm") {
14707
- Bun.spawn(["wsh", "web", "open", url]);
14708
- console.log(`Opened Wave panel`);
14709
- } else {
14710
- console.log(`Open in browser or Wave panel: wsh web open ${url}`);
14711
- }
14712
- console.log(`Watching ${plotsDir} for new plots...`);
14713
- console.log(`Press Ctrl+C to stop`);
14714
14777
  }
14715
14778
  var COMPOSITE_MARKS, CLIENT_HTML = `<!DOCTYPE html>
14716
14779
  <html lang="en">
package/dist/serve.d.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Watches .ggterm/plots/ for new plots and pushes them to connected
5
5
  * browsers via WebSocket. Renders interactive Vega-Lite in a dark-themed page.
6
+ *
7
+ * Uses node:http and a minimal WebSocket implementation for Node.js compatibility.
6
8
  */
7
9
  export declare function handleServe(port?: number): void;
8
10
  //# sourceMappingURL=serve.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAweH,wBAAgB,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAkG/C"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA6gBH,wBAAgB,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CA2I/C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggterm/core",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Grammar of Graphics engine for terminals",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",