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,276 @@
1
+ // Deleting a local copy — the script file itself, and optionally the
2
+ // dependency files that were copied beside it — off the disk.
3
+ //
4
+ // The counterpart of `local-copy.ts`, and it can only undo what that
5
+ // module did: `chrome.downloads.removeFile` deletes a file the browser
6
+ // itself downloaded, found by its absolute path in Chrome's download
7
+ // history. Nothing else an extension can call deletes a file, so a copy
8
+ // whose history entry is gone can only be reported, not removed.
9
+ //
10
+ // See docs/local-delete.md.
11
+ import { isFileUrl, locationFsPath } from '../file-locations.js';
12
+ import { describeError, } from './script-store.js';
13
+ import { dlog } from '../debug-log.js';
14
+ import { downloadsCopiesDir, readDownloadsCopiesSetting, } from './local-copy.js';
15
+ // The copies setting, or null when it can't be read — a surface that
16
+ // asks "is this row a local copy?" while painting must not be taken
17
+ // down by a storage failure, and null is the safe answer (no row
18
+ // offers a file deletion). One helper so the pages that ask can't
19
+ // drift on what a failure means.
20
+ export async function readCopiesSetting() {
21
+ try {
22
+ return await readDownloadsCopiesSetting();
23
+ }
24
+ catch (err) {
25
+ dlog(`[SourceMonkey] could not read the local-copies setting: ${describeError(err)}`);
26
+ return null;
27
+ }
28
+ }
29
+ // ---------- Which scripts can be deleted from disk ----------
30
+ // The absolute path of `script`'s file when it is a copy we wrote, and
31
+ // null when it isn't. `collection` is the row's own collection.
32
+ //
33
+ // Both halves are required, and the collection is the important one:
34
+ // the row has to sit in the directory collection AT the copies
35
+ // directory. That is what makes the rest of this module's reasoning
36
+ // true — a file in that directory is shared only within that
37
+ // collection, so its scripts are the whole set that could be reading
38
+ // it, and `copyDeletePlan` can answer "does anything else need this?"
39
+ // from one collection's rows.
40
+ //
41
+ // A row somewhere else whose file happens to be in that directory — an
42
+ // individual script the user added by hand at that path — is refused.
43
+ // Deleting through it would compute the shared/own split against the
44
+ // wrong set of scripts and could take a library another copy is
45
+ // loading. Individual-file rows, overlapping directories and manifests
46
+ // all break the assumption in the same way; none of them is worth the
47
+ // mess (see TODO.md).
48
+ //
49
+ export function deletableCopyPath(script, collection, setting) {
50
+ if (!setting || !collection)
51
+ return null;
52
+ const dir = downloadsCopiesDir(setting.downloadsDir);
53
+ if (!isCopiesDir(collection.path, dir))
54
+ return null;
55
+ const path = locationFsPath(script.fileUrl);
56
+ return path.startsWith(`${dir}/`) ? path : null;
57
+ }
58
+ // Is this collection the one at the copies directory? Compared as
59
+ // paths, so the collection is recognized however it was created — the
60
+ // one our first copy made, or one the user had already added there and
61
+ // we adopted (see local-copy.md).
62
+ function isCopiesDir(collectionPath, copiesDir) {
63
+ return locationFsPath(collectionPath).replace(/\/+$/, '') === copiesDir;
64
+ }
65
+ // What deleting `script` would remove, given every script in its
66
+ // collection (`siblings`, which may include `script` itself).
67
+ //
68
+ // Only siblings are consulted because only they can be reading these
69
+ // files: a local stand-in is searched for under the script's own folder
70
+ // and its collection root (see local-file-overrides.md), so a file
71
+ // under the copies directory is invisible to a script outside it.
72
+ export function copyDeletePlan(script, siblings, copiesDir) {
73
+ // Which files another script in the collection is loading. Only
74
+ // whether, not which scripts: the dialog doesn't name them — a
75
+ // userscript name is long and there can be several.
76
+ const others = new Set();
77
+ for (const other of siblings) {
78
+ if (other.id === script.id)
79
+ continue;
80
+ for (const path of localDependencyPaths(other, copiesDir).keys())
81
+ others.add(path);
82
+ }
83
+ const own = [];
84
+ const shared = [];
85
+ for (const [path, url] of localDependencyPaths(script, copiesDir)) {
86
+ const dep = {
87
+ path,
88
+ relativePath: path.slice(copiesDir.length + 1),
89
+ url,
90
+ };
91
+ (others.has(path) ? shared : own).push(dep);
92
+ }
93
+ const path = locationFsPath(script.fileUrl);
94
+ return {
95
+ path,
96
+ relativePath: path.startsWith(`${copiesDir}/`) ? path.slice(copiesDir.length + 1) : path,
97
+ own,
98
+ shared,
99
+ };
100
+ }
101
+ // The files under `copiesDir` a script is currently loading its
102
+ // `@require` / `@resource` entries from, as path → declared URL.
103
+ //
104
+ // What the load pipeline actually read is the authority, so this lists
105
+ // files in use rather than paths a copy might once have written: the
106
+ // `localUrl` of a stand-in for a web URL, or the `file://` URL itself
107
+ // of a relative ref. Deduped by path — one file can serve two
108
+ // declarations.
109
+ function localDependencyPaths(script, copiesDir) {
110
+ const out = new Map();
111
+ for (const file of [...(script.requires ?? []), ...(script.resources ?? [])]) {
112
+ const readFrom = file.localUrl ?? (isFileUrl(file.url) ? file.url : null);
113
+ if (!readFrom)
114
+ continue;
115
+ const path = locationFsPath(readFrom);
116
+ if (!path.startsWith(`${copiesDir}/`) || out.has(path))
117
+ continue;
118
+ out.set(path, file.url);
119
+ }
120
+ return out;
121
+ }
122
+ // ---------- Which files the browser can still be asked about ----------
123
+ // Every completed download whose file is this path AND that Chrome
124
+ // still believes is on disk. `filename` is an exact match on the
125
+ // absolute path, and the record outlives the session — it is cleared
126
+ // with the user's browsing data, not on restart.
127
+ //
128
+ // `exists` is the important half. Chrome refuses `removeFile` with
129
+ // *Download file already deleted* for a record it has marked gone, and
130
+ // that mark is sticky: it is set by Chrome's own existence checks, so a
131
+ // copy edited by a tool that saves via write-to-temp-and-rename can
132
+ // carry it while the file is sitting right there. Filtering here means
133
+ // the dialog's lists and the deletion agree — a file Chrome won't act
134
+ // on is shown as one we can't delete, before anything is pressed,
135
+ // rather than failing half way through.
136
+ function completedDownloads(path) {
137
+ return chrome.downloads.search({ filename: path, state: 'complete', exists: true });
138
+ }
139
+ // The subset of `paths` Chrome still holds a download record for — the
140
+ // only ones `removeFile` can act on. Asked up front so the dialog can
141
+ // say which files are going and which are being left, rather than
142
+ // finding out one failure at a time.
143
+ //
144
+ // A search that throws counts as "no record": the answer is only used
145
+ // to decide what to attempt, and attempting a file we can't ask about
146
+ // would fail anyway.
147
+ export async function trackedFiles(paths) {
148
+ const tracked = new Set();
149
+ await Promise.all(paths.map(async (path) => {
150
+ try {
151
+ if ((await completedDownloads(path)).length > 0)
152
+ tracked.add(path);
153
+ }
154
+ catch (err) {
155
+ dlog(`[SourceMonkey] could not look up the download record for ${path}: ${describeError(err)}`);
156
+ }
157
+ }));
158
+ return tracked;
159
+ }
160
+ // ---------- Deleting ----------
161
+ // Chrome holds no usable download record for the file, so `removeFile`
162
+ // has nothing to act on — the user cleared their downloads, or Chrome
163
+ // has marked the file gone (see `completedDownloads`). Two sentences,
164
+ // both punctuated: this lands in the dialog's error slot and in the
165
+ // viewer's message line, never spliced after a prefix
166
+ // (docs/diagnostics.md#wording).
167
+ //
168
+ // `subject` names what could not be deleted. The script's own file is
169
+ // `SCRIPT_FILE`, with no path in it: the dialog is already showing the
170
+ // file and its full path two rows up, and a relative path with a
171
+ // subdirectory in it makes the sentence hard to read. A dependency
172
+ // file, reported away from any list of its own, carries its path.
173
+ //
174
+ // `alsoOthers` when the same block would have taken other files with
175
+ // it — the copy's own dependencies. They are all still on disk, so the
176
+ // second line names them all rather than just the one it reports on.
177
+ //
178
+ // The second line says what to do next, not just that we can't. With
179
+ // `watched`, the dialog is showing this for the script file with its
180
+ // Delete disabled, and the row has no other trash can (a directory
181
+ // collection's rows are removed by refreshing, which scans the file
182
+ // straight back in) — so without it the user is looking at a dead
183
+ // end. The dialog watches for the file going and then offers to
184
+ // remove the entry (`watchDisk` in the dialog), which is what the
185
+ // line promises. Without `watched` — a dependency reported after the
186
+ // script has already gone, or a dialog that can't watch because file
187
+ // access is off — the line promises nothing.
188
+ export const SCRIPT_FILE = 'Script file';
189
+ export function noDownloadRecord(subject, alsoOthers = false, watched = false) {
190
+ const files = alsoOthers ? 'files' : 'file';
191
+ return `${subject} is no longer in Chrome download history, so SourceMonkey cannot delete it.\n`
192
+ + (watched
193
+ ? `Delete the ${files} by hand, and Delete will then remove this script.`
194
+ : `You can delete the ${files} by hand.`);
195
+ }
196
+ // Ends with a period, unlike most messages: it is joined with the
197
+ // other per-file reports into one line (the viewer's not-found detail,
198
+ // the collections page's error line), where an unpunctuated sentence
199
+ // runs into the next one. Nothing splices it after a prefix.
200
+ function deleteFailed(name, detail) {
201
+ return `Deleting ${name} failed: ${detail}.`;
202
+ }
203
+ // Delete one file the browser downloaded, by its absolute path.
204
+ // Resolves with null on success, or the message to show.
205
+ //
206
+ // `removeFile` deletes the bytes; `erase` then drops the history row,
207
+ // which would otherwise sit in the user's downloads list pointing at a
208
+ // file that is gone. Erasing is best effort — the file is already gone
209
+ // by then, which is what was asked for.
210
+ //
211
+ // **Nothing is erased unless the file went.** The record is the only
212
+ // handle there is on that file: erasing one we failed to delete with
213
+ // would leave the file on disk and take away the one way of ever
214
+ // removing it.
215
+ async function deleteDownloadedFile(path, name, kind = 'File', alsoOthers = false) {
216
+ let items;
217
+ try {
218
+ items = await completedDownloads(path);
219
+ }
220
+ catch (err) {
221
+ return deleteFailed(name, describeError(err));
222
+ }
223
+ if (items.length === 0) {
224
+ return noDownloadRecord(kind === 'Script' ? SCRIPT_FILE : `File ${name}`, alsoOthers);
225
+ }
226
+ // Newest first: one path can have been written several times over
227
+ // (every Replace adds a record), and the latest is the one whose
228
+ // file is on disk now. Older records for the same path are erased
229
+ // too, so the downloads list doesn't keep rows for a file that has
230
+ // gone.
231
+ const ordered = [...items].sort((a, b) => (b.startTime ?? '').localeCompare(a.startTime ?? ''));
232
+ let lastError = '';
233
+ for (const item of ordered) {
234
+ try {
235
+ await chrome.downloads.removeFile(item.id);
236
+ }
237
+ catch (err) {
238
+ // Chrome refuses a record it believes is already gone. Another
239
+ // record for the same path may still hold the live one, so this
240
+ // is only fatal once every record has been tried — and the
241
+ // record we couldn't delete with is left alone.
242
+ lastError = describeError(err);
243
+ continue;
244
+ }
245
+ // The file is gone, so every record for that path now points at
246
+ // nothing. Erased only from here.
247
+ for (const stale of ordered) {
248
+ try {
249
+ await chrome.downloads.erase({ id: stale.id });
250
+ }
251
+ catch (err) {
252
+ dlog(`[SourceMonkey] could not erase the download record for ${path}: ${describeError(err)}`);
253
+ }
254
+ }
255
+ return null;
256
+ }
257
+ return deleteFailed(name, lastError || 'The file could not be removed');
258
+ }
259
+ export async function deleteCopyFiles(request) {
260
+ if (request.script) {
261
+ const scriptError = await deleteDownloadedFile(request.script.path, request.script.relativePath, 'Script',
262
+ // The dependencies were going with it and are staying too, so the
263
+ // message says so — the same answer the dialog gives when it knows
264
+ // up front that the script's record has gone.
265
+ request.dependencies.length > 0);
266
+ if (scriptError)
267
+ return { scriptError, dependencyErrors: [] };
268
+ }
269
+ const dependencyErrors = [];
270
+ for (const dep of request.dependencies) {
271
+ const err = await deleteDownloadedFile(dep.path, dep.relativePath);
272
+ if (err)
273
+ dependencyErrors.push(err);
274
+ }
275
+ return { scriptError: null, dependencyErrors };
276
+ }
@@ -0,0 +1,108 @@
1
+ // Pure naming helpers for script and collection locations, split from
2
+ // directories.ts so pages that only label locations (the toolbar popup,
3
+ // via collection-display.ts) don't load the scanning and fetch code.
4
+ // directories.ts re-exports all of it.
5
+ import { isHttpUrl } from '../file-locations.js';
6
+ // Classify a script location by scheme: which built-in default
7
+ // container it would install into, and which scheme chip its row wears
8
+ // inside a container. github URLs here mean both the human-facing
9
+ // `github.com/...` form and the already-normalized
10
+ // `raw.githubusercontent.com/...` form, since `normalizeCollectionInput`
11
+ // may have rewritten the input before this runs.
12
+ export function containerKindForSingleFileInput(input) {
13
+ if (/^https?:\/\/(www\.)?github\.com\//i.test(input))
14
+ return 'github';
15
+ if (/^https?:\/\/raw\.githubusercontent\.com\//i.test(input))
16
+ return 'github';
17
+ if (isHttpUrl(input))
18
+ return 'http';
19
+ return 'file';
20
+ }
21
+ // ---------- Scratch scripts ----------
22
+ //
23
+ // A script whose only copy is its text in extension storage, with no
24
+ // file or URL behind it. Only sm-dev, the development tool, writes
25
+ // them, over the worker's test surface (docs/sm-dev-design.md).
26
+ //
27
+ // - Each sm-dev run has its own collection of them, its test
28
+ // collection, at `container:sm-dev:<run>`, where `<run>` is the run's
29
+ // unique id.
30
+ // - Each "location" is a `scratch:///<run>/<path>.user.js` URL that
31
+ // names the storage row holding the text, so the ordinary refresh
32
+ // pipeline reads it back like any other body and a rewrite of the
33
+ // same name is picked up as a content change.
34
+ // - The run in the URL is what lets the script run: only in tabs that
35
+ // run's claim holds (sm-dev-link.ts).
36
+ const SM_DEV_CONTAINER_PREFIX = 'container:sm-dev:';
37
+ // Three slashes: with an empty host the run and name are the URL's
38
+ // path, so the basename every label is cut from is the script's own
39
+ // name rather than the whole URL.
40
+ const SCRATCH_SCHEME = 'scratch:///';
41
+ export const SCRATCH_TEXT_KEY_PREFIX = 'scratch:';
42
+ export const SCRATCH_FILES_KEY_PREFIX = 'scratchFiles:';
43
+ export const SCRATCH_SOURCE_KEY_PREFIX = 'scratchSource:';
44
+ // Kept to a URL path segment's safe characters.
45
+ function safeSegment(text) {
46
+ return text.replace(/[^A-Za-z0-9._-]+/g, '-');
47
+ }
48
+ export function isScratchUrl(input) {
49
+ return input.startsWith(SCRATCH_SCHEME);
50
+ }
51
+ // The location for run `run`'s scratch script called `name`: a name,
52
+ // or the path sm-dev was given, relative or absolute, kept whole so
53
+ // two files of the same name stay apart. Ends in `.user.js` so every
54
+ // "is this a script" check reads it as one, and so its basename labels
55
+ // it in the viewer and the logs.
56
+ //
57
+ // Its segments are percent-encoded, as a file:// URL's would be, so the
58
+ // name reads back as written wherever it is shown decoded (the viewer,
59
+ // stack lines): `My%20Script.user.js` stays that, not `My-20Script`.
60
+ export function scratchUrl(run, name) {
61
+ // A lone surrogate has no encoding (encodeURIComponent throws on it),
62
+ // so it becomes U+FFFD, as a decoder would show it.
63
+ const whole = name.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, '\uFFFD');
64
+ const segments = whole.split('/').map((segment) => encodeURIComponent(segment));
65
+ const base = (segments.pop() ?? '').replace(/\.user\.js$/i, '') || 'script';
66
+ return `${SCRATCH_SCHEME}${safeSegment(run)}/${[...segments, `${base}.user.js`].join('/')}`;
67
+ }
68
+ // The run a scratch script belongs to. Null for a script that isn't one.
69
+ export function scratchRunOf(url) {
70
+ if (!isScratchUrl(url))
71
+ return null;
72
+ const rest = url.slice(SCRATCH_SCHEME.length);
73
+ const slash = rest.indexOf('/');
74
+ return slash < 0 ? '' : rest.slice(0, slash);
75
+ }
76
+ export function scratchTextKey(url) {
77
+ return SCRATCH_TEXT_KEY_PREFIX + url;
78
+ }
79
+ // The storage row holding the `@require` / `@resource` bodies sm-dev
80
+ // pushed with a scratch script: the ones it read from disk, which the
81
+ // extension couldn't (see `PushedFile` in external-files.ts).
82
+ export function scratchFilesKey(url) {
83
+ return SCRATCH_FILES_KEY_PREFIX + url;
84
+ }
85
+ // The storage row naming the file on the developer's disk a scratch
86
+ // script came from (`ScratchSource`), for sm-dev's `script list`.
87
+ export function scratchSourceKey(url) {
88
+ return SCRATCH_SOURCE_KEY_PREFIX + url;
89
+ }
90
+ // The path of run `run`'s test collection.
91
+ export function smDevContainerPath(run) {
92
+ return SM_DEV_CONTAINER_PREFIX + safeSegment(run);
93
+ }
94
+ // An sm-dev test collection, which only its run changes: no script is
95
+ // moved into or out of one, or installed into one, by hand.
96
+ export function isSmDevCollection(path) {
97
+ return smDevRunOfContainer(path) !== null;
98
+ }
99
+ // The run a test collection belongs to, or null for any other
100
+ // collection.
101
+ export function smDevRunOfContainer(path) {
102
+ return path.startsWith(SM_DEV_CONTAINER_PREFIX) ? path.slice(SM_DEV_CONTAINER_PREFIX.length) : null;
103
+ }
104
+ // The scheme chip's kind for a container script: where it came from.
105
+ // A scratch script has no location; its only copy is in storage.
106
+ export function scriptSchemeKind(location) {
107
+ return isScratchUrl(location) ? 'scratch' : containerKindForSingleFileInput(location);
108
+ }
@@ -0,0 +1,158 @@
1
+ // Formatting for the per-script event log.
2
+ //
3
+ // Two consumers, one set of strings:
4
+ // - the viewer's Log pane table (Time / Status / Source / Message)
5
+ // - the CSV the pane's Save and Copy buttons hand out
6
+ //
7
+ // Keeping both here is what makes an exported row read the same as the
8
+ // row on screen: the timestamp spelling and the `tab 17 ·
9
+ // script-wrapper` source label are written once.
10
+ //
11
+ // The Message column is the exception, and deliberately so. A message
12
+ // carrying a stack trace only becomes readable after its refs are
13
+ // mapped back through the injected entry's source map — which needs
14
+ // the viewer's tabs and the script's current layout, neither of which
15
+ // belongs in a pure module. So `formatLogCsv` takes the renderer as an
16
+ // argument: the viewer passes the one behind its own table, and the
17
+ // default here is the honest fallback for a caller that has no source
18
+ // map to consult.
19
+ //
20
+ // Pure (no DOM, no chrome.*) so unit tests can drive it directly.
21
+ import { describeDiagnostic } from '../diagnostic-location.js';
22
+ import { formatScriptStatusLabel, logIdSuffix } from '../script-display.js';
23
+ // Local wall-clock time, sortable spelling: `2026-08-05 14:03:22.017`.
24
+ // Not ISO/UTC — the reader is looking at their own machine's log and
25
+ // correlating it with what they just saw happen in a tab.
26
+ export function formatLogTimestamp(ms) {
27
+ const d = new Date(ms);
28
+ const yyyy = String(d.getFullYear());
29
+ const mo = String(d.getMonth() + 1).padStart(2, '0');
30
+ const dd = String(d.getDate()).padStart(2, '0');
31
+ const hh = String(d.getHours()).padStart(2, '0');
32
+ const mm = String(d.getMinutes()).padStart(2, '0');
33
+ const ss = String(d.getSeconds()).padStart(2, '0');
34
+ const mmm = String(d.getMilliseconds()).padStart(3, '0');
35
+ return `${yyyy}-${mo}-${dd} ${hh}:${mm}:${ss}.${mmm}`;
36
+ }
37
+ // The Source cell: `tab 17 · script-wrapper` for anything that came
38
+ // from injected code in a specific tab, the bare label otherwise.
39
+ export function formatLogSource(entry) {
40
+ return entry.tabId !== undefined ? `tab ${entry.tabId} · ${entry.source}` : entry.source;
41
+ }
42
+ // The Status cell: the state the entry moved the script into, in the
43
+ // Status badge's own words (`REFRESH FAILED`, `INVALID`, `OK`), or the
44
+ // bare level (`info` / `warning` / `error`) for an entry about
45
+ // something the script itself did. See `LogEntry.state`.
46
+ export function formatLogStatus(entry) {
47
+ return entry.state !== undefined ? formatScriptStatusLabel(entry.state) : entry.level;
48
+ }
49
+ // The CSS class the pane colours the Status cell by: the display state
50
+ // when the entry carries one, the level otherwise (`userscript.html`
51
+ // defines both families).
52
+ export function logStatusClass(entry) {
53
+ return entry.state !== undefined ? `log-state-${entry.state}` : `log-level-${entry.level}`;
54
+ }
55
+ // The Message cell as plain text, with a derived location flattened
56
+ // into the same sentence the pane renders as a link.
57
+ //
58
+ // Correct for every entry SourceMonkey wrote about a script, and wrong
59
+ // only for the stack refs inside an entry the script itself produced —
60
+ // see the note at the top of the file, and `logMessageText` in
61
+ // `userscript.ts` for the renderer that handles those.
62
+ export function formatLogMessage(entry) {
63
+ return describeDiagnostic(entry.message, entry.location);
64
+ }
65
+ // Column headings, in order — the pane's `<th>` row and the CSV's
66
+ // header row are the same four.
67
+ export const LOG_COLUMNS = ['Time', 'Status', 'Source', 'Message'];
68
+ // ---------- CSV ----------
69
+ // Quote when the value would otherwise be ambiguous or awkward to read
70
+ // back: the separator, the quote character itself, and any whitespace —
71
+ // the last of which covers leading/trailing spaces a parser would
72
+ // otherwise be free to trim.
73
+ const NEEDS_QUOTING = /[",\s]/;
74
+ export function csvField(value) {
75
+ if (!NEEDS_QUOTING.test(value))
76
+ return value;
77
+ return `"${value.replace(/"/g, '""')}"`;
78
+ }
79
+ // Fold a value onto one physical line: `\n`, `\r` as their two-
80
+ // character escapes.
81
+ //
82
+ // CSV would allow the newline inside quotes, and a spreadsheet would
83
+ // read it back — but this file is called `.log`, and one record per
84
+ // line is what makes it work with the tools a log is actually reached
85
+ // for. A stack trace spanning eight physical lines means `grep` finds
86
+ // a fragment with no timestamp on it, `wc -l` counts frames instead of
87
+ // events, and `tail -n` cuts an entry in half.
88
+ //
89
+ // Backslash is escaped first, and only for that reason: log messages
90
+ // carry Windows paths and regex sources, so without it a literal `\n`
91
+ // in the text and a real newline would decode to the same thing, and
92
+ // the escape wouldn't be reversible.
93
+ export function foldToOneLine(value) {
94
+ return value
95
+ .replace(/\\/g, '\\\\')
96
+ .replace(/\n/g, '\\n')
97
+ .replace(/\r/g, '\\r');
98
+ }
99
+ // One record, one line — by construction, not by the caller
100
+ // remembering to flatten first.
101
+ export function csvRow(fields) {
102
+ return fields.map((f) => csvField(foldToOneLine(f))).join(',');
103
+ }
104
+ // The whole log as CSV text, header row first, one entry per line.
105
+ //
106
+ // Rows are newest-first, matching the pane's table rather than the
107
+ // append order in storage: this is an export of what the reader is
108
+ // looking at, and a log whose top line is the event they just saw is
109
+ // the point of the ordering in both places.
110
+ //
111
+ // `renderMessage` is how the caller supplies its own Message column;
112
+ // see the note at the top of the file.
113
+ export function formatLogCsv(entries, renderMessage = formatLogMessage) {
114
+ const lines = [csvRow(LOG_COLUMNS)];
115
+ for (let i = entries.length - 1; i >= 0; i--) {
116
+ const e = entries[i];
117
+ lines.push(csvRow([
118
+ formatLogTimestamp(e.timestamp),
119
+ formatLogStatus(e),
120
+ formatLogSource(e),
121
+ renderMessage(e),
122
+ ]));
123
+ }
124
+ return `${lines.join('\n')}\n`;
125
+ }
126
+ // Suggested filename for a saved log: the script's log ID with its
127
+ // `.user.js` / `.js` extension swapped for `.log`, keeping its `#E4`
128
+ // (or `#N`) suffix so two scripts sharing a basename don't suggest the
129
+ // same file. `bold-on-hover.user.js#E4` → `bold-on-hover#E4.log`.
130
+ //
131
+ // `.log` rather than `.csv` even though the contents are CSV: this is
132
+ // a log first, and a double-click should open a text editor rather
133
+ // than a spreadsheet.
134
+ export function logFileName(logId) {
135
+ // Suffix first, extension second — `logIdSuffix` explains
136
+ // why the `#N` match is anchored, and `script-source-url.ts` explains
137
+ // why the order matters (`Chapter #2.user.js`).
138
+ const trimmed = sanitizeFilename(logId).trim();
139
+ const dupe = logIdSuffix(trimmed);
140
+ // One alternation rather than two `.replace`s: chaining them strips
141
+ // twice for a name like `bar.js.user.js`, eating a `.js` that is part
142
+ // of the name.
143
+ const base = (dupe ? trimmed.slice(0, -dupe.length) : trimmed)
144
+ .replace(/\.(user\.)?js$/i, '');
145
+ // Fallback for a script we have no label for at all — the viewer
146
+ // asks for a filename whether or not the log ID map has landed.
147
+ return `${base || 'script'}${dupe}.log`;
148
+ }
149
+ // A log ID is a decoded on-disk filename, which is attacker-controlled
150
+ // for an untrusted script bundle. Path separators are already gone
151
+ // (`scriptBasename` takes the leaf twice), so what is left is control
152
+ // characters, which have no business in a filename the browser is
153
+ // about to write. Same substitution as `sanitizeLabel` in
154
+ // `script-source-url.ts`, for the same reason.
155
+ function sanitizeFilename(name) {
156
+ // eslint-disable-next-line no-control-regex
157
+ return name.replace(/[\u0000-\u001f\u007f\u2028\u2029]/g, '_');
158
+ }