opentray 0.16.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;
@@ -136,8 +363,7 @@ function createClient(transport, options = {}) {
136
363
  const handle = createAppHandle(transport, app.appId, nextRequestId);
137
364
  const configured = options.appOptions;
138
365
  if (configured?.name !== void 0) await handle.setName(configured.name);
139
- if (configured?.icon !== void 0) await handle.setIcon(configured.icon);
140
- else if (trayOptions.icon !== void 0 && await handle.getIcon() === null) await handle.setIcon(trayOptions.icon);
366
+ if (configured?.appIcon !== void 0) await handle.setAppIcon(configured.appIcon);
141
367
  return {
142
368
  app,
143
369
  handle
@@ -332,18 +558,35 @@ const createAppHandle = (transport, appId, nextRequestId) => {
332
558
  }), requestId, "ack");
333
559
  return (await requestIdentity()).appName;
334
560
  },
335
- async getIcon() {
336
- return (await requestIdentity()).icon ?? null;
561
+ async getAppIcon() {
562
+ return (await requestIdentity()).appIcon ?? null;
337
563
  },
338
- async setIcon(icon) {
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);
339
583
  const requestId = nextRequestId();
340
584
  expectResponse(await transport.request({
341
585
  type: "set-app-icon",
342
586
  requestId,
343
587
  appId,
344
- icon
588
+ appIcon
345
589
  }), requestId, "ack");
346
- return icon;
347
590
  }
348
591
  };
349
592
  };
@@ -444,7 +687,8 @@ const resolveBrokerNativeTarget = (platform = process.platform, arch = process.a
444
687
  const packageOs = platformToPackageOs(platform);
445
688
  return {
446
689
  packageName: `@opentray/${packageOs}-${normalizeArch(arch)}`,
447
- binaryRelativePath: `bin/${packageOs === "windows" ? "opentray.exe" : "opentray"}`
690
+ binaryRelativePath: `bin/${packageOs === "windows" ? "opentray.exe" : "opentray"}`,
691
+ ...packageOs === "darwin" ? { carrierTemplateRelativePath: "app/Info.plist" } : {}
448
692
  };
449
693
  };
450
694
  const resolveBrokerArtifactTarget = (platform = process.platform, arch = process.arch) => {
@@ -520,10 +764,19 @@ const resolveBrokerCommand = async (paths, options = {}) => {
520
764
  const arch = options.arch ?? process.arch;
521
765
  const target = resolveBrokerNativeTarget(platform, arch);
522
766
  const installed = await (options.resolveInstalledBrokerBinary ?? resolveInstalledBrokerBinary)(target, { platform });
523
- 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
+ }
524
773
  const sourceDir = options.sourceDir ?? dirname(fileURLToPath(sourceUrl));
525
774
  const workspaceRoot = await (options.findWorkspaceRoot ?? findWorkspaceRoot)(sourceDir);
526
- 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
+ }
527
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, {
528
781
  packageName: target.packageName,
529
782
  ...installed.binaryPath === void 0 ? {} : { binaryPath: installed.binaryPath }
@@ -539,14 +792,28 @@ const resolveInstalledBrokerBinary = async (target, options = {}) => {
539
792
  throw error;
540
793
  }
541
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
+ }
542
803
  try {
543
804
  await (options.assertBinaryAccessible ?? assertInstalledBrokerBinaryAccessible)(binaryPath, platform);
544
805
  return {
545
806
  binary: binaryPath,
546
- binaryPath
807
+ binaryPath,
808
+ ...carrierTemplatePath === void 0 ? {} : { carrierTemplatePath },
809
+ ...carrierTemplate === void 0 ? {} : { carrierTemplate }
547
810
  };
548
811
  } catch (error) {
549
- 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
+ };
550
817
  throw error;
551
818
  }
552
819
  };
@@ -593,15 +860,39 @@ const ensureDevBrokerBinary = async (workspaceRoot, paths, platform) => {
593
860
  await runCargoBuild(workspaceRoot, platform === "win32" ? targetDir : void 0);
594
861
  return binary;
595
862
  };
