opentray 0.15.0 → 0.17.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/dist/index.mjs CHANGED
@@ -1,13 +1,240 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { PROTOCOL_VERSION, PROTOCOL_VERSION as PROTOCOL_VERSION$1, brokerArtifactIdentityEquals, createBrokerEndpointIdentity, createBrokerEndpointIdentity as createBrokerEndpointIdentity$1, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatUnixSocketPath as formatUnixSocketPath$1, formatWindowsPipeName, formatWindowsPipeName as formatWindowsPipeName$1, isBrokerArtifactIdentity, isSupportedProtocolVersion, parseServerFrame, sanitizeCallerLabel } from "@opentray/spec";
3
3
  import { access, mkdir, open, readFile, realpath, rm, unlink, writeFile } from "node:fs/promises";
4
- import { basename, dirname, join } from "node:path";
4
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { createConnection } from "node:net";
6
6
  import { homedir } from "node:os";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { clearDarwinAppBundleOwner, ensureDarwinAppBundle, resolveDefaultDarwinAppBundlePath, resolveOpenTrayPackageIdentity, writeDarwinAppBundleOwner } from "@opentray/packaging";
8
9
  import { constants } from "node:fs";
9
10
  import { spawn } from "node:child_process";
10
11
  import { createHash } from "node:crypto";
