From 47ea7e4215c0812160f5449bbf6bedeee83db431 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 16 Aug 2026 07:34:21 +0000 Subject: [PATCH 1/2] feat: port the shell tools to TypeScript, with types and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports all five commands out of profullstack/scripts: gh-prs, gh-prs-merge, gh-prs-fix-all, tcfeed and domainjson. The point is not the language. It is the two things bash was making expensive. Typed, validated responses. Every gh call went through `jq -r` into a string compare, and `jq -r '.mergeable'` on a response that never had the field prints the four characters `null` — which is not MERGEABLE, so a mergeable PR read as ineligible for a reason nobody wrote, and the failure was indistinguishable from a real verdict. Responses are now parsed once and validated by shape, with the offending field named. Tests. The originals had none, so verifying a change meant running it against live pull requests. 51 tests here stub the subprocess layer, so gh is never invoked and the suite finishes in under a second. gh-prs-merge keeps the --fix behaviour and its refusals: a conflict GitHub declines to merge is left alone with its message printed, a check that ran and failed is a result rather than an obstacle, and there is no --admin. Two structural rules the port enforces: Nothing under bin/ does work at import time. Every entry guards on isMain(import.meta.url) and anything testable lives in src/. That is not decorative — a test that imported bin/gh-prs-fix-all.ts to reach one pure function ran the tool, taking the suite from 60ms to 93 seconds and sweeping live pull requests with --fix implied. isMain resolves the realpath first. These install as symlinks, so argv[1] is the link while import.meta.url is its target; comparing raw reports "imported" for every installed command at once. Commands install as files on PATH rather than shell aliases, because the moshcode pit runs aliases with `zsh -c`, which reads neither ~/.zshrc nor ~/.zsh_aliases. install-links refuses to take over a name it does not own without --force, and refuses a real file even with it. Verified against the originals: gh-prs-merge dry run is byte-identical, and domainjson is set-identical across every record type, rdap key and axfr entry (only DNS round-robin ordering differs). Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 3 + README.md | 136 ++++- bin/domainjson.ts | 113 ++++ bin/gh-prs-fix-all.ts | 50 ++ bin/gh-prs-merge.ts | 122 ++++ bin/gh-prs.ts | 74 +++ bin/tcfeed.ts | 30 + package.json | 28 + pnpm-lock.yaml | 1208 +++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 9 + scripts/install-links.mjs | 134 ++++ src/args.ts | 115 ++++ src/domain.ts | 362 +++++++++++ src/exec.ts | 76 +++ src/fix-all.ts | 37 ++ src/format.ts | 107 ++++ src/gh.ts | 295 +++++++++ src/is-main.ts | 27 + src/prs-list.ts | 130 ++++ src/prs-merge.ts | 351 +++++++++++ src/tcfeed-launch.ts | 58 ++ test/format.test.ts | 221 +++++++ test/prs-merge.test.ts | 322 ++++++++++ tsconfig.json | 21 + 24 files changed, 4028 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100755 bin/domainjson.ts create mode 100755 bin/gh-prs-fix-all.ts create mode 100755 bin/gh-prs-merge.ts create mode 100755 bin/gh-prs.ts create mode 100755 bin/tcfeed.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 scripts/install-links.mjs create mode 100644 src/args.ts create mode 100644 src/domain.ts create mode 100644 src/exec.ts create mode 100644 src/fix-all.ts create mode 100644 src/format.ts create mode 100644 src/gh.ts create mode 100644 src/is-main.ts create mode 100644 src/prs-list.ts create mode 100644 src/prs-merge.ts create mode 100644 src/tcfeed-launch.ts create mode 100644 test/format.test.ts create mode 100644 test/prs-merge.test.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c45938 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*.log +.DS_Store diff --git a/README.md b/README.md index bbdd2e4..ba6ca4c 100644 --- a/README.md +++ b/README.md @@ -1 +1,135 @@ -# cli-tools \ No newline at end of file +# cli-tools + +Local command-line tools, in TypeScript, on PATH. + +Ported from the bash and JavaScript originals in +[`profullstack/scripts`](https://github.com/profullstack/scripts). The point of +the port is not the language — it is the two things bash was making expensive: + +- **Typed, validated responses.** Every `gh` call used to go through `jq -r` into + a string compare. `jq -r '.mergeable'` on a response that never had the field + prints the four characters `null`, which is not `MERGEABLE`, so a perfectly + mergeable PR read as ineligible *for a reason nobody wrote*. Now an + unrecognised field is named in an error instead of silently becoming a string. +- **Tests.** The originals had none. Verifying a change meant running it against + live pull requests, which is a poor place to discover you were wrong. + +## Commands + +| Command | What it does | +| --- | --- | +| `gh-prs` | List every open PR across the owners you name | +| `gh-prs-merge` | Sweep open PRs and squash-merge the ones genuinely ready | +| `gh-prs-fix-all` | Fix the open threatcrush-scan PRs that are broken because of us | +| `tcfeed` | Find repositories worth scanning, scan them, print a shortlist | +| `domainjson` | whois-style, JSON-first name lookup | + +## Install + +```bash +pnpm install +pnpm link # symlink bin/*.ts into ~/.local/bin +``` + +The names already exist in `~/.local/bin` pointing at `~/scripts/bin`, so a +plain run reports them as not-ours and changes nothing. To migrate: + +```bash +node scripts/install-links.mjs --dry-run --force # see what would move +node scripts/install-links.mjs --force # take them over +``` + +`--force` takes over a *symlink*. A real file of the same name is still +refused — clobbering someone's actual binary to install a convenience is not a +trade a script gets to make on its own. + +To go back: + +```bash +pnpm unlink # remove the ones we own +ln -sf ~/scripts/bin/gh-prs-merge ~/.local/bin/gh-prs-merge # and so on +``` + +## Aliases must be real executables + +These install as files on PATH rather than shell aliases or functions, and that +is load-bearing. + +The moshcode pit runs its aliases with `zsh -c `, and `zsh -c` is a +non-interactive shell: it reads neither `~/.zshrc` nor `~/.zsh_aliases`. A +function defined there is simply not there, so `/alias tcfeed "tcfeed"` in the +pit answered `command not found` while the identical word worked when typed at a +prompt. A file on PATH works from an interactive shell, from `zsh -c`, and from +the pit, because none of them have to have sourced anything first. + +Nothing should alias *to* these either. A function beats PATH, so a wrapper of +the same name silently shadows the file and the two drift apart. + +``` +/alias prs "gh-prs --orgs profullstack" +/alias merge "gh-prs-merge --orgs profullstack --apply --fix" +/alias merge-dry "gh-prs-merge --orgs profullstack" +/alias fixprs "gh-prs-fix-all" +/alias feed "tcfeed" +/alias whoisj "domainjson" +``` + +## `gh-prs-merge --fix` + +A skip is not always a verdict on the PR. Two PRs were once skipped as +`mergeStateStatus=UNSTABLE` purely because a check had not reported yet; nothing +was wrong with either, and both merged unchanged minutes later. + +`--fix` repairs a repairable skip **once**, then judges the PR again against the +identical rules. It requires `--apply`, because every repair writes. + +| Blocker | Repair | +| --- | --- | +| checks still running | Wait for them to settle, up to `--fix-wait` (default 600s) | +| `mergeStateStatus=BEHIND` | Ask GitHub to merge the base branch in | +| `mergeable=CONFLICTING` | Same request; succeeds when the base merely moved | + +What it will not do is as much of the design: + +- **A conflict GitHub declines to merge is left alone**, and the message it gave + is printed as `FIXME`. Resolving one means choosing between two authors' + intent, and a batch tool that guesses produces a merge nobody wrote and nobody + reviewed. +- **A check that ran and failed is a result, not an obstacle.** Retrying until it + passes is how a flaky suite becomes a green one that means nothing. +- **No `--admin`.** Branch protections stay enforced. + +## Nothing under `bin/` does work at import time + +Every entry point guards its side effects with `isMain(import.meta.url)`, and +anything worth testing lives in `src/`. + +This is not decorative. A test that imported `bin/gh-prs-fix-all.ts` to reach one +pure function *ran the tool*: the suite went from 60ms to 93 seconds and swept +live pull requests with `--fix` implied. The guard and the `src/` split are both +that lesson. + +The `realpath` in `isMain` matters too — these install as symlinks, so +`process.argv[1]` is the link while `import.meta.url` is its target. Comparing +them raw reports "imported" for every installed command, disabling all of them at +once. + +## Development + +```bash +pnpm test # vitest +pnpm typecheck # tsc --noEmit +``` + +Tests stub the subprocess layer rather than the network, so `gh` is never +invoked. The suite runs in well under a second; if it starts taking longer, +something is reaching the network that should not be. + +## Differences from the originals + +Deliberate, and small: + +- `gh-prs` prints `No open PRs found.` instead of a bare header row. +- `gh-prs-merge` adds `fixed=` to its summary line. +- `domainjson` output is unchanged in structure; DNS answers arrive in + round-robin order, so array ordering varies between runs of either version. diff --git a/bin/domainjson.ts b/bin/domainjson.ts new file mode 100755 index 0000000..4992cd3 --- /dev/null +++ b/bin/domainjson.ts @@ -0,0 +1,113 @@ +#!/usr/bin/env -S npx --yes tsx +/** + * domainjson — whois-style, JSON-first name lookup. + * + * One JSON object on stdout: + * + * { "name": ..., "rdap": {...} | "moshpit": {...}, "dns": {...} } + * + * Names ending in a Moshpit TLD (https://pit.moshcode.sh) are served from the + * registry API; everything else goes through the OpenRDAP CLI (`rdap`), whose + * flags are passed through unchanged except the output-format flags — the RDAP + * portion is always JSON. Either way, dig adds records, hosts, reverse, and + * per-nameserver AXFR attempts. + */ + +import { isMain } from '../src/is-main.ts'; +import { + DEFAULT_REGISTRY, + DEFAULT_TIMEOUT_MS, + dnsSection, + fetchPitTlds, + findRdapBinary, + moshpitSection, + parseDomainArgs, + rdapSection, +} from '../src/domain.ts'; + +const USAGE = `Usage: + domainjson [openrdap-args...] + domainjson --name example.com + domainjson --registry https://pit.moshcode.sh --timeout 4000 example.hacker + +The final non-flag argument is the name to look up. OpenRDAP flags +(-s/--server, -t/--type, -T/--timeout, -k, --bs-url, --cache-dir, -P/-C/-K, +...) are passed through unchanged; output-format flags (--text, --whois, +--raw, --json) are dropped and JSON is always forced. + +Options: + --registry URL Moshpit registry base URL (default: ${DEFAULT_REGISTRY}) + --timeout MS per-query timeout for HTTP and dig (default: ${DEFAULT_TIMEOUT_MS}) + --name NAME the name to look up (alternative to the positional) + -h, --help show this help +`; + +/** Errors are JSON too. A tool whose output is parsed should not switch shape. */ +function fail(message: string, code = 2): never { + process.stdout.write(`${JSON.stringify({ error: message })}\n`); + process.exit(code); +} + +if (isMain(import.meta.url)) { + const parsed = parseDomainArgs(process.argv.slice(2)); + + if (parsed.help) { + process.stdout.write(USAGE); + process.exit(0); + } + + if (parsed.error) { + if (parsed.error === 'no name given') process.stderr.write(USAGE); + fail(parsed.error); + } + + const { own, passthrough } = parsed; + const name = own.name!.toLowerCase(); + const out: Record = { name }; + + let moshpitData: Record | null = null; + let registryNote: string | null = null; + + try { + const tlds = await fetchPitTlds(own.registry, own.timeout); + const ending = name.includes('.') ? (name.split('.').pop() ?? null) : null; + if (ending && tlds.has(ending)) { + moshpitData = await moshpitSection(own.registry, name, own.timeout); + out.moshpit = moshpitData; + } + } catch (error) { + // Registry unreachable: carry on as a plain RDAP+DNS lookup, but say so + // rather than letting the absence of a moshpit section imply the name is + // simply not one. + registryNote = `moshpit registry unavailable: ${ + error instanceof Error ? error.message : String(error) + }`; + } + + if (!moshpitData) { + const binary = await findRdapBinary(); + const rdap = binary + ? await rdapSection(binary, passthrough, name, own.timeout) + : { + error: + 'openrdap CLI not found (looked for ~/go/bin/rdap, rdap, openrdap on PATH)', + }; + if (registryNote) rdap.note = registryNote; + out.rdap = rdap; + } + + const dns = await dnsSection(name, own.timeout, moshpitData); + out.dns = dns; + + const rdapOk = Boolean(out.rdap) && !(out.rdap as { error?: unknown }).error; + const moshpitOk = Boolean(out.moshpit); + const dnsOk = Object.keys(dns.records).length > 0 || dns.hosts.length > 0; + + if (!rdapOk && !moshpitOk && !dnsOk) { + out.error = 'every data source failed (moshpit, rdap, dns)'; + process.stdout.write(`${JSON.stringify(out, null, 2)}\n`); + process.exit(1); + } + + process.stdout.write(`${JSON.stringify(out, null, 2)}\n`); +} diff --git a/bin/gh-prs-fix-all.ts b/bin/gh-prs-fix-all.ts new file mode 100755 index 0000000..e3e229f --- /dev/null +++ b/bin/gh-prs-fix-all.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env -S npx --yes tsx +/** + * gh-prs-fix-all — look at every open threatcrush-scan pull request, and fix + * the ones that are broken because of us. + * + * gh-prs-fix-all # fix ours, report theirs, leave theirs alone + * gh-prs-fix-all --dry-run # change nothing, just say what stands + * gh-prs-fix-all owner/name ... # only these + * + * The name says fix-all and it will not fix all, which is deliberate. Pushing + * the branch to a fork sets off whatever the upstream repo runs on push, so + * their test suite goes red against a commit that only added files under + * .github/. Those failures are reported and never touched. Failures in our own + * workflow that it does not recognise are printed rather than guessed at: a + * speculative commit pushed onto a stranger's review is worse than red, + * because red is at least honest. + */ + +import { readFileSync } from 'node:fs'; +import { isMain } from '../src/is-main.ts'; +import { buildArgs, hasCheckSubcommand } from '../src/fix-all.ts'; +import { launch, missingScriptMessage, resolveScript } from '../src/tcfeed-launch.ts'; + +async function main(argv: readonly string[]): Promise { + const { repo, script, exists } = resolveScript(); + + if (!exists) { + process.stderr.write(`${missingScriptMessage('gh-prs-fix-all', repo, script)}\n`); + return 1; + } + + if (!hasCheckSubcommand(readFileSync(script, 'utf8'))) { + process.stderr.write( + [ + `gh-prs-fix-all: ${script} has no \`check\` subcommand.`, + ` git -C ${repo} pull # it is on master, this checkout is behind`, + ' Without this guard the old script would read `check` as a post', + ' count, fall back to 50, and go scan reddit instead.', + '', + ].join('\n'), + ); + return 1; + } + + return launch('gh-prs-fix-all', buildArgs(argv)); +} + +if (isMain(import.meta.url)) { + process.exitCode = await main(process.argv.slice(2)); +} diff --git a/bin/gh-prs-merge.ts b/bin/gh-prs-merge.ts new file mode 100755 index 0000000..55c44d6 --- /dev/null +++ b/bin/gh-prs-merge.ts @@ -0,0 +1,122 @@ +#!/usr/bin/env -S npx --yes tsx +/** + * gh-prs-merge — sweep open pull requests and squash-merge the ones that are + * genuinely ready. + * + * Dry run by default. `--apply` merges. `--fix` additionally repairs the + * blockers that are mechanical rather than substantive, then judges the PR + * again against the identical rules. + * + * gh-prs-merge --orgs profullstack + * gh-prs-merge --orgs profullstack --apply + * gh-prs-merge --orgs profullstack --apply --fix + */ + +import { csv, integer, parseArgs, UsageError } from '../src/args.ts'; +import { isMain } from '../src/is-main.ts'; +import { Gh } from '../src/gh.ts'; +import { defaults, render, renderSummary, sweep, type MergeOptions } from '../src/prs-merge.ts'; + +const USAGE = `Usage: + gh-prs-merge --orgs ORG1,ORG2 [--users USER1,USER2] [--limit N] [--apply] + gh-prs-merge --users USER1,USER2 [--limit N] [--apply] + +By default, this performs a dry run. + +Options: + --orgs ORG1,ORG2 Search repositories owned by these organizations + --users USER1,USER2 Search repositories owned by these personal accounts + --limit N Maximum PRs per owner; default 1000 + --apply Actually squash-merge eligible PRs + --allow-no-checks Also merge clean PRs that have no CI checks + --no-ready-drafts Ignore draft PRs instead of marking them ready + --fix Repair mechanical blockers, then re-evaluate + --fix-wait SECONDS How long --fix waits on running checks; default 600 + -h, --help Show this help + +Eligibility: + - PR is open, and not a draft (or was successfully marked ready) + - mergeable is MERGEABLE and mergeStateStatus is CLEAN + - at least one CI check exists, unless --allow-no-checks + - every check is pass or skipping + - the head commit has not changed when the merge is submitted + +Fixing (--fix): + A skip is not always a verdict on the PR. Some are this tool arriving at the + wrong moment. Requires --apply, since every repair writes. + + Repaired: + checks still running waits for them to settle, up to --fix-wait + mergeStateStatus=BEHIND asks GitHub to merge the base branch in + mergeable=CONFLICTING same request; it succeeds when the base merely + moved underneath the branch + + Never repaired: + A conflict GitHub declines to merge is left alone and its message printed. + Resolving one means choosing between two authors' intent. A check that ran + and failed is a result, not an obstacle. +`; + +async function main(argv: string[]): Promise { + const parsed = parseArgs(argv, { + boolean: ['--apply', '--allow-no-checks', '--ready-drafts', '--no-ready-drafts', '--fix', '--no-fix', '-h', '--help'], + string: ['--orgs', '--users', '--limit', '--fix-wait'], + }); + + if (parsed.flags.has('-h') || parsed.flags.has('--help')) { + process.stdout.write(USAGE); + return 0; + } + + const options: MergeOptions = { + orgs: csv(parsed.values, '--orgs'), + users: csv(parsed.values, '--users'), + limit: integer(parsed.values, '--limit', defaults.limit, { min: 1, max: 1000 }), + apply: parsed.flags.has('--apply'), + allowNoChecks: parsed.flags.has('--allow-no-checks'), + readyDrafts: !parsed.flags.has('--no-ready-drafts'), + fix: parsed.flags.has('--fix') && !parsed.flags.has('--no-fix'), + fixWaitMs: + integer(parsed.values, '--fix-wait', defaults.fixWaitMs / 1000, { max: 86_400 }) * 1000, + pollMs: defaults.pollMs, + }; + + if (options.orgs.length === 0 && options.users.length === 0) { + process.stderr.write(USAGE); + throw new UsageError('pass --orgs, --users, or both'); + } + + // A dry run that mutated every repairable PR is the one thing a dry run + // promises not to do. + if (options.fix && !options.apply) { + throw new UsageError('--fix requires --apply'); + } + + const summary = await sweep(options, new Gh(), (line) => { + const text = render(line); + if (line.kind === 'failed' || line.kind === 'fixme' || line.kind === 'warn') { + process.stderr.write(`${text}\n`); + } else { + process.stdout.write(`${text}\n`); + } + }); + + process.stdout.write(`${renderSummary(summary)}\n`); + return summary.failed > 0 ? 1 : 0; +} + +if (isMain(import.meta.url)) { + try { + process.exitCode = await main(process.argv.slice(2)); + } catch (error) { + if (error instanceof UsageError) { + process.stderr.write(`gh-prs-merge: ${error.message}\n`); + process.exitCode = 2; + } else { + process.stderr.write( + `gh-prs-merge: ${error instanceof Error ? error.message : String(error)}\n`, + ); + process.exitCode = 1; + } + } +} diff --git a/bin/gh-prs.ts b/bin/gh-prs.ts new file mode 100755 index 0000000..fde347e --- /dev/null +++ b/bin/gh-prs.ts @@ -0,0 +1,74 @@ +#!/usr/bin/env -S npx --yes tsx +/** + * gh-prs — list every open pull request across the owners you name. + * + * gh-prs --orgs profullstack,moshcoder,h4kr,infernetprotocol + * gh-prs --users ralyodio,devpreshy + * gh-prs --orgs profullstack --users ralyodio + */ + +import { csv, integer, parseArgs, UsageError } from '../src/args.ts'; +import { isMain } from '../src/is-main.ts'; +import { list } from '../src/prs-list.ts'; + +const USAGE = `Usage: + gh-prs --orgs ORG1,ORG2 [--users USER1,USER2] [--limit NUMBER] + gh-prs --users USER1,USER2 [--limit NUMBER] + +Options: + --orgs ORG1,ORG2 Search repositories owned by these organizations + --users USER1,USER2 Search repositories owned by these personal accounts + --limit NUMBER Maximum PRs per owner; default 1000 + --no-links Never emit terminal hyperlinks + -h, --help Show this help + +Examples: + gh-prs --orgs profullstack,moshcoder,h4kr,infernetprotocol + gh-prs --users ralyodio,devpreshy +`; + +async function main(argv: string[]): Promise { + const parsed = parseArgs(argv, { + boolean: ['--no-links', '-h', '--help'], + string: ['--orgs', '--users', '--limit'], + }); + + if (parsed.flags.has('-h') || parsed.flags.has('--help')) { + process.stdout.write(USAGE); + return 0; + } + + const orgs = csv(parsed.values, '--orgs'); + const users = csv(parsed.values, '--users'); + + if (orgs.length === 0 && users.length === 0) { + process.stderr.write(USAGE); + throw new UsageError('pass --orgs, --users, or both'); + } + + const options = { + orgs, + users, + limit: integer(parsed.values, '--limit', 1000, { min: 1, max: 1000 }), + ...(parsed.flags.has('--no-links') ? { hyperlinks: false } : {}), + }; + + process.stdout.write(`${await list(options)}\n`); + return 0; +} + +if (isMain(import.meta.url)) { + try { + process.exitCode = await main(process.argv.slice(2)); + } catch (error) { + if (error instanceof UsageError) { + process.stderr.write(`gh-prs: ${error.message}\n`); + process.exitCode = 2; + } else { + process.stderr.write( + `gh-prs: ${error instanceof Error ? error.message : String(error)}\n`, + ); + process.exitCode = 1; + } + } +} diff --git a/bin/tcfeed.ts b/bin/tcfeed.ts new file mode 100755 index 0000000..9047bcd --- /dev/null +++ b/bin/tcfeed.ts @@ -0,0 +1,30 @@ +#!/usr/bin/env -S npx --yes tsx +/** + * tcfeed — read the newest posts on a subreddit, find the repositories they + * link, scan each one, and print a shortlist worth reading. + * + * tcfeed # the 50 newest posts + * tcfeed 100 # more of them + * tcfeed --forget # look at everything again next time + * tcfeed pr owner/name [--dry-run] # install the scan workflow + * tcfeed check [--fix] # how are the open requests doing + * + * An executable rather than a shell function, and that is the whole point of + * it. The moshcode pit runs its aliases with `zsh -c `, and `zsh -c` + * is a non-interactive shell: it reads neither ~/.zshrc nor ~/.zsh_aliases, so + * a function defined there is simply not there. `/alias tcfeed "tcfeed"` in the + * pit answered `command not found` while the identical word worked when typed + * at a prompt. + * + * Nothing should alias to this either. A function beats PATH, so a wrapper of + * the same name silently shadows this file and the two drift apart. + * + * TCFEED_REPO where threatcrush is checked out + */ + +import { isMain } from '../src/is-main.ts'; +import { launch } from '../src/tcfeed-launch.ts'; + +if (isMain(import.meta.url)) { + process.exitCode = await launch('tcfeed', process.argv.slice(2)); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e21434 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "@profullstack/cli-tools", + "version": "0.1.0", + "private": true, + "description": "Local command-line tools, in TypeScript, exposed on PATH.", + "type": "module", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/profullstack/cli-tools.git" + }, + "engines": { + "node": ">=20" + }, + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc -p tsconfig.json --noEmit", + "link": "node scripts/install-links.mjs", + "unlink": "node scripts/install-links.mjs --remove" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..f13ebbc --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1208 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^22.10.2 + version: 22.20.1 + tsx: + specifier: ^4.19.2 + version: 4.23.12 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.20.1) + +packages: + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tsx@4.23.12: + resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + +snapshots: + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + + '@types/estree@1.0.9': {} + + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.20.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.20.1) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + assertion-error@2.0.1: {} + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fsevents@2.3.3: + optional: true + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.62.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.4 + '@rollup/rollup-android-arm64': 4.62.4 + '@rollup/rollup-darwin-arm64': 4.62.4 + '@rollup/rollup-darwin-x64': 4.62.4 + '@rollup/rollup-freebsd-arm64': 4.62.4 + '@rollup/rollup-freebsd-x64': 4.62.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 + '@rollup/rollup-linux-arm-musleabihf': 4.62.4 + '@rollup/rollup-linux-arm64-gnu': 4.62.4 + '@rollup/rollup-linux-arm64-musl': 4.62.4 + '@rollup/rollup-linux-loong64-gnu': 4.62.4 + '@rollup/rollup-linux-loong64-musl': 4.62.4 + '@rollup/rollup-linux-ppc64-gnu': 4.62.4 + '@rollup/rollup-linux-ppc64-musl': 4.62.4 + '@rollup/rollup-linux-riscv64-gnu': 4.62.4 + '@rollup/rollup-linux-riscv64-musl': 4.62.4 + '@rollup/rollup-linux-s390x-gnu': 4.62.4 + '@rollup/rollup-linux-x64-gnu': 4.62.4 + '@rollup/rollup-linux-x64-musl': 4.62.4 + '@rollup/rollup-openbsd-x64': 4.62.4 + '@rollup/rollup-openharmony-arm64': 4.62.4 + '@rollup/rollup-win32-arm64-msvc': 4.62.4 + '@rollup/rollup-win32-ia32-msvc': 4.62.4 + '@rollup/rollup-win32-x64-gnu': 4.62.4 + '@rollup/rollup-win32-x64-msvc': 4.62.4 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tsx@4.23.12: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + vite-node@2.1.9(@types/node@22.20.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.20.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@22.20.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.26 + rollup: 4.62.4 + optionalDependencies: + '@types/node': 22.20.1 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@22.20.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.20.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.20.1) + vite-node: 2.1.9(@types/node@22.20.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..ad0ee26 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,9 @@ +# pnpm 11 no longer reads the `pnpm` field in package.json, and an unapproved +# install script fails the install outright rather than merely warning. +# +# esbuild (vitest's bundler) ships prebuilt binaries that its install script +# puts in place. Approved one package at a time rather than blanket-allowed, so +# a new dependency wanting a shell at install time stays a decision someone +# makes on purpose. +allowBuilds: + esbuild: true diff --git a/scripts/install-links.mjs b/scripts/install-links.mjs new file mode 100644 index 0000000..e55375a --- /dev/null +++ b/scripts/install-links.mjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node +/** + * Symlink every bin/*.ts into ~/.local/bin, without the .ts suffix. + * + * Real executables on PATH, deliberately, and not shell aliases or functions. + * The moshcode pit runs its aliases with `zsh -c `, and `zsh -c` is a + * non-interactive shell: it reads neither ~/.zshrc nor ~/.zsh_aliases. A + * function defined there is simply not there, so `/alias tcfeed "tcfeed"` + * answered `command not found` while the identical word worked when typed at a + * prompt. A file on PATH works from an interactive shell, from `zsh -c`, and + * from the pit, because none of them have to have sourced anything first. + * + * node scripts/install-links.mjs # link + * node scripts/install-links.mjs --dry-run # say what it would do + * node scripts/install-links.mjs --force # take over links owned elsewhere + * node scripts/install-links.mjs --remove # unlink the ones we own + * + * These names already exist in ~/.local/bin pointing at ~/scripts/bin, so a + * plain run reports them as not-ours and changes nothing. --force takes over a + * *symlink*; a real file of that name is still refused, because clobbering + * someone's actual binary to install a convenience is not a trade this gets to + * make on its own. + */ + +import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, symlinkSync, unlinkSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(here, '..'); +const binDir = join(repoRoot, 'bin'); +const target = process.env.CLI_TOOLS_PREFIX ?? join(homedir(), '.local', 'bin'); + +const dryRun = process.argv.includes('--dry-run'); +const remove = process.argv.includes('--remove'); +const force = process.argv.includes('--force'); + +const commands = readdirSync(binDir) + .filter((entry) => entry.endsWith('.ts')) + .map((entry) => ({ name: entry.replace(/\.ts$/, ''), source: join(binDir, entry) })) + .sort((a, b) => a.name.localeCompare(b.name)); + +if (commands.length === 0) { + console.error('install-links: no bin/*.ts found'); + process.exit(1); +} + +if (!dryRun) mkdirSync(target, { recursive: true }); + +let changed = 0; +let skipped = 0; + +for (const { name, source } of commands) { + const link = join(target, name); + const existing = existsSync(link) || isBrokenLink(link); + + // Only ever touch a symlink that points into this repository. A real file of + // the same name is someone else's, and clobbering it to install a + // convenience is not a trade this script gets to make. + const ours = existing && isLinkInto(link, binDir); + + if (remove) { + if (!existing) continue; + if (!ours) { + console.warn(`SKIP ${link} — not a link into ${binDir}`); + skipped += 1; + continue; + } + console.log(`${dryRun ? 'WOULD-UNLINK' : 'UNLINK'} ${link}`); + if (!dryRun) unlinkSync(link); + changed += 1; + continue; + } + + if (existing && !ours) { + // A real file is never taken over, --force or not. + if (!force || !isSymlink(link)) { + console.warn( + `SKIP ${link} — ${isSymlink(link) ? 'points elsewhere; use --force' : 'is a real file'}`, + ); + skipped += 1; + continue; + } + console.log(`TAKEOVER ${link} — was -> ${readlinkSync(link)}`); + } + + if (existing && readlinkSync(link) === source) { + continue; + } + + console.log(`${dryRun ? 'WOULD-LINK' : 'LINK'} ${link} -> ${source}`); + if (!dryRun) { + if (existing) unlinkSync(link); + // The shebang only runs if the target is executable. + chmodSync(source, 0o755); + symlinkSync(source, link); + } + changed += 1; +} + +console.log( + `\n${remove ? 'Unlinked' : 'Linked'} ${changed} command(s) in ${target}` + + (skipped ? `, skipped ${skipped}` : ''), +); + +if (!remove && !process.env.PATH?.split(':').includes(target)) { + console.warn(`\nWARN: ${target} is not on PATH. Add it:\n export PATH="${target}:$PATH"`); +} + +function isSymlink(path) { + try { + return lstatSync(path).isSymbolicLink(); + } catch { + return false; + } +} + +function isBrokenLink(path) { + try { + return lstatSync(path).isSymbolicLink(); + } catch { + return false; + } +} + +function isLinkInto(path, directory) { + try { + if (!lstatSync(path).isSymbolicLink()) return false; + return resolve(dirname(path), readlinkSync(path)).startsWith(directory); + } catch { + return false; + } +} diff --git a/src/args.ts b/src/args.ts new file mode 100644 index 0000000..41fc963 --- /dev/null +++ b/src/args.ts @@ -0,0 +1,115 @@ +/** + * Argument parsing shared by every tool here. + * + * The bash originals hand-rolled a `while (($#))` loop per script, each + * supporting `--flag value` and `--flag=value` by writing both cases out. They + * drifted: one validated its numeric input, another accepted `--limit abc` and + * failed later inside an arithmetic expansion with a message naming neither + * the flag nor the value. + */ + +export class UsageError extends Error { + constructor(message: string) { + super(message); + this.name = 'UsageError'; + } +} + +export interface ParsedArgs { + flags: Set; + values: Map; + positional: string[]; +} + +export interface Spec { + /** Flags that take no value. */ + boolean?: readonly string[]; + /** Flags that require a value. */ + string?: readonly string[]; +} + +export function parseArgs(argv: readonly string[], spec: Spec): ParsedArgs { + const booleans = new Set(spec.boolean ?? []); + const strings = new Set(spec.string ?? []); + + const flags = new Set(); + const values = new Map(); + const positional: string[] = []; + + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]!; + + if (argument === '--') { + positional.push(...argv.slice(index + 1)); + break; + } + + if (!argument.startsWith('-')) { + positional.push(argument); + continue; + } + + const equals = argument.indexOf('='); + const name = equals === -1 ? argument : argument.slice(0, equals); + const inline = equals === -1 ? undefined : argument.slice(equals + 1); + + if (booleans.has(name)) { + if (inline !== undefined) { + throw new UsageError(`${name} does not take a value`); + } + flags.add(name); + continue; + } + + if (strings.has(name)) { + if (inline !== undefined) { + values.set(name, inline); + continue; + } + const next = argv[index + 1]; + // A following flag is a missing value, not the value. `--limit --apply` + // used to set limit to the string "--apply". + if (next === undefined || next.startsWith('-')) { + throw new UsageError(`${name} requires a value`); + } + values.set(name, next); + index += 1; + continue; + } + + throw new UsageError(`unknown option: ${name}`); + } + + return { flags, values, positional }; +} + +export function integer( + values: Map, + name: string, + fallback: number, + { min = 0, max = Number.MAX_SAFE_INTEGER }: { min?: number; max?: number } = {}, +): number { + const raw = values.get(name); + if (raw === undefined) return fallback; + + if (!/^\d+$/.test(raw)) { + throw new UsageError(`${name} must be a non-negative integer, got ${JSON.stringify(raw)}`); + } + + const parsed = Number(raw); + if (parsed < min || parsed > max) { + throw new UsageError(`${name} must be between ${min} and ${max}, got ${parsed}`); + } + + return parsed; +} + +/** Split `a,b , c` into `['a','b','c']`, dropping empties. */ +export function csv(values: Map, name: string): string[] { + const raw = values.get(name); + if (!raw) return []; + return raw + .split(',') + .map((item) => item.trim()) + .filter(Boolean); +} diff --git a/src/domain.ts b/src/domain.ts new file mode 100644 index 0000000..7c112a7 --- /dev/null +++ b/src/domain.ts @@ -0,0 +1,362 @@ +import { constants } from 'node:fs'; +import { access } from 'node:fs/promises'; +import net from 'node:net'; +import os from 'node:os'; +import path from 'node:path'; +import { run } from './exec.ts'; + +export const DEFAULT_REGISTRY = 'https://pit.moshcode.sh'; +export const DEFAULT_TIMEOUT_MS = 4000; +export const DIG = '/usr/bin/dig'; +export const RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS'] as const; + +export type RecordType = (typeof RECORD_TYPES)[number]; + +/** OpenRDAP flags producing non-JSON output; dropped because JSON is forced. */ +export const OUTPUT_FORMAT_FLAGS = new Set([ + '--text', '-w', '--whois', '-r', '--raw', '-j', '--json', +]); + +/** OpenRDAP flags that consume the next argument, so it is not the name. */ +export const VALUE_FLAGS = new Set([ + '-T', '--timeout', + '-s', '--server', + '-t', '--type', + '--cache-dir', '--bs-url', '--bs-ttl', + '-P', '--p12', + '-C', '--cert', + '-K', '--key', +]); + +export interface OwnOptions { + registry: string; + timeout: number; + name: string | null; +} + +export interface ParsedDomainArgs { + own: OwnOptions; + passthrough: string[]; + help?: boolean; + error?: string; +} + +/** + * Split our flags from OpenRDAP's. + * + * Unlike the shared `parseArgs`, this one must pass through flags it does not + * know, because most of them belong to another program. So it cannot reject an + * unknown option, and the price is that it has to know which foreign flags + * consume a value — otherwise `-s https://rdap.example` reads the URL as the + * name to look up. + */ +export function parseDomainArgs(argv: readonly string[]): ParsedDomainArgs { + const own: OwnOptions = { + registry: DEFAULT_REGISTRY, + timeout: DEFAULT_TIMEOUT_MS, + name: null, + }; + const passthrough: string[] = []; + const positionals: string[] = []; + + const bad = (message: string): ParsedDomainArgs => ({ own, passthrough, error: message }); + + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]!; + + if (argument === '-h' || argument === '--help') return { own, passthrough, help: true }; + + if (argument === '--registry' || argument === '--timeout' || argument === '--name') { + const value = argv[index + 1]; + index += 1; + if (value === undefined) return bad(`${argument} requires a value`); + + if (argument === '--registry') own.registry = value.replace(/\/+$/, ''); + else if (argument === '--name') own.name = value; + else { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + return bad('--timeout must be a positive integer (ms)'); + } + own.timeout = parsed; + } + continue; + } + + if (argument.startsWith('--registry=')) { + own.registry = argument.slice('--registry='.length).replace(/\/+$/, ''); + continue; + } + + if (argument.startsWith('--timeout=')) { + const parsed = Number.parseInt(argument.slice('--timeout='.length), 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + return bad('--timeout must be a positive integer (ms)'); + } + own.timeout = parsed; + continue; + } + + if (argument.startsWith('--name=')) { + own.name = argument.slice('--name='.length); + continue; + } + + const flagName = argument.startsWith('--') ? (argument.split('=')[0] ?? argument) : argument; + + if (OUTPUT_FORMAT_FLAGS.has(flagName)) continue; + + if (argument.startsWith('-')) { + passthrough.push(argument); + if (!argument.includes('=') && VALUE_FLAGS.has(flagName)) { + const value = argv[index + 1]; + index += 1; + if (value === undefined) return bad(`${argument} requires a value`); + passthrough.push(value); + } + continue; + } + + positionals.push(argument); + } + + // The final non-flag argument is the name; earlier positionals go to rdap. + if (!own.name && positionals.length > 0) own.name = positionals.pop() ?? null; + passthrough.push(...positionals); + + if (!own.name) return bad('no name given'); + + return { own, passthrough }; +} + +export async function fetchJson(url: string, timeoutMs: number): Promise> { + const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }); + if (!response.ok) throw new Error(`HTTP ${response.status} from ${url}`); + return (await response.json()) as Record; +} + +export async function dig(args: readonly string[], timeoutMs: number): Promise { + const seconds = Math.max(1, Math.ceil(timeoutMs / 1000)); + const result = await run(DIG, [`+time=${seconds}`, '+tries=1', ...args], { + timeoutMs: timeoutMs + 1000, + }); + if (result.code !== 0 && !result.stdout) { + throw new Error(result.stderr.trim() || `dig exited ${result.code}`); + } + return result.stdout; +} + +const lines = (text: string): string[] => + text.split('\n').map((line) => line.trim()).filter(Boolean); + +export async function digLines( + name: string, + type: string, + server: string | null, + timeoutMs: number, +): Promise { + const args = [...(server ? [`@${server}`, '-p', '5354'] : []), '+short', name, type]; + return lines(await dig(args, timeoutMs)); +} + +export function tcpOpen(port: number, timeoutMs: number): Promise { + return new Promise((resolve) => { + const socket = net.connect({ host: '127.0.0.1', port }); + const done = (open: boolean): void => { + socket.destroy(); + resolve(open); + }; + socket.setTimeout(timeoutMs); + socket.once('connect', () => done(true)); + socket.once('timeout', () => done(false)); + socket.once('error', () => done(false)); + }); +} + +export async function findRdapBinary(): Promise { + const candidates = [path.join(os.homedir(), 'go', 'bin', 'rdap'), 'rdap', 'openrdap']; + + for (const candidate of candidates) { + try { + if (candidate.includes(path.sep)) { + await access(candidate, constants.X_OK); + } else { + const found = await run('which', [candidate]); + if (found.code !== 0) continue; + } + return candidate; + } catch { + // keep looking + } + } + return null; +} + +/** + * Every Moshpit ending, paged. + * + * The registry paginates at 1000. Any failure means "treat as non-moshpit", + * which is why the caller wraps this rather than this swallowing errors: a + * registry outage should downgrade the lookup, not silently claim the name is + * not a Moshpit one. + */ +export async function fetchPitTlds(registry: string, timeoutMs: number): Promise> { + const first = await fetchJson(`${registry}/api/moshpit/tlds?limit=1000&offset=0`, timeoutMs); + const entries: unknown[] = [...((first.tlds as unknown[]) ?? [])]; + const total = typeof first.total === 'number' ? first.total : entries.length; + + const pages: Promise[] = []; + for (let offset = entries.length; offset < total; offset += 1000) { + pages.push( + fetchJson(`${registry}/api/moshpit/tlds?limit=1000&offset=${offset}`, timeoutMs).then( + (page) => { + entries.push(...((page.tlds as unknown[]) ?? [])); + }, + ), + ); + } + await Promise.all(pages); + + return new Set( + entries + .map((entry) => + typeof entry === 'string' ? entry : String((entry as { tld?: unknown })?.tld ?? ''), + ) + .filter(Boolean), + ); +} + +export async function moshpitSection( + registry: string, + name: string, + timeoutMs: number, +): Promise> { + const query = `name=${encodeURIComponent(name)}`; + const [resolved, pins] = await Promise.all([ + fetchJson(`${registry}/api/moshpit/resolve?${query}&records=1`, timeoutMs), + fetchJson(`${registry}/api/moshpit/pins?${query}`, timeoutMs).catch(() => null), + ]); + return { ...resolved, pins: (pins?.pins as unknown[]) ?? [] }; +} + +export async function rdapSection( + binary: string, + passthrough: readonly string[], + name: string, + timeoutMs: number, +): Promise> { + const result = await run(binary, [...passthrough, '--json', name], { + timeoutMs: Math.max(timeoutMs * 4, 30_000), + }); + + if (result.code !== 0) { + return { error: `openrdap failed: ${result.stderr.trim() || `exited ${result.code}`}` }; + } + + try { + return JSON.parse(result.stdout) as Record; + } catch (error) { + return { + error: `openrdap failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } +} + +export interface DnsSection { + records: Partial>; + hosts: string[]; + reverse: { address: string; ptr: string[] }[]; + axfr: { ns: string; transfer: string; lines?: number }[]; +} + +export async function dnsSection( + name: string, + timeoutMs: number, + moshpitData: Record | null, +): Promise { + const section: DnsSection = { records: {}, hosts: [], reverse: [], axfr: [] }; + + // Moshpit names resolve through the local bridge on 127.0.0.1:5354 when it + // is up; otherwise fall back to what the registry API returned. + const bridge = moshpitData ? await tcpOpen(5354, Math.min(timeoutMs, 1500)) : false; + const server = bridge ? '127.0.0.1' : null; + + if (moshpitData && !bridge) { + const records = moshpitData.records as Record | undefined; + if (records && typeof records === 'object') { + for (const type of RECORD_TYPES) { + const values = records[type] ?? records[type.toLowerCase()]; + if (Array.isArray(values) && values.length > 0) { + section.records[type] = values.map(String); + } + } + } + + const hosts = new Set(); + if (typeof moshpitData.target === 'string' && moshpitData.target) { + hosts.add(moshpitData.target); + } + for (const value of section.records.A ?? []) hosts.add(value); + for (const value of section.records.AAAA ?? []) hosts.add(value); + section.hosts = [...hosts]; + } else { + // One query per type, deliberately not ANY: authoritative servers are + // allowed to answer ANY with a minimal subset. + const answers = await Promise.all( + RECORD_TYPES.map(async (type) => { + try { + return [type, await digLines(name, type, server, timeoutMs)] as const; + } catch { + return [type, [] as string[]] as const; + } + }), + ); + + for (const [type, found] of answers) { + if (found.length > 0) section.records[type] = found; + } + + section.hosts = [ + ...new Set([...(section.records.A ?? []), ...(section.records.AAAA ?? [])]), + ]; + } + + section.reverse = await Promise.all( + section.hosts.map(async (address) => { + try { + const out = await dig( + [...(server ? [`@${server}`, '-p', '5354'] : []), '+short', '-x', address], + timeoutMs, + ); + return { address, ptr: lines(out) }; + } catch { + return { address, ptr: [] }; + } + }), + ); + + // AXFR against each authoritative nameserver; refusal is data, not failure. + for (const ns of section.records.NS ?? []) { + const host = ns.replace(/\.$/, ''); + const entry: { ns: string; transfer: string; lines?: number } = { + ns: host, + transfer: 'failed', + }; + try { + const out = await dig([`@${host}`, name, 'AXFR'], timeoutMs); + if (/status:\s*REFUSED/i.test(out)) { + entry.transfer = 'refused'; + } else if (/XFR size:/i.test(out)) { + entry.transfer = 'ok'; + entry.lines = out + .split('\n') + .filter((line) => line.trim() && !line.startsWith(';')).length; + } + } catch { + // stays "failed" + } + section.axfr.push(entry); + } + + return section; +} diff --git a/src/exec.ts b/src/exec.ts new file mode 100644 index 0000000..6c10f2a --- /dev/null +++ b/src/exec.ts @@ -0,0 +1,76 @@ +import { execFile } from 'node:child_process'; + +export interface RunResult { + code: number; + stdout: string; + stderr: string; +} + +export interface RunOptions { + /** Milliseconds before the child is killed. Default 120_000. */ + timeoutMs?: number; + env?: NodeJS.ProcessEnv; + cwd?: string; +} + +/** + * Run a command with an argv array and never a shell. + * + * The bash original interpolated PR URLs and branch names into command lines. + * Nothing there was attacker-controlled in practice, but the shape is the one + * that breaks on a branch called `feat/it's-fine` long before it breaks on + * anything malicious — and it breaks silently, because the shell splits the + * word and `gh` receives two arguments it does not recognise. + * + * A non-zero exit is a *result*, not an exception. `gh pr checks` exits 1 when + * a check is pending while still printing perfectly good JSON, and `gh pr + * update-branch` exits 1 on a conflict, which is a thing the caller wants to + * read rather than a thing that should unwind the run. + */ +export function run( + file: string, + args: readonly string[], + options: RunOptions = {}, +): Promise { + const { timeoutMs = 120_000, env, cwd } = options; + + return new Promise((resolve) => { + execFile( + file, + [...args], + { + timeout: timeoutMs, + // GitHub API responses are comfortably larger than the 1MB default, + // and truncation would surface as a JSON parse error blamed on gh. + maxBuffer: 64 * 1024 * 1024, + encoding: 'utf8', + ...(env ? { env } : {}), + ...(cwd ? { cwd } : {}), + }, + (error, stdout, stderr) => { + let code = 0; + + if (error) { + const withCode = error as NodeJS.ErrnoException & { code?: number | string }; + code = typeof withCode.code === 'number' ? withCode.code : 1; + + // ENOENT means the binary is missing, which is a setup problem and + // not something a caller can interpret from an exit code. + if (withCode.code === 'ENOENT') { + resolve({ + code: 127, + stdout: '', + stderr: `command not found: ${file}`, + }); + return; + } + } + + resolve({ code, stdout: stdout ?? '', stderr: stderr ?? '' }); + }, + ); + }); +} + +export const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/fix-all.ts b/src/fix-all.ts new file mode 100644 index 0000000..381f9a3 --- /dev/null +++ b/src/fix-all.ts @@ -0,0 +1,37 @@ +/** + * The parts of `gh-prs-fix-all` that are decisions rather than side effects. + * + * They live here, and not in `bin/`, for a reason that cost a real test run to + * learn: a `bin/` entry point does its work at import time, so a test that + * imported it to reach one pure function *ran the tool*. The suite went from + * 60ms to 93 seconds and swept live pull requests with `--fix` implied. + * + * Nothing under `bin/` should export anything a test wants. If a function is + * worth testing, it belongs here. + */ + +/** + * Refuse a checkout that predates the `check` subcommand. + * + * tcfeed reads its first argument as a post count and falls back to 50 when it + * is not a number, so an older script answers `check` by fetching reddit and + * scanning ten strangers' repositories — a long, rate-limited, entirely wrong + * thing to do in response to "fix my pull requests". + */ +export function hasCheckSubcommand(source: string): boolean { + return source.includes("argument === 'check'"); +} + +/** + * `check` reports and changes nothing; `--fix` makes it act. + * + * `--dry-run`/`-n` are consumed here rather than passed on, because reporting + * is already what `check` does without `--fix`. + */ +export function buildArgs(argv: readonly string[]): string[] { + const dryRun = argv.some((argument) => argument === '--dry-run' || argument === '-n'); + const passthrough = argv.filter( + (argument) => argument !== '--dry-run' && argument !== '-n', + ); + return ['check', ...passthrough, ...(dryRun ? [] : ['--fix'])]; +} diff --git a/src/format.ts b/src/format.ts new file mode 100644 index 0000000..1d03350 --- /dev/null +++ b/src/format.ts @@ -0,0 +1,107 @@ +/** + * Table and time formatting shared by the listing tools. + * + * These were a jq program piped into an awk program. Both were correct; both + * were also the reason nobody touched the output, because changing a column + * meant editing two languages that agree only by convention about which field + * is which. + */ + +const SECOND = 1; +const MINUTE = 60; +const HOUR = 3600; +const DAY = 86_400; +const WEEK = 604_800; +const MONTH = 2_629_800; +const YEAR = 31_557_600; + +/** `3 hours ago`, matching the jq original's thresholds exactly. */ +export function timeAgo(iso: string, now: Date = new Date()): string { + const then = Date.parse(iso); + if (Number.isNaN(then)) return 'unknown'; + + // Clamped at zero: a clock a second behind the server should read "0 seconds + // ago", never "-1 seconds ago". + const seconds = Math.max(0, (now.getTime() - then) / 1000); + + const scale = (unit: number, name: string): string => { + const count = Math.floor(seconds / unit); + return `${count} ${name}${count === 1 ? '' : 's'} ago`; + }; + + if (seconds < MINUTE) return scale(SECOND, 'second'); + if (seconds < HOUR) return scale(MINUTE, 'minute'); + if (seconds < DAY) return scale(HOUR, 'hour'); + if (seconds < WEEK) return scale(DAY, 'day'); + if (seconds < MONTH) return scale(WEEK, 'week'); + if (seconds < YEAR) return scale(MONTH, 'month'); + return scale(YEAR, 'year'); +} + +/** Collapse the whitespace that would otherwise break a row across lines. */ +export const clean = (value: unknown): string => String(value ?? '').replace(/[\t\r\n]+/g, ' '); + +export function truncate(value: string, length: number): string { + return value.length > length ? `${value.slice(0, length - 3)}...` : value; +} + +// Written as an escape rather than a literal 0x1B byte: the raw character is +// invisible in a diff and does not survive every copy-paste intact. +const ESC = '\u001b'; + +/** An OSC-8 terminal hyperlink. */ +export const hyperlink = (text: string, url: string): string => + `${ESC}]8;;${url}${ESC}\\${text}${ESC}]8;;${ESC}\\`; + +export interface TableOptions { + /** Column indices to wrap in a link to the row's `url`. */ + linkColumns?: readonly number[]; + /** Per-row link target, by row index (header is row 0). */ + urls?: readonly (string | undefined)[]; + hyperlinks?: boolean; + gap?: string; +} + +/** + * Pad columns to the widest cell, then join. + * + * Padding is measured on the *unlinked* text. An OSC-8 escape is zero-width on + * screen but a dozen characters to `String.length`, so padding the decorated + * cell throws every following column out by the length of a URL — which is how + * the awk version stayed correct: it padded first and decorated second. + */ +export function table(rows: readonly (readonly string[])[], options: TableOptions = {}): string { + const { linkColumns = [], urls = [], hyperlinks = false, gap = ' ' } = options; + + const widths: number[] = []; + for (const row of rows) { + row.forEach((cell, column) => { + widths[column] = Math.max(widths[column] ?? 0, cell.length); + }); + } + + return rows + .map((row, rowIndex) => { + const url = urls[rowIndex]; + const last = row.length - 1; + + return row + .map((cell, column) => { + const padded = column < last ? cell.padEnd(widths[column] ?? 0) : cell; + const decorate = + hyperlinks && rowIndex > 0 && url && linkColumns.includes(column); + // Trailing pad sits outside the link so the clickable region is the + // text, not the whitespace after it. + if (!decorate) return padded; + const trimmed = padded.trimEnd(); + return hyperlink(trimmed, url) + ' '.repeat(padded.length - trimmed.length); + }) + .join(gap) + .trimEnd(); + }) + .join('\n'); +} + +/** Terminal hyperlinks only make sense on a real terminal. */ +export const supportsHyperlinks = (stream: { isTTY?: boolean } = process.stdout): boolean => + Boolean(stream.isTTY) && (process.env.TERM ?? 'dumb') !== 'dumb'; diff --git a/src/gh.ts b/src/gh.ts new file mode 100644 index 0000000..4d4ee54 --- /dev/null +++ b/src/gh.ts @@ -0,0 +1,295 @@ +import { run, sleep, type RunResult } from './exec.ts'; + +/** + * A typed front door to the `gh` CLI. + * + * The bash originals piped every response through `jq -r` and compared the + * result to a string. That reads fine and fails badly: `jq -r '.mergeable'` on + * a response that never had the field prints the four characters `null`, which + * is not `MERGEABLE`, so the PR is reported ineligible for a reason nobody + * wrote. The failure is indistinguishable from a genuine verdict. + * + * So responses are parsed once, validated by shape, and any field that is + * missing or unrecognised is *named* in the error rather than silently + * becoming a string. + */ + +export class GhError extends Error { + constructor( + message: string, + readonly result?: RunResult, + ) { + super(message); + this.name = 'GhError'; + } +} + +/** Values GitHub documents for `mergeable`, plus the honest fallback. */ +export const MERGEABLE = ['MERGEABLE', 'CONFLICTING', 'UNKNOWN'] as const; +export type Mergeable = (typeof MERGEABLE)[number]; + +export const MERGE_STATE = [ + 'BEHIND', + 'BLOCKED', + 'CLEAN', + 'DIRTY', + 'DRAFT', + 'HAS_HOOKS', + 'UNKNOWN', + 'UNSTABLE', +] as const; +export type MergeState = (typeof MERGE_STATE)[number]; + +/** Buckets `gh pr checks --json bucket` reports. */ +export const BUCKET = ['pass', 'fail', 'pending', 'skipping', 'cancel'] as const; +export type Bucket = (typeof BUCKET)[number]; + +export interface PullRequest { + url: string; + title: string; + state: string; + isDraft: boolean; + mergeable: Mergeable; + mergeStateStatus: MergeState; + headRefOid: string; +} + +export interface Check { + name: string; + bucket: Bucket; +} + +function fail(field: string, value: unknown, where: string): never { + throw new GhError( + `${where}: unexpected value for ${field}: ${JSON.stringify(value)}`, + ); +} + +function asString(value: unknown, field: string, where: string): string { + if (typeof value !== 'string') fail(field, value, where); + return value; +} + +function asEnum( + allowed: readonly T[], + value: unknown, + field: string, + where: string, +): T { + if (typeof value !== 'string' || !allowed.includes(value as T)) { + fail(field, value, where); + } + return value as T; +} + +export function parsePullRequest(raw: unknown, where = 'gh pr view'): PullRequest { + if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) { + throw new GhError(`${where}: expected an object, got ${JSON.stringify(raw)}`); + } + + const record = raw as Record; + + if (typeof record.isDraft !== 'boolean') fail('isDraft', record.isDraft, where); + + return { + url: asString(record.url, 'url', where), + title: asString(record.title, 'title', where), + state: asString(record.state, 'state', where), + isDraft: record.isDraft, + mergeable: asEnum(MERGEABLE, record.mergeable, 'mergeable', where), + mergeStateStatus: asEnum( + MERGE_STATE, + record.mergeStateStatus, + 'mergeStateStatus', + where, + ), + headRefOid: asString(record.headRefOid, 'headRefOid', where), + }; +} + +export function parseChecks(raw: unknown, where = 'gh pr checks'): Check[] { + if (!Array.isArray(raw)) { + throw new GhError(`${where}: expected an array, got ${JSON.stringify(raw)}`); + } + + return raw.map((entry) => { + const record = entry as Record; + return { + name: asString(record.name, 'name', where), + bucket: asEnum(BUCKET, record.bucket, 'bucket', where), + }; + }); +} + +export interface GhOptions { + /** Swap in a fake for tests. */ + exec?: typeof run; +} + +export class Gh { + private readonly exec: typeof run; + + constructor(options: GhOptions = {}) { + this.exec = options.exec ?? run; + } + + private async call(args: readonly string[]): Promise { + // GH_PAGER=cat so a configured pager cannot block on a TTY that is not + // there. The bash version set this on every call site and missed none by + // luck rather than design. + return this.exec('gh', args, { + env: { ...process.env, GH_PAGER: 'cat', CLICOLOR: '0' }, + }); + } + + private async json( + args: readonly string[], + parse: (raw: unknown) => T, + { allowNonZero = false }: { allowNonZero?: boolean } = {}, + ): Promise { + const result = await this.call(args); + + // Some subcommands exit non-zero *and* print usable JSON — `gh pr checks` + // does exactly that whenever anything is pending or failing. Reading the + // exit code there would discard the answer we asked for. + if (result.code !== 0 && !allowNonZero) { + throw new GhError( + `gh ${args.join(' ')} exited ${result.code}: ${result.stderr.trim()}`, + result, + ); + } + + const text = result.stdout.trim(); + if (!text) { + throw new GhError(`gh ${args.join(' ')} printed no JSON`, result); + } + + let raw: unknown; + try { + raw = JSON.parse(text); + } catch { + throw new GhError( + `gh ${args.join(' ')} printed output that is not JSON: ${text.slice(0, 200)}`, + result, + ); + } + + return parse(raw); + } + + /** + * Read a PR, retrying while GitHub is still computing mergeability. + * + * `UNKNOWN` is not a state a PR rests in; it means "ask again". Treating it + * as a verdict is how a perfectly mergeable PR gets skipped for + * `mergeable=UNKNOWN` a second after it was opened. + */ + async pullRequest( + url: string, + { attempts = 5, awaitReady = false, delayMs = 2000 } = {}, + ): Promise { + let last: unknown; + + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + const pr = await this.json( + [ + 'pr', + 'view', + url, + '--json', + 'state,isDraft,mergeable,mergeStateStatus,headRefOid,title,url', + ], + (raw) => parsePullRequest(raw), + ); + + const settled = pr.mergeable !== 'UNKNOWN'; + const ready = !awaitReady || !pr.isDraft; + + if (settled && ready) return pr; + last = pr; + } catch (error) { + last = error; + } + + if (attempt < attempts) await sleep(delayMs); + } + + if (last instanceof Error) throw last; + if (last) return last as PullRequest; + throw new GhError(`could not read ${url}`); + } + + async checks(url: string): Promise { + try { + return await this.json( + ['pr', 'checks', url, '--json', 'bucket,name'], + (raw) => parseChecks(raw), + { allowNonZero: true }, + ); + } catch (error) { + // A PR with no checks at all makes `gh` print nothing rather than `[]`. + // That is "no checks", which the caller already has a rule for, and not + // an error worth aborting a sweep over. + if (error instanceof GhError && /printed no JSON/.test(error.message)) { + return []; + } + throw error; + } + } + + async searchPrs( + qualifier: 'org' | 'user', + owner: string, + { limit, includeDrafts }: { limit: number; includeDrafts: boolean }, + ): Promise<{ url: string; createdAt: string }[]> { + const args = [ + 'search', + 'prs', + `${qualifier}:${owner}`, + '--state=open', + '--archived=false', + '--sort=created', + '--order=asc', + `--limit=${limit}`, + '--json', + 'url,createdAt', + ]; + + if (!includeDrafts) args.push('--draft=false'); + + return this.json(args, (raw) => { + if (!Array.isArray(raw)) { + throw new GhError('gh search prs: expected an array'); + } + return raw.map((entry) => { + const record = entry as Record; + return { + url: asString(record.url, 'url', 'gh search prs'), + createdAt: asString(record.createdAt, 'createdAt', 'gh search prs'), + }; + }); + }); + } + + async ready(url: string): Promise { + return this.call(['pr', 'ready', url]); + } + + async updateBranch(url: string): Promise { + return this.call(['pr', 'update-branch', url]); + } + + /** + * Squash-merge, pinned to the head we judged. + * + * `--match-head-commit` is the whole safety property: between reading the + * checks and submitting the merge, someone can push. Without it the merge + * lands on a commit nothing verified. + * + * Deliberately no `--admin`. Branch protections stay enforced. + */ + async squashMerge(url: string, headSha: string): Promise { + return this.call(['pr', 'merge', url, '--squash', '--match-head-commit', headSha]); + } +} diff --git a/src/is-main.ts b/src/is-main.ts new file mode 100644 index 0000000..56c46c3 --- /dev/null +++ b/src/is-main.ts @@ -0,0 +1,27 @@ +import { realpathSync } from 'node:fs'; +import { pathToFileURL } from 'node:url'; + +/** + * Was this module executed, or merely imported? + * + * Every entry point under `bin/` guards its side effects with this. Without it + * an `import` of the module *runs the tool*, which is not a hypothetical: a + * test that imported `bin/gh-prs-fix-all.ts` to reach one pure function swept + * live pull requests with `--fix` implied and took the suite from 60ms to 93 + * seconds. + * + * The realpath matters. These commands are installed as symlinks in + * `~/.local/bin`, so `process.argv[1]` is the link while `import.meta.url` is + * the file it points at, and comparing them raw says "imported" for every + * installed command — disabling all of them at once. + */ +export function isMain(moduleUrl: string, argv: readonly string[] = process.argv): boolean { + const entry = argv[1]; + if (!entry) return false; + + try { + return pathToFileURL(realpathSync(entry)).href === moduleUrl; + } catch { + return pathToFileURL(entry).href === moduleUrl; + } +} diff --git a/src/prs-list.ts b/src/prs-list.ts new file mode 100644 index 0000000..6932041 --- /dev/null +++ b/src/prs-list.ts @@ -0,0 +1,130 @@ +import { Gh, GhError } from './gh.ts'; +import { clean, supportsHyperlinks, table, timeAgo, truncate } from './format.ts'; +import { run } from './exec.ts'; + +export interface SearchedPr { + url: string; + number: number; + title: string; + createdAt: string; + updatedAt: string; + repository: { nameWithOwner: string }; + author: { login: string } | null; +} + +export interface ListOptions { + orgs: string[]; + users: string[]; + limit: number; + hyperlinks?: boolean; + now?: Date; +} + +const HEADER = [ + 'ORG/USER', + 'REPOSITORY', + 'PR', + 'AUTHOR', + 'TITLE', + 'UPDATED', + 'LINK', +] as const; + +function parse(raw: unknown): SearchedPr[] { + if (!Array.isArray(raw)) throw new GhError('gh search prs: expected an array'); + + return raw.map((entry) => { + const record = entry as Record; + const repository = (record.repository ?? {}) as Record; + const author = record.author as Record | null | undefined; + + return { + url: String(record.url ?? ''), + number: Number(record.number ?? 0), + title: String(record.title ?? ''), + createdAt: String(record.createdAt ?? ''), + updatedAt: String(record.updatedAt ?? ''), + repository: { nameWithOwner: String(repository.nameWithOwner ?? '') }, + author: author && typeof author.login === 'string' ? { login: author.login } : null, + }; + }); +} + +export async function search( + options: ListOptions, + exec: typeof run = run, +): Promise { + const found = new Map(); + const warnings: string[] = []; + + for (const [qualifier, owners] of [ + ['org', options.orgs], + ['user', options.users], + ] as const) { + for (const owner of owners) { + const result = await exec( + 'gh', + [ + 'search', + 'prs', + `${qualifier}:${owner}`, + '--state=open', + '--archived=false', + '--sort=created', + '--order=desc', + `--limit=${options.limit}`, + '--json', + 'repository,number,title,url,author,createdAt,updatedAt', + ], + { env: { ...process.env, GH_PAGER: 'cat' } }, + ); + + if (result.code !== 0) { + warnings.push(`skipped inaccessible or invalid scope ${qualifier}:${owner}`); + continue; + } + + for (const pr of parse(JSON.parse(result.stdout || '[]'))) { + if (!found.has(pr.url)) found.set(pr.url, pr); + } + } + } + + for (const warning of warnings) process.stderr.write(`WARN: ${warning}\n`); + + // Newest first, matching the original's sort_by(.createdAt) | reverse. + return [...found.values()].sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); +} + +export function renderTable(prs: readonly SearchedPr[], options: ListOptions = {} as ListOptions): string { + const rows: string[][] = [[...HEADER]]; + const urls: (string | undefined)[] = [undefined]; + + for (const pr of prs) { + rows.push([ + pr.repository.nameWithOwner.split('/')[0] ?? '', + pr.repository.nameWithOwner, + `#${pr.number}`, + pr.author?.login ?? '-', + truncate(clean(pr.title), 70), + timeAgo(pr.updatedAt, options.now ?? new Date()), + pr.url, + ]); + urls.push(pr.url); + } + + return table(rows, { + // The PR number and the URL are both clickable, as before. + linkColumns: [2, 6], + urls, + hyperlinks: options.hyperlinks ?? supportsHyperlinks(), + }); +} + +export async function list(options: ListOptions, exec: typeof run = run): Promise { + const prs = await search(options, exec); + if (prs.length === 0) return 'No open PRs found.'; + return renderTable(prs, options); +} + +export { Gh }; diff --git a/src/prs-merge.ts b/src/prs-merge.ts new file mode 100644 index 0000000..95fa8f4 --- /dev/null +++ b/src/prs-merge.ts @@ -0,0 +1,351 @@ +import { Gh, type Check, type PullRequest } from './gh.ts'; +import { sleep } from './exec.ts'; + +export interface MergeOptions { + orgs: string[]; + users: string[]; + limit: number; + apply: boolean; + allowNoChecks: boolean; + readyDrafts: boolean; + fix: boolean; + fixWaitMs: number; + /** Poll interval while waiting on running checks. Shortened by tests. */ + pollMs: number; +} + +export interface Summary { + ready: number; + readied: number; + fixed: number; + merged: number; + skipped: number; + failed: number; +} + +export type Line = + | { kind: 'mode'; text: string } + | { kind: 'ready'; url: string; title: string; checks: number } + | { kind: 'merged'; url: string } + | { kind: 'readied'; url: string; title: string } + | { kind: 'would-ready'; url: string; title: string } + | { kind: 'fixing'; url: string; text: string } + | { kind: 'waiting'; url: string; pending: number } + | { kind: 'fixme'; url: string; text: string } + | { kind: 'skip'; url: string; reason: string; title: string } + | { kind: 'failed'; url: string; reason: string } + | { kind: 'warn'; text: string }; + +export const defaults = { + limit: 1000, + fixWaitMs: 600_000, + pollMs: 20_000, +} as const; + +const isBad = (check: Check): boolean => + check.bucket !== 'pass' && check.bucket !== 'skipping'; + +/** + * Why this PR cannot be merged, or empty when it can. + * + * One function so `--fix` re-judges with the same rules rather than a copy of + * them. In the bash version this logic was inline in the loop, which is why + * adding a re-check meant duplicating it. + */ +export function reasonNotMergeable( + pr: PullRequest, + checks: Check[], + allowNoChecks: boolean, +): string { + if (pr.state !== 'OPEN') return `state=${pr.state}`; + if (pr.isDraft) return 'draft'; + if (pr.mergeable !== 'MERGEABLE') return `mergeable=${pr.mergeable}`; + if (pr.mergeStateStatus !== 'CLEAN') return `mergeStateStatus=${pr.mergeStateStatus}`; + if (checks.length === 0 && !allowNoChecks) return 'no CI checks found'; + + const bad = checks.filter(isBad); + if (bad.length > 0) { + return `checks not green: ${bad.map((c) => `${c.name}=${c.bucket}`).join(', ')}`; + } + + return ''; +} + +/** A blocker `--fix` is willing to act on. */ +export function isRepairable(pr: PullRequest, checks: Check[]): boolean { + if (checks.some((check) => check.bucket === 'pending')) return true; + return ( + pr.mergeStateStatus === 'BEHIND' || + pr.mergeStateStatus === 'DIRTY' || + pr.mergeable === 'CONFLICTING' + ); +} + +export async function sweep( + options: MergeOptions, + gh: Gh, + emit: (line: Line) => void, +): Promise { + const summary: Summary = { + ready: 0, + readied: 0, + fixed: 0, + merged: 0, + skipped: 0, + failed: 0, + }; + + const found = new Map(); + + for (const [qualifier, owners] of [ + ['org', options.orgs], + ['user', options.users], + ] as const) { + for (const owner of owners) { + try { + const prs = await gh.searchPrs(qualifier, owner, { + limit: options.limit, + includeDrafts: options.readyDrafts, + }); + for (const pr of prs) { + if (!found.has(pr.url)) found.set(pr.url, pr.createdAt); + } + } catch (error) { + emit({ + kind: 'warn', + text: `skipped inaccessible or invalid scope ${qualifier}:${owner} — ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + } + } + + const urls = [...found.entries()] + .sort((a, b) => (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0)) + .map(([url]) => url); + + if (options.apply) { + emit({ + kind: 'mode', + text: options.readyDrafts + ? 'MODE: APPLY — drafts will be marked ready and eligible PRs squash-merged.' + : 'MODE: APPLY — eligible PRs will be squash-merged.', + }); + if (options.fix) { + emit({ + kind: 'mode', + text: 'MODE: FIX — repairable blockers will be repaired once, then re-judged.', + }); + } + } else { + emit({ kind: 'mode', text: 'MODE: DRY RUN — nothing will be merged. Add --apply to merge.' }); + } + + for (const url of urls) { + let pr: PullRequest; + + try { + pr = await gh.pullRequest(url); + } catch (error) { + emit({ + kind: 'skip', + url, + reason: `could not read PR metadata — ${ + error instanceof Error ? error.message : String(error) + }`, + title: '', + }); + summary.skipped += 1; + continue; + } + + // Take drafts out of draft first, then judge them like any other PR. + if (pr.isDraft && pr.state === 'OPEN' && options.readyDrafts) { + if (!options.apply) { + emit({ kind: 'would-ready', url, title: pr.title }); + summary.skipped += 1; + continue; + } + + const readied = await gh.ready(url); + if (readied.code !== 0) { + emit({ kind: 'failed', url, reason: 'could not mark draft ready' }); + summary.failed += 1; + continue; + } + + emit({ kind: 'readied', url, title: pr.title }); + summary.readied += 1; + + try { + pr = await gh.pullRequest(url, { awaitReady: true }); + } catch (error) { + emit({ + kind: 'skip', + url, + reason: `could not re-read PR metadata after marking ready — ${ + error instanceof Error ? error.message : String(error) + }`, + title: pr.title, + }); + summary.skipped += 1; + continue; + } + } + + let checks = await gh.checks(url); + let reason = reasonNotMergeable(pr, checks, options.allowNoChecks); + + if (reason && options.fix && isRepairable(pr, checks)) { + const repaired = await repair({ url, checks, reason, options, gh, emit }); + + if (repaired) { + summary.fixed += 1; + try { + pr = await gh.pullRequest(url); + checks = await gh.checks(url); + reason = reasonNotMergeable(pr, checks, options.allowNoChecks); + } catch (error) { + emit({ + kind: 'warn', + text: `could not re-read ${url} after repair — ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + } + } + + if (reason) { + emit({ kind: 'skip', url, reason, title: pr.title }); + summary.skipped += 1; + continue; + } + + emit({ kind: 'ready', url, title: pr.title, checks: checks.length }); + summary.ready += 1; + + if (!options.apply) continue; + + const merged = await gh.squashMerge(url, pr.headRefOid); + if (merged.code === 0) { + emit({ kind: 'merged', url }); + summary.merged += 1; + } else { + emit({ + kind: 'failed', + url, + reason: `GitHub refused the merge: ${merged.stderr.trim() || merged.stdout.trim()}`, + }); + summary.failed += 1; + } + } + + return summary; +} + +/** + * One repair attempt. Returns true when something was done. + * + * Deliberately not a loop: if a repair did not make the PR mergeable, doing it + * again will not either, and a tool that keeps trying turns a sweep into an + * afternoon of API calls. + */ +async function repair(args: { + url: string; + checks: Check[]; + reason: string; + options: MergeOptions; + gh: Gh; + emit: (line: Line) => void; +}): Promise { + const { url, checks, reason, options, gh, emit } = args; + + // Checks still running. The PR is not blocked, it is unfinished — the only + // defect is that we looked too early. This is the common false skip. + if (checks.some((check) => check.bucket === 'pending')) { + emit({ + kind: 'fixing', + url, + text: `checks still running; waiting up to ${Math.round(options.fixWaitMs / 1000)}s`, + }); + await waitForChecks(url, options, gh, emit); + return true; + } + + // Base branch moved. GitHub merges it in without a local checkout, and only + // when the result needs no human judgement. + emit({ kind: 'fixing', url, text: `${reason}; asking GitHub to merge the base branch in` }); + + const updated = await gh.updateBranch(url); + + if (updated.code !== 0) { + // A real conflict. Print what GitHub said and leave it alone: choosing + // between two authors' intent is not a batch operation. + const message = (updated.stderr.trim() || updated.stdout.trim()).replace(/\s+/g, ' '); + emit({ kind: 'fixme', url, text: `GitHub could not merge the base in: ${message}` }); + return false; + } + + // New head, so every check re-runs. Waiting here is what makes the repair + // worth anything — otherwise the re-judge sees a pending suite and skips for + // the very reason we just set in motion. + await waitForChecks(url, options, gh, emit); + return true; +} + +async function waitForChecks( + url: string, + options: MergeOptions, + gh: Gh, + emit: (line: Line) => void, +): Promise { + const deadline = Date.now() + options.fixWaitMs; + + for (;;) { + const checks = await gh.checks(url); + const pending = checks.filter((check) => check.bucket === 'pending').length; + + if (pending === 0) return; + if (Date.now() >= deadline) return; + + emit({ kind: 'waiting', url, pending }); + await sleep(options.pollMs); + } +} + +export function render(line: Line): string { + switch (line.kind) { + case 'mode': + return line.text; + case 'ready': + return `READY ${line.url} — ${line.checks} checks green — ${line.title}`; + case 'merged': + return `MERGED ${line.url}`; + case 'readied': + return `READIED ${line.url} — ${line.title}`; + case 'would-ready': + return `WOULD-READY ${line.url} — draft; would mark ready, then re-check — ${line.title}`; + case 'fixing': + return `FIXING ${line.url} — ${line.text}`; + case 'waiting': + return ` … ${line.pending} check(s) still running on ${line.url}; waiting`; + case 'fixme': + return `FIXME ${line.url} — ${line.text}`; + case 'skip': + return `SKIP ${line.url} — ${line.reason} — ${line.title}`; + case 'failed': + return `FAILED ${line.url} — ${line.reason}`; + case 'warn': + return `WARN: ${line.text}`; + } +} + +export function renderSummary(summary: Summary): string { + return ( + `\nSummary: ready=${summary.ready} readied=${summary.readied} ` + + `fixed=${summary.fixed} merged=${summary.merged} ` + + `skipped=${summary.skipped} failed=${summary.failed}` + ); +} diff --git a/src/tcfeed-launch.ts b/src/tcfeed-launch.ts new file mode 100644 index 0000000..03cb5e9 --- /dev/null +++ b/src/tcfeed-launch.ts @@ -0,0 +1,58 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { homedir } from 'node:os'; +import { spawn } from 'node:child_process'; + +/** + * Launch `tcfeed.ts`, which lives in the threatcrush checkout rather than here. + * + * It is not vendored because it is 94KB of scanner-adjacent code that changes + * with the scanner, and a copy would drift the moment threatcrush shipped + * anything. What lives here is only the part that has to be on PATH. + * + * Everything configurable is read from the environment by the script itself, + * so TCFEED_MIN_GAP, TCFEED_MAX, TCFEED_PAUSE, TCFEED_SUB, TCFEED_CACHE, TC_BIN + * and the rest keep working exactly as before. + */ + +export const DEFAULT_REPO = join(homedir(), 'src', 'profullstack', 'threatcrush'); + +export function resolveScript(repo = process.env.TCFEED_REPO ?? DEFAULT_REPO): { + repo: string; + script: string; + exists: boolean; +} { + const script = join(repo, 'bin', 'tcfeed.ts'); + return { repo, script, exists: existsSync(script) }; +} + +export function missingScriptMessage(name: string, repo: string, script: string): string { + return [ + `${name}: no script at ${script}`, + ` git -C ${repo} pull # it lives in the threatcrush repo`, + ' TCFEED_REPO=/elsewhere # if the checkout moved', + ].join('\n'); +} + +export function launch(name: string, args: readonly string[]): Promise { + const { repo, script, exists } = resolveScript(); + + if (!exists) { + process.stderr.write(`${missingScriptMessage(name, repo, script)}\n`); + return Promise.resolve(1); + } + + // Run from the repo so npx resolves tsx against its node_modules before + // reaching for the network. The script itself does not care where it is. + return new Promise((resolve) => { + const child = spawn('npx', ['--yes', 'tsx', 'bin/tcfeed.ts', ...args], { + cwd: repo, + stdio: 'inherit', + }); + child.on('error', (error) => { + process.stderr.write(`${name}: could not start tsx — ${error.message}\n`); + resolve(1); + }); + child.on('close', (code) => resolve(code ?? 1)); + }); +} diff --git a/test/format.test.ts b/test/format.test.ts new file mode 100644 index 0000000..e74a131 --- /dev/null +++ b/test/format.test.ts @@ -0,0 +1,221 @@ +import { describe, expect, it } from 'vitest'; +import { clean, hyperlink, table, timeAgo, truncate } from '../src/format.ts'; +import { csv, integer, parseArgs, UsageError } from '../src/args.ts'; +import { buildArgs, hasCheckSubcommand } from '../src/fix-all.ts'; +import { parseDomainArgs } from '../src/domain.ts'; +import { isMain } from '../src/is-main.ts'; + +const NOW = new Date('2026-08-16T12:00:00Z'); +const ago = (seconds: number): string => + timeAgo(new Date(NOW.getTime() - seconds * 1000).toISOString(), NOW); + +describe('timeAgo', () => { + it('matches the jq original at each threshold', () => { + expect(ago(1)).toBe('1 second ago'); + expect(ago(59)).toBe('59 seconds ago'); + expect(ago(60)).toBe('1 minute ago'); + expect(ago(3599)).toBe('59 minutes ago'); + expect(ago(3600)).toBe('1 hour ago'); + expect(ago(86_399)).toBe('23 hours ago'); + expect(ago(86_400)).toBe('1 day ago'); + expect(ago(604_800)).toBe('1 week ago'); + expect(ago(2_629_800)).toBe('1 month ago'); + expect(ago(31_557_600)).toBe('1 year ago'); + }); + + it('never reads negative when the clock is behind the server', () => { + expect(timeAgo(new Date(NOW.getTime() + 5000).toISOString(), NOW)).toBe('0 seconds ago'); + }); + + it('says so rather than printing NaN for an unparseable date', () => { + expect(timeAgo('not a date', NOW)).toBe('unknown'); + }); +}); + +describe('cell formatting', () => { + it('collapses whitespace that would break a row across lines', () => { + expect(clean('a\nb\tc')).toBe('a b c'); + expect(clean(null)).toBe(''); + }); + + it('truncates to exactly the requested width, ellipsis included', () => { + expect(truncate('abcdefghij', 10)).toBe('abcdefghij'); + expect(truncate('abcdefghijk', 10)).toHaveLength(10); + expect(truncate('abcdefghijk', 10)).toBe('abcdefg...'); + }); +}); + +describe('table', () => { + const rows = [ + ['ORG', 'PR'], + ['acme', '#1'], + ['verylongowner', '#22'], + ]; + + it('pads columns to the widest cell', () => { + const out = table(rows).split('\n'); + expect(out[0]).toBe('ORG PR'); + expect(out[1]).toBe('acme #1'); + expect(out[2]).toBe('verylongowner #22'); + }); + + it('leaves plain text alone when hyperlinks are off', () => { + expect(table(rows, { linkColumns: [1], urls: [undefined, 'u1', 'u2'] })).not.toContain( + '', + ); + }); + + /** + * The bug this guards: an OSC-8 escape is zero-width on screen but a dozen + * characters to String.length. Padding the decorated cell throws every + * following column out by the length of a URL. + */ + it('measures padding on the undecorated text', () => { + const linked = table( + [ + ['A', 'B'], + ['x', 'y'], + ['xxxxx', 'y'], + ], + { linkColumns: [0], urls: [undefined, 'https://example.com/1', 'https://example.com/2'], hyperlinks: true }, + ).split('\n'); + + // Strip the escapes and the layout must be identical to the plain version. + const stripped = linked.map((line) => line.replace(/\]8;;[^]*\\/g, '')); + expect(stripped[1]).toBe('x y'); + expect(stripped[2]).toBe('xxxxx y'); + }); + + it('never decorates the header row', () => { + const out = table(rows, { + linkColumns: [0], + urls: ['header-url', 'u1', 'u2'], + hyperlinks: true, + }).split('\n'); + expect(out[0]).not.toContain(''); + }); + + it('builds a well-formed OSC-8 sequence', () => { + expect(hyperlink('text', 'https://example.com')).toBe( + ']8;;https://example.com\\text]8;;\\', + ); + }); +}); + +describe('parseArgs', () => { + const spec = { boolean: ['--apply'], string: ['--limit'] } as const; + + it('accepts both --flag value and --flag=value', () => { + expect(parseArgs(['--limit', '5'], spec).values.get('--limit')).toBe('5'); + expect(parseArgs(['--limit=5'], spec).values.get('--limit')).toBe('5'); + }); + + /** `--limit --apply` used to set limit to the string "--apply". */ + it('treats a following flag as a missing value', () => { + expect(() => parseArgs(['--limit', '--apply'], spec)).toThrow(UsageError); + }); + + it('rejects a value on a boolean flag', () => { + expect(() => parseArgs(['--apply=yes'], spec)).toThrow(/does not take a value/); + }); + + it('rejects an unknown option instead of ignoring it', () => { + expect(() => parseArgs(['--nope'], spec)).toThrow(/unknown option: --nope/); + }); + + it('stops flag parsing at --', () => { + expect(parseArgs(['--', '--limit'], spec).positional).toEqual(['--limit']); + }); + + it('validates integers with the flag name in the message', () => { + const { values } = parseArgs(['--limit', 'abc'], spec); + expect(() => integer(values, '--limit', 1)).toThrow(/--limit must be a non-negative integer/); + }); + + it('bounds integers', () => { + const { values } = parseArgs(['--limit', '5000'], spec); + expect(() => integer(values, '--limit', 1, { min: 1, max: 1000 })).toThrow(/between 1 and 1000/); + }); + + it('splits and trims csv, dropping empties', () => { + const values = new Map([['--orgs', ' a , b ,, c ']]); + expect(csv(values, '--orgs')).toEqual(['a', 'b', 'c']); + expect(csv(new Map(), '--orgs')).toEqual([]); + }); +}); + +describe('gh-prs-fix-all argument shaping', () => { + it('adds --fix by default', () => { + expect(buildArgs([])).toEqual(['check', '--fix']); + }); + + it('omits --fix for a dry run, and does not pass the flag on', () => { + expect(buildArgs(['--dry-run'])).toEqual(['check']); + expect(buildArgs(['-n'])).toEqual(['check']); + }); + + it('passes repositories through', () => { + expect(buildArgs(['owner/name'])).toEqual(['check', 'owner/name', '--fix']); + expect(buildArgs(['owner/name', '-n'])).toEqual(['check', 'owner/name']); + }); + + it('recognises the check subcommand guard', () => { + expect(hasCheckSubcommand("if (argument === 'check') {")).toBe(true); + expect(hasCheckSubcommand('const x = 1;')).toBe(false); + }); +}); + +describe('isMain', () => { + /** + * The regression this exists for: importing `bin/gh-prs-fix-all.ts` to reach + * one pure function *ran the tool*. The suite went from 60ms to 93 seconds + * and swept live pull requests with `--fix` implied. Nothing under bin/ may + * do work at import time. + */ + it('is false when the module was imported rather than executed', () => { + expect(isMain('file:///tools/bin/thing.ts', ['node', '/tools/bin/other.ts'])).toBe(false); + }); + + it('is true when the module is the entry point', () => { + const url = new URL(import.meta.url); + expect(isMain(url.href, ['node', url.pathname])).toBe(true); + }); + + it('is false with no entry point at all', () => { + expect(isMain('file:///tools/bin/thing.ts', ['node'])).toBe(false); + }); +}); + +describe('domainjson argument parsing', () => { + it('takes the final non-flag argument as the name', () => { + const parsed = parseDomainArgs(['-s', 'https://rdap.example', 'example.com']); + expect(parsed.own.name).toBe('example.com'); + // The server URL must not be mistaken for the name. + expect(parsed.passthrough).toEqual(['-s', 'https://rdap.example']); + }); + + it('drops output-format flags because JSON is forced', () => { + expect(parseDomainArgs(['--text', '--whois', 'example.com']).passthrough).toEqual([]); + }); + + it('strips trailing slashes from the registry', () => { + expect(parseDomainArgs(['--registry', 'https://x.test///', 'a.com']).own.registry).toBe( + 'https://x.test', + ); + }); + + it('rejects a non-positive timeout', () => { + expect(parseDomainArgs(['--timeout', '0', 'a.com']).error).toMatch(/positive integer/); + expect(parseDomainArgs(['--timeout=abc', 'a.com']).error).toMatch(/positive integer/); + }); + + it('reports a missing name rather than looking up nothing', () => { + expect(parseDomainArgs([]).error).toBe('no name given'); + }); + + it('prefers --name over the positional', () => { + const parsed = parseDomainArgs(['--name', 'chosen.com', 'other.com']); + expect(parsed.own.name).toBe('chosen.com'); + expect(parsed.passthrough).toContain('other.com'); + }); +}); diff --git a/test/prs-merge.test.ts b/test/prs-merge.test.ts new file mode 100644 index 0000000..cbd7c70 --- /dev/null +++ b/test/prs-merge.test.ts @@ -0,0 +1,322 @@ +import { describe, expect, it } from 'vitest'; +import { Gh, parseChecks, parsePullRequest, GhError } from '../src/gh.ts'; +import type { RunResult } from '../src/exec.ts'; +import { + defaults, + isRepairable, + reasonNotMergeable, + render, + sweep, + type Line, + type MergeOptions, +} from '../src/prs-merge.ts'; + +const PR_URL = 'https://github.com/acme/repo/pull/1'; + +interface StubStep { + mergeable?: string; + mergeStateStatus?: string; + isDraft?: boolean; + state?: string; + checks?: { name: string; bucket: string }[]; +} + +/** + * A scripted `gh`. + * + * Each PR read and check read advances through `steps`, so a test can say "the + * first look sees a pending suite, the second sees it green" — which is the + * behaviour --fix exists for and the one a single-shot stub cannot express. + */ +function stubGh(options: { + steps: StubStep[]; + updateBranchFails?: boolean; + mergeFails?: boolean; +}) { + const calls: string[] = []; + let viewIndex = 0; + let checkIndex = 0; + + const step = (index: number): StubStep => + options.steps[Math.min(index, options.steps.length - 1)]!; + + const exec = async (_file: string, args: readonly string[]): Promise => { + calls.push(args.join(' ')); + const ok = (stdout: string): RunResult => ({ code: 0, stdout, stderr: '' }); + + if (args[0] === 'search') { + return ok(JSON.stringify([{ url: PR_URL, createdAt: '2026-01-01T00:00:00Z' }])); + } + + if (args[0] === 'pr' && args[1] === 'view') { + const current = step(viewIndex); + viewIndex += 1; + return ok( + JSON.stringify({ + url: PR_URL, + title: 'stub pr', + state: current.state ?? 'OPEN', + isDraft: current.isDraft ?? false, + mergeable: current.mergeable ?? 'MERGEABLE', + mergeStateStatus: current.mergeStateStatus ?? 'CLEAN', + headRefOid: 'deadbeef', + }), + ); + } + + if (args[0] === 'pr' && args[1] === 'checks') { + const current = step(checkIndex); + checkIndex += 1; + const checks = current.checks ?? [{ name: 'test', bucket: 'pass' }]; + // gh exits non-zero while still printing JSON when anything is pending. + return { + code: checks.some((c) => c.bucket !== 'pass') ? 1 : 0, + stdout: JSON.stringify(checks), + stderr: '', + }; + } + + if (args[0] === 'pr' && args[1] === 'update-branch') { + return options.updateBranchFails + ? { code: 1, stdout: '', stderr: 'X Cannot update PR branch due to conflicts' } + : ok('Updated branch'); + } + + if (args[0] === 'pr' && args[1] === 'merge') { + return options.mergeFails + ? { code: 1, stdout: '', stderr: 'refused' } + : ok('Merged'); + } + + if (args[0] === 'pr' && args[1] === 'ready') return ok(''); + + return ok(''); + }; + + return { gh: new Gh({ exec }), calls }; +} + +function baseOptions(overrides: Partial = {}): MergeOptions { + return { + orgs: ['acme'], + users: [], + limit: 10, + apply: true, + allowNoChecks: false, + readyDrafts: true, + fix: false, + fixWaitMs: 1_000, + pollMs: 1, + ...overrides, + }; +} + +async function runSweep(gh: Gh, options: MergeOptions) { + const lines: Line[] = []; + const summary = await sweep(options, gh, (line) => lines.push(line)); + return { summary, lines, text: lines.map(render).join('\n') }; +} + +describe('eligibility rules', () => { + const pr = { + url: PR_URL, + title: 't', + state: 'OPEN', + isDraft: false, + mergeable: 'MERGEABLE', + mergeStateStatus: 'CLEAN', + headRefOid: 'abc', + } as const; + + it('accepts an open, clean PR with green checks', () => { + expect(reasonNotMergeable(pr, [{ name: 'test', bucket: 'pass' }], false)).toBe(''); + }); + + it('names the specific blocker', () => { + expect(reasonNotMergeable({ ...pr, state: 'CLOSED' }, [], false)).toBe('state=CLOSED'); + expect(reasonNotMergeable({ ...pr, isDraft: true }, [], false)).toBe('draft'); + expect(reasonNotMergeable({ ...pr, mergeable: 'CONFLICTING' }, [], false)).toBe( + 'mergeable=CONFLICTING', + ); + expect(reasonNotMergeable({ ...pr, mergeStateStatus: 'UNSTABLE' }, [], false)).toBe( + 'mergeStateStatus=UNSTABLE', + ); + expect(reasonNotMergeable(pr, [], false)).toBe('no CI checks found'); + expect(reasonNotMergeable(pr, [{ name: 'test', bucket: 'fail' }], false)).toBe( + 'checks not green: test=fail', + ); + }); + + it('treats skipping as acceptable but pending as not', () => { + expect(reasonNotMergeable(pr, [{ name: 'a', bucket: 'skipping' }], false)).toBe(''); + expect(reasonNotMergeable(pr, [{ name: 'a', bucket: 'pending' }], false)).toContain( + 'a=pending', + ); + }); + + it('only calls mechanical blockers repairable', () => { + expect(isRepairable(pr, [{ name: 'a', bucket: 'pending' }])).toBe(true); + expect(isRepairable({ ...pr, mergeStateStatus: 'BEHIND' }, [])).toBe(true); + expect(isRepairable({ ...pr, mergeable: 'CONFLICTING' }, [])).toBe(true); + // A check that ran and failed is a result, not an obstacle. + expect(isRepairable(pr, [{ name: 'a', bucket: 'fail' }])).toBe(false); + }); +}); + +describe('gh response validation', () => { + it('names the offending field rather than yielding a null string', () => { + // `jq -r '.mergeable'` printed the four characters "null" here, which is + // not MERGEABLE, so the PR read as ineligible for a reason nobody wrote. + expect(() => parsePullRequest({ url: 'u', title: 't', state: 'OPEN', isDraft: false, mergeStateStatus: 'CLEAN', headRefOid: 'a' })) + .toThrow(/mergeable/); + }); + + it('rejects a bucket it does not know instead of silently passing it', () => { + expect(() => parseChecks([{ name: 'a', bucket: 'sideways' }])).toThrow(/bucket/); + }); + + it('accepts the documented shapes', () => { + expect(parseChecks([{ name: 'a', bucket: 'pass' }])).toEqual([ + { name: 'a', bucket: 'pass' }, + ]); + }); + + it('reports non-JSON output as such', async () => { + const gh = new Gh({ + exec: async () => ({ code: 0, stdout: 'gh: not logged in', stderr: '' }), + }); + await expect(gh.pullRequest(PR_URL, { attempts: 1 })).rejects.toBeInstanceOf(GhError); + }); +}); + +describe('sweep', () => { + it('merges an eligible PR, pinned to the head it judged', async () => { + const { gh, calls } = stubGh({ steps: [{}] }); + const { summary, text } = await runSweep(gh, baseOptions()); + + expect(summary.merged).toBe(1); + expect(text).toContain('MERGED'); + expect(calls.some((c) => c.includes('--match-head-commit deadbeef'))).toBe(true); + // Protections stay enforced. + expect(calls.some((c) => c.includes('--admin'))).toBe(false); + }); + + it('merges nothing in a dry run', async () => { + const { gh, calls } = stubGh({ steps: [{}] }); + const { summary } = await runSweep(gh, baseOptions({ apply: false })); + + expect(summary.ready).toBe(1); + expect(summary.merged).toBe(0); + expect(calls.some((c) => c.startsWith('pr merge'))).toBe(false); + }); + + it('skips a conflicting PR untouched when --fix is off', async () => { + const { gh, calls } = stubGh({ + steps: [{ mergeable: 'CONFLICTING', mergeStateStatus: 'DIRTY' }], + }); + const { summary, text } = await runSweep(gh, baseOptions()); + + expect(summary.skipped).toBe(1); + expect(text).toContain('mergeable=CONFLICTING'); + expect(calls.some((c) => c.startsWith('pr update-branch'))).toBe(false); + }); + + it('repairs a BEHIND branch and then merges it', async () => { + const { gh, calls } = stubGh({ + steps: [{ mergeStateStatus: 'BEHIND' }, { mergeStateStatus: 'CLEAN' }], + }); + const { summary, text } = await runSweep(gh, baseOptions({ fix: true })); + + expect(text).toContain('FIXING'); + expect(summary.fixed).toBe(1); + expect(summary.merged).toBe(1); + expect(calls.some((c) => c.startsWith('pr update-branch'))).toBe(true); + }); + + it('refuses to guess at a conflict GitHub will not merge', async () => { + const { gh, calls } = stubGh({ + steps: [{ mergeable: 'CONFLICTING', mergeStateStatus: 'DIRTY' }], + updateBranchFails: true, + }); + const { summary, text } = await runSweep(gh, baseOptions({ fix: true })); + + expect(text).toContain('FIXME'); + expect(text).toContain('Cannot update PR branch due to conflicts'); + expect(summary.merged).toBe(0); + expect(summary.skipped).toBe(1); + // Repair did not succeed, so it is not counted as one. + expect(summary.fixed).toBe(0); + expect(calls.some((c) => c.startsWith('pr merge'))).toBe(false); + }); + + it('waits for a running check, then merges once it goes green', async () => { + // The false skip that motivated --fix: nothing was wrong with the PR, the + // sweep simply looked before Socket had reported. + const { gh } = stubGh({ + steps: [ + { mergeStateStatus: 'UNSTABLE', checks: [{ name: 'socket', bucket: 'pending' }] }, + { mergeStateStatus: 'CLEAN', checks: [{ name: 'socket', bucket: 'pass' }] }, + ], + }); + const { summary, text } = await runSweep(gh, baseOptions({ fix: true })); + + expect(text).toContain('checks still running'); + expect(summary.merged).toBe(1); + }); + + it('gives up waiting at --fix-wait rather than hanging', async () => { + const { gh } = stubGh({ + steps: [{ mergeStateStatus: 'UNSTABLE', checks: [{ name: 'x', bucket: 'pending' }] }], + }); + const { summary } = await runSweep(gh, baseOptions({ fix: true, fixWaitMs: 0, pollMs: 1 })); + + expect(summary.merged).toBe(0); + expect(summary.skipped).toBe(1); + }); + + it('counts a refused merge as failed, not skipped', async () => { + const { gh } = stubGh({ steps: [{}], mergeFails: true }); + const { summary } = await runSweep(gh, baseOptions()); + + expect(summary.failed).toBe(1); + expect(summary.merged).toBe(0); + }); + + it('marks a draft ready, then judges it normally', async () => { + const { gh, calls } = stubGh({ steps: [{ isDraft: true }, { isDraft: false }] }); + const { summary } = await runSweep(gh, baseOptions()); + + expect(calls.some((c) => c.startsWith('pr ready'))).toBe(true); + expect(summary.readied).toBe(1); + expect(summary.merged).toBe(1); + }); + + it('reports a draft without touching it in a dry run', async () => { + const { gh, calls } = stubGh({ steps: [{ isDraft: true }] }); + const { summary, text } = await runSweep(gh, baseOptions({ apply: false })); + + expect(text).toContain('WOULD-READY'); + expect(calls.some((c) => c.startsWith('pr ready'))).toBe(false); + expect(summary.skipped).toBe(1); + }); + + it('carries on when one scope is inaccessible', async () => { + const gh = new Gh({ + exec: async (_f, args) => + args[0] === 'search' + ? { code: 1, stdout: '', stderr: 'not found' } + : { code: 0, stdout: '[]', stderr: '' }, + }); + const { summary, text } = await runSweep(gh, baseOptions({ orgs: ['nope'] })); + + expect(text).toContain('skipped inaccessible or invalid scope'); + expect(summary.failed).toBe(0); + }); +}); + +describe('defaults', () => { + it('waits ten minutes and polls every twenty seconds', () => { + expect(defaults.fixWaitMs).toBe(600_000); + expect(defaults.pollMs).toBe(20_000); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6cd0210 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "moduleResolution": "bundler", + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "verbatimModuleSyntax": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src", "bin", "test", "scripts"] +} From aa92f79f4c1d62d0a935c7ab2aa401c37edbe901 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 16 Aug 2026 07:40:20 +0000 Subject: [PATCH 2/2] fix: upgrade vitest to 4, clearing a blocking vite advisory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Socket blocked the PR on GHSA-fx2h-pf6j-xcff (high) — vite's `server.fs.deny` bypass on Windows alternate paths. vitest 2.1.9 pulled in vite 5.4.21, and that range is only patched at 6.4.3. vitest 4.1.10 resolves vite 8.2.1, above the 8.0.16 fix. Not exploitable here — it is a dev-server path check in a devDependency of a repo with no dev server — but the fix is a version bump, and arguing for an exception costs more than taking it. All 51 tests pass unchanged on vitest 4. Also corrects the README. It repeated the older tools' claim that the moshcode pit runs aliases with a non-interactive `zsh -c`, which is why they had to be files on PATH. That is no longer true: src/aliases.mjs runs `$SHELL -ic`, which does source ~/.zsh_aliases. Verified both ways and recorded the actual, weaker reason to stay on PATH — a file works from every caller without anything having been sourced first. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 40 +- package.json | 2 +- pnpm-lock.yaml | 983 +++++++++++++++++++------------------------------ 3 files changed, 414 insertions(+), 611 deletions(-) diff --git a/README.md b/README.md index ba6ca4c..22d5962 100644 --- a/README.md +++ b/README.md @@ -50,28 +50,38 @@ pnpm unlink # remove the ones we own ln -sf ~/scripts/bin/gh-prs-merge ~/.local/bin/gh-prs-merge # and so on ``` -## Aliases must be real executables +## Files on PATH, not shell functions -These install as files on PATH rather than shell aliases or functions, and that -is load-bearing. +These install as executables on PATH rather than shell aliases or functions. -The moshcode pit runs its aliases with `zsh -c `, and `zsh -c` is a -non-interactive shell: it reads neither `~/.zshrc` nor `~/.zsh_aliases`. A -function defined there is simply not there, so `/alias tcfeed "tcfeed"` in the -pit answered `command not found` while the identical word worked when typed at a -prompt. A file on PATH works from an interactive shell, from `zsh -c`, and from -the pit, because none of them have to have sourced anything first. +The older tools carry a comment saying this is because the moshcode pit runs +aliases with `zsh -c`, a non-interactive shell that reads neither `~/.zshrc` nor +`~/.zsh_aliases`. **That is no longer true** — `src/aliases.mjs` in current +moshcode runs `$SHELL -ic`, which is interactive and does source them. Verified: + +```console +$ zsh -ic 'gh-prs-all --help' # works — the pit's path +$ zsh -c 'gh-prs-all --help' # zsh:1: command not found +``` + +The reason to stay on PATH is the weaker but still sufficient one: a file works +from every caller — an interactive shell, `zsh -c`, a systemd unit, a CI step — +without anything having been sourced first. A shell alias only works where a +startup file was read. Nothing should alias *to* these either. A function beats PATH, so a wrapper of the same name silently shadows the file and the two drift apart. +Pit aliases (`/alias set ""`, stored in +`~/.moshcode/aliases.json`): + ``` -/alias prs "gh-prs --orgs profullstack" -/alias merge "gh-prs-merge --orgs profullstack --apply --fix" -/alias merge-dry "gh-prs-merge --orgs profullstack" -/alias fixprs "gh-prs-fix-all" -/alias feed "tcfeed" -/alias whoisj "domainjson" +/alias set prs "gh-prs --orgs profullstack" +/alias set merge "gh-prs-merge --orgs profullstack --apply --fix" +/alias set merge-dry "gh-prs-merge --orgs profullstack" +/alias set fixprs "gh-prs-fix-all" +/alias set feed "tcfeed" +/alias set whoisj "domainjson" ``` ## `gh-prs-merge --fix` diff --git a/package.json b/package.json index 3e21434..0e0ebd5 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,6 @@ "@types/node": "^22.10.2", "tsx": "^4.19.2", "typescript": "^5.7.2", - "vitest": "^2.1.8" + "vitest": "^4.1.10" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f13ebbc..72e1b76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,209 +18,107 @@ importers: specifier: ^5.7.2 version: 5.9.3 vitest: - specifier: ^2.1.8 - version: 2.1.9(@types/node@22.20.1) + specifier: ^4.1.10 + version: 4.1.10(@types/node@22.20.1)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.2)(tsx@4.23.12)) packages: - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.28.2': resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.28.2': resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.28.2': resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.28.2': resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.28.2': resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.28.2': resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.28.2': resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.28.2': resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.28.2': resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.28.2': resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.28.2': resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.28.2': resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.28.2': resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.28.2': resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.28.2': resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.28.2': resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} engines: {node: '>=18'} @@ -233,12 +131,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.28.2': resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} engines: {node: '>=18'} @@ -251,12 +143,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.28.2': resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} engines: {node: '>=18'} @@ -269,48 +155,24 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.28.2': resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.28.2': resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.28.2': resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.28.2': resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} engines: {node: '>=18'} @@ -320,150 +182,110 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@napi-rs/lzma-linux-x64-gnu@1.5.1': - resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} - engines: {node: ^22.20 || ^24.12 || >=25} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-android-arm-eabi@4.62.4': - resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} - cpu: [arm] - os: [android] + '@oxc-project/types@0.144.0': + resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==} - '@rollup/rollup-android-arm64@4.62.4': - resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + '@rolldown/binding-android-arm64@1.2.4': + resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.62.4': - resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + '@rolldown/binding-darwin-arm64@1.2.4': + resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.62.4': - resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + '@rolldown/binding-darwin-x64@1.2.4': + resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.62.4': - resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.62.4': - resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + '@rolldown/binding-freebsd-x64@1.2.4': + resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.62.4': - resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': + resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.62.4': - resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} - cpu: [arm] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.62.4': - resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + '@rolldown/binding-linux-arm64-gnu@1.2.4': + resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.62.4': - resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + '@rolldown/binding-linux-arm64-musl@1.2.4': + resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.62.4': - resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.62.4': - resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.62.4': - resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.62.4': - resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + '@rolldown/binding-linux-ppc64-gnu@1.2.4': + resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.62.4': - resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} - cpu: [riscv64] - os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.62.4': - resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.62.4': - resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + '@rolldown/binding-linux-s390x-gnu@1.2.4': + resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.62.4': - resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + '@rolldown/binding-linux-x64-gnu@1.2.4': + resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.62.4': - resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + '@rolldown/binding-linux-x64-musl@1.2.4': + resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.62.4': - resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.62.4': - resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + '@rolldown/binding-openharmony-arm64@1.2.4': + resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.62.4': - resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + '@rolldown/binding-win32-arm64-msvc@1.2.4': + resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.62.4': - resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.62.4': - resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + '@rolldown/binding-win32-x64-msvc@1.2.4': + resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.62.4': - resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} - cpu: [x64] - os: [win32] + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -471,71 +293,52 @@ packages: '@types/node@22.20.1': resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} - '@vitest/expect@2.1.9': - resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@2.1.9': - resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@2.1.9': - resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@2.1.9': - resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@2.1.9': - resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@2.1.9': - resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@2.1.9': - resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - check-error@2.1.3: - resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} - engines: {node: '>= 16'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} @@ -549,42 +352,123 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + postcss@8.5.26: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} - rollup@4.62.4: - resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + rolldown@1.2.4: + resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true siginfo@2.0.0: @@ -597,25 +481,22 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} tsx@4.23.12: @@ -631,30 +512,33 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - vite-node@2.1.9: - resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} + vite@8.2.1: + resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true - less: + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: optional: true - lightningcss: + less: optional: true sass: optional: true @@ -666,24 +550,44 @@ packages: optional: true terser: optional: true + tsx: + optional: true + yaml: + optional: true - vitest@2.1.9: - resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} - engines: {node: ^18.0.0 || >=20.0.0} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.9 - '@vitest/ui': 2.1.9 + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true + '@opentelemetry/api': + optional: true '@types/node': optional: true - '@vitest/browser': + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': optional: true '@vitest/ui': optional: true @@ -699,232 +603,140 @@ packages: snapshots: - '@esbuild/aix-ppc64@0.21.5': - optional: true - '@esbuild/aix-ppc64@0.28.2': optional: true - '@esbuild/android-arm64@0.21.5': - optional: true - '@esbuild/android-arm64@0.28.2': optional: true - '@esbuild/android-arm@0.21.5': - optional: true - '@esbuild/android-arm@0.28.2': optional: true - '@esbuild/android-x64@0.21.5': - optional: true - '@esbuild/android-x64@0.28.2': optional: true - '@esbuild/darwin-arm64@0.21.5': - optional: true - '@esbuild/darwin-arm64@0.28.2': optional: true - '@esbuild/darwin-x64@0.21.5': - optional: true - '@esbuild/darwin-x64@0.28.2': optional: true - '@esbuild/freebsd-arm64@0.21.5': - optional: true - '@esbuild/freebsd-arm64@0.28.2': optional: true - '@esbuild/freebsd-x64@0.21.5': - optional: true - '@esbuild/freebsd-x64@0.28.2': optional: true - '@esbuild/linux-arm64@0.21.5': - optional: true - '@esbuild/linux-arm64@0.28.2': optional: true - '@esbuild/linux-arm@0.21.5': - optional: true - '@esbuild/linux-arm@0.28.2': optional: true - '@esbuild/linux-ia32@0.21.5': - optional: true - '@esbuild/linux-ia32@0.28.2': optional: true - '@esbuild/linux-loong64@0.21.5': - optional: true - '@esbuild/linux-loong64@0.28.2': optional: true - '@esbuild/linux-mips64el@0.21.5': - optional: true - '@esbuild/linux-mips64el@0.28.2': optional: true - '@esbuild/linux-ppc64@0.21.5': - optional: true - '@esbuild/linux-ppc64@0.28.2': optional: true - '@esbuild/linux-riscv64@0.21.5': - optional: true - '@esbuild/linux-riscv64@0.28.2': optional: true - '@esbuild/linux-s390x@0.21.5': - optional: true - '@esbuild/linux-s390x@0.28.2': optional: true - '@esbuild/linux-x64@0.21.5': - optional: true - '@esbuild/linux-x64@0.28.2': optional: true '@esbuild/netbsd-arm64@0.28.2': optional: true - '@esbuild/netbsd-x64@0.21.5': - optional: true - '@esbuild/netbsd-x64@0.28.2': optional: true '@esbuild/openbsd-arm64@0.28.2': optional: true - '@esbuild/openbsd-x64@0.21.5': - optional: true - '@esbuild/openbsd-x64@0.28.2': optional: true '@esbuild/openharmony-arm64@0.28.2': optional: true - '@esbuild/sunos-x64@0.21.5': - optional: true - '@esbuild/sunos-x64@0.28.2': optional: true - '@esbuild/win32-arm64@0.21.5': - optional: true - '@esbuild/win32-arm64@0.28.2': optional: true - '@esbuild/win32-ia32@0.21.5': - optional: true - '@esbuild/win32-ia32@0.28.2': optional: true - '@esbuild/win32-x64@0.21.5': - optional: true - '@esbuild/win32-x64@0.28.2': optional: true '@jridgewell/sourcemap-codec@1.5.5': {} - '@napi-rs/lzma-linux-x64-gnu@1.5.1': - optional: true - - '@rollup/rollup-android-arm-eabi@4.62.4': - optional: true - - '@rollup/rollup-android-arm64@4.62.4': - optional: true - - '@rollup/rollup-darwin-arm64@4.62.4': - optional: true - - '@rollup/rollup-darwin-x64@4.62.4': - optional: true - - '@rollup/rollup-freebsd-arm64@4.62.4': - optional: true - - '@rollup/rollup-freebsd-x64@4.62.4': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.62.4': - optional: true + '@oxc-project/types@0.144.0': {} - '@rollup/rollup-linux-arm-musleabihf@4.62.4': + '@rolldown/binding-android-arm64@1.2.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.62.4': + '@rolldown/binding-darwin-arm64@1.2.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.62.4': + '@rolldown/binding-darwin-x64@1.2.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.62.4': + '@rolldown/binding-freebsd-x64@1.2.4': optional: true - '@rollup/rollup-linux-loong64-musl@4.62.4': + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.62.4': + '@rolldown/binding-linux-arm64-gnu@1.2.4': optional: true - '@rollup/rollup-linux-ppc64-musl@4.62.4': + '@rolldown/binding-linux-arm64-musl@1.2.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.62.4': + '@rolldown/binding-linux-ppc64-gnu@1.2.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.62.4': + '@rolldown/binding-linux-s390x-gnu@1.2.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.62.4': + '@rolldown/binding-linux-x64-gnu@1.2.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.62.4': + '@rolldown/binding-linux-x64-musl@1.2.4': optional: true - '@rollup/rollup-linux-x64-musl@4.62.4': + '@rolldown/binding-openharmony-arm64@1.2.4': optional: true - '@rollup/rollup-openbsd-x64@4.62.4': + '@rolldown/binding-win32-arm64-msvc@1.2.4': optional: true - '@rollup/rollup-openharmony-arm64@4.62.4': + '@rolldown/binding-win32-x64-msvc@1.2.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.62.4': - optional: true + '@rolldown/pluginutils@1.0.1': {} - '@rollup/rollup-win32-ia32-msvc@4.62.4': - optional: true + '@standard-schema/spec@1.1.0': {} - '@rollup/rollup-win32-x64-gnu@4.62.4': - optional: true + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 - '@rollup/rollup-win32-x64-msvc@4.62.4': - optional: true + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.9': {} @@ -932,93 +744,56 @@ snapshots: dependencies: undici-types: 6.21.0 - '@vitest/expect@2.1.9': + '@vitest/expect@4.1.10': dependencies: - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - tinyrainbow: 1.2.0 - - '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.20.1))': + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.2)(tsx@4.23.12))': dependencies: - '@vitest/spy': 2.1.9 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(@types/node@22.20.1) + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.28.2)(tsx@4.23.12) - '@vitest/pretty-format@2.1.9': + '@vitest/pretty-format@4.1.10': dependencies: - tinyrainbow: 1.2.0 + tinyrainbow: 3.1.1 - '@vitest/runner@2.1.9': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 2.1.9 - pathe: 1.1.2 + '@vitest/utils': 4.1.10 + pathe: 2.0.3 - '@vitest/snapshot@2.1.9': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 2.1.9 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 - pathe: 1.1.2 + pathe: 2.0.3 - '@vitest/spy@2.1.9': - dependencies: - tinyspy: 3.0.2 + '@vitest/spy@4.1.10': {} - '@vitest/utils@2.1.9': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 2.1.9 - loupe: 3.2.1 - tinyrainbow: 1.2.0 + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 assertion-error@2.0.1: {} - cac@6.7.14: {} - - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.3 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 - - check-error@2.1.3: {} + chai@6.2.2: {} - debug@4.4.3: - dependencies: - ms: 2.1.3 + convert-source-map@2.0.0: {} - deep-eql@5.0.2: {} + detect-libc@2.1.2: {} - es-module-lexer@1.7.0: {} - - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 + es-module-lexer@2.3.1: {} esbuild@0.28.2: optionalDependencies: @@ -1055,62 +830,101 @@ snapshots: expect-type@1.4.0: {} + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + fsevents@2.3.3: optional: true - loupe@3.2.1: {} + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - ms@2.1.3: {} - nanoid@3.3.18: {} - pathe@1.1.2: {} + obug@2.1.4: {} - pathval@2.0.1: {} + pathe@2.0.3: {} picocolors@1.1.1: {} + picomatch@4.0.5: {} + postcss@8.5.26: dependencies: nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 - rollup@4.62.4: + rolldown@1.2.4: dependencies: - '@types/estree': 1.0.9 + '@oxc-project/types': 0.144.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@napi-rs/lzma-linux-x64-gnu': 1.5.1 - '@rollup/rollup-android-arm-eabi': 4.62.4 - '@rollup/rollup-android-arm64': 4.62.4 - '@rollup/rollup-darwin-arm64': 4.62.4 - '@rollup/rollup-darwin-x64': 4.62.4 - '@rollup/rollup-freebsd-arm64': 4.62.4 - '@rollup/rollup-freebsd-x64': 4.62.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 - '@rollup/rollup-linux-arm-musleabihf': 4.62.4 - '@rollup/rollup-linux-arm64-gnu': 4.62.4 - '@rollup/rollup-linux-arm64-musl': 4.62.4 - '@rollup/rollup-linux-loong64-gnu': 4.62.4 - '@rollup/rollup-linux-loong64-musl': 4.62.4 - '@rollup/rollup-linux-ppc64-gnu': 4.62.4 - '@rollup/rollup-linux-ppc64-musl': 4.62.4 - '@rollup/rollup-linux-riscv64-gnu': 4.62.4 - '@rollup/rollup-linux-riscv64-musl': 4.62.4 - '@rollup/rollup-linux-s390x-gnu': 4.62.4 - '@rollup/rollup-linux-x64-gnu': 4.62.4 - '@rollup/rollup-linux-x64-musl': 4.62.4 - '@rollup/rollup-openbsd-x64': 4.62.4 - '@rollup/rollup-openharmony-arm64': 4.62.4 - '@rollup/rollup-win32-arm64-msvc': 4.62.4 - '@rollup/rollup-win32-ia32-msvc': 4.62.4 - '@rollup/rollup-win32-x64-gnu': 4.62.4 - '@rollup/rollup-win32-x64-msvc': 4.62.4 - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.2.4 + '@rolldown/binding-darwin-arm64': 1.2.4 + '@rolldown/binding-darwin-x64': 1.2.4 + '@rolldown/binding-freebsd-x64': 1.2.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.4 + '@rolldown/binding-linux-arm64-gnu': 1.2.4 + '@rolldown/binding-linux-arm64-musl': 1.2.4 + '@rolldown/binding-linux-ppc64-gnu': 1.2.4 + '@rolldown/binding-linux-s390x-gnu': 1.2.4 + '@rolldown/binding-linux-x64-gnu': 1.2.4 + '@rolldown/binding-linux-x64-musl': 1.2.4 + '@rolldown/binding-openharmony-arm64': 1.2.4 + '@rolldown/binding-win32-arm64-msvc': 1.2.4 + '@rolldown/binding-win32-x64-msvc': 1.2.4 siginfo@2.0.0: {} @@ -1118,17 +932,18 @@ snapshots: stackback@0.0.2: {} - std-env@3.10.0: {} + std-env@4.2.0: {} tinybench@2.9.0: {} - tinyexec@0.3.2: {} + tinyexec@1.3.0: {} - tinypool@1.1.1: {} - - tinyrainbow@1.2.0: {} + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 - tinyspy@3.0.2: {} + tinyrainbow@3.1.1: {} tsx@4.23.12: dependencies: @@ -1140,67 +955,45 @@ snapshots: undici-types@6.21.0: {} - vite-node@2.1.9(@types/node@22.20.1): + vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.2)(tsx@4.23.12): dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 1.1.2 - vite: 5.4.21(@types/node@22.20.1) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vite@5.4.21(@types/node@22.20.1): - dependencies: - esbuild: 0.21.5 + lightningcss: 1.33.0 + picomatch: 4.0.5 postcss: 8.5.26 - rollup: 4.62.4 + rolldown: 1.2.4 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 22.20.1 + esbuild: 0.28.2 fsevents: 2.3.3 + tsx: 4.23.12 - vitest@2.1.9(@types/node@22.20.1): + vitest@4.1.10(@types/node@22.20.1)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.2)(tsx@4.23.12)): dependencies: - '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.20.1)) - '@vitest/pretty-format': 2.1.9 - '@vitest/runner': 2.1.9 - '@vitest/snapshot': 2.1.9 - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - debug: 4.4.3 + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.2)(tsx@4.23.12)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 expect-type: 1.4.0 magic-string: 0.30.21 - pathe: 1.1.2 - std-env: 3.10.0 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.1.1 - tinyrainbow: 1.2.0 - vite: 5.4.21(@types/node@22.20.1) - vite-node: 2.1.9(@types/node@22.20.1) + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.28.2)(tsx@4.23.12) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.20.1 transitivePeerDependencies: - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser why-is-node-running@2.3.0: dependencies: