opentray 0.16.0 → 0.18.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
- import { access, mkdir, open, readFile, realpath, rm, unlink, writeFile } from "node:fs/promises";
4
- import { basename, dirname, join } from "node:path";
3
+ import { access, appendFile, link, mkdir, open, readFile, realpath, rm, stat, unlink, writeFile } from "node:fs/promises";
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, convergeDarwinAppBundleIdentity, ensureDarwinAppBundle, resolveDefaultDarwinAppBundlePath, resolveOpenTrayPackageIdentity, updateDarwinAppLaunchDescriptor, writeDarwinAppBundleOwner } from "@opentray/packaging";
9
+ import { createHash, randomUUID } from "node:crypto";
8
10
  import { constants } from "node:fs";
9
11
  import { spawn } from "node:child_process";
10
- 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
@@ -291,6 +517,11 @@ const attachEventfulTrayHandle = (handle, source, appId, nextMountId) => {
291
517
  onTrayDoubleClick(handler) {
292
518
  return listen("trayDoubleClick", handler);
293
519
  },
520
+ onAppReopenRequested(handler) {
521
+ return source.onEvent((frame) => {
522
+ if (frame.type === "app-event" && frame.event.type === "reopenRequested" && frame.event.appId === appId) handler(frame.event);
523
+ });
524
+ },
294
525
  extend(extension, options) {
295
526
  const context = createTrayExtensionContext(eventful, appId, extension, options, nextMountId);
296
527
  const capability = extension.extend(eventful, context, options);
@@ -332,18 +563,35 @@ const createAppHandle = (transport, appId, nextRequestId) => {
332
563
  }), requestId, "ack");
333
564
  return (await requestIdentity()).appName;
334
565
  },
335
- async getIcon() {
336
- return (await requestIdentity()).icon ?? null;
566
+ async getAppIcon() {
567
+ return (await requestIdentity()).appIcon ?? null;
337
568
  },
338
- async setIcon(icon) {
569
+ async getAppIconVariant() {
570
+ return (await requestIdentity()).appIconVariant ?? null;
571
+ },
572
+ async setAppIcon(iconOrVariant) {
573
+ if (typeof iconOrVariant === "string") {
574
+ const identity = await requestIdentity();
575
+ if (identity.appIcon === void 0) selectAppIconVariant([], iconOrVariant);
576
+ else selectAppIconVariant(identity.appIcon, iconOrVariant);
577
+ const requestId = nextRequestId();
578
+ expectResponse(await transport.request({
579
+ type: "set-app-icon-variant",
580
+ requestId,
581
+ appId,
582
+ variant: iconOrVariant
583
+ }), requestId, "ack");
584
+ return;
585
+ }
586
+ if (iconOrVariant !== null) await validateAppIcon(iconOrVariant);
587
+ const appIcon = iconOrVariant === null ? null : normalizeAppIcon(iconOrVariant);
339
588
  const requestId = nextRequestId();
340
589
  expectResponse(await transport.request({
341
590
  type: "set-app-icon",
342
591
  requestId,
343
592
  appId,
344
- icon
593
+ appIcon
345
594
  }), requestId, "ack");
346
- return icon;
347
595
  }
348
596
  };
349
597
  };
@@ -412,6 +660,7 @@ const requestIdOf = (frame) => {
412
660
  case "error": return frame.requestId;
413
661
  case "ready":
414
662
  case "event":
663
+ case "app-event":
415
664
  case "ext-event": return;
416
665
  }
417
666
  };
@@ -444,7 +693,8 @@ const resolveBrokerNativeTarget = (platform = process.platform, arch = process.a
444
693
  const packageOs = platformToPackageOs(platform);
445
694
  return {
446
695
  packageName: `@opentray/${packageOs}-${normalizeArch(arch)}`,
447
- binaryRelativePath: `bin/${packageOs === "windows" ? "opentray.exe" : "opentray"}`
696
+ binaryRelativePath: `bin/${packageOs === "windows" ? "opentray.exe" : "opentray"}`,
697
+ ...packageOs === "darwin" ? { carrierTemplateRelativePath: "app/Info.plist" } : {}
448
698
  };
449
699
  };
