@chrismessina/raycast-downloader 0.1.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 +140 -0
- package/dist/curl.d.ts +106 -0
- package/dist/curl.d.ts.map +1 -0
- package/dist/curl.js +264 -0
- package/dist/curl.js.map +1 -0
- package/dist/detach.d.ts +119 -0
- package/dist/detach.d.ts.map +1 -0
- package/dist/detach.js +439 -0
- package/dist/detach.js.map +1 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +81 -0
- package/dist/errors.js.map +1 -0
- package/dist/history.d.ts +153 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +405 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/lock.d.ts +48 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +314 -0
- package/dist/lock.js.map +1 -0
- package/dist/paths.d.ts +117 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +328 -0
- package/dist/paths.js.map +1 -0
- package/dist/progress.d.ts +43 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +111 -0
- package/dist/progress.js.map +1 -0
- package/dist/runner.bundle.js +1906 -0
- package/dist/runner.d.ts +17 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +381 -0
- package/dist/runner.js.map +1 -0
- package/dist/status.d.ts +179 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +510 -0
- package/dist/status.js.map +1 -0
- package/package.json +116 -0
- package/scripts/bundle-runner.mjs +113 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Messina
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# @chrismessina/raycast-downloader
|
|
2
|
+
|
|
3
|
+
Downloads for Raycast extensions that survive the window closing.
|
|
4
|
+
|
|
5
|
+
## The problem
|
|
6
|
+
|
|
7
|
+
Raycast unloads a command when the user presses Escape or pops back to root search. From the [lifecycle docs](https://developers.raycast.com/information/lifecycle):
|
|
8
|
+
|
|
9
|
+
> Any async work you kick off should not be relied on to keep running.
|
|
10
|
+
|
|
11
|
+
An in-flight stream to disk is torn down mid-write, leaving a truncated file. `no-view` mode does not help — it runs until its promise resolves, and the promise *is* the thing doing the downloading.
|
|
12
|
+
|
|
13
|
+
So the transfer runs in a **detached child process** that outlives the command, reporting through a status file on disk rather than through memory.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { startDownload, watchStatus } from "@chrismessina/raycast-downloader";
|
|
17
|
+
|
|
18
|
+
const ticket = await startDownload({
|
|
19
|
+
url: signedUrl,
|
|
20
|
+
outputPath: "/Users/me/Downloads/recording.mp4",
|
|
21
|
+
expectedBytes: 383713905,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// The user can dismiss Raycast here. The download continues.
|
|
25
|
+
|
|
26
|
+
watchStatus(ticket.id, {
|
|
27
|
+
onChange: (s) => console.log(s.bytesDownloaded, "/", s.totalBytes),
|
|
28
|
+
onSettled: (s) => console.log(s.state),
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Scope of the guarantee
|
|
33
|
+
|
|
34
|
+
**Survives:** Raycast dismissal, the command being unloaded, the parent process exiting.
|
|
35
|
+
|
|
36
|
+
**Does not survive** (without user action): machine sleep, power loss, unattended network drops. Partial files are always retained, so an interrupted transfer resumes via HTTP Range instead of starting over.
|
|
37
|
+
|
|
38
|
+
This is stated narrowly on purpose. Detached spawn solves parent-process-exit — the problem Raycast creates. It is not a download supervisor.
|
|
39
|
+
|
|
40
|
+
## Platforms
|
|
41
|
+
|
|
42
|
+
macOS and Windows. `curl` is used for the transfer and ships with both (Windows 10+ includes it at `System32\curl.exe`).
|
|
43
|
+
|
|
44
|
+
The supervision around the transfer differs, because the two platforms have genuinely different process models:
|
|
45
|
+
|
|
46
|
+
| | macOS / POSIX | Windows |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| Detach | `detached: true` + `unref()` | `unref()` only — `detached` there means "new console window" |
|
|
49
|
+
| Cancel | `process.kill(-pid)` on the process group | `taskkill /PID <pid> /T` walks the tree |
|
|
50
|
+
| Identity | `ps -o lstart` | PowerShell `Get-Process().StartTime`, falling back to `wmic` on older builds |
|
|
51
|
+
| Who records the outcome | the runner's SIGTERM handler | `killDownload`, since nothing catchable is delivered |
|
|
52
|
+
|
|
53
|
+
### When identity can't be established
|
|
54
|
+
|
|
55
|
+
Some environments can't resolve a process's start time at all. Two different questions then get two different answers, deliberately:
|
|
56
|
+
|
|
57
|
+
**"Is this download still running?"** → falls back to the runner's heartbeat. A process that hasn't written a status in 30 seconds (60 missed beats) is treated as dead. Biasing unconditionally toward "alive" would be worse than it sounds: a dead runner would never be reconciled, its partial never reaped, and a watcher would poll a stuck status forever.
|
|
58
|
+
|
|
59
|
+
**"Should I signal this pid?"** → refuses. Guessing wrong about liveness costs a mislabelled status; guessing wrong about identity kills an unrelated process tree. `killDownload` returns `false` and records a terminal status rather than signalling on an unproven pid.
|
|
60
|
+
|
|
61
|
+
`canVerifyProcessIdentity()` exposes which regime you're in.
|
|
62
|
+
|
|
63
|
+
## Two layers
|
|
64
|
+
|
|
65
|
+
**Layer A — transport** (`status`, `detach`, `curl`): for extensions downloading from a URL.
|
|
66
|
+
|
|
67
|
+
**Layer B — everything else** (`paths`, `errors`, `progress`, `history`): useful to any extension that puts a file on disk, however it got the bytes.
|
|
68
|
+
|
|
69
|
+
The split is deliberate. A tool that owns its own transport — a vendor CLI, say, which never exposes a URL, picks its own output path, and emits no progress — can consume Layer B without being forced through a URL-shaped API that does not fit it.
|
|
70
|
+
|
|
71
|
+
## Design notes
|
|
72
|
+
|
|
73
|
+
Each of these exists because the obvious implementation is wrong in a way that only shows up in production.
|
|
74
|
+
|
|
75
|
+
**Process identity is `(pid, startTime)`, never a bare pid.** macOS `kern.maxproc` is 16000, so pids get recycled. A liveness check of `kill(pid, 0)` alone answers "does *some* process have this pid" — and cancelling on that basis can `kill(-pid)` an unrelated process group.
|
|
76
|
+
|
|
77
|
+
**Credentials never touch argv.** `ps` is world-readable. Measured: a URL passed as a curl argument is visible to any process on the machine; written to a `0600` config file and passed by path as `curl -K <file>`, it is not. The config is unlinked as soon as curl's first output byte proves it has been read. Signed URLs are bearer credentials, so the config-file channel is a requirement rather than a preference.
|
|
78
|
+
|
|
79
|
+
**Timeouts are throughput-based, not wall-clock.** `--max-time` counts machine sleep against the budget, so a laptop closed for ten minutes guarantees a spurious failure on a healthy transfer. `--speed-limit`/`--speed-time` measure actual throughput and are sleep-tolerant.
|
|
80
|
+
|
|
81
|
+
**`.part` file plus atomic rename.** Cancelling a naive download leaves a truncated file that looks exactly like a successful one. Bytes land in `<outputPath>.part` and are renamed only after the size is verified.
|
|
82
|
+
|
|
83
|
+
**Status writes are atomic and polled, not watched.** `writeFileSync(tmp)` + `renameSync` means a reader never sees half-written JSON — but the rename replaces the inode, so an `fs.watch` bound to the original file silently stops firing. Hence polling.
|
|
84
|
+
|
|
85
|
+
**Liveness and throughput are separate signals.** `heartbeatAt` advances while the process lives; `lastByteAt` advances only when bytes move. Conflated, a hung-but-alive transfer reads as healthy forever.
|
|
86
|
+
|
|
87
|
+
**`finalizing` is a real state.** The runner writes `finalizing` → renames → writes `completed`. A crash between the rename and the completion write leaves a correct file on disk; reconciliation recognizes that rather than reporting a failure and prompting a needless re-download of hundreds of megabytes.
|
|
88
|
+
|
|
89
|
+
**curl's default meter, not `--progress-bar`.** The progress-bar mode emits only a percentage. The default meter carries real bytes, total, speed and ETA — verified against captured output, not assumed.
|
|
90
|
+
|
|
91
|
+
## Known limitations
|
|
92
|
+
|
|
93
|
+
Stated plainly so a consumer doesn't discover them the hard way.
|
|
94
|
+
|
|
95
|
+
**Leases are per-instance, not distributed.** `acquireLease` serializes adoption well enough for two windows of the same extension, but it is a read-then-write on a file, not a true compare-and-swap. Two processes racing within the same millisecond can both believe they won. The realistic case — a user opening a second window seconds later — is covered.
|
|
96
|
+
|
|
97
|
+
**Reusing an `id` while its download is still running starts a competing writer.** Despite "reuse it to resume", there is no active-status check. Reuse an id only after the previous transfer reached a terminal state.
|
|
98
|
+
|
|
99
|
+
**Schema evolution has no in-flight migration.** Readers reject a status whose `schema` they don't recognize. If a future version bumps it while a download from the old version is mid-flight, that transfer becomes invisible to watch/cancel/prune — it still completes, but nothing tracks it.
|
|
100
|
+
|
|
101
|
+
**Bundling.** `startDownload` resolves `runner.js` from `__dirname`. If a consumer inlines `detach.js` into a single bundle without copying `dist/runner.js` alongside, it throws "runner not found" — loudly, not silently. Keep the package external, or copy the runner into the bundle directory.
|
|
102
|
+
|
|
103
|
+
**`npm test` runs against the built `dist`** — the script builds first, so it is never stale, but invoking `node --test` directly is. The integration suite hits the real network; set `SKIP_INTEGRATION=1` to skip it.
|
|
104
|
+
|
|
105
|
+
## API
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
paths expandHome · isContained · resolveDirectory · uniquePath · sanitizeFilename
|
|
109
|
+
errors DownloadError · DownloadErrorCode · classifyHttpStatus · isDownloadError
|
|
110
|
+
progress formatBytes · formatSpeed · formatEta · formatProgressLine · createThrottle
|
|
111
|
+
history createDownloadHistory
|
|
112
|
+
status writeStatus · readStatus · listStatuses · watchStatus · pruneStatuses
|
|
113
|
+
isAlive · isStalled · isTerminal · acquireLease · releaseLease
|
|
114
|
+
detach startDownload · killDownload · reconcile · runnerPath
|
|
115
|
+
curl buildCurlConfig · parseCurlMeter · parseWriteOut · classifyCurlFailure
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Import from the root or from a subpath (`@chrismessina/raycast-downloader/paths`).
|
|
119
|
+
|
|
120
|
+
## Notes for consumers
|
|
121
|
+
|
|
122
|
+
**Never put a signed URL in `meta`.** It is persisted to the status file. Store an identifier you can re-resolve from instead.
|
|
123
|
+
|
|
124
|
+
**Retry policy is yours.** Consumers differ too much to share one: `DownloadError.retryable` gives you the signal, the loop stays in your code.
|
|
125
|
+
|
|
126
|
+
**`uniquePath` numbering starts where you say.** Existing extensions differ (`(1)` vs `(2)`); `startAt` preserves that, because unifying it silently renames files users already have.
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npm run build # tsc
|
|
132
|
+
npm test # node --test, includes live network integration tests
|
|
133
|
+
SKIP_INTEGRATION=1 npm test # unit tests only
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Zero runtime dependencies. `@raycast/api` is a peer, and is loaded lazily so the runner and the tests work outside a Raycast host.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
package/dist/curl.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* curl transport: config construction, progress parsing, exit classification.
|
|
3
|
+
*
|
|
4
|
+
* Why curl rather than Node's own `fetch`: the transfer has to outlive the
|
|
5
|
+
* Raycast command that started it. A detached Node process could do it, but
|
|
6
|
+
* curl already handles Range resume, redirects, retries and connection timeouts,
|
|
7
|
+
* and it is present on every macOS install.
|
|
8
|
+
*
|
|
9
|
+
* Two decisions here are load-bearing, both verified empirically:
|
|
10
|
+
*
|
|
11
|
+
* 1. The URL is passed in a 0600 CONFIG FILE, never on the command line.
|
|
12
|
+
* Signed URLs are bearer credentials; argv is world-readable via `ps`.
|
|
13
|
+
* Measured: with the URL as an argument it is visible in `ps`; via `-K` it
|
|
14
|
+
* is not.
|
|
15
|
+
* 2. Timeouts are THROUGHPUT-based (`--speed-limit`/`--speed-time`), not
|
|
16
|
+
* wall-clock (`--max-time`). `--max-time` counts machine sleep against the
|
|
17
|
+
* budget, so a laptop closed for ten minutes guarantees a spurious failure
|
|
18
|
+
* on an otherwise healthy transfer.
|
|
19
|
+
*/
|
|
20
|
+
import { DownloadError, type SignalName } from "./errors";
|
|
21
|
+
export declare function hasCurl(): boolean;
|
|
22
|
+
/** Bytes/sec below which a transfer is considered dead, sustained for `stallSeconds`. */
|
|
23
|
+
export declare const DEFAULT_SPEED_LIMIT_BYTES = 1024;
|
|
24
|
+
export declare const DEFAULT_STALL_SECONDS = 120;
|
|
25
|
+
export interface CurlConfigOptions {
|
|
26
|
+
url: string;
|
|
27
|
+
/** Destination. Callers should pass the `.part` path, not the final one. */
|
|
28
|
+
outputPath: string;
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
followRedirects?: boolean;
|
|
31
|
+
/** Continue a partial transfer via HTTP Range. */
|
|
32
|
+
resume?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Stall THRESHOLD, not a rate limit: if throughput stays below this for
|
|
35
|
+
* `stallSeconds`, curl aborts. Lowering it makes stall detection more
|
|
36
|
+
* forgiving; it does not slow the transfer down.
|
|
37
|
+
*/
|
|
38
|
+
speedLimitBytes?: number;
|
|
39
|
+
stallSeconds?: number;
|
|
40
|
+
connectTimeoutSeconds?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Cap the transfer rate in bytes/sec (curl `--limit-rate`). Genuinely slows
|
|
43
|
+
* the download — useful for tests and for not saturating a connection.
|
|
44
|
+
*/
|
|
45
|
+
limitRateBytes?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Absolute wall-clock cap. Deliberately optional and unset by default —
|
|
48
|
+
* see the note above about sleep.
|
|
49
|
+
*/
|
|
50
|
+
maxTimeSeconds?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the contents of a curl config file (`curl -K <file>`).
|
|
54
|
+
*
|
|
55
|
+
* Everything sensitive lives in this file, which the caller must create 0600 and
|
|
56
|
+
* delete once curl has started.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildCurlConfig(options: CurlConfigOptions): string;
|
|
59
|
+
export interface CurlProgress {
|
|
60
|
+
bytesDownloaded: number;
|
|
61
|
+
totalBytes?: number;
|
|
62
|
+
speedBytesPerSec?: number;
|
|
63
|
+
etaSeconds?: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Parse curl's default progress meter.
|
|
67
|
+
*
|
|
68
|
+
* Deliberately NOT `--progress-bar`: that mode emits only a percentage, which is
|
|
69
|
+
* why the existing Fetch extension's speed and ETA fields are hardcoded to zero
|
|
70
|
+
* and its speed UI has never rendered. The default meter carries real numbers:
|
|
71
|
+
*
|
|
72
|
+
* % Total % Received % Xferd Average Speed Time Time Time Current
|
|
73
|
+
* Dload Upload Total Spent Left Speed
|
|
74
|
+
* 13 365M 13 48.7M 0 0 8058k 0 0:00:46 0:00:06 0:00:40 8501k
|
|
75
|
+
*
|
|
76
|
+
* Returns the most recent complete sample, or null if none is present yet.
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseCurlMeter(buffer: string): CurlProgress | null;
|
|
79
|
+
/** Parse curl's abbreviated sizes: `1234`, `48.7M`, `365M`, `8058k`. */
|
|
80
|
+
export declare function parseCurlSize(field: string | undefined): number | undefined;
|
|
81
|
+
/** Parse `H:MM:SS`. curl prints `--:--:--` when it has no estimate. */
|
|
82
|
+
export declare function parseCurlDuration(field: string | undefined): number | undefined;
|
|
83
|
+
export interface CurlWriteOut {
|
|
84
|
+
sizeDownload?: number;
|
|
85
|
+
speedDownload?: number;
|
|
86
|
+
httpCode?: number;
|
|
87
|
+
}
|
|
88
|
+
/** Parse the trailing `write-out` block (size, speed, http_code — one per line). */
|
|
89
|
+
export declare function parseWriteOut(stdout: string): CurlWriteOut;
|
|
90
|
+
export interface ClassifyCurlInput {
|
|
91
|
+
exitCode: number | null;
|
|
92
|
+
signal?: SignalName | null;
|
|
93
|
+
httpCode?: number;
|
|
94
|
+
stderrTail?: string;
|
|
95
|
+
/** True when the local process deliberately terminated curl. */
|
|
96
|
+
cancelled?: boolean;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Turn a finished curl invocation into a typed error.
|
|
100
|
+
*
|
|
101
|
+
* HTTP status is consulted before the exit code: curl exits 22 for every 4xx/5xx
|
|
102
|
+
* under `fail-with-body`, and "403 Forbidden" is far more actionable than
|
|
103
|
+
* "curl exited 22".
|
|
104
|
+
*/
|
|
105
|
+
export declare function classifyCurlFailure(input: ClassifyCurlInput): DownloadError;
|
|
106
|
+
//# sourceMappingURL=curl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"curl.d.ts","sourceRoot":"","sources":["../src/curl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,aAAa,EAA0B,KAAK,UAAU,EAAsB,MAAM,UAAU,CAAC;AAetG,wBAAgB,OAAO,IAAI,OAAO,CAWjC;AAED,yFAAyF;AACzF,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAC9C,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CA+ClE;AAuBD,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA0BlE;AAED,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAW3E;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAQ/E;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAU1D;AAwBD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,iBAAiB,GAAG,aAAa,CAwB3E"}
|
package/dist/curl.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* curl transport: config construction, progress parsing, exit classification.
|
|
4
|
+
*
|
|
5
|
+
* Why curl rather than Node's own `fetch`: the transfer has to outlive the
|
|
6
|
+
* Raycast command that started it. A detached Node process could do it, but
|
|
7
|
+
* curl already handles Range resume, redirects, retries and connection timeouts,
|
|
8
|
+
* and it is present on every macOS install.
|
|
9
|
+
*
|
|
10
|
+
* Two decisions here are load-bearing, both verified empirically:
|
|
11
|
+
*
|
|
12
|
+
* 1. The URL is passed in a 0600 CONFIG FILE, never on the command line.
|
|
13
|
+
* Signed URLs are bearer credentials; argv is world-readable via `ps`.
|
|
14
|
+
* Measured: with the URL as an argument it is visible in `ps`; via `-K` it
|
|
15
|
+
* is not.
|
|
16
|
+
* 2. Timeouts are THROUGHPUT-based (`--speed-limit`/`--speed-time`), not
|
|
17
|
+
* wall-clock (`--max-time`). `--max-time` counts machine sleep against the
|
|
18
|
+
* budget, so a laptop closed for ten minutes guarantees a spurious failure
|
|
19
|
+
* on an otherwise healthy transfer.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.DEFAULT_STALL_SECONDS = exports.DEFAULT_SPEED_LIMIT_BYTES = void 0;
|
|
23
|
+
exports.hasCurl = hasCurl;
|
|
24
|
+
exports.buildCurlConfig = buildCurlConfig;
|
|
25
|
+
exports.parseCurlMeter = parseCurlMeter;
|
|
26
|
+
exports.parseCurlSize = parseCurlSize;
|
|
27
|
+
exports.parseCurlDuration = parseCurlDuration;
|
|
28
|
+
exports.parseWriteOut = parseWriteOut;
|
|
29
|
+
exports.classifyCurlFailure = classifyCurlFailure;
|
|
30
|
+
const errors_1 = require("./errors");
|
|
31
|
+
/**
|
|
32
|
+
* Is `curl` available on this system?
|
|
33
|
+
*
|
|
34
|
+
* Called before spawning so a missing binary is reported as a prerequisite the
|
|
35
|
+
* user can act on, rather than as a generic transport failure surfacing minutes
|
|
36
|
+
* after `startDownload` already reported success.
|
|
37
|
+
*
|
|
38
|
+
* Present by default on macOS and on Windows 10+ (`System32\curl.exe`), so this
|
|
39
|
+
* is a guard against unusual environments rather than a common path. Cached —
|
|
40
|
+
* the answer cannot change within a process's lifetime in any way that matters.
|
|
41
|
+
*/
|
|
42
|
+
let curlAvailable;
|
|
43
|
+
function hasCurl() {
|
|
44
|
+
if (curlAvailable !== undefined)
|
|
45
|
+
return curlAvailable;
|
|
46
|
+
try {
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
48
|
+
const { execFileSync } = require("node:child_process");
|
|
49
|
+
execFileSync("curl", ["--version"], { stdio: "ignore", timeout: 5000 });
|
|
50
|
+
curlAvailable = true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
curlAvailable = false;
|
|
54
|
+
}
|
|
55
|
+
return curlAvailable;
|
|
56
|
+
}
|
|
57
|
+
/** Bytes/sec below which a transfer is considered dead, sustained for `stallSeconds`. */
|
|
58
|
+
exports.DEFAULT_SPEED_LIMIT_BYTES = 1024;
|
|
59
|
+
exports.DEFAULT_STALL_SECONDS = 120;
|
|
60
|
+
/**
|
|
61
|
+
* Build the contents of a curl config file (`curl -K <file>`).
|
|
62
|
+
*
|
|
63
|
+
* Everything sensitive lives in this file, which the caller must create 0600 and
|
|
64
|
+
* delete once curl has started.
|
|
65
|
+
*/
|
|
66
|
+
function buildCurlConfig(options) {
|
|
67
|
+
const { url, outputPath, headers = {}, followRedirects = true, resume = false, speedLimitBytes = exports.DEFAULT_SPEED_LIMIT_BYTES, stallSeconds = exports.DEFAULT_STALL_SECONDS, connectTimeoutSeconds = 30, limitRateBytes, maxTimeSeconds, } = options;
|
|
68
|
+
const lines = [
|
|
69
|
+
`url = "${escapeConfigValue(url)}"`,
|
|
70
|
+
`output = "${escapeConfigValue(outputPath)}"`,
|
|
71
|
+
// `fail` (not `fail-with-body`): on a 4xx/5xx, curl must write NOTHING to
|
|
72
|
+
// the output file.
|
|
73
|
+
//
|
|
74
|
+
// With `fail-with-body`, an error response body lands in the `.part` file —
|
|
75
|
+
// measured: a 404 wrote 21 bytes of `{"error":"Not found"}`. Because the
|
|
76
|
+
// partial is deliberately retained for resume, the next attempt's
|
|
77
|
+
// `continue-at = -` would start AFTER those bytes, splicing an error
|
|
78
|
+
// document into the middle of the media. The file would then pass a
|
|
79
|
+
// size check and be published as complete. Silent corruption is far worse
|
|
80
|
+
// than losing an error body we never surface to the user anyway.
|
|
81
|
+
"fail",
|
|
82
|
+
`connect-timeout = ${connectTimeoutSeconds}`,
|
|
83
|
+
// Throughput-based stall detection: survives sleep, catches a dead socket.
|
|
84
|
+
`speed-limit = ${speedLimitBytes}`,
|
|
85
|
+
`speed-time = ${stallSeconds}`,
|
|
86
|
+
// Authoritative final numbers, parsed from stdout on exit.
|
|
87
|
+
'write-out = "\\n%{size_download}\\n%{speed_download}\\n%{http_code}\\n"',
|
|
88
|
+
];
|
|
89
|
+
if (followRedirects)
|
|
90
|
+
lines.push("location");
|
|
91
|
+
// `-C -` asks curl to work out the offset from the existing file.
|
|
92
|
+
if (resume)
|
|
93
|
+
lines.push("continue-at = -");
|
|
94
|
+
if (limitRateBytes !== undefined)
|
|
95
|
+
lines.push(`limit-rate = ${limitRateBytes}`);
|
|
96
|
+
if (maxTimeSeconds !== undefined)
|
|
97
|
+
lines.push(`max-time = ${maxTimeSeconds}`);
|
|
98
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
99
|
+
lines.push(`header = "${escapeConfigValue(`${name}: ${value}`)}"`);
|
|
100
|
+
}
|
|
101
|
+
return lines.join("\n") + "\n";
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Escape a value for curl's config quoting.
|
|
105
|
+
*
|
|
106
|
+
* Escaping quotes and backslashes is not sufficient on its own: curl's config
|
|
107
|
+
* format is line-oriented, so a raw newline inside a value ends the line and
|
|
108
|
+
* whatever follows is parsed as a fresh directive. Measured behavior is that
|
|
109
|
+
* curl rejects the resulting malformed URL rather than honoring the smuggled
|
|
110
|
+
* directive — but that is curl's parser saving us, not our own correctness.
|
|
111
|
+
*
|
|
112
|
+
* So control characters are rejected outright rather than escaped. A URL or
|
|
113
|
+
* header value containing a newline is malformed anyway; refusing it makes
|
|
114
|
+
* config injection structurally impossible instead of contingent on curl.
|
|
115
|
+
*/
|
|
116
|
+
function escapeConfigValue(value) {
|
|
117
|
+
// eslint-disable-next-line no-control-regex
|
|
118
|
+
if (/[\x00-\x1f\x7f]/.test(value)) {
|
|
119
|
+
throw new errors_1.DownloadError("validation", "Refusing a value containing control characters.");
|
|
120
|
+
}
|
|
121
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Parse curl's default progress meter.
|
|
125
|
+
*
|
|
126
|
+
* Deliberately NOT `--progress-bar`: that mode emits only a percentage, which is
|
|
127
|
+
* why the existing Fetch extension's speed and ETA fields are hardcoded to zero
|
|
128
|
+
* and its speed UI has never rendered. The default meter carries real numbers:
|
|
129
|
+
*
|
|
130
|
+
* % Total % Received % Xferd Average Speed Time Time Time Current
|
|
131
|
+
* Dload Upload Total Spent Left Speed
|
|
132
|
+
* 13 365M 13 48.7M 0 0 8058k 0 0:00:46 0:00:06 0:00:40 8501k
|
|
133
|
+
*
|
|
134
|
+
* Returns the most recent complete sample, or null if none is present yet.
|
|
135
|
+
*/
|
|
136
|
+
function parseCurlMeter(buffer) {
|
|
137
|
+
// Meter updates are separated by \r; take complete lines only.
|
|
138
|
+
const lines = buffer.split(/[\r\n]+/).filter((line) => line.trim().length > 0);
|
|
139
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
140
|
+
const fields = lines[i].trim().split(/\s+/);
|
|
141
|
+
// 12 columns; the first is a percentage, so require a leading integer.
|
|
142
|
+
if (fields.length < 12)
|
|
143
|
+
continue;
|
|
144
|
+
if (!/^\d+$/.test(fields[0]))
|
|
145
|
+
continue;
|
|
146
|
+
const totalBytes = parseCurlSize(fields[1]);
|
|
147
|
+
const bytesDownloaded = parseCurlSize(fields[3]);
|
|
148
|
+
if (bytesDownloaded === undefined)
|
|
149
|
+
continue;
|
|
150
|
+
const speedBytesPerSec = parseCurlSize(fields[6]);
|
|
151
|
+
const etaSeconds = parseCurlDuration(fields[10]);
|
|
152
|
+
return {
|
|
153
|
+
bytesDownloaded,
|
|
154
|
+
totalBytes: totalBytes && totalBytes > 0 ? totalBytes : undefined,
|
|
155
|
+
speedBytesPerSec,
|
|
156
|
+
etaSeconds,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
/** Parse curl's abbreviated sizes: `1234`, `48.7M`, `365M`, `8058k`. */
|
|
162
|
+
function parseCurlSize(field) {
|
|
163
|
+
if (!field)
|
|
164
|
+
return undefined;
|
|
165
|
+
const match = /^(\d+(?:\.\d+)?)([kKmMgGtT])?$/.exec(field.trim());
|
|
166
|
+
if (!match)
|
|
167
|
+
return undefined;
|
|
168
|
+
const value = parseFloat(match[1]);
|
|
169
|
+
if (!Number.isFinite(value))
|
|
170
|
+
return undefined;
|
|
171
|
+
const multipliers = { k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4 };
|
|
172
|
+
const suffix = match[2]?.toLowerCase();
|
|
173
|
+
return suffix ? Math.round(value * multipliers[suffix]) : value;
|
|
174
|
+
}
|
|
175
|
+
/** Parse `H:MM:SS`. curl prints `--:--:--` when it has no estimate. */
|
|
176
|
+
function parseCurlDuration(field) {
|
|
177
|
+
if (!field || field.includes("-"))
|
|
178
|
+
return undefined;
|
|
179
|
+
const parts = field.trim().split(":");
|
|
180
|
+
if (parts.length !== 3)
|
|
181
|
+
return undefined;
|
|
182
|
+
const [h, m, s] = parts.map((p) => parseInt(p, 10));
|
|
183
|
+
if ([h, m, s].some((n) => Number.isNaN(n)))
|
|
184
|
+
return undefined;
|
|
185
|
+
return h * 3600 + m * 60 + s;
|
|
186
|
+
}
|
|
187
|
+
/** Parse the trailing `write-out` block (size, speed, http_code — one per line). */
|
|
188
|
+
function parseWriteOut(stdout) {
|
|
189
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
190
|
+
if (lines.length < 3)
|
|
191
|
+
return {};
|
|
192
|
+
const [size, speed, code] = lines.slice(-3).map((line) => Number(line.trim()));
|
|
193
|
+
return {
|
|
194
|
+
sizeDownload: Number.isFinite(size) ? size : undefined,
|
|
195
|
+
speedDownload: Number.isFinite(speed) ? speed : undefined,
|
|
196
|
+
httpCode: Number.isFinite(code) ? code : undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* curl exit codes worth distinguishing.
|
|
201
|
+
* https://curl.se/libcurl/c/libcurl-errors.html
|
|
202
|
+
*/
|
|
203
|
+
const EXIT_CODES = {
|
|
204
|
+
6: { code: "dns", message: "Could not resolve host." },
|
|
205
|
+
7: { code: "network", message: "Failed to connect to the server." },
|
|
206
|
+
18: { code: "network", message: "Transfer ended early." },
|
|
207
|
+
22: { code: "http_client", message: "The server returned an error." },
|
|
208
|
+
23: { code: "disk_full", message: "Could not write the file — the disk may be full." },
|
|
209
|
+
26: { code: "permission", message: "Could not read from the local file." },
|
|
210
|
+
28: { code: "timeout", message: "The transfer timed out." },
|
|
211
|
+
33: { code: "network", message: "The server does not support resuming; restart the download." },
|
|
212
|
+
35: { code: "tls", message: "Could not establish a secure connection." },
|
|
213
|
+
36: { code: "integrity", message: "Could not resume — the partial file is unusable." },
|
|
214
|
+
47: { code: "network", message: "Too many redirects." },
|
|
215
|
+
52: { code: "network", message: "The server returned nothing." },
|
|
216
|
+
55: { code: "network", message: "Failed to send data to the server." },
|
|
217
|
+
56: { code: "network", message: "Connection lost during transfer." },
|
|
218
|
+
63: { code: "http_client", message: "The response exceeded the maximum allowed size." },
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Turn a finished curl invocation into a typed error.
|
|
222
|
+
*
|
|
223
|
+
* HTTP status is consulted before the exit code: curl exits 22 for every 4xx/5xx
|
|
224
|
+
* under `fail-with-body`, and "403 Forbidden" is far more actionable than
|
|
225
|
+
* "curl exited 22".
|
|
226
|
+
*/
|
|
227
|
+
function classifyCurlFailure(input) {
|
|
228
|
+
const { exitCode, signal, httpCode, stderrTail, cancelled } = input;
|
|
229
|
+
if (cancelled || signal === "SIGTERM" || signal === "SIGINT") {
|
|
230
|
+
return new errors_1.DownloadError("cancelled", "Download cancelled.", { exitCode, signal });
|
|
231
|
+
}
|
|
232
|
+
if (httpCode !== undefined && httpCode >= 400) {
|
|
233
|
+
const code = (0, errors_1.classifyHttpStatus)(httpCode);
|
|
234
|
+
return new errors_1.DownloadError(code, httpErrorMessage(httpCode), { httpStatus: httpCode, exitCode, signal });
|
|
235
|
+
}
|
|
236
|
+
if (exitCode !== null && EXIT_CODES[exitCode]) {
|
|
237
|
+
const { code, message } = EXIT_CODES[exitCode];
|
|
238
|
+
return new errors_1.DownloadError(code, message, { exitCode, signal, httpStatus: httpCode });
|
|
239
|
+
}
|
|
240
|
+
// Last resort: curl's own words are more useful than a bare number.
|
|
241
|
+
const detail = stderrTail?.trim().split("\n").pop()?.trim();
|
|
242
|
+
return new errors_1.DownloadError("unknown", detail ? `Download failed: ${detail}` : `Download failed (curl exit ${exitCode ?? "unknown"}).`, { exitCode, signal, httpStatus: httpCode });
|
|
243
|
+
}
|
|
244
|
+
function httpErrorMessage(status) {
|
|
245
|
+
switch (status) {
|
|
246
|
+
case 401:
|
|
247
|
+
return "Not authorized — the credentials were rejected.";
|
|
248
|
+
case 403:
|
|
249
|
+
return "Access denied. The link may have expired, or you may not have permission to download this.";
|
|
250
|
+
case 404:
|
|
251
|
+
return "The file was not found on the server.";
|
|
252
|
+
case 410:
|
|
253
|
+
return "The download link has expired.";
|
|
254
|
+
case 416:
|
|
255
|
+
return "Could not resume from the existing partial file.";
|
|
256
|
+
case 429:
|
|
257
|
+
return "Rate limited by the server. Try again shortly.";
|
|
258
|
+
default:
|
|
259
|
+
return status >= 500
|
|
260
|
+
? `The server is temporarily unavailable (HTTP ${status}).`
|
|
261
|
+
: `The server rejected the request (HTTP ${status}).`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=curl.js.map
|
package/dist/curl.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"curl.js","sourceRoot":"","sources":["../src/curl.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;;AAiBH,0BAWC;AAwCD,0CA+CC;AA2CD,wCA0BC;AAGD,sCAWC;AAGD,8CAQC;AASD,sCAUC;AAwCD,kDAwBC;AAlSD,qCAAsG;AAEtG;;;;;;;;;;GAUG;AACH,IAAI,aAAkC,CAAC;AAEvC,SAAgB,OAAO;IACrB,IAAI,aAAa,KAAK,SAAS;QAAE,OAAO,aAAa,CAAC;IACtD,IAAI,CAAC;QACH,8DAA8D;QAC9D,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,oBAAoB,CAAwC,CAAC;QAC9F,YAAY,CAAC,MAAM,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QACxE,aAAa,GAAG,IAAI,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,aAAa,GAAG,KAAK,CAAC;IACxB,CAAC;IACD,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,yFAAyF;AAC5E,QAAA,yBAAyB,GAAG,IAAI,CAAC;AACjC,QAAA,qBAAqB,GAAG,GAAG,CAAC;AA8BzC;;;;;GAKG;AACH,SAAgB,eAAe,CAAC,OAA0B;IACxD,MAAM,EACJ,GAAG,EACH,UAAU,EACV,OAAO,GAAG,EAAE,EACZ,eAAe,GAAG,IAAI,EACtB,MAAM,GAAG,KAAK,EACd,eAAe,GAAG,iCAAyB,EAC3C,YAAY,GAAG,6BAAqB,EACpC,qBAAqB,GAAG,EAAE,EAC1B,cAAc,EACd,cAAc,GACf,GAAG,OAAO,CAAC;IAEZ,MAAM,KAAK,GAAa;QACtB,UAAU,iBAAiB,CAAC,GAAG,CAAC,GAAG;QACnC,aAAa,iBAAiB,CAAC,UAAU,CAAC,GAAG;QAC7C,0EAA0E;QAC1E,mBAAmB;QACnB,EAAE;QACF,4EAA4E;QAC5E,yEAAyE;QACzE,kEAAkE;QAClE,qEAAqE;QACrE,oEAAoE;QACpE,0EAA0E;QAC1E,iEAAiE;QACjE,MAAM;QACN,qBAAqB,qBAAqB,EAAE;QAC5C,2EAA2E;QAC3E,iBAAiB,eAAe,EAAE;QAClC,gBAAgB,YAAY,EAAE;QAC9B,2DAA2D;QAC3D,yEAAyE;KAC1E,CAAC;IAEF,IAAI,eAAe;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC5C,kEAAkE;IAClE,IAAI,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAC1C,IAAI,cAAc,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,gBAAgB,cAAc,EAAE,CAAC,CAAC;IAC/E,IAAI,cAAc,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,cAAc,cAAc,EAAE,CAAC,CAAC;IAE7E,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,KAAK,CAAC,IAAI,CAAC,aAAa,iBAAiB,CAAC,GAAG,IAAI,KAAK,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;IACrE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,iBAAiB,CAAC,KAAa;IACtC,4CAA4C;IAC5C,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,sBAAa,CAAC,YAAY,EAAE,iDAAiD,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAC3D,CAAC;AASD;;;;;;;;;;;;GAYG;AACH,SAAgB,cAAc,CAAC,MAAc;IAC3C,+DAA+D;IAC/D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAE/E,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5C,uEAAuE;QACvE,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE;YAAE,SAAS;QACjC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAAE,SAAS;QAEvC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,MAAM,eAAe,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACjD,IAAI,eAAe,KAAK,SAAS;YAAE,SAAS;QAE5C,MAAM,gBAAgB,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAClD,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QAEjD,OAAO;YACL,eAAe;YACf,UAAU,EAAE,UAAU,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;YACjE,gBAAgB;YAChB,UAAU;SACX,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,wEAAwE;AACxE,SAAgB,aAAa,CAAC,KAAyB;IACrD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,KAAK,GAAG,gCAAgC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAClE,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAE7B,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAE9C,MAAM,WAAW,GAA2B,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;IAClG,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;IACvC,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AAClE,CAAC;AAED,uEAAuE;AACvE,SAAgB,iBAAiB,CAAC,KAAyB;IACzD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACpD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAEzC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACpD,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IAC7D,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAC/B,CAAC;AAQD,oFAAoF;AACpF,SAAgB,aAAa,CAAC,MAAc;IAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAEhC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/E,OAAO;QACL,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;QACtD,aAAa,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;QACzD,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;KACnD,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,GAAiE;IAC/E,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,yBAAyB,EAAE;IACtD,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,kCAAkC,EAAE;IACnE,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,uBAAuB,EAAE;IACzD,EAAE,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,+BAA+B,EAAE;IACrE,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,kDAAkD,EAAE;IACtF,EAAE,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,qCAAqC,EAAE;IAC1E,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,yBAAyB,EAAE;IAC3D,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,6DAA6D,EAAE;IAC/F,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,0CAA0C,EAAE;IACxE,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,kDAAkD,EAAE;IACtF,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,qBAAqB,EAAE;IACvD,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,8BAA8B,EAAE;IAChE,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,oCAAoC,EAAE;IACtE,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,kCAAkC,EAAE;IACpE,EAAE,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,iDAAiD,EAAE;CACxF,CAAC;AAWF;;;;;;GAMG;AACH,SAAgB,mBAAmB,CAAC,KAAwB;IAC1D,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAEpE,IAAI,SAAS,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC7D,OAAO,IAAI,sBAAa,CAAC,WAAW,EAAE,qBAAqB,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACrF,CAAC;IAED,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,IAAI,GAAG,EAAE,CAAC;QAC9C,MAAM,IAAI,GAAG,IAAA,2BAAkB,EAAC,QAAQ,CAAC,CAAC;QAC1C,OAAO,IAAI,sBAAa,CAAC,IAAI,EAAE,gBAAgB,CAAC,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACzG,CAAC;IAED,IAAI,QAAQ,KAAK,IAAI,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9C,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC/C,OAAO,IAAI,sBAAa,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;IACtF,CAAC;IAED,oEAAoE;IACpE,MAAM,MAAM,GAAG,UAAU,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC;IAC5D,OAAO,IAAI,sBAAa,CACtB,SAAS,EACT,MAAM,CAAC,CAAC,CAAC,oBAAoB,MAAM,EAAE,CAAC,CAAC,CAAC,8BAA8B,QAAQ,IAAI,SAAS,IAAI,EAC/F,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,CAC3C,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAc;IACtC,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,GAAG;YACN,OAAO,iDAAiD,CAAC;QAC3D,KAAK,GAAG;YACN,OAAO,4FAA4F,CAAC;QACtG,KAAK,GAAG;YACN,OAAO,uCAAuC,CAAC;QACjD,KAAK,GAAG;YACN,OAAO,gCAAgC,CAAC;QAC1C,KAAK,GAAG;YACN,OAAO,kDAAkD,CAAC;QAC5D,KAAK,GAAG;YACN,OAAO,gDAAgD,CAAC;QAC1D;YACE,OAAO,MAAM,IAAI,GAAG;gBAClB,CAAC,CAAC,+CAA+C,MAAM,IAAI;gBAC3D,CAAC,CAAC,yCAAyC,MAAM,IAAI,CAAC;IAC5D,CAAC;AACH,CAAC"}
|