opentray 0.5.1 → 0.6.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 +45 -19
- package/dist/cli.d.mts +0 -8
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +4 -527
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-h6gZdeOk.d.mts → client-DQn3DLZw.d.mts} +27 -2
- package/dist/client-DQn3DLZw.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +166 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{local-broker-CEIHPBcK.mjs → local-broker-C9siWyRh.mjs} +4 -3
- package/dist/local-broker-C9siWyRh.mjs.map +1 -0
- package/dist/node.d.mts +1 -1
- package/dist/node.mjs +1 -1
- package/package.json +8 -9
- package/assets/lynx-review/README.md +0 -14
- package/assets/lynx-review/main.lynx.bundle +0 -0
- package/dist/client-B_zxQOo5.mjs +0 -111
- package/dist/client-B_zxQOo5.mjs.map +0 -1
- package/dist/client-h6gZdeOk.d.mts.map +0 -1
- package/dist/local-broker-CEIHPBcK.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,511 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as
|
|
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-CEIHPBcK.mjs";
|
|
2
|
+
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-C9siWyRh.mjs";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
import { dirname, join, resolve } from "node:path";
|
|
8
|
-
//#region src/smoke/daemon-lynx.ts
|
|
9
|
-
const menuLabels$1 = new Map([
|
|
10
|
-
[1, "Show Window"],
|
|
11
|
-
[2, "Hide Window"],
|
|
12
|
-
[99, "Quit Smoke"]
|
|
13
|
-
]);
|
|
14
|
-
const DEFAULT_LYNX_BUNDLE_ENV = "OPENTRAY_LYNX_BUNDLE";
|
|
15
|
-
const DEFAULT_LYNX_HOST_FEATURES_ENV = "OPENTRAY_LYNX_HOST_FEATURES";
|
|
16
|
-
const DEFAULT_REVIEW_BUNDLE_RELATIVE_PATH = "assets/lynx-review/main.lynx.bundle";
|
|
17
|
-
const LYNX_HOST_FEATURES = [
|
|
18
|
-
"nativeWindowApi",
|
|
19
|
-
"bindWindowGlobals",
|
|
20
|
-
"nativeScreenApi",
|
|
21
|
-
"bindScreenGlobals",
|
|
22
|
-
"frameless"
|
|
23
|
-
];
|
|
24
|
-
const runDaemonLynxSmoke = async (options = {}) => {
|
|
25
|
-
const bundlePath = resolveLynxBundlePath(options.bundlePath);
|
|
26
|
-
const hostFeatures = resolveLynxHostFeatures(options.featureExpression);
|
|
27
|
-
if (process.env.OPENTRAY_LYNX_DEBUG) console.log(`lynx debug modes: ${process.env.OPENTRAY_LYNX_DEBUG}`);
|
|
28
|
-
if (process.env[DEFAULT_LYNX_HOST_FEATURES_ENV]) console.log(`lynx host features: ${process.env[DEFAULT_LYNX_HOST_FEATURES_ENV]}`);
|
|
29
|
-
if (process.env.OPENTRAY_LYNX_DEBUG_LOG_PATH) console.log(`lynx debug log path: ${process.env.OPENTRAY_LYNX_DEBUG_LOG_PATH}`);
|
|
30
|
-
const connection = await connectLocalBroker();
|
|
31
|
-
const client = createClient(connection, { requestIdPrefix: "daemon-lynx" });
|
|
32
|
-
console.log(`connected: endpoint=${connection.endpoint} session=${connection.sessionId}`);
|
|
33
|
-
const space = await client.createSpace({
|
|
34
|
-
id: "com.example.opentray.daemon-lynx",
|
|
35
|
-
title: "OpenTray Lynx Smoke",
|
|
36
|
-
default: true
|
|
37
|
-
});
|
|
38
|
-
console.log(`space: ${JSON.stringify(space.space)}`);
|
|
39
|
-
const tray = await space.createTray({
|
|
40
|
-
trayId: "daemon-lynx-status",
|
|
41
|
-
title: "OpenTray Lynx",
|
|
42
|
-
tooltip: {
|
|
43
|
-
title: "OpenTray Lynx",
|
|
44
|
-
description: "Generic extension smoke for a real Lynx runtime window"
|
|
45
|
-
},
|
|
46
|
-
icon: createVisibleIcon$1(),
|
|
47
|
-
menu: { items: [
|
|
48
|
-
{
|
|
49
|
-
type: "item",
|
|
50
|
-
id: 1,
|
|
51
|
-
title: "Show Window"
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
type: "item",
|
|
55
|
-
id: 2,
|
|
56
|
-
title: "Hide Window"
|
|
57
|
-
},
|
|
58
|
-
{ type: "separator" },
|
|
59
|
-
{
|
|
60
|
-
type: "item",
|
|
61
|
-
id: 99,
|
|
62
|
-
title: "Quit Smoke"
|
|
63
|
-
}
|
|
64
|
-
] }
|
|
65
|
-
});
|
|
66
|
-
console.log(`tray: ${tray.trayId}`);
|
|
67
|
-
await connection.request({
|
|
68
|
-
type: "load-ext",
|
|
69
|
-
requestId: "daemon-lynx-load",
|
|
70
|
-
spaceId: space.space.spaceId,
|
|
71
|
-
name: "lynx",
|
|
72
|
-
path: "@opentray/ext-lynx"
|
|
73
|
-
});
|
|
74
|
-
console.log("lynx extension requested through the generic load-ext path");
|
|
75
|
-
let closed = false;
|
|
76
|
-
let exitTimer;
|
|
77
|
-
let resolveClosed;
|
|
78
|
-
const show = async () => {
|
|
79
|
-
const command = createLynxShowCommand({
|
|
80
|
-
bundlePath,
|
|
81
|
-
hostFeatures
|
|
82
|
-
});
|
|
83
|
-
await tray.commandExtension("lynx", command);
|
|
84
|
-
console.log(`lynx command: show features=${describeLynxHostFeatures(hostFeatures)} bundle=${bundlePath}`);
|
|
85
|
-
};
|
|
86
|
-
const hide = async () => {
|
|
87
|
-
await tray.commandExtension("lynx", { type: "hide" });
|
|
88
|
-
console.log("lynx command: hide");
|
|
89
|
-
};
|
|
90
|
-
const shutdown = async () => {
|
|
91
|
-
if (closed) return;
|
|
92
|
-
closed = true;
|
|
93
|
-
if (exitTimer !== void 0) clearTimeout(exitTimer);
|
|
94
|
-
process.off("SIGINT", onSignal);
|
|
95
|
-
process.off("SIGTERM", onSignal);
|
|
96
|
-
try {
|
|
97
|
-
await hide();
|
|
98
|
-
} catch (error) {
|
|
99
|
-
console.error(`lynx hide during shutdown failed: ${String(error)}`);
|
|
100
|
-
}
|
|
101
|
-
await connection.close();
|
|
102
|
-
resolveClosed?.();
|
|
103
|
-
};
|
|
104
|
-
const handleMenuClick = async (itemId) => {
|
|
105
|
-
switch (itemId) {
|
|
106
|
-
case 1:
|
|
107
|
-
await show();
|
|
108
|
-
return;
|
|
109
|
-
case 2:
|
|
110
|
-
await hide();
|
|
111
|
-
return;
|
|
112
|
-
case 99:
|
|
113
|
-
console.log("quit item routed; closing lynx smoke connection");
|
|
114
|
-
await shutdown();
|
|
115
|
-
return;
|
|
116
|
-
default: return;
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
connection.onEvent((frame) => {
|
|
120
|
-
console.log(`broker -> client ${JSON.stringify(frame)}`);
|
|
121
|
-
if (frame.type === "event" && frame.event.type === "menuClick") {
|
|
122
|
-
console.log(`menu click: ${menuLabels$1.get(frame.event.itemId) ?? frame.event.itemId}`);
|
|
123
|
-
handleMenuClick(frame.event.itemId);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (frame.type === "ext-event" && frame.ext === "lynx") console.log(`lynx event: ${JSON.stringify(frame.data)}`);
|
|
127
|
-
});
|
|
128
|
-
const onSignal = () => {
|
|
129
|
-
shutdown();
|
|
130
|
-
};
|
|
131
|
-
process.on("SIGINT", onSignal);
|
|
132
|
-
process.on("SIGTERM", onSignal);
|
|
133
|
-
const exitAfter = process.env.OPENTRAY_EXAMPLE_EXIT_AFTER_MS;
|
|
134
|
-
if (exitAfter !== void 0 && exitAfter.length > 0) {
|
|
135
|
-
const duration = Number.parseInt(exitAfter, 10);
|
|
136
|
-
if (Number.isInteger(duration) && duration > 0) exitTimer = setTimeout(() => {
|
|
137
|
-
shutdown();
|
|
138
|
-
}, duration);
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
await show();
|
|
142
|
-
console.log(`use the tray menu to re-show or hide the window, or press Ctrl+C to exit; features=${describeLynxHostFeatures(hostFeatures)}`);
|
|
143
|
-
} catch (error) {
|
|
144
|
-
await connection.close();
|
|
145
|
-
process.off("SIGINT", onSignal);
|
|
146
|
-
process.off("SIGTERM", onSignal);
|
|
147
|
-
throw error;
|
|
148
|
-
}
|
|
149
|
-
await new Promise((resolve) => {
|
|
150
|
-
resolveClosed = resolve;
|
|
151
|
-
});
|
|
152
|
-
};
|
|
153
|
-
const resolveLynxBundlePath = (value) => {
|
|
154
|
-
const resolved = value ?? process.env[DEFAULT_LYNX_BUNDLE_ENV];
|
|
155
|
-
if (resolved === void 0 || resolved.length === 0) {
|
|
156
|
-
const bundledReviewBundle = resolveBundledReviewBundlePath();
|
|
157
|
-
if (!existsSync(bundledReviewBundle)) throw new Error(`lynx smoke requires --bundle <path>, ${DEFAULT_LYNX_BUNDLE_ENV}=<path-to-main.lynx.bundle>, or a packaged review bundle at ${bundledReviewBundle}`);
|
|
158
|
-
return realpathSync(bundledReviewBundle);
|
|
159
|
-
}
|
|
160
|
-
const absolutePath = resolve(process.env.INIT_CWD ?? process.cwd(), resolved);
|
|
161
|
-
if (!existsSync(absolutePath)) throw new Error(`lynx smoke bundle does not exist: ${absolutePath}`);
|
|
162
|
-
return realpathSync(absolutePath);
|
|
163
|
-
};
|
|
164
|
-
const resolveLynxHostFeatures = (expression = process.env[DEFAULT_LYNX_HOST_FEATURES_ENV] ?? "") => {
|
|
165
|
-
const features = createEmptyLynxHostFeatures();
|
|
166
|
-
for (const rawToken of expression.split(",")) {
|
|
167
|
-
const token = rawToken.trim();
|
|
168
|
-
if (token.length === 0) continue;
|
|
169
|
-
applyLynxHostFeatureToken(features, token);
|
|
170
|
-
}
|
|
171
|
-
return features;
|
|
172
|
-
};
|
|
173
|
-
const createLynxShowCommand = ({ bundlePath, hostFeatures }) => {
|
|
174
|
-
const command = {
|
|
175
|
-
type: "show",
|
|
176
|
-
bundlePath,
|
|
177
|
-
width: 720,
|
|
178
|
-
height: 420,
|
|
179
|
-
minWidth: 320,
|
|
180
|
-
minHeight: 220,
|
|
181
|
-
maxWidth: 960,
|
|
182
|
-
maxHeight: 720,
|
|
183
|
-
nativeWindowApi: hostFeatures.nativeWindowApi,
|
|
184
|
-
bindWindowGlobals: hostFeatures.bindWindowGlobals,
|
|
185
|
-
nativeScreenApi: hostFeatures.nativeScreenApi,
|
|
186
|
-
bindScreenGlobals: hostFeatures.bindScreenGlobals,
|
|
187
|
-
title: `OpenTray Lynx Smoke (${describeLynxHostFeatures(hostFeatures)})`,
|
|
188
|
-
icon: createVisibleIcon$1()
|
|
189
|
-
};
|
|
190
|
-
if (hostFeatures.frameless) command.style = { frameless: true };
|
|
191
|
-
return command;
|
|
192
|
-
};
|
|
193
|
-
const describeLynxHostFeatures = (features) => {
|
|
194
|
-
const enabled = LYNX_HOST_FEATURES.filter((feature) => features[feature]);
|
|
195
|
-
return enabled.length === 0 ? "baseline" : enabled.join(",");
|
|
196
|
-
};
|
|
197
|
-
const resolveBundledReviewBundlePath = (moduleUrl = import.meta.url) => {
|
|
198
|
-
let currentDir = dirname(fileURLToPath(moduleUrl));
|
|
199
|
-
while (true) {
|
|
200
|
-
if (existsSync(join(currentDir, "package.json"))) return join(currentDir, DEFAULT_REVIEW_BUNDLE_RELATIVE_PATH);
|
|
201
|
-
const parent = dirname(currentDir);
|
|
202
|
-
if (parent === currentDir) throw new Error(`failed to resolve opentray package root from ${moduleUrl}`);
|
|
203
|
-
currentDir = parent;
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
function createEmptyLynxHostFeatures() {
|
|
207
|
-
return {
|
|
208
|
-
nativeWindowApi: false,
|
|
209
|
-
bindWindowGlobals: false,
|
|
210
|
-
nativeScreenApi: false,
|
|
211
|
-
bindScreenGlobals: false,
|
|
212
|
-
frameless: false
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
function applyLynxHostFeatureToken(features, rawToken) {
|
|
216
|
-
const disabled = rawToken.startsWith("!");
|
|
217
|
-
const token = disabled ? rawToken.slice(1) : rawToken;
|
|
218
|
-
if (token === "*" || token === "full") {
|
|
219
|
-
for (const feature of LYNX_HOST_FEATURES) features[feature] = !disabled;
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
if (token === "baseline") {
|
|
223
|
-
for (const feature of LYNX_HOST_FEATURES) features[feature] = false;
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
if (!isLynxHostFeature(token)) throw new Error(`unsupported Lynx host feature token: ${rawToken}; supported tokens: baseline, full, *, !<feature>, ${LYNX_HOST_FEATURES.join(", ")}`);
|
|
227
|
-
features[token] = !disabled;
|
|
228
|
-
if (token === "nativeWindowApi" && disabled) {
|
|
229
|
-
features.bindWindowGlobals = false;
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (token === "nativeScreenApi" && disabled) {
|
|
233
|
-
features.bindScreenGlobals = false;
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
if (token === "bindWindowGlobals" && !disabled) {
|
|
237
|
-
features.nativeWindowApi = true;
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
if (token === "bindScreenGlobals" && !disabled) features.nativeScreenApi = true;
|
|
241
|
-
}
|
|
242
|
-
function isLynxHostFeature(value) {
|
|
243
|
-
return LYNX_HOST_FEATURES.includes(value);
|
|
244
|
-
}
|
|
245
|
-
function createVisibleIcon$1() {
|
|
246
|
-
const width = 32;
|
|
247
|
-
const height = 32;
|
|
248
|
-
const data = [];
|
|
249
|
-
for (let y = 0; y < height; y += 1) for (let x = 0; x < width; x += 1) {
|
|
250
|
-
const inStroke = x <= 5 || x >= 13 && x <= 18 || y >= 26;
|
|
251
|
-
const inDiagonal = y >= x + 10 && x <= 18;
|
|
252
|
-
const alpha = inStroke || inDiagonal ? 255 : 0;
|
|
253
|
-
data.push(34, 92, 56, alpha);
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
type: "rgba",
|
|
257
|
-
width,
|
|
258
|
-
height,
|
|
259
|
-
data
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
//#endregion
|
|
263
|
-
//#region src/smoke/daemon-tray.ts
|
|
264
|
-
const menuLabels = new Map([[1, "Open WebView"]]);
|
|
265
|
-
const runDaemonTraySmoke = async () => {
|
|
266
|
-
const connection = await connectLocalBroker();
|
|
267
|
-
const client = createClient(connection, { requestIdPrefix: "daemon-smoke" });
|
|
268
|
-
console.log(`connected: endpoint=${connection.endpoint} session=${connection.sessionId}`);
|
|
269
|
-
const space = await client.createSpace({
|
|
270
|
-
id: "com.example.opentray.daemon-smoke",
|
|
271
|
-
title: "OpenTray Daemon Smoke",
|
|
272
|
-
default: true
|
|
273
|
-
});
|
|
274
|
-
console.log(`space: ${JSON.stringify(space.space)}`);
|
|
275
|
-
const tray = await space.createTray({
|
|
276
|
-
trayId: "daemon-smoke-status",
|
|
277
|
-
title: "OpenTray",
|
|
278
|
-
tooltip: {
|
|
279
|
-
title: "OpenTray",
|
|
280
|
-
description: "Single primary tray action; macOS direct-triggers without opening a menu"
|
|
281
|
-
},
|
|
282
|
-
icon: createVisibleIcon(),
|
|
283
|
-
menu: { items: [{
|
|
284
|
-
type: "item",
|
|
285
|
-
id: 1,
|
|
286
|
-
title: "Open WebView",
|
|
287
|
-
primaryEvent: true
|
|
288
|
-
}] }
|
|
289
|
-
});
|
|
290
|
-
console.log(`tray: ${tray.trayId}`);
|
|
291
|
-
await connection.request({
|
|
292
|
-
type: "load-ext",
|
|
293
|
-
requestId: "daemon-smoke-load-webview",
|
|
294
|
-
spaceId: space.space.spaceId,
|
|
295
|
-
name: "webview",
|
|
296
|
-
path: "@opentray/ext-webview"
|
|
297
|
-
});
|
|
298
|
-
console.log("webview extension requested through the generic load-ext path");
|
|
299
|
-
console.log("click the tray icon: macOS should direct-trigger the single primary action without opening a menu");
|
|
300
|
-
let closed = false;
|
|
301
|
-
let exitTimer;
|
|
302
|
-
const shutdown = async () => {
|
|
303
|
-
if (closed) return;
|
|
304
|
-
closed = true;
|
|
305
|
-
if (exitTimer !== void 0) clearTimeout(exitTimer);
|
|
306
|
-
await connection.close();
|
|
307
|
-
};
|
|
308
|
-
const commandWebview = async (command) => {
|
|
309
|
-
await tray.commandExtension("webview", command);
|
|
310
|
-
};
|
|
311
|
-
const handleMenuClick = async (itemId) => {
|
|
312
|
-
if (itemId === 1) {
|
|
313
|
-
const trayBounds = await tray.getBounds();
|
|
314
|
-
console.log(`tray bounds: ${JSON.stringify(trayBounds)}`);
|
|
315
|
-
await commandWebview({
|
|
316
|
-
type: "show",
|
|
317
|
-
html: createWebviewDemoHtml(),
|
|
318
|
-
width: 420,
|
|
319
|
-
height: 260,
|
|
320
|
-
title: "OpenTray WebView Smoke",
|
|
321
|
-
icon: {
|
|
322
|
-
type: "href",
|
|
323
|
-
href: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="
|
|
324
|
-
},
|
|
325
|
-
style: {
|
|
326
|
-
frameless: true,
|
|
327
|
-
transparent: true,
|
|
328
|
-
keepOnTop: true,
|
|
329
|
-
platform: { macos: {
|
|
330
|
-
material: "hudWindow",
|
|
331
|
-
materialState: "active"
|
|
332
|
-
} }
|
|
333
|
-
},
|
|
334
|
-
fallbackRect: trayBounds.rect ?? {
|
|
335
|
-
x: 0,
|
|
336
|
-
y: 0,
|
|
337
|
-
width: 1,
|
|
338
|
-
height: 1
|
|
339
|
-
},
|
|
340
|
-
nativeWindowApi: true,
|
|
341
|
-
bindWindowGlobals: true,
|
|
342
|
-
nativeScreenApi: true,
|
|
343
|
-
bindScreenGlobals: true,
|
|
344
|
-
nativeTrayApi: true,
|
|
345
|
-
titleSync: {
|
|
346
|
-
documentToWindow: true,
|
|
347
|
-
windowToDocument: true
|
|
348
|
-
},
|
|
349
|
-
iconSync: true,
|
|
350
|
-
nativeApiPolicy: {
|
|
351
|
-
defaultSrc: ["'local'"],
|
|
352
|
-
window: ["https://example.com"],
|
|
353
|
-
screen: ["https://example.com"],
|
|
354
|
-
tray: ["https://example.com"],
|
|
355
|
-
titleSync: ["https://example.com"],
|
|
356
|
-
iconSync: ["https://example.com"]
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
console.log("primary tray action: webview show");
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
connection.onEvent((frame) => {
|
|
364
|
-
console.log(`broker -> client ${JSON.stringify(frame)}`);
|
|
365
|
-
if (frame.type === "event" && frame.event.type === "menuClick") {
|
|
366
|
-
console.log(`menu click: ${menuLabels.get(frame.event.itemId) ?? frame.event.itemId}`);
|
|
367
|
-
handleMenuClick(frame.event.itemId);
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
const exitAfter = process.env.OPENTRAY_EXAMPLE_EXIT_AFTER_MS;
|
|
371
|
-
if (exitAfter !== void 0 && exitAfter.length > 0) {
|
|
372
|
-
const duration = Number.parseInt(exitAfter, 10);
|
|
373
|
-
if (Number.isInteger(duration) && duration > 0) exitTimer = setTimeout(() => {
|
|
374
|
-
shutdown();
|
|
375
|
-
}, duration);
|
|
376
|
-
}
|
|
377
|
-
const webviewSmoke = process.env.OPENTRAY_EXAMPLE_WEBVIEW_SMOKE;
|
|
378
|
-
try {
|
|
379
|
-
if (webviewSmoke === "show" || webviewSmoke === "1") await handleMenuClick(1);
|
|
380
|
-
if (webviewSmoke === "1") {
|
|
381
|
-
if (exitTimer !== void 0) {
|
|
382
|
-
clearTimeout(exitTimer);
|
|
383
|
-
exitTimer = void 0;
|
|
384
|
-
}
|
|
385
|
-
await sleep(300);
|
|
386
|
-
await shutdown();
|
|
387
|
-
}
|
|
388
|
-
} catch (error) {
|
|
389
|
-
await shutdown();
|
|
390
|
-
throw error;
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
function createVisibleIcon() {
|
|
394
|
-
const width = 32;
|
|
395
|
-
const height = 32;
|
|
396
|
-
const data = [];
|
|
397
|
-
for (let y = 0; y < height; y += 1) for (let x = 0; x < width; x += 1) {
|
|
398
|
-
const dx = x - 15.5;
|
|
399
|
-
const dy = y - 15.5;
|
|
400
|
-
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
401
|
-
const inRing = distance >= 9 && distance <= 14;
|
|
402
|
-
const inCore = distance <= 5;
|
|
403
|
-
const inNeedle = Math.abs(dx + dy) <= 1.2 && x >= 9 && x <= 23 && y >= 9 && y <= 23;
|
|
404
|
-
if (inRing || inCore || inNeedle) data.push(inCore ? 255 : 24, inNeedle ? 240 : 132, inRing ? 72 : 96, 255);
|
|
405
|
-
else data.push(0, 0, 0, 0);
|
|
406
|
-
}
|
|
407
|
-
return {
|
|
408
|
-
type: "rgba",
|
|
409
|
-
width,
|
|
410
|
-
height,
|
|
411
|
-
data
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
function createWebviewDemoHtml() {
|
|
415
|
-
return `<!doctype html>
|
|
416
|
-
<html lang="en">
|
|
417
|
-
<head>
|
|
418
|
-
<meta charset="utf-8" />
|
|
419
|
-
<title>OpenTray WebView</title>
|
|
420
|
-
<style>
|
|
421
|
-
:root {
|
|
422
|
-
color: #18220f;
|
|
423
|
-
background: #f6edd8;
|
|
424
|
-
font: 15px ui-rounded, "SF Pro Rounded", "Avenir Next", sans-serif;
|
|
425
|
-
}
|
|
426
|
-
body {
|
|
427
|
-
margin: 0;
|
|
428
|
-
}
|
|
429
|
-
main {
|
|
430
|
-
min-height: 100vh;
|
|
431
|
-
box-sizing: border-box;
|
|
432
|
-
padding: 22px;
|
|
433
|
-
background:
|
|
434
|
-
radial-gradient(circle at 84% 10%, rgba(34, 132, 96, 0.22), transparent 34%),
|
|
435
|
-
linear-gradient(135deg, #fff8e7 0%, #e9f0d8 100%);
|
|
436
|
-
}
|
|
437
|
-
h1 {
|
|
438
|
-
margin: 0 0 8px;
|
|
439
|
-
font-size: 24px;
|
|
440
|
-
letter-spacing: -0.04em;
|
|
441
|
-
}
|
|
442
|
-
p {
|
|
443
|
-
margin: 0 0 16px;
|
|
444
|
-
color: #526044;
|
|
445
|
-
}
|
|
446
|
-
section {
|
|
447
|
-
display: grid;
|
|
448
|
-
gap: 10px;
|
|
449
|
-
}
|
|
450
|
-
.card {
|
|
451
|
-
border: 1px solid rgba(24, 34, 15, 0.16);
|
|
452
|
-
border-radius: 14px;
|
|
453
|
-
padding: 12px;
|
|
454
|
-
background: rgba(255, 255, 255, 0.72);
|
|
455
|
-
box-shadow: 0 12px 30px rgba(56, 72, 36, 0.12);
|
|
456
|
-
}
|
|
457
|
-
.label {
|
|
458
|
-
margin-bottom: 5px;
|
|
459
|
-
color: #7b5b1d;
|
|
460
|
-
font-size: 12px;
|
|
461
|
-
font-weight: 700;
|
|
462
|
-
letter-spacing: 0.08em;
|
|
463
|
-
text-transform: uppercase;
|
|
464
|
-
}
|
|
465
|
-
code {
|
|
466
|
-
word-break: break-word;
|
|
467
|
-
}
|
|
468
|
-
</style>
|
|
469
|
-
</head>
|
|
470
|
-
<body>
|
|
471
|
-
<main>
|
|
472
|
-
<h1>OpenTray WebView</h1>
|
|
473
|
-
<p>This window was opened by the single primary tray action.</p>
|
|
474
|
-
<section>
|
|
475
|
-
<div class="card">
|
|
476
|
-
<div class="label">primary event</div>
|
|
477
|
-
<code>macOS single primary item direct-triggered menuClick -> WebView show</code>
|
|
478
|
-
</div>
|
|
479
|
-
</section>
|
|
480
|
-
</main>
|
|
481
|
-
</body>
|
|
482
|
-
</html>`;
|
|
483
|
-
}
|
|
484
|
-
function sleep(ms) {
|
|
485
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
486
|
-
}
|
|
487
|
-
//#endregion
|
|
5
|
+
import { realpathSync } from "node:fs";
|
|
488
6
|
//#region src/cli.ts
|
|
489
7
|
const packageJsonUrl = new URL("../package.json", import.meta.url);
|
|
490
8
|
const cliModulePath = fileURLToPath(import.meta.url);
|
|
491
9
|
const parseCliCommand = (argv) => {
|
|
492
|
-
const [group, action
|
|
493
|
-
if (group !== "daemon") {
|
|
494
|
-
if (group === "smoke" && action === "daemon-tray") return {
|
|
495
|
-
type: "smoke",
|
|
496
|
-
name: "daemon-tray"
|
|
497
|
-
};
|
|
498
|
-
if (group === "smoke" && action === "daemon-lynx") {
|
|
499
|
-
const smokeOptions = parseSmokeOptions(rest);
|
|
500
|
-
return {
|
|
501
|
-
type: "smoke",
|
|
502
|
-
name: "daemon-lynx",
|
|
503
|
-
...smokeOptions.bundlePath ? { bundlePath: smokeOptions.bundlePath } : {},
|
|
504
|
-
...smokeOptions.featureExpression ? { featureExpression: smokeOptions.featureExpression } : {}
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
return { type: "help" };
|
|
508
|
-
}
|
|
10
|
+
const [group, action] = argv;
|
|
11
|
+
if (group !== "daemon") return { type: "help" };
|
|
509
12
|
if (action === "start" || action === "stop" || action === "restart" || action === "health") return {
|
|
510
13
|
type: "daemon",
|
|
511
14
|
action
|
|
@@ -523,14 +26,6 @@ const runCli = async (argv) => {
|
|
|
523
26
|
printHelp();
|
|
524
27
|
return 1;
|
|
525
28
|
}
|
|
526
|
-
if (command.type === "smoke") {
|
|
527
|
-
if (command.name === "daemon-tray") await runDaemonTraySmoke();
|
|
528
|
-
else await runDaemonLynxSmoke({
|
|
529
|
-
...command.bundlePath === void 0 ? {} : { bundlePath: command.bundlePath },
|
|
530
|
-
...command.featureExpression === void 0 ? {} : { featureExpression: command.featureExpression }
|
|
531
|
-
});
|
|
532
|
-
return 0;
|
|
533
|
-
}
|
|
534
29
|
const driver = createNodeDaemonDriver(cliModulePath);
|
|
535
30
|
if (command.action === "start") {
|
|
536
31
|
const result = await startDaemon({
|
|
@@ -587,9 +82,6 @@ const runCli = async (argv) => {
|
|
|
587
82
|
};
|
|
588
83
|
const printHelp = () => {
|
|
589
84
|
console.error("Usage: opentray daemon <start|stop|restart|health>");
|
|
590
|
-
console.error(" opentray smoke daemon-tray");
|
|
591
|
-
console.error(" opentray smoke daemon-lynx [--bundle <path-to-main.lynx.bundle>] [--features <baseline|full|*,!feature,...>]");
|
|
592
|
-
console.error(" default Lynx smoke uses the packaged review bundle");
|
|
593
85
|
};
|
|
594
86
|
const formatDaemonHealthOutput = (health) => {
|
|
595
87
|
const lines = [
|
|
@@ -614,21 +106,6 @@ const isCliEntrypoint = (argvEntryPath, modulePath) => {
|
|
|
614
106
|
return argvEntryPath === modulePath;
|
|
615
107
|
}
|
|
616
108
|
};
|
|
617
|
-
function parseSmokeOptions(argv) {
|
|
618
|
-
let bundlePath;
|
|
619
|
-
let featureExpression;
|
|
620
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
621
|
-
if (argv[index] === "--bundle") {
|
|
622
|
-
bundlePath = argv[index + 1];
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
if (argv[index] === "--features") featureExpression = argv[index + 1];
|
|
626
|
-
}
|
|
627
|
-
return {
|
|
628
|
-
...bundlePath === void 0 ? {} : { bundlePath },
|
|
629
|
-
...featureExpression === void 0 ? {} : { featureExpression }
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
109
|
if (isCliEntrypoint(process.argv[1], cliModulePath)) runCli(process.argv.slice(2)).then((code) => {
|
|
633
110
|
process.exitCode = code;
|
|
634
111
|
});
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":["menuLabels","createVisibleIcon"],"sources":["../src/smoke/daemon-lynx.ts","../src/smoke/daemon-tray.ts","../src/cli.ts"],"sourcesContent":["import { existsSync, realpathSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { createClient } from \"../client\";\nimport { connectLocalBroker } from \"../local-broker\";\n\nconst menuLabels = new Map<number, string>([\n [1, \"Show Window\"],\n [2, \"Hide Window\"],\n [99, \"Quit Smoke\"],\n]);\n\nconst DEFAULT_LYNX_BUNDLE_ENV = \"OPENTRAY_LYNX_BUNDLE\";\nconst DEFAULT_LYNX_HOST_FEATURES_ENV = \"OPENTRAY_LYNX_HOST_FEATURES\";\nconst DEFAULT_REVIEW_BUNDLE_RELATIVE_PATH =\n \"assets/lynx-review/main.lynx.bundle\";\nconst LYNX_HOST_FEATURES = [\n \"nativeWindowApi\",\n \"bindWindowGlobals\",\n \"nativeScreenApi\",\n \"bindScreenGlobals\",\n \"frameless\",\n] as const;\n\nexport type LynxHostFeature = (typeof LYNX_HOST_FEATURES)[number];\n\nexport interface LynxHostFeatureState {\n nativeWindowApi: boolean;\n bindWindowGlobals: boolean;\n nativeScreenApi: boolean;\n bindScreenGlobals: boolean;\n frameless: boolean;\n}\n\ninterface LynxSmokeShowCommand {\n type: \"show\";\n bundlePath: string;\n width: number;\n height: number;\n minWidth: number;\n minHeight: number;\n maxWidth: number;\n maxHeight: number;\n nativeWindowApi: boolean;\n bindWindowGlobals: boolean;\n nativeScreenApi: boolean;\n bindScreenGlobals: boolean;\n title: string;\n icon: ReturnType<typeof createVisibleIcon>;\n style?: {\n frameless: boolean;\n };\n}\n\nexport interface DaemonLynxSmokeOptions {\n bundlePath?: string;\n featureExpression?: string;\n}\n\nexport const runDaemonLynxSmoke = async (\n options: DaemonLynxSmokeOptions = {},\n): Promise<void> => {\n const bundlePath = resolveLynxBundlePath(options.bundlePath);\n const hostFeatures = resolveLynxHostFeatures(options.featureExpression);\n if (process.env.OPENTRAY_LYNX_DEBUG) {\n console.log(`lynx debug modes: ${process.env.OPENTRAY_LYNX_DEBUG}`);\n }\n if (process.env[DEFAULT_LYNX_HOST_FEATURES_ENV]) {\n console.log(\n `lynx host features: ${process.env[DEFAULT_LYNX_HOST_FEATURES_ENV]}`,\n );\n }\n if (process.env.OPENTRAY_LYNX_DEBUG_LOG_PATH) {\n console.log(\n `lynx debug log path: ${process.env.OPENTRAY_LYNX_DEBUG_LOG_PATH}`,\n );\n }\n const connection = await connectLocalBroker();\n const client = createClient(connection, { requestIdPrefix: \"daemon-lynx\" });\n console.log(\n `connected: endpoint=${connection.endpoint} session=${connection.sessionId}`,\n );\n\n const space = await client.createSpace({\n id: \"com.example.opentray.daemon-lynx\",\n title: \"OpenTray Lynx Smoke\",\n default: true,\n });\n console.log(`space: ${JSON.stringify(space.space)}`);\n\n const tray = await space.createTray({\n trayId: \"daemon-lynx-status\",\n title: \"OpenTray Lynx\",\n tooltip: {\n title: \"OpenTray Lynx\",\n description: \"Generic extension smoke for a real Lynx runtime window\",\n },\n icon: createVisibleIcon(),\n menu: {\n items: [\n { type: \"item\", id: 1, title: \"Show Window\" },\n { type: \"item\", id: 2, title: \"Hide Window\" },\n { type: \"separator\" },\n { type: \"item\", id: 99, title: \"Quit Smoke\" },\n ],\n },\n });\n console.log(`tray: ${tray.trayId}`);\n\n await connection.request({\n type: \"load-ext\",\n requestId: \"daemon-lynx-load\",\n spaceId: space.space.spaceId,\n name: \"lynx\",\n path: \"@opentray/ext-lynx\",\n });\n console.log(\"lynx extension requested through the generic load-ext path\");\n\n let closed = false;\n let exitTimer: NodeJS.Timeout | undefined;\n let resolveClosed: (() => void) | undefined;\n\n const show = async (): Promise<void> => {\n const command = createLynxShowCommand({\n bundlePath,\n hostFeatures,\n });\n await tray.commandExtension(\"lynx\", command);\n console.log(\n `lynx command: show features=${describeLynxHostFeatures(\n hostFeatures,\n )} bundle=${bundlePath}`,\n );\n };\n\n const hide = async (): Promise<void> => {\n await tray.commandExtension(\"lynx\", { type: \"hide\" });\n console.log(\"lynx command: hide\");\n };\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 process.off(\"SIGINT\", onSignal);\n process.off(\"SIGTERM\", onSignal);\n try {\n await hide();\n } catch (error) {\n console.error(`lynx hide during shutdown failed: ${String(error)}`);\n }\n await connection.close();\n resolveClosed?.();\n };\n\n const handleMenuClick = async (itemId: number): Promise<void> => {\n switch (itemId) {\n case 1:\n await show();\n return;\n case 2:\n await hide();\n return;\n case 99:\n console.log(\"quit item routed; closing lynx smoke connection\");\n await shutdown();\n return;\n default:\n return;\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(\n `menu click: ${menuLabels.get(frame.event.itemId) ?? frame.event.itemId}`,\n );\n void handleMenuClick(frame.event.itemId);\n return;\n }\n if (frame.type === \"ext-event\" && frame.ext === \"lynx\") {\n console.log(`lynx event: ${JSON.stringify(frame.data)}`);\n }\n });\n\n const onSignal = (): void => {\n void shutdown();\n };\n process.on(\"SIGINT\", onSignal);\n process.on(\"SIGTERM\", onSignal);\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 try {\n await show();\n console.log(\n `use the tray menu to re-show or hide the window, or press Ctrl+C to exit; features=${describeLynxHostFeatures(\n hostFeatures,\n )}`,\n );\n } catch (error) {\n await connection.close();\n process.off(\"SIGINT\", onSignal);\n process.off(\"SIGTERM\", onSignal);\n throw error;\n }\n\n await new Promise<void>((resolve) => {\n resolveClosed = resolve;\n });\n};\n\nexport const resolveLynxBundlePath = (value?: string): string => {\n const resolved = value ?? process.env[DEFAULT_LYNX_BUNDLE_ENV];\n if (resolved === undefined || resolved.length === 0) {\n const bundledReviewBundle = resolveBundledReviewBundlePath();\n if (!existsSync(bundledReviewBundle)) {\n throw new Error(\n `lynx smoke requires --bundle <path>, ${DEFAULT_LYNX_BUNDLE_ENV}=<path-to-main.lynx.bundle>, or a packaged review bundle at ${bundledReviewBundle}`,\n );\n }\n return realpathSync(bundledReviewBundle);\n }\n const baseDir = process.env.INIT_CWD ?? process.cwd();\n const absolutePath = resolve(baseDir, resolved);\n if (!existsSync(absolutePath)) {\n throw new Error(`lynx smoke bundle does not exist: ${absolutePath}`);\n }\n return realpathSync(absolutePath);\n};\n\nexport const resolveLynxHostFeatures = (\n expression = process.env[DEFAULT_LYNX_HOST_FEATURES_ENV] ?? \"\",\n): LynxHostFeatureState => {\n const features = createEmptyLynxHostFeatures();\n for (const rawToken of expression.split(\",\")) {\n const token = rawToken.trim();\n if (token.length === 0) {\n continue;\n }\n applyLynxHostFeatureToken(features, token);\n }\n return features;\n};\n\nexport const createLynxShowCommand = ({\n bundlePath,\n hostFeatures,\n}: {\n bundlePath: string;\n hostFeatures: LynxHostFeatureState;\n}): LynxSmokeShowCommand => {\n const command: LynxSmokeShowCommand = {\n type: \"show\",\n bundlePath,\n width: 720,\n height: 420,\n minWidth: 320,\n minHeight: 220,\n maxWidth: 960,\n maxHeight: 720,\n nativeWindowApi: hostFeatures.nativeWindowApi,\n bindWindowGlobals: hostFeatures.bindWindowGlobals,\n nativeScreenApi: hostFeatures.nativeScreenApi,\n bindScreenGlobals: hostFeatures.bindScreenGlobals,\n title: `OpenTray Lynx Smoke (${describeLynxHostFeatures(hostFeatures)})`,\n icon: createVisibleIcon(),\n };\n if (hostFeatures.frameless) {\n command.style = { frameless: true };\n }\n return command;\n};\n\nexport const describeLynxHostFeatures = (\n features: LynxHostFeatureState,\n): string => {\n const enabled = LYNX_HOST_FEATURES.filter((feature) => features[feature]);\n return enabled.length === 0 ? \"baseline\" : enabled.join(\",\");\n};\n\nexport const resolveBundledReviewBundlePath = (\n moduleUrl = import.meta.url,\n): string => {\n let currentDir = dirname(fileURLToPath(moduleUrl));\n\n while (true) {\n const packageJson = join(currentDir, \"package.json\");\n if (existsSync(packageJson)) {\n return join(currentDir, DEFAULT_REVIEW_BUNDLE_RELATIVE_PATH);\n }\n\n const parent = dirname(currentDir);\n if (parent === currentDir) {\n throw new Error(`failed to resolve opentray package root from ${moduleUrl}`);\n }\n currentDir = parent;\n }\n};\n\nfunction createEmptyLynxHostFeatures(): LynxHostFeatureState {\n return {\n nativeWindowApi: false,\n bindWindowGlobals: false,\n nativeScreenApi: false,\n bindScreenGlobals: false,\n frameless: false,\n };\n}\n\nfunction applyLynxHostFeatureToken(\n features: LynxHostFeatureState,\n rawToken: string,\n): void {\n const disabled = rawToken.startsWith(\"!\");\n const token = disabled ? rawToken.slice(1) : rawToken;\n\n if (token === \"*\" || token === \"full\") {\n for (const feature of LYNX_HOST_FEATURES) {\n features[feature] = !disabled;\n }\n return;\n }\n if (token === \"baseline\") {\n for (const feature of LYNX_HOST_FEATURES) {\n features[feature] = false;\n }\n return;\n }\n if (!isLynxHostFeature(token)) {\n throw new Error(\n `unsupported Lynx host feature token: ${rawToken}; supported tokens: baseline, full, *, !<feature>, ${LYNX_HOST_FEATURES.join(\n \", \",\n )}`,\n );\n }\n features[token] = !disabled;\n if (token === \"nativeWindowApi\" && disabled) {\n features.bindWindowGlobals = false;\n return;\n }\n if (token === \"nativeScreenApi\" && disabled) {\n features.bindScreenGlobals = false;\n return;\n }\n if (token === \"bindWindowGlobals\" && !disabled) {\n features.nativeWindowApi = true;\n return;\n }\n if (token === \"bindScreenGlobals\" && !disabled) {\n features.nativeScreenApi = true;\n }\n}\n\nfunction isLynxHostFeature(value: string): value is LynxHostFeature {\n return (LYNX_HOST_FEATURES as readonly string[]).includes(value);\n}\n\nfunction createVisibleIcon(): {\n type: \"rgba\";\n width: number;\n height: number;\n data: number[];\n} {\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 inStroke = x <= 5 || (x >= 13 && x <= 18) || y >= 26;\n const inDiagonal = y >= x + 10 && x <= 18;\n const alpha = inStroke || inDiagonal ? 255 : 0;\n data.push(34, 92, 56, alpha);\n }\n }\n\n return { type: \"rgba\", width, height, data };\n}\n","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, \"Open WebView\"],\n]);\n\ninterface WebviewShowCommand {\n type: \"show\";\n html?: string;\n url?: string;\n width?: number;\n height?: number;\n fallbackRect?: Rect;\n nativeWindowApi?: boolean;\n bindWindowGlobals?: boolean;\n nativeScreenApi?: boolean;\n bindScreenGlobals?: boolean;\n nativeTrayApi?: boolean;\n title?: string;\n icon?: { type: \"href\"; href: string };\n style?: {\n frameless?: boolean;\n transparent?: boolean;\n keepOnTop?: boolean;\n platform?: {\n macos?: {\n material?: string | null;\n materialState?: \"followsWindowActiveState\" | \"active\" | \"inactive\";\n cornerRadius?: number | null;\n };\n };\n };\n titleSync?: boolean | { documentToWindow?: boolean; windowToDocument?: boolean };\n iconSync?: boolean | { faviconToWindow?: boolean; windowToFavicon?: boolean };\n nativeApiPolicy?: {\n defaultSrc?: string[];\n window?: string[];\n screen?: string[];\n tray?: string[];\n windowGlobals?: string[];\n screenGlobals?: string[];\n titleSync?: string[];\n iconSync?: string[];\n };\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} session=${connection.sessionId}`);\n\n const space = await client.createSpace({\n id: \"com.example.opentray.daemon-smoke\",\n title: \"OpenTray Daemon Smoke\",\n default: true,\n });\n console.log(`space: ${JSON.stringify(space.space)}`);\n\n const tray = await space.createTray({\n trayId: \"daemon-smoke-status\",\n title: \"OpenTray\",\n tooltip: {\n title: \"OpenTray\",\n description: \"Single primary tray action; macOS direct-triggers without opening a menu\",\n },\n icon: createVisibleIcon(),\n menu: {\n items: [\n { type: \"item\", id: 1, title: \"Open WebView\", primaryEvent: true },\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 spaceId: space.space.spaceId,\n name: \"webview\",\n path: \"@opentray/ext-webview\",\n });\n console.log(\"webview extension requested through the generic load-ext path\");\n console.log(\"click the tray icon: macOS should direct-trigger the single primary action without opening a menu\");\n\n let closed = false;\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 === 1) {\n const trayBounds = await tray.getBounds();\n console.log(`tray bounds: ${JSON.stringify(trayBounds)}`);\n await commandWebview({\n type: \"show\",\n html: createWebviewDemoHtml(),\n width: 420,\n height: 260,\n title: \"OpenTray WebView Smoke\",\n icon: {\n type: \"href\",\n href: \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=\",\n },\n style: {\n frameless: true,\n transparent: true,\n keepOnTop: true,\n platform: {\n macos: {\n material: \"hudWindow\",\n materialState: \"active\",\n },\n },\n },\n fallbackRect: trayBounds.rect ?? { x: 0, y: 0, width: 1, height: 1 },\n nativeWindowApi: true,\n bindWindowGlobals: true,\n nativeScreenApi: true,\n bindScreenGlobals: true,\n nativeTrayApi: true,\n titleSync: {\n documentToWindow: true,\n windowToDocument: true,\n },\n iconSync: true,\n nativeApiPolicy: {\n defaultSrc: [\"'local'\"],\n window: [\"https://example.com\"],\n screen: [\"https://example.com\"],\n tray: [\"https://example.com\"],\n titleSync: [\"https://example.com\"],\n iconSync: [\"https://example.com\"],\n },\n });\n console.log(\"primary tray action: webview show\");\n return;\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\" || webviewSmoke === \"1\") {\n await handleMenuClick(1);\n }\n if (webviewSmoke === \"1\") {\n if (exitTimer !== undefined) {\n clearTimeout(exitTimer);\n exitTimer = undefined;\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>This window was opened by the single primary tray action.</p>\n <section>\n <div class=\"card\">\n <div class=\"label\">primary event</div>\n <code>macOS single primary item direct-triggered menuClick -> WebView show</code>\n </div>\n </section>\n </main>\n </body>\n</html>`;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","#!/usr/bin/env node\nimport { realpathSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type { DaemonHealth } from \"@opentray/spec\";\n\nimport {\n createNodeDaemonDriver,\n inspectDaemon,\n restartDaemon,\n startDaemon,\n stopDaemon,\n} from \"./daemon/lifecycle\";\nimport { readPackageVersion } from \"./daemon/package-version\";\nimport { resolveDaemonPaths } from \"./daemon/paths\";\nimport { connectLocalBroker } from \"./local-broker\";\nimport { runDaemonLynxSmoke } from \"./smoke/daemon-lynx\";\nimport { runDaemonTraySmoke } from \"./smoke/daemon-tray\";\n\nconst packageJsonUrl = new URL(\"../package.json\", import.meta.url);\nconst cliModulePath = fileURLToPath(import.meta.url);\n\ntype CliCommand =\n | { type: \"daemon\"; action: \"start\" | \"stop\" | \"restart\" | \"health\" }\n | { type: \"smoke\"; name: \"daemon-tray\" }\n | {\n type: \"smoke\";\n name: \"daemon-lynx\";\n bundlePath?: string;\n featureExpression?: string;\n }\n | { type: \"help\" };\n\nexport const parseCliCommand = (argv: string[]): CliCommand => {\n const [group, action, ...rest] = argv;\n if (group !== \"daemon\") {\n if (group === \"smoke\" && action === \"daemon-tray\") {\n return { type: \"smoke\", name: \"daemon-tray\" };\n }\n if (group === \"smoke\" && action === \"daemon-lynx\") {\n const smokeOptions = parseSmokeOptions(rest);\n return {\n type: \"smoke\",\n name: \"daemon-lynx\",\n ...(smokeOptions.bundlePath\n ? { bundlePath: smokeOptions.bundlePath }\n : {}),\n ...(smokeOptions.featureExpression\n ? { featureExpression: smokeOptions.featureExpression }\n : {}),\n };\n }\n return { type: \"help\" };\n }\n if (\n action === \"start\" ||\n action === \"stop\" ||\n action === \"restart\" ||\n action === \"health\"\n ) {\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 =\n process.env.OPENTRAY_DAEMON_PACKAGE_VERSION ??\n (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 if (command.name === \"daemon-tray\") {\n await runDaemonTraySmoke();\n } else {\n await runDaemonLynxSmoke(\n {\n ...(command.bundlePath === undefined\n ? {}\n : { bundlePath: command.bundlePath }),\n ...(command.featureExpression === undefined\n ? {}\n : { featureExpression: command.featureExpression }),\n }\n );\n }\n return 0;\n }\n\n const driver = createNodeDaemonDriver(cliModulePath);\n\n if (command.action === \"start\") {\n const result = await startDaemon({ paths, driver });\n console.log(\n `opentray daemon ${result.status}: pid=${result.pid} endpoint=${result.paths.endpoint}`\n );\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(\n `opentray daemon ${result.status}: pid=${result.pid} endpoint=${result.paths.endpoint}`\n );\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(\n `expected daemon-health response, received ${response.type}`\n );\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 console.error(\n \" opentray smoke daemon-lynx [--bundle <path-to-main.lynx.bundle>] [--features <baseline|full|*,!feature,...>]\"\n );\n console.error(\" default Lynx smoke uses the packaged review bundle\");\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 const internalLease = session.internalLeaseId ?? \"(pending)\";\n lines.push(\n `- sessionId=${session.sessionId} initialized=${session.initialized} internalLeaseId=${internalLease}`\n );\n }\n\n return lines.join(\"\\n\");\n};\n\nexport const isCliEntrypoint = (\n argvEntryPath: string | undefined,\n modulePath: string\n): boolean => {\n if (argvEntryPath === undefined) return false;\n try {\n return realpathSync(argvEntryPath) === realpathSync(modulePath);\n } catch {\n return argvEntryPath === modulePath;\n }\n};\n\nfunction parseSmokeOptions(argv: string[]): {\n bundlePath?: string;\n featureExpression?: string;\n} {\n let bundlePath: string | undefined;\n let featureExpression: string | undefined;\n for (let index = 0; index < argv.length; index += 1) {\n if (argv[index] === \"--bundle\") {\n bundlePath = argv[index + 1];\n continue;\n }\n if (argv[index] === \"--features\") {\n featureExpression = argv[index + 1];\n }\n }\n return {\n ...(bundlePath === undefined ? {} : { bundlePath }),\n ...(featureExpression === undefined ? {} : { featureExpression }),\n };\n}\n\nif (isCliEntrypoint(process.argv[1], cliModulePath)) {\n runCli(process.argv.slice(2)).then((code) => {\n process.exitCode = code;\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { realpathSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type { DaemonHealth } from \"@opentray/spec\";\n\nimport {\n createNodeDaemonDriver,\n inspectDaemon,\n restartDaemon,\n startDaemon,\n stopDaemon,\n} from \"./daemon/lifecycle\";\nimport { readPackageVersion } from \"./daemon/package-version\";\nimport { resolveDaemonPaths } from \"./daemon/paths\";\nimport { connectLocalBroker } from \"./local-broker\";\n\nconst packageJsonUrl = new URL(\"../package.json\", import.meta.url);\nconst cliModulePath = fileURLToPath(import.meta.url);\n\ntype CliCommand =\n | { type: \"daemon\"; action: \"start\" | \"stop\" | \"restart\" | \"health\" }\n | { type: \"help\" };\n\nexport const parseCliCommand = (argv: string[]): CliCommand => {\n const [group, action] = argv;\n if (group !== \"daemon\") {\n return { type: \"help\" };\n }\n if (\n action === \"start\" ||\n action === \"stop\" ||\n action === \"restart\" ||\n action === \"health\"\n ) {\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 =\n process.env.OPENTRAY_DAEMON_PACKAGE_VERSION ??\n (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 const driver = createNodeDaemonDriver(cliModulePath);\n\n if (command.action === \"start\") {\n const result = await startDaemon({ paths, driver });\n console.log(\n `opentray daemon ${result.status}: pid=${result.pid} endpoint=${result.paths.endpoint}`\n );\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(\n `opentray daemon ${result.status}: pid=${result.pid} endpoint=${result.paths.endpoint}`\n );\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(\n `expected daemon-health response, received ${response.type}`\n );\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};\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 const internalLease = session.internalLeaseId ?? \"(pending)\";\n lines.push(\n `- sessionId=${session.sessionId} initialized=${session.initialized} internalLeaseId=${internalLease}`\n );\n }\n\n return lines.join(\"\\n\");\n};\n\nexport const isCliEntrypoint = (\n argvEntryPath: string | undefined,\n modulePath: string\n): boolean => {\n if (argvEntryPath === undefined) return false;\n try {\n return realpathSync(argvEntryPath) === realpathSync(modulePath);\n } catch {\n return argvEntryPath === modulePath;\n }\n};\n\nif (isCliEntrypoint(process.argv[1], cliModulePath)) {\n runCli(process.argv.slice(2)).then((code) => {\n process.exitCode = code;\n });\n}\n"],"mappings":"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"}
|