12
+ //#region src/app-icon.ts
13
+ const DEFAULT_APP_ICON_VARIANT = "default";
14
+ const APP_ICON_VARIANT_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
15
+ var InvalidAppIconError = class extends Error {
16
+ code = "OPENTRAY_INVALID_APP_ICON";
17
+ reason;
18
+ constructor(reason, platform = process.platform) {
19
+ super(`invalid appIcon for ${platform}: ${reason}`);
20
+ this.name = "InvalidAppIconError";
21
+ this.reason = reason;
22
+ }
23
+ };
24
+ var AppIconVariantNotFoundError = class extends Error {
25
+ code = "OPENTRAY_APP_ICON_VARIANT_NOT_FOUND";
26
+ variant;
27
+ constructor(variant, platform = process.platform) {
28
+ super(`appIcon variant ${variant} is unavailable for ${platform}`);
29
+ this.name = "AppIconVariantNotFoundError";
30
+ this.variant = variant;
31
+ }
32
+ };
33
+ /** Validate an explicit AppIcon, including native file signatures, before broker startup. */
34
+ const validateAppIcon = async (icon, platform = process.platform) => {
35
+ if (!Array.isArray(icon) || icon.length === 0) throw new InvalidAppIconError("the asset array must not be empty", platform);
36
+ const variantPlatforms = /* @__PURE__ */ new Map();
37
+ const seenDarwin = /* @__PURE__ */ new Set();
38
+ const seenWindows = /* @__PURE__ */ new Set();
39
+ const seenLinuxPngSizes = /* @__PURE__ */ new Set();
40
+ const seenLinuxSvg = /* @__PURE__ */ new Set();
41
+ for (const asset of icon) {
42
+ if (!isRecord$1(asset)) throw new InvalidAppIconError("asset must be an object", platform);
43
+ const assetPlatform = asset.platform;
44
+ const format = asset.format;
45
+ const variants = validateVariantNames(asset.variant, platform);
46
+ if (assetPlatform === "darwin" && format === "icns") {
47
+ assertExactKeys(asset, [
48
+ "platform",
49
+ "format",
50
+ "source"
51
+ ], platform, ["variant"]);
52
+ rejectDuplicateVariants(seenDarwin, variants, "darwin", platform);
53
+ await validateSource(asset.source, "icns", platform);
54
+ recordVariantPlatforms(variantPlatforms, variants, "darwin");
55
+ continue;
56
+ }
57
+ if (assetPlatform === "windows" && format === "ico") {
58
+ assertExactKeys(asset, [
59
+ "platform",
60
+ "format",
61
+ "source"
62
+ ], platform, ["variant"]);
63
+ rejectDuplicateVariants(seenWindows, variants, "windows", platform);
64
+ await validateSource(asset.source, "ico", platform);
65
+ recordVariantPlatforms(variantPlatforms, variants, "windows");
66
+ continue;
67
+ }
68
+ if (assetPlatform === "linux" && format === "png") {
69
+ assertExactKeys(asset, [
70
+ "platform",
71
+ "format",
72
+ "size",
73
+ "source"
74
+ ], platform, ["variant"]);
75
+ if (!Number.isInteger(asset.size) || typeof asset.size !== "number" || asset.size <= 0) throw new InvalidAppIconError("Linux PNG assets require a positive integer size", platform);
76
+ for (const variant of variants) {
77
+ const key = `${variant}\0${asset.size}`;
78
+ if (seenLinuxPngSizes.has(key)) throw new InvalidAppIconError(`duplicate Linux PNG size ${asset.size} for variant ${variant}`, platform);
79
+ seenLinuxPngSizes.add(key);
80
+ }
81
+ await validateSource(asset.source, "png", platform);
82
+ recordVariantPlatforms(variantPlatforms, variants, "linux");
83
+ continue;
84
+ }
85
+ if (assetPlatform === "linux" && format === "svg") {
86
+ assertExactKeys(asset, [
87
+ "platform",
88
+ "format",
89
+ "source"
90
+ ], platform, ["variant"]);
91
+ rejectDuplicateVariants(seenLinuxSvg, variants, "Linux SVG", platform);
92
+ await validateSource(asset.source, "svg", platform);
93
+ recordVariantPlatforms(variantPlatforms, variants, "linux");
94
+ continue;
95
+ }
96
+ throw new InvalidAppIconError(`unsupported platform/format pair ${String(assetPlatform)}/${String(format)}`, platform);
97
+ }
98
+ const currentPlatform = normalizePlatform(platform);
99
+ if (currentPlatform !== void 0) {
100
+ if (!variantPlatforms.get("default")?.has(currentPlatform)) throw new InvalidAppIconError(`no ${currentPlatform} ${DEFAULT_APP_ICON_VARIANT} asset was provided`, platform);
101
+ for (const [variant, platforms] of variantPlatforms) if (!platforms.has(currentPlatform)) throw new InvalidAppIconError(`variant ${variant} has no ${currentPlatform} asset`, platform);
102
+ }
103
+ };
104
+ /** Normalize encoded sources into JSON array bytes for the Rust wire protocol. */
105
+ const normalizeAppIcon = (icon) => icon.map((asset) => {
106
+ const source = normalizeSource(asset.source);
107
+ const variant = normalizeVariant(asset.variant);
108
+ if (asset.platform === "darwin") return {
109
+ ...asset,
110
+ variant,
111
+ source
112
+ };
113
+ if (asset.platform === "windows") return {
114
+ ...asset,
115
+ variant,
116
+ source
117
+ };
118
+ if (asset.format === "png") return {
119
+ ...asset,
120
+ variant,
121
+ source
122
+ };
123
+ return {
124
+ ...asset,
125
+ variant,
126
+ source
127
+ };
128
+ });
129
+ /** Select one semantic variant while retaining every platform's matching native asset. */
130
+ const selectAppIconVariant = (icon, variant, platform = process.platform) => {
131
+ validateVariantName(variant, platform);
132
+ const selected = icon.filter((asset) => normalizedVariantNames(asset.variant).includes(variant));
133
+ const currentPlatform = normalizePlatform(platform);
134
+ if (selected.length === 0 || currentPlatform !== void 0 && !selected.some((asset) => asset.platform === currentPlatform)) throw new AppIconVariantNotFoundError(variant, platform);
135
+ return selected;
136
+ };
137
+ const normalizeSource = (source) => source.type === "encoded" ? {
138
+ type: "encoded",
139
+ data: Array.from(source.data)
140
+ } : {
141
+ type: "file",
142
+ path: resolve(source.path)
143
+ };
144
+ const normalizeVariant = (variant) => variant === void 0 ? DEFAULT_APP_ICON_VARIANT : typeof variant === "string" ? variant : [...variant];
145
+ const normalizedVariantNames = (variant) => variant === void 0 ? [DEFAULT_APP_ICON_VARIANT] : typeof variant === "string" ? [variant] : variant;
146
+ const validateVariantNames = (variant, platform) => {
147
+ const names = variant === void 0 ? [DEFAULT_APP_ICON_VARIANT] : typeof variant === "string" ? [variant] : Array.isArray(variant) ? variant : [];
148
+ if (names.length === 0) throw new InvalidAppIconError("variant must be a name or non-empty name array", platform);
149
+ const seen = /* @__PURE__ */ new Set();
150
+ for (const name of names) {
151
+ validateVariantName(name, platform);
152
+ if (seen.has(name)) throw new InvalidAppIconError(`duplicate variant name ${name} on one asset`, platform);
153
+ seen.add(name);
154
+ }
155
+ return names;
156
+ };
157
+ function validateVariantName(variant, platform) {
158
+ if (typeof variant !== "string" || !APP_ICON_VARIANT_PATTERN.test(variant)) throw new InvalidAppIconError(`variant must match ${APP_ICON_VARIANT_PATTERN}`, platform);
159
+ }
160
+ const rejectDuplicateVariants = (seen, variants, label, hostPlatform) => {
161
+ for (const variant of variants) {
162
+ if (seen.has(variant)) throw new InvalidAppIconError(`duplicate ${label} asset for variant ${variant}`, hostPlatform);
163
+ seen.add(variant);
164
+ }
165
+ };
166
+ const recordVariantPlatforms = (variants, names, platform) => {
167
+ for (const name of names) {
168
+ const platforms = variants.get(name) ?? /* @__PURE__ */ new Set();
169
+ platforms.add(platform);
170
+ variants.set(name, platforms);
171
+ }
172
+ };
173
+ const validateSource = async (source, format, platform) => {
174
+ if (!isRecord$1(source)) throw new InvalidAppIconError("asset source must be a file or encoded payload", platform);
175
+ let bytes;
176
+ if (source.type === "file") {
177
+ assertExactKeys(source, ["type", "path"], platform);
178
+ if (typeof source.path !== "string" || source.path.trim().length === 0) throw new InvalidAppIconError("file source path must not be empty", platform);
179
+ try {
180
+ bytes = await readFile(source.path);
181
+ } catch (error) {
182
+ throw new InvalidAppIconError(`unable to read ${source.path}: ${errorMessage(error)}`, platform);
183
+ }
184
+ } else if (source.type === "encoded") {
185
+ assertExactKeys(source, ["type", "data"], platform);
186
+ bytes = encodedBytes(source.data, platform);
187
+ } else throw new InvalidAppIconError("unsupported app icon source type", platform);
188
+ if (!matchesFormat(bytes, format)) throw new InvalidAppIconError(`source bytes do not contain a valid ${format.toUpperCase()} asset`, platform);
189
+ };
190
+ const encodedBytes = (data, platform) => {
191
+ if (data instanceof Uint8Array && data.byteLength > 0) return data;
192
+ if (Array.isArray(data) && data.length > 0 && data.every((value) => Number.isInteger(value) && value >= 0 && value <= 255)) return Uint8Array.from(data);
193
+ throw new InvalidAppIconError("encoded source must contain one or more bytes in the range 0..255", platform);
194
+ };
195
+ const matchesFormat = (bytes, format) => {
196
+ if (format === "icns") return hasPrefix(bytes, [
197
+ 105,
198
+ 99,
199
+ 110,
200
+ 115
201
+ ]);
202
+ if (format === "ico") return bytes.length >= 6 && hasPrefix(bytes, [
203
+ 0,
204
+ 0,
205
+ 1,
206
+ 0
207
+ ]) && bytes[4] !== 0;
208
+ if (format === "png") return hasPrefix(bytes, [
209
+ 137,
210
+ 80,
211
+ 78,
212
+ 71,
213
+ 13,
214
+ 10,
215
+ 26,
216
+ 10
217
+ ]);
218
+ const source = new TextDecoder().decode(bytes.subarray(0, 4096)).replace(/^\uFEFF/, "");
219
+ return /<(?:\?xml[^>]*>\s*)?(?:!--[\s\S]*?-->\s*)*svg(?:\s|>)/i.test(source.trimStart());
220
+ };
221
+ const hasPrefix = (bytes, prefix) => bytes.length >= prefix.length && prefix.every((byte, index) => bytes[index] === byte);
222
+ const assertExactKeys = (record, expected, platform, optional = []) => {
223
+ const actual = Object.keys(record);
224
+ const allowed = new Set([...expected, ...optional]);
225
+ const unexpected = actual.find((key) => !allowed.has(key));
226
+ if (unexpected !== void 0) throw new InvalidAppIconError(`unexpected asset field ${unexpected}`, platform);
227
+ const missing = expected.find((key) => !(key in record));
228
+ if (missing !== void 0) throw new InvalidAppIconError(`missing asset field ${missing}`, platform);
229
+ };
230
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
231
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
232
+ const normalizePlatform = (platform) => {
233
+ if (platform === "darwin") return "darwin";
234
+ if (platform === "win32") return "windows";
235
+ if (platform === "linux") return "linux";
236
+ };
237
+ //#endregion
11
238
  //#region src/native-extension-artifact.ts
