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,346 @@
1
+ // Noticing, at paint time, that a local file a page is naming has
2
+ // changed on disk — deleted, or edited — before any refresh has run
3
+ // and updated the stored row. And, for a directory collection, that
4
+ // files have appeared which a refresh would pick up.
5
+ //
6
+ // Every surface that shows a `file://` path paints from storage, and
7
+ // storage only learns about the disk when the next refresh lists a
8
+ // directory. Until then the viewer's Location row, its Local overrides
9
+ // row, the versions table and the collections page all describe the
10
+ // file as it was last read. This module lets them say otherwise: a
11
+ // row tags the element carrying the path (`tagLocalFile`: the path,
12
+ // the size and mtime the row was read at, and the tooltips to use),
13
+ // and `markFileChanges` probes the disk and adds a chip beside each
14
+ // one that has gone or changed.
15
+ //
16
+ // See docs/file-changes.md.
17
+ import { baseName, collectionCanonicalUrl, isFileUrl, isLocalDirectoryCollection, isManifestCollection, listDirectory, locationFsPath, parentDir, } from '../file-locations.js';
18
+ import { dlog } from '../debug-log.js';
19
+ import { pendingSummary } from '../script-display.js';
20
+ // Probe `files` against their directory listings — one listing per
21
+ // distinct parent directory, rather than a read per file: a table can
22
+ // name many files in one folder, and a listing is one small read
23
+ // however many rows it settles.
24
+ //
25
+ // Conservative by construction. A file is reported missing only when
26
+ // its directory listed successfully and the name is not in it, and
27
+ // modified only when the listing supplies a size and mtime that differ
28
+ // from the stored ones. A listing that fails — the directory itself is
29
+ // gone, file access is off, a transient read error — settles nothing:
30
+ // its files come back `unknown`. That is the right way round for every
31
+ // caller: a chip that says "deleted" about a file that is there would
32
+ // be worse than no chip, and the delete dialog acts on the answer.
33
+ //
34
+ // `maxAgeMs` lets a caller that re-probes on every repaint reuse a
35
+ // listing read moments ago: the collections page repaints on every
36
+ // storage write, an enable toggle included, and nothing about the disk
37
+ // changed for that. A listing older than `maxAgeMs` is read again.
38
+ //
39
+ // A remembered listing is only ever trusted for the answer "present":
40
+ // a listing that agrees with the row is right whatever its age, but
41
+ // one that disagrees may simply predate the refresh that just updated
42
+ // the row (the refresh lists through its own cache, not this memo), so
43
+ // a folder with any other answer is listed again before it is given.
44
+ // The common case — nothing changed — pays no extra read; a change
45
+ // costs one, and the fresh listing replaces the memo.
46
+ //
47
+ // Answers one state per input, in input order — not per path: the same
48
+ // file can be held by several rows (one collection per copy), each
49
+ // read at a different time with a different stat, and each is its own
50
+ // question.
51
+ export async function probeFiles(files, maxAgeMs = 0) {
52
+ const byDir = new Map();
53
+ files.forEach((file, i) => {
54
+ const dir = parentDir(file.path);
55
+ const list = byDir.get(dir);
56
+ if (list)
57
+ list.push(i);
58
+ else
59
+ byDir.set(dir, [i]);
60
+ });
61
+ const states = new Array(files.length).fill('unknown');
62
+ const cache = new Map();
63
+ await Promise.all([...byDir].map(async ([dir, indexes]) => {
64
+ let listed = await listedDir(dir, cache, maxAgeMs);
65
+ if (listed?.remembered && indexes.some((i) => stateOf(files[i], listed) !== 'present')) {
66
+ listed = await listedDir(dir, cache, 0);
67
+ }
68
+ if (!listed)
69
+ return;
70
+ for (const i of indexes)
71
+ states[i] = stateOf(files[i], listed);
72
+ }));
73
+ return states;
74
+ }
75
+ function stateOf(file, listed) {
76
+ const entry = listed.files.get(baseName(file.path));
77
+ if (!entry)
78
+ return 'missing';
79
+ // The same test the refresh uses to decide whether to re-read the
80
+ // file (`canSkip` in script-store.ts), so a `modified` chip predicts
81
+ // exactly what a refresh will do. Equality on mtime, not ordering:
82
+ // the value is an opaque listing timestamp.
83
+ if (file.size !== undefined && file.mtime !== undefined
84
+ && entry.size !== undefined && entry.mtime !== undefined
85
+ && (entry.size !== file.size || entry.mtime !== file.mtime)) {
86
+ return 'modified';
87
+ }
88
+ return 'present';
89
+ }
90
+ // The subset of `paths` that are no longer on disk. The delete dialog's
91
+ // question; a convenience over `probeFiles`.
92
+ export async function missingFiles(paths) {
93
+ const files = [...paths].map((path) => ({ path }));
94
+ const states = await probeFiles(files);
95
+ return new Set(files.filter((_, i) => states[i] === 'missing').map((f) => f.path));
96
+ }
97
+ // `.user.js` files on disk in a directory collection that no row
98
+ // accounts for — what a refresh would add. Looks in the collection's
99
+ // root and in every directory a known script sits in, which is where
100
+ // new files land in practice; a new subdirectory with nothing known in
101
+ // it is not seen until a refresh scans it (docs/file-changes.md).
102
+ //
103
+ // Returns paths relative to the collection root, so they read the way
104
+ // the rows do.
105
+ export async function newScriptFiles(collectionPath, knownRelativePaths, maxAgeMs = 0) {
106
+ const root = locationFsPath(collectionPath).replace(/\/+$/, '');
107
+ const known = new Set(knownRelativePaths);
108
+ const dirs = new Set([root]);
109
+ for (const rel of known)
110
+ dirs.add(parentDir(`${root}/${rel}`));
111
+ const out = [];
112
+ const cache = new Map();
113
+ await Promise.all([...dirs].map(async (dir) => {
114
+ const listed = await listedDir(dir, cache, maxAgeMs);
115
+ if (!listed)
116
+ return;
117
+ const prefix = dir === root ? '' : `${dir.slice(root.length + 1)}/`;
118
+ for (const name of listed.files.keys()) {
119
+ if (!name.endsWith('.user.js'))
120
+ continue;
121
+ const rel = prefix + name;
122
+ if (!known.has(rel))
123
+ out.push(rel);
124
+ }
125
+ }));
126
+ return out.sort();
127
+ }
128
+ // Recent successful listings, kept for `maxAgeMs`. Failures are not
129
+ // remembered: the next probe retries. Bounded by the number of
130
+ // folders a page names.
131
+ const recentListings = new Map();
132
+ // The listing for `dir`, or null when it could not be read (logged,
133
+ // not thrown: callers treat it as "nothing known").
134
+ async function listedDir(dir, cache, maxAgeMs) {
135
+ const recent = recentListings.get(dir);
136
+ if (recent && Date.now() - recent.at < maxAgeMs)
137
+ return { ...recent.listed, remembered: true };
138
+ try {
139
+ const entries = await listDirectory(dir, cache);
140
+ const files = new Map();
141
+ for (const e of entries) {
142
+ if (!e.isDir)
143
+ files.set(e.name, { size: e.size, mtime: e.mtime });
144
+ }
145
+ const listed = { files, remembered: false };
146
+ recentListings.set(dir, { at: Date.now(), listed });
147
+ return listed;
148
+ }
149
+ catch (err) {
150
+ dlog(`[SourceMonkey] could not list ${dir} to check for file changes: ${err instanceof Error ? err.message : String(err)}`);
151
+ return null;
152
+ }
153
+ }
154
+ // The tag for a script's own file, or null when the script isn't a
155
+ // local file. One builder for every surface that shows one (the
156
+ // viewer's Location row, the versions table, the collections page), so
157
+ // they can't disagree on the path, the stat, or the wording.
158
+ export function scriptFileTag(script, collectionPath) {
159
+ if (!isFileUrl(script.fileUrl))
160
+ return null;
161
+ const tag = {
162
+ fsPath: locationFsPath(script.fileUrl),
163
+ missingHint: scriptFileMissingHint(collectionPath),
164
+ modifiedHint: SCRIPT_FILE_MODIFIED_HINT,
165
+ };
166
+ // Both or neither: a row with a size but no listing mtime (its
167
+ // folder didn't list when it was read) can't be compared, and a lone
168
+ // size would miss same-size edits — worse than saying nothing.
169
+ if (script.size != null && script.fsMtime !== undefined) {
170
+ tag.size = script.size;
171
+ tag.mtime = script.fsMtime;
172
+ }
173
+ return tag;
174
+ }
175
+ // Tag an element as naming a local file, for `markFileChanges`.
176
+ export function tagLocalFile(el, tag) {
177
+ el.dataset.fsPath = tag.fsPath;
178
+ if (tag.size !== undefined)
179
+ el.dataset.fsSize = String(tag.size);
180
+ if (tag.mtime !== undefined)
181
+ el.dataset.fsMtime = String(tag.mtime);
182
+ el.dataset.missingHint = tag.missingHint;
183
+ if (tag.modifiedHint)
184
+ el.dataset.modifiedHint = tag.modifiedHint;
185
+ }
186
+ export const FILE_CHIP_CLASS = 'file-change-chip';
187
+ // Probe every tagged element under `root` and put a chip after each
188
+ // one whose file is gone or changed. Safe to call after every repaint:
189
+ // an earlier chip is removed first, so a file that came back (or was
190
+ // re-read) loses its chip, and an element that left the document
191
+ // before the probe returns is simply annotated where nobody can see
192
+ // it. Resolves with the state of every tagged element, for a caller
193
+ // that wants to summarize.
194
+ //
195
+ // Async and deliberately not awaited by renders: the probe is a disk
196
+ // read, and a synchronous paint must not wait on it.
197
+ export async function markFileChanges(root, maxAgeMs = 0) {
198
+ const tagged = [...root.querySelectorAll('[data-fs-path]')];
199
+ const states = new Map();
200
+ if (tagged.length === 0)
201
+ return states;
202
+ const probed = await probeFiles(tagged.map((el) => ({
203
+ path: el.dataset.fsPath,
204
+ size: el.dataset.fsSize === undefined ? undefined : Number(el.dataset.fsSize),
205
+ mtime: el.dataset.fsMtime === undefined ? undefined : Number(el.dataset.fsMtime),
206
+ })), maxAgeMs);
207
+ tagged.forEach((el, i) => states.set(el, probed[i]));
208
+ for (const el of tagged) {
209
+ // The chip follows the path — and a copy button that directly
210
+ // follows the path, since that button belongs beside the text it
211
+ // copies (the viewer's Location and Local overrides rows).
212
+ let anchor = el;
213
+ if (anchor.nextElementSibling?.classList.contains('copy-btn'))
214
+ anchor = anchor.nextElementSibling;
215
+ const prior = anchor.nextElementSibling;
216
+ if (prior?.classList.contains(FILE_CHIP_CLASS))
217
+ prior.remove();
218
+ const state = states.get(el);
219
+ const hint = state === 'missing'
220
+ ? el.dataset.missingHint
221
+ : state === 'modified' ? el.dataset.modifiedHint : undefined;
222
+ // A modified file with no hint is one this surface doesn't report
223
+ // edits for.
224
+ if (!hint)
225
+ continue;
226
+ const chip = document.createElement('span');
227
+ chip.className = `${FILE_CHIP_CLASS} ${FILE_CHIP_CLASS}-${state}`;
228
+ chip.textContent = state === 'missing' ? 'deleted' : 'modified';
229
+ chip.title = hint;
230
+ anchor.after(chip);
231
+ }
232
+ return states;
233
+ }
234
+ // ---------- Wording ----------
235
+ //
236
+ // The hints, in one place so the surfaces that name the same file agree
237
+ // on what its deletion means. Keyed by the collection the row sits in,
238
+ // because that is what decides what happens next:
239
+ //
240
+ // - A directory collection mirrors the disk, so the next refresh drops
241
+ // the row.
242
+ // - A manifest entry is the manifest's to keep or drop: the row stays,
243
+ // running from its last copy, until the entry leaves the manifest.
244
+ // - A file added on its own is the user's: the row stays until they
245
+ // remove it (the trash can on its row).
246
+ export function scriptFileMissingHint(collectionPath) {
247
+ if (isLocalDirectoryCollection(collectionPath)) {
248
+ return 'This file has been deleted. The next refresh will remove this script.';
249
+ }
250
+ if (isManifestCollection(collectionPath)) {
251
+ return 'This file has been deleted. The last copy of the script keeps running '
252
+ + 'until the entry is removed from the manifest.';
253
+ }
254
+ return 'This file has been deleted. The last copy of the script keeps running until you remove it.';
255
+ }
256
+ // An edited script file: the same for every collection kind, since a
257
+ // refresh re-reads it wherever it sits.
258
+ export const SCRIPT_FILE_MODIFIED_HINT = 'This file has changed on disk. The next refresh will load the new version.';
259
+ // A local stand-in for a `@require` / `@resource`. Deleted: the
260
+ // declared URL is still there, so the next refresh loads it from
261
+ // there instead. Edited: the next refresh (opportunistic ones
262
+ // included — a stand-in is re-read every pass) picks it up.
263
+ export const STAND_IN_MISSING_HINT = 'This file has been deleted. The next refresh will reload the remote version.';
264
+ // The same words as for a script file: the outcome is the same.
265
+ export const STAND_IN_MODIFIED_HINT = SCRIPT_FILE_MODIFIED_HINT;
266
+ // A file sm-dev pushed with a scratch script: a snapshot no refresh
267
+ // rereads. It has no stored mtime, so only a deletion can be told.
268
+ export const PUSHED_FILE_MISSING_HINT = 'This file has been deleted since sm-dev pushed it. The script still runs the snapshot it pushed.';
269
+ // The tag for a stand-in, given its path and the stored entry. The
270
+ // entry's `size` is the bytes read, which is the file's size; with the
271
+ // listing mtime recorded beside it, an edit can be told apart.
272
+ // `pushed` is for a snapshot sm-dev pushed.
273
+ export function standInTag(fsPath, entry, pushed = false) {
274
+ const tag = {
275
+ fsPath,
276
+ missingHint: pushed ? PUSHED_FILE_MISSING_HINT : STAND_IN_MISSING_HINT,
277
+ ...(pushed ? {} : { modifiedHint: STAND_IN_MODIFIED_HINT }),
278
+ };
279
+ if (entry.size !== undefined && entry.fsMtime !== undefined) {
280
+ tag.size = entry.size;
281
+ tag.mtime = entry.fsMtime;
282
+ }
283
+ return tag;
284
+ }
285
+ // Counts a directory or `file://` manifest collection's changes on
286
+ // disk: its rows' probed `files`, plus the new files a directory holds,
287
+ // or whether the manifest file changed (`manifest`: its path and the
288
+ // stat its last scan recorded). Null for any other collection.
289
+ export async function pendingOnDisk(collectionPath, manifest, files, maxAgeMs = 0) {
290
+ const isDirectory = isLocalDirectoryCollection(collectionPath);
291
+ if (!isDirectory && !(isManifestCollection(collectionPath) && manifest))
292
+ return null;
293
+ const counts = { modified: 0, deleted: 0, added: 0 };
294
+ for (const f of files) {
295
+ if (f.state === 'missing')
296
+ counts.deleted += 1;
297
+ else if (f.state === 'modified')
298
+ counts.modified += 1;
299
+ }
300
+ if (isDirectory) {
301
+ const root = locationFsPath(collectionPath).replace(/\/+$/, '');
302
+ const known = files.map((f) => f.fsPath.slice(root.length + 1));
303
+ counts.added = (await newScriptFiles(collectionPath, known, maxAgeMs)).length;
304
+ }
305
+ else {
306
+ const [state] = await probeFiles([manifest], maxAgeMs);
307
+ if (state === 'modified')
308
+ counts.manifest = true;
309
+ }
310
+ return counts;
311
+ }
312
+ // The manifest file to probe for a `file://` manifest collection, with
313
+ // the stat its last scan recorded; null for any other.
314
+ export function manifestToProbe(collection) {
315
+ if (!isManifestCollection(collection.path) || !isFileUrl(collectionCanonicalUrl(collection.path)))
316
+ return null;
317
+ return { path: locationFsPath(collection.path), size: collection.manifestSize, mtime: collection.manifestMtime };
318
+ }
319
+ // The tooltip of a collection's "refresh pending" chip: the summary, and
320
+ // what to do. Empty when nothing differs.
321
+ export function refreshPendingHint(counts) {
322
+ const summary = pendingSummary(counts);
323
+ return summary ? `${summary} on disk. Refresh to load.` : '';
324
+ }
325
+ export const REFRESH_PENDING_CHIP_CLASS = `${FILE_CHIP_CLASS} ${FILE_CHIP_CLASS}-pending`;
326
+ // Put a "refresh pending" chip at the end of `nameEl` (a collection's
327
+ // display name), or remove the one there, to match `hint` (empty =
328
+ // nothing pending). Inside the name rather than after it: the heading
329
+ // is a two-column grid (bubble, name), where a third child would start
330
+ // a new row.
331
+ export function syncRefreshPendingChip(nameEl, hint) {
332
+ const existing = nameEl.querySelector(`:scope > .${FILE_CHIP_CLASS}-pending`);
333
+ if (!hint) {
334
+ existing?.remove();
335
+ return;
336
+ }
337
+ if (existing) {
338
+ existing.title = hint;
339
+ return;
340
+ }
341
+ const chip = document.createElement('span');
342
+ chip.className = REFRESH_PENDING_CHIP_CLASS;
343
+ chip.textContent = 'refresh pending';
344
+ chip.title = hint;
345
+ nameEl.appendChild(chip);
346
+ }
@@ -0,0 +1,85 @@
1
+ // Where a message from injected code came from: one of a page's
2
+ // iframes or a page's own (top-level) frame, and which page. See
3
+ // docs/frames.md.
4
+ //
5
+ // `sender.frameId > 0` alone can't tell: Chrome numbers a prerendered
6
+ // page's top frame like an iframe (its `frameType` is `outermost_frame`,
7
+ // which `MessageSender` doesn't carry). So a document with a frame id
8
+ // other than 0 is looked up with `chrome.webNavigation.getFrame`, and
9
+ // walked up through its parents to the page's document.
10
+ //
11
+ // Answers are cached by document id, which Chrome never reuses. A
12
+ // document's place doesn't change, but whether it is still in the tab
13
+ // does, so `live` is asked afresh when the caller needs it.
14
+ // Bounded: an entry per document of every frame that ever messaged us.
15
+ const CACHE_CAP = 1000;
16
+ const cache = new Map();
17
+ function remember(documentId, placed) {
18
+ if (cache.size >= CACHE_CAP)
19
+ cache.delete(cache.keys().next().value);
20
+ cache.set(documentId, placed);
21
+ }
22
+ // The frame `documentId` is, or null when Chrome no longer has it (it
23
+ // answers null, or refuses). `documentId` isn't in our @types/chrome
24
+ // version of `GetFrameDetails`, hence the cast.
25
+ async function getFrame(tabId, documentId) {
26
+ try {
27
+ const details = { tabId, documentId };
28
+ return (await chrome.webNavigation.getFrame(details));
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ // Frames nest, but not this deep in practice; the bound keeps a
35
+ // misbehaving answer from looping.
36
+ const MAX_DEPTH = 32;
37
+ /**
38
+ * Where `sender`'s document is. `checkLive` asks Chrome whether it is
39
+ * still in the tab even when its place is cached.
40
+ */
41
+ export async function frameInfo(sender, checkLive = false) {
42
+ const tabId = sender.tab?.id;
43
+ const documentId = sender.documentId ?? '';
44
+ const frameId = sender.frameId;
45
+ if (frameId === 0)
46
+ return { subframe: false, topDocId: documentId, live: true };
47
+ // Nothing to look up with: by the frame id, as before.
48
+ if (tabId == null || !documentId || typeof frameId !== 'number') {
49
+ return { subframe: typeof frameId === 'number' && frameId > 0, topDocId: '', live: true };
50
+ }
51
+ const cached = cache.get(documentId);
52
+ if (cached && !checkLive)
53
+ return { ...cached, live: true };
54
+ const frame = await getFrame(tabId, documentId);
55
+ if (!frame)
56
+ return { ...(cached ?? { subframe: true, topDocId: '' }), live: false };
57
+ if (cached)
58
+ return { ...cached, live: true };
59
+ let placed;
60
+ if (frame.parentFrameId === -1) {
61
+ placed = { subframe: false, topDocId: documentId, topUrl: frame.url };
62
+ }
63
+ else {
64
+ placed = { subframe: true, topDocId: '' };
65
+ let parent = frame.parentDocumentId;
66
+ for (let depth = 0; parent && depth < MAX_DEPTH; depth++) {
67
+ const up = cache.get(parent);
68
+ if (up && !up.subframe) {
69
+ placed = { subframe: true, topDocId: up.topDocId, topUrl: up.topUrl };
70
+ break;
71
+ }
72
+ const next = await getFrame(tabId, parent);
73
+ if (!next)
74
+ break;
75
+ if (next.parentFrameId === -1) {
76
+ placed = { subframe: true, topDocId: parent, topUrl: next.url };
77
+ break;
78
+ }
79
+ parent = next.parentDocumentId;
80
+ }
81
+ }
82
+ if (placed.topDocId)
83
+ remember(documentId, placed);
84
+ return { ...placed, live: true };
85
+ }