sourcemonkey 0.0.0-stage → 0.5.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.
Files changed (367) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/dist/bytes.js +37 -0
  4. package/dist/channel-placeholder.js +5 -0
  5. package/dist/collection-scan.js +17 -0
  6. package/dist/collections.html +353 -0
  7. package/dist/compare.html +358 -0
  8. package/dist/composed-source.js +205 -0
  9. package/dist/connect-prompt.html +47 -0
  10. package/dist/debug/icons.html +211 -0
  11. package/dist/debug/storage.html +163 -0
  12. package/dist/debug-log.js +75 -0
  13. package/dist/diagnostic-location.js +64 -0
  14. package/dist/eval-hook.js +308 -0
  15. package/dist/extension/background.js +5263 -0
  16. package/dist/extension/channel.js +133 -0
  17. package/dist/extension/code-links.js +187 -0
  18. package/dist/extension/code-tabs.js +460 -0
  19. package/dist/extension/collection-dialog.js +619 -0
  20. package/dist/extension/collection-display.js +322 -0
  21. package/dist/extension/collections.js +2905 -0
  22. package/dist/extension/compare.js +867 -0
  23. package/dist/extension/concurrency.js +186 -0
  24. package/dist/extension/connect-grants.js +109 -0
  25. package/dist/extension/connect-prompt-page.js +70 -0
  26. package/dist/extension/connect-prompt.js +266 -0
  27. package/dist/extension/copy-button.js +63 -0
  28. package/dist/extension/copy-dialog.js +502 -0
  29. package/dist/extension/copy-preference.js +66 -0
  30. package/dist/extension/debug/icons.js +203 -0
  31. package/dist/extension/debug/storage.js +365 -0
  32. package/dist/extension/deep-link.js +77 -0
  33. package/dist/extension/delete-dialog.js +432 -0
  34. package/dist/extension/delete-icon.js +40 -0
  35. package/dist/extension/details-tooltips.js +66 -0
  36. package/dist/extension/directories.js +679 -0
  37. package/dist/extension/export-backup.js +407 -0
  38. package/dist/extension/export-dialog.js +142 -0
  39. package/dist/extension/file-access.js +74 -0
  40. package/dist/extension/file-changes.js +346 -0
  41. package/dist/extension/frame-info.js +85 -0
  42. package/dist/extension/github-provider.js +336 -0
  43. package/dist/extension/glyph.js +84 -0
  44. package/dist/extension/gm-bindings.js +601 -0
  45. package/dist/extension/gm-download.js +419 -0
  46. package/dist/extension/gm-menu.js +228 -0
  47. package/dist/extension/gm-notification.js +141 -0
  48. package/dist/extension/gm-originator-map.js +23 -0
  49. package/dist/extension/gm-tab-data.js +121 -0
  50. package/dist/extension/gm-tab.js +160 -0
  51. package/dist/extension/gm-url-change.js +111 -0
  52. package/dist/extension/gm-url-scheme.js +27 -0
  53. package/dist/extension/gm-values.js +176 -0
  54. package/dist/extension/gm-window-control.js +104 -0
  55. package/dist/extension/gm-xhr-headers.js +333 -0
  56. package/dist/extension/gm-xhr.js +584 -0
  57. package/dist/extension/injector.js +3145 -0
  58. package/dist/extension/install-capture.js +277 -0
  59. package/dist/extension/line-diff.js +233 -0
  60. package/dist/extension/local-copy.js +560 -0
  61. package/dist/extension/local-delete.js +276 -0
  62. package/dist/extension/location-kinds.js +108 -0
  63. package/dist/extension/log-format.js +158 -0
  64. package/dist/extension/managed-changes.js +361 -0
  65. package/dist/extension/managed-items.js +57 -0
  66. package/dist/extension/match-index.js +375 -0
  67. package/dist/extension/missing-scripts.js +66 -0
  68. package/dist/extension/more-menu.js +109 -0
  69. package/dist/extension/move-dialog.js +197 -0
  70. package/dist/extension/offscreen.js +37 -0
  71. package/dist/extension/options.js +504 -0
  72. package/dist/extension/page-heading.js +170 -0
  73. package/dist/extension/page-links.js +199 -0
  74. package/dist/extension/parse-check-client.js +70 -0
  75. package/dist/extension/parse-check.js +161 -0
  76. package/dist/extension/parse-sandbox-host.js +172 -0
  77. package/dist/extension/parse-sandbox.js +175 -0
  78. package/dist/extension/popup-commands-open.js +41 -0
  79. package/dist/extension/popup-view.js +387 -0
  80. package/dist/extension/popup.js +703 -0
  81. package/dist/extension/prism-userscript.js +53 -0
  82. package/dist/extension/refresh-error.js +136 -0
  83. package/dist/extension/refresh-targets.js +160 -0
  84. package/dist/extension/related-versions.js +1144 -0
  85. package/dist/extension/resize-observer-loop.js +19 -0
  86. package/dist/extension/row-cache.js +387 -0
  87. package/dist/extension/script-blobs.js +413 -0
  88. package/dist/extension/script-details.js +658 -0
  89. package/dist/extension/script-enablement.js +285 -0
  90. package/dist/extension/script-identity.js +310 -0
  91. package/dist/extension/script-keys.js +54 -0
  92. package/dist/extension/script-log-id.js +155 -0
  93. package/dist/extension/script-log.js +290 -0
  94. package/dist/extension/script-rejection.js +104 -0
  95. package/dist/extension/script-sites.js +158 -0
  96. package/dist/extension/script-store.js +3507 -0
  97. package/dist/extension/script-updater.js +991 -0
  98. package/dist/extension/script-url-hash.js +61 -0
  99. package/dist/extension/select-all.js +37 -0
  100. package/dist/extension/select-open-guard.js +90 -0
  101. package/dist/extension/site-icon.js +1261 -0
  102. package/dist/extension/sm-dev-link.js +302 -0
  103. package/dist/extension/solo-click.js +58 -0
  104. package/dist/extension/sticky-numbers.js +188 -0
  105. package/dist/extension/storage-breakdown.js +253 -0
  106. package/dist/extension/storage-scan.js +86 -0
  107. package/dist/extension/storage-usage.js +194 -0
  108. package/dist/extension/structural-keys.js +29 -0
  109. package/dist/extension/sw-teardown.js +36 -0
  110. package/dist/extension/tab-status.js +403 -0
  111. package/dist/extension/toolbar-summary.js +105 -0
  112. package/dist/extension/user-scripts-access.js +43 -0
  113. package/dist/extension/userscript.js +3672 -0
  114. package/dist/extension/value-store.js +204 -0
  115. package/dist/extension/version-probe-fetch.js +68 -0
  116. package/dist/extension/worker-client.js +109 -0
  117. package/dist/extension/worker-ops.js +171 -0
  118. package/dist/extension/zip-writer.js +24 -0
  119. package/dist/external-files.js +878 -0
  120. package/dist/file-loader.js +436 -0
  121. package/dist/file-locations.js +439 -0
  122. package/dist/generated-wrappers.js +5 -0
  123. package/dist/github-auth.js +148 -0
  124. package/dist/github-urls.js +46 -0
  125. package/dist/gm-api-catalog.js +217 -0
  126. package/dist/gm-connect.js +273 -0
  127. package/dist/gm-xhr-types.js +5 -0
  128. package/dist/http-read.js +142 -0
  129. package/dist/icons/icon-128.png +0 -0
  130. package/dist/icons/icon-16.png +0 -0
  131. package/dist/icons/icon-48.png +0 -0
  132. package/dist/local-file-overrides.js +558 -0
  133. package/dist/manifest.json +38 -0
  134. package/dist/markup-response.js +126 -0
  135. package/dist/match-targets.js +305 -0
  136. package/dist/menu-captions.js +22 -0
  137. package/dist/offscreen.html +39 -0
  138. package/dist/options.html +197 -0
  139. package/dist/parse-check-protocol.js +51 -0
  140. package/dist/parse-location.js +171 -0
  141. package/dist/parse-sandbox.html +56 -0
  142. package/dist/phase-timing.js +155 -0
  143. package/dist/popup.html +197 -0
  144. package/dist/public-suffixes.js +6957 -0
  145. package/dist/run-context.js +31 -0
  146. package/dist/runtime-messages.js +34 -0
  147. package/dist/sandboxed-page.js +35 -0
  148. package/dist/script-build.js +787 -0
  149. package/dist/script-display.js +383 -0
  150. package/dist/script-icon.js +43 -0
  151. package/dist/script-links.js +204 -0
  152. package/dist/script-source-url.js +214 -0
  153. package/dist/script-world.js +65 -0
  154. package/dist/sm-dev-protocol.js +84 -0
  155. package/dist/source-map.js +177 -0
  156. package/dist/stack-link.js +138 -0
  157. package/dist/styles.css +825 -0
  158. package/dist/unsafe-window-use.js +229 -0
  159. package/dist/url-match.js +734 -0
  160. package/dist/userscript-header.js +248 -0
  161. package/dist/userscript.html +1165 -0
  162. package/dist/vendor/fflate.LICENSE +21 -0
  163. package/dist/vendor/fflate.js +2692 -0
  164. package/dist/vendor/prism/prism-line-numbers.css +1 -0
  165. package/dist/vendor/prism/prism-line-numbers.js +1 -0
  166. package/dist/vendor/prism/prism.css +1 -0
  167. package/dist/vendor/prism/prism.js +1946 -0
  168. package/dist/version-compare.js +141 -0
  169. package/dist/version-probe.js +120 -0
  170. package/lib/build-info.json +1 -0
  171. package/lib/bytes.d.ts +3 -0
  172. package/lib/bytes.js +37 -0
  173. package/lib/channel-placeholder.d.ts +1 -0
  174. package/lib/channel-placeholder.js +5 -0
  175. package/lib/collection-scan.d.ts +8 -0
  176. package/lib/collection-scan.js +17 -0
  177. package/lib/composed-source.d.ts +28 -0
  178. package/lib/composed-source.js +205 -0
  179. package/lib/debug-log.d.ts +9 -0
  180. package/lib/debug-log.js +75 -0
  181. package/lib/diagnostic-location.d.ts +8 -0
  182. package/lib/diagnostic-location.js +64 -0
  183. package/lib/eval-hook.d.ts +34 -0
  184. package/lib/eval-hook.js +308 -0
  185. package/lib/external-files.d.ts +111 -0
  186. package/lib/external-files.js +878 -0
  187. package/lib/file-loader.d.ts +48 -0
  188. package/lib/file-loader.js +436 -0
  189. package/lib/file-locations.d.ts +44 -0
  190. package/lib/file-locations.js +439 -0
  191. package/lib/generated-wrappers.d.ts +11 -0
  192. package/lib/generated-wrappers.js +5 -0
  193. package/lib/github-auth.d.ts +12 -0
  194. package/lib/github-auth.js +148 -0
  195. package/lib/github-urls.d.ts +1 -0
  196. package/lib/github-urls.js +46 -0
  197. package/lib/gm-api-catalog.d.ts +24 -0
  198. package/lib/gm-api-catalog.js +217 -0
  199. package/lib/gm-connect.d.ts +34 -0
  200. package/lib/gm-connect.js +273 -0
  201. package/lib/gm-xhr-types.d.ts +68 -0
  202. package/lib/gm-xhr-types.js +5 -0
  203. package/lib/harness/browser-extensions.d.ts +73 -0
  204. package/lib/harness/browser-extensions.js +285 -0
  205. package/lib/harness/browser-lang.d.ts +20 -0
  206. package/lib/harness/browser-lang.js +86 -0
  207. package/lib/harness/browser-launch.d.ts +14 -0
  208. package/lib/harness/browser-launch.js +184 -0
  209. package/lib/harness/checks.d.ts +110 -0
  210. package/lib/harness/checks.js +321 -0
  211. package/lib/harness/cli.d.ts +2 -0
  212. package/lib/harness/cli.js +2259 -0
  213. package/lib/harness/command-args.d.ts +91 -0
  214. package/lib/harness/command-args.js +369 -0
  215. package/lib/harness/command-lines.d.ts +80 -0
  216. package/lib/harness/command-lines.js +180 -0
  217. package/lib/harness/command-list.d.ts +206 -0
  218. package/lib/harness/command-list.js +1475 -0
  219. package/lib/harness/console-commands.d.ts +463 -0
  220. package/lib/harness/console-commands.js +3216 -0
  221. package/lib/harness/download-watch.d.ts +21 -0
  222. package/lib/harness/download-watch.js +156 -0
  223. package/lib/harness/extension-backend.d.ts +41 -0
  224. package/lib/harness/extension-backend.js +100 -0
  225. package/lib/harness/extension-host.d.ts +112 -0
  226. package/lib/harness/extension-host.js +552 -0
  227. package/lib/harness/extension-launch.d.ts +67 -0
  228. package/lib/harness/extension-launch.js +285 -0
  229. package/lib/harness/extension-link.d.ts +315 -0
  230. package/lib/harness/extension-link.js +607 -0
  231. package/lib/harness/external-manager.d.ts +146 -0
  232. package/lib/harness/external-manager.js +414 -0
  233. package/lib/harness/fixture.d.ts +165 -0
  234. package/lib/harness/fixture.js +461 -0
  235. package/lib/harness/frame-sessions.d.ts +71 -0
  236. package/lib/harness/frame-sessions.js +248 -0
  237. package/lib/harness/frames.d.ts +68 -0
  238. package/lib/harness/frames.js +216 -0
  239. package/lib/harness/help-format.d.ts +17 -0
  240. package/lib/harness/help-format.js +127 -0
  241. package/lib/harness/host.d.ts +352 -0
  242. package/lib/harness/host.js +423 -0
  243. package/lib/harness/index.d.ts +8 -0
  244. package/lib/harness/index.js +10 -0
  245. package/lib/harness/input-file.d.ts +18 -0
  246. package/lib/harness/input-file.js +61 -0
  247. package/lib/harness/json-output.d.ts +565 -0
  248. package/lib/harness/json-output.js +71 -0
  249. package/lib/harness/manager-actions.d.ts +21 -0
  250. package/lib/harness/manager-actions.js +129 -0
  251. package/lib/harness/manager-commands.d.ts +142 -0
  252. package/lib/harness/manager-commands.js +1196 -0
  253. package/lib/harness/manager-fetch.d.ts +33 -0
  254. package/lib/harness/manager-fetch.js +239 -0
  255. package/lib/harness/managers.d.ts +24 -0
  256. package/lib/harness/managers.js +27 -0
  257. package/lib/harness/output-style.d.ts +125 -0
  258. package/lib/harness/output-style.js +322 -0
  259. package/lib/harness/page-commands.d.ts +134 -0
  260. package/lib/harness/page-commands.js +1141 -0
  261. package/lib/harness/page-output.d.ts +67 -0
  262. package/lib/harness/page-output.js +139 -0
  263. package/lib/harness/page-watch.d.ts +171 -0
  264. package/lib/harness/page-watch.js +607 -0
  265. package/lib/harness/parse-check-node.d.ts +3 -0
  266. package/lib/harness/parse-check-node.js +132 -0
  267. package/lib/harness/path-args.d.ts +22 -0
  268. package/lib/harness/path-args.js +148 -0
  269. package/lib/harness/playwright-chromium.d.ts +16 -0
  270. package/lib/harness/playwright-chromium.js +86 -0
  271. package/lib/harness/prepare.d.ts +168 -0
  272. package/lib/harness/prepare.js +364 -0
  273. package/lib/harness/prompt-editor.d.ts +17 -0
  274. package/lib/harness/prompt-editor.js +354 -0
  275. package/lib/harness/prompt-history.d.ts +13 -0
  276. package/lib/harness/prompt-history.js +89 -0
  277. package/lib/harness/rebuild-advice.d.ts +6 -0
  278. package/lib/harness/rebuild-advice.js +39 -0
  279. package/lib/harness/run-log.d.ts +47 -0
  280. package/lib/harness/run-log.js +140 -0
  281. package/lib/harness/run-logs.d.ts +132 -0
  282. package/lib/harness/run-logs.js +241 -0
  283. package/lib/harness/run.d.ts +194 -0
  284. package/lib/harness/run.js +1973 -0
  285. package/lib/harness/runs.d.ts +229 -0
  286. package/lib/harness/runs.js +726 -0
  287. package/lib/harness/save-html.d.ts +39 -0
  288. package/lib/harness/save-html.js +89 -0
  289. package/lib/harness/saved-pages.d.ts +120 -0
  290. package/lib/harness/saved-pages.js +275 -0
  291. package/lib/harness/script-mark.d.ts +17 -0
  292. package/lib/harness/script-mark.js +56 -0
  293. package/lib/harness/snapshot.d.ts +143 -0
  294. package/lib/harness/snapshot.js +474 -0
  295. package/lib/harness/stuck-tabs.d.ts +19 -0
  296. package/lib/harness/stuck-tabs.js +131 -0
  297. package/lib/harness/tab-refs.d.ts +20 -0
  298. package/lib/harness/tab-refs.js +80 -0
  299. package/lib/harness/tampermonkey.d.ts +71 -0
  300. package/lib/harness/tampermonkey.js +356 -0
  301. package/lib/harness/tm-ids.d.ts +18 -0
  302. package/lib/harness/tm-ids.js +88 -0
  303. package/lib/harness/violentmonkey.d.ts +34 -0
  304. package/lib/harness/violentmonkey.js +162 -0
  305. package/lib/harness/wait.d.ts +77 -0
  306. package/lib/harness/wait.js +458 -0
  307. package/lib/http-read.d.ts +30 -0
  308. package/lib/http-read.js +142 -0
  309. package/lib/local-file-overrides.d.ts +35 -0
  310. package/lib/local-file-overrides.js +558 -0
  311. package/lib/manifest.json +38 -0
  312. package/lib/markup-response.d.ts +11 -0
  313. package/lib/markup-response.js +126 -0
  314. package/lib/match-targets.d.ts +18 -0
  315. package/lib/match-targets.js +305 -0
  316. package/lib/menu-captions.d.ts +1 -0
  317. package/lib/menu-captions.js +22 -0
  318. package/lib/parse-check-protocol.d.ts +27 -0
  319. package/lib/parse-check-protocol.js +51 -0
  320. package/lib/parse-location.d.ts +12 -0
  321. package/lib/parse-location.js +171 -0
  322. package/lib/phase-timing.d.ts +12 -0
  323. package/lib/phase-timing.js +155 -0
  324. package/lib/public-suffixes.d.ts +1 -0
  325. package/lib/public-suffixes.js +6957 -0
  326. package/lib/run-context.d.ts +2 -0
  327. package/lib/run-context.js +31 -0
  328. package/lib/runtime-messages.d.ts +10 -0
  329. package/lib/runtime-messages.js +34 -0
  330. package/lib/sandboxed-page.d.ts +5 -0
  331. package/lib/sandboxed-page.js +35 -0
  332. package/lib/script-build.d.ts +111 -0
  333. package/lib/script-build.js +787 -0
  334. package/lib/script-display.d.ts +51 -0
  335. package/lib/script-display.js +383 -0
  336. package/lib/script-icon.d.ts +16 -0
  337. package/lib/script-icon.js +43 -0
  338. package/lib/script-links.d.ts +24 -0
  339. package/lib/script-links.js +204 -0
  340. package/lib/script-source-url.d.ts +8 -0
  341. package/lib/script-source-url.js +214 -0
  342. package/lib/script-world.d.ts +6 -0
  343. package/lib/script-world.js +65 -0
  344. package/lib/sm-dev-protocol.d.ts +251 -0
  345. package/lib/sm-dev-protocol.js +84 -0
  346. package/lib/source-map.d.ts +32 -0
  347. package/lib/source-map.js +177 -0
  348. package/lib/stack-link.d.ts +15 -0
  349. package/lib/stack-link.js +138 -0
  350. package/lib/unsafe-window-use.d.ts +1 -0
  351. package/lib/unsafe-window-use.js +229 -0
  352. package/lib/url-match.d.ts +40 -0
  353. package/lib/url-match.js +734 -0
  354. package/lib/userscript-header.d.ts +33 -0
  355. package/lib/userscript-header.js +248 -0
  356. package/lib/version-compare.d.ts +6 -0
  357. package/lib/version-compare.js +141 -0
  358. package/lib/version-probe.d.ts +17 -0
  359. package/lib/version-probe.js +120 -0
  360. package/package.json +55 -3
  361. package/skills/SourceMonkey-dev/SKILL.md +868 -0
  362. package/skills/SourceMonkey-dev/references/cdp.md +293 -0
  363. package/skills/SourceMonkey-dev/references/commands.md +297 -0
  364. package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
  365. package/skills/SourceMonkey-dev/references/specs.md +46 -0
  366. package/skills/SourceMonkey-friction/SKILL.md +103 -0
  367. package/skills/SourceMonkey-friction/references/template.md +59 -0
