sliccy 6.154.1 → 6.154.3

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.
Files changed (190) hide show
  1. package/dist/node-server/browser-shutdown.d.ts +6 -1
  2. package/dist/node-server/browser-shutdown.js +3 -3
  3. package/dist/node-server/chrome-launch.d.ts +3 -0
  4. package/dist/node-server/chrome-launch.js +2 -1
  5. package/dist/node-server/electron-controller.d.ts +63 -2
  6. package/dist/node-server/electron-controller.js +86 -48
  7. package/dist/node-server/electron-runtime.d.ts +15 -1
  8. package/dist/node-server/electron-runtime.js +10 -10
  9. package/dist/node-server/fetch-proxy-gzip.js +5 -7
  10. package/dist/node-server/hosted-bootstrap.d.ts +1 -0
  11. package/dist/node-server/hosted-bootstrap.js +12 -14
  12. package/dist/node-server/hostfs.d.ts +37 -1
  13. package/dist/node-server/hostfs.js +5 -5
  14. package/dist/node-server/install-cli.d.ts +1 -0
  15. package/dist/node-server/install-cli.js +1 -1
  16. package/dist/node-server/launch-url.js +2 -6
  17. package/dist/node-server/leader-restart.d.ts +9 -0
  18. package/dist/node-server/leader-restart.js +1 -1
  19. package/dist/node-server/publish-chrome-web-store-main.d.ts +1 -0
  20. package/dist/node-server/publish-chrome-web-store-main.js +13 -0
  21. package/dist/node-server/publish-chrome-web-store.d.ts +2 -0
  22. package/dist/node-server/publish-chrome-web-store.js +2 -15
  23. package/dist/node-server/release-package-main.d.ts +1 -0
  24. package/dist/node-server/release-package-main.js +17 -0
  25. package/dist/node-server/release-package.d.ts +38 -3
  26. package/dist/node-server/release-package.js +46 -50
  27. package/dist/node-server/routes/fetch-proxy.d.ts +40 -1
  28. package/dist/node-server/routes/fetch-proxy.js +20 -12
  29. package/dist/node-server/routes/secrets.d.ts +23 -1
  30. package/dist/node-server/routes/secrets.js +34 -30
  31. package/dist/node-server/secrets/proxy-manager.d.ts +2 -1
  32. package/dist/node-server/secrets/proxy-manager.js +27 -26
  33. package/dist/node-server/secrets/session-id-file.d.ts +5 -1
  34. package/dist/node-server/secrets/session-id-file.js +4 -4
  35. package/dist/node-server/secrets/sign-and-forward.d.ts +8 -0
  36. package/dist/node-server/secrets/sign-and-forward.js +1 -1
  37. package/dist/node-server/sudo/select.d.ts +2 -0
  38. package/dist/node-server/sudo/select.js +1 -1
  39. package/dist/node-server/sudo/tty-backend.d.ts +1 -0
  40. package/dist/node-server/sudo/tty-backend.js +1 -1
  41. package/dist/ui/.vite/manifest.json +171 -171
  42. package/dist/ui/assets/SKILL-BGww2dFz.js +1 -0
  43. package/dist/ui/assets/{SKILL-cqUV8C7U.js → SKILL-Cl9PVarg.js} +1 -1
  44. package/dist/ui/assets/{account-store-BqdqUcgS.js → account-store-BYDUpeqo.js} +1 -1
  45. package/dist/ui/assets/{account-store-BgFuxoIt.js → account-store-BvEgWXDi.js} +1 -1
  46. package/dist/ui/assets/{adobe-BWEqvpfE.js → adobe-Bf44Mdem.js} +1 -1
  47. package/dist/ui/assets/{adobe-De7VHe-t.js → adobe-DIeHS2Yx.js} +1 -1
  48. package/dist/ui/assets/{agent-bridge-EXV0qNBB.js → agent-bridge-pmiSNwoc.js} +1 -1
  49. package/dist/ui/assets/{agent-panels-dcldnH3V.js → agent-panels-CWBbnFvW.js} +1 -1
  50. package/dist/ui/assets/api-endpoint-3qEcn7BY.js +1 -0
  51. package/dist/ui/assets/{apply-layout-DVgK-NlB.js → apply-layout-Eft8a3mq.js} +1 -1
  52. package/dist/ui/assets/{apps-BqrASr5C.js → apps-BklKPyC6.js} +1 -1
  53. package/dist/ui/assets/{auto-mount-table-0jMTi9rV.js → auto-mount-table-BDyGIOIb.js} +1 -1
  54. package/dist/ui/assets/{azure-openai-vXGFZfPE.js → azure-openai-7VfJn11R.js} +1 -1
  55. package/dist/ui/assets/{azure-openai-94kdulWT.js → azure-openai-o6cG2UDZ.js} +1 -1
  56. package/dist/ui/assets/{backend-hostfs-BxfjZHzO.js → backend-hostfs-BJU2QL9q.js} +1 -1
  57. package/dist/ui/assets/{budget-usage-source-BNEsxDHX.js → budget-usage-source-B5GSAynq.js} +1 -1
  58. package/dist/ui/assets/{cdp-D0GOzfX9.js → cdp-Di9nU0k7.js} +1 -1
  59. package/dist/ui/assets/{cerebras-eckgXNEU.js → cerebras-BRpdCI1J.js} +1 -1
  60. package/dist/ui/assets/{cerebras-v41u2mpJ.js → cerebras-DuUgRhfz.js} +1 -1
  61. package/dist/ui/assets/{client-BOSdqiLb.js → client-B6iKb_ye.js} +1 -1
  62. package/dist/ui/assets/{composer-speech-ButVgF9_.js → composer-speech-Chlrvbhg.js} +1 -1
  63. package/dist/ui/assets/{connect-surface-C0WXwJHl.js → connect-surface-DBXtjfUP.js} +1 -1
  64. package/dist/ui/assets/{core-D59qaXEb.js → core-C9qgaWgE.js} +2 -2
  65. package/dist/ui/assets/{directed-approval-D1CLB6cG.js → directed-approval-C2OTs8pv.js} +1 -1
  66. package/dist/ui/assets/dist-BPPG9gF6.js +12 -0
  67. package/dist/ui/assets/dist-BnmWpUNI.js +18 -0
  68. package/dist/ui/assets/{dist-DCrgApJQ.js → dist-CFI3XeIO.js} +1 -1
  69. package/dist/ui/assets/{dist-UJah3hVh.js → dist-Ciikigd5.js} +1 -1
  70. package/dist/ui/assets/{ensure-speech-assets-uGHvkmNC.js → ensure-speech-assets-DOu9EHnU.js} +1 -1
  71. package/dist/ui/assets/{extension-ops-B9_1VB7W.js → extension-ops-B05mAXsq.js} +1 -1
  72. package/dist/ui/assets/{fs-DK3k_wQh.js → fs-Dqh17T5J.js} +2 -2
  73. package/dist/ui/assets/{fs-mC_RRTZZ.js → fs-T4T9WqhX.js} +1 -1
  74. package/dist/ui/assets/{github-CT-Oyj_0.js → github-CL_7yoHL.js} +1 -1
  75. package/dist/ui/assets/github-auth-A6Mopmg8.js +1 -0
  76. package/dist/ui/assets/{github-copilot-CACLYChL.js → github-copilot-B2ugBoRh.js} +1 -1
  77. package/dist/ui/assets/{github-copilot-D2H-jvYu.js → github-copilot-_5qoxGCT.js} +1 -1
  78. package/dist/ui/assets/github-install-8yCqacop.js +1 -0
  79. package/dist/ui/assets/{github-BLOM522m.js → github-koUNqi7Y.js} +2 -2
  80. package/dist/ui/assets/github-zip-CV41OIqx.js +1 -0
  81. package/dist/ui/assets/{guest-tool-gate-6uzbOlEb.js → guest-tool-gate-_NuvhpkB.js} +1 -1
  82. package/dist/ui/assets/{hear-BXcOoxVl.js → hear-DScAQlnK.js} +1 -1
  83. package/dist/ui/assets/{help-BAqixBa0.js → help-BvOxNE_5.js} +1 -1
  84. package/dist/ui/assets/{js-realm-worker-CRY5Fv-C.js → js-realm-worker-fE5Mi5F2.js} +1 -1
  85. package/dist/ui/assets/{kernel-worker-C1DLWYBM.js → kernel-worker-DURzH7I0.js} +586 -585
  86. package/dist/ui/assets/{lick-urls-HYeTtmbf.js → lick-urls-BZVq_P3x.js} +1 -1
  87. package/dist/ui/assets/{lick-ws-bridge-DqmithfF.js → lick-ws-bridge-CDToX4F0.js} +1 -1
  88. package/dist/ui/assets/{local-llm-D723hPLf.js → local-llm-D_HQ1XG8.js} +1 -1
  89. package/dist/ui/assets/{local-llm-Cv98D1SL.js → local-llm-QN3Afs2P.js} +1 -1
  90. package/dist/ui/assets/{main-C-pVQBFE.js → main-DyPgoSsm.js} +3 -3
  91. package/dist/ui/assets/{main-cherry-BdH-nz0p.js → main-cherry-v6ksBa29.js} +1 -1
  92. package/dist/ui/assets/{memory-curation-seam-eUVjWT6W.js → memory-curation-seam-DXGqoWuF.js} +1 -1
  93. package/dist/ui/assets/{mount-3VFe3Pa7.js → mount-CdTXGHvm.js} +3 -3
  94. package/dist/ui/assets/{mount-Dh4tJ6XQ.js → mount-Drg9K5Dc.js} +1 -1
  95. package/dist/ui/assets/{mount-bridge-client-B5qLR9Xe.js → mount-bridge-client-DWY67yip.js} +1 -1
  96. package/dist/ui/assets/{mount-recovery-Cp7KYqTq.js → mount-recovery-CUhmg6UT.js} +1 -1
  97. package/dist/ui/assets/{new-session-hwwKqrO5.js → new-session-BBCvIUGf.js} +2 -2
  98. package/dist/ui/assets/{oauth-bootstrap-BP7bAgwJ.js → oauth-bootstrap-37qdAFNO.js} +2 -2
  99. package/dist/ui/assets/{oauth-service-BZl-9EoE.js → oauth-service-IlBYPvwr.js} +1 -1
  100. package/dist/ui/assets/{onboarding-orchestrator-DOOTJ9OG.js → onboarding-orchestrator-CgzN2Wut.js} +1 -1
  101. package/dist/ui/assets/{openai-codex--3foibzj.js → openai-codex-CByg7ExV.js} +1 -1
  102. package/dist/ui/assets/{openai-codex-vGpH6Ldz.js → openai-codex-yA937LOi.js} +1 -1
  103. package/dist/ui/assets/{openrouter-D17vOiK5.js → openrouter-Bq9TiHqe.js} +1 -1
  104. package/dist/ui/assets/{openrouter-B3v5OQPe.js → openrouter-D1YZxJcz.js} +1 -1
  105. package/dist/ui/assets/{openrouter-free-Mha-ykI6.js → openrouter-free-BwEyNzqx.js} +1 -1
  106. package/dist/ui/assets/{openrouter-free-DndyeHH8.js → openrouter-free-Db1fart9.js} +1 -1
  107. package/dist/ui/assets/{openrouter-oauth-C4aGXNeF.js → openrouter-oauth-CFlNuCUA.js} +1 -1
  108. package/dist/ui/assets/{openrouter-oauth-ov7dnUnh.js → openrouter-oauth-Cj61jTQa.js} +1 -1
  109. package/dist/ui/assets/{panel-rpc-handlers-CDsWA3s8.js → panel-rpc-handlers-CLsTGXPT.js} +2 -2
  110. package/dist/ui/assets/{panelize-shell-CXpZWzgn.js → panelize-shell-Bf9yr7Qf.js} +1 -1
  111. package/dist/ui/assets/{port-bridge-client-CzOd_VUf.js → port-bridge-client-Ds3xwapn.js} +1 -1
  112. package/dist/ui/assets/provenance-DdmsQ3Nr.js +1 -0
  113. package/dist/ui/assets/{provider-BtT9_ZsY.js → provider-BD-0pPN8.js} +1 -1
  114. package/dist/ui/assets/{provider-2fDgJArv.js → provider-C66yENxs.js} +2 -2
  115. package/dist/ui/assets/provider-settings-DysstkNf.js +1 -0
  116. package/dist/ui/assets/provider-store-access-BeX0OGEs.js +1 -0
  117. package/dist/ui/assets/provider-store-access-Cc45yVjV.js +1 -0
  118. package/dist/ui/assets/{providers-BWmPj6ob.js → providers-BtR_Oc-m.js} +2 -2
  119. package/dist/ui/assets/{providers-D_mm-oKv.js → providers-C2bj3jKv.js} +1 -1
  120. package/dist/ui/assets/proxied-fetch-DEK8DT-z.js +1 -0
  121. package/dist/ui/assets/{quick-llm-Ch_X9AH9.js → quick-llm-D-nM71kO.js} +1 -1
  122. package/dist/ui/assets/{recovery-screen-Ldb8xS5S.js → recovery-screen-YuMG99R0.js} +1 -1
  123. package/dist/ui/assets/{redirect-uri-C6BjkDE0.js → redirect-uri-CgKdq7-K.js} +1 -1
  124. package/dist/ui/assets/rest-ops-Bps3Ay-k.js +1 -0
  125. package/dist/ui/assets/{run-DXUToOLa.js → run-CDicdmnK.js} +1 -1
  126. package/dist/ui/assets/{run-Bmb6g7U2.js → run-CdzvBpS0.js} +3 -3
  127. package/dist/ui/assets/{run-xG3-BOwR.js → run-D5LoTYwZ.js} +1 -1
  128. package/dist/ui/assets/{secrets-bridge-client-DBeju3mP.js → secrets-bridge-client-ZpBL1Zhl.js} +1 -1
  129. package/dist/ui/assets/{session-jsonl-DlNZajGM.js → session-jsonl-By2mMQqb.js} +1 -1
  130. package/dist/ui/assets/{setup-feature-flags-remote-D-IPtvuE.js → setup-feature-flags-remote-BuBwyibG.js} +1 -1
  131. package/dist/ui/assets/sidecar-repair-DRkuMyU_.js +1 -0
  132. package/dist/ui/assets/{silent-renew-backoff-BocHwLdw.js → silent-renew-backoff-CS_htRXY.js} +1 -1
  133. package/dist/ui/assets/{skills-CTDnQTUW.js → skills-Cg34jB8I.js} +1 -1
  134. package/dist/ui/assets/slicc-version-DbT116fI.js +1 -0
  135. package/dist/ui/assets/{snapshot-BZ-_n6uU.js → snapshot-B6SF0dpr.js} +1 -1
  136. package/dist/ui/assets/{snapshot-features-BWzc1ERL.js → snapshot-features-A-jmNbR9.js} +1 -1
  137. package/dist/ui/assets/{soundscape-CowYlYSr.js → soundscape-smt9sQsm.js} +1 -1
  138. package/dist/ui/assets/{speak-rCiisrK7.js → speak-DeYQY3R8.js} +1 -1
  139. package/dist/ui/assets/{sprinkle-manager-DaVvW4Pl.js → sprinkle-manager-q3Ku7Zvz.js} +1 -1
  140. package/dist/ui/assets/{sprinkle-renderer-CDcbztf1.js → sprinkle-renderer-B9Re-_rl.js} +1 -1
  141. package/dist/ui/assets/{state-DHftNsPN.js → state-NPdZC8rd.js} +1 -1
  142. package/dist/ui/assets/{store-CTM4679Z.js → store-BbRPb7Di.js} +1 -1
  143. package/dist/ui/assets/{store-MZJeSOYF.js → store-BfdKxUK3.js} +1 -1
  144. package/dist/ui/assets/{sudo-DLRz2gLx.js → sudo-CtUlj7iw.js} +1 -1
  145. package/dist/ui/assets/{sync-dialog-BUFCYPxf.js → sync-dialog-CXgM1Uvh.js} +1 -1
  146. package/dist/ui/assets/{transformers-env-BfjOXz9f.js → transformers-env-C7dJtjmn.js} +1 -1
  147. package/dist/ui/assets/{tray-runtime-config--rtR6Gxd.js → tray-runtime-config-CRZxBChd.js} +1 -1
  148. package/dist/ui/assets/{tray-sidecar-BP70hrzZ.js → tray-sidecar-BtFzGslJ.js} +1 -1
  149. package/dist/ui/assets/types-Dzr0qYl3.js +1 -0
  150. package/dist/ui/assets/{upgrade-detection-Be8A7nY7.js → upgrade-detection-BWpOvoVx.js} +1 -1
  151. package/dist/ui/assets/upskill-lick-skip-ZGNBIEXS.js +1 -0
  152. package/dist/ui/assets/{voice-reply-BtqQr2DX.js → voice-reply-CCkglCEp.js} +1 -1
  153. package/dist/ui/assets/{wc-attach-zW7DuRo7.js → wc-attach-C8CgBet1.js} +2 -2
  154. package/dist/ui/assets/{wc-extension-DGYx9jMG.js → wc-extension-CHb4P34s.js} +2 -2
  155. package/dist/ui/assets/{wc-follower-Bc6Mmmda.js → wc-follower-DmIZM7ao.js} +3 -3
  156. package/dist/ui/assets/{wc-follower-oauth-DOaUEVw8.js → wc-follower-oauth-CQgoqG9v.js} +1 -1
  157. package/dist/ui/assets/{wc-gelatiere-CdqOnO6V.js → wc-gelatiere-B8JhDjog.js} +1 -1
  158. package/dist/ui/assets/{wc-live-BrKyGs-7.js → wc-live-gCevk2Vh.js} +5 -5
  159. package/dist/ui/assets/{wc-nav-dSAbBsA5.js → wc-nav-CpHEdfwe.js} +2 -2
  160. package/dist/ui/assets/{wc-onboarding-C1uyya9g.js → wc-onboarding-iwpabkC3.js} +2 -2
  161. package/dist/ui/assets/{wc-placeholder-Cw7uGVuJ.js → wc-placeholder-Cnl3OQfj.js} +3 -3
  162. package/dist/ui/assets/{wc-secret-request-CwA6DiWC.js → wc-secret-request-B9oB6kAt.js} +1 -1
  163. package/dist/ui/assets/{wc-settings-DJEhDr23.js → wc-settings-_0roTDdf.js} +3 -3
  164. package/dist/ui/assets/{wc-shell-C-DwVQCG.js → wc-shell-DkJPUxGT.js} +3 -3
  165. package/dist/ui/assets/{wc-shortcut-config-D9HBEfqv.js → wc-shortcut-config-CIup4nE8.js} +1 -1
  166. package/dist/ui/assets/{wc-shortcuts-7i4KncH2.js → wc-shortcuts-C9SNe-vm.js} +1 -1
  167. package/dist/ui/assets/{wc-sprinkles-DsRJYYRI.js → wc-sprinkles-C7_z9Yfm.js} +2 -2
  168. package/dist/ui/assets/{wc-transcript-export-Dq9HNkpA.js → wc-transcript-export-DnNvjokD.js} +2 -2
  169. package/dist/ui/assets/{wc-tray-f6xi76Bq.js → wc-tray-DhWe0-Ie.js} +4 -4
  170. package/dist/ui/assets/welcome-detection-BbyyaOna.js +1 -0
  171. package/dist/ui/assets/{whisper-session-B6knWfce.js → whisper-session-CMXLPRdV.js} +2 -2
  172. package/dist/ui/assets/{xai-grok-B0KRInrx.js → xai-grok-BNRMntfw.js} +1 -1
  173. package/dist/ui/assets/{xai-grok-BIfBT2BL.js → xai-grok-BiF6I0r6.js} +1 -1
  174. package/dist/ui/index.html +3 -3
  175. package/dist/ui/packages/webapp/index.html +3 -3
  176. package/package.json +3 -3
  177. package/dist/ui/assets/SKILL-CuHGVKei.js +0 -1
  178. package/dist/ui/assets/api-endpoint-Lx_FU5cP.js +0 -1
  179. package/dist/ui/assets/dist-B3W7EgGh.js +0 -12
  180. package/dist/ui/assets/dist-CmI2JIly.js +0 -18
  181. package/dist/ui/assets/github-install-Bk-KTJD-.js +0 -1
  182. package/dist/ui/assets/github-zip-Dvb4zHOl.js +0 -1
  183. package/dist/ui/assets/provider-settings-D2AYwHOa.js +0 -1
  184. package/dist/ui/assets/provider-store-access-BdvWm9Kj.js +0 -1
  185. package/dist/ui/assets/provider-store-access-C5A5Se3D.js +0 -1
  186. package/dist/ui/assets/proxied-fetch-B57RqgUb.js +0 -1
  187. package/dist/ui/assets/rest-ops-CB2up_K0.js +0 -1
  188. package/dist/ui/assets/sidecar-repair-DRUvDMSe.js +0 -1
  189. package/dist/ui/assets/slicc-version-CWl4uhV3.js +0 -1
  190. package/dist/ui/assets/welcome-detection-DmqQ-hO4.js +0 -1