596
- const runCargoBuild = (workspaceRoot, targetDir) => new Promise((resolve, reject) => {
597
- const child = spawn("cargo", [
598
- "build",
599
- "-p",
600
- "opentray-bin",
601
- ...targetDir === void 0 ? [] : ["--target-dir", targetDir]
602
- ], {
603
- cwd: workspaceRoot,
604
- 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
605
896
  });
606
897
  child.once("error", reject);
607
898
  child.once("exit", (code) => {
@@ -609,7 +900,7 @@ const runCargoBuild = (workspaceRoot, targetDir) => new Promise((resolve, reject
609
900
  resolve();
610
901
  return;
611
902
  }
612
- 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"}`));
613
904
  });
614
905
  });
615
906
  const exists = async (path) => {
@@ -624,6 +915,13 @@ const binaryAccessMode = (platform) => platform === "win32" ? constants.F_OK : c
624
915
  const assertInstalledBrokerBinaryAccessible = async (binaryPath, platform) => {
625
916
  await access(binaryPath, binaryAccessMode(platform));
626
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
+ });
627
925
  function isNodeError$1(error) {
628
926
  return error instanceof Error && "code" in error;
629
927
  }
@@ -631,9 +929,13 @@ function isNodeError$1(error) {
631
929
  //#region src/daemon/lifecycle.ts
632
930
  const DAEMON_STDIO_ENV = "OPENTRAY_DAEMON_STDIO";
633
931
  const DEFAULT_DAEMON_LOCK_TIMEOUT_MS = 5e3;
634
- const createNodeDaemonDriver = (cliEntrypoint) => ({
932
+ const createNodeDaemonDriver = (cliEntrypoint, options = {}) => ({
635
933
  async resolveBroker(paths) {
636
- 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
+ });
637
939
  },
638
940
  async isAlive(pid) {
639
941
  return isProcessAlive(pid);
@@ -654,22 +956,36 @@ const createNodeDaemonDriver = (cliEntrypoint) => ({
654
956
  stdio: resolveBrokerStdio(process.env[DAEMON_STDIO_ENV])
655
957
  });
656
958
  child.unref();
657
- 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;
658
962
  },
659
963
  async stop(pid) {
660
964
  process.kill(pid, "SIGTERM");
965
+ if (process.platform === "darwin" && options.appBundle !== void 0) await clearDarwinAppBundleOwner(options.appBundle.path, pid);
661
966
  }
662
967
  });
663
968
  const resolveBrokerStdio = (value) => {
664
969
  return value === "inherit" ? "inherit" : "ignore";
665
970
  };
666
971
  const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_TIMEOUT_MS }) => {
667
- const broker = await driver.resolveBroker(paths);
668
972
  await mkdir(paths.runtimeDir, { recursive: true });
669
973
  const lock = await acquireLock(paths.lockFile, lockTimeoutMs);
670
974
  try {
671
975
  const existingPid = await readPid(paths.pidFile);
672
- 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) {
673
989
  if (readyMatchesBroker(await readReadyMetadata(paths.readyFile), paths, existingPid, broker)) return {
674
990
  status: "already-running",
675
991
  pid: existingPid,
@@ -758,6 +1074,7 @@ const readReadyMetadata = async (readyFile) => {
758
1074
  };
759
1075
  };
760
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";
761
1078
  const executablePathsEqual = (left, right, platform = process.platform) => {
762
1079
  if (platform !== "win32") return left === right;
763
1080
  const normalize = (value) => value.replaceAll("\\", "/").replace(/^\/\/?\?\//u, "").toLowerCase();
@@ -854,7 +1171,16 @@ const connectLocalBroker = async (options = {}) => {
854
1171
  const autoStart = options.autoStart ?? endpoint === paths.endpoint;
855
1172
  if (autoStart && endpoint !== paths.endpoint) throw new Error("local broker autoStart requires the derived same-version endpoint");
856
1173
  const cliEntrypoint = options.cliEntrypoint ?? resolveCliEntrypoint();
857
- 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
+ });
858
1184
  const expectedBrokerArtifactIdentity = autoStart ? (await startDaemon({
859
1185
  paths,
860
1186
  driver
@@ -868,6 +1194,20 @@ const connectLocalBroker = async (options = {}) => {
868
1194
  }
869
1195
  return connection;
870
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
+ };
871
1211
  const normalizeAppIdentityField = (value) => {
872
1212
  const trimmed = value?.trim();
873
1213
  return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
@@ -1141,12 +1481,17 @@ const registerClickId = (state, id) => {
1141
1481
  //#endregion
1142
1482
  //#region src/sdk.ts
1143
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);
1144
1486
  const normalized = normalizeCreateTrayOptions(options);
1145
- const connection = await connectLocalBroker(runtimeOptions);
1487
+ const connection = await connectLocalBroker({
1488
+ ...runtimeOptions,
1489
+ ...appIcon === void 0 ? {} : { appIcon }
1490
+ });
1146
1491
  try {
1147
1492
  const tray = await createClient(connection, { appOptions: {
1148
1493
  ...runtimeOptions.appName === void 0 ? {} : { name: runtimeOptions.appName },
1149
- ...runtimeOptions.appIcon === void 0 ? {} : { icon: runtimeOptions.appIcon }
1494
+ ...appIcon === void 0 ? {} : { appIcon }
1150
1495
  } }).createTray(normalized.options);
1151
1496
  return wrapCreateTrayHandle(tray, {
1152
1497
  closeConnection: () => connection.close(),
@@ -1211,6 +1556,6 @@ const bindMenuClickHandlers = (tray, handlers) => {
1211
1556
  };
1212
1557
  const noop = () => {};
1213
1558
  //#endregion
1214
- 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 };
1215
1560
 
1216
1561
  //# sourceMappingURL=index.mjs.map