12
239
  var NativeExtensionArtifactResolutionError = class extends Error {
13
240
  target;
@@ -129,8 +356,26 @@ const createInitFrame = (clientVersion) => ({
129
356
  });
130
357
  function createClient(transport, options = {}) {
131
358
  const nextRequestId = createRequestIdFactory(options.requestIdPrefix ?? "opentray");
359
+ let appPromise;
360
+ const ensureApp = async (trayOptions) => {
361
+ appPromise ??= (async () => {
362
+ const app = await resolveDefaultAppRef(transport, nextRequestId);
363
+ const handle = createAppHandle(transport, app.appId, nextRequestId);
364
+ const configured = options.appOptions;
365
+ if (configured?.name !== void 0) await handle.setName(configured.name);
366
+ if (configured?.appIcon !== void 0) await handle.setAppIcon(configured.appIcon);
367
+ return {
368
+ app,
369
+ handle
370
+ };
371
+ })().catch((error) => {
372
+ appPromise = void 0;
373
+ throw error;
374
+ });
375
+ return appPromise;
376
+ };
132
377
  return { async createTray(options) {
133
- const app = await resolveDefaultAppRef(transport, nextRequestId);
378
+ const { app } = await ensureApp(options);
134
379
  const requestId = nextRequestId();
135
380
  const frame = expectResponse(await transport.request({
136
381
  type: "create-tray",
@@ -150,6 +395,7 @@ function createTrayHandle(transport, appId, trayId, nextRequestId = createReques
150
395
  };
151
396
  const handle = {
152
397
  trayId,
398
+ app: createAppHandle(transport, appId, nextRequestId),
153
399
  async getBounds() {
154
400
  const requestId = nextRequestId();
155
401
  return expectResponse(await transport.request({
@@ -288,6 +534,62 @@ const resolveDefaultAppRef = async (transport, nextRequestId) => {
288
534
  requestId
289
535
  }), requestId, "default-app").app;
290
536
  };
537
+ const createAppHandle = (transport, appId, nextRequestId) => {
538
+ const requestIdentity = async () => {
539
+ const requestId = nextRequestId();
540
+ return expectResponse(await transport.request({
541
+ type: "get-app-identity",
542
+ requestId,
543
+ appId
544
+ }), requestId, "app-identity").identity;
545
+ };
546
+ return {
547
+ appId,
548
+ async getName() {
549
+ return (await requestIdentity()).appName;
550
+ },
551
+ async setName(name) {
552
+ const requestId = nextRequestId();
553
+ expectResponse(await transport.request({
554
+ type: "set-app-name",
555
+ requestId,
556
+ appId,
557
+ name
558
+ }), requestId, "ack");
559
+ return (await requestIdentity()).appName;
560
+ },
561
+ async getAppIcon() {
562
+ return (await requestIdentity()).appIcon ?? null;
563
+ },
564
+ async getAppIconVariant() {
565
+ return (await requestIdentity()).appIconVariant ?? null;
566
+ },
567
+ async setAppIcon(iconOrVariant) {
568
+ if (typeof iconOrVariant === "string") {
569
+ const identity = await requestIdentity();
570
+ if (identity.appIcon === void 0) selectAppIconVariant([], iconOrVariant);
571
+ else selectAppIconVariant(identity.appIcon, iconOrVariant);
572
+ const requestId = nextRequestId();
573
+ expectResponse(await transport.request({
574
+ type: "set-app-icon-variant",
575
+ requestId,
576
+ appId,
577
+ variant: iconOrVariant
578
+ }), requestId, "ack");
579
+ return;
580
+ }
581
+ if (iconOrVariant !== null) await validateAppIcon(iconOrVariant);
582
+ const appIcon = iconOrVariant === null ? null : normalizeAppIcon(iconOrVariant);
583
+ const requestId = nextRequestId();
584
+ expectResponse(await transport.request({
585
+ type: "set-app-icon",
586
+ requestId,
587
+ appId,
588
+ appIcon
589
+ }), requestId, "ack");
590
+ }
591
+ };
592
+ };
291
593
  const createTrayExtensionContext = (tray, appId, extension, options, nextMountId) => {
292
594
  const mount = extension.resolveMount?.(options) ?? {};
293
595
  const name = mount.name ?? extension.name;
@@ -344,6 +646,7 @@ const requestIdOf = (frame) => {
344
646
  switch (frame.type) {
345
647
  case "app-created":
346
648
  case "default-app":
649
+ case "app-identity":
347
650
  case "tray-created":
348
651
  case "tray-bounds":
349
652
  case "ack":
@@ -384,7 +687,8 @@ const resolveBrokerNativeTarget = (platform = process.platform, arch = process.a
384
687
  const packageOs = platformToPackageOs(platform);
385
688
  return {
386
689
  packageName: `@opentray/${packageOs}-${normalizeArch(arch)}`,
387
- binaryRelativePath: `bin/${packageOs === "windows" ? "opentray.exe" : "opentray"}`
690
+ binaryRelativePath: `bin/${packageOs === "windows" ? "opentray.exe" : "opentray"}`,
691
+ ...packageOs === "darwin" ? { carrierTemplateRelativePath: "app/Info.plist" } : {}
388
692
  };
389
693
  };
390
694
  const resolveBrokerArtifactTarget = (platform = process.platform, arch = process.arch) => {
@@ -460,10 +764,19 @@ const resolveBrokerCommand = async (paths, options = {}) => {
460
764
  const arch = options.arch ?? process.arch;
461
765
  const target = resolveBrokerNativeTarget(platform, arch);
462
766
  const installed = await (options.resolveInstalledBrokerBinary ?? resolveInstalledBrokerBinary)(target, { platform });
463
- if (installed.binary !== void 0) return commandForBinary(installed.binary, paths);
767
+ if (installed.binary !== void 0) {
768
+ if (platform !== "darwin") return commandForBinary(installed.binary, paths);
769
+ if (options.appBundle === void 0) return commandForBinary(installed.binary, paths);
770
+ if (installed.carrierTemplate === void 0) throw missingDarwinCarrierError(target, installed, arch);
771
+ return commandForBinary(await ensureDarwinBundle(options, paths, installed.binary, installed.carrierTemplate), paths);
772
+ }
464
773
  const sourceDir = options.sourceDir ?? dirname(fileURLToPath(sourceUrl));
465
774
  const workspaceRoot = await (options.findWorkspaceRoot ?? findWorkspaceRoot)(sourceDir);
466
- if (workspaceRoot !== void 0) return commandForBinary(await (options.ensureDevBrokerBinary ?? ensureDevBrokerBinary)(workspaceRoot, paths, platform), paths);
775
+ if (workspaceRoot !== void 0) {
776
+ const binary = await (options.ensureDevBrokerBinary ?? ensureDevBrokerBinary)(workspaceRoot, paths, platform);
777
+ if (platform !== "darwin") return commandForBinary(binary, paths);
778
+ return commandForBinary(options.appBundle === void 0 ? binary : await ensureDarwinBundle(options, paths, binary, await (options.ensureDevDarwinCarrierTemplate ?? ensureDevDarwinCarrierTemplate)(workspaceRoot, paths)), paths);
779
+ }
467
780
  throw new MissingPlatformBrokerBinaryError(installed.binaryPath === void 0 ? `unable to resolve OpenTray broker binary for ${platform}/${arch}; install "${target.packageName}" for this platform or set OPENTRAY_BROKER_BIN` : `unable to resolve OpenTray broker binary for ${platform}/${arch}; package "${target.packageName}" was found but binary "${installed.binaryPath}" is not accessible; restage the runtime package or set OPENTRAY_BROKER_BIN`, platform, arch, {
468
781
  packageName: target.packageName,
469
782
  ...installed.binaryPath === void 0 ? {} : { binaryPath: installed.binaryPath }
@@ -479,14 +792,28 @@ const resolveInstalledBrokerBinary = async (target, options = {}) => {
479
792
  throw error;
480
793
  }
481
794
  const binaryPath = join(dirname(packageJsonPath), target.binaryRelativePath);
795
+ const carrierTemplatePath = target.carrierTemplateRelativePath === void 0 ? void 0 : join(dirname(packageJsonPath), target.carrierTemplateRelativePath);
796
+ let carrierTemplate;
797
+ if (carrierTemplatePath !== void 0) try {
798
+ await (options.assertCarrierAccessible ?? assertInstalledCarrierAccessible)(carrierTemplatePath);
799
+ carrierTemplate = carrierTemplatePath;
800
+ } catch (error) {
801
+ if (!isNodeError$1(error) || error.code !== "ENOENT" && error.code !== "EACCES" && error.code !== "EPERM") throw error;
802
+ }
482
803
  try {
483
804
  await (options.assertBinaryAccessible ?? assertInstalledBrokerBinaryAccessible)(binaryPath, platform);
484
805
  return {
485
806
  binary: binaryPath,
486
- binaryPath
807
+ binaryPath,
808
+ ...carrierTemplatePath === void 0 ? {} : { carrierTemplatePath },
809
+ ...carrierTemplate === void 0 ? {} : { carrierTemplate }
487
810
  };
488
811
  } catch (error) {
489
- if (isNodeError$1(error) && (error.code === "ENOENT" || error.code === "EACCES" || error.code === "EPERM")) return { binaryPath };
812
+ if (isNodeError$1(error) && (error.code === "ENOENT" || error.code === "EACCES" || error.code === "EPERM")) return {
813
+ binaryPath,
814
+ ...carrierTemplatePath === void 0 ? {} : { carrierTemplatePath },
815
+ ...carrierTemplate === void 0 ? {} : { carrierTemplate }
816
+ };
490
817
  throw error;
491
818
  }
492
819
  };
@@ -533,15 +860,39 @@ const ensureDevBrokerBinary = async (workspaceRoot, paths, platform) => {
533
860
  await runCargoBuild(workspaceRoot, platform === "win32" ? targetDir : void 0);
534
861
  return binary;
535
862
  };
536
- const runCargoBuild = (workspaceRoot, targetDir) => new Promise((resolve, reject) => {
537
- const child = spawn("cargo", [
538
- "build",
539
- "-p",
540
- "opentray-bin",
541
- ...targetDir === void 0 ? [] : ["--target-dir", targetDir]
542
- ], {
543
- cwd: workspaceRoot,
544
- stdio: process.env.OPENTRAY_BROKER_BUILD_LOGS === "1" ? "inherit" : "ignore"
863
+ const ensureDevDarwinCarrierTemplate = async (workspaceRoot, _paths) => {
864
+ return join(workspaceRoot, "packages/darwin-app-carrier/Info.plist");
865
+ };
866
+ const ensureDarwinBundle = async (options, paths, brokerPath, templatePath) => {
867
+ const appBundle = options.appBundle;
868
+ if (appBundle === void 0) return brokerPath;
869
+ const arch = options.arch ?? process.arch;
870
+ if (arch !== "arm64" && arch !== "x64") throw new Error(`unsupported Darwin app bundle architecture: ${arch}`);
871
+ return ensureDarwinAppBundle({
872
+ bundlePath: appBundle.path,
873
+ packageName: options.packageIdentity?.name ?? paths.callerLabel,
874
+ appId: paths.appId,
875
+ appName: paths.appName,
876
+ target: {
877
+ os: "darwin",
878
+ arch
879
+ },
880
+ brokerPath,
881
+ templatePath,
882
+ ...options.appIcon === void 0 ? {} : { appIcon: options.appIcon },
883
+ ...appBundle.reinitialize === void 0 ? {} : { reinitialize: appBundle.reinitialize }
884
+ });
885
+ };
886
+ const runCargoBuild = (workspaceRoot, targetDir) => runProcess("cargo", [
887
+ "build",
888
+ "-p",
889
+ "opentray-bin",
890
+ ...targetDir === void 0 ? [] : ["--target-dir", targetDir]
891
+ ], workspaceRoot, process.env.OPENTRAY_BROKER_BUILD_LOGS === "1" ? "inherit" : "ignore");
892
+ const runProcess = (command, args, cwd, stdio = "ignore") => new Promise((resolve, reject) => {
893
+ const child = spawn(command, [...args], {
894
+ cwd,
895
+ stdio
545
896
  });
546
897
  child.once("error", reject);
547
898
  child.once("exit", (code) => {
@@ -549,7 +900,7 @@ const runCargoBuild = (workspaceRoot, targetDir) => new Promise((resolve, reject
549
900
  resolve();
550
901
  return;
551
902
  }
552
- reject(/* @__PURE__ */ new Error(`cargo build -p opentray-bin failed with code ${code ?? "unknown"}`));
903
+ reject(/* @__PURE__ */ new Error(`${command} ${args.join(" ")} failed with code ${code ?? "unknown"}`));
553
904
  });
554
905
  });
555
906
  const exists = async (path) => {
@@ -564,6 +915,13 @@ const binaryAccessMode = (platform) => platform === "win32" ? constants.F_OK : c
564
915
  const assertInstalledBrokerBinaryAccessible = async (binaryPath, platform) => {
565
916
  await access(binaryPath, binaryAccessMode(platform));
566
917
  };
918
+ const assertInstalledCarrierAccessible = async (carrierPath) => {
919
+ await access(carrierPath, constants.F_OK);
920
+ };
921
+ const missingDarwinCarrierError = (target, resolution, arch) => new MissingPlatformBrokerBinaryError(`unable to resolve OpenTray Darwin app bundle template for ${target.packageName}; expected ${resolution.carrierTemplatePath ?? target.carrierTemplateRelativePath ?? "app/Info.plist"}`, "darwin", arch, {
922
+ packageName: target.packageName,
923
+ ...resolution.carrierTemplatePath === void 0 ? {} : { binaryPath: resolution.carrierTemplatePath }
924
+ });
567
925
  function isNodeError$1(error) {
568
926
  return error instanceof Error && "code" in error;
569
927
  }
@@ -571,9 +929,13 @@ function isNodeError$1(error) {
571
929
  //#region src/daemon/lifecycle.ts
572
930
  const DAEMON_STDIO_ENV = "OPENTRAY_DAEMON_STDIO";
573
931
  const DEFAULT_DAEMON_LOCK_TIMEOUT_MS = 5e3;
574
- const createNodeDaemonDriver = (cliEntrypoint) => ({
932
+ const createNodeDaemonDriver = (cliEntrypoint, options = {}) => ({
575
933
  async resolveBroker(paths) {
576
- return resolveBrokerArtifact(paths);
934
+ return resolveBrokerArtifact(paths, {
935
+ ...options.appBundle === void 0 ? {} : { appBundle: options.appBundle },
936
+ ...options.appIcon === void 0 ? {} : { appIcon: options.appIcon },
937
+ ...options.packageIdentity === void 0 ? {} : { packageIdentity: options.packageIdentity }
938
+ });
577
939
  },
578
940
  async isAlive(pid) {
579
941
  return isProcessAlive(pid);
@@ -594,22 +956,36 @@ const createNodeDaemonDriver = (cliEntrypoint) => ({
594
956
  stdio: resolveBrokerStdio(process.env[DAEMON_STDIO_ENV])
595
957
  });
596
958
  child.unref();
597
- return child.pid ?? 0;
959
+ const pid = child.pid ?? 0;
960
+ if (process.platform === "darwin" && options.appBundle !== void 0 && pid > 0) await writeDarwinAppBundleOwner(options.appBundle.path, pid, broker.artifactIdentity.executableHash);
961
+ return pid;
598
962
  },
599
963
  async stop(pid) {
600
964
  process.kill(pid, "SIGTERM");
965
+ if (process.platform === "darwin" && options.appBundle !== void 0) await clearDarwinAppBundleOwner(options.appBundle.path, pid);
601
966
  }
602
967
  });
603
968
  const resolveBrokerStdio = (value) => {
604
969
  return value === "inherit" ? "inherit" : "ignore";
605
970
  };
606
971
  const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_TIMEOUT_MS }) => {
607
- const broker = await driver.resolveBroker(paths);
608
972
  await mkdir(paths.runtimeDir, { recursive: true });
609
973
  const lock = await acquireLock(paths.lockFile, lockTimeoutMs);
610
974
  try {
611
975
  const existingPid = await readPid(paths.pidFile);
612
- if (existingPid !== void 0 && await driver.isAlive(existingPid)) {
976
+ let existingAlive = existingPid !== void 0 && await driver.isAlive(existingPid);
977
+ let broker;
978
+ try {
979
+ broker = await driver.resolveBroker(paths);
980
+ } catch (error) {
981
+ if (existingAlive && isBundleInUseError(error) && existingPid !== void 0) {
982
+ await driver.stop(existingPid);
983
+ if (!await waitUntilStopped(driver, existingPid)) throw new Error(`incompatible daemon broker did not stop within the bounded shutdown window: pid=${existingPid}`);
984
+ existingAlive = false;
985
+ broker = await driver.resolveBroker(paths);
986
+ } else throw error;
987
+ }
988
+ if (existingPid !== void 0 && existingAlive) {
613
989
  if (readyMatchesBroker(await readReadyMetadata(paths.readyFile), paths, existingPid, broker)) return {
614
990
  status: "already-running",
615
991
  pid: existingPid,
@@ -698,6 +1074,7 @@ const readReadyMetadata = async (readyFile) => {
698
1074
  };
699
1075
  };
700
1076
  const readyMatchesBroker = (ready, paths, pid, broker) => ready !== void 0 && ready.pid === pid && ready.endpoint === paths.endpoint && ready.packageVersion === paths.packageVersion && ready.protocolVersion === paths.protocolVersion && ready.appId === paths.appId && ready.appName === paths.appName && ready.callerLabel === paths.callerLabel && executablePathsEqual(ready.executablePath, broker.executablePath) && brokerArtifactIdentityEquals(ready.brokerArtifactIdentity, broker.artifactIdentity);
1077
+ const isBundleInUseError = (error) => error instanceof Error && "code" in error && error.code === "bundle_in_use";
701
1078
  const executablePathsEqual = (left, right, platform = process.platform) => {
702
1079
  if (platform !== "win32") return left === right;
703
1080
  const normalize = (value) => value.replaceAll("\\", "/").replace(/^\/\/?\?\//u, "").toLowerCase();
@@ -794,7 +1171,16 @@ const connectLocalBroker = async (options = {}) => {
794
1171
  const autoStart = options.autoStart ?? endpoint === paths.endpoint;
795
1172
  if (autoStart && endpoint !== paths.endpoint) throw new Error("local broker autoStart requires the derived same-version endpoint");
796
1173
  const cliEntrypoint = options.cliEntrypoint ?? resolveCliEntrypoint();
797
- const driver = options.daemonDriver ?? createNodeDaemonDriver(cliEntrypoint);
1174
+ const packageIdentity = process.platform === "darwin" && autoStart ? await resolveOpenTrayPackageIdentity({
1175
+ ...options.packageName === void 0 ? {} : { packageName: options.packageName },
1176
+ ...options.packageRoot === void 0 ? {} : { packageRoot: options.packageRoot }
1177
+ }) : void 0;
1178
+ const appBundle = process.platform === "darwin" && autoStart ? resolveDarwinAppBundleOptions(options.appBundle, paths, packageIdentity) : void 0;
1179
+ const driver = options.daemonDriver ?? createNodeDaemonDriver(cliEntrypoint, {
1180
+ ...appBundle === void 0 ? {} : { appBundle },
1181
+ ...options.appIcon === void 0 ? {} : { appIcon: options.appIcon },
1182
+ ...packageIdentity === void 0 ? {} : { packageIdentity }
1183
+ });
798
1184
  const expectedBrokerArtifactIdentity = autoStart ? (await startDaemon({
799
1185
  paths,
800
1186
  driver
@@ -808,6 +1194,20 @@ const connectLocalBroker = async (options = {}) => {
808
1194
  }
809
1195
  return connection;
810
1196
  };
1197
+ const resolveDarwinAppBundleOptions = (configured, paths, packageIdentity) => {
1198
+ if (packageIdentity === void 0) throw new Error("Darwin app bundle resolution requires the caller package identity");
1199
+ const configuredPath = configured?.path;
1200
+ return {
1201
+ path: configuredPath === void 0 ? resolveDefaultDarwinAppBundlePath({
1202
+ homeDir: paths.homeDir,
1203
+ packageName: packageIdentity.name,
1204
+ appName: paths.appName
1205
+ }) : typeof configuredPath === "string" ? isAbsolute(configuredPath) ? configuredPath : resolve(packageIdentity.root, configuredPath) : configuredPath.protocol === "file:" ? fileURLToPath(configuredPath) : (() => {
1206
+ throw new Error(`Darwin appBundle.path must be a file URL: ${configuredPath.href}`);
1207
+ })(),
1208
+ ...configured?.reinitialize === void 0 ? {} : { reinitialize: configured.reinitialize }
1209
+ };
1210
+ };
811
1211
  const normalizeAppIdentityField = (value) => {
812
1212
  const trimmed = value?.trim();
813
1213
  return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
@@ -944,6 +1344,7 @@ const responseRequestId = (frame) => {
944
1344
  switch (frame.type) {
945
1345
  case "app-created":
946
1346
  case "default-app":
1347
+ case "app-identity":
947
1348
  case "tray-created":
948
1349
  case "tray-bounds":
949
1350
  case "ack":
@@ -1080,10 +1481,18 @@ const registerClickId = (state, id) => {
1080
1481
  //#endregion
1081
1482
  //#region src/sdk.ts
1082
1483
  const createTray = async (options, runtimeOptions = {}) => {
1484
+ if (runtimeOptions.appIcon !== void 0) await validateAppIcon(runtimeOptions.appIcon);
1485
+ const appIcon = runtimeOptions.appIcon === void 0 ? void 0 : normalizeAppIcon(runtimeOptions.appIcon);
1083
1486
  const normalized = normalizeCreateTrayOptions(options);
1084
- const connection = await connectLocalBroker(runtimeOptions);
1487
+ const connection = await connectLocalBroker({
1488
+ ...runtimeOptions,
1489
+ ...appIcon === void 0 ? {} : { appIcon }
1490
+ });
1085
1491
  try {
1086
- const tray = await createClient(connection).createTray(normalized.options);
1492
+ const tray = await createClient(connection, { appOptions: {
1493
+ ...runtimeOptions.appName === void 0 ? {} : { name: runtimeOptions.appName },
1494
+ ...appIcon === void 0 ? {} : { appIcon }
1495
+ } }).createTray(normalized.options);
1087
1496
  return wrapCreateTrayHandle(tray, {
1088
1497
  closeConnection: () => connection.close(),
1089
1498
  destroyPromise: void 0,
@@ -1147,6 +1556,6 @@ const bindMenuClickHandlers = (tray, handlers) => {
1147
1556
  };
1148
1557
  const noop = () => {};
1149
1558
  //#endregion
1150
- export { NativeExtensionArtifactResolutionError, PROTOCOL_VERSION, createBrokerEndpointIdentity, createClient, createInitFrame, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion, resolveNativeExtensionArtifact };
1559
+ export { AppIconVariantNotFoundError, DEFAULT_APP_ICON_VARIANT, InvalidAppIconError, NativeExtensionArtifactResolutionError, PROTOCOL_VERSION, createBrokerEndpointIdentity, createClient, createInitFrame, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion, resolveNativeExtensionArtifact, validateAppIcon };
1151
1560
 
1152
1561
  //# sourceMappingURL=index.mjs.map