@@ -0,0 +1,285 @@
1
+ // The userscript managers in a browser, and turning them on, off and
2
+ // in: what `status`, a run's first lines and the `manager` commands
3
+ // read and do.
4
+ //
5
+ // - Over a CDP WebSocket of sm-dev's own, since Playwright's browser
6
+ // session can't reach the hidden target used here.
7
+ // - Listing, enabling and "Allow User Scripts" go through a hidden
8
+ // `chrome://extensions` target (no tab in the tab strip), calling
9
+ // the `chrome.developerPrivate` and `chrome.management` functions
10
+ // behind that page's own toggles. CDP has no calls of its own for
11
+ // them. No user gesture is needed.
12
+ // - Loading an unpacked extension is CDP's `Extensions.loadUnpacked`,
13
+ // which works over a debugging port in Chrome and Chromium alike.
14
+ import { quoted, WARNING_PREFIX } from './output-style.js';
15
+ import { MANAGERS, MANAGER_NAMES, managerFlag } from './managers.js';
16
+ import { sourceMonkeyExtensionId } from './extension-backend.js';
17
+ // The browser's own WebSocket. Every send fails once `signal` aborts or
18
+ // the socket closes, so nothing waits on a browser that's gone.
19
+ async function openBrowserSocket(endpoint, signal) {
20
+ const aborted = new Promise((_, reject) => {
21
+ const fail = () => reject(new Error('The browser took too long to answer'));
22
+ if (signal.aborted)
23
+ fail();
24
+ else
25
+ signal.addEventListener('abort', fail);
26
+ });
27
+ aborted.catch(() => undefined);
28
+ const waiting = new Map();
29
+ const url = /^wss?:/.test(endpoint) ? endpoint
30
+ : (await Promise.race([fetch(`${endpoint}/json/version`, { signal }).then((r) => r.json()), aborted])).webSocketDebuggerUrl;
31
+ const open = new WebSocket(url);
32
+ let closed = false;
33
+ const settleAll = () => {
34
+ closed = true;
35
+ for (const answer of waiting.values())
36
+ answer({ error: { message: 'The connection to the browser closed' } });
37
+ waiting.clear();
38
+ };
39
+ try {
40
+ await Promise.race([
41
+ new Promise((resolve, reject) => {
42
+ open.onopen = resolve;
43
+ open.onerror = () => reject(new Error('The browser\'s CDP socket could not be opened'));
44
+ }),
45
+ aborted,
46
+ ]);
47
+ }
48
+ catch (err) {
49
+ open.close();
50
+ throw err;
51
+ }
52
+ open.onclose = settleAll;
53
+ let last = 0;
54
+ open.onmessage = (event) => {
55
+ const reply = JSON.parse(String(event.data));
56
+ waiting.get(reply.id)?.(reply);
57
+ waiting.delete(reply.id);
58
+ };
59
+ const send = async (method, params = {}, sessionId) => {
60
+ if (closed)
61
+ throw new Error(`${method} failed: The connection to the browser closed`);
62
+ const id = ++last;
63
+ const reply = await Promise.race([
64
+ new Promise((resolve) => {
65
+ waiting.set(id, resolve);
66
+ open.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }));
67
+ }),
68
+ aborted,
69
+ ]);
70
+ if (reply.error)
71
+ throw new Error(`${method} failed: ${reply.error.message}`);
72
+ return reply.result ?? {};
73
+ };
74
+ return {
75
+ send,
76
+ close: () => {
77
+ open.close();
78
+ settleAll();
79
+ },
80
+ };
81
+ }
82
+ // The browser's own WebSocket, open for `fn`, closed after. Throws when
83
+ // it can't be reached, or `fn` takes longer than `timeoutMs`.
84
+ async function withBrowserSocket(endpoint, timeoutMs, fn) {
85
+ const signal = AbortSignal.timeout(timeoutMs);
86
+ const aborted = new Promise((_, reject) => {
87
+ signal.addEventListener('abort', () => reject(new Error('The browser took too long to answer')));
88
+ });
89
+ aborted.catch(() => undefined);
90
+ const socket = await openBrowserSocket(endpoint, signal);
91
+ try {
92
+ return await Promise.race([fn(socket.send, signal), aborted]);
93
+ }
94
+ finally {
95
+ socket.close();
96
+ }
97
+ }
98
+ // A hidden target (no tab in the tab strip) at `url`, attached, once
99
+ // `ready` (an expression) is true there. Not past `signal`, when every
100
+ // call fails at once. Returns its evaluate and its target id.
101
+ async function hiddenTarget(send, url, ready, signal) {
102
+ const { targetId } = await send('Target.createTarget', { url, hidden: true });
103
+ try {
104
+ const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true });
105
+ const evaluate = async (expression) => {
106
+ const reply = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }, sessionId);
107
+ if (reply.exceptionDetails) {
108
+ const { exception, text } = reply.exceptionDetails;
109
+ // A thrown string (as Violentmonkey's calls throw) has no description.
110
+ const why = exception?.description ?? (typeof exception?.value === 'string' ? exception.value : undefined) ?? text ?? 'It threw';
111
+ throw new Error(why.split('\n')[0]);
112
+ }
113
+ return reply.result?.value;
114
+ };
115
+ // The page's functions are there once it has loaded.
116
+ while (!await evaluate(ready).catch(() => false)) {
117
+ signal.throwIfAborted();
118
+ await new Promise((r) => setTimeout(r, 50));
119
+ }
120
+ return { evaluate, targetId };
121
+ }
122
+ catch (err) {
123
+ await send('Target.closeTarget', { targetId }).catch(() => undefined);
124
+ throw err;
125
+ }
126
+ }
127
+ // A hidden `chrome://extensions` target for `fn`'s calls, closed after.
128
+ async function withExtensionsPage(endpoint, timeoutMs, fn) {
129
+ return withBrowserSocket(endpoint, timeoutMs, async (send, signal) => {
130
+ const { evaluate, targetId } = await hiddenTarget(send, 'chrome://extensions', 'typeof chrome?.developerPrivate?.getExtensionsInfo === "function"', signal);
131
+ try {
132
+ return await fn(evaluate);
133
+ }
134
+ finally {
135
+ await send('Target.closeTarget', { targetId }).catch(() => undefined);
136
+ }
137
+ });
138
+ }
139
+ /**
140
+ * Opens a hidden page at `url` in the browser at `endpoint`, once
141
+ * `ready` (an expression) is true there, for several calls. Throws when
142
+ * it isn't ready within `timeoutMs`.
143
+ */
144
+ export async function openHiddenPage(endpoint, url, ready, timeoutMs = 10_000) {
145
+ const life = new AbortController();
146
+ const opening = AbortSignal.any([life.signal, AbortSignal.timeout(timeoutMs)]);
147
+ const socket = await openBrowserSocket(endpoint, life.signal);
148
+ let target;
149
+ try {
150
+ target = await Promise.race([
151
+ hiddenTarget(socket.send, url, ready, opening),
152
+ new Promise((_, reject) => opening.addEventListener('abort', () => reject(new Error('The page took too long to open')))),
153
+ ]);
154
+ }
155
+ catch (err) {
156
+ socket.close();
157
+ throw err;
158
+ }
159
+ return {
160
+ evaluate: async (expression, callMs = 30_000) => {
161
+ let timer;
162
+ try {
163
+ return await Promise.race([
164
+ target.evaluate(expression),
165
+ new Promise((_, reject) => {
166
+ timer = setTimeout(() => reject(new Error('The browser took too long to answer')), callMs);
167
+ }),
168
+ ]);
169
+ }
170
+ finally {
171
+ clearTimeout(timer);
172
+ }
173
+ },
174
+ close: async () => {
175
+ await socket.send('Target.closeTarget', { targetId: target.targetId }).catch(() => undefined);
176
+ socket.close();
177
+ life.abort();
178
+ },
179
+ };
180
+ }
181
+ // Which manager an extension is: by its id (SourceMonkey's fixed one,
182
+ // or a Web Store id, which a fetched copy keeps), else by its name.
183
+ function managerOf(info, sourceMonkeyId) {
184
+ if (info.id === sourceMonkeyId)
185
+ return 'sourcemonkey';
186
+ const byId = MANAGER_NAMES.find((name) => MANAGERS[name].storeId === info.id);
187
+ if (byId)
188
+ return byId;
189
+ return MANAGER_NAMES.find((name) => info.name.toLowerCase().startsWith(MANAGERS[name].label.toLowerCase())) ?? null;
190
+ }
191
+ /**
192
+ * The managers installed in the browser at `endpoint`, enabled or not:
193
+ * SourceMonkey first, then Tampermonkey, then Violentmonkey. Throws when
194
+ * it can't tell.
195
+ */
196
+ export async function installedManagers(endpoint, timeoutMs = 5000) {
197
+ const sourceMonkeyId = await sourceMonkeyExtensionId();
198
+ const infos = await withExtensionsPage(endpoint, timeoutMs, (evaluate) => evaluate('chrome.developerPrivate.getExtensionsInfo().then((all) => all.map((i) => ({ id: i.id, name: i.name, version: i.version, state: i.state, location: i.location, path: i.path, userScriptsAccess: i.userScriptsAccess })))'));
199
+ const found = [];
200
+ for (const info of infos) {
201
+ const manager = managerOf(info, sourceMonkeyId);
202
+ if (!manager)
203
+ continue;
204
+ found.push({
205
+ manager,
206
+ id: info.id,
207
+ name: info.name,
208
+ version: info.version,
209
+ enabled: info.state === 'ENABLED',
210
+ userScripts: info.userScriptsAccess?.isActive === true,
211
+ ...(info.location === 'UNPACKED' && info.path ? { path: info.path } : {}),
212
+ ...(info.location === 'FROM_STORE' ? { fromStore: true } : {}),
213
+ });
214
+ }
215
+ // In a fixed order, rather than Chrome's.
216
+ return found.sort((a, b) => MANAGER_NAMES.indexOf(a.manager) - MANAGER_NAMES.indexOf(b.manager));
217
+ }
218
+ /** `installedManagers`, or null when it can't tell, for what only reports. */
219
+ export function managersIn(endpoint) {
220
+ return installedManagers(endpoint).catch(() => null);
221
+ }
222
+ /** Turns an extension on or off, as its toggle in `chrome://extensions` does. */
223
+ export async function setExtensionEnabled(endpoint, id, enabled) {
224
+ await withExtensionsPage(endpoint, 10_000, (evaluate) => evaluate(`chrome.management.setEnabled(${JSON.stringify(id)}, ${enabled})`));
225
+ }
226
+ /** Turns on an extension's "Allow User Scripts", as its details page does. */
227
+ export async function allowUserScripts(endpoint, id) {
228
+ await withExtensionsPage(endpoint, 10_000, (evaluate) => evaluate(`chrome.developerPrivate.updateExtensionConfiguration({ extensionId: ${JSON.stringify(id)}, userScriptsAccess: true })`));
229
+ }
230
+ /**
231
+ * Loads the unpacked extension in `dir` into the browser, or reloads it
232
+ * from there when it's loaded already. Returns its id.
233
+ */
234
+ export async function loadUnpacked(endpoint, dir) {
235
+ const { id } = await withBrowserSocket(endpoint, 30_000, (send) => send('Extensions.loadUnpacked', { path: dir }));
236
+ return id;
237
+ }
238
+ /**
239
+ * Warns of each manager enabled beside `chosen`, whose scripts run in
240
+ * the run's tabs too, with how to turn it off. For a run's first lines,
241
+ * `status`, and the commands that connect without a run. `browserArg`:
242
+ * the flags that name the browser in the advice (see `browserArg` in
243
+ * run.ts).
244
+ */
245
+ export function otherManagersWarning(managers, chosen, browserArg = '') {
246
+ const others = [...new Set(managers.filter((m) => m.enabled && m.manager !== chosen).map((m) => m.manager))];
247
+ return others.map((manager) => `${WARNING_PREFIX}${MANAGERS[manager].label} is also enabled in this browser, so its scripts run too. To turn it off, use ${quoted(`sm-dev${browserArg} ${managerFlag(manager)} manager disable`)}.`);
248
+ }
249
+ /**
250
+ * The managers in the browser, for `status`: `SourceMonkey 0.0.1 (in
251
+ * use), Tampermonkey 5.5.1 (disabled)`. Each copy is listed, with what
252
+ * keeps it from running scripts. `None` when there are none. `mark`:
253
+ * what the chosen one is, when enabled.
254
+ */
255
+ export function managersText(managers, chosen, mark = 'in use') {
256
+ if (managers.length === 0)
257
+ return 'None';
258
+ return managers.map((m) => {
259
+ const notes = [
260
+ ...(m.manager === chosen && m.enabled ? [mark] : []),
261
+ ...(!m.enabled ? ['disabled'] : []),
262
+ ...(!m.userScripts ? ['Allow User Scripts is off'] : []),
263
+ ];
264
+ return `${m.name} ${m.version}${notes.length > 0 ? ` (${notes.join(', ')})` : ''}`;
265
+ }).join(', ');
266
+ }
267
+ export function managerState(managers, name) {
268
+ const copies = managers.filter((m) => m.manager === name);
269
+ return copies.some((m) => m.enabled) ? 'installed' : copies.length > 0 ? 'disabled' : 'absent';
270
+ }
271
+ /** How a run's first line says the chosen manager's state: `has Tampermonkey`. */
272
+ export function stateLabel(state, name) {
273
+ const { label } = MANAGERS[name];
274
+ return { installed: `has ${label}`, disabled: `${label} disabled`, absent: `no ${label}` }[state];
275
+ }
276
+ /** The browser at `endpoint`: Chrome's version, and whether it's headless. Null when nothing answers. */
277
+ export async function browserVersion(endpoint) {
278
+ try {
279
+ const info = await withBrowserSocket(endpoint, 2000, (send) => send('Browser.getVersion'));
280
+ return { version: (info.product ?? '').replace(/^\w+\//, ''), headless: /\bHeadlessChrome\//.test(info.userAgent ?? '') };
281
+ }
282
+ catch {
283
+ return null;
284
+ }
285
+ }
@@ -0,0 +1,20 @@
1
+ import type { Page } from '@playwright/test';
2
+ /** What `--lang` takes, for its messages. */
3
+ export declare const LANG_NEEDS = "The --lang flag needs a language tag like zh-CN, or a list like zh-CN,en";
4
+ /** Whether `value` looks like a language tag (`en`, `zh-CN`). */
5
+ export declare function isLanguageTag(value: unknown): value is string;
6
+ /**
7
+ * A `--lang` value as language tags, in the canonical case
8
+ * (`zh-cn` is `zh-CN`). Throws an Error with the message to show when
9
+ * a tag isn't one.
10
+ */
11
+ export declare function parseLang(text: string): string[];
12
+ /** Chrome's own flags for the languages, for a browser the run launches. */
13
+ export declare function langLaunchArgs(langs: string[] | undefined): string[];
14
+ /**
15
+ * Has `page` prefer `langs` from now on, through a CDP session left
16
+ * open, since an override lasts only while the session that set it
17
+ * does. Applies from the tab's next document on. A failure is said
18
+ * on `out`, naming the step, and the tab goes on without.
19
+ */
20
+ export declare function applyLang(page: Page, langs: string[] | undefined, out: (line: string) => void): Promise<void>;
@@ -0,0 +1,86 @@
1
+ // `start --lang`: the languages the run's tabs say they prefer, as
2
+ // `navigator.language(s)`, the `Accept-Language` header, and the
3
+ // default locale of `Intl` (dates, numbers).
4
+ //
5
+ // Set per tab over CDP (the Chrome DevTools Protocol), held for the
6
+ // tab's life by a session of its own, so it works in a shared browser
7
+ // without touching its other tabs. That reaches the tab's own frames
8
+ // only: cross-site iframes and workers have targets of their own, and
9
+ // keep the browser's languages. A headless temp browser is also
10
+ // launched with Chrome's own language flags, which reach those too,
11
+ // and a popup's first load.
12
+ //
13
+ // The extension's own requests (`GM_xmlhttpRequest`, `GM_download`)
14
+ // carry the browser's languages, not a tab's: a launched browser's
15
+ // flags, or a connected one's own.
16
+ /** What `--lang` takes, for its messages. */
17
+ export const LANG_NEEDS = 'The --lang flag needs a language tag like zh-CN, or a list like zh-CN,en';
18
+ // A language first, then subtags: Intl alone also takes private-use
19
+ // tags (`x-foo`), which no browser sends.
20
+ const LANGUAGE_TAG = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$/;
21
+ /** Whether `value` looks like a language tag (`en`, `zh-CN`). */
22
+ export function isLanguageTag(value) {
23
+ return typeof value === 'string' && LANGUAGE_TAG.test(value);
24
+ }
25
+ function notATag(tag) {
26
+ return new Error(`The --lang flag got ${tag}, which is not a language tag. It needs one like zh-CN, or a list like zh-CN,en.`);
27
+ }
28
+ /**
29
+ * A `--lang` value as language tags, in the canonical case
30
+ * (`zh-cn` is `zh-CN`). Throws an Error with the message to show when
31
+ * a tag isn't one.
32
+ */
33
+ export function parseLang(text) {
34
+ const tags = text.split(',').map((t) => t.trim());
35
+ if (tags.some((t) => t === ''))
36
+ throw new Error(LANG_NEEDS);
37
+ for (const tag of tags) {
38
+ if (!isLanguageTag(tag))
39
+ throw notATag(tag);
40
+ try {
41
+ Intl.getCanonicalLocales(tag);
42
+ }
43
+ catch {
44
+ throw notATag(tag);
45
+ }
46
+ }
47
+ return [...new Set(Intl.getCanonicalLocales(tags))];
48
+ }
49
+ /** Chrome's own flags for the languages, for a browser the run launches. */
50
+ export function langLaunchArgs(langs) {
51
+ return langs?.length ? [`--lang=${langs[0]}`, `--accept-lang=${langs.join(',')}`] : [];
52
+ }
53
+ /**
54
+ * Has `page` prefer `langs` from now on, through a CDP session left
55
+ * open, since an override lasts only while the session that set it
56
+ * does. Applies from the tab's next document on. A failure is said
57
+ * on `out`, naming the step, and the tab goes on without.
58
+ */
59
+ export async function applyLang(page, langs, out) {
60
+ if (!langs?.length)
61
+ return;
62
+ let step = 'Connecting to the tab failed';
63
+ try {
64
+ const session = await page.context().newCDPSession(page);
65
+ // An empty userAgent keeps the browser's own, and its client hints
66
+ // (navigator.userAgentData); only the languages change.
67
+ step = 'Setting navigator.languages and Accept-Language failed';
68
+ await session.send('Emulation.setUserAgentOverride', { userAgent: '', acceptLanguage: langs.join(',') });
69
+ // Intl's. Last, as only one connection to the browser can hold it.
70
+ step = 'Setting Intl\'s locale failed';
71
+ await session.send('Emulation.setLocaleOverride', { locale: langs[0] });
72
+ }
73
+ catch (err) {
74
+ if (page.isClosed())
75
+ return;
76
+ const message = err.message;
77
+ out(/already in effect/.test(message)
78
+ ? `[sm-dev] Could not set the tab's locale for Intl to ${langs[0]}: Another connection to the browser already overrides the tab's locale`
79
+ : `[sm-dev] Could not set the tab's languages to ${langs.join(',')}: ${step} (${cdpDetail(message)})`);
80
+ }
81
+ }
82
+ // What a CDP call's error says, without Playwright's and the
83
+ // protocol's prefixes: `Protocol error (X.y): Not allowed` is `Not allowed`.
84
+ function cdpDetail(message) {
85
+ return message.replace(/^cdpSession\.send: /, '').replace(/^Protocol error \([^)]*\): /, '').split('\n')[0];
86
+ }
@@ -0,0 +1,14 @@
1
+ /** The profile a port's browser keeps. */
2
+ export declare function defaultProfile(port: number): string;
3
+ /** The path with the home directory as ~, for messages. */
4
+ export declare function withTilde(file: string): string;
5
+ /**
6
+ * Why `--open-browser` can't start a browser at `endpoint`, or null: it
7
+ * starts one on this machine only.
8
+ */
9
+ export declare function launchEndpointMistake(endpoint: string, setting?: string, starter?: string): string | null;
10
+ /**
11
+ * Starts the browser at `endpoint` unless one answers there already,
12
+ * and says which. Throws an error saying why it couldn't.
13
+ */
14
+ export declare function launchBrowser(endpoint: string, profile: string | undefined, out: (line: string) => void, muted?: boolean, headless?: boolean): Promise<void>;
@@ -0,0 +1,184 @@
1
+ // `sm-dev --open-browser`: starts a browser that stays open after sm-dev
2
+ // exits, with a window and a debugging port, for runs to connect to.
3
+ // Unlike a temp browser (Playwright's own launch, which ends
4
+ // with the run), it is spawned on its own, as a person would start it.
5
+ //
6
+ // - The port is `--browser`'s (default $SM_DEV_BROWSER or 9233), which
7
+ // is also where later commands connect, so `sm-dev --open-browser
8
+ // start x.user.js --detach` needs nothing else.
9
+ // - The profile is kept, so sign-ins and extensions carry over:
10
+ // `~/.config/sm-dev/browser-profile-<port>`, one for each port, since
11
+ // only one Chrome at a time can use a profile. `--profile` picks
12
+ // another.
13
+ // - Idempotent: a browser already answering on the port is used as is,
14
+ // whatever its profile.
15
+ // - Google Chrome when it's installed (better at Google sign-in than
16
+ // other builds), else Playwright's Chromium. $SM_DEV_BROWSER_BINARY
17
+ // names another binary, and $SM_DEV_BROWSER_ARGS adds arguments (the
18
+ // tests add --headless=new).
19
+ import { execFileSync, spawn } from 'node:child_process';
20
+ import { existsSync, openSync, closeSync, readFileSync, readlinkSync } from 'node:fs';
21
+ import { mkdir } from 'node:fs/promises';
22
+ import os from 'node:os';
23
+ import path from 'node:path';
24
+ import { loadPlaywright, missingChromium } from './playwright-chromium.js';
25
+ import { processAlive } from './runs.js';
26
+ import { quoted } from './output-style.js';
27
+ // How long a started browser has to open its port.
28
+ const START_TIMEOUT_MS = 10_000;
29
+ /** Where Google Chrome is installed, by platform, most likely first. */
30
+ function chromeCandidates() {
31
+ if (process.platform === 'darwin') {
32
+ return ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', path.join(os.homedir(), 'Applications/Google Chrome.app/Contents/MacOS/Google Chrome')];
33
+ }
34
+ if (process.platform === 'win32') {
35
+ const dirs = [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter((d) => !!d);
36
+ return dirs.map((d) => path.join(d, 'Google', 'Chrome', 'Application', 'chrome.exe'));
37
+ }
38
+ return ['/opt/google/chrome/chrome', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable'];
39
+ }
40
+ /** The browser to start, and what to call it. */
41
+ async function browserBinary() {
42
+ const chosen = process.env.SM_DEV_BROWSER_BINARY?.trim();
43
+ if (chosen)
44
+ return { file: chosen, name: path.basename(chosen) };
45
+ const chrome = chromeCandidates().find((f) => existsSync(f));
46
+ if (chrome)
47
+ return { file: chrome, name: 'Google Chrome' };
48
+ const { chromium, install } = await loadPlaywright();
49
+ const file = chromium.executablePath();
50
+ const missing = missingChromium(file, install);
51
+ if (missing)
52
+ throw new Error(`No browser to launch: Google Chrome isn't installed. ${missing}`);
53
+ return { file, name: 'Chromium' };
54
+ }
55
+ /** The profile a port's browser keeps. */
56
+ export function defaultProfile(port) {
57
+ const config = process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), '.config');
58
+ return path.join(config, 'sm-dev', `browser-profile-${port}`);
59
+ }
60
+ /** The path with the home directory as ~, for messages. */
61
+ export function withTilde(file) {
62
+ const home = os.homedir();
63
+ return file === home || file.startsWith(home + path.sep) ? '~' + file.slice(home.length) : file;
64
+ }
65
+ async function answers(endpoint) {
66
+ try {
67
+ const res = await fetch(`${endpoint}/json/version`, { signal: AbortSignal.timeout(2000) });
68
+ return res.ok;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ /**
75
+ * Why the profile can't be used, when another browser has it: Chrome
76
+ * locks a profile with a `SingletonLock` link to `host-pid`, and a
77
+ * second browser on it hands its window to the first and exits,
78
+ * dropping its debugging port. Null when it's free, or when the lock
79
+ * can't be read (Windows keeps it differently) or is another machine's
80
+ * (a shared home directory): the start then fails, or Chrome asks.
81
+ */
82
+ function profileInUse(dir) {
83
+ let owner;
84
+ try {
85
+ owner = readlinkSync(path.join(dir, 'SingletonLock'));
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ const match = /^(.*)-(\d+)$/.exec(owner);
91
+ if (!match || match[1] !== os.hostname() || !processAlive(Number(match[2])))
92
+ return null;
93
+ const port = debuggingPort(dir, Number(match[2]));
94
+ return port
95
+ ? `The profile ${withTilde(dir)} is in use by the browser on port ${port}. Use ${quoted(`--browser ${port}`)}, or another ${quoted('--profile')}`
96
+ : `The profile ${withTilde(dir)} is in use by a browser without a debugging port (pid ${match[2]}). Close it, or use another ${quoted('--profile')}`;
97
+ }
98
+ // Where the browser with this pid listens, if it does: the port in its
99
+ // command line, or (for --remote-debugging-port=0) the one Chrome writes
100
+ // in the profile.
101
+ function debuggingPort(dir, pid) {
102
+ try {
103
+ const args = execFileSync('ps', ['-o', 'args=', '-p', String(pid)], { encoding: 'utf8' });
104
+ const port = /--remote-debugging-port=(\d+)/.exec(args)?.[1];
105
+ if (port && port !== '0')
106
+ return port;
107
+ }
108
+ catch {
109
+ // No ps (Windows), or it's gone.
110
+ }
111
+ try {
112
+ return readFileSync(path.join(dir, 'DevToolsActivePort'), 'utf8').split('\n')[0].trim() || undefined;
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
118
+ /**
119
+ * Why `--open-browser` can't start a browser at `endpoint`, or null: it
120
+ * starts one on this machine only.
121
+ */
122
+ export function launchEndpointMistake(endpoint, setting = '--browser', starter = 'The --open-browser flag starts a browser') {
123
+ const url = new URL(endpoint);
124
+ // Chrome's debugging port listens on 127.0.0.1 only, so not [::1];
125
+ // and a port is all it takes, not a path.
126
+ const local = ['127.0.0.1', 'localhost'].includes(url.hostname) && url.port && url.pathname === '/' && !url.search;
127
+ if (local)
128
+ return null;
129
+ return `${starter} on this machine, so ${setting} must be a local port, not ${endpoint.replace(/^https?:\/\//, '')}`;
130
+ }
131
+ /**
132
+ * Starts the browser at `endpoint` unless one answers there already,
133
+ * and says which. Throws an error saying why it couldn't.
134
+ */
135
+ export async function launchBrowser(endpoint, profile, out, muted = false, headless = false) {
136
+ const url = new URL(endpoint);
137
+ const where = `${url.hostname}:${url.port}`;
138
+ if (await answers(endpoint)) {
139
+ out(`[sm-dev] A browser is already running at ${where}`);
140
+ return;
141
+ }
142
+ const port = Number(url.port);
143
+ const dir = path.resolve(profile ?? defaultProfile(port));
144
+ await mkdir(dir, { recursive: true, mode: 0o700 });
145
+ const inUse = profileInUse(dir);
146
+ if (inUse)
147
+ throw new Error(inUse);
148
+ const { file, name } = await browserBinary();
149
+ const extra = process.env.SM_DEV_BROWSER_ARGS?.split(/\s+/).filter(Boolean) ?? [];
150
+ // Its output goes to a file in the profile, where it can be read if
151
+ // it fails to start.
152
+ const logFile = path.join(dir, 'sm-dev-browser.log');
153
+ const log = openSync(logFile, 'w');
154
+ // Why it stopped before its port opened: its exit code, or the error
155
+ // that kept it from starting at all (no such file, say).
156
+ let ended = null;
157
+ let child;
158
+ try {
159
+ child = spawn(file, [
160
+ `--remote-debugging-port=${port}`, `--user-data-dir=${dir}`,
161
+ '--no-first-run', '--no-default-browser-check', ...(muted ? ['--mute-audio'] : []), ...(headless ? ['--headless=new'] : []), ...extra,
162
+ ], { detached: true, stdio: ['ignore', log, log] });
163
+ child.on('exit', (code, signal) => { ended ??= `It exited (${signal ?? `code ${code}`})`; });
164
+ child.on('error', (err) => { ended = `It could not be started from ${file}: ${err.message}`; });
165
+ child.unref();
166
+ }
167
+ finally {
168
+ closeSync(log);
169
+ }
170
+ const deadline = Date.now() + START_TIMEOUT_MS;
171
+ while (!await answers(endpoint)) {
172
+ if (ended !== null || Date.now() > deadline) {
173
+ // One that's up without its port would hold the profile's lock,
174
+ // and the next --open-browser would be refused.
175
+ if (ended === null)
176
+ child.kill();
177
+ const last = readFileSync(logFile, 'utf8').trim().split('\n').filter(Boolean).at(-1);
178
+ const why = ended ?? `Its port didn't open within ${START_TIMEOUT_MS / 1000}s, so it was closed`;
179
+ throw new Error(`Could not launch ${name} at ${where}: ${why}${last ? `. Its log (${withTilde(logFile)}) ends with: ${last}` : ''}`);
180
+ }
181
+ await new Promise((r) => setTimeout(r, 100));
182
+ }
183
+ out(`[sm-dev] Launched ${name} at ${where} (profile ${withTilde(dir)})`);
184
+ }