450
700
  const resolveBrokerArtifactTarget = (platform = process.platform, arch = process.arch) => {
@@ -520,10 +770,19 @@ const resolveBrokerCommand = async (paths, options = {}) => {
520
770
  const arch = options.arch ?? process.arch;
521
771
  const target = resolveBrokerNativeTarget(platform, arch);
522
772
  const installed = await (options.resolveInstalledBrokerBinary ?? resolveInstalledBrokerBinary)(target, { platform });
523
- if (installed.binary !== void 0) return commandForBinary(installed.binary, paths);
773
+ if (installed.binary !== void 0) {
774
+ if (platform !== "darwin") return commandForBinary(installed.binary, paths);
775
+ if (options.appBundle === void 0) return commandForBinary(installed.binary, paths);
776
+ if (installed.carrierTemplate === void 0) throw missingDarwinCarrierError(target, installed, arch);
777
+ return commandForBinary(await ensureDarwinBundle(options, paths, installed.binary, installed.carrierTemplate), paths);
778
+ }
524
779
  const sourceDir = options.sourceDir ?? dirname(fileURLToPath(sourceUrl));
525
780
  const workspaceRoot = await (options.findWorkspaceRoot ?? findWorkspaceRoot)(sourceDir);
526
- if (workspaceRoot !== void 0) return commandForBinary(await (options.ensureDevBrokerBinary ?? ensureDevBrokerBinary)(workspaceRoot, paths, platform), paths);
781
+ if (workspaceRoot !== void 0) {
782
+ const binary = await (options.ensureDevBrokerBinary ?? ensureDevBrokerBinary)(workspaceRoot, paths, platform);
783
+ if (platform !== "darwin") return commandForBinary(binary, paths);
784
+ return commandForBinary(options.appBundle === void 0 ? binary : await ensureDarwinBundle(options, paths, binary, await (options.ensureDevDarwinCarrierTemplate ?? ensureDevDarwinCarrierTemplate)(workspaceRoot, paths)), paths);
785
+ }
527
786
  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
787
  packageName: target.packageName,
529
788
  ...installed.binaryPath === void 0 ? {} : { binaryPath: installed.binaryPath }
@@ -539,14 +798,28 @@ const resolveInstalledBrokerBinary = async (target, options = {}) => {
539
798
  throw error;
540
799
  }
541
800
  const binaryPath = join(dirname(packageJsonPath), target.binaryRelativePath);
801
+ const carrierTemplatePath = target.carrierTemplateRelativePath === void 0 ? void 0 : join(dirname(packageJsonPath), target.carrierTemplateRelativePath);
802
+ let carrierTemplate;
803
+ if (carrierTemplatePath !== void 0) try {
804
+ await (options.assertCarrierAccessible ?? assertInstalledCarrierAccessible)(carrierTemplatePath);
805
+ carrierTemplate = carrierTemplatePath;
806
+ } catch (error) {
807
+ if (!isNodeError$1(error) || error.code !== "ENOENT" && error.code !== "EACCES" && error.code !== "EPERM") throw error;
808
+ }
542
809
  try {
543
810
  await (options.assertBinaryAccessible ?? assertInstalledBrokerBinaryAccessible)(binaryPath, platform);
544
811
  return {
545
812
  binary: binaryPath,
546
- binaryPath
813
+ binaryPath,
814
+ ...carrierTemplatePath === void 0 ? {} : { carrierTemplatePath },
815
+ ...carrierTemplate === void 0 ? {} : { carrierTemplate }
547
816
  };
548
817
  } catch (error) {
549
- if (isNodeError$1(error) && (error.code === "ENOENT" || error.code === "EACCES" || error.code === "EPERM")) return { binaryPath };
818
+ if (isNodeError$1(error) && (error.code === "ENOENT" || error.code === "EACCES" || error.code === "EPERM")) return {
819
+ binaryPath,
820
+ ...carrierTemplatePath === void 0 ? {} : { carrierTemplatePath },
821
+ ...carrierTemplate === void 0 ? {} : { carrierTemplate }
822
+ };
550
823
  throw error;
551
824
  }
552
825
  };
@@ -593,15 +866,39 @@ const ensureDevBrokerBinary = async (workspaceRoot, paths, platform) => {
593
866
  await runCargoBuild(workspaceRoot, platform === "win32" ? targetDir : void 0);
594
867
  return binary;
595
868
  };
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"
869
+ const ensureDevDarwinCarrierTemplate = async (workspaceRoot, _paths) => {
870
+ return join(workspaceRoot, "packages/darwin-app-carrier/Info.plist");
871
+ };
872
+ const ensureDarwinBundle = async (options, paths, brokerPath, templatePath) => {
873
+ const appBundle = options.appBundle;
874
+ if (appBundle === void 0) return brokerPath;
875
+ const arch = options.arch ?? process.arch;
876
+ if (arch !== "arm64" && arch !== "x64") throw new Error(`unsupported Darwin app bundle architecture: ${arch}`);
877
+ return ensureDarwinAppBundle({
878
+ bundlePath: appBundle.path,
879
+ packageName: options.packageIdentity?.name ?? paths.callerLabel,
880
+ appId: paths.appId,
881
+ appName: paths.appName,
882
+ target: {
883
+ os: "darwin",
884
+ arch
885
+ },
886
+ brokerPath,
887
+ templatePath,
888
+ ...options.appIcon === void 0 ? {} : { appIcon: options.appIcon },
889
+ ...appBundle.reinitialize === void 0 ? {} : { reinitialize: appBundle.reinitialize }
890
+ });
891
+ };
892
+ const runCargoBuild = (workspaceRoot, targetDir) => runProcess("cargo", [
893
+ "build",
894
+ "-p",
895
+ "opentray-bin",
896
+ ...targetDir === void 0 ? [] : ["--target-dir", targetDir]
897
+ ], workspaceRoot, process.env.OPENTRAY_BROKER_BUILD_LOGS === "1" ? "inherit" : "ignore");
898
+ const runProcess = (command, args, cwd, stdio = "ignore") => new Promise((resolve, reject) => {
899
+ const child = spawn(command, [...args], {
900
+ cwd,
901
+ stdio
605
902
  });
606
903
  child.once("error", reject);
607
904
  child.once("exit", (code) => {
@@ -609,7 +906,7 @@ const runCargoBuild = (workspaceRoot, targetDir) => new Promise((resolve, reject
609
906
  resolve();
610
907
  return;
611
908
  }
612
- reject(/* @__PURE__ */ new Error(`cargo build -p opentray-bin failed with code ${code ?? "unknown"}`));
909
+ reject(/* @__PURE__ */ new Error(`${command} ${args.join(" ")} failed with code ${code ?? "unknown"}`));
613
910
  });
614
911
  });
615
912
  const exists = async (path) => {
@@ -624,52 +921,134 @@ const binaryAccessMode = (platform) => platform === "win32" ? constants.F_OK : c
624
921
  const assertInstalledBrokerBinaryAccessible = async (binaryPath, platform) => {
625
922
  await access(binaryPath, binaryAccessMode(platform));
626
923
  };
924
+ const assertInstalledCarrierAccessible = async (carrierPath) => {
925
+ await access(carrierPath, constants.F_OK);
926
+ };
927
+ 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, {
928
+ packageName: target.packageName,
929
+ ...resolution.carrierTemplatePath === void 0 ? {} : { binaryPath: resolution.carrierTemplatePath }
930
+ });
627
931
  function isNodeError$1(error) {
628
932
  return error instanceof Error && "code" in error;
629
933
  }
630
934
  //#endregion
631
935
  //#region src/daemon/lifecycle.ts
632
936
  const DAEMON_STDIO_ENV = "OPENTRAY_DAEMON_STDIO";
633
- const DEFAULT_DAEMON_LOCK_TIMEOUT_MS = 5e3;
634
- const createNodeDaemonDriver = (cliEntrypoint) => ({
937
+ const DAEMON_POLL_INTERVAL_MS = 50;
938
+ const DEFAULT_DAEMON_READY_TIMEOUT_MS = 1e4;
939
+ const DEFAULT_DAEMON_LOCK_TIMEOUT_MS = 15e3;
940
+ const ABANDONED_RECLAIM_CLAIM_MS = 1e3;
941
+ const createNodeDaemonDriver = (cliEntrypoint, options = {}) => ({
635
942
  async resolveBroker(paths) {
636
- return resolveBrokerArtifact(paths);
943
+ return resolveBrokerArtifact(paths, {
944
+ ...options.appBundle === void 0 ? {} : { appBundle: options.appBundle },
945
+ ...options.appIcon === void 0 ? {} : { appIcon: options.appIcon },
946
+ ...options.packageIdentity === void 0 ? {} : { packageIdentity: options.packageIdentity }
947
+ });
637
948
  },
638
949
  async isAlive(pid) {
639
950
  return isProcessAlive(pid);
640
951
  },
641
952
  async spawnBroker(paths, broker) {
642
- const child = spawn(broker.command, broker.args, {
643
- cwd: broker.cwd,
644
- detached: true,
645
- env: {
646
- ...process.env,
647
- OPENTRAY_DAEMON_HOME: paths.homeDir,
648
- OPENTRAY_DAEMON_PACKAGE_VERSION: paths.packageVersion,
649
- OPENTRAY_DAEMON_CLI_ENTRYPOINT: cliEntrypoint,
650
- OPENTRAY_DAEMON_APP_ID: paths.appId,
651
- OPENTRAY_DAEMON_APP_NAME: paths.appName,
652
- OPENTRAY_DAEMON_CALLER_LABEL: paths.callerLabel
653
- },
654
- stdio: resolveBrokerStdio(process.env[DAEMON_STDIO_ENV])
953
+ const env = options.env ?? process.env;
954
+ const stdio = resolveBrokerStdio(env[DAEMON_STDIO_ENV]);
955
+ if (stdio === "log") await mkdir(paths.runtimeDir, { recursive: true });
956
+ const logHandle = stdio === "log" ? await open(paths.brokerLog, "a") : void 0;
957
+ if (logHandle !== void 0) await logHandle.appendFile(`${JSON.stringify({
958
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
959
+ event: "broker-spawn",
960
+ command: broker.command,
961
+ cwd: broker.cwd ?? process.cwd(),
962
+ appId: paths.appId,
963
+ appName: paths.appName
964
+ })}\n`, "utf8");
965
+ if (isAbsolute(broker.command)) try {
966
+ await access(broker.command, process.platform === "win32" ? constants.F_OK : constants.X_OK);
967
+ } catch (error) {
968
+ if (stdio === "log") await appendFile(paths.brokerLog, `${JSON.stringify({
969
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
970
+ event: "broker-spawn-error",
971
+ command: broker.command,
972
+ error: error instanceof Error ? error.message : String(error)
973
+ })}\n`, "utf8").catch(() => void 0);
974
+ await logHandle?.close();
975
+ return 0;
976
+ }
977
+ let child;
978
+ try {
979
+ child = spawn(broker.command, broker.args, {
980
+ cwd: broker.cwd,
981
+ detached: true,
982
+ env: {
983
+ ...env,
984
+ OPENTRAY_DAEMON_HOME: paths.homeDir,
985
+ OPENTRAY_DAEMON_PACKAGE_VERSION: paths.packageVersion,
986
+ OPENTRAY_DAEMON_CLI_ENTRYPOINT: cliEntrypoint,
987
+ OPENTRAY_DAEMON_APP_ID: paths.appId,
988
+ OPENTRAY_DAEMON_APP_NAME: paths.appName,
989
+ OPENTRAY_DAEMON_CALLER_LABEL: paths.callerLabel
990
+ },
991
+ stdio: logHandle === void 0 ? stdio === "log" ? "ignore" : stdio : [
992
+ "ignore",
993
+ logHandle.fd,
994
+ logHandle.fd
995
+ ]
996
+ });
997
+ } finally {
998
+ await logHandle?.close();
999
+ }
1000
+ child.on("error", (error) => {
1001
+ if (stdio !== "log") return;
1002
+ appendFile(paths.brokerLog, `${JSON.stringify({
1003
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1004
+ event: "broker-spawn-error",
1005
+ command: broker.command,
1006
+ error: error.message
1007
+ })}\n`, "utf8").catch(() => void 0);
1008
+ });
1009
+ child.once("exit", (code, signal) => {
1010
+ if (stdio !== "log") return;
1011
+ appendFile(paths.brokerLog, `${JSON.stringify({
1012
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1013
+ event: "broker-exit",
1014
+ command: broker.command,
1015
+ code,
1016
+ signal
1017
+ })}\n`, "utf8").catch(() => void 0);
655
1018
  });
656
1019
  child.unref();
657
- return child.pid ?? 0;
1020
+ const pid = child.pid ?? 0;
1021
+ if (process.platform === "darwin" && options.appBundle !== void 0 && pid > 0) await writeDarwinAppBundleOwner(options.appBundle.path, pid, broker.artifactIdentity.executableHash);
1022
+ return pid;
658
1023
  },
659
1024
  async stop(pid) {
660
1025
  process.kill(pid, "SIGTERM");
1026
+ if (process.platform === "darwin" && options.appBundle !== void 0) await clearDarwinAppBundleOwner(options.appBundle.path, pid);
661
1027
  }
662
1028
  });
663
1029
  const resolveBrokerStdio = (value) => {
664
- return value === "inherit" ? "inherit" : "ignore";
1030
+ if (value === "inherit") return "inherit";
1031
+ if (value === "ignore") return "ignore";
1032
+ return "log";
665
1033
  };
666
- const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_TIMEOUT_MS }) => {
667
- const broker = await driver.resolveBroker(paths);
1034
+ const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_TIMEOUT_MS, readinessTimeoutMs = DEFAULT_DAEMON_READY_TIMEOUT_MS }) => {
668
1035
  await mkdir(paths.runtimeDir, { recursive: true });
669
- const lock = await acquireLock(paths.lockFile, lockTimeoutMs);
1036
+ const lock = await acquireDaemonLock(paths.lockFile, lockTimeoutMs);
670
1037
  try {
671
1038
  const existingPid = await readPid(paths.pidFile);
672
- if (existingPid !== void 0 && await driver.isAlive(existingPid)) {
1039
+ let existingAlive = existingPid !== void 0 && await driver.isAlive(existingPid);
1040
+ let broker;
1041
+ try {
1042
+ broker = await driver.resolveBroker(paths);
1043
+ } catch (error) {
1044
+ if (existingAlive && isBundleInUseError(error) && existingPid !== void 0) {
1045
+ await driver.stop(existingPid);
1046
+ if (!await waitUntilStopped(driver, existingPid)) throw new Error(`incompatible daemon broker did not stop within the bounded shutdown window: pid=${existingPid}`);
1047
+ existingAlive = false;
1048
+ broker = await driver.resolveBroker(paths);
1049
+ } else throw error;
1050
+ }
1051
+ if (existingPid !== void 0 && existingAlive) {
673
1052
  if (readyMatchesBroker(await readReadyMetadata(paths.readyFile), paths, existingPid, broker)) return {
674
1053
  status: "already-running",
675
1054
  pid: existingPid,
@@ -684,7 +1063,7 @@ const startDaemon = async ({ paths, driver, lockTimeoutMs = DEFAULT_DAEMON_LOCK_
684
1063
  if (!Number.isInteger(pid) || pid <= 0) throw new Error("daemon broker process did not expose a valid pid");
685
1064
  await writeFile(paths.pidFile, `${pid}\n`, "utf8");
686
1065
  try {
687
- await waitForReadyFile(paths, driver, pid, broker);
1066
+ await waitForReadyFile(paths, driver, pid, broker, readinessTimeoutMs);
688
1067
  } catch (error) {
689
1068
  if (await driver.isAlive(pid)) {
690
1069
  await driver.stop(pid);
@@ -721,19 +1100,22 @@ const cleanupRuntimeFiles = async (paths) => {
721
1100
  const waitUntilStopped = async (driver, pid) => {
722
1101
  for (let attempt = 0; attempt < 20; attempt += 1) {
723
1102
  if (!await driver.isAlive(pid)) return true;
724
- await sleep(50);
1103
+ await sleep(DAEMON_POLL_INTERVAL_MS);
725
1104
  }
726
1105
  return false;
727
1106
  };
728
- const waitForReadyFile = async (paths, driver, pid, broker) => {
729
- for (let attempt = 0; attempt < 40; attempt += 1) {
1107
+ const waitForReadyFile = async (paths, driver, pid, broker, timeoutMs) => {
1108
+ const deadline = Date.now() + timeoutMs;
1109
+ while (true) {
730
1110
  const ready = await readReadyMetadata(paths.readyFile);
731
1111
  if (readyMatchesBroker(ready, paths, pid, broker)) return;
732
1112
  if (ready !== void 0) throw new Error(`daemon readiness artifact identity mismatch: expected=${JSON.stringify(broker.artifactIdentity)} actual=${JSON.stringify(ready.brokerArtifactIdentity)}`);
733
- if (!await driver.isAlive(pid)) throw new Error(`daemon broker exited before readiness: pid=${pid}; readyFile=${paths.readyFile}; set ${DAEMON_STDIO_ENV}=inherit to inspect broker stderr`);
734
- await sleep(50);
1113
+ if (!await driver.isAlive(pid)) throw new Error(`daemon broker exited before readiness: pid=${pid}; readyFile=${paths.readyFile}; brokerLog=${paths.brokerLog}; set ${DAEMON_STDIO_ENV}=inherit to mirror broker stderr`);
1114
+ const remainingMs = deadline - Date.now();
1115
+ if (remainingMs <= 0) break;
1116
+ await sleep(Math.min(DAEMON_POLL_INTERVAL_MS, remainingMs));
735
1117
  }
736
- throw new Error(`timed out waiting for daemon readiness: ${paths.readyFile}`);
1118
+ throw new Error(`timed out waiting for daemon readiness after ${timeoutMs}ms: pid=${pid}; readyFile=${paths.readyFile}; brokerLog=${paths.brokerLog}; set ${DAEMON_STDIO_ENV}=inherit to mirror broker stderr`);
737
1119
  };
738
1120
  const readReadyMetadata = async (readyFile) => {
739
1121
  let value;
@@ -758,24 +1140,31 @@ const readReadyMetadata = async (readyFile) => {
758
1140
  };
759
1141
  };
760
1142
  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);
1143
+ const isBundleInUseError = (error) => error instanceof Error && "code" in error && error.code === "bundle_in_use";
761
1144
  const executablePathsEqual = (left, right, platform = process.platform) => {
762
1145
  if (platform !== "win32") return left === right;
763
1146
  const normalize = (value) => value.replaceAll("\\", "/").replace(/^\/\/?\?\//u, "").toLowerCase();
764
1147
  return normalize(left) === normalize(right);
765
1148
  };
766
- const acquireLock = async (lockFile, timeoutMs) => {
1149
+ const acquireDaemonLock = async (lockFile, timeoutMs) => {
767
1150
  const deadline = Date.now() + timeoutMs;
1151
+ const owner = `${JSON.stringify({
1152
+ pid: process.pid,
1153
+ token: randomUUID()
1154
+ })}\n`;
768
1155
  while (Date.now() <= deadline) try {
769
1156
  const handle = await open(lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
770
- await handle.writeFile(`${process.pid}\n`, "utf8");
1157
+ await handle.writeFile(owner, "utf8");
771
1158
  return { async release() {
772
1159
  await handle.close();
1160
+ if (await readLockSource(lockFile) !== owner) return;
773
1161
  await unlink(lockFile).catch((error) => {
774
1162
  if (!isNodeError(error) || error.code !== "ENOENT") throw error;
775
1163
  });
776
1164
  } };
777
1165
  } catch (error) {
778
1166
  if (isNodeError(error) && error.code === "EEXIST") {
1167
+ if (await reclaimDeadLockOwner(lockFile)) continue;
779
1168
  await sleep(25);
780
1169
  continue;
781
1170
  }
@@ -783,12 +1172,72 @@ const acquireLock = async (lockFile, timeoutMs) => {
783
1172
  }
784
1173
  throw new Error(`timed out acquiring daemon lock: ${lockFile}`);
785
1174
  };
1175
+ const reclaimDeadLockOwner = async (lockFile) => {
1176
+ const observed = await readLockSource(lockFile);
1177
+ if (observed === void 0) return true;
1178
+ const owner = parseLockOwner(observed);
1179
+ if (owner === void 0 || isProcessAlive(owner.pid)) return false;
1180
+ const claimFile = `${lockFile}.reclaim-${createHash("sha256").update(observed).digest("hex").slice(0, 16)}`;
1181
+ try {
1182
+ await link(lockFile, claimFile);
1183
+ } catch (error) {
1184
+ if (isNodeError(error) && error.code === "ENOENT") return true;
1185
+ if (isNodeError(error) && error.code === "EEXIST") {
1186
+ await clearAbandonedReclaimClaim(claimFile);
1187
+ return false;
1188
+ }
1189
+ throw error;
1190
+ }
1191
+ try {
1192
+ if (await readLockSource(lockFile) !== observed || isProcessAlive(owner.pid)) return false;
1193
+ try {
1194
+ await unlink(lockFile);
1195
+ return true;
1196
+ } catch (error) {
1197
+ if (isNodeError(error) && error.code === "ENOENT") return true;
1198
+ throw error;
1199
+ }
1200
+ } finally {
1201
+ await unlink(claimFile).catch((error) => {
1202
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
1203
+ });
1204
+ }
1205
+ };
1206
+ const clearAbandonedReclaimClaim = async (claimFile) => {
1207
+ try {
1208
+ const claim = await stat(claimFile);
1209
+ if (Date.now() - claim.ctimeMs < ABANDONED_RECLAIM_CLAIM_MS) return;
1210
+ await unlink(claimFile);
1211
+ } catch (error) {
1212
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
1213
+ }
1214
+ };
1215
+ const readLockSource = async (lockFile) => {
1216
+ try {
1217
+ return await readFile(lockFile, "utf8");
1218
+ } catch (error) {
1219
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
1220
+ throw error;
1221
+ }
1222
+ };
1223
+ const parseLockOwner = (source) => {
1224
+ const legacyPid = Number(source.trim());
1225
+ if (Number.isInteger(legacyPid) && legacyPid > 0) return { pid: legacyPid };
1226
+ let value;
1227
+ try {
1228
+ value = JSON.parse(source);
1229
+ } catch {
1230
+ return;
1231
+ }
1232
+ if (!isRecord(value) || typeof value.pid !== "number" || !Number.isInteger(value.pid) || value.pid <= 0 || typeof value.token !== "string" || value.token.length === 0) return;
1233
+ return { pid: value.pid };
1234
+ };
786
1235
  const isProcessAlive = (pid) => {
787
1236
  try {
788
1237
  process.kill(pid, 0);
789
1238
  return true;
790
- } catch {
791
- return false;
1239
+ } catch (error) {
1240
+ return !(isNodeError(error) && error.code === "ESRCH");
792
1241
  }
793
1242
  };
794
1243
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -827,7 +1276,8 @@ const resolveDaemonPaths = ({ homeDir, packageVersion, callerLabel, appId, appNa
827
1276
  endpoint,
828
1277
  pidFile: `${runtimeDir}/broker.pid`,
829
1278
  lockFile: `${runtimeDir}/broker.lock`,
830
- readyFile: `${runtimeDir}/ready.json`
1279
+ readyFile: `${runtimeDir}/ready.json`,
1280
+ brokerLog: `${runtimeDir}/broker.log`
831
1281
  };
832
1282
  };
833
1283
  const normalizeAppIdentityField$1 = (value, fallback) => {
@@ -854,7 +1304,16 @@ const connectLocalBroker = async (options = {}) => {
854
1304
  const autoStart = options.autoStart ?? endpoint === paths.endpoint;
855
1305
  if (autoStart && endpoint !== paths.endpoint) throw new Error("local broker autoStart requires the derived same-version endpoint");
856
1306
  const cliEntrypoint = options.cliEntrypoint ?? resolveCliEntrypoint();
857
- const driver = options.daemonDriver ?? createNodeDaemonDriver(cliEntrypoint);
1307
+ const packageIdentity = process.platform === "darwin" && autoStart ? await resolveOpenTrayPackageIdentity({
1308
+ ...options.packageName === void 0 ? {} : { packageName: options.packageName },
1309
+ ...options.packageRoot === void 0 ? {} : { packageRoot: options.packageRoot }
1310
+ }) : void 0;
1311
+ const appBundle = process.platform === "darwin" && autoStart ? resolveDarwinAppBundleOptions(options.appBundle, paths, packageIdentity) : void 0;
1312
+ const driver = options.daemonDriver ?? createNodeDaemonDriver(cliEntrypoint, {
1313
+ ...appBundle === void 0 ? {} : { appBundle },
1314
+ ...options.appIcon === void 0 ? {} : { appIcon: options.appIcon },
1315
+ ...packageIdentity === void 0 ? {} : { packageIdentity }
1316
+ });
858
1317
  const expectedBrokerArtifactIdentity = autoStart ? (await startDaemon({
859
1318
  paths,
860
1319
  driver
@@ -862,12 +1321,51 @@ const connectLocalBroker = async (options = {}) => {
862
1321
  const connection = new LocalBrokerConnection(await connectSocket(endpoint), endpoint, callerLabel);
863
1322
  try {
864
1323
  await connection.init(clientVersion, options.protocolVersion ?? PROTOCOL_VERSION$1, expectedBrokerArtifactIdentity);
1324
+ if (appBundle !== void 0 && options.appLaunch !== void 0) {
1325
+ await updateDarwinAppLaunchDescriptor(appBundle.path, options.appLaunch);
1326
+ const convergence = await convergeDarwinAppBundleIdentity({
1327
+ currentBundlePath: appBundle.path,
1328
+ appId: paths.appId,
1329
+ managedAppsRoot: resolve(paths.homeDir, ".opentray/apps"),
1330
+ legacyRuntimeRoot: resolve(paths.homeDir, ".opentray"),
1331
+ legacyBundlePaths: [resolve(paths.runtimeDir, "darwin-carrier/OpenTray.app")]
1332
+ });
1333
+ await appendBrokerDiagnostic(paths.brokerLog, {
1334
+ event: "bundle-identity-convergence",
1335
+ removed: convergence.removed,
1336
+ skippedLiveOwner: convergence.skippedLiveOwner,
1337
+ failedUnregister: convergence.failedUnregister
1338
+ });
1339
+ }
865
1340
  } catch (error) {
866
1341
  await connection.close();
867
1342
  throw error;
868
1343
  }
869
1344
  return connection;
870
1345
  };
1346
+ const appendBrokerDiagnostic = async (path, value) => {
1347
+ try {
1348
+ await mkdir(dirname(path), { recursive: true });
1349
+ await appendFile(path, `${JSON.stringify({
1350
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1351
+ ...value
1352
+ })}\n`, "utf8");
1353
+ } catch {}
1354
+ };
1355
+ const resolveDarwinAppBundleOptions = (configured, paths, packageIdentity) => {
1356
+ if (packageIdentity === void 0) throw new Error("Darwin app bundle resolution requires the caller package identity");
1357
+ const configuredPath = configured?.path;
1358
+ return {
1359
+ path: configuredPath === void 0 ? resolveDefaultDarwinAppBundlePath({
1360
+ homeDir: paths.homeDir,
1361
+ packageName: packageIdentity.name,
1362
+ appName: paths.appName
1363
+ }) : typeof configuredPath === "string" ? isAbsolute(configuredPath) ? configuredPath : resolve(packageIdentity.root, configuredPath) : configuredPath.protocol === "file:" ? fileURLToPath(configuredPath) : (() => {
1364
+ throw new Error(`Darwin appBundle.path must be a file URL: ${configuredPath.href}`);
1365
+ })(),
1366
+ ...configured?.reinitialize === void 0 ? {} : { reinitialize: configured.reinitialize }
1367
+ };
1368
+ };
871
1369
  const normalizeAppIdentityField = (value) => {
872
1370
  const trimmed = value?.trim();
873
1371
  return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
@@ -980,7 +1478,7 @@ var LocalBrokerConnection = class {
980
1478
  this.pending.delete(requestId);
981
1479
  return;
982
1480
  }
983
- if (frame.type === "event" || frame.type === "ext-event") for (const listener of this.listeners) listener(frame);
1481
+ if (frame.type === "event" || frame.type === "app-event" || frame.type === "ext-event") for (const listener of this.listeners) listener(frame);
984
1482
  }
985
1483
  write(frame) {
986
1484
  this.socket.write(`${JSON.stringify(frame)}\n`);
@@ -1012,6 +1510,7 @@ const responseRequestId = (frame) => {
1012
1510
  case "runtime-host-health": return frame.requestId;
1013
1511
  case "ready":
1014
1512
  case "event":
1513
+ case "app-event":
1015
1514
  case "ext-event":
1016
1515
  case "error": return;
1017
1516
  }
@@ -1139,14 +1638,53 @@ const registerClickId = (state, id) => {
1139
1638
  state.seenClickIds = seen;
1140
1639
  };
1141
1640
  //#endregion
1641
+ //#region src/app-launch.ts
1642
+ const normalizeAppLaunch = (configured, context = {}) => {
1643
+ const currentCwd = context.cwd ?? process.cwd();
1644
+ if (configured === void 0 || configured === null) return {
1645
+ schemaVersion: 1,
1646
+ command: validateRequiredText(context.execPath ?? process.execPath, "process.execPath"),
1647
+ args: [...(context.argv ?? process.argv).slice(1)].map((arg, index) => validateArg(arg, `process.argv[${index + 1}]`)),
1648
+ cwd: validateRequiredText(currentCwd, "process.cwd()")
1649
+ };
1650
+ const command = validateRequiredText(configured.command, "appLaunch.command");
1651
+ const cwd = resolve(currentCwd, configured.cwd === void 0 ? currentCwd : validateRequiredText(configured.cwd, "appLaunch.cwd"));
1652
+ const args = [...configured.args ?? []].map((arg, index) => validateArg(arg, `appLaunch.args[${index}]`));
1653
+ return {
1654
+ schemaVersion: 1,
1655
+ command: resolvePathLikeCommand(command, currentCwd),
1656
+ args,
1657
+ cwd
1658
+ };
1659
+ };
1660
+ const resolvePathLikeCommand = (command, cwd) => {
1661
+ if (isAbsolute(command) || command.includes("/") || command.includes("\\")) return resolve(cwd, command);
1662
+ return command;
1663
+ };
1664
+ const validateRequiredText = (value, field) => {
1665
+ if (value.trim().length === 0 || value.includes("\0")) throw new Error(`${field} must be a non-empty string without NUL`);
1666
+ return value;
1667
+ };
1668
+ const validateArg = (value, field) => {
1669
+ if (value.includes("\0")) throw new Error(`${field} must be a string without NUL`);
1670
+ return value;
1671
+ };
1672
+ //#endregion
1142
1673
  //#region src/sdk.ts
1143
1674
  const createTray = async (options, runtimeOptions = {}) => {
1675
+ if (runtimeOptions.appIcon !== void 0) await validateAppIcon(runtimeOptions.appIcon);
1676
+ const appIcon = runtimeOptions.appIcon === void 0 ? void 0 : normalizeAppIcon(runtimeOptions.appIcon);
1144
1677
  const normalized = normalizeCreateTrayOptions(options);
1145
- const connection = await connectLocalBroker(runtimeOptions);
1678
+ const appLaunch = normalizeAppLaunch(runtimeOptions.appLaunch);
1679
+ const connection = await connectLocalBroker({
1680
+ ...runtimeOptions,
1681
+ ...appIcon === void 0 ? {} : { appIcon },
1682
+ appLaunch
1683
+ });
1146
1684
  try {
1147
1685
  const tray = await createClient(connection, { appOptions: {
1148
1686
  ...runtimeOptions.appName === void 0 ? {} : { name: runtimeOptions.appName },
1149
- ...runtimeOptions.appIcon === void 0 ? {} : { icon: runtimeOptions.appIcon }
1687
+ ...appIcon === void 0 ? {} : { appIcon }
1150
1688
  } }).createTray(normalized.options);
1151
1689
  return wrapCreateTrayHandle(tray, {
1152
1690
  closeConnection: () => connection.close(),
@@ -1211,6 +1749,6 @@ const bindMenuClickHandlers = (tray, handlers) => {
1211
1749
  };
1212
1750
  const noop = () => {};
1213
1751
  //#endregion
1214
- export { NativeExtensionArtifactResolutionError, PROTOCOL_VERSION, createBrokerEndpointIdentity, createClient, createInitFrame, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion, resolveNativeExtensionArtifact };
1752
+ export { AppIconVariantNotFoundError, DEFAULT_APP_ICON_VARIANT, InvalidAppIconError, NativeExtensionArtifactResolutionError, PROTOCOL_VERSION, createBrokerEndpointIdentity, createClient, createInitFrame, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion, resolveNativeExtensionArtifact, validateAppIcon };
1215
1753
 
1216
1754
  //# sourceMappingURL=index.mjs.map