@@ -1,7 +1,12 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
+ import WebSocket from 'ws';
2
3
  export interface LaunchedBrowserHandle {
3
4
  launchedBrowserProcess: ChildProcess | null;
4
5
  launchedBrowserLabel: string;
5
6
  }
7
+ export interface BrowserShutdownDependencies {
8
+ fetchImpl?: typeof fetch;
9
+ createWebSocket?: (url: string) => WebSocket;
10
+ }
6
11
  /** Best-effort graceful close of the launched browser, escalating to SIGKILL. */
7
- export declare function closeLaunchedBrowserGracefully(state: LaunchedBrowserHandle, cdpPort: number): Promise<void>;
12
+ export declare function closeLaunchedBrowserGracefully(state: LaunchedBrowserHandle, cdpPort: number, dependencies?: BrowserShutdownDependencies): Promise<void>;
@@ -1,16 +1,16 @@
1
1
  import WebSocket from 'ws';
2
2
  import { probeCdpAlive } from './chrome-launch.js';
3
3
  /** Best-effort graceful close of the launched browser, escalating to SIGKILL. */
4
- export async function closeLaunchedBrowserGracefully(state, cdpPort) {
4
+ export async function closeLaunchedBrowserGracefully(state, cdpPort, dependencies = {}) {
5
5
  const browser = state.launchedBrowserProcess;
6
6
  if (!browser)
7
7
  return;
8
8
  try {
9
- const res = await fetch(`http://127.0.0.1:${cdpPort}/json/version`, {
9
+ const res = await (dependencies.fetchImpl ?? fetch)(`http://127.0.0.1:${cdpPort}/json/version`, {
10
10
  signal: AbortSignal.timeout(500),
11
11
  });
12
12
  const json = (await res.json());
13
- const browserWs = new WebSocket(json.webSocketDebuggerUrl);
13
+ const browserWs = (dependencies.createWebSocket ?? ((url) => new WebSocket(url)))(json.webSocketDebuggerUrl);
14
14
  await new Promise((resolve, reject) => {
15
15
  browserWs.on('open', () => {
16
16
  try {
@@ -1,5 +1,6 @@
1
1
  import type { ChildProcess } from 'child_process';
2
2
  import { existsSync, readdirSync } from 'fs';
3
+ import { request as httpRequest } from 'http';
3
4
  /**
4
5
  * Default startup timeout for Chrome's CDP listener. Overridable via the
5
6
  * `SLICC_CDP_LAUNCH_TIMEOUT_MS` environment variable so cold/contended CI
@@ -235,6 +236,8 @@ export interface ProbeCdpAliveOptions {
235
236
  * for ours.
236
237
  */
237
238
  expectedWebSocketPath?: string | null;
239
+ /** Test seam for synchronous request-construction failures. */
240
+ requestImpl?: typeof httpRequest;
238
241
  }
239
242
  /**
240
243
  * Single-shot HTTP probe of Chrome's `/json/version` endpoint. Resolves
@@ -800,6 +800,7 @@ function defaultPidIsAlive(pid) {
800
800
  export function probeCdpAlive(port, options = {}) {
801
801
  const timeoutMs = options.timeoutMs ?? 500;
802
802
  const expectedWebSocketPath = options.expectedWebSocketPath ?? null;
803
+ const requestImpl = options.requestImpl ?? httpRequest;
803
804
  return new Promise((resolve) => {
804
805
  let resolved = false;
805
806
  const settle = (alive) => {
@@ -819,7 +820,7 @@ export function probeCdpAlive(port, options = {}) {
819
820
  }
820
821
  let req;
821
822
  try {
822
- req = httpRequest({
823
+ req = requestImpl({
823
824
  host: '127.0.0.1',
824
825
  port,
825
826
  path: '/json/version',
@@ -1,5 +1,7 @@
1
- import { BRIDGE_ROLE_FOLLOWER, BRIDGE_ROLE_LEADER, BRIDGE_ROLE_QUERY_PARAM } from './_shared/index.js';
2
- import { type ChildProcess } from 'child_process';
1
+ import { BRIDGE_ROLE_FOLLOWER, BRIDGE_ROLE_LEADER, BRIDGE_ROLE_QUERY_PARAM, type CDPPayload } from './_shared/index.js';
2
+ import { type ChildProcess, type SpawnOptions } from 'child_process';
3
+ import * as http from 'http';
4
+ import { WebSocket } from 'ws';
3
5
  import { type ElectronInspectableTarget } from './electron-runtime.js';
4
6
  export { BRIDGE_ROLE_FOLLOWER, BRIDGE_ROLE_LEADER, BRIDGE_ROLE_QUERY_PARAM };
5
7
  /**
@@ -72,6 +74,8 @@ interface RunningProcessInfo {
72
74
  commandLine: string;
73
75
  executablePath: string | null;
74
76
  }
77
+ /** Send a CDP command over a target WebSocket; returns the message id. */
78
+ export type CdpSend = (method: string, params?: CDPPayload) => number;
75
79
  /**
76
80
  * One element of a CDP `Network.Request.postDataEntries` array. `bytes` is the
77
81
  * protocol `binary` type — base64 in the JSON wire form, so it survives bodies
@@ -86,11 +90,31 @@ export declare function findMatchingElectronAppPids(runningProcesses: RunningPro
86
90
  export declare class ElectronAppAlreadyRunningError extends Error {
87
91
  constructor(message: string);
88
92
  }
93
+ export declare function parseUnixProcessList(stdout: string): RunningProcessInfo[];
94
+ export declare function parseWindowsProcessList(stdout: string): RunningProcessInfo[];
95
+ export declare function listRunningProcesses(platform?: NodeJS.Platform, run?: (command: string, args: string[]) => Promise<{
96
+ stdout: string;
97
+ }>): Promise<RunningProcessInfo[]>;
98
+ export declare function isPidAlive(pid: number, signal?: (pid: number, signal: 0) => void): boolean;
99
+ interface ProcessLifecycle {
100
+ isAlive: (pid: number) => boolean;
101
+ kill: (pid: number, signal?: NodeJS.Signals) => void;
102
+ now: () => number;
103
+ sleep: (milliseconds: number) => Promise<void>;
104
+ }
105
+ export declare function waitForPidsToExit(pids: number[], timeoutMs?: number, lifecycle?: ProcessLifecycle): Promise<boolean>;
106
+ export declare function terminateRunningApp(pids: number[], lifecycle?: ProcessLifecycle): Promise<void>;
107
+ export declare function findRunningElectronAppPids(appPath: string, platform?: NodeJS.Platform): Promise<number[]>;
89
108
  export declare function launchElectronApp(options: {
90
109
  appPath: string;
91
110
  cdpPort: number;
92
111
  kill: boolean;
93
112
  platform?: NodeJS.Platform;
113
+ }, dependencies?: {
114
+ exists?: (path: string) => boolean;
115
+ findRunningPids?: (appPath: string, platform?: NodeJS.Platform) => Promise<number[]>;
116
+ terminate?: (pids: number[]) => Promise<void>;
117
+ spawn?: (command: string, args: string[], options: SpawnOptions) => ChildProcess;
94
118
  }): Promise<{
95
119
  child: ChildProcess;
96
120
  displayName: string;
@@ -109,6 +133,12 @@ export declare function decodePngPixels(base64Data: string): {
109
133
  * sampling a grid of pixels for performance.
110
134
  */
111
135
  export declare function computeAverageLuminance(pixels: Buffer, width: number, height: number, sampleStep?: number): number;
136
+ /**
137
+ * Detect whether the target app is using a light or dark theme by taking
138
+ * a CDP screenshot and analyzing the average luminance.
139
+ * Returns 'light' or 'dark'.
140
+ */
141
+ export declare function detectAppThemeFromScreenshot(ws: WebSocket, send: CdpSend): Promise<'light' | 'dark'>;
112
142
  /**
113
143
  * Resolve the `Fetch.enable` origin pattern for CSP-bypass escalation. Mirrors
114
144
  * swift-server's `OverlayTargetSession.fetchProxyOrigin` (Wave 5): prefer the
@@ -159,6 +189,20 @@ export declare function decodeCdpRequestPostBody(request: {
159
189
  hasPostData?: boolean;
160
190
  postDataEntries?: CdpPostDataEntry[];
161
191
  }): CdpPostBody;
192
+ /**
193
+ * Translate a Node http(s) response's headers into the array shape required by
194
+ * `Fetch.fulfillRequest`, stripping CSP and other hop-by-hop headers that are
195
+ * invalid in fulfill responses and rewriting `content-length` to match the
196
+ * actually-buffered body length. Returns whether any CSP header was stripped
197
+ * so the caller can log it.
198
+ */
199
+ export declare function buildFulfillResponseHeaders(rawHeaders: http.IncomingHttpHeaders, contentLength: number): {
200
+ responseHeaders: Array<{
201
+ name: string;
202
+ value: string;
203
+ }>;
204
+ strippedCSP: boolean;
205
+ };
162
206
  /**
163
207
  * Pre-built bootstrap scripts for thin-mode injection — one per overlay
164
208
  * role. The injector picks `leader` for the first injected target and
@@ -175,6 +219,9 @@ export interface ThinBootstrapSet {
175
219
  */
176
220
  status: string;
177
221
  }
222
+ export declare function logOverlayReinjectionFailure(context: string, error: unknown): void;
223
+ export declare const logPresenceReinjectionFailure: (error: unknown) => void;
224
+ export declare const logNavigationReinjectionFailure: (error: unknown) => void;
178
225
  /**
179
226
  * Empty-viewport message shown by the status-only overlay for an app that
180
227
  * denies the overlay iframe's network egress. Kept in sync with swift-server's
@@ -265,6 +312,7 @@ export declare class ElectronOverlayInjector {
265
312
  private readonly onEgressBlocked?;
266
313
  private syncTimer;
267
314
  private syncing;
315
+ private readonly runScheduledSync;
268
316
  private constructor();
269
317
  static create(options: {
270
318
  cdpPort: number;
@@ -318,6 +366,18 @@ export declare class ElectronOverlayInjector {
318
366
  _testingEgressBlockedTargets(): ReadonlySet<string>;
319
367
  /** Test-only: seed a target as already egress-blocked. */
320
368
  _testingSeedEgressBlockedTarget(url: string): void;
369
+ /** Test-only: seed a connection so sync/stop cleanup can be driven deterministically. */
370
+ _testingSeedConnection(targetId: string, connection: Pick<WebSocket, 'close'>): void;
371
+ /** Test-only: drive the identity-guarded connection cleanup. */
372
+ _testingDropConnection(targetId: string, connection: WebSocket): void;
373
+ /** Test-only: invoke the same callback installed by `start()`. */
374
+ _testingRunScheduledSync(): Promise<void>;
375
+ /** Test-only: exercise the overlay-loaded timeout classifier directly. */
376
+ _testingProbeOverlayIframeLoaded(ws: WebSocket, send: CdpSend): Promise<boolean>;
377
+ /** Test-only: exercise the overlay-eviction timeout classifier directly. */
378
+ _testingProbeOverlayEvicted(ws: WebSocket, send: CdpSend): Promise<boolean>;
379
+ /** Test-only: drive a paused Fetch event without requiring the full escalation flow. */
380
+ _testingHandleFetchRequestPaused(ws: WebSocket, send: CdpSend, msg: unknown): void;
321
381
  /** Test-only: drive a single `syncTargets` pass without `start()`'s interval. */
322
382
  _testingSyncTargets(): Promise<void>;
323
383
  /** Test-only: close any sockets opened by `_testingConnectToTarget`. */
@@ -325,6 +385,7 @@ export declare class ElectronOverlayInjector {
325
385
  start(): Promise<void>;
326
386
  stop(): void;
327
387
  private syncTargets;
388
+ private dropConnectionIfCurrent;
328
389
  /**
329
390
  * Check if the overlay iframe loaded successfully by evaluating a probe
330
391
  * script. Walks the `<slicc-launcher>` host's (open) shadow root to find the
@@ -1,5 +1,5 @@
1
1
  import { BRIDGE_ROLE_FOLLOWER, BRIDGE_ROLE_LEADER, BRIDGE_ROLE_QUERY_PARAM, ELECTRON_OVERLAY_APP_PATH, SLICC_HOSTED_ORIGIN, TRAY_QUERY_PARAM, } from './_shared/index.js';
2
- import { execFile as nodeExecFile, spawn } from 'child_process';
2
+ import { execFile as nodeExecFile, spawn, } from 'child_process';
3
3
  import { existsSync } from 'fs';
4
4
  import { readFile } from 'fs/promises';
5
5
  import * as http from 'http';
@@ -105,7 +105,7 @@ export class ElectronAppAlreadyRunningError extends Error {
105
105
  this.name = 'ElectronAppAlreadyRunningError';
106
106
  }
107
107
  }
108
- function parseUnixProcessList(stdout) {
108
+ export function parseUnixProcessList(stdout) {
109
109
  const processes = [];
110
110
  for (const rawLine of stdout.split('\n')) {
111
111
  const line = rawLine.trim();
@@ -125,7 +125,7 @@ function parseUnixProcessList(stdout) {
125
125
  }
126
126
  return processes;
127
127
  }
128
- function parseWindowsProcessList(stdout) {
128
+ export function parseWindowsProcessList(stdout) {
129
129
  const trimmed = stdout.trim();
130
130
  if (!trimmed)
131
131
  return [];
@@ -139,93 +139,101 @@ function parseWindowsProcessList(stdout) {
139
139
  }))
140
140
  .filter((processInfo) => Number.isFinite(processInfo.pid) && processInfo.pid > 0);
141
141
  }
142
- async function listRunningProcesses(platform = process.platform) {
142
+ export async function listRunningProcesses(platform = process.platform, run = execFile) {
143
143
  if (platform === 'win32') {
144
- const { stdout } = await execFile('powershell', [
144
+ const { stdout } = await run('powershell', [
145
145
  '-NoProfile',
146
146
  '-Command',
147
147
  'Get-CimInstance Win32_Process | Select-Object ProcessId, ExecutablePath, CommandLine | ConvertTo-Json -Compress',
148
148
  ]);
149
149
  return parseWindowsProcessList(stdout);
150
150
  }
151
- const { stdout } = await execFile('ps', ['-ax', '-o', 'pid=', '-o', 'command=']);
151
+ const { stdout } = await run('ps', ['-ax', '-o', 'pid=', '-o', 'command=']);
152
152
  return parseUnixProcessList(stdout);
153
153
  }
154
- function isPidAlive(pid) {
154
+ export function isPidAlive(pid, signal = process.kill.bind(process)) {
155
155
  try {
156
- process.kill(pid, 0);
156
+ signal(pid, 0);
157
157
  return true;
158
158
  }
159
159
  catch {
160
160
  return false;
161
161
  }
162
162
  }
163
- async function waitForPidsToExit(pids, timeoutMs = 5000) {
164
- const deadline = Date.now() + timeoutMs;
165
- while (Date.now() < deadline) {
166
- if (pids.every((pid) => !isPidAlive(pid)))
163
+ const defaultProcessLifecycle = {
164
+ isAlive: isPidAlive,
165
+ kill: (pid, signal) => process.kill(pid, signal),
166
+ now: Date.now,
167
+ sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
168
+ };
169
+ export async function waitForPidsToExit(pids, timeoutMs = 5000, lifecycle = defaultProcessLifecycle) {
170
+ const deadline = lifecycle.now() + timeoutMs;
171
+ while (lifecycle.now() < deadline) {
172
+ if (pids.every((pid) => !lifecycle.isAlive(pid)))
167
173
  return true;
168
- await new Promise((resolve) => setTimeout(resolve, 100));
174
+ await lifecycle.sleep(100);
169
175
  }
170
- return pids.every((pid) => !isPidAlive(pid));
176
+ return pids.every((pid) => !lifecycle.isAlive(pid));
171
177
  }
172
- async function terminateRunningApp(pids) {
178
+ export async function terminateRunningApp(pids, lifecycle = defaultProcessLifecycle) {
173
179
  for (const pid of pids) {
174
- if (!isPidAlive(pid))
180
+ if (!lifecycle.isAlive(pid))
175
181
  continue;
176
182
  try {
177
- process.kill(pid);
183
+ lifecycle.kill(pid);
178
184
  }
179
185
  catch {
180
186
  // Ignore individual termination failures and fall back to force-kill below if needed.
181
187
  }
182
188
  }
183
- if (await waitForPidsToExit(pids))
189
+ if (await waitForPidsToExit(pids, 5000, lifecycle))
184
190
  return;
185
191
  for (const pid of pids) {
186
- if (!isPidAlive(pid))
192
+ if (!lifecycle.isAlive(pid))
187
193
  continue;
188
194
  try {
189
- process.kill(pid, 'SIGKILL');
195
+ lifecycle.kill(pid, 'SIGKILL');
190
196
  }
191
197
  catch {
192
198
  // Ignore final cleanup failures.
193
199
  }
194
200
  }
195
- await waitForPidsToExit(pids, 3000);
201
+ await waitForPidsToExit(pids, 3000, lifecycle);
196
202
  }
197
- async function findRunningElectronAppPids(appPath, platform = process.platform) {
203
+ export async function findRunningElectronAppPids(appPath, platform = process.platform) {
198
204
  const { processMatchPatterns } = buildElectronAppLaunchSpec(appPath, { cdpPort: 0, platform });
199
205
  const runningProcesses = await listRunningProcesses(platform);
200
206
  return findMatchingElectronAppPids(runningProcesses, processMatchPatterns);
201
207
  }
202
- export async function launchElectronApp(options) {
208
+ export async function launchElectronApp(options, dependencies = {}) {
203
209
  const launchSpec = buildElectronAppLaunchSpec(options.appPath, {
204
210
  cdpPort: options.cdpPort,
205
211
  platform: options.platform,
206
212
  });
207
- if (!existsSync(launchSpec.resolvedAppPath)) {
213
+ const exists = dependencies.exists ?? existsSync;
214
+ if (!exists(launchSpec.resolvedAppPath)) {
208
215
  throw new Error(`Electron app not found at ${launchSpec.resolvedAppPath}`);
209
216
  }
210
- if (!existsSync(launchSpec.command)) {
217
+ if (!exists(launchSpec.command)) {
211
218
  throw new Error(`Electron executable not found at ${launchSpec.command}. Pass the app executable path directly if needed.`);
212
219
  }
213
- const runningPids = await findRunningElectronAppPids(launchSpec.resolvedAppPath, options.platform);
220
+ const runningPids = await (dependencies.findRunningPids ?? findRunningElectronAppPids)(launchSpec.resolvedAppPath, options.platform);
214
221
  const platform = options.platform ?? process.platform;
215
222
  const isMacAppBundle = platform === 'darwin' && launchSpec.resolvedAppPath.toLowerCase().endsWith('.app');
216
223
  if (runningPids.length > 0 && !options.kill) {
217
224
  throw new ElectronAppAlreadyRunningError(`${launchSpec.displayName} is already running. Re-run with --kill to relaunch it with remote debugging enabled.`);
218
225
  }
219
226
  if (runningPids.length > 0) {
220
- await terminateRunningApp(runningPids);
227
+ await (dependencies.terminate ?? terminateRunningApp)(runningPids);
221
228
  }
229
+ const spawnProcess = dependencies.spawn ?? spawn;
222
230
  const child = isMacAppBundle
223
- ? spawn('open', ['-n', '-a', launchSpec.resolvedAppPath, '-W', '--args', ...launchSpec.args], {
231
+ ? spawnProcess('open', ['-n', '-a', launchSpec.resolvedAppPath, '-W', '--args', ...launchSpec.args], {
224
232
  env: process.env,
225
233
  stdio: ['ignore', 'pipe', 'pipe'],
226
234
  detached: false,
227
235
  })
228
- : spawn(launchSpec.command, launchSpec.args, {
236
+ : spawnProcess(launchSpec.command, launchSpec.args, {
229
237
  env: process.env,
230
238
  stdio: ['ignore', 'pipe', 'pipe'],
231
239
  detached: false,
@@ -369,7 +377,7 @@ export function computeAverageLuminance(pixels, width, height, sampleStep = 4) {
369
377
  * a CDP screenshot and analyzing the average luminance.
370
378
  * Returns 'light' or 'dark'.
371
379
  */
372
- function detectAppThemeFromScreenshot(ws, send) {
380
+ export function detectAppThemeFromScreenshot(ws, send) {
373
381
  return new Promise((resolve) => {
374
382
  // Take a small JPEG screenshot for speed — we only need luminance
375
383
  const screenshotId = send('Page.captureScreenshot', {
@@ -524,7 +532,7 @@ function buildProxyRequestHeaders(requestHeaders, bodyLength) {
524
532
  * actually-buffered body length. Returns whether any CSP header was stripped
525
533
  * so the caller can log it.
526
534
  */
527
- function buildFulfillResponseHeaders(rawHeaders, contentLength) {
535
+ export function buildFulfillResponseHeaders(rawHeaders, contentLength) {
528
536
  const HOP_BY_HOP = new Set([
529
537
  'content-security-policy',
530
538
  'content-security-policy-report-only',
@@ -558,6 +566,16 @@ function buildFulfillResponseHeaders(rawHeaders, contentLength) {
558
566
  }
559
567
  return { responseHeaders, strippedCSP };
560
568
  }
569
+ export function logOverlayReinjectionFailure(context, error) {
570
+ const message = error instanceof Error ? error.message : String(error);
571
+ console.warn(`[electron-float] ${context} re-injection failed: ${message}`);
572
+ }
573
+ export const logPresenceReinjectionFailure = (error) => {
574
+ logOverlayReinjectionFailure('Presence-check', error);
575
+ };
576
+ export const logNavigationReinjectionFailure = (error) => {
577
+ logOverlayReinjectionFailure('Navigation', error);
578
+ };
561
579
  /**
562
580
  * Empty-viewport message shown by the status-only overlay for an app that
563
581
  * denies the overlay iframe's network egress. Kept in sync with swift-server's
@@ -680,6 +698,9 @@ export class ElectronOverlayInjector {
680
698
  onEgressBlocked;
681
699
  syncTimer = null;
682
700
  syncing = false;
701
+ runScheduledSync = async () => {
702
+ await this.syncTargets();
703
+ };
683
704
  constructor(cdpPort, servePort, thinBootstraps, bridgeToken, onEgressBlocked, probeDelayMs = 1500, presenceCheckIntervalMs = ELECTRON_OVERLAY_PRESENCE_CHECK_INTERVAL_MS) {
684
705
  this.cdpPort = cdpPort;
685
706
  this.servePort = servePort;
@@ -761,6 +782,30 @@ export class ElectronOverlayInjector {
761
782
  _testingSeedEgressBlockedTarget(url) {
762
783
  this.egressBlockedTargets.add(url);
763
784
  }
785
+ /** Test-only: seed a connection so sync/stop cleanup can be driven deterministically. */
786
+ _testingSeedConnection(targetId, connection) {
787
+ this.connections.set(targetId, connection);
788
+ }
789
+ /** Test-only: drive the identity-guarded connection cleanup. */
790
+ _testingDropConnection(targetId, connection) {
791
+ this.dropConnectionIfCurrent(targetId, connection);
792
+ }
793
+ /** Test-only: invoke the same callback installed by `start()`. */
794
+ async _testingRunScheduledSync() {
795
+ await this.runScheduledSync();
796
+ }
797
+ /** Test-only: exercise the overlay-loaded timeout classifier directly. */
798
+ _testingProbeOverlayIframeLoaded(ws, send) {
799
+ return this.probeOverlayIframeLoaded(ws, send);
800
+ }
801
+ /** Test-only: exercise the overlay-eviction timeout classifier directly. */
802
+ _testingProbeOverlayEvicted(ws, send) {
803
+ return this.probeOverlayEvicted(ws, send);
804
+ }
805
+ /** Test-only: drive a paused Fetch event without requiring the full escalation flow. */
806
+ _testingHandleFetchRequestPaused(ws, send, msg) {
807
+ this.handleFetchRequestPaused(ws, send, msg);
808
+ }
764
809
  /** Test-only: drive a single `syncTargets` pass without `start()`'s interval. */
765
810
  async _testingSyncTargets() {
766
811
  await this.syncTargets();
@@ -779,9 +824,7 @@ export class ElectronOverlayInjector {
779
824
  }
780
825
  async start() {
781
826
  await this.syncTargets();
782
- this.syncTimer = setInterval(() => {
783
- void this.syncTargets();
784
- }, ELECTRON_OVERLAY_SYNC_INTERVAL_MS);
827
+ this.syncTimer = setInterval(this.runScheduledSync, ELECTRON_OVERLAY_SYNC_INTERVAL_MS);
785
828
  }
786
829
  stop() {
787
830
  if (this.syncTimer) {
@@ -853,6 +896,11 @@ export class ElectronOverlayInjector {
853
896
  this.syncing = false;
854
897
  }
855
898
  }
899
+ dropConnectionIfCurrent(targetId, connection) {
900
+ if (this.connections.get(targetId) === connection) {
901
+ this.connections.delete(targetId);
902
+ }
903
+ }
856
904
  /**
857
905
  * Check if the overlay iframe loaded successfully by evaluating a probe
858
906
  * script. Walks the `<slicc-launcher>` host's (open) shadow root to find the
@@ -1310,10 +1358,7 @@ export class ElectronOverlayInjector {
1310
1358
  ws.on('open', () => {
1311
1359
  this.handleSocketOpen(ws, send, target, state);
1312
1360
  presenceTimer = setInterval(() => {
1313
- void this.reinjectIfEvicted(ws, send, target, state).catch((error) => {
1314
- const message = error instanceof Error ? error.message : String(error);
1315
- console.warn(`[electron-float] Presence-check re-injection failed: ${message}`);
1316
- });
1361
+ void this.reinjectIfEvicted(ws, send, target, state).catch(logPresenceReinjectionFailure);
1317
1362
  }, this.presenceCheckIntervalMs);
1318
1363
  });
1319
1364
  // Handle CDP events: lifecycle events and Fetch interception
@@ -1332,10 +1377,7 @@ export class ElectronOverlayInjector {
1332
1377
  // navigation a no-op (its marker is wiped, so the new-document hook owns it).
1333
1378
  const isMainFrameNavigated = msg.method === 'Page.frameNavigated' && !msg.params?.frame?.parentId;
1334
1379
  if (msg.method === 'Page.navigatedWithinDocument' || isMainFrameNavigated) {
1335
- void this.reinjectIfEvicted(ws, send, target, state).catch((error) => {
1336
- const message = error instanceof Error ? error.message : String(error);
1337
- console.warn(`[electron-float] Navigation re-injection failed: ${message}`);
1338
- });
1380
+ void this.reinjectIfEvicted(ws, send, target, state).catch(logNavigationReinjectionFailure);
1339
1381
  }
1340
1382
  if (msg.method === 'Fetch.requestPaused' && state.fetchProxyActive) {
1341
1383
  this.handleFetchRequestPaused(ws, send, msg);
@@ -1352,17 +1394,13 @@ export class ElectronOverlayInjector {
1352
1394
  });
1353
1395
  ws.on('close', () => {
1354
1396
  clearPresenceTimer();
1355
- if (this.connections.get(targetId) === ws) {
1356
- this.connections.delete(targetId);
1357
- }
1397
+ this.dropConnectionIfCurrent(targetId, ws);
1358
1398
  });
1359
1399
  ws.on('error', (error) => {
1360
1400
  clearPresenceTimer();
1361
1401
  const message = error instanceof Error ? error.message : String(error);
1362
1402
  console.error(`[electron-float] Overlay target connection failed for ${target.url}:`, message);
1363
- if (this.connections.get(targetId) === ws) {
1364
- this.connections.delete(targetId);
1365
- }
1403
+ this.dropConnectionIfCurrent(targetId, ws);
1366
1404
  });
1367
1405
  }
1368
1406
  }
@@ -32,12 +32,25 @@ export declare const PORT_HASH_RANGE = 40;
32
32
  * Exported for testing.
33
33
  */
34
34
  export declare function hashString(str: string, max: number): number;
35
+ /**
36
+ * Try to listen on a specific port and host, returning the assigned port.
37
+ */
38
+ export declare function tryListenOnPort(port: number, host: string): Promise<number>;
39
+ /**
40
+ * Check if a port is available on both IPv4 (127.0.0.1) and IPv6 (::1).
41
+ * On macOS, `localhost` resolves to `::1`, so we need to check both.
42
+ */
43
+ export declare function isPortAvailable(port: number, listen?: (port: number, host: string) => Promise<number>): Promise<boolean>;
44
+ /**
45
+ * Find an available port starting from the given port.
46
+ */
47
+ export declare function findAvailablePort(startPort: number, maxAttempts?: number, available?: (port: number) => Promise<boolean>): Promise<number>;
35
48
  /**
36
49
  * Get a port for an Electron app based on its path.
37
50
  * Uses hash-based offset from base port, with fallback to next available port
38
51
  * starting from the preferred port (to stay in the app's "slot" range).
39
52
  */
40
- export declare function getElectronAppPort(appPath: string, basePort: number): Promise<number>;
53
+ export declare function getElectronAppPort(appPath: string, basePort: number, available?: (port: number) => Promise<boolean>): Promise<number>;
41
54
  /**
42
55
  * Get both CDP and serve ports for an Electron app.
43
56
  */
@@ -46,6 +59,7 @@ export declare function getElectronAppPorts(appPath: string): Promise<{
46
59
  servePort: number;
47
60
  }>;
48
61
  export declare function getElectronAppDisplayName(appPath: string): string;
62
+ export declare function isExecutableFile(path: string): boolean;
49
63
  export declare function resolveElectronAppExecutablePath(appPath: string, platform?: NodeJS.Platform): string;
50
64
  export declare function buildElectronAppProcessMatchPatterns(appPath: string, platform?: NodeJS.Platform): string[];
51
65
  export declare function buildElectronAppLaunchSpec(appPath: string, options: {
@@ -22,7 +22,7 @@ export function hashString(str, max) {
22
22
  /**
23
23
  * Try to listen on a specific port and host, returning the assigned port.
24
24
  */
25
- async function tryListenOnPort(port, host) {
25
+ export async function tryListenOnPort(port, host) {
26
26
  const { createServer } = await import('net');
27
27
  return new Promise((resolve, reject) => {
28
28
  const server = createServer();
@@ -38,11 +38,11 @@ async function tryListenOnPort(port, host) {
38
38
  * Check if a port is available on both IPv4 (127.0.0.1) and IPv6 (::1).
39
39
  * On macOS, `localhost` resolves to `::1`, so we need to check both.
40
40
  */
41
- async function isPortAvailable(port) {
41
+ export async function isPortAvailable(port, listen = tryListenOnPort) {
42
42
  try {
43
- await tryListenOnPort(port, '127.0.0.1');
43
+ await listen(port, '127.0.0.1');
44
44
  try {
45
- await tryListenOnPort(port, '::1');
45
+ await listen(port, '::1');
46
46
  }
47
47
  catch (err) {
48
48
  // ::1 may not be available on some systems — only fail on EADDRINUSE
@@ -59,10 +59,10 @@ async function isPortAvailable(port) {
59
59
  /**
60
60
  * Find an available port starting from the given port.
61
61
  */
62
- async function findAvailablePort(startPort, maxAttempts = 100) {
62
+ export async function findAvailablePort(startPort, maxAttempts = 100, available = isPortAvailable) {
63
63
  for (let i = 0; i < maxAttempts; i++) {
64
64
  const port = startPort + i;
65
- if (await isPortAvailable(port)) {
65
+ if (await available(port)) {
66
66
  return port;
67
67
  }
68
68
  }
@@ -73,14 +73,14 @@ async function findAvailablePort(startPort, maxAttempts = 100) {
73
73
  * Uses hash-based offset from base port, with fallback to next available port
74
74
  * starting from the preferred port (to stay in the app's "slot" range).
75
75
  */
76
- export async function getElectronAppPort(appPath, basePort) {
76
+ export async function getElectronAppPort(appPath, basePort, available = isPortAvailable) {
77
77
  const offset = hashString(appPath, PORT_HASH_RANGE);
78
78
  const preferredPort = basePort + offset;
79
- if (await isPortAvailable(preferredPort)) {
79
+ if (await available(preferredPort)) {
80
80
  return preferredPort;
81
81
  }
82
82
  // Fallback: find next available port starting from preferred (stay in slot range)
83
- return findAvailablePort(preferredPort + 1);
83
+ return findAvailablePort(preferredPort + 1, 100, available);
84
84
  }
85
85
  /**
86
86
  * Get both CDP and serve ports for an Electron app.
@@ -98,7 +98,7 @@ export function getElectronAppDisplayName(appPath) {
98
98
  }
99
99
  return fileName || trimmedPath;
100
100
  }
101
- function isExecutableFile(path) {
101
+ export function isExecutableFile(path) {
102
102
  try {
103
103
  accessSync(path, constants.X_OK);
104
104
  return true;
@@ -69,13 +69,11 @@ class MaybeGunzipTransform extends Transform {
69
69
  cb();
70
70
  return;
71
71
  }
72
- this.#finishDecide((err) => {
73
- if (err) {
74
- cb(err);
75
- return;
76
- }
77
- this.#endGunzipOrFinish(cb);
78
- });
72
+ // A non-empty undecided head can only be one byte: two bytes decide in
73
+ // `_transform`. One byte cannot be gzip magic, so flush it directly.
74
+ this.#decided = true;
75
+ this.#onDecided?.(false);
76
+ cb(null, this.#head);
79
77
  return;
80
78
  }
81
79
  this.#endGunzipOrFinish(cb);
@@ -2,6 +2,7 @@ import { type Account, imsTokenExpiry } from './_cloud_core/src/index.js';
2
2
  import type { Express } from 'express';
3
3
  import type { SecretStore } from './secrets/types.js';
4
4
  export { imsTokenExpiry };
5
+ export declare function readHostedConeConfig(path?: string, read?: (path: string, encoding: BufferEncoding) => string, warn?: (...args: unknown[]) => void): string | null;
5
6
  export interface HostedBootstrapPayload {
6
7
  model?: string;
7
8
  effortLevel?: string;
@@ -24,6 +24,17 @@ import { requireLoopback } from './cloud-status.js';
24
24
  // Buffer-free) so the worker's back-compat + resume paths reuse the same logic.
25
25
  export { imsTokenExpiry };
26
26
  const CONE_CONFIG_PATH = '/slicc/cone-config.json';
27
+ export function readHostedConeConfig(path = CONE_CONFIG_PATH, read = readFileSync, warn = console.warn) {
28
+ try {
29
+ return read(path, 'utf-8');
30
+ }
31
+ catch (err) {
32
+ if (err.code !== 'ENOENT') {
33
+ warn('[hosted-bootstrap] failed to read cone-config.json:', err);
34
+ }
35
+ return null;
36
+ }
37
+ }
27
38
  export function buildHostedBootstrapPayload(sources) {
28
39
  const raw = sources.readConeConfig();
29
40
  if (raw) {
@@ -55,20 +66,7 @@ export function buildHostedBootstrapPayload(sources) {
55
66
  export function registerHostedBootstrapEndpoint(app, options) {
56
67
  app.get('/api/hosted-bootstrap', requireLoopback, (_req, res) => {
57
68
  const payload = buildHostedBootstrapPayload({
58
- readConeConfig: () => {
59
- try {
60
- return readFileSync(CONE_CONFIG_PATH, 'utf-8');
61
- }
62
- catch (err) {
63
- // A missing file is the normal pre-feature/back-compat case. Any other
64
- // error (EACCES, EIO, …) means the config exists but is unreadable —
65
- // log it so a silent fallback to legacy/empty doesn't hide the cause.
66
- if (err.code !== 'ENOENT') {
67
- console.warn('[hosted-bootstrap] failed to read cone-config.json:', err);
68
- }
69
- return null;
70
- }
71
- },
69
+ readConeConfig: () => readHostedConeConfig(),
72
70
  getLegacyAdobeToken: () => options.secretStore.get('ADOBE_IMS_TOKEN')?.value,
73
71
  });
74
72
  res.json(payload);