@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 +130 -67
- package/dist/serve.d.ts +2 -0
- package/dist/serve.d.ts.map +1 -1
- package/package.json +1 -1
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.
|
|
14667
|
+
client.write(frame);
|
|
14638
14668
|
} catch {
|
|
14639
14669
|
clients.delete(client);
|
|
14640
14670
|
}
|
|
14641
14671
|
}
|
|
14642
14672
|
}, 150);
|
|
14643
14673
|
});
|
|
14644
|
-
const server =
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
const
|
|
14648
|
-
if (
|
|
14649
|
-
|
|
14650
|
-
|
|
14651
|
-
|
|
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
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
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
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
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
|
package/dist/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|