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,560 @@
1
+ // Making an editable copy of an installed script on local disk.
2
+ //
3
+ // The copy is written into the "Scripts copied in Downloads" directory
4
+ // collection — `<Downloads>/SourceMonkey/` — through `chrome.downloads`,
5
+ // the one write path an extension has that needs no dialog and reports
6
+ // back the absolute path of what it wrote. Everything else (the
7
+ // dialog, the enable sweep, the navigation) lives in `copy-dialog.ts`
8
+ // and `userscript.ts`; this module is the pure logic plus the write.
9
+ //
10
+ // See docs/local-copy.md.
11
+ import { base64ToBytes } from '../bytes.js';
12
+ import { assertWorkerContext } from '../run-context.js';
13
+ import { MAX_SCAN_DEPTH } from '../collection-scan.js';
14
+ import { collectionNameInUse } from './collection-display.js';
15
+ import { canonicalCollectionInput, isHttpUrl, baseName, joinPath, pathToFileUrl, statFromListing, } from '../file-locations.js';
16
+ import { localFileCandidates } from '../local-file-overrides.js';
17
+ import { scriptBasename } from '../script-display.js';
18
+ import { locationKey } from './script-identity.js';
19
+ import { dlog } from '../debug-log.js';
20
+ import { createCollection, describeError, emitInitialLoadLog, getCollections, refreshCollection, setScriptsEnabled, withoutBodies, } from './script-store.js';
21
+ import { callWorker } from './worker-client.js';
22
+ import { joinFullSource, splitHeaderBody } from '../userscript-header.js';
23
+ // ---------- The default collection and where Downloads is ----------
24
+ // The subdirectory of the browser's Downloads folder every copy lands
25
+ // in. Also the directory the default collection is created at.
26
+ export const DOWNLOADS_COPIES_SUBDIR = 'SourceMonkey';
27
+ // The default collection's seeded name. The user's to edit — the
28
+ // collection is found by the id recorded below, never by this.
29
+ export const DOWNLOADS_COPIES_NAME = 'Scripts copied in Downloads';
30
+ // `chrome.storage.local` key. Both values are learned from the first
31
+ // completed copy: Chrome exposes no API for where Downloads is, but a
32
+ // finished download reports its absolute path, and ours was written to
33
+ // a known relative name under it.
34
+ export const DOWNLOADS_COPIES_KEY = 'smDownloadsCopies';
35
+ export async function readDownloadsCopiesSetting() {
36
+ const got = await chrome.storage.local.get(DOWNLOADS_COPIES_KEY);
37
+ return parseDownloadsCopiesSetting(got[DOWNLOADS_COPIES_KEY]);
38
+ }
39
+ export function parseDownloadsCopiesSetting(raw) {
40
+ if (!raw || typeof raw !== 'object')
41
+ return null;
42
+ const { downloadsDir, collectionId } = raw;
43
+ if (typeof downloadsDir !== 'string' || !downloadsDir)
44
+ return null;
45
+ return {
46
+ downloadsDir,
47
+ collectionId: typeof collectionId === 'string' && collectionId ? collectionId : null,
48
+ };
49
+ }
50
+ // Worker side (`ensureDownloadsCollection`). A page writes it through
51
+ // worker-client.ts's `writeSetting`.
52
+ async function writeDownloadsCopiesSetting(setting) {
53
+ assertWorkerContext('write storage');
54
+ await chrome.storage.local.set({ [DOWNLOADS_COPIES_KEY]: setting });
55
+ }
56
+ // Where the copies go, as an absolute directory path.
57
+ export function downloadsCopiesDir(downloadsDir) {
58
+ return joinPath(downloadsDir, DOWNLOADS_COPIES_SUBDIR);
59
+ }
60
+ // The collection the setting names, or — failing the id — whichever
61
+ // collection already sits at the copies directory. Null when neither
62
+ // exists yet.
63
+ export function findDownloadsCollection(setting, collections) {
64
+ if (!setting)
65
+ return null;
66
+ const byId = setting.collectionId
67
+ ? collections.find((c) => c.id === setting.collectionId)
68
+ : undefined;
69
+ const wantedPath = dirKey(downloadsCopiesDir(setting.downloadsDir));
70
+ // A collection the id names but that has since moved out from under
71
+ // us — Chrome's Downloads location changed — is an ordinary directory
72
+ // collection now, not ours.
73
+ if (byId && dirKey(byId.path) === wantedPath)
74
+ return byId;
75
+ return collections.find((c) => dirKey(c.path) === wantedPath) ?? null;
76
+ }
77
+ // A directory path or `file://` URL as a comparable key: the bare
78
+ // path, with no trailing slash.
79
+ function dirKey(pathOrUrl) {
80
+ return canonicalCollectionInput(pathOrUrl).replace(/\/+$/, '');
81
+ }
82
+ // ---------- Names and paths ----------
83
+ // The relative path a copy of `script` gets by default: its own file
84
+ // name, made a `.user.js` if it isn't one. Nothing cleverer — a copy
85
+ // of a copy defaults to the same name too, and the dialog refuses
86
+ // that (`isCopyOfItself`) rather than inventing a name the user didn't
87
+ // ask for.
88
+ export function defaultCopyName(script) {
89
+ const base = scriptBasename(script.relativePath).replace(/[\\/:*?"<>|]/g, '_');
90
+ return base.endsWith('.user.js') ? base : `${base.replace(/\.js$/, '')}.user.js`;
91
+ }
92
+ // Two sentences, so both take a period — see docs/diagnostics.md.
93
+ export const COPY_OVER_ITSELF = 'The copy source and destination are the same. Choose another filename.';
94
+ // True when writing `fileName` under `copiesDir` would land on the very
95
+ // file `script` is loaded from — a copy standing in the copies
96
+ // directory, copied again under its own name.
97
+ export function isCopyOfItself(script, copiesDir, fileName) {
98
+ return locationKey(script.fileUrl) === locationKey(pathToFileUrl(joinPath(copiesDir, fileName.trim())));
99
+ }
100
+ export const COPY_NAME_REQUIRED = 'Filename is required';
101
+ export const COPY_NAME_NOT_USERSCRIPT = 'Filename must end with .user.js';
102
+ export const COPY_NAME_NOT_RELATIVE = 'Filename must be a relative path, with no leading slash or ..';
103
+ export const COPY_NAME_TOO_DEEP = `Filename can be at most ${MAX_SCAN_DEPTH} folders deep, or the collection will not find it`;
104
+ export const COPY_NAME_BAD_CHARS = 'Filename cannot contain \\ : * ? " < > |';
105
+ // Why `relativePath` can't name a copy, or null when it can. Mirrors
106
+ // what `chrome.downloads.download` refuses (absolute paths, `..`) and
107
+ // what the collection scan would never find (too deep).
108
+ export function validateCopyName(relativePath) {
109
+ const name = relativePath.trim();
110
+ if (!name)
111
+ return COPY_NAME_REQUIRED;
112
+ if (UNWRITABLE_CHARS.test(name))
113
+ return COPY_NAME_BAD_CHARS;
114
+ if (name.startsWith('/'))
115
+ return COPY_NAME_NOT_RELATIVE;
116
+ const segments = name.split('/');
117
+ // `.trim()` catches a blank segment too — `a/ /b.user.js` would make
118
+ // a directory named with a space.
119
+ if (segments.some((s) => s.trim() === '' || s === '.' || s === '..')) {
120
+ return COPY_NAME_NOT_RELATIVE;
121
+ }
122
+ if (!name.endsWith('.user.js') || segments[segments.length - 1] === '.user.js') {
123
+ return COPY_NAME_NOT_USERSCRIPT;
124
+ }
125
+ if (segments.length - 1 > MAX_SCAN_DEPTH)
126
+ return COPY_NAME_TOO_DEEP;
127
+ return null;
128
+ }
129
+ // The Downloads folder a saved file implies: the absolute path Chrome
130
+ // reported, minus the relative name we asked for. Null when the file
131
+ // went somewhere else — the user has Chrome's "Ask where to save each
132
+ // file" on and picked another folder.
133
+ export function downloadsDirFromSaved(absolutePath, relativeName) {
134
+ const suffix = `/${relativeName}`;
135
+ if (!absolutePath.endsWith(suffix))
136
+ return null;
137
+ const dir = absolutePath.slice(0, -suffix.length);
138
+ return dir || null;
139
+ }
140
+ // The source with `@version` / `@description` replaced, added, or
141
+ // removed. Line-for-line otherwise: every other line, the line endings,
142
+ // and the spacing between tag and value are kept as they were, so a
143
+ // diff against the original shows only the edit. A source with no
144
+ // metadata block is returned unchanged — there is nowhere to write.
145
+ export function rewriteHeaderTags(source, edits) {
146
+ const split = splitHeaderBody(source);
147
+ if (split.error)
148
+ return source;
149
+ const eol = split.headerText.includes('\r\n') ? '\r\n' : '\n';
150
+ const lines = split.headerText.split(/\r\n|\r|\n/);
151
+ for (const [tag, value] of [['version', edits.version], ['description', edits.description]]) {
152
+ if (value === undefined)
153
+ continue;
154
+ applyTagEdit(lines, tag, value.trim());
155
+ }
156
+ // Not `joinFullSource`: it restores the line break after the closing
157
+ // marker as a bare `\n`, which is fine for display but would leave a
158
+ // CRLF file with one LF line in the middle.
159
+ return split.leadingText + lines.join(eol) + eol + split.bodyText;
160
+ }
161
+ // Plain `@tag` only — a localized `@description:de` is a different tag
162
+ // and is left alone.
163
+ function tagLinePattern(tag) {
164
+ return new RegExp(`^([ \\t]*\\/\\/[ \\t]*@${tag})(?:([ \\t]+)(.*\\S))?[ \\t]*$`);
165
+ }
166
+ function applyTagEdit(lines, tag, value) {
167
+ const re = tagLinePattern(tag);
168
+ const at = lines.findIndex((l) => re.test(l));
169
+ if (at >= 0) {
170
+ if (!value) {
171
+ lines.splice(at, 1);
172
+ return;
173
+ }
174
+ const m = re.exec(lines[at]);
175
+ lines[at] = `${m[1]}${m[2] ?? ' '}${value}`;
176
+ return;
177
+ }
178
+ if (!value)
179
+ return;
180
+ // New line, placed after `@name` (any locale) and aligned to it, or
181
+ // straight after the opening marker when there is no `@name`.
182
+ const nameRe = /^([ \t]*\/\/[ \t]*@)name(?::[\w-]+)?(?:([ \t]+)(.*\S))?[ \t]*$/;
183
+ const nameAt = lines.findIndex((l) => nameRe.test(l));
184
+ let prefix = '// @';
185
+ let valueColumn = -1;
186
+ if (nameAt >= 0) {
187
+ const m = nameRe.exec(lines[nameAt]);
188
+ prefix = m[1];
189
+ if (m[3] !== undefined)
190
+ valueColumn = lines[nameAt].length - m[3].length;
191
+ }
192
+ const head = `${prefix}${tag}`;
193
+ const pad = valueColumn > head.length ? ' '.repeat(valueColumn - head.length) : ' ';
194
+ lines.splice(nameAt >= 0 ? nameAt + 1 : 1, 0, `${head}${pad}${value}`);
195
+ }
196
+ // The dependency files a copy of `script` would carry: the bytes the
197
+ // row already holds, laid out as above. Deduped by target path, so two
198
+ // declarations of one URL write one file. Skipped: an absolute URL
199
+ // that isn't http(s) (a `data:` one has no file to write), and a
200
+ // relative ref that climbs out of the script's folder, which no path
201
+ // under the copy's folder can satisfy.
202
+ export function dependencyFiles(script) {
203
+ const out = new Map();
204
+ const add = (ref, url, body) => {
205
+ const relativePath = isRelativeRef(ref)
206
+ ? writable(relativeRefPath(ref))
207
+ : isHttpUrl(url) ? mirrorPath(url) : null;
208
+ if (!relativePath || out.has(relativePath))
209
+ return;
210
+ out.set(relativePath, { url, relativePath, body, size: body.size });
211
+ };
212
+ for (const r of script.requires ?? []) {
213
+ add(r.ref, r.url, new Blob([r.source], { type: 'text/javascript' }));
214
+ }
215
+ for (const r of script.resources ?? []) {
216
+ add(r.ref, r.url, new Blob([base64ToBytes(r.bytesBase64)], { type: r.contentType || 'application/octet-stream' }));
217
+ }
218
+ return [...out.values()];
219
+ }
220
+ // A ref with no scheme and no leading `/`: one that resolves against the
221
+ // script's own folder.
222
+ export function isRelativeRef(ref) {
223
+ const trimmed = ref.trim().split('#')[0];
224
+ return trimmed !== '' && !/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !trimmed.startsWith('/');
225
+ }
226
+ // Where a relative ref lands under its script's folder, decoded to the
227
+ // on-disk spelling, so the same ref finds it beside a copy or an
228
+ // unzipped export. Null when it climbs out of that folder or names a
229
+ // directory. An encoded `/` (`..%2F`) counts as unusable rather than
230
+ // decoding into a path the ref never named.
231
+ export function relativeRefPath(ref) {
232
+ const root = '/sourcemonkey-ref-root/';
233
+ let resolved;
234
+ try {
235
+ resolved = new URL(ref.trim(), `file://${root}script.user.js`);
236
+ }
237
+ catch {
238
+ return null;
239
+ }
240
+ if (!resolved.pathname.startsWith(root))
241
+ return null;
242
+ const rest = resolved.pathname.slice(root.length);
243
+ if (!rest || rest.endsWith('/'))
244
+ return null;
245
+ let segments;
246
+ try {
247
+ segments = rest.split('/').map((seg) => decodeURIComponent(seg));
248
+ }
249
+ catch {
250
+ return null;
251
+ }
252
+ return segments.some((seg) => seg.includes('/')) ? null : segments.join('/');
253
+ }
254
+ // `<host>/<path>` of a URL as an on-disk relative path, via the same
255
+ // candidate builder the override lookup uses so the two can't drift.
256
+ // Null when the URL yields no candidate (no path, or a path that names
257
+ // a directory).
258
+ //
259
+ // The longest candidate `chrome.downloads` can actually write. It
260
+ // refuses a path with characters illegal on any platform — `:` above
261
+ // all, which a `host:port` mirror directory would carry. The lookup
262
+ // tries every shorter suffix too, so dropping to `lib/util.js` beside
263
+ // the script still resolves; a basename that itself can't be written
264
+ // yields null and the dependency is skipped.
265
+ export function mirrorPath(url) {
266
+ // A stand-in root no real file sits under: the candidate builder
267
+ // offers nothing for a URL already inside its root, which `file:///`
268
+ // would be for every `file://` URL.
269
+ const root = 'file:///sourcemonkey-mirror-root/';
270
+ for (const candidate of localFileCandidates(url, [root])) {
271
+ if (!candidate.startsWith(root))
272
+ continue;
273
+ let decoded;
274
+ try {
275
+ decoded = candidate.slice(root.length).split('/').map((seg) => decodeURIComponent(seg)).join('/');
276
+ }
277
+ catch {
278
+ continue;
279
+ }
280
+ if (!UNWRITABLE_CHARS.test(decoded))
281
+ return decoded;
282
+ }
283
+ return null;
284
+ }
285
+ function writable(path) {
286
+ return path !== null && !UNWRITABLE_CHARS.test(path) ? path : null;
287
+ }
288
+ // What `chrome.downloads.download` rejects in a `filename` — the
289
+ // Windows-illegal set, enforced everywhere. Same set `validateCopyName`
290
+ // refuses for the script's own name.
291
+ const UNWRITABLE_CHARS = /[\\:*?"<>|]/;
292
+ // Every file the copy writes, the script first. Dependencies sit under
293
+ // the script's own folder.
294
+ export function planFiles(script, plan) {
295
+ const source = joinFullSource(script.leadingText ?? '', script.headerText, script.bodyText);
296
+ const text = rewriteHeaderTags(source, { version: plan.version, description: plan.description });
297
+ const files = [
298
+ { relativePath: plan.fileName, body: new Blob([text], { type: 'text/javascript' }) },
299
+ ];
300
+ if (plan.includeDependencies) {
301
+ const dir = plan.fileName.includes('/') ? plan.fileName.slice(0, plan.fileName.lastIndexOf('/')) : '';
302
+ for (const dep of dependencyFiles(script)) {
303
+ files.push({ relativePath: dir ? `${dir}/${dep.relativePath}` : dep.relativePath, body: dep.body });
304
+ }
305
+ }
306
+ return files;
307
+ }
308
+ // Which of `files` already exist under `copiesDir`, read through the
309
+ // same directory listing a refresh uses. A listing that can't be read
310
+ // (the folder doesn't exist yet, say) counts as empty — the write will
311
+ // create it.
312
+ export async function checkExistingFiles(copiesDir, files) {
313
+ const cache = new Map();
314
+ const existing = [];
315
+ for (const f of files) {
316
+ const abs = joinPath(copiesDir, f.relativePath);
317
+ const slash = abs.lastIndexOf('/');
318
+ let entry = null;
319
+ try {
320
+ entry = await statFromListing(abs.slice(0, slash), abs.slice(slash + 1), cache);
321
+ }
322
+ catch {
323
+ // No listing, no file.
324
+ }
325
+ if (!entry || entry.isDir)
326
+ continue;
327
+ existing.push(f.relativePath);
328
+ }
329
+ return { existing };
330
+ }
331
+ // ---------- Writing ----------
332
+ export const COPY_CANCELLED = 'Saving the copy was cancelled';
333
+ // How long to wait for one download to finish. A blob of a few hundred
334
+ // KB completes in milliseconds; the budget only matters when Chrome is
335
+ // holding the file for a Save As box the user has left open.
336
+ const DOWNLOAD_TIMEOUT_MS = 10 * 60_000;
337
+ export async function writeUnderDownloads(files, overwrite) {
338
+ const saved = [];
339
+ for (const f of files) {
340
+ saved.push(await downloadOne(f.relativePath, f.body, overwrite));
341
+ }
342
+ return saved;
343
+ }
344
+ // Delete files this copy wrote, after it turned out not to be a copy.
345
+ // Best effort and never thrown from: the caller is already reporting a
346
+ // failure, and a file we couldn't remove is a smaller problem than
347
+ // replacing that report with a second one.
348
+ //
349
+ // Why bother: a `.user.js` left behind in the copies directory is not
350
+ // inert. The next refresh of that collection scans it in as a new
351
+ // script — and a new row lands ENABLED — so an abandoned copy would
352
+ // start running a script the user never agreed to install.
353
+ async function discardWritten(written) {
354
+ for (const w of written) {
355
+ try {
356
+ await chrome.downloads.removeFile(w.id);
357
+ }
358
+ catch (err) {
359
+ dlog(`[SourceMonkey] could not remove ${w.path} after a failed copy: ${describeError(err)}`);
360
+ }
361
+ // The download-history entry too, so the failed copy doesn't sit in
362
+ // the user's downloads list pointing at a file that is gone.
363
+ try {
364
+ await chrome.downloads.erase({ id: w.id });
365
+ }
366
+ catch {
367
+ // Nothing to do about it, and nothing to tell the user.
368
+ }
369
+ }
370
+ }
371
+ async function downloadOne(relativePath, body, overwrite) {
372
+ const url = URL.createObjectURL(body);
373
+ try {
374
+ let id;
375
+ try {
376
+ id = await chrome.downloads.download({
377
+ url,
378
+ filename: relativePath,
379
+ saveAs: false,
380
+ conflictAction: overwrite ? 'overwrite' : 'uniquify',
381
+ });
382
+ }
383
+ catch (err) {
384
+ // Chrome's own wording ("Invalid filename") names neither the
385
+ // file nor the step.
386
+ throw new Error(`Saving ${relativePath} was refused by Chrome: ${describeChromeError(err)}`);
387
+ }
388
+ const deadline = Date.now() + DOWNLOAD_TIMEOUT_MS;
389
+ let accepted = false;
390
+ while (Date.now() < deadline) {
391
+ const [item] = await chrome.downloads.search({ id });
392
+ if (item) {
393
+ if (item.state === 'complete')
394
+ return { id, path: item.filename };
395
+ if (item.state === 'interrupted') {
396
+ if (item.error === 'USER_CANCELED')
397
+ throw new Error(COPY_CANCELLED);
398
+ throw new Error(`Saving ${relativePath} failed: ${item.error ?? 'download interrupted'}`);
399
+ }
400
+ // A file type Chrome wants confirmed before it lands. Not
401
+ // expected for `.user.js` (verified), but the prompt is the
402
+ // right answer if a Chrome policy ever flags it — once.
403
+ if (item.danger !== 'safe' && item.danger !== 'accepted' && !accepted) {
404
+ accepted = true;
405
+ await chrome.downloads.acceptDanger(id);
406
+ }
407
+ }
408
+ await new Promise((r) => setTimeout(r, 100));
409
+ }
410
+ throw new Error(`Saving ${relativePath} timed out`);
411
+ }
412
+ finally {
413
+ // The download has read the blob by the time `search` reports it
414
+ // complete or interrupted, and a URL that outlives its page dies
415
+ // with it either way.
416
+ URL.revokeObjectURL(url);
417
+ }
418
+ }
419
+ export const COPY_NOT_FOUND_AFTER_REFRESH = 'Internal error: the copy was written but the collection did not find it';
420
+ // Write the copy (and any dependencies), then make sure the Downloads
421
+ // collection exists and has scanned it in. Resolves with the new row.
422
+ //
423
+ // The first copy ever is the one that learns where Downloads is, so it
424
+ // has to write before it can create the collection; every later copy
425
+ // finds the setting in place. One order serves both.
426
+ export async function makeLocalCopy(request) {
427
+ const { script, plan } = request;
428
+ const files = planFiles(script, plan).map((f) => ({
429
+ ...f,
430
+ downloadPath: `${DOWNLOADS_COPIES_SUBDIR}/${f.relativePath}`,
431
+ }));
432
+ // Only for telling a rename apart from a redirect below; the
433
+ // authoritative answer is the path the write itself reports.
434
+ const priorDir = (await readDownloadsCopiesSetting())?.downloadsDir ?? null;
435
+ const written = [];
436
+ let downloadsDir = '';
437
+ try {
438
+ for (const f of files) {
439
+ const one = await downloadOneChecked(f, request.overwrite);
440
+ written.push(one);
441
+ // Every file has to have landed where it was asked to: the script
442
+ // because that is what gets registered, the dependencies because
443
+ // a renamed one is a file the override lookup will never find.
444
+ // The first file is also what teaches us where Downloads is.
445
+ const dir = downloadsDirFromSaved(one.path, f.downloadPath);
446
+ if (!dir || (downloadsDir && dir !== downloadsDir)) {
447
+ throw new Error(savedElsewhere(one.path, f.downloadPath, downloadsDir ? downloadsCopiesDir(downloadsDir)
448
+ : priorDir && downloadsCopiesDir(priorDir)));
449
+ }
450
+ downloadsDir = dir;
451
+ }
452
+ }
453
+ catch (err) {
454
+ // Nothing is registered, so anything already on disk is a file the
455
+ // user never asked to keep — and a `.user.js` among them would be
456
+ // scanned in as a script on the next refresh.
457
+ await discardWritten(written);
458
+ throw err;
459
+ }
460
+ // The files are on disk; the worker, which alone writes storage,
461
+ // makes them a script (docs/worker-writes.md).
462
+ const registered = await callWorker('registerLocalCopy', downloadsDir, plan.fileName);
463
+ return { ...registered, path: written[0].path };
464
+ }
465
+ // Worker side of a local copy, once its files are on disk in the
466
+ // copies directory under `downloadsDir`: the collection that holds
467
+ // them, refreshed so it lists the copy, and the copy turned on.
468
+ export async function registerLocalCopy(downloadsDir, fileName) {
469
+ const collection = await ensureDownloadsCollection(downloadsDir);
470
+ const refreshed = await refreshCollection(collection.id);
471
+ const row = refreshed?.scripts.find((s) => s.relativePath === fileName);
472
+ if (!row)
473
+ throw new Error(COPY_NOT_FOUND_AFTER_REFRESH);
474
+ // A new copy is an install, so its Log starts with one. A row that was
475
+ // already listed (a replaced copy, or one a new collection's own add
476
+ // scanned in and logged) has its Log already.
477
+ if (!collection.scriptIds.includes(row.id))
478
+ await emitInitialLoadLog(row, true);
479
+ // Without its bodies: the page only needs which row it is.
480
+ return { script: withoutBodies(await enableCopy(row)), collection: refreshed?.collection ?? collection };
481
+ }
482
+ // One file, written and reported. Split out only so `makeLocalCopy`'s
483
+ // loop reads as the checks it performs.
484
+ async function downloadOneChecked(file, overwrite) {
485
+ return await downloadOne(file.downloadPath, file.body, overwrite);
486
+ }
487
+ // A copy is meant to be the version that runs, and the caller's
488
+ // exclusive-enable sweep only acts on a row that is already running.
489
+ // A REPLACED file inherits the row it replaced — including a prior
490
+ // `enabled: false`, which `rebuildCollection` deliberately preserves
491
+ // across a refresh — so without this, replacing a copy the user had
492
+ // switched off would land them on a page with nothing running and no
493
+ // explanation. Failing to write it is not worth blocking the copy over:
494
+ // the row exists either way, and the page shows its real enable state.
495
+ async function enableCopy(row) {
496
+ if (row.enabled)
497
+ return row;
498
+ try {
499
+ await setScriptsEnabled([{ id: row.id, enabled: true }]);
500
+ return { ...row, enabled: true };
501
+ }
502
+ catch (err) {
503
+ dlog(`[SourceMonkey] could not enable the new copy: ${describeError(err)}`);
504
+ return row;
505
+ }
506
+ }
507
+ function describeChromeError(err) {
508
+ return err instanceof Error ? err.message : String(err);
509
+ }
510
+ // A file that didn't land where it was asked to. Two different things
511
+ // happened, and the fix differs, so they are two messages rather than
512
+ // one that guesses — see docs/diagnostics.md#wording.
513
+ //
514
+ // `copiesDir` is what we can compare against: null on the very first
515
+ // copy, where the Downloads folder is still unknown and "a different
516
+ // folder" is the only thing that can be said.
517
+ function savedElsewhere(path, asked, copiesDir) {
518
+ const dir = path.slice(0, path.lastIndexOf('/'));
519
+ if (copiesDir && dir === copiesDir) {
520
+ // Chrome renamed around a file that appeared between the dialog's
521
+ // check and the write. The copy is on disk under a name nobody
522
+ // asked for, so it is not registered.
523
+ return `Chrome saved the copy as ${baseName(path)} instead of ${baseName(asked)}, `
524
+ + 'so it was not added as a script. Delete that file, or copy again to replace it.';
525
+ }
526
+ return `The copy was saved to ${path}, outside the collection directory, `
527
+ + 'so it was not added as a script';
528
+ }
529
+ // The Downloads collection for `downloadsDir`: the one the setting
530
+ // names, else one already at that directory (adopted), else a new one.
531
+ // Records what it found so the next copy needs no lookup by path.
532
+ export async function ensureDownloadsCollection(downloadsDir) {
533
+ const prior = await readDownloadsCopiesSetting();
534
+ const setting = { downloadsDir, collectionId: prior?.collectionId ?? null };
535
+ const collections = await getCollections();
536
+ let collection = findDownloadsCollection(setting, collections);
537
+ if (!collection) {
538
+ const created = await createCollection({
539
+ kind: 'location',
540
+ path: downloadsCopiesDir(downloadsDir),
541
+ // The seeded name, unless something already wears it — the
542
+ // collection at the previous Downloads location, say. Then the
543
+ // path-derived name will do; nothing looks this collection up by
544
+ // its name.
545
+ name: collectionNameInUse(DOWNLOADS_COPIES_NAME, collections) ? undefined : DOWNLOADS_COPIES_NAME,
546
+ });
547
+ // `createCollection` answers null for a path that is already a
548
+ // collection — which `findDownloadsCollection` just said there
549
+ // wasn't, so another tab beat us to it. Take theirs.
550
+ collection = created?.kind === 'newCollection'
551
+ ? created.collection
552
+ : findDownloadsCollection(setting, await getCollections());
553
+ if (!collection)
554
+ throw new Error('Internal error: could not create the Downloads collection');
555
+ }
556
+ if (prior?.downloadsDir !== downloadsDir || prior.collectionId !== collection.id) {
557
+ await writeDownloadsCopiesSetting({ downloadsDir, collectionId: collection.id });
558
+ }
559
+ return collection;
560
+ }