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,336 @@
1
+ // GitHub-specific collection handling: URL parsing, default-branch
2
+ // resolution, raw-URL normalization, recursive Contents-API scanner,
3
+ // and the per-script file/display URL builders for the github-dir
4
+ // collection kind. `directories.ts` dispatches to the exports here from
5
+ // its collection-kind branches; nothing in this file is aware of file://
6
+ // or local-filesystem collections.
7
+ //
8
+ // Two ergonomic problems we paper over here:
9
+ //
10
+ // 1. `github.com/<o>/<r>/blob/<ref>/<path>` URLs (what the user copies
11
+ // from the browser address bar) serve an HTML rendering of the
12
+ // file, not the raw bytes. `normalizeGithubInput` rewrites those to
13
+ // their `raw.githubusercontent.com` counterpart at add-collection time
14
+ // so the rest of the pipeline treats them as ordinary HTTP single-
15
+ // file / manifest collections. Recognised file shapes are `.user.js`
16
+ // files and the fixed `script_manifest.json` manifest filename.
17
+ // 2. GitHub directory URLs (`/tree/<ref>/<path>` and, by extension, a
18
+ // misused `/blob/<ref>/<dir>` form) have no direct "give me all the
19
+ // `.user.js` files inside" endpoint. `scanGithubDir` uses the
20
+ // Contents API to walk the tree at scan time, returning the same
21
+ // `FoundScript` shape the other collection kinds produce.
22
+ //
23
+ // Bare repo URLs (`github.com/<o>/<r>` with no `/blob|tree/...`
24
+ // segment) resolve to `tree/<default-branch>` via one repo-metadata
25
+ // API call at add time; the resolved branch is baked into the stored
26
+ // collection path so refreshes are deterministic afterwards.
27
+ //
28
+ // Branch names containing slashes (e.g. `feature/foo`) aren't
29
+ // supported — the matcher takes the first ref segment and assumes the
30
+ // rest is the path.
31
+ import { MAX_SCAN_DEPTH, MANIFEST_FILENAME } from '../collection-scan.js';
32
+ import { githubAuthHeaders, getGithubAuthToken } from '../github-auth.js';
33
+ import { mapWithConcurrency, SCAN_GITHUB_DIRS_CONCURRENCY } from './concurrency.js';
34
+ const GITHUB_URL_RE = /^https?:\/\/github\.com\/([^\/?#]+)\/([^\/?#]+)\/(blob|tree)\/([^\/?#]+)(?:\/([^?#]*))?(?:\?[^#]*)?(?:#.*)?$/i;
35
+ // Matches a bare `https://github.com/<owner>/<repo>` URL — no
36
+ // `/blob|tree/...` segment, no path. Used to recognise repo-root
37
+ // inputs at add time so we can resolve the default branch and store
38
+ // a canonical `/tree/<branch>` URL.
39
+ const GITHUB_REPO_ROOT_RE = /^https?:\/\/github\.com\/([^\/?#]+)\/([^\/?#]+?)(?:\.git)?\/?(?:\?[^#]*)?(?:#.*)?$/i;
40
+ // Parse a github.com browser URL. Returns null for anything that
41
+ // doesn't match the `(blob|tree)/<ref>/...` shape (including repo-root
42
+ // URLs and any non-github host).
43
+ export function parseGithubUrl(input) {
44
+ const m = GITHUB_URL_RE.exec(input);
45
+ if (!m)
46
+ return null;
47
+ return {
48
+ owner: m[1],
49
+ repo: m[2].replace(/\.git$/, ''),
50
+ kind: m[3].toLowerCase(),
51
+ ref: m[4],
52
+ path: (m[5] ?? '').replace(/\/+$/, ''),
53
+ };
54
+ }
55
+ // True for a github.com URL pointing at a directory. `/tree/...` is the
56
+ // canonical form, but we also accept `/blob/<ref>/<dir>` (the form a
57
+ // user might end up with by editing a blob URL by hand) as long as the
58
+ // path doesn't look like a recognised file: `.user.js` (single script)
59
+ // or basename `script_manifest.json` (manifest).
60
+ export function isGithubDirCollection(input) {
61
+ const p = parseGithubUrl(input);
62
+ if (!p)
63
+ return false;
64
+ return !isGithubFilePath(p.path);
65
+ }
66
+ // True when the parsed GitHub path points at one of the file shapes we
67
+ // recognise (a userscript or a manifest under its fixed filename).
68
+ function isGithubFilePath(p) {
69
+ if (p.endsWith('.user.js'))
70
+ return true;
71
+ if (p === MANIFEST_FILENAME)
72
+ return true;
73
+ if (p.endsWith('/' + MANIFEST_FILENAME))
74
+ return true;
75
+ return false;
76
+ }
77
+ // Build the raw.githubusercontent.com URL for a parsed file URL. Caller
78
+ // must have already verified the URL's path is a recognised file shape
79
+ // (see `isGithubFilePath` / `normalizeGithubInput`'s inline check).
80
+ function githubFileUrlToRaw(p) {
81
+ return `https://raw.githubusercontent.com/${p.owner}/${p.repo}/${p.ref}/${p.path}`;
82
+ }
83
+ // Resolve a per-script entry under a github-dir collection to its raw
84
+ // URL (what `fetchHttpText` reads). Exported for `directories.ts`'s
85
+ // `scriptFileUrl` dispatch.
86
+ export function githubDirScriptFileUrl(collectionPath, relativePath) {
87
+ const p = parseGithubUrl(collectionPath);
88
+ if (!p)
89
+ throw new Error(`Not a GitHub URL: ${collectionPath}`);
90
+ const full = p.path ? `${p.path}/${relativePath}` : relativePath;
91
+ return `https://raw.githubusercontent.com/${p.owner}/${p.repo}/${p.ref}/${full}`;
92
+ }
93
+ // Resolve a per-script entry under a github-dir collection to its
94
+ // human-displayable github.com URL (what the viewer's Location row
95
+ // shows). Exported for `directories.ts`'s `scriptAbsolutePath`
96
+ // dispatch.
97
+ export function githubDirScriptDisplayUrl(collectionPath, relativePath) {
98
+ const p = parseGithubUrl(collectionPath);
99
+ if (!p)
100
+ throw new Error(`Not a GitHub URL: ${collectionPath}`);
101
+ const full = p.path ? `${p.path}/${relativePath}` : relativePath;
102
+ return `https://github.com/${p.owner}/${p.repo}/blob/${p.ref}/${full}`;
103
+ }
104
+ // Parse a bare `github.com/<owner>/<repo>` URL — repo root, no ref.
105
+ // Returns null for anything that already parses via `parseGithubUrl`
106
+ // (i.e. URLs that bring their own `/blob|tree/...` segment).
107
+ function parseGithubRepoRoot(input) {
108
+ if (parseGithubUrl(input))
109
+ return null;
110
+ const m = GITHUB_REPO_ROOT_RE.exec(input);
111
+ if (!m)
112
+ return null;
113
+ return { owner: m[1], repo: m[2].replace(/\.git$/, '') };
114
+ }
115
+ // True for any GitHub input a collection can be built from without
116
+ // naming a file: a `/tree/...` (or file-less `/blob/...`) directory URL,
117
+ // or a bare repo root. The repo root needs one API call to resolve its
118
+ // default branch before it can be stored (`normalizeGithubInput`), but
119
+ // recognising it takes none — which is what lets the collection
120
+ // dialog classify a location on every keystroke.
121
+ export function isGithubDirInput(input) {
122
+ return isGithubDirCollection(input) || parseGithubRepoRoot(input) !== null;
123
+ }
124
+ // Fetch a repo's default branch via the GitHub repository metadata
125
+ // endpoint. One API call (counts against the same 60/hr/IP budget as
126
+ // the Contents API for unauthenticated callers). Called only at
127
+ // add-collection time for bare-repo URLs; resolved branch gets baked
128
+ // into the canonical collection path so refresh is deterministic
129
+ // afterwards.
130
+ async function fetchGithubDefaultBranch(owner, repo) {
131
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
132
+ let response;
133
+ try {
134
+ response = await fetch(apiUrl, {
135
+ cache: 'no-store',
136
+ headers: {
137
+ 'Accept': 'application/vnd.github+json',
138
+ ...githubAuthHeaders(apiUrl),
139
+ },
140
+ });
141
+ }
142
+ catch (err) {
143
+ throw new Error(`Could not read ${apiUrl}: ${err instanceof Error ? err.message : String(err)}`);
144
+ }
145
+ // Every failure below shares the `Could not read <url>: <reason>`
146
+ // shape the rest of the fetch pipeline uses (`directories.ts`), so a
147
+ // GitHub failure and a plain HTTP one read the same way. "Repo
148
+ // metadata" was our name for this request, not something the reader
149
+ // asked for.
150
+ if (!response.ok) {
151
+ throw new Error(githubStatusError(apiUrl, response.status));
152
+ }
153
+ let json;
154
+ try {
155
+ json = await response.json();
156
+ }
157
+ catch (err) {
158
+ // The parser's own complaint is kept: `Unexpected token <` is how a
159
+ // captive portal or proxy error page announces itself, and dropping
160
+ // it would leave "not valid JSON" with nothing to act on.
161
+ throw new Error(`Could not read ${apiUrl}: the response is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
162
+ }
163
+ const branch = json?.default_branch;
164
+ if (typeof branch !== 'string' || !branch) {
165
+ throw new Error(`Could not read ${apiUrl}: GitHub did not report the repo's default branch`);
166
+ }
167
+ return branch;
168
+ }
169
+ // At add-collection time, transparently rewrite a github.com input into
170
+ // the canonical form the rest of the pipeline expects:
171
+ // - File URL — `/blob/<ref>/<path>.user.js` or a path whose basename
172
+ // is `script_manifest.json` — gets rewritten to its raw URL.
173
+ // - Bare repo URL (`github.com/<o>/<r>` with no ref) → `/tree/<default-branch>`,
174
+ // resolved via the repo metadata endpoint (one API call).
175
+ // - Directory URLs (`/tree/<ref>/...`) pass through unchanged.
176
+ // - Non-github inputs pass through unchanged (no network call).
177
+ // Async because the repo-root case needs to consult the GitHub API to
178
+ // resolve the default branch. Idempotent on canonical outputs so the
179
+ // caller-and-callee normalize pattern (see `addCollection`) doesn't
180
+ // double-up on API calls.
181
+ export async function normalizeGithubInput(input) {
182
+ const p = parseGithubUrl(input);
183
+ if (p) {
184
+ if (p.path && isGithubFilePath(p.path)) {
185
+ return githubFileUrlToRaw(p);
186
+ }
187
+ return input;
188
+ }
189
+ const root = parseGithubRepoRoot(input);
190
+ if (root) {
191
+ const branch = await fetchGithubDefaultBranch(root.owner, root.repo);
192
+ return `https://github.com/${root.owner}/${root.repo}/tree/${branch}`;
193
+ }
194
+ return input;
195
+ }
196
+ // Build the GitHub Contents API URL for a directory inside a repo.
197
+ // Empty `path` lists the repo root at the given ref.
198
+ function githubContentsApiUrl(p, subPath) {
199
+ const base = `https://api.github.com/repos/${p.owner}/${p.repo}/contents`;
200
+ const full = subPath ? `${p.path ? p.path + '/' : ''}${subPath}` : p.path;
201
+ const encPath = full
202
+ ? '/' + full.split('/').map(encodeURIComponent).join('/')
203
+ : '';
204
+ return `${base}${encPath}?ref=${encodeURIComponent(p.ref)}`;
205
+ }
206
+ // Tack a private-repo / rate-limit hint onto the GitHub API error so
207
+ // the failure is self-diagnosing. GitHub returns the same 404 for
208
+ // "repo doesn't exist" and "repo exists but you can't see it without
209
+ // auth", so the hint covers both. 403 unauthenticated almost always
210
+ // means the per-IP rate limit ran out; an authenticated 403 means the
211
+ // token doesn't have access to the repo or is missing a scope.
212
+ // The hint text branches on whether a token is configured so the
213
+ // user gets a next-step pointer rather than stale advice.
214
+ //
215
+ // A sentence of its own on a new line, after the status line's period
216
+ // (`githubStatusError`). The collections page, the dialog, the Log tab
217
+ // and sm-dev show the line break; a surface that collapses whitespace
218
+ // shows a space.
219
+ function githubAuthHint(status) {
220
+ const hasToken = !!getGithubAuthToken();
221
+ if (status === 404) {
222
+ if (hasToken) {
223
+ return 'Repo not found, or your token can\'t see this repo. Check the token\'s repo access on github.com.';
224
+ }
225
+ return 'Repo not found, or repo is private. Set a GitHub token on the Options page to fetch private repos.';
226
+ }
227
+ if (status === 403 || status === 429) {
228
+ if (hasToken) {
229
+ return 'Rate limit hit, or your token lacks the required scope. Authenticated requests get 5000/hour against api.github.com.';
230
+ }
231
+ return 'Likely the unauthenticated 60/hr API rate limit. Set a GitHub token on the Options page to raise it to 5000/hr.';
232
+ }
233
+ if (status === 401) {
234
+ return 'Token rejected. Re-check the value on the Options page.';
235
+ }
236
+ return '';
237
+ }
238
+ // A failed GitHub API read: the status, then the hint for it, if any.
239
+ function githubStatusError(apiUrl, status) {
240
+ const hint = githubAuthHint(status);
241
+ return hint
242
+ ? `Could not read ${apiUrl}: HTTP ${status}.\n${hint}`
243
+ : `Could not read ${apiUrl}: HTTP ${status}`;
244
+ }
245
+ // Fetch a single Contents API response. Returns the parsed JSON array.
246
+ // Throws on non-2xx, non-array, or network failure — caller
247
+ // (refreshCollection) turns the error into a per-collection
248
+ // `scanError`.
249
+ async function fetchGithubContents(apiUrl) {
250
+ let response;
251
+ try {
252
+ response = await fetch(apiUrl, {
253
+ cache: 'no-store',
254
+ headers: {
255
+ 'Accept': 'application/vnd.github+json',
256
+ ...githubAuthHeaders(apiUrl),
257
+ },
258
+ });
259
+ }
260
+ catch (err) {
261
+ throw new Error(`Could not read ${apiUrl}: ${err instanceof Error ? err.message : String(err)}`);
262
+ }
263
+ if (!response.ok) {
264
+ // 403 with rate-limit headers is the common failure mode for
265
+ // unauthenticated repeat callers; 404 means either the repo / path
266
+ // doesn't exist OR it's a private repo (GitHub returns the same
267
+ // status for both unauthenticated). Surface the status plus the
268
+ // private-repo hint so the user has something concrete to act on.
269
+ throw new Error(githubStatusError(apiUrl, response.status));
270
+ }
271
+ let json;
272
+ try {
273
+ json = await response.json();
274
+ }
275
+ catch (err) {
276
+ // The parser's own complaint is kept: `Unexpected token <` is how a
277
+ // captive portal or proxy error page announces itself, and dropping
278
+ // it would leave "not valid JSON" with nothing to act on.
279
+ throw new Error(`Could not read ${apiUrl}: the response is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
280
+ }
281
+ if (!Array.isArray(json)) {
282
+ // GitHub answers a file path with an object rather than a listing,
283
+ // so this is the "you pointed at a file" case, not a broken API.
284
+ throw new Error(`Could not read ${apiUrl}: that path is not a folder in the repo`);
285
+ }
286
+ return json;
287
+ }
288
+ // Recursive scan of a GitHub directory collection via the Contents API.
289
+ // Mirrors the shape of `scanForUserScripts` / `scanManifest`: returns a
290
+ // flat `FoundScript[]` keyed by paths relative to the collection's root
291
+ // dir, capped at MAX_SCAN_DEPTH levels deep.
292
+ //
293
+ // Cost: one Contents API call per directory walked. Unauthenticated
294
+ // callers share a 60/hour budget per IP, so deep trees can drain
295
+ // quickly; authenticated-collections support is in TODO.md.
296
+ export async function scanGithubDir(input, depth = 0, prefix = '') {
297
+ const p = parseGithubUrl(input);
298
+ if (!p)
299
+ throw new Error(`Not a GitHub URL: ${input}`);
300
+ // `prefix` carries a trailing slash on every recursive call
301
+ // (`prefix + entry.name + '/'`); strip it so the Contents-API path
302
+ // ends in the directory name without an extra `/`.
303
+ const subPath = prefix.replace(/\/+$/, '');
304
+ const apiUrl = githubContentsApiUrl(p, subPath);
305
+ const entries = await fetchGithubContents(apiUrl);
306
+ const results = [];
307
+ const subdirs = [];
308
+ for (const entry of entries) {
309
+ if (entry.type === 'file' && entry.name.endsWith('.user.js')) {
310
+ const found = {
311
+ name: entry.name,
312
+ relativePath: prefix + entry.name,
313
+ };
314
+ if (typeof entry.size === 'number')
315
+ found.size = entry.size;
316
+ results.push(found);
317
+ }
318
+ else if (entry.type === 'dir' && depth < MAX_SCAN_DEPTH) {
319
+ subdirs.push(entry);
320
+ }
321
+ }
322
+ // Listed together rather than one after another — same reasoning as
323
+ // the local scan, but the round trips here are API calls rather than
324
+ // disk reads, so the saving is far bigger per directory. A repo whose
325
+ // root holds three subdirectories cost four serialized calls before.
326
+ //
327
+ // Bounded tighter than the local walk: these all hit api.github.com,
328
+ // which throttles concurrent bursts on top of the hourly quota. The
329
+ // request COUNT is unchanged either way, which is what the quota
330
+ // actually meters.
331
+ const nested = await mapWithConcurrency(subdirs, SCAN_GITHUB_DIRS_CONCURRENCY, (entry) => scanGithubDir(input, depth + 1, prefix + entry.name + '/'));
332
+ for (const sub of nested)
333
+ results.push(...sub);
334
+ results.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
335
+ return results;
336
+ }
@@ -0,0 +1,84 @@
1
+ // Rendering a glyph from the shared sprite (src/glyph-sprite.svg) into
2
+ // the page.
3
+ //
4
+ // Every icon in the extension is one `<symbol>` in that sprite, inlined
5
+ // once per page by the build. All this module does is build the tiny
6
+ // `<svg><use href="#id">` that points at one — but it is shared because
7
+ // the alternative is each caller inventing its own class name and
8
+ // sizing, which is how two buttons wearing "the same" icon end up
9
+ // different sizes.
10
+ //
11
+ // Colour and size come from CSS on the button (`.btn-glyph` in
12
+ // styles.css), not from attributes here, so a glyph inherits
13
+ // `currentColor` and tracks the surrounding font.
14
+ const NS = 'http://www.w3.org/2000/svg';
15
+ // The class the shared sizing/spin rules key off. Callers with their
16
+ // own rules (the site-icon globe, the removal cans) pass their own.
17
+ export const GLYPH_CLASS = 'btn-glyph';
18
+ // Build an `<svg>` referencing one sprite symbol. `aria-hidden` on
19
+ // every one of them: a glyph never carries the accessible name, the
20
+ // button's own `aria-label` does.
21
+ export function renderGlyph(doc, symbolId, className = GLYPH_CLASS) {
22
+ const svg = doc.createElementNS(NS, 'svg');
23
+ svg.setAttribute('viewBox', '0 0 24 24');
24
+ svg.setAttribute('class', className);
25
+ svg.setAttribute('aria-hidden', 'true');
26
+ const use = doc.createElementNS(NS, 'use');
27
+ use.setAttribute('href', symbolId.startsWith('#') ? symbolId : `#${symbolId}`);
28
+ svg.appendChild(use);
29
+ return svg;
30
+ }
31
+ // Replace a button's contents with a glyph (and optional label), and
32
+ // give it `label` as its tooltip.
33
+ //
34
+ // `label` also becomes the accessible name, but ONLY for an icon-only
35
+ // button. A button that shows words has to be announced by the words a
36
+ // sighted user can read — an `aria-label` overrides them, so "Refresh
37
+ // All" on screen would be announced (and matched by voice control) as
38
+ // something else entirely.
39
+ export function setGlyphButton(btn, symbolId, label, opts = {}) {
40
+ const doc = btn.ownerDocument;
41
+ btn.textContent = '';
42
+ btn.classList.add('icon-btn');
43
+ btn.appendChild(renderGlyph(doc, symbolId));
44
+ // Truthy, not `!== undefined`: an empty label would render an empty
45
+ // `.btn-label`, which `setGlyphButtonLabel` then reads as "this
46
+ // button shows its own text" and drops the `aria-label` for — a
47
+ // button announced as nothing at all.
48
+ if (opts.text) {
49
+ const span = doc.createElement('span');
50
+ span.className = 'btn-label';
51
+ span.textContent = opts.text;
52
+ btn.appendChild(span);
53
+ }
54
+ setGlyphButtonLabel(btn, label);
55
+ }
56
+ // Retitle a glyph button without rebuilding it. The busy state uses
57
+ // this: the glyph itself has to survive, because it is what spins.
58
+ //
59
+ // The `aria-label` goes on only where there is no visible label to
60
+ // override — see `setGlyphButton`. A labelled button's accessible name
61
+ // is its own text, and its `title` is the longer explanation.
62
+ export function setGlyphButtonLabel(btn, label) {
63
+ btn.title = label;
64
+ if (btn.querySelector('.btn-label'))
65
+ btn.removeAttribute('aria-label');
66
+ else
67
+ btn.setAttribute('aria-label', label);
68
+ }
69
+ // The in-flight state for a button whose action takes a while.
70
+ //
71
+ // The glyph spins in place rather than the label changing to
72
+ // "Refreshing…". That used to be the only signal, and it can't survive
73
+ // an icon-only button — but it was also the thing that made the button
74
+ // change width mid-click, nudging its neighbours. Spinning the glyph
75
+ // says the same thing and moves nothing.
76
+ //
77
+ // `aria-live` is deliberately not used: the tooltip and the disabled
78
+ // state carry it, and a row of collections each announcing their own
79
+ // refresh would be worse than silence.
80
+ export function setGlyphButtonBusy(btn, busy, labels) {
81
+ btn.disabled = busy;
82
+ btn.querySelector(`.${GLYPH_CLASS}`)?.classList.toggle('spinning', busy);
83
+ setGlyphButtonLabel(btn, busy ? labels.busy : labels.idle);
84
+ }