lens-inspector 0.1.0

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 ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "lens-inspector",
3
+ "version": "0.1.0",
4
+ "description": "Browser-based scene graph inspector for Lens Studio. See every object in your scene, live.",
5
+ "type": "module",
6
+ "bin": {
7
+ "lens-inspector": "server.js"
8
+ },
9
+ "files": [
10
+ "server.js",
11
+ "index.html",
12
+ "SceneInspector.ts",
13
+ "example.js",
14
+ "README.md"
15
+ ],
16
+ "dependencies": {
17
+ "ws": "^8.16.0"
18
+ },
19
+ "keywords": [
20
+ "lens-studio",
21
+ "snap",
22
+ "spectacles",
23
+ "scene-graph",
24
+ "inspector",
25
+ "devtools",
26
+ "ar"
27
+ ],
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/a-sumo/lens-inspector.git"
32
+ },
33
+ "devDependencies": {}
34
+ }
package/server.js ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Lens Studio Scene Inspector - Dev Server
4
+ *
5
+ * Tiny server that bridges Lens Studio and the browser viewer.
6
+ * LS connects via WebSocket and pushes the live scene graph.
7
+ * Browser connects via WebSocket and receives it.
8
+ * Also proxies LS MCP calls (design-time scene graph) to avoid CORS.
9
+ *
10
+ * Usage:
11
+ * node server.js # default port 8200
12
+ * node server.js --port 9000 # custom port
13
+ * node server.js --mcp http://host:8733 # custom MCP URL
14
+ */
15
+
16
+ import { createServer } from "http";
17
+ import { WebSocketServer, WebSocket } from "ws";
18
+ import { readFileSync } from "fs";
19
+ import { join, dirname } from "path";
20
+ import { fileURLToPath } from "url";
21
+ import { exec } from "child_process";
22
+ import { platform } from "os";
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const args = process.argv.slice(2);
26
+
27
+ function arg(flag, fallback) {
28
+ const i = args.indexOf(flag);
29
+ return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
30
+ }
31
+
32
+ const PORT = parseInt(arg("--port", "8200"), 10);
33
+ const MCP_URL = arg("--mcp", "http://localhost:8733/mcp");
34
+ const NO_OPEN = args.includes("--no-open");
35
+
36
+ // Connected clients
37
+ const browsers = new Set();
38
+ const lsClients = new Set();
39
+
40
+ // Latest scene snapshot (so new browser tabs get it immediately)
41
+ let lastScene = null;
42
+
43
+ // --- HTTP server ---
44
+ const html = readFileSync(join(__dirname, "index.html"));
45
+
46
+ const http = createServer(async (req, res) => {
47
+ res.setHeader("Access-Control-Allow-Origin", "*");
48
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
49
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
50
+
51
+ if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
52
+
53
+ // Serve viewer
54
+ if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
55
+ res.writeHead(200, { "Content-Type": "text/html" });
56
+ res.end(html);
57
+ return;
58
+ }
59
+
60
+ // MCP proxy (avoids CORS when browser fetches design-time scene)
61
+ if (req.method === "POST" && req.url.startsWith("/mcp")) {
62
+ let body = "";
63
+ req.on("data", c => body += c);
64
+ req.on("end", async () => {
65
+ try {
66
+ const url = new URL(req.url, `http://localhost:${PORT}`);
67
+ const mcpUrl = url.searchParams.get("url") || MCP_URL;
68
+ const token = url.searchParams.get("token") || "";
69
+ const headers = { "Content-Type": "application/json" };
70
+ if (token) headers["Authorization"] = "Bearer " + token;
71
+
72
+ const resp = await fetch(mcpUrl, { method: "POST", headers, body });
73
+ const result = await resp.text();
74
+ res.writeHead(resp.status, { "Content-Type": "application/json" });
75
+ res.end(result);
76
+ } catch (e) {
77
+ res.writeHead(502, { "Content-Type": "application/json" });
78
+ res.end(JSON.stringify({ error: e.message }));
79
+ }
80
+ });
81
+ return;
82
+ }
83
+
84
+ // Health
85
+ if (req.method === "GET" && req.url === "/health") {
86
+ res.writeHead(200, { "Content-Type": "application/json" });
87
+ res.end(JSON.stringify({ ls: lsClients.size, browsers: browsers.size, hasScene: !!lastScene }));
88
+ return;
89
+ }
90
+
91
+ res.writeHead(404); res.end("Not found");
92
+ });
93
+
94
+ // --- WebSocket ---
95
+ const wss = new WebSocketServer({ server: http });
96
+
97
+ wss.on("connection", (ws, req) => {
98
+ const url = new URL(req.url || "/", `http://localhost:${PORT}`);
99
+ const role = url.searchParams.get("role");
100
+
101
+ if (role === "ls") {
102
+ // Lens Studio inspector component
103
+ lsClients.add(ws);
104
+ console.log(`[inspector] LS connected (${lsClients.size} total)`);
105
+
106
+ ws.on("message", (raw) => {
107
+ const str = raw.toString();
108
+ lastScene = str;
109
+ // Broadcast to all browsers
110
+ for (const b of browsers) {
111
+ if (b.readyState === WebSocket.OPEN) b.send(str);
112
+ }
113
+ });
114
+
115
+ ws.on("close", () => {
116
+ lsClients.delete(ws);
117
+ console.log(`[inspector] LS disconnected (${lsClients.size} total)`);
118
+ });
119
+ } else {
120
+ // Browser viewer
121
+ browsers.add(ws);
122
+ console.log(`[inspector] Browser connected (${browsers.size} total)`);
123
+
124
+ // Send last known scene immediately
125
+ if (lastScene && ws.readyState === WebSocket.OPEN) {
126
+ ws.send(lastScene);
127
+ }
128
+
129
+ ws.on("close", () => {
130
+ browsers.delete(ws);
131
+ console.log(`[inspector] Browser disconnected (${browsers.size} total)`);
132
+ });
133
+ }
134
+
135
+ ws.on("error", (e) => console.error("[inspector] WS error:", e.message));
136
+ });
137
+
138
+ http.listen(PORT, () => {
139
+ const url = `http://localhost:${PORT}`;
140
+ console.log(`\n Lens Studio Scene Inspector running at ${url}`);
141
+ console.log(` Waiting for Lens Studio to connect...\n`);
142
+
143
+ // Auto-open browser
144
+ if (!NO_OPEN) {
145
+ const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
146
+ exec(`${cmd} ${url}`);
147
+ }
148
+ });