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.
- package/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/bytes.js +37 -0
- package/dist/channel-placeholder.js +5 -0
- package/dist/collection-scan.js +17 -0
- package/dist/collections.html +353 -0
- package/dist/compare.html +358 -0
- package/dist/composed-source.js +205 -0
- package/dist/connect-prompt.html +47 -0
- package/dist/debug/icons.html +211 -0
- package/dist/debug/storage.html +163 -0
- package/dist/debug-log.js +75 -0
- package/dist/diagnostic-location.js +64 -0
- package/dist/eval-hook.js +308 -0
- package/dist/extension/background.js +5263 -0
- package/dist/extension/channel.js +133 -0
- package/dist/extension/code-links.js +187 -0
- package/dist/extension/code-tabs.js +460 -0
- package/dist/extension/collection-dialog.js +619 -0
- package/dist/extension/collection-display.js +322 -0
- package/dist/extension/collections.js +2905 -0
- package/dist/extension/compare.js +867 -0
- package/dist/extension/concurrency.js +186 -0
- package/dist/extension/connect-grants.js +109 -0
- package/dist/extension/connect-prompt-page.js +70 -0
- package/dist/extension/connect-prompt.js +266 -0
- package/dist/extension/copy-button.js +63 -0
- package/dist/extension/copy-dialog.js +502 -0
- package/dist/extension/copy-preference.js +66 -0
- package/dist/extension/debug/icons.js +203 -0
- package/dist/extension/debug/storage.js +365 -0
- package/dist/extension/deep-link.js +77 -0
- package/dist/extension/delete-dialog.js +432 -0
- package/dist/extension/delete-icon.js +40 -0
- package/dist/extension/details-tooltips.js +66 -0
- package/dist/extension/directories.js +679 -0
- package/dist/extension/export-backup.js +407 -0
- package/dist/extension/export-dialog.js +142 -0
- package/dist/extension/file-access.js +74 -0
- package/dist/extension/file-changes.js +346 -0
- package/dist/extension/frame-info.js +85 -0
- package/dist/extension/github-provider.js +336 -0
- package/dist/extension/glyph.js +84 -0
- package/dist/extension/gm-bindings.js +601 -0
- package/dist/extension/gm-download.js +419 -0
- package/dist/extension/gm-menu.js +228 -0
- package/dist/extension/gm-notification.js +141 -0
- package/dist/extension/gm-originator-map.js +23 -0
- package/dist/extension/gm-tab-data.js +121 -0
- package/dist/extension/gm-tab.js +160 -0
- package/dist/extension/gm-url-change.js +111 -0
- package/dist/extension/gm-url-scheme.js +27 -0
- package/dist/extension/gm-values.js +176 -0
- package/dist/extension/gm-window-control.js +104 -0
- package/dist/extension/gm-xhr-headers.js +333 -0
- package/dist/extension/gm-xhr.js +584 -0
- package/dist/extension/injector.js +3145 -0
- package/dist/extension/install-capture.js +277 -0
- package/dist/extension/line-diff.js +233 -0
- package/dist/extension/local-copy.js +560 -0
- package/dist/extension/local-delete.js +276 -0
- package/dist/extension/location-kinds.js +108 -0
- package/dist/extension/log-format.js +158 -0
- package/dist/extension/managed-changes.js +361 -0
- package/dist/extension/managed-items.js +57 -0
- package/dist/extension/match-index.js +375 -0
- package/dist/extension/missing-scripts.js +66 -0
- package/dist/extension/more-menu.js +109 -0
- package/dist/extension/move-dialog.js +197 -0
- package/dist/extension/offscreen.js +37 -0
- package/dist/extension/options.js +504 -0
- package/dist/extension/page-heading.js +170 -0
- package/dist/extension/page-links.js +199 -0
- package/dist/extension/parse-check-client.js +70 -0
- package/dist/extension/parse-check.js +161 -0
- package/dist/extension/parse-sandbox-host.js +172 -0
- package/dist/extension/parse-sandbox.js +175 -0
- package/dist/extension/popup-commands-open.js +41 -0
- package/dist/extension/popup-view.js +387 -0
- package/dist/extension/popup.js +703 -0
- package/dist/extension/prism-userscript.js +53 -0
- package/dist/extension/refresh-error.js +136 -0
- package/dist/extension/refresh-targets.js +160 -0
- package/dist/extension/related-versions.js +1144 -0
- package/dist/extension/resize-observer-loop.js +19 -0
- package/dist/extension/row-cache.js +387 -0
- package/dist/extension/script-blobs.js +413 -0
- package/dist/extension/script-details.js +658 -0
- package/dist/extension/script-enablement.js +285 -0
- package/dist/extension/script-identity.js +310 -0
- package/dist/extension/script-keys.js +54 -0
- package/dist/extension/script-log-id.js +155 -0
- package/dist/extension/script-log.js +290 -0
- package/dist/extension/script-rejection.js +104 -0
- package/dist/extension/script-sites.js +158 -0
- package/dist/extension/script-store.js +3507 -0
- package/dist/extension/script-updater.js +991 -0
- package/dist/extension/script-url-hash.js +61 -0
- package/dist/extension/select-all.js +37 -0
- package/dist/extension/select-open-guard.js +90 -0
- package/dist/extension/site-icon.js +1261 -0
- package/dist/extension/sm-dev-link.js +302 -0
- package/dist/extension/solo-click.js +58 -0
- package/dist/extension/sticky-numbers.js +188 -0
- package/dist/extension/storage-breakdown.js +253 -0
- package/dist/extension/storage-scan.js +86 -0
- package/dist/extension/storage-usage.js +194 -0
- package/dist/extension/structural-keys.js +29 -0
- package/dist/extension/sw-teardown.js +36 -0
- package/dist/extension/tab-status.js +403 -0
- package/dist/extension/toolbar-summary.js +105 -0
- package/dist/extension/user-scripts-access.js +43 -0
- package/dist/extension/userscript.js +3672 -0
- package/dist/extension/value-store.js +204 -0
- package/dist/extension/version-probe-fetch.js +68 -0
- package/dist/extension/worker-client.js +109 -0
- package/dist/extension/worker-ops.js +171 -0
- package/dist/extension/zip-writer.js +24 -0
- package/dist/external-files.js +878 -0
- package/dist/file-loader.js +436 -0
- package/dist/file-locations.js +439 -0
- package/dist/generated-wrappers.js +5 -0
- package/dist/github-auth.js +148 -0
- package/dist/github-urls.js +46 -0
- package/dist/gm-api-catalog.js +217 -0
- package/dist/gm-connect.js +273 -0
- package/dist/gm-xhr-types.js +5 -0
- package/dist/http-read.js +142 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/local-file-overrides.js +558 -0
- package/dist/manifest.json +38 -0
- package/dist/markup-response.js +126 -0
- package/dist/match-targets.js +305 -0
- package/dist/menu-captions.js +22 -0
- package/dist/offscreen.html +39 -0
- package/dist/options.html +197 -0
- package/dist/parse-check-protocol.js +51 -0
- package/dist/parse-location.js +171 -0
- package/dist/parse-sandbox.html +56 -0
- package/dist/phase-timing.js +155 -0
- package/dist/popup.html +197 -0
- package/dist/public-suffixes.js +6957 -0
- package/dist/run-context.js +31 -0
- package/dist/runtime-messages.js +34 -0
- package/dist/sandboxed-page.js +35 -0
- package/dist/script-build.js +787 -0
- package/dist/script-display.js +383 -0
- package/dist/script-icon.js +43 -0
- package/dist/script-links.js +204 -0
- package/dist/script-source-url.js +214 -0
- package/dist/script-world.js +65 -0
- package/dist/sm-dev-protocol.js +84 -0
- package/dist/source-map.js +177 -0
- package/dist/stack-link.js +138 -0
- package/dist/styles.css +825 -0
- package/dist/unsafe-window-use.js +229 -0
- package/dist/url-match.js +734 -0
- package/dist/userscript-header.js +248 -0
- package/dist/userscript.html +1165 -0
- package/dist/vendor/fflate.LICENSE +21 -0
- package/dist/vendor/fflate.js +2692 -0
- package/dist/vendor/prism/prism-line-numbers.css +1 -0
- package/dist/vendor/prism/prism-line-numbers.js +1 -0
- package/dist/vendor/prism/prism.css +1 -0
- package/dist/vendor/prism/prism.js +1946 -0
- package/dist/version-compare.js +141 -0
- package/dist/version-probe.js +120 -0
- package/lib/build-info.json +1 -0
- package/lib/bytes.d.ts +3 -0
- package/lib/bytes.js +37 -0
- package/lib/channel-placeholder.d.ts +1 -0
- package/lib/channel-placeholder.js +5 -0
- package/lib/collection-scan.d.ts +8 -0
- package/lib/collection-scan.js +17 -0
- package/lib/composed-source.d.ts +28 -0
- package/lib/composed-source.js +205 -0
- package/lib/debug-log.d.ts +9 -0
- package/lib/debug-log.js +75 -0
- package/lib/diagnostic-location.d.ts +8 -0
- package/lib/diagnostic-location.js +64 -0
- package/lib/eval-hook.d.ts +34 -0
- package/lib/eval-hook.js +308 -0
- package/lib/external-files.d.ts +111 -0
- package/lib/external-files.js +878 -0
- package/lib/file-loader.d.ts +48 -0
- package/lib/file-loader.js +436 -0
- package/lib/file-locations.d.ts +44 -0
- package/lib/file-locations.js +439 -0
- package/lib/generated-wrappers.d.ts +11 -0
- package/lib/generated-wrappers.js +5 -0
- package/lib/github-auth.d.ts +12 -0
- package/lib/github-auth.js +148 -0
- package/lib/github-urls.d.ts +1 -0
- package/lib/github-urls.js +46 -0
- package/lib/gm-api-catalog.d.ts +24 -0
- package/lib/gm-api-catalog.js +217 -0
- package/lib/gm-connect.d.ts +34 -0
- package/lib/gm-connect.js +273 -0
- package/lib/gm-xhr-types.d.ts +68 -0
- package/lib/gm-xhr-types.js +5 -0
- package/lib/harness/browser-extensions.d.ts +73 -0
- package/lib/harness/browser-extensions.js +285 -0
- package/lib/harness/browser-lang.d.ts +20 -0
- package/lib/harness/browser-lang.js +86 -0
- package/lib/harness/browser-launch.d.ts +14 -0
- package/lib/harness/browser-launch.js +184 -0
- package/lib/harness/checks.d.ts +110 -0
- package/lib/harness/checks.js +321 -0
- package/lib/harness/cli.d.ts +2 -0
- package/lib/harness/cli.js +2259 -0
- package/lib/harness/command-args.d.ts +91 -0
- package/lib/harness/command-args.js +369 -0
- package/lib/harness/command-lines.d.ts +80 -0
- package/lib/harness/command-lines.js +180 -0
- package/lib/harness/command-list.d.ts +206 -0
- package/lib/harness/command-list.js +1475 -0
- package/lib/harness/console-commands.d.ts +463 -0
- package/lib/harness/console-commands.js +3216 -0
- package/lib/harness/download-watch.d.ts +21 -0
- package/lib/harness/download-watch.js +156 -0
- package/lib/harness/extension-backend.d.ts +41 -0
- package/lib/harness/extension-backend.js +100 -0
- package/lib/harness/extension-host.d.ts +112 -0
- package/lib/harness/extension-host.js +552 -0
- package/lib/harness/extension-launch.d.ts +67 -0
- package/lib/harness/extension-launch.js +285 -0
- package/lib/harness/extension-link.d.ts +315 -0
- package/lib/harness/extension-link.js +607 -0
- package/lib/harness/external-manager.d.ts +146 -0
- package/lib/harness/external-manager.js +414 -0
- package/lib/harness/fixture.d.ts +165 -0
- package/lib/harness/fixture.js +461 -0
- package/lib/harness/frame-sessions.d.ts +71 -0
- package/lib/harness/frame-sessions.js +248 -0
- package/lib/harness/frames.d.ts +68 -0
- package/lib/harness/frames.js +216 -0
- package/lib/harness/help-format.d.ts +17 -0
- package/lib/harness/help-format.js +127 -0
- package/lib/harness/host.d.ts +352 -0
- package/lib/harness/host.js +423 -0
- package/lib/harness/index.d.ts +8 -0
- package/lib/harness/index.js +10 -0
- package/lib/harness/input-file.d.ts +18 -0
- package/lib/harness/input-file.js +61 -0
- package/lib/harness/json-output.d.ts +565 -0
- package/lib/harness/json-output.js +71 -0
- package/lib/harness/manager-actions.d.ts +21 -0
- package/lib/harness/manager-actions.js +129 -0
- package/lib/harness/manager-commands.d.ts +142 -0
- package/lib/harness/manager-commands.js +1196 -0
- package/lib/harness/manager-fetch.d.ts +33 -0
- package/lib/harness/manager-fetch.js +239 -0
- package/lib/harness/managers.d.ts +24 -0
- package/lib/harness/managers.js +27 -0
- package/lib/harness/output-style.d.ts +125 -0
- package/lib/harness/output-style.js +322 -0
- package/lib/harness/page-commands.d.ts +134 -0
- package/lib/harness/page-commands.js +1141 -0
- package/lib/harness/page-output.d.ts +67 -0
- package/lib/harness/page-output.js +139 -0
- package/lib/harness/page-watch.d.ts +171 -0
- package/lib/harness/page-watch.js +607 -0
- package/lib/harness/parse-check-node.d.ts +3 -0
- package/lib/harness/parse-check-node.js +132 -0
- package/lib/harness/path-args.d.ts +22 -0
- package/lib/harness/path-args.js +148 -0
- package/lib/harness/playwright-chromium.d.ts +16 -0
- package/lib/harness/playwright-chromium.js +86 -0
- package/lib/harness/prepare.d.ts +168 -0
- package/lib/harness/prepare.js +364 -0
- package/lib/harness/prompt-editor.d.ts +17 -0
- package/lib/harness/prompt-editor.js +354 -0
- package/lib/harness/prompt-history.d.ts +13 -0
- package/lib/harness/prompt-history.js +89 -0
- package/lib/harness/rebuild-advice.d.ts +6 -0
- package/lib/harness/rebuild-advice.js +39 -0
- package/lib/harness/run-log.d.ts +47 -0
- package/lib/harness/run-log.js +140 -0
- package/lib/harness/run-logs.d.ts +132 -0
- package/lib/harness/run-logs.js +241 -0
- package/lib/harness/run.d.ts +194 -0
- package/lib/harness/run.js +1973 -0
- package/lib/harness/runs.d.ts +229 -0
- package/lib/harness/runs.js +726 -0
- package/lib/harness/save-html.d.ts +39 -0
- package/lib/harness/save-html.js +89 -0
- package/lib/harness/saved-pages.d.ts +120 -0
- package/lib/harness/saved-pages.js +275 -0
- package/lib/harness/script-mark.d.ts +17 -0
- package/lib/harness/script-mark.js +56 -0
- package/lib/harness/snapshot.d.ts +143 -0
- package/lib/harness/snapshot.js +474 -0
- package/lib/harness/stuck-tabs.d.ts +19 -0
- package/lib/harness/stuck-tabs.js +131 -0
- package/lib/harness/tab-refs.d.ts +20 -0
- package/lib/harness/tab-refs.js +80 -0
- package/lib/harness/tampermonkey.d.ts +71 -0
- package/lib/harness/tampermonkey.js +356 -0
- package/lib/harness/tm-ids.d.ts +18 -0
- package/lib/harness/tm-ids.js +88 -0
- package/lib/harness/violentmonkey.d.ts +34 -0
- package/lib/harness/violentmonkey.js +162 -0
- package/lib/harness/wait.d.ts +77 -0
- package/lib/harness/wait.js +458 -0
- package/lib/http-read.d.ts +30 -0
- package/lib/http-read.js +142 -0
- package/lib/local-file-overrides.d.ts +35 -0
- package/lib/local-file-overrides.js +558 -0
- package/lib/manifest.json +38 -0
- package/lib/markup-response.d.ts +11 -0
- package/lib/markup-response.js +126 -0
- package/lib/match-targets.d.ts +18 -0
- package/lib/match-targets.js +305 -0
- package/lib/menu-captions.d.ts +1 -0
- package/lib/menu-captions.js +22 -0
- package/lib/parse-check-protocol.d.ts +27 -0
- package/lib/parse-check-protocol.js +51 -0
- package/lib/parse-location.d.ts +12 -0
- package/lib/parse-location.js +171 -0
- package/lib/phase-timing.d.ts +12 -0
- package/lib/phase-timing.js +155 -0
- package/lib/public-suffixes.d.ts +1 -0
- package/lib/public-suffixes.js +6957 -0
- package/lib/run-context.d.ts +2 -0
- package/lib/run-context.js +31 -0
- package/lib/runtime-messages.d.ts +10 -0
- package/lib/runtime-messages.js +34 -0
- package/lib/sandboxed-page.d.ts +5 -0
- package/lib/sandboxed-page.js +35 -0
- package/lib/script-build.d.ts +111 -0
- package/lib/script-build.js +787 -0
- package/lib/script-display.d.ts +51 -0
- package/lib/script-display.js +383 -0
- package/lib/script-icon.d.ts +16 -0
- package/lib/script-icon.js +43 -0
- package/lib/script-links.d.ts +24 -0
- package/lib/script-links.js +204 -0
- package/lib/script-source-url.d.ts +8 -0
- package/lib/script-source-url.js +214 -0
- package/lib/script-world.d.ts +6 -0
- package/lib/script-world.js +65 -0
- package/lib/sm-dev-protocol.d.ts +251 -0
- package/lib/sm-dev-protocol.js +84 -0
- package/lib/source-map.d.ts +32 -0
- package/lib/source-map.js +177 -0
- package/lib/stack-link.d.ts +15 -0
- package/lib/stack-link.js +138 -0
- package/lib/unsafe-window-use.d.ts +1 -0
- package/lib/unsafe-window-use.js +229 -0
- package/lib/url-match.d.ts +40 -0
- package/lib/url-match.js +734 -0
- package/lib/userscript-header.d.ts +33 -0
- package/lib/userscript-header.js +248 -0
- package/lib/version-compare.d.ts +6 -0
- package/lib/version-compare.js +141 -0
- package/lib/version-probe.d.ts +17 -0
- package/lib/version-probe.js +120 -0
- package/package.json +55 -3
- package/skills/SourceMonkey-dev/SKILL.md +868 -0
- package/skills/SourceMonkey-dev/references/cdp.md +293 -0
- package/skills/SourceMonkey-dev/references/commands.md +297 -0
- package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
- package/skills/SourceMonkey-dev/references/specs.md +46 -0
- package/skills/SourceMonkey-friction/SKILL.md +103 -0
- 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
|
+
}
|