opentray 0.1.0 → 0.2.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/README.md CHANGED
@@ -24,3 +24,40 @@ Run a protocol-only example that creates a surface, creates a tray, dispatches a
24
24
  ```bash
25
25
  pnpm --filter opentray example:basic
26
26
  ```
27
+
28
+ Run the human-visible daemon tray example:
29
+
30
+ ```bash
31
+ pnpm --filter opentray example:daemon-tray
32
+ ```
33
+
34
+ After installing from npm, use the published CLI smoke path instead of workspace scripts:
35
+
36
+ ```bash
37
+ opentray smoke daemon-tray
38
+ ```
39
+
40
+ This example starts or reuses the same-version daemon automatically, creates a real tray through the public SDK, and prints broker-routed menu events. Use manual lifecycle commands only for operator/debug control:
41
+
42
+ ```bash
43
+ pnpm --filter opentray cli -- daemon start
44
+ pnpm --filter opentray cli -- daemon stop
45
+ pnpm --filter opentray cli -- daemon restart
46
+ ```
47
+
48
+ The menu includes `WebView Commands` entries that call the `@opentray/ext-webview` facade. On macOS, `Show HTML` loads the platform WebView dynamic library and opens a real native WebView window through daemon-owned UI capability; `Navigate`, `Post Message`, `Evaluate JS`, and `Hide` operate on that window.
49
+
50
+ First-stage platform packages are published for macOS, Linux, and Windows. macOS is the current human-visual acceptance path. Linux and Windows artifacts are present for package topology validation, but unsupported broker/WebView capability must fail explicitly rather than pretending a visible UI exists.
51
+
52
+ For local native smoke before npm publish, stage the current platform artifacts first:
53
+
54
+ ```bash
55
+ cargo build -p opentray-bin -p opentray-ext-webview
56
+ bun run scripts/binaries/stage-local.ts --kind daemon --source target/debug/opentray
57
+ bun run scripts/binaries/stage-local.ts --kind webview --source target/debug/libopentray_ext_webview.dylib
58
+ OPENTRAY_EXAMPLE_WEBVIEW_SMOKE=1 pnpm --filter opentray cli -- smoke daemon-tray
59
+ ```
60
+
61
+ The daemon exits automatically after 30 seconds with no connected clients. Set `OPENTRAY_DAEMON_IDLE_TIMEOUT_MS=0` to keep it alive during debugging, or provide another millisecond value for a custom idle release window.
62
+
63
+ Current native icon support is `rgba`. `encoded` and `file` are typed protocol shapes, but the native `tray-icon` backend reports them as unsupported until decoding and file loading policy are implemented.
package/dist/cli.d.mts ADDED
@@ -0,0 +1,18 @@
1
+ import { DaemonHealth } from "@opentray/spec";
2
+
3
+ //#region src/cli.d.ts
4
+ type CliCommand = {
5
+ type: "daemon";
6
+ action: "start" | "stop" | "restart" | "health";
7
+ } | {
8
+ type: "smoke";
9
+ name: "daemon-tray";
10
+ } | {
11
+ type: "help";
12
+ };
13
+ declare const parseCliCommand: (argv: string[]) => CliCommand;
14
+ declare const runCli: (argv: string[]) => Promise<number>;
15
+ declare const formatDaemonHealthOutput: (health: DaemonHealth) => string;
16
+ //#endregion
17
+ export { formatDaemonHealthOutput, parseCliCommand, runCli };
18
+ //# sourceMappingURL=cli.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.mts","names":[],"sources":["../src/cli.ts"],"mappings":";;;KAcK,UAAA;EACC,IAAA;EAAgB,MAAA;AAAA;EAChB,IAAA;EAAe,IAAA;AAAA;EACf,IAAA;AAAA;AAAA,cAEO,eAAA,GAAmB,IAAA,eAAiB,UAahD;AAAA,cAEY,MAAA,GAAgB,IAAA,eAAiB,OAAO;AAAA,cA+ExC,wBAAA,GAA4B,MAAoB,EAAZ,YAAY"}
package/dist/cli.mjs ADDED
@@ -0,0 +1,474 @@
1
+ #!/usr/bin/env node
2
+ import { t as createClient } from "./client-BfXT-Zz9.mjs";
3
+ import { a as inspectDaemon, c as stopDaemon, i as createNodeDaemonDriver, n as resolveDaemonPaths, o as restartDaemon, r as readPackageVersion, s as startDaemon, t as connectLocalBroker } from "./local-broker-CXOpd9Z8.mjs";
4
+ import { homedir } from "node:os";
5
+ import { fileURLToPath } from "node:url";
6
+ //#region src/smoke/daemon-tray.ts
7
+ const menuLabels = new Map([
8
+ [1, "Primary Action"],
9
+ [3, "Checked Capability"],
10
+ [4, "Radio: Active"],
11
+ [5, "Radio: Passive"],
12
+ [6, "Nested Action"],
13
+ [7, "Nested Check"],
14
+ [8, "WebView: Show HTML"],
15
+ [9, "WebView: Navigate"],
16
+ [10, "WebView: Post Message"],
17
+ [11, "WebView: Evaluate"],
18
+ [12, "WebView: Hide"],
19
+ [99, "Quit Demo"]
20
+ ]);
21
+ const runDaemonTraySmoke = async () => {
22
+ const connection = await connectLocalBroker();
23
+ const client = createClient(connection, { requestIdPrefix: "daemon-smoke" });
24
+ console.log(`connected: endpoint=${connection.endpoint} lease=${connection.leaseId}`);
25
+ const surface = await client.createSurface({
26
+ appId: "com.example.opentray.daemon-smoke",
27
+ title: "OpenTray Daemon Smoke",
28
+ default: true
29
+ });
30
+ console.log(`surface: ${JSON.stringify(surface.surface)}`);
31
+ const tray = await surface.createTray({
32
+ trayId: "daemon-smoke-status",
33
+ title: "OpenTray",
34
+ tooltip: {
35
+ title: "OpenTray",
36
+ description: "npm-installed daemon smoke with broker-routed menu events"
37
+ },
38
+ icon: createVisibleIcon(),
39
+ menu: { items: [
40
+ {
41
+ type: "item",
42
+ id: 1,
43
+ title: "Primary Action"
44
+ },
45
+ {
46
+ type: "item",
47
+ id: 2,
48
+ title: "Disabled Action",
49
+ enabled: false
50
+ },
51
+ {
52
+ type: "check",
53
+ id: 3,
54
+ title: "Checked Capability",
55
+ checked: true
56
+ },
57
+ {
58
+ type: "radio",
59
+ id: 4,
60
+ title: "Radio: Active",
61
+ group: 1,
62
+ checked: true
63
+ },
64
+ {
65
+ type: "radio",
66
+ id: 5,
67
+ title: "Radio: Passive",
68
+ group: 1
69
+ },
70
+ { type: "separator" },
71
+ {
72
+ type: "submenu",
73
+ title: "Nested Actions",
74
+ items: [{
75
+ type: "item",
76
+ id: 6,
77
+ title: "Nested Action"
78
+ }, {
79
+ type: "check",
80
+ id: 7,
81
+ title: "Nested Check",
82
+ checked: false
83
+ }]
84
+ },
85
+ {
86
+ type: "submenu",
87
+ title: "WebView Commands",
88
+ items: [
89
+ {
90
+ type: "item",
91
+ id: 8,
92
+ title: "Show HTML"
93
+ },
94
+ {
95
+ type: "item",
96
+ id: 9,
97
+ title: "Navigate"
98
+ },
99
+ {
100
+ type: "item",
101
+ id: 10,
102
+ title: "Post Message"
103
+ },
104
+ {
105
+ type: "item",
106
+ id: 11,
107
+ title: "Evaluate JS"
108
+ },
109
+ {
110
+ type: "item",
111
+ id: 12,
112
+ title: "Hide"
113
+ }
114
+ ]
115
+ },
116
+ { type: "separator" },
117
+ {
118
+ type: "item",
119
+ id: 99,
120
+ title: "Quit Demo"
121
+ }
122
+ ] }
123
+ });
124
+ console.log(`tray: ${tray.trayId}`);
125
+ await connection.request({
126
+ type: "load-ext",
127
+ requestId: "daemon-smoke-load-webview",
128
+ surfaceId: surface.surface.surfaceId,
129
+ name: "webview",
130
+ path: "@opentray/ext-webview"
131
+ });
132
+ console.log("webview extension requested through the generic load-ext path");
133
+ console.log("open the system tray item and choose any enabled menu item to see routed events");
134
+ let closed = false;
135
+ let messageCount = 0;
136
+ let evalCount = 0;
137
+ let exitTimer;
138
+ const shutdown = async () => {
139
+ if (closed) return;
140
+ closed = true;
141
+ if (exitTimer !== void 0) clearTimeout(exitTimer);
142
+ await connection.close();
143
+ };
144
+ const commandWebview = async (command) => {
145
+ await tray.commandExtension("webview", command);
146
+ };
147
+ const handleMenuClick = async (itemId) => {
148
+ if (itemId === 99) {
149
+ console.log("quit item routed; closing smoke connection");
150
+ await shutdown();
151
+ return;
152
+ }
153
+ if (itemId < 8 || itemId > 12) return;
154
+ switch (itemId) {
155
+ case 8:
156
+ await commandWebview({
157
+ type: "show",
158
+ html: createWebviewDemoHtml(),
159
+ width: 420,
160
+ height: 260,
161
+ fallbackRect: {
162
+ x: 0,
163
+ y: 0,
164
+ width: 1,
165
+ height: 1
166
+ }
167
+ });
168
+ console.log("webview command: show");
169
+ break;
170
+ case 9:
171
+ await commandWebview({
172
+ type: "navigate",
173
+ url: "https://example.com/opentray-status"
174
+ });
175
+ console.log("webview command: navigate");
176
+ break;
177
+ case 10:
178
+ messageCount += 1;
179
+ await commandWebview({
180
+ type: "postMessage",
181
+ payload: {
182
+ kind: "ping",
183
+ source: "opentray smoke daemon-tray",
184
+ count: messageCount,
185
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
186
+ }
187
+ });
188
+ console.log("webview command: postMessage");
189
+ break;
190
+ case 11:
191
+ evalCount += 1;
192
+ await commandWebview({
193
+ type: "evaluate",
194
+ js: createVisibleEvaluateScript(evalCount)
195
+ });
196
+ console.log("webview command: evaluate");
197
+ break;
198
+ case 12:
199
+ await commandWebview({ type: "hide" });
200
+ console.log("webview command: hide");
201
+ break;
202
+ }
203
+ };
204
+ connection.onEvent((frame) => {
205
+ console.log(`broker -> client ${JSON.stringify(frame)}`);
206
+ if (frame.type === "event" && frame.event.type === "menuClick") {
207
+ console.log(`menu click: ${menuLabels.get(frame.event.itemId) ?? frame.event.itemId}`);
208
+ handleMenuClick(frame.event.itemId);
209
+ }
210
+ });
211
+ const exitAfter = process.env.OPENTRAY_EXAMPLE_EXIT_AFTER_MS;
212
+ if (exitAfter !== void 0 && exitAfter.length > 0) {
213
+ const duration = Number.parseInt(exitAfter, 10);
214
+ if (Number.isInteger(duration) && duration > 0) exitTimer = setTimeout(() => {
215
+ shutdown();
216
+ }, duration);
217
+ }
218
+ const webviewSmoke = process.env.OPENTRAY_EXAMPLE_WEBVIEW_SMOKE;
219
+ try {
220
+ if (webviewSmoke === "show") await handleMenuClick(8);
221
+ else if (webviewSmoke === "1") {
222
+ if (exitTimer !== void 0) {
223
+ clearTimeout(exitTimer);
224
+ exitTimer = void 0;
225
+ }
226
+ for (const itemId of [
227
+ 8,
228
+ 10,
229
+ 11,
230
+ 9,
231
+ 12
232
+ ]) await handleMenuClick(itemId);
233
+ await sleep(300);
234
+ await shutdown();
235
+ }
236
+ } catch (error) {
237
+ await shutdown();
238
+ throw error;
239
+ }
240
+ };
241
+ function createVisibleIcon() {
242
+ const width = 32;
243
+ const height = 32;
244
+ const data = [];
245
+ for (let y = 0; y < height; y += 1) for (let x = 0; x < width; x += 1) {
246
+ const dx = x - 15.5;
247
+ const dy = y - 15.5;
248
+ const distance = Math.sqrt(dx * dx + dy * dy);
249
+ const inRing = distance >= 9 && distance <= 14;
250
+ const inCore = distance <= 5;
251
+ const inNeedle = Math.abs(dx + dy) <= 1.2 && x >= 9 && x <= 23 && y >= 9 && y <= 23;
252
+ if (inRing || inCore || inNeedle) data.push(inCore ? 255 : 24, inNeedle ? 240 : 132, inRing ? 72 : 96, 255);
253
+ else data.push(0, 0, 0, 0);
254
+ }
255
+ return {
256
+ type: "rgba",
257
+ width,
258
+ height,
259
+ data
260
+ };
261
+ }
262
+ function createWebviewDemoHtml() {
263
+ return `<!doctype html>
264
+ <html lang="en">
265
+ <head>
266
+ <meta charset="utf-8" />
267
+ <title>OpenTray WebView</title>
268
+ <style>
269
+ :root {
270
+ color: #18220f;
271
+ background: #f6edd8;
272
+ font: 15px ui-rounded, "SF Pro Rounded", "Avenir Next", sans-serif;
273
+ }
274
+ body {
275
+ margin: 0;
276
+ }
277
+ main {
278
+ min-height: 100vh;
279
+ box-sizing: border-box;
280
+ padding: 22px;
281
+ background:
282
+ radial-gradient(circle at 84% 10%, rgba(34, 132, 96, 0.22), transparent 34%),
283
+ linear-gradient(135deg, #fff8e7 0%, #e9f0d8 100%);
284
+ }
285
+ h1 {
286
+ margin: 0 0 8px;
287
+ font-size: 24px;
288
+ letter-spacing: -0.04em;
289
+ }
290
+ p {
291
+ margin: 0 0 16px;
292
+ color: #526044;
293
+ }
294
+ section {
295
+ display: grid;
296
+ gap: 10px;
297
+ }
298
+ .card {
299
+ border: 1px solid rgba(24, 34, 15, 0.16);
300
+ border-radius: 14px;
301
+ padding: 12px;
302
+ background: rgba(255, 255, 255, 0.72);
303
+ box-shadow: 0 12px 30px rgba(56, 72, 36, 0.12);
304
+ }
305
+ .label {
306
+ margin-bottom: 5px;
307
+ color: #7b5b1d;
308
+ font-size: 12px;
309
+ font-weight: 700;
310
+ letter-spacing: 0.08em;
311
+ text-transform: uppercase;
312
+ }
313
+ code {
314
+ word-break: break-word;
315
+ }
316
+ </style>
317
+ </head>
318
+ <body>
319
+ <main>
320
+ <h1>OpenTray WebView</h1>
321
+ <p>Use the tray menu to mutate this native WebView through extension commands.</p>
322
+ <section>
323
+ <div class="card">
324
+ <div class="label">postMessage</div>
325
+ <code id="message-status">Waiting for WebView Commands -> Post Message</code>
326
+ </div>
327
+ <div class="card">
328
+ <div class="label">evaluate JS</div>
329
+ <code id="eval-status">Waiting for WebView Commands -> Evaluate JS</code>
330
+ </div>
331
+ </section>
332
+ </main>
333
+ <script>
334
+ window.addEventListener("message", (event) => {
335
+ const target = document.getElementById("message-status");
336
+ if (target) {
337
+ target.textContent = JSON.stringify(event.data, null, 2);
338
+ }
339
+ });
340
+ <\/script>
341
+ </body>
342
+ </html>`;
343
+ }
344
+ function createVisibleEvaluateScript(count) {
345
+ return `(() => {
346
+ let target = document.getElementById("eval-status");
347
+ if (!target) {
348
+ const panel = document.createElement("div");
349
+ panel.style.cssText = "position:fixed;left:16px;right:16px;bottom:16px;z-index:2147483647;padding:12px;border-radius:12px;background:#fff8e7;color:#18220f;box-shadow:0 12px 30px rgba(0,0,0,.18);font:14px ui-rounded, sans-serif;";
350
+ panel.textContent = "OpenTray Evaluate JS: ";
351
+ target = document.createElement("code");
352
+ target.id = "eval-status";
353
+ panel.appendChild(target);
354
+ document.body.appendChild(panel);
355
+ }
356
+ if (target) {
357
+ target.textContent = "Evaluate JS updated the WebView, count=${count}, at " + new Date().toLocaleTimeString();
358
+ }
359
+ window.__OPENTRAY_DEMO__ = { evaluated: true, count: ${count} };
360
+ })();`;
361
+ }
362
+ function sleep(ms) {
363
+ return new Promise((resolve) => setTimeout(resolve, ms));
364
+ }
365
+ //#endregion
366
+ //#region src/cli.ts
367
+ const packageJsonUrl = new URL("../package.json", import.meta.url);
368
+ const parseCliCommand = (argv) => {
369
+ const [group, action] = argv;
370
+ if (group !== "daemon") {
371
+ if (group === "smoke" && action === "daemon-tray") return {
372
+ type: "smoke",
373
+ name: "daemon-tray"
374
+ };
375
+ return { type: "help" };
376
+ }
377
+ if (action === "start" || action === "stop" || action === "restart" || action === "health") return {
378
+ type: "daemon",
379
+ action
380
+ };
381
+ return { type: "help" };
382
+ };
383
+ const runCli = async (argv) => {
384
+ const command = parseCliCommand(argv);
385
+ const packageVersion = process.env.OPENTRAY_DAEMON_PACKAGE_VERSION ?? await readPackageVersion(packageJsonUrl);
386
+ const paths = resolveDaemonPaths({
387
+ homeDir: process.env.OPENTRAY_HOME ?? homedir(),
388
+ packageVersion
389
+ });
390
+ if (command.type === "help") {
391
+ printHelp();
392
+ return 1;
393
+ }
394
+ if (command.type === "smoke") {
395
+ await runDaemonTraySmoke();
396
+ return 0;
397
+ }
398
+ const driver = createNodeDaemonDriver(fileURLToPath(import.meta.url));
399
+ if (command.action === "start") {
400
+ const result = await startDaemon({
401
+ paths,
402
+ driver
403
+ });
404
+ console.log(`opentray daemon ${result.status}: pid=${result.pid} endpoint=${result.paths.endpoint}`);
405
+ return 0;
406
+ }
407
+ if (command.action === "stop") {
408
+ const result = await stopDaemon({
409
+ paths,
410
+ driver
411
+ });
412
+ console.log(result.status === "stopped" ? `opentray daemon stopped: pid=${result.pid}` : "opentray daemon not running");
413
+ return 0;
414
+ }
415
+ if (command.action === "restart") {
416
+ const result = await restartDaemon({
417
+ paths,
418
+ driver
419
+ });
420
+ console.log(`opentray daemon ${result.status}: pid=${result.pid} endpoint=${result.paths.endpoint}`);
421
+ return 0;
422
+ }
423
+ if (command.action === "health") {
424
+ if ((await inspectDaemon({
425
+ paths,
426
+ driver
427
+ })).status === "not-running") {
428
+ console.log("opentray daemon not running");
429
+ return 0;
430
+ }
431
+ const connection = await connectLocalBroker({
432
+ autoStart: false,
433
+ endpoint: paths.endpoint,
434
+ homeDir: paths.homeDir,
435
+ packageVersion
436
+ });
437
+ try {
438
+ const response = await connection.request({
439
+ type: "health",
440
+ requestId: "opentray-daemon-health"
441
+ });
442
+ if (response.type !== "daemon-health") throw new Error(`expected daemon-health response, received ${response.type}`);
443
+ console.log(formatDaemonHealthOutput(response.health));
444
+ } finally {
445
+ await connection.close();
446
+ }
447
+ return 0;
448
+ }
449
+ printHelp();
450
+ return 1;
451
+ };
452
+ const printHelp = () => {
453
+ console.error("Usage: opentray daemon <start|stop|restart|health>");
454
+ console.error(" opentray smoke daemon-tray");
455
+ };
456
+ const formatDaemonHealthOutput = (health) => {
457
+ const lines = [
458
+ "opentray daemon running",
459
+ `pid: ${health.pid}`,
460
+ `endpoint: ${health.endpoint}`,
461
+ `packageVersion: ${health.packageVersion}`,
462
+ `protocolVersion: ${health.protocolVersion}`,
463
+ `sessions: ${health.sessionCount}`
464
+ ];
465
+ for (const session of health.sessions) lines.push(`- sessionId=${session.sessionId} initialized=${session.initialized} leaseId=${session.leaseId ?? "(pending)"}`);
466
+ return lines.join("\n");
467
+ };
468
+ if (process.argv[1] === fileURLToPath(import.meta.url)) runCli(process.argv.slice(2)).then((code) => {
469
+ process.exitCode = code;
470
+ });
471
+ //#endregion
472
+ export { formatDaemonHealthOutput, parseCliCommand, runCli };
473
+
474
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/smoke/daemon-tray.ts","../src/cli.ts"],"sourcesContent":["import type { Rect } from \"@opentray/spec\";\n\nimport { createClient } from \"../client\";\nimport { connectLocalBroker } from \"../local-broker\";\n\nconst menuLabels = new Map<number, string>([\n [1, \"Primary Action\"],\n [3, \"Checked Capability\"],\n [4, \"Radio: Active\"],\n [5, \"Radio: Passive\"],\n [6, \"Nested Action\"],\n [7, \"Nested Check\"],\n [8, \"WebView: Show HTML\"],\n [9, \"WebView: Navigate\"],\n [10, \"WebView: Post Message\"],\n [11, \"WebView: Evaluate\"],\n [12, \"WebView: Hide\"],\n [99, \"Quit Demo\"],\n]);\n\ninterface WebviewShowCommand {\n type: \"show\";\n html?: string;\n url?: string;\n width?: number;\n height?: number;\n fallbackRect?: Rect;\n}\n\nexport const runDaemonTraySmoke = async (): Promise<void> => {\n const connection = await connectLocalBroker();\n const client = createClient(connection, { requestIdPrefix: \"daemon-smoke\" });\n console.log(`connected: endpoint=${connection.endpoint} lease=${connection.leaseId}`);\n\n const surface = await client.createSurface({\n appId: \"com.example.opentray.daemon-smoke\",\n title: \"OpenTray Daemon Smoke\",\n default: true,\n });\n console.log(`surface: ${JSON.stringify(surface.surface)}`);\n\n const tray = await surface.createTray({\n trayId: \"daemon-smoke-status\",\n title: \"OpenTray\",\n tooltip: {\n title: \"OpenTray\",\n description: \"npm-installed daemon smoke with broker-routed menu events\",\n },\n icon: createVisibleIcon(),\n menu: {\n items: [\n { type: \"item\", id: 1, title: \"Primary Action\" },\n { type: \"item\", id: 2, title: \"Disabled Action\", enabled: false },\n { type: \"check\", id: 3, title: \"Checked Capability\", checked: true },\n { type: \"radio\", id: 4, title: \"Radio: Active\", group: 1, checked: true },\n { type: \"radio\", id: 5, title: \"Radio: Passive\", group: 1 },\n { type: \"separator\" },\n {\n type: \"submenu\",\n title: \"Nested Actions\",\n items: [\n { type: \"item\", id: 6, title: \"Nested Action\" },\n { type: \"check\", id: 7, title: \"Nested Check\", checked: false },\n ],\n },\n {\n type: \"submenu\",\n title: \"WebView Commands\",\n items: [\n { type: \"item\", id: 8, title: \"Show HTML\" },\n { type: \"item\", id: 9, title: \"Navigate\" },\n { type: \"item\", id: 10, title: \"Post Message\" },\n { type: \"item\", id: 11, title: \"Evaluate JS\" },\n { type: \"item\", id: 12, title: \"Hide\" },\n ],\n },\n { type: \"separator\" },\n { type: \"item\", id: 99, title: \"Quit Demo\" },\n ],\n },\n });\n console.log(`tray: ${tray.trayId}`);\n\n await connection.request({\n type: \"load-ext\",\n requestId: \"daemon-smoke-load-webview\",\n surfaceId: surface.surface.surfaceId,\n name: \"webview\",\n path: \"@opentray/ext-webview\",\n });\n console.log(\"webview extension requested through the generic load-ext path\");\n console.log(\"open the system tray item and choose any enabled menu item to see routed events\");\n\n let closed = false;\n let messageCount = 0;\n let evalCount = 0;\n let exitTimer: NodeJS.Timeout | undefined;\n\n const shutdown = async (): Promise<void> => {\n if (closed) {\n return;\n }\n closed = true;\n if (exitTimer !== undefined) {\n clearTimeout(exitTimer);\n }\n await connection.close();\n };\n\n const commandWebview = async (command: WebviewShowCommand | Record<string, unknown>): Promise<void> => {\n await tray.commandExtension(\"webview\", command);\n };\n\n const handleMenuClick = async (itemId: number): Promise<void> => {\n if (itemId === 99) {\n console.log(\"quit item routed; closing smoke connection\");\n await shutdown();\n return;\n }\n\n if (itemId < 8 || itemId > 12) {\n return;\n }\n\n switch (itemId) {\n case 8:\n await commandWebview({\n type: \"show\",\n html: createWebviewDemoHtml(),\n width: 420,\n height: 260,\n fallbackRect: { x: 0, y: 0, width: 1, height: 1 },\n });\n console.log(\"webview command: show\");\n break;\n case 9:\n await commandWebview({ type: \"navigate\", url: \"https://example.com/opentray-status\" });\n console.log(\"webview command: navigate\");\n break;\n case 10:\n messageCount += 1;\n await commandWebview({\n type: \"postMessage\",\n payload: {\n kind: \"ping\",\n source: \"opentray smoke daemon-tray\",\n count: messageCount,\n sentAt: new Date().toISOString(),\n },\n });\n console.log(\"webview command: postMessage\");\n break;\n case 11:\n evalCount += 1;\n await commandWebview({ type: \"evaluate\", js: createVisibleEvaluateScript(evalCount) });\n console.log(\"webview command: evaluate\");\n break;\n case 12:\n await commandWebview({ type: \"hide\" });\n console.log(\"webview command: hide\");\n break;\n }\n };\n\n connection.onEvent((frame) => {\n console.log(`broker -> client ${JSON.stringify(frame)}`);\n if (frame.type === \"event\" && frame.event.type === \"menuClick\") {\n console.log(`menu click: ${menuLabels.get(frame.event.itemId) ?? frame.event.itemId}`);\n void handleMenuClick(frame.event.itemId);\n }\n });\n\n const exitAfter = process.env.OPENTRAY_EXAMPLE_EXIT_AFTER_MS;\n if (exitAfter !== undefined && exitAfter.length > 0) {\n const duration = Number.parseInt(exitAfter, 10);\n if (Number.isInteger(duration) && duration > 0) {\n exitTimer = setTimeout(() => {\n void shutdown();\n }, duration);\n }\n }\n\n const webviewSmoke = process.env.OPENTRAY_EXAMPLE_WEBVIEW_SMOKE;\n try {\n if (webviewSmoke === \"show\") {\n await handleMenuClick(8);\n } else if (webviewSmoke === \"1\") {\n if (exitTimer !== undefined) {\n clearTimeout(exitTimer);\n exitTimer = undefined;\n }\n for (const itemId of [8, 10, 11, 9, 12]) {\n await handleMenuClick(itemId);\n }\n await sleep(300);\n await shutdown();\n }\n } catch (error) {\n await shutdown();\n throw error;\n }\n};\n\nfunction createVisibleIcon(): { type: \"rgba\"; width: number; height: number; data: number[] } {\n const width = 32;\n const height = 32;\n const data: number[] = [];\n\n for (let y = 0; y < height; y += 1) {\n for (let x = 0; x < width; x += 1) {\n const dx = x - 15.5;\n const dy = y - 15.5;\n const distance = Math.sqrt(dx * dx + dy * dy);\n const inRing = distance >= 9 && distance <= 14;\n const inCore = distance <= 5;\n const inNeedle = Math.abs(dx + dy) <= 1.2 && x >= 9 && x <= 23 && y >= 9 && y <= 23;\n\n if (inRing || inCore || inNeedle) {\n data.push(inCore ? 255 : 24, inNeedle ? 240 : 132, inRing ? 72 : 96, 255);\n } else {\n data.push(0, 0, 0, 0);\n }\n }\n }\n\n return { type: \"rgba\", width, height, data };\n}\n\nfunction createWebviewDemoHtml(): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>OpenTray WebView</title>\n <style>\n :root {\n color: #18220f;\n background: #f6edd8;\n font: 15px ui-rounded, \"SF Pro Rounded\", \"Avenir Next\", sans-serif;\n }\n body {\n margin: 0;\n }\n main {\n min-height: 100vh;\n box-sizing: border-box;\n padding: 22px;\n background:\n radial-gradient(circle at 84% 10%, rgba(34, 132, 96, 0.22), transparent 34%),\n linear-gradient(135deg, #fff8e7 0%, #e9f0d8 100%);\n }\n h1 {\n margin: 0 0 8px;\n font-size: 24px;\n letter-spacing: -0.04em;\n }\n p {\n margin: 0 0 16px;\n color: #526044;\n }\n section {\n display: grid;\n gap: 10px;\n }\n .card {\n border: 1px solid rgba(24, 34, 15, 0.16);\n border-radius: 14px;\n padding: 12px;\n background: rgba(255, 255, 255, 0.72);\n box-shadow: 0 12px 30px rgba(56, 72, 36, 0.12);\n }\n .label {\n margin-bottom: 5px;\n color: #7b5b1d;\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n }\n code {\n word-break: break-word;\n }\n </style>\n </head>\n <body>\n <main>\n <h1>OpenTray WebView</h1>\n <p>Use the tray menu to mutate this native WebView through extension commands.</p>\n <section>\n <div class=\"card\">\n <div class=\"label\">postMessage</div>\n <code id=\"message-status\">Waiting for WebView Commands -> Post Message</code>\n </div>\n <div class=\"card\">\n <div class=\"label\">evaluate JS</div>\n <code id=\"eval-status\">Waiting for WebView Commands -> Evaluate JS</code>\n </div>\n </section>\n </main>\n <script>\n window.addEventListener(\"message\", (event) => {\n const target = document.getElementById(\"message-status\");\n if (target) {\n target.textContent = JSON.stringify(event.data, null, 2);\n }\n });\n </script>\n </body>\n</html>`;\n}\n\nfunction createVisibleEvaluateScript(count: number): string {\n return `(() => {\n let target = document.getElementById(\"eval-status\");\n if (!target) {\n const panel = document.createElement(\"div\");\n panel.style.cssText = \"position:fixed;left:16px;right:16px;bottom:16px;z-index:2147483647;padding:12px;border-radius:12px;background:#fff8e7;color:#18220f;box-shadow:0 12px 30px rgba(0,0,0,.18);font:14px ui-rounded, sans-serif;\";\n panel.textContent = \"OpenTray Evaluate JS: \";\n target = document.createElement(\"code\");\n target.id = \"eval-status\";\n panel.appendChild(target);\n document.body.appendChild(panel);\n }\n if (target) {\n target.textContent = \"Evaluate JS updated the WebView, count=${count}, at \" + new Date().toLocaleTimeString();\n }\n window.__OPENTRAY_DEMO__ = { evaluated: true, count: ${count} };\n })();`;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","#!/usr/bin/env node\nimport { fileURLToPath } from \"node:url\";\nimport { homedir } from \"node:os\";\n\nimport type { DaemonHealth } from \"@opentray/spec\";\n\nimport { createNodeDaemonDriver, inspectDaemon, restartDaemon, startDaemon, stopDaemon } from \"./daemon/lifecycle\";\nimport { readPackageVersion } from \"./daemon/package-version\";\nimport { resolveDaemonPaths } from \"./daemon/paths\";\nimport { connectLocalBroker } from \"./local-broker\";\nimport { runDaemonTraySmoke } from \"./smoke/daemon-tray\";\n\nconst packageJsonUrl = new URL(\"../package.json\", import.meta.url);\n\ntype CliCommand =\n | { type: \"daemon\"; action: \"start\" | \"stop\" | \"restart\" | \"health\" }\n | { type: \"smoke\"; name: \"daemon-tray\" }\n | { type: \"help\" };\n\nexport const parseCliCommand = (argv: string[]): CliCommand => {\n const [group, action] = argv;\n if (group !== \"daemon\") {\n if (group === \"smoke\" && action === \"daemon-tray\") {\n return { type: \"smoke\", name: \"daemon-tray\" };\n }\n return { type: \"help\" };\n }\n if (action === \"start\" || action === \"stop\" || action === \"restart\" || action === \"health\") {\n return { type: \"daemon\", action };\n }\n\n return { type: \"help\" };\n};\n\nexport const runCli = async (argv: string[]): Promise<number> => {\n const command = parseCliCommand(argv);\n const packageVersion = process.env.OPENTRAY_DAEMON_PACKAGE_VERSION ?? (await readPackageVersion(packageJsonUrl));\n const paths = resolveDaemonPaths({\n homeDir: process.env.OPENTRAY_HOME ?? homedir(),\n packageVersion,\n });\n\n if (command.type === \"help\") {\n printHelp();\n return 1;\n }\n\n if (command.type === \"smoke\") {\n await runDaemonTraySmoke();\n return 0;\n }\n\n const driver = createNodeDaemonDriver(fileURLToPath(import.meta.url));\n\n if (command.action === \"start\") {\n const result = await startDaemon({ paths, driver });\n console.log(`opentray daemon ${result.status}: pid=${result.pid} endpoint=${result.paths.endpoint}`);\n return 0;\n }\n\n if (command.action === \"stop\") {\n const result = await stopDaemon({ paths, driver });\n console.log(\n result.status === \"stopped\"\n ? `opentray daemon stopped: pid=${result.pid}`\n : \"opentray daemon not running\",\n );\n return 0;\n }\n\n if (command.action === \"restart\") {\n const result = await restartDaemon({ paths, driver });\n console.log(`opentray daemon ${result.status}: pid=${result.pid} endpoint=${result.paths.endpoint}`);\n return 0;\n }\n\n if (command.action === \"health\") {\n const inspected = await inspectDaemon({ paths, driver });\n if (inspected.status === \"not-running\") {\n console.log(\"opentray daemon not running\");\n return 0;\n }\n\n const connection = await connectLocalBroker({\n autoStart: false,\n endpoint: paths.endpoint,\n homeDir: paths.homeDir,\n packageVersion,\n });\n try {\n const response = await connection.request({\n type: \"health\",\n requestId: \"opentray-daemon-health\",\n });\n if (response.type !== \"daemon-health\") {\n throw new Error(`expected daemon-health response, received ${response.type}`);\n }\n console.log(formatDaemonHealthOutput(response.health));\n } finally {\n await connection.close();\n }\n return 0;\n }\n\n printHelp();\n return 1;\n};\n\nconst printHelp = (): void => {\n console.error(\"Usage: opentray daemon <start|stop|restart|health>\");\n console.error(\" opentray smoke daemon-tray\");\n};\n\nexport const formatDaemonHealthOutput = (health: DaemonHealth): string => {\n const lines = [\n \"opentray daemon running\",\n `pid: ${health.pid}`,\n `endpoint: ${health.endpoint}`,\n `packageVersion: ${health.packageVersion}`,\n `protocolVersion: ${health.protocolVersion}`,\n `sessions: ${health.sessionCount}`,\n ];\n\n for (const session of health.sessions) {\n lines.push(\n `- sessionId=${session.sessionId} initialized=${session.initialized} leaseId=${session.leaseId ?? \"(pending)\"}`,\n );\n }\n\n return lines.join(\"\\n\");\n};\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n runCli(process.argv.slice(2)).then((code) => {\n process.exitCode = code;\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ import { PROTOCOL_VERSION } from "@opentray/spec";
2
+ //#region src/client.ts
3
+ const createInitFrame = (clientVersion) => ({
4
+ type: "init",
5
+ protocolVersion: PROTOCOL_VERSION,
6
+ clientVersion
7
+ });
8
+ const createClient = (transport, options = {}) => {
9
+ const nextRequestId = createRequestIdFactory(options.requestIdPrefix ?? "opentray");
10
+ return { async createSurface(surfaceOptions) {
11
+ const requestId = nextRequestId();
12
+ const surface = expectResponse(await transport.request({
13
+ type: "create-surface",
14
+ requestId,
15
+ ...surfaceOptions
16
+ }), requestId, "surface-created").surface;
17
+ return createSurfaceHandle(transport, surface, nextRequestId);
18
+ } };
19
+ };
20
+ const createSurfaceHandle = (transport, surface, nextRequestId = createRequestIdFactory("opentray")) => ({
21
+ surface,
22
+ async createTray(options) {
23
+ const requestId = nextRequestId();
24
+ return createTrayHandle(transport, surface, expectResponse(await transport.request({
25
+ type: "create-tray",
26
+ requestId,
27
+ surface,
28
+ tray: options
29
+ }), requestId, "tray-created").trayId, nextRequestId);
30
+ }
31
+ });
32
+ const createTrayHandle = (transport, surface, trayId, nextRequestId = createRequestIdFactory("opentray")) => ({
33
+ surface,
34
+ trayId,
35
+ async commandExtension(ext, data) {
36
+ const requestId = nextRequestId();
37
+ expectResponse(await transport.request({
38
+ type: "ext-command",
39
+ requestId,
40
+ surfaceId: surface.surfaceId,
41
+ trayId,
42
+ ext,
43
+ data
44
+ }), requestId, "ack");
45
+ },
46
+ async destroy() {
47
+ const requestId = nextRequestId();
48
+ expectResponse(await transport.request({
49
+ type: "destroy-tray",
50
+ requestId,
51
+ surfaceId: surface.surfaceId,
52
+ trayId
53
+ }), requestId, "ack");
54
+ }
55
+ });
56
+ const createRequestIdFactory = (prefix) => {
57
+ let next = 1;
58
+ return () => {
59
+ const requestId = `${prefix}-${next}`;
60
+ next += 1;
61
+ return requestId;
62
+ };
63
+ };
64
+ const expectResponse = (frame, requestId, type) => {
65
+ if (frame.type === "error") throw new Error(`${frame.code}: ${frame.message}`);
66
+ if (frame.type !== type) throw new Error(`expected ${type} for ${requestId}, received ${frame.type}`);
67
+ if (requestIdOf(frame) !== requestId) throw new Error(`expected response for ${requestId}, received ${requestIdOf(frame) ?? "none"}`);
68
+ return frame;
69
+ };
70
+ const requestIdOf = (frame) => {
71
+ switch (frame.type) {
72
+ case "surface-created":
73
+ case "default-surface":
74
+ case "tray-created":
75
+ case "ack":
76
+ case "daemon-health": return frame.requestId;
77
+ case "error": return frame.requestId;
78
+ case "ready":
79
+ case "event":
80
+ case "ext-event": return;
81
+ }
82
+ };
83
+ //#endregion
84
+ export { createTrayHandle as i, createInitFrame as n, createSurfaceHandle as r, createClient as t };
85
+
86
+ //# sourceMappingURL=client-BfXT-Zz9.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-BfXT-Zz9.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import {\n PROTOCOL_VERSION,\n type ClientFrame,\n type ClientRequestFrame,\n type RequestId,\n type ServerFrame,\n type SurfaceOptions,\n type SurfaceRef,\n type TrayId,\n type TrayOptions,\n} from \"@opentray/spec\";\n\nexport interface OpenTrayTransport {\n request(frame: ClientRequestFrame): Promise<ServerFrame>;\n}\n\nexport interface OpenTrayClient {\n createSurface(options: SurfaceOptions): Promise<SurfaceHandle>;\n}\n\nexport interface SurfaceHandle {\n surface: SurfaceRef;\n createTray(options: TrayOptions): Promise<TrayHandle>;\n}\n\nexport interface TrayHandle {\n surface: SurfaceRef;\n trayId: TrayId;\n commandExtension(ext: string, data: unknown): Promise<void>;\n destroy(): Promise<void>;\n}\n\nexport const createInitFrame = (clientVersion: string): ClientFrame => ({\n type: \"init\",\n protocolVersion: PROTOCOL_VERSION,\n clientVersion,\n});\n\nexport interface CreateClientOptions {\n requestIdPrefix?: string;\n}\n\nexport const createClient = (\n transport: OpenTrayTransport,\n options: CreateClientOptions = {},\n): OpenTrayClient => {\n const nextRequestId = createRequestIdFactory(options.requestIdPrefix ?? \"opentray\");\n\n return {\n async createSurface(surfaceOptions): Promise<SurfaceHandle> {\n const requestId = nextRequestId();\n const response = await transport.request({\n type: \"create-surface\",\n requestId,\n ...surfaceOptions,\n });\n const surface = expectResponse(response, requestId, \"surface-created\").surface;\n return createSurfaceHandle(transport, surface, nextRequestId);\n },\n };\n};\n\nexport const createSurfaceHandle = (\n transport: OpenTrayTransport,\n surface: SurfaceRef,\n nextRequestId: () => RequestId = createRequestIdFactory(\"opentray\"),\n): SurfaceHandle => ({\n surface,\n async createTray(options: TrayOptions): Promise<TrayHandle> {\n const requestId = nextRequestId();\n const response = await transport.request({\n type: \"create-tray\",\n requestId,\n surface,\n tray: options,\n });\n const frame = expectResponse(response, requestId, \"tray-created\");\n return createTrayHandle(transport, surface, frame.trayId, nextRequestId);\n },\n});\n\nexport const createTrayHandle = (\n transport: OpenTrayTransport,\n surface: SurfaceRef,\n trayId: TrayId,\n nextRequestId: () => RequestId = createRequestIdFactory(\"opentray\"),\n): TrayHandle => ({\n surface,\n trayId,\n async commandExtension(ext: string, data: unknown): Promise<void> {\n const requestId = nextRequestId();\n const response = await transport.request({\n type: \"ext-command\",\n requestId,\n surfaceId: surface.surfaceId,\n trayId,\n ext,\n data,\n });\n expectResponse(response, requestId, \"ack\");\n },\n async destroy(): Promise<void> {\n const requestId = nextRequestId();\n const response = await transport.request({\n type: \"destroy-tray\",\n requestId,\n surfaceId: surface.surfaceId,\n trayId,\n });\n expectResponse(response, requestId, \"ack\");\n },\n});\n\nconst createRequestIdFactory = (prefix: string): (() => RequestId) => {\n let next = 1;\n return () => {\n const requestId = `${prefix}-${next}`;\n next += 1;\n return requestId;\n };\n};\n\nconst expectResponse = <TType extends ServerFrame[\"type\"]>(\n frame: ServerFrame,\n requestId: RequestId,\n type: TType,\n): Extract<ServerFrame, { type: TType }> => {\n if (frame.type === \"error\") {\n throw new Error(`${frame.code}: ${frame.message}`);\n }\n if (frame.type !== type) {\n throw new Error(`expected ${type} for ${requestId}, received ${frame.type}`);\n }\n if (requestIdOf(frame) !== requestId) {\n throw new Error(`expected response for ${requestId}, received ${requestIdOf(frame) ?? \"none\"}`);\n }\n return frame as Extract<ServerFrame, { type: TType }>;\n};\n\nconst requestIdOf = (frame: ServerFrame): RequestId | undefined => {\n switch (frame.type) {\n case \"surface-created\":\n case \"default-surface\":\n case \"tray-created\":\n case \"ack\":\n case \"daemon-health\":\n return frame.requestId;\n case \"error\":\n return frame.requestId;\n case \"ready\":\n case \"event\":\n case \"ext-event\":\n return undefined;\n }\n};\n"],"mappings":";;AAgCA,MAAa,mBAAmB,mBAAwC;CACtE,MAAM;CACN,iBAAiB;CACjB;AACF;AAMA,MAAa,gBACX,WACA,UAA+B,CAAC,MACb;CACnB,MAAM,gBAAgB,uBAAuB,QAAQ,mBAAmB,UAAU;CAElF,OAAO,EACL,MAAM,cAAc,gBAAwC;EAC1D,MAAM,YAAY,cAAc;EAMhC,MAAM,UAAU,eAAe,MALR,UAAU,QAAQ;GACvC,MAAM;GACN;GACA,GAAG;EACL,CAAC,GACwC,WAAW,iBAAiB,EAAE;EACvE,OAAO,oBAAoB,WAAW,SAAS,aAAa;CAC9D,EACF;AACF;AAEA,MAAa,uBACX,WACA,SACA,gBAAiC,uBAAuB,UAAU,OAC/C;CACnB;CACA,MAAM,WAAW,SAA2C;EAC1D,MAAM,YAAY,cAAc;EAQhC,OAAO,iBAAiB,WAAW,SADrB,eAAe,MANN,UAAU,QAAQ;GACvC,MAAM;GACN;GACA;GACA,MAAM;EACR,CAAC,GACsC,WAAW,cACF,EAAE,QAAQ,aAAa;CACzE;AACF;AAEA,MAAa,oBACX,WACA,SACA,QACA,gBAAiC,uBAAuB,UAAU,OAClD;CAChB;CACA;CACA,MAAM,iBAAiB,KAAa,MAA8B;EAChE,MAAM,YAAY,cAAc;EAShC,eAAe,MARQ,UAAU,QAAQ;GACvC,MAAM;GACN;GACA,WAAW,QAAQ;GACnB;GACA;GACA;EACF,CAAC,GACwB,WAAW,KAAK;CAC3C;CACA,MAAM,UAAyB;EAC7B,MAAM,YAAY,cAAc;EAOhC,eAAe,MANQ,UAAU,QAAQ;GACvC,MAAM;GACN;GACA,WAAW,QAAQ;GACnB;EACF,CAAC,GACwB,WAAW,KAAK;CAC3C;AACF;AAEA,MAAM,0BAA0B,WAAsC;CACpE,IAAI,OAAO;CACX,aAAa;EACX,MAAM,YAAY,GAAG,OAAO,GAAG;EAC/B,QAAQ;EACR,OAAO;CACT;AACF;AAEA,MAAM,kBACJ,OACA,WACA,SAC0C;CAC1C,IAAI,MAAM,SAAS,SACjB,MAAM,IAAI,MAAM,GAAG,MAAM,KAAK,IAAI,MAAM,SAAS;CAEnD,IAAI,MAAM,SAAS,MACjB,MAAM,IAAI,MAAM,YAAY,KAAK,OAAO,UAAU,aAAa,MAAM,MAAM;CAE7E,IAAI,YAAY,KAAK,MAAM,WACzB,MAAM,IAAI,MAAM,yBAAyB,UAAU,aAAa,YAAY,KAAK,KAAK,QAAQ;CAEhG,OAAO;AACT;AAEA,MAAM,eAAe,UAA8C;CACjE,QAAQ,MAAM,MAAd;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,iBACH,OAAO,MAAM;EACf,KAAK,SACH,OAAO,MAAM;EACf,KAAK;EACL,KAAK;EACL,KAAK,aACH;CACJ;AACF"}