pax_global_header00006660000000000000000000000064152203416130014507gustar00rootroot0000000000000052 comment=7b5f87de149d699c0bd955867fe5d57418b6ec68 ludviglundgren-qbittorrent-cli-4dbbde7/000077500000000000000000000000001522034161300204355ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/.github/000077500000000000000000000000001522034161300217755ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/.github/FUNDING.yml000066400000000000000000000000271522034161300236110ustar00rootroot00000000000000github: ludviglundgren ludviglundgren-qbittorrent-cli-4dbbde7/.github/dependabot.yml000066400000000000000000000012771522034161300246340ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: weekly day: saturday time: "07:00" groups: github: patterns: - "*" - package-ecosystem: gomod directory: / schedule: interval: monthly groups: golang: patterns: - "*" ludviglundgren-qbittorrent-cli-4dbbde7/.github/workflows/000077500000000000000000000000001522034161300240325ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/.github/workflows/docs.yml000066400000000000000000000022141522034161300255040ustar00rootroot00000000000000name: Deploy docs on: push: branches: - master paths: - "docs/**" - ".github/workflows/docs.yml" workflow_dispatch: # Allow the workflow to deploy to GitHub Pages. permissions: contents: read pages: write id-token: write # Allow one concurrent deployment, without cancelling in-progress runs. concurrency: group: pages cancel-in-progress: false jobs: build: name: Build site runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 # Enables GitHub Pages for the repo on first run, so no manual # "Settings -> Pages -> Source: GitHub Actions" step is required. - name: Setup Pages uses: actions/configure-pages@v6 with: enablement: true - name: Build with Astro uses: withastro/action@v6 with: path: ./docs deploy: name: Deploy to GitHub Pages needs: build runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v5 ludviglundgren-qbittorrent-cli-4dbbde7/.github/workflows/release.yml000066400000000000000000000053561522034161300262060ustar00rootroot00000000000000name: goreleaser on: pull_request: push: branches: - "master" - "develop" tags: - 'v*' env: GO_VERSION: '1.25' jobs: test: name: Test runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Test run: go test -v ./... docs: name: Check generated docs runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 - name: Set up Go uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Generate docs run: make docs - name: Verify docs are up to date run: | # Use porcelain so newly added (untracked) command pages are caught too. if [ -n "$(git status --porcelain -- docs/src/content/docs/commands)" ]; then echo "::error::Generated command docs are out of date. Run 'make docs' and commit the result." git status --porcelain -- docs/src/content/docs/commands git diff -- docs/src/content/docs/commands exit 1 fi build: name: Build Go binaries needs: [ test ] if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Run GoReleaser build uses: goreleaser/goreleaser-action@v7 with: distribution: goreleaser version: "~> v2" args: release --clean --skip=validate --skip=publish --parallelism 5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload assets uses: actions/upload-artifact@v7 with: name: qbittorrent-cli path: | dist/*.tar.gz dist/*.zip release: name: Build and publish binaries needs: [ test ] if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Run GoReleaser uses: goreleaser/goreleaser-action@v7 with: version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ludviglundgren-qbittorrent-cli-4dbbde7/.gitignore000066400000000000000000000001021522034161300224160ustar00rootroot00000000000000.idea .vagrant .DS_Store .qbt.toml .devcontainer dist bin testdataludviglundgren-qbittorrent-cli-4dbbde7/.goreleaser.yml000066400000000000000000000007541522034161300233740ustar00rootroot00000000000000version: 2 env: - GO111MODULE=on before: hooks: - go mod download builds: - main: ./cmd/qbt/main.go binary: qbt env: - CGO_ENABLED=0 goos: - linux - windows - darwin goarch: - 386 - amd64 - arm - arm64 ignore: - goos: windows goarch: arm - goos: windows goarch: arm64 release: prerelease: auto changelog: sort: asc filters: exclude: - '^docs:' - '^test:' ludviglundgren-qbittorrent-cli-4dbbde7/.qbt.toml.example000066400000000000000000000025561522034161300236400ustar00rootroot00000000000000[qbittorrent] addr = "http://127.0.0.1:6776" # qbittorrent webui-api hostname/ip #apikey = "APIKEY" # qbittorrent webui-api api key, qbit 5.2.x+ (optional) login = "user" # qbittorrent webui-api user (optional) password = "password" # qbittorrent webui-api password (optional) #basicUser = "user" # qbittorrent webui-api basic auth user (optional) #basicPass = "password" # qbittorrent webui-api basic auth password (optional) # some trackers are bugged and need to reannounce before torrent can start [reannounce] enabled = true # true or false attempts = 10 # attempts to run. Run max 10-30 times interval = 7000 # interval between attempts in milliseconds [rules] enabled = true # enable or disable rules max_active_downloads = 2 # set max active downloads [[compare]] addr = "http://100.100.100.100:6776" login = "user" password = "password" #basicUser = "user" #basicPass = "password" [[compare]] # you can specify multiple compare blocks addr = "http://100.100.100.101:6776" login = "user" password = "password" #basicUser = "user" #basicPass = "password" [add] # download pieces in sequential order by default, useful for video streaming sequential = false # prioritize first and last pieces for all new torrents first_last_piece = false ludviglundgren-qbittorrent-cli-4dbbde7/LICENSE000066400000000000000000000020601522034161300214400ustar00rootroot00000000000000MIT License Copyright (c) 2023 Ludvig Lundgren Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ludviglundgren-qbittorrent-cli-4dbbde7/Makefile000066400000000000000000000001711522034161300220740ustar00rootroot00000000000000.PHONY: deps build docs deps: go mod tidy build: go build -o bin/qbt cmd/qbt/main.go docs: go run ./tools/gen-docs ludviglundgren-qbittorrent-cli-4dbbde7/README.md000066400000000000000000000126511522034161300217210ustar00rootroot00000000000000# qbittorrent-cli [![goreleaser](https://github.com/ludviglundgren/qbittorrent-cli/actions/workflows/release.yml/badge.svg)](https://github.com/ludviglundgren/qbittorrent-cli/actions/workflows/release.yml) A CLI to manage qBittorrent. Add torrents, manage categories and tags, reannounce, and import from other clients. > [!NOTE] > **📖 Full documentation lives at [ludviglundgren.github.io/qbittorrent-cli](https://ludviglundgren.github.io/qbittorrent-cli/).** > The complete, always up-to-date command and flag reference is generated directly from the source - see [Command reference](#command-reference) below. ## Features * qBittorrent v5 compatible (since v2.1.0) * Add torrents from file, glob pattern, URL or magnet - handy with autodl-irssi * Reannounce torrents for troublesome trackers * Limit how many torrents download simultaneously * Import torrents with state from Deluge and rTorrent * Manage categories and tags * Compare torrents between instances * Self updater ## Install `qbt` is a single static binary for Linux, macOS and Windows. Download the [latest release](https://github.com/ludviglundgren/qbittorrent-cli/releases/latest), extract it, and move it somewhere in your `$PATH`: ```shell tar -xzvf qbittorrent-cli_${VERSION}_linux_amd64.tar.gz sudo mv qbt /usr/bin/ qbt # prints usage ``` Or install with Go: ```shell go install github.com/ludviglundgren/qbittorrent-cli/cmd/qbt@latest ``` See the [installation guide](https://ludviglundgren.github.io/qbittorrent-cli/getting-started/installation/) for building from source and other options. ## Configuration Create `.qbt.toml` in `~/.config/qbt/`: ```shell mkdir -p ~/.config/qbt && touch ~/.config/qbt/.qbt.toml ``` A bare-minimum config - see the [full example](.qbt.toml.example): ```toml [qbittorrent] addr = "http://127.0.0.1:6776" # qbittorrent webui-api hostname/ip apikey = "APIKEY" # qbittorrent webui-api api key, qbit 5.2.x+ (optional) login = "user" # qbittorrent webui-api user (optional) password = "password" # qbittorrent webui-api password (optional) [rules] enabled = true # enable or disable rules max_active_downloads = 2 # set max active downloads ``` The [configuration guide](https://ludviglundgren.github.io/qbittorrent-cli/getting-started/configuration/) documents every option, including `[reannounce]`, `[add]`, `[[compare]]` instances and autodl-irssi / ruTorrent setup. ## Usage Run `qbt help` or `qbt [command] --help` for details on any command. ```shell # Add a torrent with a category and tags qbt torrent add my-file.torrent --category linux-iso --tags iso # Add everything matching a glob, paused qbt torrent add ./files/*.torrent --paused --skip-hash-check # List downloading torrents in a category qbt torrent list --filter downloading --category linux-iso # Reannounce a torrent that is stuck on the tracker qbt torrent reannounce --hash # Move torrents from NVMe to HDD after 45 minutes of seeding (handy in cron) qbt torrent category move --from nvme --to hdd --min-seed-time 45 # Set a share ratio limit on all torrents qbt torrent share-limit set --all --ratio 2.0 # Import torrents (and their state) from Deluge - always dry-run first qbt torrent import deluge \ --source-dir ~/.config/deluge/state \ --qbit-dir ~/.local/share/qBittorrent/BT_backup \ --dry-run ``` ## Command reference The full reference for every command and flag is generated from the source and published at [ludviglundgren.github.io/qbittorrent-cli](https://ludviglundgren.github.io/qbittorrent-cli/commands/qbt/). | Command | Description | | ---------------------------------------------------------------------------------------- | ------------------------------------------------------- | | [`torrent`](https://ludviglundgren.github.io/qbittorrent-cli/commands/qbt_torrent/) | Add, list, export, import and manage torrents | | [`category`](https://ludviglundgren.github.io/qbittorrent-cli/commands/qbt_category/) | Add, edit, delete and list categories | | [`tag`](https://ludviglundgren.github.io/qbittorrent-cli/commands/qbt_tag/) | Add, delete and list tags | | [`transfer`](https://ludviglundgren.github.io/qbittorrent-cli/commands/qbt_transfer/) | Show transfer / session status and speeds | | [`app`](https://ludviglundgren.github.io/qbittorrent-cli/commands/qbt_app/) | Show qBittorrent application and Web API versions | | [`bencode`](https://ludviglundgren.github.io/qbittorrent-cli/commands/qbt_bencode/) | Edit bencode files such as `.fastresume` | | [`version`](https://ludviglundgren.github.io/qbittorrent-cli/commands/qbt_version/) | Print `qbt` version info | | [`update`](https://ludviglundgren.github.io/qbittorrent-cli/commands/qbt_update/) | Update `qbt` to the latest version | ## Contributing The command reference under `docs/src/content/docs/commands/` is generated - do not edit it by hand. After changing a command or flag, regenerate it: ```shell make docs ``` CI fails if the committed docs are out of date. The documentation site is built with [Starlight](https://starlight.astro.build/) and lives in [`docs/`](docs/). ## License [MIT](LICENSE) ludviglundgren-qbittorrent-cli-4dbbde7/cmd/000077500000000000000000000000001522034161300212005ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/cmd/app.go000066400000000000000000000033451522034161300223140ustar00rootroot00000000000000package cmd import ( "fmt" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunApp cmd for application info func RunApp() *cobra.Command { var command = &cobra.Command{ Use: "app", Short: "App subcommand", Long: "Do various app actions", } command.AddCommand(RunAppVersion()) return command } // RunAppVersion cmd to view application info func RunAppVersion() *cobra.Command { var command = &cobra.Command{ Use: "version", Short: "Get qBittorrent version info", } var ( output string ) command.Flags().StringVar(&output, "output", "", "Print as [formatted text (default), json]") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } appVersion, err := qb.GetAppVersionCtx(ctx) if err != nil { return errors.Wrap(err, "could not get app version") } webapiVersion, err := qb.GetWebAPIVersionCtx(ctx) if err != nil { return errors.Wrap(err, "could not get web api version") } switch output { case "json": fmt.Printf(`{"app_version":"%s","api_version":"%s"}`, appVersion, webapiVersion) default: fmt.Printf("qBittorrent version info:\nApp version: %s\nAPI version: %s\n", appVersion, webapiVersion) } return nil } return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/bencode.go000066400000000000000000000064361522034161300231370ustar00rootroot00000000000000package cmd import ( "io/fs" "log" "os" "path/filepath" "strings" "github.com/ludviglundgren/qbittorrent-cli/pkg/qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/zeebo/bencode" ) // RunBencode cmd for bencode operations func RunBencode() *cobra.Command { var command = &cobra.Command{ Use: "bencode", Short: "Bencode subcommand", Long: `Do various bencode operations`, } command.AddCommand(RunBencodeEdit()) return command } func RunBencodeEdit() *cobra.Command { var command = &cobra.Command{ Use: "edit", Short: "edit bencode data", Long: "Edit bencode files like .fastresume. Shut down client and make a backup of data before.", Example: ` qbt bencode edit --dir /home/user/.local/share/qBittorrent/BT_backup --pattern '/home/user01/torrents' --replace '/home/test/torrents'`, } var ( dry bool verbose bool dir string pattern string replace string ) command.Flags().BoolVar(&dry, "dry-run", false, "Dry run, don't write changes") command.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") command.Flags().StringVar(&dir, "dir", "", "Dir with fast-resume files (required)") command.Flags().StringVar(&pattern, "pattern", "", "Pattern to change (required)") command.Flags().StringVar(&replace, "replace", "", "Text to replace pattern with (required)") command.MarkFlagRequired("dir") command.MarkFlagRequired("pattern") command.MarkFlagRequired("replace") command.RunE = func(cmd *cobra.Command, args []string) error { // make sure dir exists before walk _, err := os.Stat(dir) if err != nil { if os.IsNotExist(err) { return errors.Wrapf(err, "Directory does not exist: %s\n", dir) } return errors.Wrapf(err, "Directory error: %s\n", dir) } matchedFiles := 0 err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } matched, err := filepath.Match("*.fastresume", info.Name()) if err != nil { return errors.Wrap(err, "error matching files") } if matched { matchedFiles++ if err := processFastResume(path, pattern, replace, verbose, dry); err != nil { return errors.Wrapf(err, "error processing file: %s", path) } } return nil }) if err != nil { return errors.Wrap(err, "error reading files") } log.Printf("Found, matched and replaced in '%d' files\n", matchedFiles) return nil } return command } func processFastResume(path, pattern, replace string, verbose, dry bool) error { if dry { if verbose { log.Printf("dry-run: replaced: '%s' with '%s' in %s\n", pattern, replace, path) } } else { read, err := os.ReadFile(path) if err != nil { return errors.Wrapf(err, "error reading file: %s", path) } var fastResume qbittorrent.Fastresume if err := bencode.DecodeString(string(read), &fastResume); err != nil { return errors.Wrapf(err, "could not decode fastresume: %s", path) } fastResume.SavePath = strings.Replace(fastResume.SavePath, pattern, replace, -1) if err = fastResume.Encode(path); err != nil { return errors.Wrapf(err, "could not create qBittorrent fastresume file: %s", path) } if verbose { log.Printf("Replaced: '%s' with '%s' in %s\n", pattern, replace, path) } } return nil } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/category.go000066400000000000000000000157101522034161300233500ustar00rootroot00000000000000package cmd import ( "encoding/json" "fmt" "log" "os" "text/template" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunCategory cmd for category actions func RunCategory() *cobra.Command { var command = &cobra.Command{ Use: "category", Short: "Category subcommand", Long: `Do various category actions`, } command.AddCommand(RunCategoryAdd()) command.AddCommand(RunCategoryDelete()) command.AddCommand(RunCategoryEdit()) command.AddCommand(RunCategoryList()) return command } // RunCategoryList cmd to add categories func RunCategoryList() *cobra.Command { var command = &cobra.Command{ Use: "list", Short: "List categories", Long: `List categories`, } var ( output string ) command.Flags().StringVar(&output, "output", "", "Print as [formatted text (default), json]") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } cats, err := qb.GetCategoriesCtx(ctx) if err != nil { return errors.Wrap(err, "could not get categories") } if len(cats) == 0 { log.Println("No categories found") return nil } switch output { case "json": res, err := json.Marshal(cats) if err != nil { return errors.Wrap(err, "could not marshal categories") } fmt.Println(string(res)) default: if err := printCategoryList(cats); err != nil { return errors.Wrap(err, "could not print category list") } } return nil } return command } var categoryItemTemplate = `{{ range .}} Name: {{.Name}} Save path: {{.SavePath}} {{end}} ` func printCategoryList(categories map[string]qbittorrent.Category) error { tmpl, err := template.New("category-list").Parse(categoryItemTemplate) if err != nil { return err } err = tmpl.Execute(os.Stdout, categories) if err != nil { return errors.Wrap(err, "could not generate template") } return nil } // RunCategoryAdd cmd to add categories func RunCategoryAdd() *cobra.Command { var ( dry bool savePath string ) var command = &cobra.Command{ Use: "add", Short: "Add category", Long: "Add new category", Example: ` qbt category add test-category qbt category add test-category --save-path "/home/user/torrents/test-category"`, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("requires a category as first argument") } return nil }, } command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.Flags().StringVar(&savePath, "save-path", "", "Category default save-path. Optional. Defaults to dir in default save dir.") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } // args // first arg is path to torrent file category := args[0] if dry { log.Printf("dry-run: successfully added category: %s\n", category) return nil } else { if err := qb.CreateCategoryCtx(ctx, category, savePath); err != nil { return errors.Wrap(err, "could not create category") } log.Printf("successfully added category: %s\n", category) } return nil } return command } // RunCategoryDelete cmd to delete categories func RunCategoryDelete() *cobra.Command { var ( dry bool ) var command = &cobra.Command{ Use: "delete", Short: "Delete category", Long: "Delete category", Example: ` qbt category delete test-category`, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("requires a category as first argument") } return nil }, } command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } // args // first arg is path to torrent file category := args[0] if dry { log.Printf("dry-run: successfully deleted category: %s\n", category) return nil } else { if err := qb.RemoveCategoriesCtx(ctx, []string{category}); err != nil { return errors.Wrap(err, "could not delete category") } log.Printf("successfully deleted category: %s\n", category) } return nil } return command } // RunCategoryEdit cmd to edit category func RunCategoryEdit() *cobra.Command { var ( dry bool savePath string ) var command = &cobra.Command{ Use: "edit", Short: "Edit category", Long: "Edit category", Example: ` qbt category edit test-category --save-path "/home/user/new/path" qbt category edit test-category --save-path ""`, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("requires a category as first argument") } return nil }, } command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.Flags().StringVar(&savePath, "save-path", "", "Edit category save-path") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } // args // first arg is path to torrent file category := args[0] if dry { log.Printf("dry-run: successfully edited category: %s\n", category) return nil } else { if err := qb.EditCategoryCtx(ctx, category, savePath); err != nil { return errors.Wrap(err, "could not edit category") } log.Printf("successfully edited category: %s\n", category) } return nil } return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/qbt/000077500000000000000000000000001522034161300217665ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/cmd/qbt/main.go000066400000000000000000000004521522034161300232420ustar00rootroot00000000000000package main import ( "log" "os" "github.com/ludviglundgren/qbittorrent-cli/cmd" ) var ( version = "dev" commit = "none" date = "unknown" ) func main() { log.SetFlags(0) rootCmd := cmd.NewRootCmd(version, commit, date) if err := rootCmd.Execute(); err != nil { os.Exit(1) } } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/root.go000066400000000000000000000027511522034161300225170ustar00rootroot00000000000000package cmd import ( "io" "log" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/spf13/cobra" ) // NewRootCmd builds the root qbt command with all subcommands attached. // // version, commit and date are injected at build time and surfaced by // `qbt version`. It is used both by the CLI entrypoint (cmd/qbt) and by the // documentation generator (tools/gen-docs), so the generated docs always match // the real command tree. func NewRootCmd(version, commit, date string) *cobra.Command { var silentOutput bool rootCmd := &cobra.Command{ Use: "qbt", Short: "Manage qBittorrent with cli", Long: `Manage qBittorrent from command line. Documentation is available at https://github.com/ludviglundgren/qbittorrent-cli`, PersistentPreRun: func(cmd *cobra.Command, args []string) { if silentOutput { log.SetOutput(io.Discard) // Uncomment to also suppress standard output // os.Stdout = io.Discard } }, } // override config rootCmd.PersistentFlags().StringVar(&config.CfgFile, "config", "", "config file (default is $HOME/.config/qbt/.qbt.toml)") rootCmd.PersistentFlags().BoolVarP(&silentOutput, "quiet", "q", false, "suppress output") rootCmd.AddCommand(RunApp()) rootCmd.AddCommand(RunBencode()) rootCmd.AddCommand(RunTorrent()) rootCmd.AddCommand(RunTransfer()) rootCmd.AddCommand(RunCategory()) rootCmd.AddCommand(RunTag()) rootCmd.AddCommand(RunVersion(version, commit, date)) rootCmd.AddCommand(RunUpdate(version)) return rootCmd } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/tag.go000066400000000000000000000114021522034161300223000ustar00rootroot00000000000000package cmd import ( "encoding/json" "fmt" "log" "os" "text/template" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTag cmd for tag actions func RunTag() *cobra.Command { var command = &cobra.Command{ Use: "tag", Short: "Tag subcommand", Long: `Do various tag actions`, } command.AddCommand(RunTagAdd()) command.AddCommand(RunTagDelete()) command.AddCommand(RunTagList()) return command } // RunTagList cmd to list tags func RunTagList() *cobra.Command { var command = &cobra.Command{ Use: "list", Short: "List tags", Long: "List tags.", } var ( output string ) command.Flags().StringVar(&output, "output", "", "Print as [formatted text (default), json]") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } tags, err := qb.GetTagsCtx(ctx) if err != nil { return errors.Wrap(err, "could not get tags") } if len(tags) == 0 { log.Println("No tags found") return nil } switch output { case "json": res, err := json.Marshal(tags) if err != nil { return errors.Wrap(err, "could not parshal tags") } fmt.Println(string(res)) default: if err := printTagsList(tags); err != nil { return err } } return nil } return command } var tagItemTemplate = `{{ range .}} Name: {{.}} {{end}} ` func printTagsList(tags []string) error { tmpl, err := template.New("tags-list").Parse(tagItemTemplate) if err != nil { return errors.Wrap(err, "could not create template") } err = tmpl.Execute(os.Stdout, tags) if err != nil { return errors.Wrap(err, "could not generate tags list template") } return nil } // RunTagAdd cmd to add tags func RunTagAdd() *cobra.Command { var ( dry bool ) var command = &cobra.Command{ Use: "add", Short: "Add tags", Long: "Add tags", Example: ` qbt tag add tag1`, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("requires a tag as first argument") } return nil }, } command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } // args // first arg is tag tag := args[0] if dry { log.Printf("dry-run: successfully created tag: %s\n", tag) return nil } else { if err := qb.CreateTagsCtx(ctx, args); err != nil { return errors.Wrapf(err, "could not create tag: %s", tag) } log.Printf("successfully created tag: %s\n", tag) } return nil } return command } // RunTagDelete cmd to delete tags func RunTagDelete() *cobra.Command { var ( dry bool ) var command = &cobra.Command{ Use: "delete", Short: "Delete tags", Long: "Delete tags.", Example: ` qbt tag delete tag1`, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("requires a tag as first argument") } return nil }, } command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } // args // first arg is tag tag := args[0] if dry { log.Printf("dry-run: successfully deleted tag: %s\n", tag) return nil } else { if err := qb.DeleteTagsCtx(ctx, []string{tag}); err != nil { return errors.Wrapf(err, "could not delete tag: %s", tag) } log.Printf("successfully deleted tag: %s\n", tag) } return nil } return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent.go000066400000000000000000000015541522034161300232310ustar00rootroot00000000000000package cmd import ( "github.com/spf13/cobra" ) // RunTorrent cmd for torrent operations func RunTorrent() *cobra.Command { var command = &cobra.Command{ Use: "torrent", Short: "Torrent subcommand", Long: `Do various torrent operations`, } command.AddCommand(RunTorrentAdd()) command.AddCommand(RunTorrentCategory()) command.AddCommand(RunTorrentCompare()) command.AddCommand(RunTorrentExport()) command.AddCommand(RunTorrentHash()) command.AddCommand(RunTorrentImport()) command.AddCommand(RunTorrentList()) command.AddCommand(RunTorrentPause()) command.AddCommand(RunTorrentReannounce()) command.AddCommand(RunTorrentRecheck()) command.AddCommand(RunTorrentRemove()) command.AddCommand(RunTorrentResume()) command.AddCommand(RunTorrentShareLimit()) command.AddCommand(RunTorrentTag()) command.AddCommand(RunTorrentTracker()) return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_add.go000066400000000000000000000274251522034161300240460ustar00rootroot00000000000000package cmd import ( "context" "io" "log" "net/http" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "time" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/anacrolix/torrent/metainfo" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" ) type QBitStopCondition string const ( QBitStopConditionNone QBitStopCondition = "None" QbitStopConditionMetadataReceived QBitStopCondition = "MetadataReceived" QbitStopConditionFilesChecked QBitStopCondition = "FilesChecked" ) // RunTorrentAdd cmd to add torrents func RunTorrentAdd() *cobra.Command { var ( dry bool paused bool skipHashCheck bool sequential bool firstLastPiece bool removeStalled bool savePath string category string tags []string ignoreRules bool uploadLimit uint64 downloadLimit uint64 stopCondition string sleep time.Duration recheck bool ) command := &cobra.Command{ Use: "add", Short: "Add torrent(s)", Long: `Add new torrent(s) to qBittorrent from file or magnet. Supports glob pattern for files like: ./files/*.torrent`, Example: ` qbt torrent add my-file.torrent --category test --tags tag1 qbt torrent add ./files/*.torrent --paused --skip-hash-check qbt torrent add magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcad53426&dn=download`, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("requires a torrent file, glob or magnet as first argument") } return nil }, } command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.Flags().BoolVar(&paused, "paused", false, "Add torrent in paused state") command.Flags().BoolVar(&skipHashCheck, "skip-hash-check", false, "Skip hash check") command.Flags().BoolVar(&sequential, "sequential", false, "Download torrent pieces in sequential order") command.Flags().BoolVar(&firstLastPiece, "first-last-piece", false, "Prioritize first and last pieces for preview") command.Flags().BoolVar(&ignoreRules, "ignore-rules", false, "Ignore rules from config") command.Flags().BoolVar(&removeStalled, "remove-stalled", false, "Remove stalled torrents from re-announce") command.Flags().StringVar(&savePath, "save-path", "", "Add torrent to the specified path") command.Flags().StringVar(&category, "category", "", "Add torrent to the specified category") command.Flags().StringVar(&stopCondition, "stop-condition", "", "Add torrent with the specified stop condition. Possible values: None, MetadataReceived, FilesChecked. Example: --stop-condition MetadataReceived") command.Flags().Uint64Var(&uploadLimit, "limit-ul", 0, "Set torrent upload speed limit. Unit in bytes/second") command.Flags().Uint64Var(&downloadLimit, "limit-dl", 0, "Set torrent download speed limit. Unit in bytes/second") command.Flags().DurationVar(&sleep, "sleep", 200*time.Millisecond, "Set the amount of time to wait between adding torrents in seconds") command.Flags().StringArrayVar(&tags, "tags", []string{}, "Add tags to torrent") command.Flags().BoolVar(&recheck, "recheck", false, "Force recheck after adding (useful when using --paused)") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() // args // first arg is path to torrent file filePath := args[0] qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } if config.Rules.Enabled && !ignoreRules { activeDownloads, err := qb.GetTorrentsCtx(ctx, qbittorrent.TorrentFilterOptions{Filter: qbittorrent.TorrentFilterDownloading}) if err != nil { return errors.Wrap(err, "could not fetch torrents") } if len(activeDownloads) >= config.Rules.MaxActiveDownloads { log.Printf("max active downloads of (%d) reached, skip adding\n", config.Rules.MaxActiveDownloads) return nil } } options := map[string]string{} if paused { options["paused"] = "true" options["stopped"] = "true" } if skipHashCheck { options["skip_checking"] = "true" } if command.Flags().Changed("sequential") { if sequential { options["sequentialDownload"] = "true" } } else if config.Add.Sequential { options["sequentialDownload"] = "true" } if command.Flags().Changed("first-last-piece") { if firstLastPiece { options["firstLastPiecePrio"] = "true" } } else if config.Add.FirstLastPiece { options["firstLastPiecePrio"] = "true" } if savePath != "" { options["savepath"] = savePath options["autoTMM"] = "false" } if category != "" { options["category"] = category } if tags != nil { options["tags"] = strings.Join(tags, ",") } if stopCondition != "" && (stopCondition == string(QBitStopConditionNone) || stopCondition == string(QbitStopConditionMetadataReceived) || stopCondition == string(QbitStopConditionFilesChecked)) { options["stop_condition"] = stopCondition } if uploadLimit > 0 { options["upLimit"] = strconv.FormatUint(uploadLimit, 10) } if downloadLimit > 0 { options["dlLimit"] = strconv.FormatUint(uploadLimit, 10) } if strings.HasPrefix(filePath, "magnet:") { if dry { log.Printf("dry-run: successfully added torrent from magnet %s!\n", filePath) return nil } if _, err := qb.AddTorrentFromUrlCtx(ctx, filePath, options); err != nil { return errors.Wrapf(err, "adding torrent %s failed", filePath) } hash := "" // some trackers are bugged or slow, so we need to re-announce the torrent until it works if config.Reannounce.Enabled && !paused { magnet, err := metainfo.ParseMagnetUri(filePath) if err != nil { return errors.Wrapf(err, "could not parse magnet URI: %s", filePath) } hash := magnet.InfoHash.String() wg := sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() if err := checkTrackerStatus(ctx, qb, removeStalled, hash); err != nil { log.Fatalf("could not get tracker status for torrent: %q\n", err) } }() wg.Wait() } if paused && recheck { magnet, err := metainfo.ParseMagnetUri(filePath) if err == nil { hash = magnet.InfoHash.String() if err := qb.RecheckCtx(ctx, []string{hash}); err != nil { log.Printf("could not recheck torrent: %s err: %q\n", hash, err) } else { log.Printf("rechecked torrent: %s\n", hash) } } } log.Printf("successfully added torrent from magnet: %s %s\n", filePath, hash) return nil } else { var files []string var err error var tempFile *os.File defer func() { if tempFile != nil { os.Remove(tempFile.Name()) tempFile.Close() } }() if strings.HasPrefix(filePath, "https://") || strings.HasPrefix(filePath, "http://") { tempFile, err = os.CreateTemp("", "qbt-torrent-dl") if err != nil { return errors.Wrap(err, "could not create tmp file") } response, err := http.Get(filePath) if err != nil { return errors.Wrapf(err, "could not download file: %s", filePath) } defer response.Body.Close() if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusCreated && response.StatusCode != http.StatusNoContent { return errors.Errorf("unexpected status: %d", response.StatusCode) } _, err = io.Copy(tempFile, response.Body) if err != nil { return errors.Wrap(err, "could not write download locally") } files = []string{tempFile.Name()} } else if IsGlobPattern(filePath) { files, err = filepath.Glob(filePath) if err != nil { return errors.Wrapf(err, "could not find files matching: %s", filePath) } } else { _, err := os.Lstat(filePath) if err != nil { return errors.Wrapf(err, "could not find file: %s", filePath) } files = []string{filePath} } if len(files) == 0 { log.Printf("found 0 torrents matching %s\n", filePath) return nil } log.Printf("found (%d) torrent(s) to add\n", len(files)) wg := sync.WaitGroup{} success := 0 for _, file := range files { if dry { log.Printf("dry-run: torrent %s successfully added!\n", file) continue } // set savePath again options["savepath"] = savePath if _, err := qb.AddTorrentFromFileCtx(ctx, file, options); err != nil { return errors.Wrapf(err, "could not add torrent: %s", file) } // Get meta info from file to find out the hash for later use t, err := metainfo.LoadFromFile(file) if err != nil { log.Printf("could not open file: %s", file) continue } hash := t.HashInfoBytes().String() if paused && recheck { if err := qb.RecheckCtx(ctx, []string{hash}); err != nil { log.Printf("could not recheck torrent: %s err: %q\n", hash, err) } else { log.Printf("rechecked torrent: %s\n", hash) } } // some trackers are bugged or slow, so we need to re-announce the torrent until it works if config.Reannounce.Enabled && !paused { wg.Add(1) go func() { defer wg.Done() if err := checkTrackerStatus(ctx, qb, removeStalled, hash); err != nil { log.Printf("could not get tracker status for torrent: %s err: %q\n", hash, err) } }() } success++ log.Printf("successfully added torrent: %s\n", hash) if len(files) > 1 { log.Printf("sleeping %v before adding next torrent...\n", sleep) time.Sleep(sleep) continue } } wg.Wait() log.Printf("successfully added %d torrent(s)\n", success) } return nil } return command } // IsGlobPattern reports whether path contains any of the magic characters // recognized by Match. func IsGlobPattern(path string) bool { magicChars := `*?[` if runtime.GOOS != "windows" { magicChars = `*?[\` } return strings.ContainsAny(path, magicChars) } func checkTrackerStatus(ctx context.Context, qb *qbittorrent.Client, removeStalled bool, hash string) error { announceOK := false attempts := 0 time.Sleep(time.Duration(config.Reannounce.Interval) * time.Millisecond) log.Printf("starting reannounce for %s\n", hash) for attempts < config.Reannounce.Attempts { log.Printf("[%d/%d] reannounce attempt for %s\n", attempts, config.Reannounce.Attempts, hash) trackers, err := qb.GetTorrentTrackersCtx(ctx, hash) if err != nil { log.Fatalf("could not get trackers of torrent: %v %v", hash, err) } // check if status not working or something else _, working := findTrackerStatus(trackers, 2) if working { log.Printf("[%d/%d] reannounce found working tracker for %s\n", attempts, config.Reannounce.Attempts, hash) announceOK = true break } if err := qb.ReAnnounceTorrentsCtx(ctx, []string{hash}); err != nil { return err } time.Sleep(time.Duration(config.Reannounce.Interval) * time.Millisecond) attempts++ continue } if !announceOK { if removeStalled { log.Println("Announce not ok, deleting torrent") if err := qb.DeleteTorrentsCtx(ctx, []string{hash}, false); err != nil { return err } } } return nil } // Check if status not working or something else // https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-trackers // // 0 Tracker is disabled (used for DHT, PeX, and LSD) // 1 Tracker has not been contacted yet // 2 Tracker has been contacted and is working // 3 Tracker is updating // 4 Tracker has been contacted, but it is not working (or doesn't send proper replies) func findTrackerStatus(slice []qbittorrent.TorrentTracker, val int) (int, bool) { for i, item := range slice { if int(item.Status) == val { return i, true } } return -1, false } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_category.go000066400000000000000000000200601522034161300251170ustar00rootroot00000000000000package cmd import ( "log" "strings" "time" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/ludviglundgren/qbittorrent-cli/pkg/utils" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTorrentCategory cmd for torrent category operations func RunTorrentCategory() *cobra.Command { var command = &cobra.Command{ Use: "category", Short: "Torrent category subcommand", Long: `Do various torrent category operations`, } command.AddCommand(RunTorrentCategoryChange()) command.AddCommand(RunTorrentCategorySet()) command.AddCommand(RunTorrentCategoryUnSet()) return command } // RunTorrentCategorySet cmd for torrent category operations func RunTorrentCategorySet() *cobra.Command { var command = &cobra.Command{ Use: "set", Short: "Set torrent category", Long: `Set category for torrents via hashes`, Example: ` qbt torrent category set test-category --hashes hash1,hash2`, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("requires a category as first argument") } return nil }, } var ( dry bool hashes []string ) command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.Flags().StringSliceVar(&hashes, "hashes", []string{}, "Torrent hashes, as comma separated list") command.RunE = func(cmd *cobra.Command, args []string) error { if len(hashes) == 0 { return errors.New("no hashes supplied!") } err := utils.ValidateHash(hashes) if err != nil { return errors.Wrap(err, "invalid hashes supplied") } config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } // args // first arg is path to torrent file category := args[0] if dry { log.Printf("dry-run: successfully set category %s on torrents: %v\n", category, hashes) return nil } else { if err := qb.SetCategoryCtx(ctx, hashes, category); err != nil { return errors.Wrapf(err, "could not set category %s on torrents %v", category, hashes) } log.Printf("successfully set category %s on torrents: %v\n", category, hashes) } return nil } return command } // RunTorrentCategoryUnSet cmd for torrent category operations func RunTorrentCategoryUnSet() *cobra.Command { var command = &cobra.Command{ Use: "unset", Short: "Unset torrent category", Long: `Unset category for torrents via hashes`, Example: ` qbt torrent category unset --hashes hash1,hash2`, } var ( dry bool hashes []string ) command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.Flags().StringSliceVar(&hashes, "hashes", []string{}, "Torrent hashes, as comma separated list") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } if dry { log.Printf("dry-run: successfully unset category on torrents: %v\n", hashes) return nil } else { if err := qb.SetCategoryCtx(ctx, hashes, ""); err != nil { return errors.Wrapf(err, "could not unset category on torrents %v", hashes) } log.Printf("successfully unset category on torrents: %v\n", hashes) } return nil } return command } // RunTorrentCategoryChange cmd to move torrents from some category to another func RunTorrentCategoryChange() *cobra.Command { var ( dry bool fromCategories []string targetCategory string includeTags []string excludeTags []string minSeedTime int ) var command = &cobra.Command{ Use: "move", Short: "move torrents between categories", Long: `Move torrents from one category to another`, Example: ` qbt torrent category move --from cat1 --to cat2`, } command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.Flags().StringSliceVar(&fromCategories, "from", []string{}, "Move from categories (required)") command.Flags().StringVar(&targetCategory, "to", "", "Move to the specified category (required)") command.Flags().StringSliceVar(&includeTags, "include-tags", []string{}, "Include torrents with provided tags") command.Flags().StringSliceVar(&excludeTags, "exclude-tags", []string{}, "Exclude torrents with provided tags") command.Flags().IntVar(&minSeedTime, "min-seed-time", 0, "Minimum seed time in MINUTES before moving.") command.MarkFlagRequired("from") command.MarkFlagRequired("to") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } var hashes []string for _, cat := range fromCategories { torrents, err := qb.GetTorrentsCtx(ctx, qbittorrent.TorrentFilterOptions{Category: cat}) if err != nil { return errors.Wrapf(err, "could not get torrents by category: %s", cat) } for _, torrent := range torrents { // only grab completed torrents since we don't specify filter state if torrent.Progress != 1 { continue } if len(includeTags) > 0 { if _, validTag := validateTag(includeTags, torrent.Tags); !validTag { continue } } if len(excludeTags) > 0 { if tag, found := validateTag(excludeTags, torrent.Tags); found { log.Printf("ignoring torrent %s %s containng tag: %s of tags: %s", torrent.Name, torrent.Hash, tag, excludeTags) continue } } // check TimeActive (seconds), CompletionOn (epoch) SeenComplete if minSeedTime > 0 { completedTime := time.Unix(torrent.CompletionOn, 0) completedTimePlusMinSeedTime := completedTime.Add(time.Duration(minSeedTime) * time.Minute) currentTime := time.Now() diff := currentTime.After(completedTimePlusMinSeedTime) if !diff { continue } } hashes = append(hashes, torrent.Hash) } } if len(hashes) == 0 { log.Printf("Could not find any matching torrents to move from (%s) to (%s) with tags (%s) and min-seed-time %d minutes\n", strings.Join(fromCategories, ","), targetCategory, strings.Join(includeTags, ","), minSeedTime) return nil } if dry { log.Printf("dry-run: Found %d matching torrents to move from (%s) to (%s)\n", len(hashes), strings.Join(fromCategories, ","), targetCategory) log.Printf("dry-run: Successfully moved %d torrents from (%s) to (%s)\n", len(hashes), strings.Join(fromCategories, ","), targetCategory) } else { log.Printf("Found %d matching torrents to move from (%s) to (%s)\n", len(hashes), strings.Join(fromCategories, ","), targetCategory) if err := qb.SetCategoryCtx(ctx, hashes, targetCategory); err != nil { return errors.Wrapf(err, "could not pause torrents: %v", hashes) } log.Printf("Successfully moved %d torrents from (%s) to (%s)\n", len(hashes), strings.Join(fromCategories, ","), targetCategory) } return nil } return command } func validateTag(includeTags []string, torrentTags string) (string, bool) { tagList := strings.Split(torrentTags, ", ") for _, includeTag := range includeTags { for _, tag := range tagList { if tag == includeTag { return tag, true } } } return "", false } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_compare.go000066400000000000000000000152451522034161300247410ustar00rootroot00000000000000package cmd import ( "log" "time" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/autobrr/go-qbittorrent" "github.com/dustin/go-humanize" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTorrentCompare cmd to compare torrents between clients func RunTorrentCompare() *cobra.Command { var ( dry bool tagDuplicates bool tag string sourceAddr string sourceAPIKey string sourceUser string sourcePass string sourceBasicUser string sourceBasicPass string compareAddr string compareAPIKey string compareUser string comparePass string compareBasicUser string compareBasicPass string ) var command = &cobra.Command{ Use: "compare", Short: "Compare torrents", Long: `Compare torrents between clients`, Example: ` qbt torrent compare --addr http://localhost:10000 --user u --pass p --compare-addr http://url.com:10000 --compare-user u --compare-pass p`, //Args: func(cmd *cobra.Command, args []string) error { // if len(args) < 1 { // return errors.New("requires a torrent file as first argument") // } // // return nil //}, } command.Flags().BoolVar(&dry, "dry-run", false, "dry run") command.Flags().BoolVar(&tagDuplicates, "tag-duplicates", false, "tag duplicates on compare") command.Flags().StringVar(&tag, "tag", "compare-dupe", "set a custom tag for duplicates on compare. default: compare-dupe") command.Flags().StringVar(&sourceAddr, "host", "", "Source host") command.Flags().StringVar(&compareAPIKey, "api-key", "", "Source api key") command.Flags().StringVar(&sourceUser, "user", "", "Source user") command.Flags().StringVar(&sourcePass, "pass", "", "Source pass") command.Flags().StringVar(&sourceBasicUser, "basic-user", "", "Source basic auth user") command.Flags().StringVar(&sourceBasicPass, "basic-pass", "", "Source basic auth pass") command.Flags().StringVar(&compareAddr, "compare-host", "", "Secondary host") command.Flags().StringVar(&compareAPIKey, "compare-api-key", "", "Secondary api key") command.Flags().StringVar(&compareUser, "compare-user", "", "Secondary user") command.Flags().StringVar(&comparePass, "compare-pass", "", "Secondary pass") command.Flags().StringVar(&compareBasicUser, "compare-basic-user", "", "Secondary basic auth user") command.Flags().StringVar(&compareBasicPass, "compare-basic-pass", "", "Secondary basic auth pass") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() if sourceAddr == "" { sourceAddr = config.Qbit.Host } if sourceAPIKey == "" { sourceAPIKey = config.Qbit.APIKey } if sourceUser == "" { sourceUser = config.Qbit.Login } if sourcePass == "" { sourcePass = config.Qbit.Password } if sourceBasicUser == "" { sourceBasicUser = config.Qbit.BasicUser } if sourceBasicPass == "" { sourceBasicPass = config.Qbit.BasicPass } qbtSettings := qbittorrent.Config{ Host: sourceAddr, APIKey: sourceAPIKey, Username: sourceUser, Password: sourcePass, BasicUser: sourceBasicUser, BasicPass: sourceBasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } sourceData, err := qb.GetTorrentsCtx(ctx, qbittorrent.TorrentFilterOptions{}) if err != nil { return errors.Wrap(err, "could not get torrents") } log.Printf("Found: %d torrents on source\n", len(sourceData)) // Start comparison for _, compareConfig := range config.Compare { compareAddr := compareConfig.Addr compareAPIKey := compareConfig.APIKey compareUser := compareConfig.Login comparePass := compareConfig.Password compareBasicUser := compareConfig.BasicUser compareBasicPass := compareConfig.BasicPass qbtSettingsCompare := qbittorrent.Config{ Host: compareAddr, APIKey: compareAPIKey, Username: compareUser, Password: comparePass, BasicUser: compareBasicUser, BasicPass: compareBasicPass, } qbCompare := qbittorrent.NewClient(qbtSettingsCompare) if err = qbCompare.LoginCtx(ctx); err != nil { return errors.Wrapf(err, "could not login to qbit: %s", compareAddr) } compareData, err := qbCompare.GetTorrentsCtx(ctx, qbittorrent.TorrentFilterOptions{}) if err != nil { return errors.Wrapf(err, "could not get torrents from compare client") } log.Printf("Found: %d torrents on compare\n", len(compareData)) duplicateTorrents, err := compare(sourceData, compareData) if err != nil { return err } // Process duplicate torrents if tagDuplicates { if !dry { log.Printf("found: %d duplicate torrents from compare %s\n", len(duplicateTorrents), compareAddr) batch := 20 for i := 0; i < len(duplicateTorrents); i += batch { j := i + batch if j > len(duplicateTorrents) { j = len(duplicateTorrents) } if err := qbCompare.AddTagsCtx(ctx, duplicateTorrents[i:j], tag); err != nil { return errors.Wrapf(err, "could not set tag: %s", tag) } // sleep before next request time.Sleep(time.Second * 1) } } else { log.Printf("dry-run: found: %d duplicate torrents from compare %s\n", len(duplicateTorrents), compareAddr) } } // --rm-duplicates // --save save to file } return nil } return command } func compare(source, compare []qbittorrent.Torrent) ([]string, error) { sourceTorrents := make(map[string]qbittorrent.Torrent, 0) for _, s := range source { sourceTorrents[s.Hash] = qbittorrent.Torrent{ Category: s.Category, Downloaded: s.Downloaded, Hash: s.Hash, Name: s.Name, Progress: s.Progress, Ratio: s.Ratio, Size: s.Size, State: s.State, Tags: s.Tags, Tracker: s.Tracker, Uploaded: s.Uploaded, } } duplicateTorrentIDs := make([]string, 0) duplicateTorrentsSlice := make([]qbittorrent.Torrent, 0) var totalSize uint64 for _, c := range compare { if _, ok := sourceTorrents[c.Hash]; ok { duplicateTorrentIDs = append(duplicateTorrentIDs, c.Hash) totalSize += uint64(c.Size) duplicateTorrentsSlice = append(duplicateTorrentsSlice, qbittorrent.Torrent{ Category: c.Category, Downloaded: c.Downloaded, Hash: c.Hash, Name: c.Name, Progress: c.Progress, Ratio: c.Ratio, Size: c.Size, State: c.State, Tags: c.Tags, Tracker: c.Tracker, Uploaded: c.Uploaded, }) } } // print duplicates log.Printf("Found: %d duplicate torrents\n", len(duplicateTorrentsSlice)) log.Printf("Total reclaimable space: %v\n", humanize.Bytes(totalSize)) return duplicateTorrentIDs, nil } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_compare_test.go000066400000000000000000000023171522034161300257740ustar00rootroot00000000000000package cmd import ( "testing" "github.com/autobrr/go-qbittorrent" "github.com/magiconair/properties/assert" ) func Test_compare(t *testing.T) { type args struct { source []qbittorrent.Torrent compare []qbittorrent.Torrent } tests := []struct { name string args args matches int wantErr bool }{ { name: "", args: args{ source: []qbittorrent.Torrent{ {Name: "test_1", Hash: "1ED3AEA7B65B9EFF17F85AB8240AFB896BA6A6FD"}, {Name: "test_2", Hash: "CB93E8D27A32F80FB28FFD9BD07BF0A25912F3A5"}, }, compare: []qbittorrent.Torrent{ {Name: "test_1", Hash: "1ED3AEA7B65B9EFF17F85AB8240AFB896BA6A6FD"}, }, }, matches: 1, wantErr: false, }, { name: "", args: args{ source: []qbittorrent.Torrent{ {Name: "test_2", Hash: "CB93E8D27A32F80FB28FFD9BD07BF0A25912F3A5"}, }, compare: []qbittorrent.Torrent{ {Name: "test_1", Hash: "1ED3AEA7B65B9EFF17F85AB8240AFB896BA6A6FD"}, }, }, matches: 0, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if matches, err := compare(tt.args.source, tt.args.compare); (err != nil) != tt.wantErr { assert.Equal(t, matches, tt.matches) } }) } } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_export.go000066400000000000000000000425121522034161300246310ustar00rootroot00000000000000package cmd import ( "encoding/json" "fmt" "io/fs" "log" "os" "path/filepath" "strings" "time" "github.com/ludviglundgren/qbittorrent-cli/internal/config" fsutil "github.com/ludviglundgren/qbittorrent-cli/internal/fs" "github.com/ludviglundgren/qbittorrent-cli/pkg/archive" qbit "github.com/ludviglundgren/qbittorrent-cli/pkg/qbittorrent" "github.com/ludviglundgren/qbittorrent-cli/pkg/utils" "github.com/anacrolix/torrent/metainfo" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/zeebo/bencode" ) func RunTorrentExport() *cobra.Command { var command = &cobra.Command{ Use: "export", Short: "Export torrents", Long: "Export torrents and fastresume by category", Example: ` qbt torrent export --source ~/.local/share/data/qBittorrent/BT_backup --export-dir ~/qbt-backup --include-category=movies,tv`, } f := export{ dry: false, verbose: false, archive: false, sourceDir: "", exportDir: "", includeCategory: nil, excludeCategory: nil, includeTag: nil, excludeTag: nil, tags: map[string]struct{}{}, category: map[string]qbittorrent.Category{}, hashes: map[string]qbittorrent.Torrent{}, } var skipManifest bool command.Flags().BoolVar(&f.dry, "dry-run", false, "dry run") command.Flags().BoolVarP(&f.verbose, "verbose", "v", false, "verbose output") command.Flags().BoolVarP(&f.archive, "archive", "a", false, "archive export dir to .tar.gz") command.Flags().BoolVar(&skipManifest, "skip-manifest", false, "Do not export all used tags and categories into manifest") command.Flags().StringVar(&f.sourceDir, "source", "", "Dir with torrent and fast-resume files (required)") command.Flags().StringVar(&f.exportDir, "export-dir", "", "Dir to export files to (required)") command.Flags().StringSliceVar(&f.includeCategory, "include-category", []string{}, "Export torrents from these categories. Comma separated") command.Flags().StringSliceVar(&f.excludeCategory, "exclude-category", []string{}, "Exclude categories. Comma separated") command.Flags().StringSliceVar(&f.includeTag, "include-tag", []string{}, "Include tags. Comma separated") command.Flags().StringSliceVar(&f.excludeTag, "exclude-tag", []string{}, "Exclude tags. Comma separated") command.MarkFlagRequired("source") command.MarkFlagRequired("export-dir") command.MarkFlagsMutuallyExclusive("include-category", "exclude-category") command.MarkFlagsMutuallyExclusive("include-tag", "exclude-tag") command.RunE = func(cmd *cobra.Command, args []string) error { var err error f.sourceDir, err = utils.ExpandTilde(f.sourceDir) if err != nil { return errors.Wrap(err, "could not read source-dir") } f.exportDir, err = utils.ExpandTilde(f.exportDir) if err != nil { return errors.Wrap(err, "could not read export-dir") } if _, err := os.Stat(f.sourceDir); err != nil { if os.IsNotExist(err) { return errors.Wrapf(err, "source dir %s does not exist", f.sourceDir) } return err } log.Printf("Preparing to export torrents using source-dir: %q export-dir: %q\n", f.sourceDir, f.exportDir) // get torrents from client by categories config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrapf(err, "failed to login") } var torrents []qbittorrent.Torrent if len(f.includeCategory) > 0 { for _, category := range f.includeCategory { torrentsByCategory, err := qb.GetTorrentsCtx(ctx, qbittorrent.TorrentFilterOptions{Category: category}) if err != nil { return errors.Wrapf(err, "could not get torrents for category: %s", category) } torrents = append(torrents, torrentsByCategory...) } } else { torrents, err = qb.GetTorrentsCtx(ctx, qbittorrent.TorrentFilterOptions{}) if err != nil { return errors.Wrap(err, "could not get torrents") } } for _, tor := range torrents { // only grab completed torrents //if tor.Progress != 1 { // continue //} if len(f.excludeCategory) > 0 && containsCategory(f.excludeCategory, tor.Category) { continue } if tor.Tags != "" { tags := strings.Split(tor.Tags, ", ") // check tags and exclude categories if len(f.includeTag) > 0 && !containsTag(f.includeTag, tags) { continue } if len(f.excludeTag) > 0 && containsTag(f.excludeTag, tags) { continue } for _, tag := range tags { _, ok := f.tags[tag] if !ok { f.tags[tag] = struct{}{} } } } if tor.Category != "" { f.category[tor.Category] = qbittorrent.Category{ Name: tor.Category, SavePath: "", } } // append hash to map of hashes to gather f.hashes[strings.ToLower(tor.Hash)] = tor } if len(f.hashes) == 0 { return errors.Errorf("Could not find any matching torrents to export from client") } log.Printf("Found (%d) matching torrents\n", len(f.hashes)) if err := processExport(f.sourceDir, f.exportDir, f.hashes, f.dry, f.verbose); err != nil { return errors.Wrapf(err, "could not process torrents") } // write export manifest with categories and tags // can be used for import later on if !skipManifest { // get categories if len(f.category) > 0 { cats, err := qb.GetCategoriesCtx(ctx) if err != nil { return errors.Wrapf(err, "could not get categories from qbit") } for name, category := range cats { _, ok := f.category[name] if !ok { continue } f.category[name] = category } } if f.dry { log.Println("dry-run: Saved export manifest to file") } else { if err := exportManifest(f.hashes, f.tags, f.category, f.exportDir); err != nil { return errors.Wrapf(err, "could not export manifest") } } } log.Println("Successfully exported torrents!") if f.archive { currentWorkingDirectory, err := os.Getwd() if err != nil { return errors.Wrap(err, "could not get current working directory") } archiveFilename := filepath.Join(currentWorkingDirectory, "qbittorrent-export.tar.gz") if f.dry { log.Printf("dry-run: Archive exported dir to: %s\n", archiveFilename) } else { log.Printf("Archive exported dir to: %s\n", archiveFilename) if err := archive.TarGzDirectory(f.exportDir, archiveFilename); err != nil { return errors.Wrapf(err, "could not archive exported torrents") } } log.Println("Successfully archived exported torrents!") } return nil } return command } func exportManifest(hashes map[string]qbittorrent.Torrent, tags map[string]struct{}, categories map[string]qbittorrent.Category, exportDir string) error { data := Manifest{ Tags: make([]string, 0), Categories: []qbittorrent.Category{}, Torrents: make([]basicTorrent, 0), } for tag, _ := range tags { data.Tags = append(data.Tags, tag) } for _, category := range categories { data.Categories = append(data.Categories, category) } for _, torrent := range hashes { data.Torrents = append(data.Torrents, basicTorrent{ Hash: torrent.Hash, Name: torrent.Name, Tags: torrent.Tags, Category: torrent.Category, Tracker: torrent.Tracker, }) } //currentWorkingDirectory, err := os.Getwd() //if err != nil { // return errors.Wrap(err, "could not get current working directory") //} currentTime := time.Now() timestamp := currentTime.Format("20060102150405") // Create a new manifestFile in the current working directory. manifestFileName := fmt.Sprintf("export-manifest-%s.json", timestamp) manifestFilePath := filepath.Join(exportDir, manifestFileName) manifestFile, err := os.Create(manifestFilePath) if err != nil { return errors.Wrapf(err, "could not create manifestFile: %s", manifestFilePath) } defer manifestFile.Close() // create new encoder with pretty print encoder := json.NewEncoder(manifestFile) encoder.SetIndent("", " ") if err := encoder.Encode(&data); err != nil { return errors.Wrap(err, "could not encode manifest to json") } log.Printf("Saved export manifest to %s\n", manifestFilePath) return nil } func processExport(sourceDir, exportDir string, hashes map[string]qbittorrent.Torrent, dry, verbose bool) error { exportTorrentCount := 0 exportFastresumeCount := 0 failedCount := 0 // check if export dir exists, if not then lets create it if err := createDirIfNotExists(exportDir); err != nil { return errors.Wrapf(err, "could not check if dir exists: %s", exportDir) } // qbittorrent from v4.5.x removes the announce-urls from the .torrent file so we need to add that back needTrackerFix := false // keep track of processed fastresume files processedFastResumeHashes := map[string]bool{} // exportTorrent processes a single matched .torrent file (and its .fastresume). // Any error it returns only concerns this torrent: the caller logs it and moves // on to the remaining files so a single problematic file can't abort the whole // export (see https://github.com/ludviglundgren/qbittorrent-cli/issues/135). exportTorrent := func(dirPath, fileName, torrentHash string, torrent qbittorrent.Torrent) error { outFile := filepath.Join(exportDir, fileName) // determine if this should be run on first run and the ones after if (exportTorrentCount == 0 && !needTrackerFix) || needTrackerFix { // open file and check if announce is in there. If it's not, open .fastresume and combine before output torrentFile, err := os.Open(dirPath) if err != nil { return errors.Wrapf(err, "could not open torrent file: %s", dirPath) } defer torrentFile.Close() torrentInfo, err := metainfo.Load(torrentFile) if err != nil { return errors.Wrapf(err, "could not open file: %s", dirPath) } wroteFastResume := false if torrentInfo.Announce == "" { needTrackerFix = true sourceFastResumeFilePath := filepath.Join(sourceDir, torrentHash+".fastresume") fastResumeFile, err := os.Open(sourceFastResumeFilePath) if err != nil { return errors.Wrapf(err, "could not open fastresume file: %s", sourceFastResumeFilePath) } defer fastResumeFile.Close() // open fastresume and get announce var fastResume qbit.Fastresume if err := bencode.NewDecoder(fastResumeFile).Decode(&fastResume); err != nil { return errors.Wrapf(err, "could not decode fastresume file: %s", sourceFastResumeFilePath) } if len(fastResume.Trackers) == 0 { return errors.New("no trackers found in fastresume") } torrentInfo.Announce = fastResume.Trackers[0][0] torrentInfo.AnnounceList = metainfo.AnnounceList(fastResume.Trackers) if len(torrentInfo.UrlList) == 0 && len(fastResume.UrlList) > 0 { torrentInfo.UrlList = fastResume.UrlList } // copy .fastresume here already since we already have it open fastresumeFilePath := filepath.Join(exportDir, torrentHash+".fastresume") newFastResumeFile, err := os.Create(fastresumeFilePath) if err != nil { return errors.Wrapf(err, "could not create new fastresume file: %s", fastresumeFilePath) } defer newFastResumeFile.Close() if err := bencode.NewEncoder(newFastResumeFile).Encode(&fastResume); err != nil { return errors.Wrapf(err, "could not encode fastresume to file: %s", fastresumeFilePath) } wroteFastResume = true } // write new torrent file to destination path newTorrentFile, err := os.Create(outFile) if err != nil { return errors.Wrapf(err, "could not create new torrent file: %s", outFile) } defer newTorrentFile.Close() if err := torrentInfo.Write(newTorrentFile); err != nil { return errors.Wrapf(err, "could not write torrent info into file %s", outFile) } // torrent (and the rebuilt fastresume, if any) written successfully; only // now commit the counters so a partial failure can't inflate the totals if wroteFastResume { // make sure the fastresume is only written once processedFastResumeHashes[torrentHash] = true exportFastresumeCount++ log.Printf("[%d/%d] exported: %s %s\n", exportFastresumeCount, len(hashes), torrentHash+".fastresume", torrent.Name) } exportTorrentCount++ log.Printf("[%d/%d] exported: %s %s\n", exportTorrentCount, len(hashes), fileName, torrent.Name) return nil } // only do this if !needTrackerFix if err := fsutil.CopyFile(dirPath, outFile); err != nil { return errors.Wrapf(err, "could not copy file: %s to %s", dirPath, outFile) } // process if fastresume has not already been copied wroteFastResume := false if _, done := processedFastResumeHashes[torrentHash]; !done { sourceFastResumeFilePath := filepath.Join(sourceDir, torrentHash+".fastresume") fastResumeFilePath := filepath.Join(exportDir, torrentHash+".fastresume") if err := fsutil.CopyFile(sourceFastResumeFilePath, fastResumeFilePath); err != nil { return errors.Wrapf(err, "could not copy file: %s to %s", sourceFastResumeFilePath, fastResumeFilePath) } wroteFastResume = true } // torrent (and fastresume) copied successfully; only now commit the counters exportTorrentCount++ log.Printf("[%d/%d] exported: %s %s\n", exportTorrentCount, len(hashes), fileName, torrent.Name) if wroteFastResume { exportFastresumeCount++ log.Printf("[%d/%d] exported: %s %s\n", exportFastresumeCount, len(hashes), torrentHash+".fastresume", torrent.Name) } return nil } // check BT_backup dir, pick torrent and fastresume files by hash err := filepath.WalkDir(sourceDir, func(dirPath string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } fileName := d.Name() ext := filepath.Ext(fileName) if ext != ".torrent" { return nil } torrentHash := fileNameTrimExt(fileName) // if filename not in hashes return and check next torrent, ok := hashes[torrentHash] if !ok { return nil } if verbose { log.Printf("Processing: %s\n", fileName) } if dry { exportTorrentCount++ log.Printf("dry-run: [%d/%d] exported: %s %s\n", exportTorrentCount, len(hashes), torrentHash+".torrent", torrent.Name) exportFastresumeCount++ log.Printf("dry-run: [%d/%d] exported: %s %s\n", exportFastresumeCount, len(hashes), torrentHash+".fastresume", torrent.Name) return nil } // process the torrent; on failure log it and continue with the next file // instead of aborting the entire export if err := exportTorrent(dirPath, fileName, torrentHash, torrent); err != nil { failedCount++ log.Printf("skipping %s: %v\n", fileName, err) // remove any half-written output for this torrent so the export dir // never ends up with a partial or mismatched .torrent/.fastresume pair _ = os.Remove(filepath.Join(exportDir, fileName)) _ = os.Remove(filepath.Join(exportDir, torrentHash+".fastresume")) } return nil }) if err != nil { log.Printf("error reading files: %q\n", err) return err } log.Printf("Exported (%d) files in total: fastresume (%d) torrents (%d)\n", exportFastresumeCount+exportTorrentCount, exportFastresumeCount, exportTorrentCount) if failedCount > 0 { log.Printf("Skipped (%d) torrents due to errors, see the messages above\n", failedCount) } // if every matched torrent failed to export, surface it as an error instead of // reporting success - otherwise a fully broken run would still exit 0 if exportTorrentCount == 0 && failedCount > 0 { return errors.Errorf("failed to export any torrents (%d skipped due to errors)", failedCount) } return nil } func fileNameTrimExt(fileName string) string { return strings.ToLower(strings.TrimSuffix(fileName, filepath.Ext(fileName))) } // isValidExt check if the input ext is one of the ext we want func isValidExt(filename string) bool { valid := []string{".torrent", ".fastresume"} for _, s := range valid { if s == filename { return true } } return false } // createDirIfNotExists check if export dir exists, if not then lets create it func createDirIfNotExists(dir string) error { if _, err := os.Stat(dir); err != nil { if os.IsNotExist(err) { if err := os.MkdirAll(dir, os.ModePerm); err != nil { return err } return nil } return err } return nil } type export struct { dry bool verbose bool archive bool sourceDir string exportDir string includeCategory []string excludeCategory []string includeTag []string excludeTag []string tags map[string]struct{} category map[string]qbittorrent.Category hashes map[string]qbittorrent.Torrent } func containsTag(contains []string, tags []string) bool { for _, s := range tags { s = strings.ToLower(s) for _, contain := range contains { if s == strings.ToLower(contain) { return true } } } return false } func containsCategory(contains []string, category string) bool { for _, cat := range contains { if strings.EqualFold(category, cat) { return true } } return false } type basicTorrent struct { Hash string `json:"hash"` Name string `json:"name"` Tags string `json:"tags"` Category string `json:"category"` Tracker string `json:"tracker"` } type Manifest struct { Tags []string `json:"tags"` Categories []qbittorrent.Category `json:"categories"` Torrents []basicTorrent `json:"torrents"` } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_export_test.go000066400000000000000000000163501522034161300256710ustar00rootroot00000000000000package cmd import ( "bytes" "os" "path/filepath" "reflect" "testing" "github.com/anacrolix/torrent/metainfo" "github.com/autobrr/go-qbittorrent" "github.com/zeebo/bencode" ) func Test_export_processHashes(t *testing.T) { type args struct { sourceDir string exportDir string hashes map[string]qbittorrent.Torrent dry bool verbose bool } tests := []struct { name string args args wantErr bool }{ { name: "test_1", args: args{ sourceDir: "../test/config/qBittorrent/BT_backup", exportDir: "../test/export", hashes: map[string]qbittorrent.Torrent{ "5ba4939a00a9b21629a0ad7d376898b768d997a3": {}, }, dry: false, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := processExport(tt.args.sourceDir, tt.args.exportDir, tt.args.hashes, tt.args.dry, tt.args.verbose); (err != nil) != tt.wantErr { t.Errorf("processExport() error = %v, wantErr %v", err, tt.wantErr) } }) } } // minimal single-file torrent with no announce, which forces processExport down // the path that reads the matching .fastresume to recover the trackers. const torrentNoAnnounce = "d4:infod6:lengthi1e4:name1:a12:piece lengthi16384e6:pieces0:ee" func writeFile(t *testing.T, path string, data []byte) { t.Helper() if err := os.WriteFile(path, data, 0o644); err != nil { t.Fatalf("could not write %s: %v", path, err) } } // Test_processExport_continuesOnBadFastresume verifies that a single problematic // fastresume file does not abort the whole export: the bad file is skipped and the // remaining torrents are still exported (issue #135). func Test_processExport_continuesOnBadFastresume(t *testing.T) { sourceDir := t.TempDir() exportDir := t.TempDir() const badHash = "1111111111111111111111111111111111111111" // sorts first const goodHash = "2222222222222222222222222222222222222222" // sorts after the bad one // bad pair: valid torrent but an unparseable fastresume writeFile(t, filepath.Join(sourceDir, badHash+".torrent"), []byte(torrentNoAnnounce)) writeFile(t, filepath.Join(sourceDir, badHash+".fastresume"), []byte("this is not bencode")) // good pair: valid torrent + fastresume that stores trackers as a flat list goodFastresume, err := bencode.EncodeBytes(map[string]interface{}{ "save_path": "/downloads", "trackers": []string{"https://tracker/announce"}, }) if err != nil { t.Fatalf("could not encode good fastresume: %v", err) } writeFile(t, filepath.Join(sourceDir, goodHash+".torrent"), []byte(torrentNoAnnounce)) writeFile(t, filepath.Join(sourceDir, goodHash+".fastresume"), goodFastresume) hashes := map[string]qbittorrent.Torrent{ badHash: {}, goodHash: {}, } if err := processExport(sourceDir, exportDir, hashes, false, false); err != nil { t.Fatalf("processExport() returned error, want nil: %v", err) } // the good torrent (and its fastresume) must have been exported mustExist(t, filepath.Join(exportDir, goodHash+".torrent")) mustExist(t, filepath.Join(exportDir, goodHash+".fastresume")) // the bad torrent must have been skipped, not exported mustNotExist(t, filepath.Join(exportDir, badHash+".torrent")) mustNotExist(t, filepath.Join(exportDir, badHash+".fastresume")) // the flat-list trackers from the fastresume must have been recovered into the // exported .torrent (announce + announce-list), proving the end-to-end fix mi, err := metainfo.LoadFromFile(filepath.Join(exportDir, goodHash+".torrent")) if err != nil { t.Fatalf("could not load exported torrent: %v", err) } if mi.Announce != "https://tracker/announce" { t.Errorf("exported torrent Announce = %q, want %q", mi.Announce, "https://tracker/announce") } wantAnnounceList := metainfo.AnnounceList{{"https://tracker/announce"}} if !reflect.DeepEqual(mi.AnnounceList, wantAnnounceList) { t.Errorf("exported torrent AnnounceList = %#v, want %#v", mi.AnnounceList, wantAnnounceList) } } // minimal single-file torrent that already carries an announce URL. Torrents like // this (pre v4.5.x qBittorrent) keep the trackers in the .torrent, so processExport // takes the plain copy path instead of rebuilding from the .fastresume. const torrentWithAnnounce = "d8:announce17:http://t/announce4:infod6:lengthi1e4:name1:a12:piece lengthi16384e6:pieces0:ee" // Test_processExport_copyBranchExportsRealFastresume guards the copy path: the // exported .fastresume must be a copy of the SOURCE .fastresume, not of the // .torrent file (the latter was a real bug). The copy path only runs from the // second announce-carrying torrent onwards, so we export two of them. func Test_processExport_copyBranchExportsRealFastresume(t *testing.T) { sourceDir := t.TempDir() exportDir := t.TempDir() const firstHash = "1111111111111111111111111111111111111111" const secondHash = "2222222222222222222222222222222222222222" torrentBytes := []byte(torrentWithAnnounce) secondFastresume := []byte("FASTRESUME-CONTENT-FOR-SECOND-TORRENT") writeFile(t, filepath.Join(sourceDir, firstHash+".torrent"), torrentBytes) writeFile(t, filepath.Join(sourceDir, firstHash+".fastresume"), []byte("FASTRESUME-CONTENT-FOR-FIRST-TORRENT")) writeFile(t, filepath.Join(sourceDir, secondHash+".torrent"), torrentBytes) writeFile(t, filepath.Join(sourceDir, secondHash+".fastresume"), secondFastresume) hashes := map[string]qbittorrent.Torrent{ firstHash: {}, secondHash: {}, } if err := processExport(sourceDir, exportDir, hashes, false, false); err != nil { t.Fatalf("processExport() returned error, want nil: %v", err) } // the second torrent goes through the copy path; its exported .fastresume must // byte-equal the source .fastresume (and must NOT be the .torrent content) got, err := os.ReadFile(filepath.Join(exportDir, secondHash+".fastresume")) if err != nil { t.Fatalf("could not read exported fastresume: %v", err) } if !bytes.Equal(got, secondFastresume) { t.Errorf("exported fastresume = %q, want source fastresume %q", got, secondFastresume) } if bytes.Equal(got, torrentBytes) { t.Errorf("exported fastresume wrongly contains the .torrent bytes") } } // Test_processExport_allFailReturnsError verifies that when every matched torrent // fails to export, processExport surfaces an error instead of reporting success. func Test_processExport_allFailReturnsError(t *testing.T) { sourceDir := t.TempDir() exportDir := t.TempDir() const hash = "1111111111111111111111111111111111111111" writeFile(t, filepath.Join(sourceDir, hash+".torrent"), []byte(torrentNoAnnounce)) writeFile(t, filepath.Join(sourceDir, hash+".fastresume"), []byte("this is not bencode")) hashes := map[string]qbittorrent.Torrent{hash: {}} if err := processExport(sourceDir, exportDir, hashes, false, false); err == nil { t.Fatal("processExport() returned nil, want error when all torrents fail") } // nothing should have been written to the export dir mustNotExist(t, filepath.Join(exportDir, hash+".torrent")) mustNotExist(t, filepath.Join(exportDir, hash+".fastresume")) } func mustExist(t *testing.T, path string) { t.Helper() if _, err := os.Stat(path); err != nil { t.Errorf("expected file to exist: %s (%v)", path, err) } } func mustNotExist(t *testing.T, path string) { t.Helper() if _, err := os.Stat(path); !os.IsNotExist(err) { t.Errorf("expected file to not exist: %s (err=%v)", path, err) } } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_hash.go000066400000000000000000000022141522034161300242260ustar00rootroot00000000000000package cmd import ( "fmt" "strings" "github.com/anacrolix/torrent/metainfo" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTorrentHash cmd to add torrents func RunTorrentHash() *cobra.Command { var command = &cobra.Command{ Use: "hash", Short: "Print the hash of a torrent file or magnet", Example: ` qbt torrent hash file.torrent`, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("requires a torrent file or magnet URI as first argument") } return nil }, } command.RunE = func(cmd *cobra.Command, args []string) error { filePath := args[0] hash := "" if strings.HasPrefix(filePath, "magnet:") { metadata, err := metainfo.ParseMagnetUri(filePath) if err != nil { return errors.Wrapf(err, "could not parse magnet URI: %s", filePath) } hash = metadata.InfoHash.HexString() } else { metadata, err := metainfo.LoadFromFile(filePath) if err != nil { return errors.Wrapf(err, "could not parse torrent file: %s", filePath) } hash = metadata.HashInfoBytes().HexString() } fmt.Println(hash) return nil } return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_import.go000066400000000000000000000117501522034161300246220ustar00rootroot00000000000000package cmd import ( "log" "os" "path/filepath" "time" "github.com/ludviglundgren/qbittorrent-cli/internal/importer" "github.com/mholt/archives" "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTorrentImport cmd import torrents func RunTorrentImport() *cobra.Command { var command = &cobra.Command{ Use: "import {rtorrent | deluge} --source-dir dir --qbit-dir dir2 [--skip-backup] [--dry-run]", Short: "Import torrents", Long: `Import torrents with state from other clients [rtorrent, deluge]`, Example: ` qbt torrent import deluge --source-dir ~/.config/deluge/state/ --qbit-dir ~/.local/share/data/qBittorrent/BT_backup --dry-run qbt torrent import rtorrent --source-dir ~/.sessions --qbit-dir ~/.local/share/data/qBittorrent/BT_backup --dry-run`, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("requires a source client [rtorrent, deluge] as first argument") } return cobra.OnlyValidArgs(cmd, args) }, ValidArgs: []string{"rtorrent", "deluge"}, } var ( sourceDir string qbitDir string dryRun bool skipBackup bool ) command.Flags().BoolVar(&dryRun, "dry-run", false, "Run without importing anything") command.Flags().StringVar(&sourceDir, "source-dir", "", "source client state dir (required)") command.Flags().StringVar(&qbitDir, "qbit-dir", "", "qBittorrent BT_backup dir. Commonly ~/.local/share/qBittorrent/BT_backup (required)") command.Flags().BoolVar(&skipBackup, "skip-backup", false, "Skip backup before import") command.MarkFlagRequired("source-dir") command.MarkFlagRequired("qbit-dir") command.RunE = func(cmd *cobra.Command, args []string) error { source := args[0] var imp importer.Importer switch source { case "deluge": imp = importer.NewDelugeImporter() case "rtorrent": imp = importer.NewRTorrentImporter() default: return errors.Errorf("error: unsupported client: %s", source) } // TODO check if program is running, if true exit // Backup data before running if !skipBackup { log.Print("prepare to backup torrent data before import..\n") homeDir, err := homedir.Dir() if err != nil { return errors.Wrap(err, "could not find home directory") } timeStamp := time.Now().Format("20060102150405") sourceBackupArchive := filepath.Join(homeDir, "qbt_backup", source+"_backup_"+timeStamp+".tar.gz") qbitBackupArchive := filepath.Join(homeDir, "qbt_backup", "qBittorrent_backup_"+timeStamp+".tar.gz") if dryRun { log.Printf("dry-run: creating %s backup of directory: %s to %s ...\n", source, sourceDir, sourceBackupArchive) } else { log.Printf("creating %s backup of directory: %s to %s ...\n", source, sourceDir, sourceBackupArchive) // map files on disk to their paths in the archive using default settings (second arg) files, err := archives.FilesFromDisk(cmd.Context(), nil, map[string]string{ sourceDir: "", }) if err != nil { return err } // create the output file we'll write to out, err := os.Create(sourceBackupArchive) if err != nil { return err } defer out.Close() format := archives.CompressedArchive{ Compression: archives.Gz{}, Archival: archives.Tar{}, } // create the archive err = format.Archive(cmd.Context(), out, files) if err != nil { return errors.Wrapf(err, "could not create backup archive: %s", out.Name()) } } if dryRun { log.Printf("dry-run: creating qBittorrent backup of directory: %s to %s ...\n", qbitDir, qbitBackupArchive) } else { log.Printf("creating qBittorrent backup of directory: %s to %s ...\n", qbitDir, qbitBackupArchive) // map files on disk to their paths in the archive using default settings (second arg) files, err := archives.FilesFromDisk(cmd.Context(), nil, map[string]string{ qbitDir: "", }) if err != nil { return err } // create the output file we'll write to out, err := os.Create(qbitBackupArchive) if err != nil { return err } defer out.Close() format := archives.CompressedArchive{ Compression: archives.Gz{}, Archival: archives.Tar{}, } // create the archive err = format.Archive(cmd.Context(), out, files) if err != nil { return errors.Wrapf(err, "could not create backup archive: %s", out.Name()) } } log.Print("Backup completed!\n") } start := time.Now() if dryRun { log.Printf("dry-run: preparing to import torrents from: %s dir: %s\n", source, sourceDir) log.Println("dry-run: no data will be written") } else { log.Printf("preparing to import torrents from: %s dir: %s\n", source, sourceDir) } opts := importer.Options{ SourceDir: sourceDir, QbitDir: qbitDir, DryRun: dryRun, } if err := imp.Import(opts); err != nil { return errors.Wrapf(err, "could not import from %s", source) } elapsed := time.Since(start) log.Printf("\nImport finished in: %s\n", elapsed) return nil } return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_list.go000066400000000000000000000112131522034161300242550ustar00rootroot00000000000000package cmd import ( "encoding/json" "fmt" "log" "os" "strings" "text/template" "time" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/ludviglundgren/qbittorrent-cli/pkg/utils" "github.com/autobrr/go-qbittorrent" "github.com/autobrr/go-qbittorrent/errors" "github.com/dustin/go-humanize" "github.com/spf13/cobra" ) // RunTorrentList cmd to list torrents func RunTorrentList() *cobra.Command { var ( filter = "all" category string tag string hashes []string output string ) var command = &cobra.Command{ Use: "list", Short: "List torrents", Long: `List all torrents, or torrents with a specific filters. Get by filter, category, tag and hashes. Can be combined`, Example: `qbt torrent list --filter=downloading --category=linux-iso`, } command.Flags().StringVar(&output, "output", "", "Print as [formatted text (default), json]") command.Flags().StringVarP(&filter, "filter", "f", "all", "Filter by state. Available filters: all, downloading, seeding, completed, paused, active, inactive, resumed, \nstalled, stalled_uploading, stalled_downloading, errored") command.Flags().StringVarP(&category, "category", "c", "", "Filter by category. All categories by default.") command.Flags().StringVarP(&tag, "tag", "t", "", "Filter by tag. Single tag: tag1") command.Flags().StringSliceVar(&hashes, "hashes", []string{}, "Filter by hashes. Separated by comma: \"hash1,hash2\".") command.RunE = func(cmd *cobra.Command, args []string) error { if len(hashes) > 0 { if err := utils.ValidateHash(hashes); err != nil { return errors.Wrap(err, "invalid hashes supplied") } } config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } req := qbittorrent.TorrentFilterOptions{ Filter: qbittorrent.TorrentFilter(strings.ToLower(filter)), Category: category, Tag: tag, Hashes: hashes, } // get torrent list with default filter of all torrents, err := qb.GetTorrentsCtx(ctx, req) if err != nil { return errors.Wrap(err, "could not get torrents") } if len(torrents) == 0 { log.Printf("No torrents found with filter: %s\n", filter) return nil } switch output { case "json": res, err := json.Marshal(torrents) if err != nil { return errors.Wrap(err, "could not marshal torrents list to json") } fmt.Println(string(res)) default: if err := printList(torrents); err != nil { return errors.Wrap(err, "could not print torrent list") } } return nil } return command } var torrentItemTemplate = `{{ range .}} [*] {{.Name}} Hash: {{.Hash}} State: {{.State}} Category: {{.Category}} Tags: [{{.Tags}}] Size: {{.Size}} Downloaded: {{.Completed}} / {{.TotalSize}} Uploaded: {{.Uploaded}} Added: {{.Added}} Time Active: {{.TimeActive}} Save path: {{.SavePath}} {{end}} ` type ItemData struct { Hash string Name string State string Category string Tags string SavePath string Added string TimeActive string Size string TotalSize string Completed string Uploaded string } func printList(torrents []qbittorrent.Torrent) error { tmpl, err := template.New("item").Parse(torrentItemTemplate) if err != nil { return err } var data []ItemData for _, torrent := range torrents { //if torrent.DlSpeed > 0 { // fmt.Printf( // "DL Speed: %s/s%s", // humanize.Bytes(uint64(torrent.DlSpeed)), // space, // ) //} else if torrent.UpSpeed > 0 { // fmt.Printf( // "UP Speed: %s/s%s", // humanize.Bytes(uint64(torrent.UpSpeed)), // space, // ) //} d := ItemData{ Hash: torrent.Hash, Name: torrent.Name, State: string(torrent.State), Size: humanize.Bytes(uint64(torrent.Size)), TotalSize: humanize.Bytes(uint64(torrent.TotalSize)), Category: torrent.Category, Tags: torrent.Tags, SavePath: torrent.SavePath, Added: humanize.RelTime(time.Unix(0, 0), time.Unix(int64(torrent.AddedOn), 0), "ago", ""), TimeActive: humanize.RelTime(time.Unix(0, 0), time.Unix(int64(torrent.TimeActive), 0), "", ""), Completed: humanize.Bytes(uint64(torrent.Completed)), Uploaded: humanize.Bytes(uint64(torrent.Uploaded)), } data = append(data, d) } err = tmpl.Execute(os.Stdout, data) if err != nil { return err } return nil } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_pause.go000066400000000000000000000040561522034161300244260ustar00rootroot00000000000000package cmd import ( "log" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/ludviglundgren/qbittorrent-cli/pkg/utils" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTorrentPause cmd to pause torrents func RunTorrentPause() *cobra.Command { var ( pauseAll bool hashes []string ) var command = &cobra.Command{ Use: "pause", Short: "Pause specified torrent(s)", Long: `Pause the torrent(s) indicated by the supplied hash(es), or pause every torrent with --all.`, Example: ` qbt torrent pause --all qbt torrent pause HASH1 HASH2 qbt torrent pause --hashes HASH1,HASH2`, } command.Flags().BoolVar(&pauseAll, "all", false, "Pauses all torrents") command.Flags().StringSliceVar(&hashes, "hashes", []string{}, "Add hashes as comma separated list") command.RunE = func(cmd *cobra.Command, args []string) error { // Treat positional arguments as hashes too, so `qbt torrent pause HASH` works // alongside the --hashes flag. hashes = append(hashes, args...) if pauseAll { hashes = []string{"all"} } else { if len(hashes) == 0 { return errors.New("no torrents specified: provide hash(es) as arguments or with --hashes, or use --all") } if err := utils.ValidateHash(hashes); err != nil { return errors.Wrap(err, "invalid hashes supplied") } } config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } if err := batchRequests(hashes, func(start, end int) error { return qb.PauseCtx(ctx, hashes[start:end]) }); err != nil { return errors.Wrap(err, "could not pause torrents") } log.Printf("torrent(s) successfully paused") return nil } return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_pause_test.go000066400000000000000000000022171522034161300254620ustar00rootroot00000000000000package cmd import ( "io" "strings" "testing" ) // TestRunTorrentPause_requiresTarget mirrors the resume guard (issue #132): // `qbt torrent pause` with no target must fail loudly instead of silently // no-opping. Both error paths return before any network call, so the command // can be exercised without a live qBittorrent. func TestRunTorrentPause_requiresTarget(t *testing.T) { tests := []struct { name string args []string wantErr string }{ { name: "no target returns error instead of no-op success", args: []string{}, wantErr: "no torrents specified", }, { name: "invalid positional hash is rejected", args: []string{"not-a-valid-hash"}, wantErr: "invalid hashes supplied", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { command := RunTorrentPause() command.SetArgs(tt.args) command.SetOut(io.Discard) command.SetErr(io.Discard) err := command.Execute() if err == nil { t.Fatalf("expected error, got nil") } if !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error()) } }) } } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_reannounce.go000066400000000000000000000071731522034161300254510ustar00rootroot00000000000000package cmd import ( "context" "log" "time" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTorrentReannounce cmd to reannounce torrents func RunTorrentReannounce() *cobra.Command { var ( dry bool hash string category string tag string attempts int interval int ) var command = &cobra.Command{ Use: "reannounce", Short: "Reannounce torrent(s)", Long: `Reannounce torrents with non-OK tracker status.`, } command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.Flags().StringVar(&hash, "hash", "", "Reannounce torrent with hash") command.Flags().StringVar(&category, "category", "", "Reannounce torrents with category") command.Flags().StringVar(&tag, "tag", "", "Reannounce torrents with tag") command.Flags().IntVar(&attempts, "attempts", 50, "Reannounce torrents X times") command.Flags().IntVar(&interval, "interval", 7000, "Reannounce torrents X times with interval Y. In MS") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } req := qbittorrent.TorrentFilterOptions{ //Filter: qbittorrent.TorrentFilterDownloading, Category: category, Tag: tag, Hashes: []string{hash}, } activeDownloads, err := qb.GetTorrentsCtx(ctx, req) if err != nil { return errors.Wrap(err, "could not fetch torrents") } if hash != "" && len(activeDownloads) != 1 { return errors.Errorf("torrent not found: %s", hash) } if dry { log.Println("dry-run: torrents successfully re-announced!") } else { for _, torrent := range activeDownloads { if torrent.Progress == 0 && torrent.TimeActive < 120 { go func(torrent qbittorrent.Torrent) { log.Printf("torrent %s %s not working, active for %ds, re-announcing...\n", torrent.Hash, torrent.Name, torrent.TimeActive) // some trackers are bugged or slow, so we need to re-announce the torrent until it works if err = reannounceTorrent(ctx, qb, interval, attempts, torrent.Hash); err != nil { log.Printf("could not re-announce torrent: %s %s err: %q\n", torrent.Hash, torrent.Name, err) } log.Printf("successfully re-announced torrent: %s %s err: %q\n", torrent.Hash, torrent.Name, err) }(torrent) } } log.Println("torrents successfully re-announced") } return nil } return command } func reannounceTorrent(ctx context.Context, qb *qbittorrent.Client, interval, attempts int, hash string) error { announceOK := false attempt := 0 time.Sleep(time.Duration(interval) * time.Millisecond) for attempt < attempts { trackers, err := qb.GetTorrentTrackersCtx(ctx, hash) if err != nil { log.Fatalf("could not get trackers of torrent: %s err: %q", hash, err) } // check if status not working or something else _, working := findTrackerStatus(trackers, 2) if working { announceOK = true break } if err = qb.ReAnnounceTorrentsCtx(ctx, []string{hash}); err != nil { return err } time.Sleep(time.Duration(interval) * time.Millisecond) attempt++ continue } if !announceOK { log.Println("announce still not ok") return errors.New("announce still not ok") } return nil } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_recheck.go000066400000000000000000000032221522034161300247070ustar00rootroot00000000000000package cmd import ( "log" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/ludviglundgren/qbittorrent-cli/pkg/utils" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTorrentRecheck cmd to recheck torrents func RunTorrentRecheck() *cobra.Command { var ( hashes []string ) var command = &cobra.Command{ Use: "recheck", Short: "Recheck specified torrent(s)", Long: `Rechecks torrents indicated by hash(es).`, Example: ` qbt torrent recheck --hashes HASH qbt torrent recheck --hashes HASH1,HASH2 `, } command.Flags().StringSliceVar(&hashes, "hashes", []string{}, "Add hashes as comma separated list") command.RunE = func(cmd *cobra.Command, args []string) error { if len(hashes) == 0 { return errors.Errorf("no hashes supplied to recheck") } err := utils.ValidateHash(hashes) if err != nil { return errors.Wrap(err, "invalid hashes supplied") } config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } err = batchRequests(hashes, func(start, end int) error { return qb.RecheckCtx(ctx, hashes[start:end]) }) if err != nil { return errors.Wrap(err, "could not reched torrents") } log.Printf("torrent(s) successfully recheckd") return nil } return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_remove.go000066400000000000000000000100341522034161300245770ustar00rootroot00000000000000package cmd import ( "log" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/ludviglundgren/qbittorrent-cli/pkg/utils" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTorrentRemove cmd to remove torrents func RunTorrentRemove() *cobra.Command { var ( dryRun bool removeAll bool deleteFiles bool hashes []string includeCategory []string includeTags []string excludeTags []string filter string ) var command = &cobra.Command{ Use: "remove", Short: "Removes specified torrent(s)", Long: `Removes torrents indicated by hash, name or a prefix of either. Whitespace indicates next prefix unless argument is surrounded by quotes`, } command.Flags().BoolVar(&dryRun, "dry-run", false, "Display what would be done without actually doing it") command.Flags().BoolVar(&removeAll, "all", false, "Removes all torrents") command.Flags().BoolVar(&deleteFiles, "delete-files", false, "Also delete downloaded files from torrent(s)") command.Flags().StringVarP(&filter, "filter", "f", "", "Filter by state: all, active, paused, completed, stalled, errored") command.Flags().StringSliceVar(&hashes, "hashes", []string{}, "Add hashes as comma separated list") command.Flags().StringSliceVarP(&includeCategory, "include-category", "c", []string{}, "Remove torrents from these categories. Comma separated") command.Flags().StringSliceVar(&includeTags, "include-tags", []string{}, "Include torrents with provided tags") command.Flags().StringSliceVar(&excludeTags, "exclude-tags", []string{}, "Exclude torrents with provided tags") command.RunE = func(cmd *cobra.Command, args []string) error { if len(hashes) > 0 { if err := utils.ValidateHash(hashes); err != nil { return errors.Wrap(err, "invalid hashes supplied") } } config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } if removeAll { hashes = []string{"all"} } options := qbittorrent.TorrentFilterOptions{} if filter != "" { options.Filter = qbittorrent.TorrentFilter(filter) if len(includeCategory) == 0 { includeCategory = []string{""} } } if len(includeCategory) > 0 { for _, category := range includeCategory { options.Category = category torrents, err := qb.GetTorrentsCtx(ctx, options) if err != nil { return errors.Wrapf(err, "could not get torrents for category: %s", category) } for _, torrent := range torrents { if len(includeTags) > 0 { if _, validTag := validateTag(includeTags, torrent.Tags); !validTag { continue } } if len(excludeTags) > 0 { if _, found := validateTag(excludeTags, torrent.Tags); found { continue } } hashes = append(hashes, torrent.Hash) } } } if len(hashes) == 0 { log.Println("No torrents found to remove") return nil } if dryRun { if hashes[0] == "all" { log.Println("dry-run: all torrents to be removed") } else { log.Printf("dry-run: (%d) torrents to be removed\n", len(hashes)) } } else { if hashes[0] == "all" { log.Println("all torrents to be removed") } else { log.Printf("(%d) torrents to be removed\n", len(hashes)) } err := batchRequests(hashes, func(start, end int) error { return qb.DeleteTorrentsCtx(ctx, hashes[start:end], deleteFiles) }) if err != nil { return errors.Wrap(err, "could not delete torrents") } if hashes[0] == "all" { log.Println("successfully removed all torrents") } else { log.Printf("successfully removed (%d) torrents\n", len(hashes)) } } log.Printf("torrent(s) successfully deleted\n") return nil } return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_resume.go000066400000000000000000000051021522034161300246020ustar00rootroot00000000000000package cmd import ( "log" "time" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/ludviglundgren/qbittorrent-cli/pkg/utils" "github.com/pkg/errors" "github.com/autobrr/go-qbittorrent" "github.com/spf13/cobra" ) // RunTorrentResume cmd to resume torrents func RunTorrentResume() *cobra.Command { var ( resumeAll bool hashes []string ) var command = &cobra.Command{ Use: "resume", Short: "Resume specified torrent(s)", Long: `Resume the torrent(s) indicated by the supplied hash(es), or resume every torrent with --all.`, Example: ` qbt torrent resume --all qbt torrent resume HASH1 HASH2 qbt torrent resume --hashes HASH1,HASH2`, } command.Flags().BoolVar(&resumeAll, "all", false, "resumes all torrents") command.Flags().StringSliceVar(&hashes, "hashes", []string{}, "Add hashes as comma separated list") command.RunE = func(cmd *cobra.Command, args []string) error { // Treat positional arguments as hashes too, so `qbt torrent resume HASH` works // alongside the --hashes flag. hashes = append(hashes, args...) if resumeAll { hashes = []string{"all"} } else { if len(hashes) == 0 { return errors.New("no torrents specified: provide hash(es) as arguments or with --hashes, or use --all") } if err := utils.ValidateHash(hashes); err != nil { return errors.Wrap(err, "invalid hashes supplied") } } config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } if err := batchRequests(hashes, func(start, end int) error { return qb.ResumeCtx(ctx, hashes[start:end]) }); err != nil { return errors.Wrap(err, "could not resume torrents") } log.Printf("torrent(s) successfully resumed") return nil } return command } // batchRequests split into multiple requests because qbit uses application/x-www-form-urlencoded // which might lead to too big requests func batchRequests(hashes []string, fn func(start, end int) error) error { // Split the hashes into groups of 20 to avoid flooding qbittorrent batch := 25 for i := 0; i < len(hashes); i += batch { j := i + batch if j > len(hashes) { j = len(hashes) } if err := fn(i, j); err != nil { return err } time.Sleep(time.Second * 1) } return nil } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_resume_test.go000066400000000000000000000023071522034161300256450ustar00rootroot00000000000000package cmd import ( "io" "strings" "testing" ) // TestRunTorrentResume_requiresTarget guards against the false-positive reported // in issue #132: `qbt torrent resume` with no target must fail loudly instead of // printing "torrent(s) successfully resumed". Both error paths return before any // network call, so the command can be exercised without a live qBittorrent. func TestRunTorrentResume_requiresTarget(t *testing.T) { tests := []struct { name string args []string wantErr string }{ { name: "no target returns error instead of false-positive success", args: []string{}, wantErr: "no torrents specified", }, { name: "invalid positional hash is rejected", args: []string{"not-a-valid-hash"}, wantErr: "invalid hashes supplied", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { command := RunTorrentResume() command.SetArgs(tt.args) command.SetOut(io.Discard) command.SetErr(io.Discard) err := command.Execute() if err == nil { t.Fatalf("expected error, got nil") } if !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error()) } }) } } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_share_limit.go000066400000000000000000000175711522034161300256170ustar00rootroot00000000000000package cmd import ( "log" "strconv" "strings" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/ludviglundgren/qbittorrent-cli/pkg/utils" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTorrentShareLimit cmd for torrent share limit operations func RunTorrentShareLimit() *cobra.Command { var command = &cobra.Command{ Use: "share-limit", Short: "Torrent share limit subcommand", Long: `Do various torrent share limit operations`, } command.AddCommand(RunTorrentShareLimitSet()) return command } // RunTorrentShareLimitSet cmd to set share limits for torrents func RunTorrentShareLimitSet() *cobra.Command { var ( dry bool shareAll bool hashes []string includeCategory []string includeTags []string excludeTags []string ratioLimit float64 seedingTimeLimit int64 inactiveSeedingTimeLimit int64 ) var command = &cobra.Command{ Use: "set", Short: "Set torrent share limits", Long: `Set share limits (ratio, seeding time, inactive seeding time) for torrents. Limit values use qBittorrent's special semantics: -2 use the global limit (default) -1 no limit (unlimited) >=0 a specific value (ratio, or minutes for the seeding time limits) qBittorrent applies all three limits in a single request, so any limit you do not set is reset to the global limit (-2). The applied values are printed when the command runs. Note: on qBittorrent 5.x (Web API 2.12+) this also resets each torrent's share limit action and mode to their defaults.`, Example: ` qbt torrent share-limit set --hashes hash1,hash2 --ratio 2.0 qbt torrent share-limit set --all --seeding-time 1440 qbt torrent share-limit set --include-category movies --ratio 1.5 --seeding-time 10080 qbt torrent share-limit set --hashes hash1 --ratio -1`, } command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.Flags().BoolVar(&shareAll, "all", false, "Set share limits for all torrents") command.Flags().StringSliceVar(&hashes, "hashes", []string{}, "Torrent hashes, as comma separated list") command.Flags().StringSliceVarP(&includeCategory, "include-category", "c", []string{}, "Set share limits for torrents in these categories. Comma separated") command.Flags().StringSliceVar(&includeTags, "include-tags", []string{}, "Include torrents with any of these tags. Comma separated") command.Flags().StringSliceVar(&excludeTags, "exclude-tags", []string{}, "Exclude torrents with any of these tags. Comma separated") command.Flags().Float64Var(&ratioLimit, "ratio", -2, "Ratio limit. -2 = global, -1 = unlimited, >=0 = ratio") command.Flags().Int64Var(&seedingTimeLimit, "seeding-time", -2, "Seeding time limit in MINUTES. -2 = global, -1 = unlimited, >=0 = minutes") command.Flags().Int64Var(&inactiveSeedingTimeLimit, "inactive-seeding-time", -2, "Inactive seeding time limit in MINUTES. -2 = global, -1 = unlimited, >=0 = minutes") command.RunE = func(cmd *cobra.Command, args []string) error { // require at least one limit to be set so we don't silently reset // every limit to the global value if !cmd.Flags().Changed("ratio") && !cmd.Flags().Changed("seeding-time") && !cmd.Flags().Changed("inactive-seeding-time") { return errors.New("you must set at least one limit: --ratio, --seeding-time or --inactive-seeding-time") } if err := validateShareLimits(ratioLimit, seedingTimeLimit, inactiveSeedingTimeLimit); err != nil { return err } // require at least one target if !shareAll && len(hashes) == 0 && len(includeCategory) == 0 && len(includeTags) == 0 { return errors.New("no torrents specified. Use --hashes, --all, --include-category or --include-tags") } if len(hashes) > 0 { if err := utils.ValidateHash(hashes); err != nil { return errors.Wrap(err, "invalid hashes supplied") } } config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } // resolve target hashes if shareAll { hashes = []string{"all"} } else if len(includeCategory) > 0 || len(includeTags) > 0 { // when only tags are provided, fetch all torrents (empty category) // and filter them down by tag if len(includeCategory) == 0 { includeCategory = []string{""} } // append category/tag matches to any explicitly provided hashes // so the two selection methods union instead of overwrite for _, category := range includeCategory { torrents, err := qb.GetTorrentsCtx(ctx, qbittorrent.TorrentFilterOptions{Category: category}) if err != nil { return errors.Wrapf(err, "could not get torrents for category: %s", category) } for _, torrent := range torrents { if len(includeTags) > 0 { if _, validTag := validateTag(includeTags, torrent.Tags); !validTag { continue } } if len(excludeTags) > 0 { if _, found := validateTag(excludeTags, torrent.Tags); found { continue } } hashes = append(hashes, torrent.Hash) } } } if len(hashes) == 0 { log.Println("No torrents found to set share limits on") return nil } opts := qbittorrent.ShareLimitOptions{ RatioLimit: ratioLimit, SeedingTimeLimit: seedingTimeLimit, InactiveSeedingTimeLimit: inactiveSeedingTimeLimit, } limitsDesc := formatShareLimits(opts) target := strconv.Itoa(len(hashes)) + " torrent(s)" if shareAll { target = "all torrents" } if dry { log.Printf("dry-run: would set share limits (%s) on %s\n", limitsDesc, target) return nil } err := batchRequests(hashes, func(start, end int) error { return qb.SetTorrentShareLimitCtx(ctx, hashes[start:end], opts) }) if err != nil { return errors.Wrap(err, "could not set share limits") } log.Printf("successfully set share limits (%s) on %s\n", limitsDesc, target) return nil } return command } // validateShareLimits checks that each limit is within qBittorrent's accepted // range: -2 (global), -1 (unlimited) or any value >= 0. func validateShareLimits(ratioLimit float64, seedingTimeLimit, inactiveSeedingTimeLimit int64) error { // ratio is a float, so a simple `< -2` check would let fractional // sentinels like -1.5 or -0.5 through; only -2, -1 and >= 0 are valid if ratioLimit < 0 && ratioLimit != -1 && ratioLimit != -2 { return errors.Errorf("invalid ratio limit %s: must be -2 (global), -1 (unlimited) or >= 0", strconv.FormatFloat(ratioLimit, 'f', -1, 64)) } if seedingTimeLimit < -2 { return errors.Errorf("invalid seeding-time limit %d: must be -2 (global), -1 (unlimited) or >= 0", seedingTimeLimit) } if inactiveSeedingTimeLimit < -2 { return errors.Errorf("invalid inactive-seeding-time limit %d: must be -2 (global), -1 (unlimited) or >= 0", inactiveSeedingTimeLimit) } return nil } // formatShareLimits builds a human readable description of the share limits. func formatShareLimits(opts qbittorrent.ShareLimitOptions) string { return strings.Join([]string{ "ratio=" + formatRatioLimit(opts.RatioLimit), "seeding-time=" + formatTimeLimit(opts.SeedingTimeLimit), "inactive-seeding-time=" + formatTimeLimit(opts.InactiveSeedingTimeLimit), }, ", ") } func formatRatioLimit(ratioLimit float64) string { switch ratioLimit { case -2: return "global" case -1: return "unlimited" default: return strconv.FormatFloat(ratioLimit, 'f', 2, 64) } } func formatTimeLimit(minutes int64) string { switch minutes { case -2: return "global" case -1: return "unlimited" default: return strconv.FormatInt(minutes, 10) + "m" } } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_share_limit_test.go000066400000000000000000000057271522034161300266560ustar00rootroot00000000000000package cmd import ( "testing" "github.com/autobrr/go-qbittorrent" ) func Test_validateShareLimits(t *testing.T) { tests := []struct { name string ratioLimit float64 seedingTimeLimit int64 inactiveSeedingTimeLimit int64 wantErr bool }{ {name: "defaults global", ratioLimit: -2, seedingTimeLimit: -2, inactiveSeedingTimeLimit: -2, wantErr: false}, {name: "unlimited", ratioLimit: -1, seedingTimeLimit: -1, inactiveSeedingTimeLimit: -1, wantErr: false}, {name: "explicit values", ratioLimit: 2.0, seedingTimeLimit: 1440, inactiveSeedingTimeLimit: 0, wantErr: false}, {name: "zero ratio", ratioLimit: 0, seedingTimeLimit: -2, inactiveSeedingTimeLimit: -2, wantErr: false}, {name: "invalid ratio below -2", ratioLimit: -3, seedingTimeLimit: -2, inactiveSeedingTimeLimit: -2, wantErr: true}, {name: "invalid fractional ratio -1.5", ratioLimit: -1.5, seedingTimeLimit: -2, inactiveSeedingTimeLimit: -2, wantErr: true}, {name: "invalid fractional ratio -0.5", ratioLimit: -0.5, seedingTimeLimit: -2, inactiveSeedingTimeLimit: -2, wantErr: true}, {name: "invalid seeding-time", ratioLimit: -2, seedingTimeLimit: -3, inactiveSeedingTimeLimit: -2, wantErr: true}, {name: "invalid inactive-seeding-time", ratioLimit: -2, seedingTimeLimit: -2, inactiveSeedingTimeLimit: -10, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateShareLimits(tt.ratioLimit, tt.seedingTimeLimit, tt.inactiveSeedingTimeLimit) if (err != nil) != tt.wantErr { t.Errorf("validateShareLimits() error = %v, wantErr %v", err, tt.wantErr) } }) } } func Test_formatRatioLimit(t *testing.T) { tests := []struct { name string ratio float64 want string }{ {name: "global", ratio: -2, want: "global"}, {name: "unlimited", ratio: -1, want: "unlimited"}, {name: "zero", ratio: 0, want: "0.00"}, {name: "value", ratio: 2.5, want: "2.50"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := formatRatioLimit(tt.ratio); got != tt.want { t.Errorf("formatRatioLimit() = %v, want %v", got, tt.want) } }) } } func Test_formatTimeLimit(t *testing.T) { tests := []struct { name string minutes int64 want string }{ {name: "global", minutes: -2, want: "global"}, {name: "unlimited", minutes: -1, want: "unlimited"}, {name: "zero", minutes: 0, want: "0m"}, {name: "value", minutes: 1440, want: "1440m"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := formatTimeLimit(tt.minutes); got != tt.want { t.Errorf("formatTimeLimit() = %v, want %v", got, tt.want) } }) } } func Test_formatShareLimits(t *testing.T) { opts := qbittorrent.ShareLimitOptions{RatioLimit: 2.0, SeedingTimeLimit: 1440, InactiveSeedingTimeLimit: -2} want := "ratio=2.00, seeding-time=1440m, inactive-seeding-time=global" if got := formatShareLimits(opts); got != want { t.Errorf("formatShareLimits() = %v, want %v", got, want) } } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_tag.go000066400000000000000000000222411522034161300240600ustar00rootroot00000000000000package cmd import ( "context" "log" "strings" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/autobrr/go-qbittorrent" "github.com/dustin/go-humanize" "github.com/pkg/errors" "github.com/spf13/cobra" ) var trackerIssues = []string{ "tracker is down", "maintenance", "down", "it may be down", "unreachable", "(unreachable)", "bad gateway", "tracker unavailable", } var trackerMessages = []string{ "unregistered", "not registered", "torrent not found", "torrent is not found", "unknown torrent", "retitled", "truncated", "torrent is not authorized for use on this tracker", "infohash not found", "not found", "not exist", "unknown", "unknown torrent", "uploaded", "upgraded", "season pack", "packs are available", "pack is available", "internal available", "season pack out", "dead", "dupe", "complete season uploaded", "problem with", "specifically banned", "trump", "trumped", "nuked", "i'm sorry dave, i can't do that", "problem with description", "problem with file", "problem with pack", "specifically banned", "other", "torrent has been deleted", } //var DefaultTags = []string{ // "Not Working", // "Unregistered", // "Tracker Down", // //"Duplicate", //} type DefaultTag string const ( DefaultTagUnregistered DefaultTag = "Unregistered" DefaultTagNotWorking DefaultTag = "Not Working" ) func (t DefaultTag) String() string { switch t { case DefaultTagUnregistered: return "Unregistered" case DefaultTagNotWorking: return "Not Working" } return "" } // RunTorrentTag cmd for torrent tag operations func RunTorrentTag() *cobra.Command { var command = &cobra.Command{ Use: "tag", Short: "Torrent tag subcommand", Long: `Do various torrent tag operations`, } command.AddCommand(RunTorrentTagNotWorking()) return command } type tagData struct { Hashes []string HashTagMap map[string][]string TotalSize uint64 } // RunTorrentTagNotWorking tag torrents func RunTorrentTagNotWorking() *cobra.Command { var ( dryRun bool tagUnregistered bool tagNotWorking bool //size bool ) var command = &cobra.Command{ Use: "issues", Short: "tag torrents with issues", Long: `Tag torrents that may have broken trackers or be unregistered`, Example: ` qbt torrent tag issues --unregistered --not-working`, } command.Flags().BoolVar(&dryRun, "dry-run", false, "Dry run, do not tag torrents") command.Flags().BoolVar(&tagUnregistered, "unregistered", false, "tag unregistered") command.Flags().BoolVar(&tagNotWorking, "not-working", false, "tag not working torrents") //command.Flags().BoolVar(&size, "size", false, "collect size per tag") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := context.Background() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } torrents, err := qb.GetTorrentsCtx(ctx, qbittorrent.TorrentFilterOptions{}) if err != nil { return errors.Wrap(err, "could not get torrents") } var totalSize uint64 unregisteredTorrents := &tagData{ Hashes: []string{}, TotalSize: 0, } notWorkingTorrents := &tagData{ Hashes: []string{}, TotalSize: 0, } removeTaggedTorrents := &tagData{ Hashes: []string{}, HashTagMap: map[string][]string{ DefaultTagUnregistered.String(): {}, DefaultTagNotWorking.String(): {}, }, TotalSize: 0, } // process each torrent and check if tags should be added or removed for _, torrent := range torrents { totalSize += uint64(torrent.Size) trackers, err := qb.GetTorrentTrackersCtx(ctx, torrent.Hash) if err != nil { return errors.Wrapf(err, "could not get trackers for torrent: %s", torrent.Hash) } processTorrentTags(torrent, trackers, removeTaggedTorrents, unregisteredTorrents, notWorkingTorrents, tagUnregistered, tagUnregistered) } log.Printf("total torrents (%d) with a total size of: %s\n", len(torrents), humanize.Bytes(totalSize)) // remove tags from torrents that should not have certain tags if dryRun { log.Printf("dry-run: clearing defualt tags from torrents\n") } else { log.Printf("clearing defualt tags from torrents\n") for tag, hashes := range removeTaggedTorrents.HashTagMap { err := batchRequests(hashes, func(start, end int) error { return qb.RemoveTagsCtx(ctx, hashes[start:end], tag) }) if err != nil { return errors.Wrapf(err, "could not remove tag %s from torrents %v", tag, hashes) } log.Printf("successfully cleared tags from (%d) torrents\n", len(hashes)) } } // --unregistered add tag unregistered if tagUnregistered { countUnregisteredTorrents := len(unregisteredTorrents.Hashes) log.Printf("reclaimable space (%s) from (%d) unregistered torrents\n", humanize.Bytes(unregisteredTorrents.TotalSize), countUnregisteredTorrents) if dryRun { log.Printf("dry-run: tagging (%d) unregistered torrents\n", countUnregisteredTorrents) log.Printf("dry-run: successfully tagged (%d) unregistered torrents\n", countUnregisteredTorrents) } else { log.Printf("tagging (%d) unregistered torrents\n", countUnregisteredTorrents) err := batchRequests(unregisteredTorrents.Hashes, func(start, end int) error { return qb.AddTagsCtx(ctx, unregisteredTorrents.Hashes[start:end], DefaultTagUnregistered.String()) }) if err != nil { return errors.Wrapf(err, "could not add tag %s to torrents %v", DefaultTagUnregistered, unregisteredTorrents.Hashes) } log.Printf("successfully tagged (%d) unregistered torrents\n", countUnregisteredTorrents) } } if tagNotWorking { countNotWorkingTorrents := len(notWorkingTorrents.Hashes) log.Printf("reclaimable space (%s) from (%d) not working torrents\n", humanize.Bytes(notWorkingTorrents.TotalSize), countNotWorkingTorrents) if dryRun { log.Printf("dry-run: tagging (%d) not working torrents\n", countNotWorkingTorrents) log.Printf("dry-run: successfully tagged (%d) not working torrents\n", countNotWorkingTorrents) } else { log.Printf("tagging (%d) not working torrents\n", len(notWorkingTorrents.Hashes)) err := batchRequests(notWorkingTorrents.Hashes, func(start, end int) error { return qb.AddTagsCtx(ctx, notWorkingTorrents.Hashes[start:end], DefaultTagNotWorking.String()) }) if err != nil { return errors.Wrapf(err, "could not add tag %s to torrents %v", DefaultTagNotWorking, notWorkingTorrents.Hashes) } log.Printf("successfully tagged (%d) not working torrents\n", countNotWorkingTorrents) } } return nil } return command } func processTorrentTags(torrent qbittorrent.Torrent, trackers []qbittorrent.TorrentTracker, removeTaggedTorrents *tagData, unregisteredTorrents *tagData, notWorkingTorrents *tagData, tagUnregistered bool, tagNotWorking bool) bool { var isUnregistered bool var isNotWorking bool //var isTrackerDown bool // check for our default tags to first clear if torrent.Tags != "" { torrentTags := strings.Split(torrent.Tags, ", ") for _, tag := range torrentTags { if strings.Contains(tag, DefaultTagUnregistered.String()) { isUnregistered = true } if strings.Contains(tag, DefaultTagNotWorking.String()) { isNotWorking = true } } } foundTrackerUnregistered := false foundTrackerNotWorking := false for _, tracker := range trackers { if tracker.Status == qbittorrent.TrackerStatusDisabled { continue } //if tracker.Url == "** [DHT] **" || tracker.Url == "** [PeX] **" || tracker.Url == "** [LSD] **" { // continue //} lowerTrackerMessage := strings.ToLower(tracker.Message) if tagUnregistered { for _, msg := range trackerMessages { if strings.Contains(lowerTrackerMessage, msg) { //isUnregistered = true foundTrackerUnregistered = true break } } } if tagNotWorking { for _, msg := range trackerIssues { if strings.Contains(lowerTrackerMessage, msg) { //isNotWorking = true foundTrackerNotWorking = true break } } } } // if initial status was isNotWorking and the tracker message changed we need to clear the tag for the hash if isNotWorking && !foundTrackerNotWorking { removeTaggedTorrents.HashTagMap[DefaultTagNotWorking.String()] = append(removeTaggedTorrents.HashTagMap[DefaultTagUnregistered.String()], torrent.Hash) } if isUnregistered && !foundTrackerUnregistered { removeTaggedTorrents.HashTagMap[DefaultTagUnregistered.String()] = append(unregisteredTorrents.HashTagMap[DefaultTagUnregistered.String()], torrent.Hash) } // found new torrent to tag if !isUnregistered && foundTrackerUnregistered { unregisteredTorrents.TotalSize += uint64(torrent.Size) unregisteredTorrents.Hashes = append(unregisteredTorrents.Hashes, torrent.Hash) } // found new torrent to tag if !isNotWorking && foundTrackerNotWorking { notWorkingTorrents.TotalSize += uint64(torrent.Size) notWorkingTorrents.Hashes = append(notWorkingTorrents.Hashes, torrent.Hash) } return true } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_tag_test.go000066400000000000000000000041211522034161300251140ustar00rootroot00000000000000package cmd import ( "testing" "github.com/autobrr/go-qbittorrent" ) func Test_processTorrentTags(t *testing.T) { type args struct { torrent qbittorrent.Torrent trackers []qbittorrent.TorrentTracker removeTaggedTorrents *tagData unregisteredTorrents *tagData notWorkingTorrents *tagData tagUnregistered bool tagNotWorking bool } tests := []struct { name string args args want bool }{ { name: "test1", args: args{ torrent: qbittorrent.Torrent{Hash: "1111", Tracker: "", Tags: "Unregistered"}, trackers: []qbittorrent.TorrentTracker{ {Url: "http://test.local", Status: 2, NumPeers: 1, NumSeeds: 1, NumLeechers: 0, NumDownloaded: 1, Message: "OK"}, }, removeTaggedTorrents: &tagData{Hashes: []string{}, HashTagMap: map[string][]string{DefaultTagUnregistered.String(): {}, DefaultTagNotWorking.String(): {}}}, unregisteredTorrents: &tagData{Hashes: []string{}}, notWorkingTorrents: &tagData{Hashes: []string{}}, tagUnregistered: true, tagNotWorking: true, }, want: true, }, { name: "test", args: args{ torrent: qbittorrent.Torrent{Hash: "1111", Tracker: "", Tags: ""}, trackers: []qbittorrent.TorrentTracker{ {Url: "http://test.local", Status: 1, NumPeers: 1, NumSeeds: 1, NumLeechers: 0, NumDownloaded: 1, Message: "Unregistered torrent"}, }, removeTaggedTorrents: &tagData{Hashes: []string{}, HashTagMap: map[string][]string{DefaultTagUnregistered.String(): {}, DefaultTagNotWorking.String(): {}}}, unregisteredTorrents: &tagData{Hashes: []string{}}, notWorkingTorrents: &tagData{Hashes: []string{}}, tagUnregistered: true, tagNotWorking: true, }, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := processTorrentTags(tt.args.torrent, tt.args.trackers, tt.args.removeTaggedTorrents, tt.args.unregisteredTorrents, tt.args.notWorkingTorrents, tt.args.tagUnregistered, tt.args.tagNotWorking); got != tt.want { t.Errorf("processTorrent() = %v, want %v", got, tt.want) } }) } } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/torrent_tracker.go000066400000000000000000000053311522034161300247410ustar00rootroot00000000000000package cmd import ( "log" "strings" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/autobrr/go-qbittorrent" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTorrentTracker cmd for torrent tracker operations func RunTorrentTracker() *cobra.Command { var command = &cobra.Command{ Use: "tracker", Short: "Torrent tracker subcommand", Long: `Do various torrent category operations`, } command.AddCommand(RunTorrentTrackerEdit()) return command } // RunTorrentTrackerEdit cmd for torrent tracker operations func RunTorrentTrackerEdit() *cobra.Command { var command = &cobra.Command{ Use: "edit", Short: "Edit torrent tracker", Long: `Edit tracker for torrents via hashes`, Example: ` qbt torrent tracker edit --old url.old/test --new url.com/test`, } var ( dry bool oldURL string newURL string ) command.Flags().BoolVar(&dry, "dry-run", false, "Run without doing anything") command.Flags().StringVar(&oldURL, "old", "", "Old tracker URL to replace") command.Flags().StringVar(&newURL, "new", "", "New tracker URL") command.MarkFlagRequired("old") command.MarkFlagRequired("new") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } torrents, err := qb.GetTorrentsCtx(ctx, qbittorrent.TorrentFilterOptions{}) if err != nil { errors.Wrap(err, "could not get torrents") } var torrentsToUpdate []qbittorrent.Torrent for _, torrent := range torrents { if strings.Contains(torrent.Tracker, oldURL) { torrentsToUpdate = append(torrentsToUpdate, torrent) } } if len(torrentsToUpdate) == 0 { log.Printf("found no torrents with tracker %q\n", oldURL) return nil } for i, torrent := range torrentsToUpdate { if dry { log.Printf("dry-run: [%d/%d] updating tracker for torrent %s %q\n", i+1, len(torrentsToUpdate), torrent.Hash, torrent.Name) } else { log.Printf("[%d/%d] updating tracker for torrent %s %q\n", i+1, len(torrentsToUpdate), torrent.Hash, torrent.Name) if err := qb.EditTrackerCtx(ctx, torrent.Hash, torrent.Tracker, newURL); err != nil { return errors.Wrapf(err, "could not edit tracker for torrent: %s", torrent.Hash) } } } log.Printf("successfully updated tracker for (%d) torrents\n", len(torrentsToUpdate)) return nil } return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/transfer.go000066400000000000000000000035241522034161300233570ustar00rootroot00000000000000package cmd import ( "encoding/json" "fmt" "github.com/ludviglundgren/qbittorrent-cli/internal/config" "github.com/autobrr/go-qbittorrent" "github.com/dustin/go-humanize" "github.com/pkg/errors" "github.com/spf13/cobra" ) // RunTransfer cmd for transfer info func RunTransfer() *cobra.Command { var command = &cobra.Command{ Use: "transfer", Short: "Transfer info subcommand", Long: "Get status and speed info", } command.AddCommand(RunTransferInfo()) return command } // RunTransferInfo cmd to view transfer info func RunTransferInfo() *cobra.Command { var command = &cobra.Command{ Use: "info", Short: "Get qBittorrent transfer info", } var ( output string ) command.Flags().StringVar(&output, "output", "", "Print as [formatted text (default), json]") command.RunE = func(cmd *cobra.Command, args []string) error { config.InitConfig() qbtSettings := qbittorrent.Config{ Host: config.Qbit.Addr, APIKey: config.Qbit.APIKey, Username: config.Qbit.Login, Password: config.Qbit.Password, BasicUser: config.Qbit.BasicUser, BasicPass: config.Qbit.BasicPass, } qb := qbittorrent.NewClient(qbtSettings) ctx := cmd.Context() if err := qb.LoginCtx(ctx); err != nil { return errors.Wrap(err, "could not login to qbit") } info, err := qb.GetTransferInfo() if err != nil { return errors.Wrap(err, "could not get transfer info") } switch output { case "json": res, err := json.Marshal(info) if err != nil { return errors.Wrap(err, "could not marshal transfer info") } fmt.Println(string(res)) default: fmt.Printf("qBittorrent transfer info:\nConnection status: %s\nSession upload: %s\n\nSession download: %s\n", info.ConnectionStatus, humanize.Bytes(uint64(info.UpInfoData)), humanize.Bytes(uint64(info.DlInfoData))) } return nil } return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/update.go000066400000000000000000000023221522034161300230100ustar00rootroot00000000000000package cmd import ( "log" "github.com/blang/semver" "github.com/pkg/errors" "github.com/rhysd/go-github-selfupdate/selfupdate" "github.com/spf13/cobra" ) func RunUpdate(version string) *cobra.Command { var command = &cobra.Command{ Use: "update", Short: "Update qbittorrent-cli to latest version", Example: ` qbt update`, SilenceUsage: false, } var verbose bool command.Flags().BoolVar(&verbose, "verbose", false, "Verbose output: Print changelog") command.RunE = func(cmd *cobra.Command, args []string) error { v, err := semver.ParseTolerant(version) if err != nil { return errors.Wrapf(err, "could not parse version string: %s", version) } latest, err := selfupdate.UpdateSelf(v, "ludviglundgren/qbittorrent-cli") if err != nil { return errors.Wrap(err, "binary update failed") } if latest.Version.Equals(v) { // latest version is the same as current version. It means current binary is up-to-date. log.Println("Current binary is the latest version", version) } else { log.Println("Successfully updated to version: ", latest.Version) if verbose { log.Println("Release note:\n", latest.ReleaseNotes) } } return nil } return command } ludviglundgren-qbittorrent-cli-4dbbde7/cmd/version.go000066400000000000000000000020061522034161300232120ustar00rootroot00000000000000package cmd import ( "encoding/json" "fmt" "github.com/pkg/errors" "github.com/spf13/cobra" ) func RunVersion(version, commit, date string) *cobra.Command { var output string var command = &cobra.Command{ Use: "version", Short: "Print qbt version info", Example: ` qbt version qbt version --output json`, SilenceUsage: false, } command.Flags().StringVar(&output, "output", "text", "Print as [text, json]") command.RunE = func(cmd *cobra.Command, args []string) error { switch output { case "text": fmt.Printf(`qbt - qbitttorrent cli Version: %s Commit: %s Date: %s `, version, commit, date) return nil case "json": res, err := json.Marshal(versionInfo{Version: version, Commit: commit, Date: date}) if err != nil { return errors.Wrap(err, "could not marshal version info to json") } fmt.Println(string(res)) } return nil } return command } type versionInfo struct { Version string `json:"version"` Commit string `json:"commit"` Date string `json:"date"` } ludviglundgren-qbittorrent-cli-4dbbde7/docs/000077500000000000000000000000001522034161300213655ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/docs/.gitignore000066400000000000000000000003261522034161300233560ustar00rootroot00000000000000# build output dist/ # generated types .astro/ # dependencies node_modules/ # logs npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # environment variables .env .env.production # macOS .DS_Store ludviglundgren-qbittorrent-cli-4dbbde7/docs/astro.config.mjs000066400000000000000000000023201522034161300244710ustar00rootroot00000000000000// @ts-check import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; // https://astro.build/config export default defineConfig({ // GitHub Pages project site: served from https://.github.io// site: 'https://ludviglundgren.github.io', base: '/qbittorrent-cli', integrations: [ starlight({ title: 'qbittorrent-cli', description: 'A CLI to manage qBittorrent - add torrents, manage categories and tags, reannounce, import from other clients and more.', social: [ { icon: 'github', label: 'GitHub', href: 'https://github.com/ludviglundgren/qbittorrent-cli', }, ], editLink: { baseUrl: 'https://github.com/ludviglundgren/qbittorrent-cli/edit/master/docs/', }, sidebar: [ { label: 'Getting started', items: [ { label: 'Installation', slug: 'getting-started/installation' }, { label: 'Configuration', slug: 'getting-started/configuration' }, ], }, { label: 'Command reference', items: [{ autogenerate: { directory: 'commands' } }], }, ], }), ], }); ludviglundgren-qbittorrent-cli-4dbbde7/docs/package-lock.json000066400000000000000000007401131522034161300246070ustar00rootroot00000000000000{ "name": "qbittorrent-cli-docs", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "qbittorrent-cli-docs", "version": "0.0.1", "dependencies": { "@astrojs/starlight": "^0.41.1", "astro": "^7.0.3" } }, "node_modules/@astrojs/compiler-binding": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.2.3.tgz", "integrity": "sha512-Xz3iBNse+hXXD25IXxsuXEt2ai8klAWE15CRm/EQBc9+aE3jXaF07DZx+iakk3HC6NHvWlEPzLPyxsLgPzOJsw==", "license": "MIT", "engines": { "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { "@astrojs/compiler-binding-darwin-arm64": "0.2.3", "@astrojs/compiler-binding-darwin-x64": "0.2.3", "@astrojs/compiler-binding-linux-arm64-gnu": "0.2.3", "@astrojs/compiler-binding-linux-arm64-musl": "0.2.3", "@astrojs/compiler-binding-linux-x64-gnu": "0.2.3", "@astrojs/compiler-binding-linux-x64-musl": "0.2.3", "@astrojs/compiler-binding-wasm32-wasi": "0.2.3", "@astrojs/compiler-binding-win32-arm64-msvc": "0.2.3", "@astrojs/compiler-binding-win32-x64-msvc": "0.2.3" } }, "node_modules/@astrojs/compiler-binding-darwin-arm64": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.2.3.tgz", "integrity": "sha512-sJIHeL1ONXEBLob8ZaXfmX6iCftUno08G/cMXj2FJnL0xNbHuELcEq1mjxHVFHNgUYu4P7xJNm2mpc0zUEPoKw==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@astrojs/compiler-binding-darwin-x64": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.2.3.tgz", "integrity": "sha512-P0NYu6aaIeLCqFfszxxBHL0a5WRaYigNVbDoO654Gi5Q2au5duDb5xZBv5EqUg4qnQVC173FXNvGZu1M7nk+/w==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.2.3.tgz", "integrity": "sha512-PqVN5AqhuDqfx3ejaerwrC8codpV9jnyKV+IOel027qsJ1anFUJLdjUlY8VVys0xgd8lmqveX11OkcaQj/otTg==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.2.3.tgz", "integrity": "sha512-O3e2CbN4yTsRguWYNnRd0p5YQ0H3fb7KpcR0W4R319q/gq5B1pJ7eqNbiO3b8g2AuiEcRTiUz5jeGT9j69cxOQ==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.2.3.tgz", "integrity": "sha512-hbLBjXVp+96psMe7/7uqyrquGiULXANrq6REVxxPK/I5VzebZ7LHmSfykmByUbLyR1u+K6CTBKgvdQsK2L+2Xw==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@astrojs/compiler-binding-linux-x64-musl": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.2.3.tgz", "integrity": "sha512-vIiEvOwrJfHZMaTmqUCrFTIwMYL0+PD3Rvy7kFDQgERyx3zhaw8CPa01MCCqa+/sj344BGrXKZ6ti37SgNLMhw==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@astrojs/compiler-binding-wasm32-wasi": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.2.3.tgz", "integrity": "sha512-p9S2X8z/mUR2SMzAVJRFMCt8YaalKR+pjl2DgpdjzCQc6ww4bo8kiy54tgKqxZeNF5c+/2tCDTQIxVSm9V1FsA==", "cpu": [ "wasm32" ], "license": "MIT", "optional": true, "dependencies": { "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.2.3.tgz", "integrity": "sha512-vcCG6JttIb5vbSmcxO2O398hpVj7lQ349iS7cjgYP6ZuLVEnw+9qPAr2MM2kJkU5wEGZqJ2gyi/M7UJoPwH1iQ==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.2.3.tgz", "integrity": "sha512-hKssjNvC36e00Inb1GW1JsVyCFSCGnIjKem4S8q0VIW6cpWAUpvYB4qQU2HIDGD6SDX0ork4F5sWkNWkp2hrGQ==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@astrojs/compiler-rs": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.2.3.tgz", "integrity": "sha512-JRAtRcPxS4JeAZEIQFQ6GecBs/Wyp4m6/E8vBNxSgVfo1AtRVLUqRCl5oCGOZ0X/BSBB3Vef/7IlzyiGKi2ORA==", "license": "MIT", "dependencies": { "@astrojs/compiler-binding": "0.2.3" } }, "node_modules/@astrojs/internal-helpers": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.0.tgz", "integrity": "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "node_modules/@astrojs/markdown-remark": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.0.tgz", "integrity": "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==", "license": "MIT", "dependencies": { "@astrojs/internal-helpers": "0.10.0", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "node_modules/@astrojs/markdown-satteri": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.3.2.tgz", "integrity": "sha512-feXuUPy41gVfeM7EHT1ciUim8ozGr+YHXab9uUBc1Hk8y60DQosO8ldL+AoPXnCAoGj1OChwHfvXmmJ6XVnY9A==", "license": "MIT", "dependencies": { "@astrojs/internal-helpers": "0.10.0", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "satteri": "^0.9.1" } }, "node_modules/@astrojs/mdx": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-7.0.0.tgz", "integrity": "sha512-LKwNA8nnLtEM0auoP6OfH/UnlKe1Ub59qZjbcYkZjPBGw6PkJewWkA/1qwLpECvV6gMDd6TR6eqV9p/VYZrcrQ==", "license": "MIT", "dependencies": { "@astrojs/internal-helpers": "0.10.0", "@astrojs/markdown-remark": "7.2.0", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "engines": { "node": ">=22.12.0" }, "peerDependencies": { "@astrojs/markdown-satteri": "^0.3.1-alpha.0", "astro": "^7.0.0-alpha.0" }, "peerDependenciesMeta": { "@astrojs/markdown-satteri": { "optional": true } } }, "node_modules/@astrojs/prism": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", "license": "MIT", "dependencies": { "prismjs": "^1.30.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@astrojs/sitemap": { "version": "3.7.3", "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.3.tgz", "integrity": "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==", "license": "MIT", "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "node_modules/@astrojs/starlight": { "version": "0.41.1", "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.41.1.tgz", "integrity": "sha512-avf2OmrVg6GdVU18juebjjIIuLa+uS3syHuJ/3yDaEFP/8it+YvcxRrYDSf7K6rC4v770UxIddba2hAqQyTeYA==", "license": "MIT", "dependencies": { "@astrojs/markdown-satteri": "^0.3.2", "@astrojs/mdx": "^7.0.0", "@astrojs/sitemap": "^3.7.2", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.44.0", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.3", "hast-util-select": "^6.0.4", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", "i18next": "^26.0.7", "js-yaml": "^4.1.1", "klona": "^2.0.6", "magic-string": "^0.30.21", "mdast-util-directive": "^3.1.0", "mdast-util-to-markdown": "^2.1.2", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.5.2", "rehype": "^13.0.2", "rehype-format": "^5.0.1", "remark-directive": "^4.0.0", "satteri": "^0.9.1", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@astrojs/markdown-remark": "^7.2.0", "astro": "^7.0.2" }, "peerDependenciesMeta": { "@astrojs/markdown-remark": { "optional": true } } }, "node_modules/@astrojs/telemetry": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", "integrity": "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==", "license": "MIT", "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "is-wsl": "^3.1.1", "which-pm-runs": "^1.1.0" }, "engines": { "node": "18.20.8 || ^20.3.0 || >=22.0.0" } }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/types": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@bruits/satteri-darwin-arm64": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.9.3.tgz", "integrity": "sha512-dRUZZrdwh1asfTOyM1nDNmzolhnHtlIFpqYrl1Tdd3YVcaebKmrfJgGL7NAoGPjbEwYmZxaugrxA0uzw83c0dw==", "cpu": [ "arm64" ], "optional": true, "os": [ "darwin" ] }, "node_modules/@bruits/satteri-darwin-x64": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.9.3.tgz", "integrity": "sha512-wgNCTRp2hPSpNMGFv5A4+6+VXgRJIlBZ7XKb3iwjV8YjRWNIjzE5zV2fUeYynyZYVRkuJ9aYFqQmWhc1e5H+UQ==", "cpu": [ "x64" ], "optional": true, "os": [ "darwin" ] }, "node_modules/@bruits/satteri-linux-arm64-gnu": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.9.3.tgz", "integrity": "sha512-A/pWy8Jb/PhDYc2/JFuYh06gFJcsfBUBDl81YydGYBrL/Z4nItDfhNDNOibyeSN/lKKDRlycIHEIajjErk00sQ==", "cpu": [ "arm64" ], "optional": true, "os": [ "linux" ] }, "node_modules/@bruits/satteri-linux-arm64-musl": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.9.3.tgz", "integrity": "sha512-L6YxmyOSickzo4pE5WmZfNTJnjX0MtgKOsuwQfNZECTx9Ir5vl2B37EIwnxe2AybuPPHl+FqVQtthNDUdH4Vgg==", "cpu": [ "arm64" ], "optional": true, "os": [ "linux" ] }, "node_modules/@bruits/satteri-linux-x64-gnu": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.9.3.tgz", "integrity": "sha512-RgH6GPihg9Lzs2yHUsMjqiLxfLyOdmBty8sg9pBY9B4CBnvdOzvg8vklqN+C4qrEEdA9TwpbDpHr1AshLKyRpw==", "cpu": [ "x64" ], "optional": true, "os": [ "linux" ] }, "node_modules/@bruits/satteri-linux-x64-musl": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.9.3.tgz", "integrity": "sha512-BeWhVORjNTIomePznUKiMbHZTqC0j7sMXZFsISmbX+po5d33KLkqBqKh6K332CHJ8KUmCWx16FfPjwsoysttQg==", "cpu": [ "x64" ], "optional": true, "os": [ "linux" ] }, "node_modules/@bruits/satteri-wasm32-wasi": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.9.3.tgz", "integrity": "sha512-dFNcOHKWV2cztCPnYTn7kZ9D7kNOt8N239z5ysFkNHLxJrfK7zaKIXQbfXYN32C+JoVFqAcTIOeWH2+VnsCOHg==", "cpu": [ "wasm32" ], "optional": true, "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@bruits/satteri-win32-arm64-msvc": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.9.3.tgz", "integrity": "sha512-VnwjBHiAra/PNNEza8eSZdQiG4A3PtTJJwUDtOPAc6iTs0BWZwZX8+OPUZE7//yQCBhgvEMcI8vpwsAwCb6qGQ==", "cpu": [ "arm64" ], "optional": true, "os": [ "win32" ] }, "node_modules/@bruits/satteri-win32-x64-msvc": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.9.3.tgz", "integrity": "sha512-Dsoe4reWe69MyILmMwU6iISIceTW7YIFqbyym7haf9DhUvqkYfMAyp7GMM21JzV0SpG9A2BwzFVP7iq9mmxrpA==", "cpu": [ "x64" ], "optional": true, "os": [ "win32" ] }, "node_modules/@capsizecss/unpack": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", "license": "MIT", "dependencies": { "fontkitten": "^1.0.3" }, "engines": { "node": ">=18" } }, "node_modules/@clack/core": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.2.tgz", "integrity": "sha512-0Ty/1Gfm+Kb07sXcuESjyKfwEhSy4Ns1AgeEisHb/bDY5fWme0tTeTkU14T1Gmcs17YIjB/teiDe4uaCghbYqQ==", "license": "MIT", "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" }, "engines": { "node": ">= 20.12.0" } }, "node_modules/@clack/prompts": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.6.0.tgz", "integrity": "sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==", "license": "MIT", "dependencies": { "@clack/core": "1.4.2", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" }, "engines": { "node": ">= 20.12.0" } }, "node_modules/@ctrl/tinycolor": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", "license": "MIT", "engines": { "node": ">=14" } }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "license": "MIT", "optional": true, "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-arm": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], "license": "MIT", "optional": true, "os": [ "android" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "android" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "android" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/openharmony-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "openharmony" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { "node": ">=18" } }, "node_modules/@expressive-code/core": { "version": "0.44.0", "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.0.tgz", "integrity": "sha512-xgiF2P6tYUbrhi3+x0S8xHZWT1t3Bvb3U91tAtRbLb9HLejLvYc5GZUqKICKLaUN4iSGhhNJu2fM/aH8e5yCMg==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "node_modules/@expressive-code/plugin-frames": { "version": "0.44.0", "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.0.tgz", "integrity": "sha512-V6M6+zVc1GzqCvXkQHc2m5rcFOIVzJgMq5gnfrMnVf2gwtj/sg4H93c1f/mGeqHycubwkHFUDyParAOiGeDZeA==", "license": "MIT", "dependencies": { "@expressive-code/core": "^0.44.0" } }, "node_modules/@expressive-code/plugin-shiki": { "version": "0.44.0", "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.0.tgz", "integrity": "sha512-RZsdaqlbGqyAQKuoX4myQXxjmiE2l5KBpJ/gKPh62tCdIdpWyjbzVqSo8+5XsezZxkfi8AJ/J6EUaBTPROFX/Q==", "license": "MIT", "dependencies": { "@expressive-code/core": "^0.44.0", "shiki": "^4.0.2" } }, "node_modules/@expressive-code/plugin-text-markers": { "version": "0.44.0", "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.0.tgz", "integrity": "sha512-0/m3A5b+lz2upyNq+wzZ1S69HRoJmyFs5LsR42lVZ9pmGRlBiSBYQpvqlji4DBj1+Riamxc0AvcCr5kuzOQeWA==", "license": "MIT", "dependencies": { "@expressive-code/core": "^0.44.0" } }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, "engines": { "node": ">=18" } }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@mdx-js/mdx": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "license": "MIT", "optional": true, "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "node_modules/@oslojs/encoding": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, "node_modules/@oxc-project/types": { "version": "0.137.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@pagefind/darwin-arm64": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz", "integrity": "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@pagefind/darwin-x64": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.5.2.tgz", "integrity": "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@pagefind/default-ui": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.5.2.tgz", "integrity": "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==", "license": "MIT" }, "node_modules/@pagefind/freebsd-x64": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.5.2.tgz", "integrity": "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@pagefind/linux-arm64": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.5.2.tgz", "integrity": "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@pagefind/linux-x64": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.5.2.tgz", "integrity": "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@pagefind/windows-arm64": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@pagefind/windows-arm64/-/windows-arm64-1.5.2.tgz", "integrity": "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@pagefind/windows-x64": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.5.2.tgz", "integrity": "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "android" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-darwin-arm64": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-darwin-x64": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-freebsd-x64": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", "cpu": [ "arm" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-arm64-musl": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", "cpu": [ "ppc64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", "cpu": [ "s390x" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-x64-gnu": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-x64-musl": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-openharmony-arm64": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "openharmony" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-wasm32-wasi": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", "cpu": [ "wasm32" ], "license": "MIT", "optional": true, "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, "node_modules/@rollup/pluginutils": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { "optional": true } } }, "node_modules/@rollup/pluginutils/node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, "node_modules/@shikijs/core": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.0.tgz", "integrity": "sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ==", "license": "MIT", "dependencies": { "@shikijs/primitive": "4.3.0", "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/engine-javascript": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.3.0.tgz", "integrity": "sha512-hTv/KiFf2tpiqlACPiztGGurEARWIutB8YUhcrA1pUC7VzzwKO+g5crUocrLztrZ5ro5Z4hbXg7bYclETn3gSQ==", "license": "MIT", "dependencies": { "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/engine-oniguruma": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.0.tgz", "integrity": "sha512-1vMdN3gHfnKfLYwecUI2ITJI4RhHt96xEaJumVn7Heb0IlJ8WQMIH0Voak+2j22BpSNKdnOfB/pCTPnPm2gq7A==", "license": "MIT", "dependencies": { "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/langs": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.3.0.tgz", "integrity": "sha512-rnlqFbBRSys9bT4gl/5rw9RnS0W/I84ZldXPkO7cvlEMoV85TyF/aU01N7/NbSR776RNLjrJKjfFUXJR6wN1Cg==", "license": "MIT", "dependencies": { "@shikijs/types": "4.3.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/primitive": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.3.0.tgz", "integrity": "sha512-CPkz64PTa5diRW1ggzMZH9VM/du4RNChYgVtgqrFcgruvIybmCvySv8GkiHSczUHXYuuR8TdKEwFx+UnZMpgdg==", "license": "MIT", "dependencies": { "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/themes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.3.0.tgz", "integrity": "sha512-Avgt05YiT+Y3prjIc9lmQxhJzHBcCfR6cjiFW4OyaMBbt2A6trX5rfjUzx+Vj/mE9qpArYjatnqo9XPjQNW/AQ==", "license": "MIT", "dependencies": { "@shikijs/types": "4.3.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/types": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.3.0.tgz", "integrity": "sha512-oc8b9U2SYvofKZk8e/737nIX0qwf6eV2vHFATeObAu7r+mUVpLs8Re0BmVkIjAWAYgkmG/CzLNo7rzuBzRu/wQ==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/vscode-textmate": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "license": "MIT", "dependencies": { "@types/ms": "*" } }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", "license": "MIT", "dependencies": { "@types/estree": "*" } }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "license": "MIT", "dependencies": { "@types/unist": "*" } }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", "license": "MIT" }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "license": "MIT", "dependencies": { "@types/unist": "*" } }, "node_modules/@types/mdx": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", "license": "MIT" }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/nlcst": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", "license": "MIT", "dependencies": { "@types/unist": "*" } }, "node_modules/@types/node": { "version": "24.13.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", "license": "MIT", "dependencies": { "undici-types": "~7.18.0" } }, "node_modules/@types/sax": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, "node_modules/@ungap/structured-clone": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", "license": "ISC" }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", "bin": { "acorn": "bin/acorn" }, "engines": { "node": ">=0.4.0" } }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/am-i-vibing": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.4.0.tgz", "integrity": "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==", "license": "MIT", "dependencies": { "process-ancestry": "^0.1.0" }, "bin": { "am-i-vibing": "dist/cli.mjs" } }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" }, "engines": { "node": ">= 8" } }, "node_modules/anymatch/node_modules/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "license": "Apache-2.0", "engines": { "node": ">= 0.4" } }, "node_modules/array-iterate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", "license": "MIT", "bin": { "astring": "bin/astring" } }, "node_modules/astro": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/astro/-/astro-7.0.3.tgz", "integrity": "sha512-CK+G+Tl2DMV1EXCwVG45vyurxf2IfRTklMxDhRKn+tst9Yl8rWXpudL62Fa6zin5Bt968FBvuyASj1aJShROZg==", "license": "MIT", "dependencies": { "@astrojs/compiler-rs": "^0.2.2", "@astrojs/internal-helpers": "0.10.0", "@astrojs/markdown-satteri": "0.3.2", "@astrojs/telemetry": "3.3.2", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", "unstorage": "^1.17.5", "vfile": "^6.0.3", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "bin": { "astro": "bin/astro.mjs" }, "engines": { "node": ">=22.12.0", "npm": ">=9.6.5", "pnpm": ">=7.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/astrodotbuild" }, "optionalDependencies": { "sharp": "^0.34.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.0" }, "peerDependenciesMeta": { "@astrojs/markdown-remark": { "optional": true } } }, "node_modules/astro-expressive-code": { "version": "0.44.0", "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.0.tgz", "integrity": "sha512-b1wN/ZvbJprzxlGKIpIes2kQrCY5KRLwys2tWbZAZyjGZcW5ZtgneZnBwzNRiBna9/48d4mQl19KLjcRuhO1hw==", "license": "MIT", "dependencies": { "rehype-expressive-code": "^0.44.0", "url-extras": "^0.1.0" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" } }, "node_modules/astro/node_modules/@img/sharp-darwin-arm64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/astro/node_modules/@img/sharp-darwin-x64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/astro/node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-libvips-darwin-x64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-libvips-linux-arm": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-libvips-linux-arm64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-libvips-linux-ppc64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-libvips-linux-riscv64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", "cpu": [ "riscv64" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-libvips-linux-s390x": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-libvips-linux-x64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-libvips-linuxmusl-arm64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-libvips-linuxmusl-x64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-linux-arm": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/astro/node_modules/@img/sharp-linux-arm64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" } }, "node_modules/astro/node_modules/@img/sharp-linux-ppc64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", "cpu": [ "ppc64" ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" } }, "node_modules/astro/node_modules/@img/sharp-linux-riscv64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", "cpu": [ "riscv64" ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/astro/node_modules/@img/sharp-linux-s390x": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/astro/node_modules/@img/sharp-linux-x64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/astro/node_modules/@img/sharp-linuxmusl-arm64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/astro/node_modules/@img/sharp-linuxmusl-x64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/astro/node_modules/@img/sharp-wasm32": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-win32-arm64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-win32-ia32": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/@img/sharp-win32-x64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/astro/node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "license": "Apache-2.0", "engines": { "node": ">= 0.4" } }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/bcp-47": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/bcp-47-match": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/character-entities-html4": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/character-entities-legacy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/character-reference-invalid": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "license": "MIT", "dependencies": { "readdirp": "^5.0.0" }, "engines": { "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/ci-info": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/sibiraj-s" } ], "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/collapse-white-space": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "license": "MIT", "engines": { "node": ">=16" } }, "node_modules/common-ancestor-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", "license": "BlueOak-1.0.0", "engines": { "node": ">= 18" } }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { "node": ">=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, "node_modules/cookie-es": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", "license": "MIT" }, "node_modules/crossws": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", "license": "MIT", "dependencies": { "uncrypto": "^0.1.3" } }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" }, "funding": { "url": "https://github.com/sponsors/fb55" } }, "node_modules/css-selector-parser": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/mdevils" }, { "type": "patreon", "url": "https://patreon.com/mdevils" } ], "license": "MIT" }, "node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "license": "MIT", "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, "node_modules/css-what": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, "funding": { "url": "https://github.com/sponsors/fb55" } }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, "engines": { "node": ">=4" } }, "node_modules/csso": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", "license": "MIT", "dependencies": { "css-tree": "~2.2.0" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", "npm": ">=7.0.0" } }, "node_modules/csso/node_modules/css-tree": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", "license": "MIT", "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", "npm": ">=7.0.0" } }, "node_modules/csso/node_modules/mdn-data": { "version": "2.0.28", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" }, "engines": { "node": ">=6.0" }, "peerDependenciesMeta": { "supports-color": { "optional": true } } }, "node_modules/decode-named-character-reference": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/defu": { "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", "license": "MIT" }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" } }, "node_modules/devalue": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", "license": "MIT" }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "license": "MIT", "dependencies": { "dequal": "^2.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/diff": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, "node_modules/direction": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", "license": "MIT", "bin": { "direction": "cli.js" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" }, "funding": { "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, "node_modules/dom-serializer/node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/fb55" } ], "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, "engines": { "node": ">= 4" }, "funding": { "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" } }, "node_modules/dset": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/es-module-lexer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "license": "MIT" }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/esast-util-from-js": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "hasInstallScript": true, "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { "node": ">=18" }, "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escape-string-regexp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/estree-util-attach-comments": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/estree-util-build-jsx": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/estree-util-is-identifier-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/estree-util-scope": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/estree-util-to-js": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/estree-util-visit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/expressive-code": { "version": "0.44.0", "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.0.tgz", "integrity": "sha512-JXVWVNCKlLuZLMQH8cOiDUSosT0Bb+elwE/dbAkpwFwDFmyFyWlECoWZIohh2FkIF1iI67TQJ+Ts9k7oNDh2qA==", "license": "MIT", "dependencies": { "@expressive-code/core": "^0.44.0", "@expressive-code/plugin-frames": "^0.44.0", "@expressive-code/plugin-shiki": "^0.44.0", "@expressive-code/plugin-text-markers": "^0.44.0" } }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, "node_modules/fast-string-truncated-width": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", "license": "MIT" }, "node_modules/fast-string-width": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", "license": "MIT", "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "node_modules/fast-wrap-ansi": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", "license": "MIT", "dependencies": { "fast-string-width": "^3.0.2" } }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", "engines": { "node": ">=12.0.0" }, "peerDependencies": { "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { "picomatch": { "optional": true } } }, "node_modules/flattie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/fontace": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", "license": "MIT", "dependencies": { "fontkitten": "^1.0.2" } }, "node_modules/fontkitten": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", "license": "MIT", "dependencies": { "tiny-inflate": "^1.0.3" }, "engines": { "node": ">=20" } }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/get-tsconfig": { "version": "5.0.0-beta.4", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, "engines": { "node": ">=20.20.0" }, "funding": { "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", "license": "ISC" }, "node_modules/h3": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", "license": "MIT", "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "node_modules/hast-util-embedded": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-format": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-from-html": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-from-parse5": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-has-property": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-is-body-ok-link": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-is-element": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-minify-whitespace": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-parse-selector": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-phrasing": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-raw": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-select": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-to-estree": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-to-parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-to-string": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-to-text": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/hastscript": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", "license": "MIT" }, "node_modules/html-void-elements": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/html-whitespace-sensitive-tag-names": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, "node_modules/i18next": { "version": "26.3.3", "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.3.tgz", "integrity": "sha512-aYVegyBdXSO93CMMihvr47jI7GHSOcIahMpJX+qzUXDzW4xDJf2uenIA+45vDU+YhiVdcfsql70AC9RVdMNrHg==", "funding": [ { "type": "individual", "url": "https://www.locize.com/i18next" }, { "type": "individual", "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" }, { "type": "individual", "url": "https://www.locize.com" } ], "license": "MIT", "peerDependencies": { "typescript": "^5 || ^6" }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/brc-dd" } }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-alphanumerical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-docker": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-hexadecimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "license": "MIT", "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" }, "engines": { "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-inside-container/node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" }, "engines": { "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/js-yaml": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/puzrin" }, { "type": "github", "url": "https://github.com/sponsors/nodeca" } ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "node_modules/jsonc-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, "node_modules/klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" }, "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], "license": "MPL-2.0", "optional": true, "os": [ "android" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-darwin-arm64": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], "license": "MPL-2.0", "optional": true, "os": [ "darwin" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-darwin-x64": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], "license": "MPL-2.0", "optional": true, "os": [ "darwin" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-freebsd-x64": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], "license": "MPL-2.0", "optional": true, "os": [ "freebsd" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-linux-arm-gnueabihf": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-linux-arm64-gnu": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-linux-arm64-musl": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-win32-arm64-msvc": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], "license": "MPL-2.0", "optional": true, "os": [ "win32" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-win32-x64-msvc": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], "license": "MPL-2.0", "optional": true, "os": [ "win32" ], "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/lru-cache": { "version": "11.5.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/magicast": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "node_modules/markdown-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", "license": "MIT", "engines": { "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/mdast-util-definitions": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-directive": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-from-markdown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-gfm": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-gfm-autolink-literal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-gfm-footnote": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-gfm-strikethrough": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-gfm-table": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-gfm-task-list-item": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-mdx": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-mdx-jsx": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-mdxjs-esm": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-phrasing": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-to-markdown": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdast-util-to-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/mdn-data": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "license": "CC0-1.0" }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-core-commonmark": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-extension-directive": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-gfm": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", "license": "MIT", "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-gfm-autolink-literal": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-gfm-footnote": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-gfm-strikethrough": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-gfm-table": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-gfm-tagfilter": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-gfm-task-list-item": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-mdx-expression": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-extension-mdx-jsx": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-mdx-md": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-mdxjs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", "license": "MIT", "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-mdxjs-esm": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/micromark-factory-destination": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-label": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-mdx-expression": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "node_modules/micromark-factory-space": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-title": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-whitespace": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-chunked": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-classify-character": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-combine-extensions": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-decode-numeric-character-reference": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-decode-string": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-encode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT" }, "node_modules/micromark-util-events-to-acorn": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT" }, "node_modules/micromark-util-normalize-identifier": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-resolve-all": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-sanitize-uri": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-subtokenize": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-symbol": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT" }, "node_modules/micromark-util-types": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, { "type": "OpenCollective", "url": "https://opencollective.com/unified" } ], "license": "MIT" }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.15", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, "engines": { "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/neotraverse": { "version": "0.6.18", "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", "license": "MIT", "engines": { "node": ">= 10" } }, "node_modules/nlcst-to-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", "license": "MIT", "dependencies": { "@types/nlcst": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "license": "MIT" }, "node_modules/node-mock-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" }, "funding": { "url": "https://github.com/fb55/nth-check?sponsor=1" } }, "node_modules/obug": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], "license": "MIT", "engines": { "node": ">=12.20.0" } }, "node_modules/ofetch": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", "license": "MIT", "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "license": "MIT" }, "node_modules/oniguruma-parser": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", "license": "MIT" }, "node_modules/oniguruma-to-es": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", "license": "MIT", "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "node_modules/p-limit": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", "license": "MIT", "dependencies": { "yocto-queue": "^1.2.1" }, "engines": { "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-queue": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", "license": "MIT", "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" }, "engines": { "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-timeout": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", "license": "MIT", "engines": { "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/package-manager-detector": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "license": "MIT" }, "node_modules/pagefind": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz", "integrity": "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==", "license": "MIT", "bin": { "pagefind": "lib/runner/bin.cjs" }, "optionalDependencies": { "@pagefind/darwin-arm64": "1.5.2", "@pagefind/darwin-x64": "1.5.2", "@pagefind/freebsd-x64": "1.5.2", "@pagefind/linux-arm64": "1.5.2", "@pagefind/linux-x64": "1.5.2", "@pagefind/windows-arm64": "1.5.2", "@pagefind/windows-x64": "1.5.2" } }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/parse-entities/node_modules/@types/unist": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, "node_modules/parse-latin": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", "license": "MIT", "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, "node_modules/piccolore": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", "license": "ISC" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/postcss": { "version": "8.5.16", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "funding": [ { "type": "opencollective", "url": "https://opencollective.com/postcss/" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-nested": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "funding": [ { "type": "opencollective", "url": "https://opencollective.com/postcss/" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-selector-parser": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" }, "engines": { "node": ">=4" } }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/process-ancestry": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/process-ancestry/-/process-ancestry-0.1.0.tgz", "integrity": "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==", "license": "MIT", "engines": { "node": ">=18.0.0" } }, "node_modules/property-information": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/radix3": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", "license": "MIT" }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "license": "MIT", "engines": { "node": ">= 20.19.0" }, "funding": { "type": "individual", "url": "https://paulmillr.com/funding/" } }, "node_modules/recma-build-jsx": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/recma-jsx": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", "license": "MIT", "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/recma-parse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/recma-stringify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", "license": "MIT", "dependencies": { "regex-utilities": "^2.3.0" } }, "node_modules/regex-recursion": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", "license": "MIT", "dependencies": { "regex-utilities": "^2.3.0" } }, "node_modules/regex-utilities": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, "node_modules/rehype": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/rehype-expressive-code": { "version": "0.44.0", "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.0.tgz", "integrity": "sha512-5r74C5F2sMR3X+QJH8OKWgZBO/cqRw5W1fLT6GVlSfLqepk+4j8tGFkyPqZYWjwntsBHzKPDH2zI68sZ7ScLfA==", "license": "MIT", "dependencies": { "expressive-code": "^0.44.0" } }, "node_modules/rehype-format": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/rehype-parse": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/rehype-raw": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/rehype-recma": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/rehype-stringify": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/remark-directive": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-4.0.0.tgz", "integrity": "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^4.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/remark-mdx": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", "license": "MIT", "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/remark-rehype": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/remark-smartypants": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", "license": "MIT", "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" }, "engines": { "node": ">=16.0.0" } }, "node_modules/remark-stringify": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, "node_modules/retext": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", "license": "MIT", "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/retext-latin": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", "license": "MIT", "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/retext-smartypants": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", "license": "MIT", "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/retext-stringify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", "license": "MIT", "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/rolldown": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", "license": "MIT", "dependencies": { "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.3", "@rolldown/binding-darwin-arm64": "1.1.3", "@rolldown/binding-darwin-x64": "1.1.3", "@rolldown/binding-freebsd-x64": "1.1.3", "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", "@rolldown/binding-linux-arm64-gnu": "1.1.3", "@rolldown/binding-linux-arm64-musl": "1.1.3", "@rolldown/binding-linux-ppc64-gnu": "1.1.3", "@rolldown/binding-linux-s390x-gnu": "1.1.3", "@rolldown/binding-linux-x64-gnu": "1.1.3", "@rolldown/binding-linux-x64-musl": "1.1.3", "@rolldown/binding-openharmony-arm64": "1.1.3", "@rolldown/binding-wasm32-wasi": "1.1.3", "@rolldown/binding-win32-arm64-msvc": "1.1.3", "@rolldown/binding-win32-x64-msvc": "1.1.3" } }, "node_modules/satteri": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.9.3.tgz", "integrity": "sha512-2XfBh89LCnBMFkNOeVKkBLelAZcIA17VLHsgJum1tJ2fXiPZDN/TDXv4ku46rFOQXYd41LJ0kiZh5gPqExcCsg==", "dependencies": { "@types/estree-jsx": "^1.0.5", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "@types/unist": "^3.0.3" }, "optionalDependencies": { "@bruits/satteri-darwin-arm64": "0.9.3", "@bruits/satteri-darwin-x64": "0.9.3", "@bruits/satteri-linux-arm64-gnu": "0.9.3", "@bruits/satteri-linux-arm64-musl": "0.9.3", "@bruits/satteri-linux-x64-gnu": "0.9.3", "@bruits/satteri-linux-x64-musl": "0.9.3", "@bruits/satteri-wasm32-wasi": "0.9.3", "@bruits/satteri-win32-arm64-msvc": "0.9.3", "@bruits/satteri-win32-x64-msvc": "0.9.3" } }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" } }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/shiki": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.3.0.tgz", "integrity": "sha512-NKKjWzR6LIGL3sXBrWDw9sDS9cxx42/DkysaNqJEeOWE8Kix5gpak0bc00OfDVEO4oyXSyz8+aRaqKoBD1yo7A==", "license": "MIT", "dependencies": { "@shikijs/core": "4.3.0", "@shikijs/engine-javascript": "4.3.0", "@shikijs/engine-oniguruma": "4.3.0", "@shikijs/langs": "4.3.0", "@shikijs/themes": "4.3.0", "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" }, "engines": { "node": ">=20" } }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, "node_modules/sitemap": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", "license": "MIT", "dependencies": { "@types/node": "^24.9.2", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/esm/cli.js" }, "engines": { "node": ">=20.19.5", "npm": ">=10.8.2" } }, "node_modules/smol-toml": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" }, "funding": { "url": "https://github.com/sponsors/cyyynthia" } }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 12" } }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/stream-replace-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", "license": "MIT" }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "license": "MIT", "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", "license": "MIT", "dependencies": { "style-to-object": "1.0.14" } }, "node_modules/style-to-object": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", "license": "MIT", "dependencies": { "inline-style-parser": "0.2.7" } }, "node_modules/svgo": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", "license": "MIT", "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo.js" }, "engines": { "node": ">=16" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/svgo" } }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "license": "MIT" }, "node_modules/tinyclip": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.15.tgz", "integrity": "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==", "license": "MIT", "engines": { "node": "^16.14.0 || >= 17.3.0" } }, "node_modules/tinyexec": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" }, "funding": { "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD", "optional": true }, "node_modules/ufo": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "license": "MIT" }, "node_modules/ultrahtml": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", "license": "MIT" }, "node_modules/uncrypto": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/unifont": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", "license": "MIT", "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "node_modules/unist-util-find-after": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/unist-util-modify-children": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/unist-util-position": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/unist-util-position-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/unist-util-remove-position": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/unist-util-visit": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/unist-util-visit-children": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/unist-util-visit-parents": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/unstorage": { "version": "1.17.5", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", "license": "MIT", "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "peerDependenciesMeta": { "@azure/app-configuration": { "optional": true }, "@azure/cosmos": { "optional": true }, "@azure/data-tables": { "optional": true }, "@azure/identity": { "optional": true }, "@azure/keyvault-secrets": { "optional": true }, "@azure/storage-blob": { "optional": true }, "@capacitor/preferences": { "optional": true }, "@deno/kv": { "optional": true }, "@netlify/blobs": { "optional": true }, "@planetscale/database": { "optional": true }, "@upstash/redis": { "optional": true }, "@vercel/blob": { "optional": true }, "@vercel/functions": { "optional": true }, "@vercel/kv": { "optional": true }, "aws4fetch": { "optional": true }, "db0": { "optional": true }, "idb-keyval": { "optional": true }, "ioredis": { "optional": true }, "uploadthing": { "optional": true } } }, "node_modules/url-extras": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/url-extras/-/url-extras-0.1.0.tgz", "integrity": "sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw==", "license": "MIT", "engines": { "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/vfile-location": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/vfile-message": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, "node_modules/vite": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "~1.1.2", "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.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 }, "@vitejs/devtools": { "optional": true }, "esbuild": { "optional": true }, "jiti": { "optional": true }, "less": { "optional": true }, "sass": { "optional": true }, "sass-embedded": { "optional": true }, "stylus": { "optional": true }, "sugarss": { "optional": true }, "terser": { "optional": true }, "tsx": { "optional": true }, "yaml": { "optional": true } } }, "node_modules/vitefu": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", "license": "MIT", "workspaces": [ "tests/deps/*", "tests/projects/*", "tests/projects/workspace/packages/*" ], "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "vite": { "optional": true } } }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/which-pm-runs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", "license": "MIT" }, "node_modules/yargs-parser": { "version": "22.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "license": "ISC", "engines": { "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yocto-queue": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", "license": "MIT", "engines": { "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } } } } ludviglundgren-qbittorrent-cli-4dbbde7/docs/package.json000066400000000000000000000005151522034161300236540ustar00rootroot00000000000000{ "name": "qbittorrent-cli-docs", "type": "module", "version": "0.0.1", "private": true, "scripts": { "dev": "astro dev", "start": "astro dev", "build": "astro build", "preview": "astro preview", "astro": "astro" }, "dependencies": { "@astrojs/starlight": "^0.41.1", "astro": "^7.0.3" } } ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/000077500000000000000000000000001522034161300221545ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content.config.ts000066400000000000000000000004161522034161300254430ustar00rootroot00000000000000import { defineCollection } from 'astro:content'; import { docsLoader } from '@astrojs/starlight/loaders'; import { docsSchema } from '@astrojs/starlight/schema'; export const collections = { docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), }; ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/000077500000000000000000000000001522034161300236265ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/000077500000000000000000000000001522034161300245565ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/000077500000000000000000000000001522034161300263575ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt.md000066400000000000000000000015651522034161300274760ustar00rootroot00000000000000--- title: "qbt" description: "Manage qBittorrent with cli" editUrl: false --- Manage qBittorrent with cli ### Synopsis Manage qBittorrent from command line. Documentation is available at https://github.com/ludviglundgren/qbittorrent-cli ### Options ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -h, --help help for qbt -q, --quiet suppress output ``` ### SEE ALSO * [qbt app](../qbt_app/) - App subcommand * [qbt bencode](../qbt_bencode/) - Bencode subcommand * [qbt category](../qbt_category/) - Category subcommand * [qbt tag](../qbt_tag/) - Tag subcommand * [qbt torrent](../qbt_torrent/) - Torrent subcommand * [qbt transfer](../qbt_transfer/) - Transfer info subcommand * [qbt update](../qbt_update/) - Update qbittorrent-cli to latest version * [qbt version](../qbt_version/) - Print qbt version info ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_app.md000066400000000000000000000007411522034161300303310ustar00rootroot00000000000000--- title: "qbt app" description: "App subcommand" editUrl: false --- App subcommand ### Synopsis Do various app actions ### Options ``` -h, --help help for app ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt](../qbt/) - Manage qBittorrent with cli * [qbt app version](../qbt_app_version/) - Get qBittorrent version info ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_app_version.md000066400000000000000000000010011522034161300320640ustar00rootroot00000000000000--- title: "qbt app version" description: "Get qBittorrent version info" editUrl: false --- Get qBittorrent version info ``` qbt app version [flags] ``` ### Options ``` -h, --help help for version --output string Print as [formatted text (default), json] ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt app](../qbt_app/) - App subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_bencode.md000066400000000000000000000007571522034161300311570ustar00rootroot00000000000000--- title: "qbt bencode" description: "Bencode subcommand" editUrl: false --- Bencode subcommand ### Synopsis Do various bencode operations ### Options ``` -h, --help help for bencode ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt](../qbt/) - Manage qBittorrent with cli * [qbt bencode edit](../qbt_bencode_edit/) - edit bencode data ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_bencode_edit.md000066400000000000000000000017201522034161300321530ustar00rootroot00000000000000--- title: "qbt bencode edit" description: "edit bencode data" editUrl: false --- edit bencode data ### Synopsis Edit bencode files like .fastresume. Shut down client and make a backup of data before. ``` qbt bencode edit [flags] ``` ### Examples ``` qbt bencode edit --dir /home/user/.local/share/qBittorrent/BT_backup --pattern '/home/user01/torrents' --replace '/home/test/torrents' ``` ### Options ``` --dir string Dir with fast-resume files (required) --dry-run Dry run, don't write changes -h, --help help for edit --pattern string Pattern to change (required) --replace string Text to replace pattern with (required) -v, --verbose Verbose output ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt bencode](../qbt_bencode/) - Bencode subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_category.md000066400000000000000000000012561522034161300313700ustar00rootroot00000000000000--- title: "qbt category" description: "Category subcommand" editUrl: false --- Category subcommand ### Synopsis Do various category actions ### Options ``` -h, --help help for category ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt](../qbt/) - Manage qBittorrent with cli * [qbt category add](../qbt_category_add/) - Add category * [qbt category delete](../qbt_category_delete/) - Delete category * [qbt category edit](../qbt_category_edit/) - Edit category * [qbt category list](../qbt_category_list/) - List categories ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_category_add.md000066400000000000000000000013641522034161300322000ustar00rootroot00000000000000--- title: "qbt category add" description: "Add category" editUrl: false --- Add category ### Synopsis Add new category ``` qbt category add [flags] ``` ### Examples ``` qbt category add test-category qbt category add test-category --save-path "/home/user/torrents/test-category" ``` ### Options ``` --dry-run Run without doing anything -h, --help help for add --save-path string Category default save-path. Optional. Defaults to dir in default save dir. ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt category](../qbt_category/) - Category subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_category_delete.md000066400000000000000000000010741522034161300327100ustar00rootroot00000000000000--- title: "qbt category delete" description: "Delete category" editUrl: false --- Delete category ### Synopsis Delete category ``` qbt category delete [flags] ``` ### Examples ``` qbt category delete test-category ``` ### Options ``` --dry-run Run without doing anything -h, --help help for delete ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt category](../qbt_category/) - Category subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_category_edit.md000066400000000000000000000013061522034161300323710ustar00rootroot00000000000000--- title: "qbt category edit" description: "Edit category" editUrl: false --- Edit category ### Synopsis Edit category ``` qbt category edit [flags] ``` ### Examples ``` qbt category edit test-category --save-path "/home/user/new/path" qbt category edit test-category --save-path "" ``` ### Options ``` --dry-run Run without doing anything -h, --help help for edit --save-path string Edit category save-path ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt category](../qbt_category/) - Category subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_category_list.md000066400000000000000000000010261522034161300324160ustar00rootroot00000000000000--- title: "qbt category list" description: "List categories" editUrl: false --- List categories ### Synopsis List categories ``` qbt category list [flags] ``` ### Options ``` -h, --help help for list --output string Print as [formatted text (default), json] ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt category](../qbt_category/) - Category subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_tag.md000066400000000000000000000010531522034161300303210ustar00rootroot00000000000000--- title: "qbt tag" description: "Tag subcommand" editUrl: false --- Tag subcommand ### Synopsis Do various tag actions ### Options ``` -h, --help help for tag ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt](../qbt/) - Manage qBittorrent with cli * [qbt tag add](../qbt_tag_add/) - Add tags * [qbt tag delete](../qbt_tag_delete/) - Delete tags * [qbt tag list](../qbt_tag_list/) - List tags ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_tag_add.md000066400000000000000000000007641522034161300311410ustar00rootroot00000000000000--- title: "qbt tag add" description: "Add tags" editUrl: false --- Add tags ### Synopsis Add tags ``` qbt tag add [flags] ``` ### Examples ``` qbt tag add tag1 ``` ### Options ``` --dry-run Run without doing anything -h, --help help for add ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt tag](../qbt_tag/) - Tag subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_tag_delete.md000066400000000000000000000010121522034161300316360ustar00rootroot00000000000000--- title: "qbt tag delete" description: "Delete tags" editUrl: false --- Delete tags ### Synopsis Delete tags. ``` qbt tag delete [flags] ``` ### Examples ``` qbt tag delete tag1 ``` ### Options ``` --dry-run Run without doing anything -h, --help help for delete ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt tag](../qbt_tag/) - Tag subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_tag_list.md000066400000000000000000000007541522034161300313630ustar00rootroot00000000000000--- title: "qbt tag list" description: "List tags" editUrl: false --- List tags ### Synopsis List tags. ``` qbt tag list [flags] ``` ### Options ``` -h, --help help for list --output string Print as [formatted text (default), json] ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt tag](../qbt_tag/) - Tag subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent.md000066400000000000000000000030201522034161300312370ustar00rootroot00000000000000--- title: "qbt torrent" description: "Torrent subcommand" editUrl: false --- Torrent subcommand ### Synopsis Do various torrent operations ### Options ``` -h, --help help for torrent ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt](../qbt/) - Manage qBittorrent with cli * [qbt torrent add](../qbt_torrent_add/) - Add torrent(s) * [qbt torrent category](../qbt_torrent_category/) - Torrent category subcommand * [qbt torrent compare](../qbt_torrent_compare/) - Compare torrents * [qbt torrent export](../qbt_torrent_export/) - Export torrents * [qbt torrent hash](../qbt_torrent_hash/) - Print the hash of a torrent file or magnet * [qbt torrent import](../qbt_torrent_import/) - Import torrents * [qbt torrent list](../qbt_torrent_list/) - List torrents * [qbt torrent pause](../qbt_torrent_pause/) - Pause specified torrent(s) * [qbt torrent reannounce](../qbt_torrent_reannounce/) - Reannounce torrent(s) * [qbt torrent recheck](../qbt_torrent_recheck/) - Recheck specified torrent(s) * [qbt torrent remove](../qbt_torrent_remove/) - Removes specified torrent(s) * [qbt torrent resume](../qbt_torrent_resume/) - Resume specified torrent(s) * [qbt torrent share-limit](../qbt_torrent_share-limit/) - Torrent share limit subcommand * [qbt torrent tag](../qbt_torrent_tag/) - Torrent tag subcommand * [qbt torrent tracker](../qbt_torrent_tracker/) - Torrent tracker subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_add.md000066400000000000000000000037051522034161300320610ustar00rootroot00000000000000--- title: "qbt torrent add" description: "Add torrent(s)" editUrl: false --- Add torrent(s) ### Synopsis Add new torrent(s) to qBittorrent from file or magnet. Supports glob pattern for files like: ./files/*.torrent ``` qbt torrent add [flags] ``` ### Examples ``` qbt torrent add my-file.torrent --category test --tags tag1 qbt torrent add ./files/*.torrent --paused --skip-hash-check qbt torrent add magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcad53426&dn=download ``` ### Options ``` --category string Add torrent to the specified category --dry-run Run without doing anything --first-last-piece Prioritize first and last pieces for preview -h, --help help for add --ignore-rules Ignore rules from config --limit-dl uint Set torrent download speed limit. Unit in bytes/second --limit-ul uint Set torrent upload speed limit. Unit in bytes/second --paused Add torrent in paused state --recheck Force recheck after adding (useful when using --paused) --remove-stalled Remove stalled torrents from re-announce --save-path string Add torrent to the specified path --sequential Download torrent pieces in sequential order --skip-hash-check Skip hash check --sleep duration Set the amount of time to wait between adding torrents in seconds (default 200ms) --stop-condition string Add torrent with the specified stop condition. Possible values: None, MetadataReceived, FilesChecked. Example: --stop-condition MetadataReceived --tags stringArray Add tags to torrent ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_category.md000066400000000000000000000013501522034161300331400ustar00rootroot00000000000000--- title: "qbt torrent category" description: "Torrent category subcommand" editUrl: false --- Torrent category subcommand ### Synopsis Do various torrent category operations ### Options ``` -h, --help help for category ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand * [qbt torrent category move](../qbt_torrent_category_move/) - move torrents between categories * [qbt torrent category set](../qbt_torrent_category_set/) - Set torrent category * [qbt torrent category unset](../qbt_torrent_category_unset/) - Unset torrent category ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_category_move.md000066400000000000000000000020341522034161300341660ustar00rootroot00000000000000--- title: "qbt torrent category move" description: "move torrents between categories" editUrl: false --- move torrents between categories ### Synopsis Move torrents from one category to another ``` qbt torrent category move [flags] ``` ### Examples ``` qbt torrent category move --from cat1 --to cat2 ``` ### Options ``` --dry-run Run without doing anything --exclude-tags strings Exclude torrents with provided tags --from strings Move from categories (required) -h, --help help for move --include-tags strings Include torrents with provided tags --min-seed-time int Minimum seed time in MINUTES before moving. --to string Move to the specified category (required) ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent category](../qbt_torrent_category/) - Torrent category subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_category_set.md000066400000000000000000000013431522034161300340150ustar00rootroot00000000000000--- title: "qbt torrent category set" description: "Set torrent category" editUrl: false --- Set torrent category ### Synopsis Set category for torrents via hashes ``` qbt torrent category set [flags] ``` ### Examples ``` qbt torrent category set test-category --hashes hash1,hash2 ``` ### Options ``` --dry-run Run without doing anything --hashes strings Torrent hashes, as comma separated list -h, --help help for set ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent category](../qbt_torrent_category/) - Torrent category subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_category_unset.md000066400000000000000000000013431522034161300343600ustar00rootroot00000000000000--- title: "qbt torrent category unset" description: "Unset torrent category" editUrl: false --- Unset torrent category ### Synopsis Unset category for torrents via hashes ``` qbt torrent category unset [flags] ``` ### Examples ``` qbt torrent category unset --hashes hash1,hash2 ``` ### Options ``` --dry-run Run without doing anything --hashes strings Torrent hashes, as comma separated list -h, --help help for unset ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent category](../qbt_torrent_category/) - Torrent category subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_compare.md000066400000000000000000000030071522034161300327520ustar00rootroot00000000000000--- title: "qbt torrent compare" description: "Compare torrents" editUrl: false --- Compare torrents ### Synopsis Compare torrents between clients ``` qbt torrent compare [flags] ``` ### Examples ``` qbt torrent compare --addr http://localhost:10000 --user u --pass p --compare-addr http://url.com:10000 --compare-user u --compare-pass p ``` ### Options ``` --api-key string Source api key --basic-pass string Source basic auth pass --basic-user string Source basic auth user --compare-api-key string Secondary api key --compare-basic-pass string Secondary basic auth pass --compare-basic-user string Secondary basic auth user --compare-host string Secondary host --compare-pass string Secondary pass --compare-user string Secondary user --dry-run dry run -h, --help help for compare --host string Source host --pass string Source pass --tag string set a custom tag for duplicates on compare. default: compare-dupe (default "compare-dupe") --tag-duplicates tag duplicates on compare --user string Source user ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_export.md000066400000000000000000000025101522034161300326430ustar00rootroot00000000000000--- title: "qbt torrent export" description: "Export torrents" editUrl: false --- Export torrents ### Synopsis Export torrents and fastresume by category ``` qbt torrent export [flags] ``` ### Examples ``` qbt torrent export --source ~/.local/share/data/qBittorrent/BT_backup --export-dir ~/qbt-backup --include-category=movies,tv ``` ### Options ``` -a, --archive archive export dir to .tar.gz --dry-run dry run --exclude-category strings Exclude categories. Comma separated --exclude-tag strings Exclude tags. Comma separated --export-dir string Dir to export files to (required) -h, --help help for export --include-category strings Export torrents from these categories. Comma separated --include-tag strings Include tags. Comma separated --skip-manifest Do not export all used tags and categories into manifest --source string Dir with torrent and fast-resume files (required) -v, --verbose verbose output ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_hash.md000066400000000000000000000010241522034161300322440ustar00rootroot00000000000000--- title: "qbt torrent hash" description: "Print the hash of a torrent file or magnet" editUrl: false --- Print the hash of a torrent file or magnet ``` qbt torrent hash [flags] ``` ### Examples ``` qbt torrent hash file.torrent ``` ### Options ``` -h, --help help for hash ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_import.md000066400000000000000000000022101522034161300326310ustar00rootroot00000000000000--- title: "qbt torrent import" description: "Import torrents" editUrl: false --- Import torrents ### Synopsis Import torrents with state from other clients [rtorrent, deluge] ``` qbt torrent import {rtorrent | deluge} --source-dir dir --qbit-dir dir2 [--skip-backup] [--dry-run] [flags] ``` ### Examples ``` qbt torrent import deluge --source-dir ~/.config/deluge/state/ --qbit-dir ~/.local/share/data/qBittorrent/BT_backup --dry-run qbt torrent import rtorrent --source-dir ~/.sessions --qbit-dir ~/.local/share/data/qBittorrent/BT_backup --dry-run ``` ### Options ``` --dry-run Run without importing anything -h, --help help for import --qbit-dir string qBittorrent BT_backup dir. Commonly ~/.local/share/qBittorrent/BT_backup (required) --skip-backup Skip backup before import --source-dir string source client state dir (required) ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_list.md000066400000000000000000000022011522034161300322720ustar00rootroot00000000000000--- title: "qbt torrent list" description: "List torrents" editUrl: false --- List torrents ### Synopsis List all torrents, or torrents with a specific filters. Get by filter, category, tag and hashes. Can be combined ``` qbt torrent list [flags] ``` ### Examples ``` qbt torrent list --filter=downloading --category=linux-iso ``` ### Options ``` -c, --category string Filter by category. All categories by default. -f, --filter string Filter by state. Available filters: all, downloading, seeding, completed, paused, active, inactive, resumed, stalled, stalled_uploading, stalled_downloading, errored (default "all") --hashes strings Filter by hashes. Separated by comma: "hash1,hash2". -h, --help help for list --output string Print as [formatted text (default), json] -t, --tag string Filter by tag. Single tag: tag1 ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_pause.md000066400000000000000000000014301522034161300324370ustar00rootroot00000000000000--- title: "qbt torrent pause" description: "Pause specified torrent(s)" editUrl: false --- Pause specified torrent(s) ### Synopsis Pause the torrent(s) indicated by the supplied hash(es), or pause every torrent with --all. ``` qbt torrent pause [flags] ``` ### Examples ``` qbt torrent pause --all qbt torrent pause HASH1 HASH2 qbt torrent pause --hashes HASH1,HASH2 ``` ### Options ``` --all Pauses all torrents --hashes strings Add hashes as comma separated list -h, --help help for pause ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_reannounce.md000066400000000000000000000016151522034161300334640ustar00rootroot00000000000000--- title: "qbt torrent reannounce" description: "Reannounce torrent(s)" editUrl: false --- Reannounce torrent(s) ### Synopsis Reannounce torrents with non-OK tracker status. ``` qbt torrent reannounce [flags] ``` ### Options ``` --attempts int Reannounce torrents X times (default 50) --category string Reannounce torrents with category --dry-run Run without doing anything --hash string Reannounce torrent with hash -h, --help help for reannounce --interval int Reannounce torrents X times with interval Y. In MS (default 7000) --tag string Reannounce torrents with tag ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_recheck.md000066400000000000000000000012571522034161300327350ustar00rootroot00000000000000--- title: "qbt torrent recheck" description: "Recheck specified torrent(s)" editUrl: false --- Recheck specified torrent(s) ### Synopsis Rechecks torrents indicated by hash(es). ``` qbt torrent recheck [flags] ``` ### Examples ``` qbt torrent recheck --hashes HASH qbt torrent recheck --hashes HASH1,HASH2 ``` ### Options ``` --hashes strings Add hashes as comma separated list -h, --help help for recheck ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_remove.md000066400000000000000000000023461522034161300326260ustar00rootroot00000000000000--- title: "qbt torrent remove" description: "Removes specified torrent(s)" editUrl: false --- Removes specified torrent(s) ### Synopsis Removes torrents indicated by hash, name or a prefix of either. Whitespace indicates next prefix unless argument is surrounded by quotes ``` qbt torrent remove [flags] ``` ### Options ``` --all Removes all torrents --delete-files Also delete downloaded files from torrent(s) --dry-run Display what would be done without actually doing it --exclude-tags strings Exclude torrents with provided tags -f, --filter string Filter by state: all, active, paused, completed, stalled, errored --hashes strings Add hashes as comma separated list -h, --help help for remove -c, --include-category strings Remove torrents from these categories. Comma separated --include-tags strings Include torrents with provided tags ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_resume.md000066400000000000000000000014431522034161300326260ustar00rootroot00000000000000--- title: "qbt torrent resume" description: "Resume specified torrent(s)" editUrl: false --- Resume specified torrent(s) ### Synopsis Resume the torrent(s) indicated by the supplied hash(es), or resume every torrent with --all. ``` qbt torrent resume [flags] ``` ### Examples ``` qbt torrent resume --all qbt torrent resume HASH1 HASH2 qbt torrent resume --hashes HASH1,HASH2 ``` ### Options ``` --all resumes all torrents --hashes strings Add hashes as comma separated list -h, --help help for resume ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_share-limit.md000066400000000000000000000011071522034161300335410ustar00rootroot00000000000000--- title: "qbt torrent share-limit" description: "Torrent share limit subcommand" editUrl: false --- Torrent share limit subcommand ### Synopsis Do various torrent share limit operations ### Options ``` -h, --help help for share-limit ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand * [qbt torrent share-limit set](../qbt_torrent_share-limit_set/) - Set torrent share limits ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_share-limit_set.md000066400000000000000000000042771522034161300344270ustar00rootroot00000000000000--- title: "qbt torrent share-limit set" description: "Set torrent share limits" editUrl: false --- Set torrent share limits ### Synopsis Set share limits (ratio, seeding time, inactive seeding time) for torrents. Limit values use qBittorrent's special semantics: -2 use the global limit (default) -1 no limit (unlimited) >=0 a specific value (ratio, or minutes for the seeding time limits) qBittorrent applies all three limits in a single request, so any limit you do not set is reset to the global limit (-2). The applied values are printed when the command runs. Note: on qBittorrent 5.x (Web API 2.12+) this also resets each torrent's share limit action and mode to their defaults. ``` qbt torrent share-limit set [flags] ``` ### Examples ``` qbt torrent share-limit set --hashes hash1,hash2 --ratio 2.0 qbt torrent share-limit set --all --seeding-time 1440 qbt torrent share-limit set --include-category movies --ratio 1.5 --seeding-time 10080 qbt torrent share-limit set --hashes hash1 --ratio -1 ``` ### Options ``` --all Set share limits for all torrents --dry-run Run without doing anything --exclude-tags strings Exclude torrents with any of these tags. Comma separated --hashes strings Torrent hashes, as comma separated list -h, --help help for set --inactive-seeding-time int Inactive seeding time limit in MINUTES. -2 = global, -1 = unlimited, >=0 = minutes (default -2) -c, --include-category strings Set share limits for torrents in these categories. Comma separated --include-tags strings Include torrents with any of these tags. Comma separated --ratio float Ratio limit. -2 = global, -1 = unlimited, >=0 = ratio (default -2) --seeding-time int Seeding time limit in MINUTES. -2 = global, -1 = unlimited, >=0 = minutes (default -2) ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent share-limit](../qbt_torrent_share-limit/) - Torrent share limit subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_tag.md000066400000000000000000000010251522034161300320750ustar00rootroot00000000000000--- title: "qbt torrent tag" description: "Torrent tag subcommand" editUrl: false --- Torrent tag subcommand ### Synopsis Do various torrent tag operations ### Options ``` -h, --help help for tag ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand * [qbt torrent tag issues](../qbt_torrent_tag_issues/) - tag torrents with issues ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_tag_issues.md000066400000000000000000000014011522034161300334660ustar00rootroot00000000000000--- title: "qbt torrent tag issues" description: "tag torrents with issues" editUrl: false --- tag torrents with issues ### Synopsis Tag torrents that may have broken trackers or be unregistered ``` qbt torrent tag issues [flags] ``` ### Examples ``` qbt torrent tag issues --unregistered --not-working ``` ### Options ``` --dry-run Dry run, do not tag torrents -h, --help help for issues --not-working tag not working torrents --unregistered tag unregistered ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent tag](../qbt_torrent_tag/) - Torrent tag subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_tracker.md000066400000000000000000000010521522034161300327550ustar00rootroot00000000000000--- title: "qbt torrent tracker" description: "Torrent tracker subcommand" editUrl: false --- Torrent tracker subcommand ### Synopsis Do various torrent category operations ### Options ``` -h, --help help for tracker ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent](../qbt_torrent/) - Torrent subcommand * [qbt torrent tracker edit](../qbt_torrent_tracker_edit/) - Edit torrent tracker ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_torrent_tracker_edit.md000066400000000000000000000013601522034161300337640ustar00rootroot00000000000000--- title: "qbt torrent tracker edit" description: "Edit torrent tracker" editUrl: false --- Edit torrent tracker ### Synopsis Edit tracker for torrents via hashes ``` qbt torrent tracker edit [flags] ``` ### Examples ``` qbt torrent tracker edit --old url.old/test --new url.com/test ``` ### Options ``` --dry-run Run without doing anything -h, --help help for edit --new string New tracker URL --old string Old tracker URL to replace ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt torrent tracker](../qbt_torrent_tracker/) - Torrent tracker subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_transfer.md000066400000000000000000000010071522034161300313710ustar00rootroot00000000000000--- title: "qbt transfer" description: "Transfer info subcommand" editUrl: false --- Transfer info subcommand ### Synopsis Get status and speed info ### Options ``` -h, --help help for transfer ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt](../qbt/) - Manage qBittorrent with cli * [qbt transfer info](../qbt_transfer_info/) - Get qBittorrent transfer info ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_transfer_info.md000066400000000000000000000010301522034161300324000ustar00rootroot00000000000000--- title: "qbt transfer info" description: "Get qBittorrent transfer info" editUrl: false --- Get qBittorrent transfer info ``` qbt transfer info [flags] ``` ### Options ``` -h, --help help for info --output string Print as [formatted text (default), json] ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt transfer](../qbt_transfer/) - Transfer info subcommand ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_update.md000066400000000000000000000010411522034161300310250ustar00rootroot00000000000000--- title: "qbt update" description: "Update qbittorrent-cli to latest version" editUrl: false --- Update qbittorrent-cli to latest version ``` qbt update [flags] ``` ### Examples ``` qbt update ``` ### Options ``` -h, --help help for update --verbose Verbose output: Print changelog ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt](../qbt/) - Manage qBittorrent with cli ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/commands/qbt_version.md000066400000000000000000000010601522034161300312310ustar00rootroot00000000000000--- title: "qbt version" description: "Print qbt version info" editUrl: false --- Print qbt version info ``` qbt version [flags] ``` ### Examples ``` qbt version qbt version --output json ``` ### Options ``` -h, --help help for version --output string Print as [text, json] (default "text") ``` ### Options inherited from parent commands ``` --config string config file (default is $HOME/.config/qbt/.qbt.toml) -q, --quiet suppress output ``` ### SEE ALSO * [qbt](../qbt/) - Manage qBittorrent with cli ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/getting-started/000077500000000000000000000000001522034161300276635ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/getting-started/configuration.md000066400000000000000000000077561522034161300330730ustar00rootroot00000000000000--- title: Configuration description: Configure qbittorrent-cli with a .qbt.toml file - connection, reannounce, rules, add defaults and compare instances. --- `qbt` is configured with a TOML file. Most commands need at least the `[qbittorrent]` connection block. ## Config file location Create `.qbt.toml` in `~/.config/qbt/`: ```shell mkdir -p ~/.config/qbt && touch ~/.config/qbt/.qbt.toml ``` `qbt` looks for a file named `.qbt.toml` in the following locations, in order: 1. the current working directory 2. your home directory (`$HOME`) 3. `$HOME/.config/qbt` You can also point at a specific file with the global `--config` flag: ```shell qbt --config /path/to/.qbt.toml torrent list ``` ## Minimal configuration ```toml [qbittorrent] addr = "http://127.0.0.1:6776" # qbittorrent webui-api hostname/ip login = "user" # qbittorrent webui-api user (optional) password = "password" # qbittorrent webui-api password (optional) ``` ## Connection - `[qbittorrent]` | Key | Description | | ----------- | ------------------------------------------------------------------ | | `addr` | qBittorrent Web API URL, including scheme and port | | `login` | Web API username (optional) | | `password` | Web API password (optional) | | `apikey` | Web API key - qBittorrent 5.2.x and newer only (optional) | | `basicUser` | HTTP basic auth username, if your Web API is behind basic auth | | `basicPass` | HTTP basic auth password, if your Web API is behind basic auth | ## Reannounce - `[reannounce]` Some trackers are buggy and need a reannounce before a torrent can start. These values are used by [`qbt torrent reannounce`](/qbittorrent-cli/commands/qbt_torrent_reannounce/). ```toml [reannounce] enabled = true # true or false attempts = 10 # number of attempts, typically 10-30 interval = 7000 # interval between attempts, in milliseconds ``` ## Rules - `[rules]` Limit how many torrents download simultaneously. Applied when adding torrents unless you pass `--ignore-rules`. ```toml [rules] enabled = true # enable or disable rules max_active_downloads = 2 # max active downloads ``` * On HDDs with 1 Gbit, `max_active_downloads = 2` is a good value to avoid overloading the disks while giving each torrent as much bandwidth as possible. * On SSDs and 1 Gbit+ you can increase this value. ## Add defaults - `[add]` Defaults applied by [`qbt torrent add`](/qbittorrent-cli/commands/qbt_torrent_add/). ```toml [add] sequential = false # download pieces in sequential order (useful for streaming) first_last_piece = false # prioritize first and last pieces for all new torrents ``` ## Compare instances - `[[compare]]` [`qbt torrent compare`](/qbittorrent-cli/commands/qbt_torrent_compare/) can compare torrents against other qBittorrent instances. Add one `[[compare]]` block per instance. ```toml [[compare]] addr = "http://100.100.100.100:6776" login = "user" password = "password" #basicUser = "user" #basicPass = "password" [[compare]] # you can specify multiple compare blocks addr = "http://100.100.100.101:6776" login = "user" password = "password" ``` ## autodl-irssi `qbt torrent add` works well as an autodl-irssi upload action. Edit `autodl.cfg`. Global action: ```ini [options] ... upload-type = exec upload-command = /usr/bin/qbt upload-args = add "$(TorrentPathName)" ``` Per filter: ```ini [filter example_filter_action] ... upload-type = exec upload-command = /usr/bin/qbt upload-args = add "$(TorrentPathName)" --category cat1 --tags tag1 ``` ### ruTorrent In ruTorrent, go to autodl-irssi `Preferences`, then the `Action` tab and set the following for the global action. This can be set per filter as well, where you can also add a category, tags and so on. ```text Choose .torrent action: Run Program Command: /usr/bin/qbt Arguments: add "$(TorrentPathName)" ``` ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/getting-started/installation.md000066400000000000000000000025541522034161300327140ustar00rootroot00000000000000--- title: Installation description: Install qbittorrent-cli from a release binary, with go install, or build it from source. --- `qbt` ships as a single static binary for Linux, macOS and Windows. ## Download a release binary Download the [latest release](https://github.com/ludviglundgren/qbittorrent-cli/releases/latest) for your platform and put it somewhere in your `$PATH`. Extract the archive: ```shell tar -xzvf qbittorrent-cli_${VERSION}_linux_amd64.tar.gz ``` Move it somewhere in `$PATH` (needs `sudo` if you are not root), or into your user `$HOME/bin`: ```shell sudo mv qbt /usr/bin/ ``` Verify that it runs - this should print the basic usage: ```shell qbt ``` ## Install with Go If you have Go installed you can install the latest release directly: ```shell go install github.com/ludviglundgren/qbittorrent-cli/cmd/qbt@latest ``` ## Build from source Clone the repository and build with `make`: ```shell make build ``` Or with the Go toolchain directly: ```shell go build -o bin/qbt cmd/qbt/main.go ``` ### Multi-platform builds with goreleaser Builds made with [goreleaser](https://goreleaser.com/) also embed version info: ```shell goreleaser release --snapshot --skip=publish --clean ``` ## Next steps Once installed, head to the [Configuration](/qbittorrent-cli/getting-started/configuration/) guide to connect `qbt` to your qBittorrent instance. ludviglundgren-qbittorrent-cli-4dbbde7/docs/src/content/docs/index.mdx000066400000000000000000000043521522034161300264030ustar00rootroot00000000000000--- title: qbittorrent-cli description: A CLI to manage qBittorrent - add torrents, manage categories and tags, reannounce, import from other clients and more. template: splash hero: tagline: Manage qBittorrent from the command line. actions: - text: Get started link: /qbittorrent-cli/getting-started/installation/ icon: right-arrow variant: primary - text: View on GitHub link: https://github.com/ludviglundgren/qbittorrent-cli icon: external variant: minimal --- import { Card, CardGrid } from '@astrojs/starlight/components'; `qbt` is a small, fast command-line tool to manage qBittorrent over its Web API. It is handy for automation (autodl-irssi, cron jobs), bulk operations and migrating from other clients. Add from file, glob pattern, URL or magnet - with categories, tags, save paths and rate limits. [`qbt torrent add`](/qbittorrent-cli/commands/qbt_torrent_add/) Reannounce torrents with non-OK tracker status to get past troublesome trackers. [`qbt torrent reannounce`](/qbittorrent-cli/commands/qbt_torrent_reannounce/) Bring torrents and their state over from Deluge and rTorrent. [`qbt torrent import`](/qbittorrent-cli/commands/qbt_torrent_import/) Create, edit and manage categories and tags, and move torrents between them. [`qbt category`](/qbittorrent-cli/commands/qbt_category/) · [`qbt tag`](/qbittorrent-cli/commands/qbt_tag/) ## Quick start ```shell # Install (see the Installation guide for all options) go install github.com/ludviglundgren/qbittorrent-cli/cmd/qbt@latest # Point it at your qBittorrent Web API mkdir -p ~/.config/qbt && $EDITOR ~/.config/qbt/.qbt.toml # Add a torrent qbt torrent add my-file.torrent --category linux-iso --tags iso ``` See the [Installation](/qbittorrent-cli/getting-started/installation/) and [Configuration](/qbittorrent-cli/getting-started/configuration/) guides to get set up, then browse the full [command reference](/qbittorrent-cli/commands/qbt/). ludviglundgren-qbittorrent-cli-4dbbde7/docs/tsconfig.json000066400000000000000000000001551522034161300240750ustar00rootroot00000000000000{ "extends": "astro/tsconfigs/strict", "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist"] } ludviglundgren-qbittorrent-cli-4dbbde7/go.mod000066400000000000000000000064271522034161300215540ustar00rootroot00000000000000module github.com/ludviglundgren/qbittorrent-cli go 1.25.0 require ( github.com/anacrolix/torrent v1.61.0 github.com/autobrr/go-qbittorrent v1.16.0 github.com/blang/semver v3.5.1+incompatible github.com/dustin/go-humanize v1.0.1 github.com/magiconair/properties v1.8.10 github.com/mholt/archives v0.1.5 github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/errors v0.9.1 github.com/rhysd/go-github-selfupdate v1.2.3 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/zeebo/bencode v1.0.0 golang.org/x/net v0.55.0 ) require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/STARRY-S/zip v0.2.3 // indirect github.com/anacrolix/generics v0.1.1-0.20251125230353-15d98d46693b // indirect github.com/anacrolix/missinggo v1.3.0 // indirect github.com/anacrolix/missinggo/v2 v2.10.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/avast/retry-go v3.0.0+incompatible // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/google/go-github/v30 v30.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/minlz v1.0.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/onsi/gomega v1.37.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tcnksm/go-gitconfig v0.1.2 // indirect github.com/ulikunitz/xz v0.5.15 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/crypto v0.51.0 // indirect golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect lukechampine.com/blake3 v1.4.0 // indirect ) ludviglundgren-qbittorrent-cli-4dbbde7/go.sum000066400000000000000000001571301522034161300215770ustar00rootroot00000000000000cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anacrolix/dht/v2 v2.23.0 h1:EuD17ykTTEkAMPLjBsS5QjGOwuBgLTdQhds6zPAjeVY= github.com/anacrolix/dht/v2 v2.23.0/go.mod h1:seXRz6HLw8zEnxlysf9ye2eQbrKUmch6PyOHpe/Nb/U= github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= github.com/anacrolix/generics v0.1.1-0.20251125230353-15d98d46693b h1:Kuvx/A/TTJuT9x8mn7DeGx2KW9tWn1LI8bira67xdT0= github.com/anacrolix/generics v0.1.1-0.20251125230353-15d98d46693b/go.mod h1:NGehhfeXJPBujPx0s6cstSj8B+TERsTY32Xckfx5ftc= github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y= github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw= github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc= github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ= github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY= github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA= github.com/anacrolix/missinggo/v2 v2.10.0 h1:pg0iO4Z/UhP2MAnmGcaMtp5ZP9kyWsusENWN9aolrkY= github.com/anacrolix/missinggo/v2 v2.10.0/go.mod h1:nCRMW6bRCMOVcw5z9BnSYKF+kDbtenx+hQuphf4bK8Y= github.com/anacrolix/multiless v0.4.0 h1:lqSszHkliMsZd2hsyrDvHOw4AbYWa+ijQ66LzbjqWjM= github.com/anacrolix/multiless v0.4.0/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM= github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= github.com/anacrolix/torrent v1.61.0 h1:vxo+B4SwnoP5AQWbhvnTYIaTgPSX+llYUVuQVsN4Jg8= github.com/anacrolix/torrent v1.61.0/go.mod h1:yKUKuZSSDdyOsCbuH+rDOpswl/g546gICapdrU7aUmQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/autobrr/go-qbittorrent v1.16.0 h1:H0zwzLOaCxvJTxJ3xe4+hV3rFSuxucsgKQxsGHydXCU= github.com/autobrr/go-qbittorrent v1.16.0/go.mod h1:s36a75VlatWPpHI+pNg4Ta6eB3fRxQvTZPfgMtOsQfY= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo= github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ= github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag= github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw= github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/bencode v1.0.0 h1:zgop0Wu1nu4IexAZeCZ5qbsjU4O1vMrfCrVgUjbHVuA= github.com/zeebo/bencode v1.0.0/go.mod h1:Ct7CkrWIQuLWAy9M3atFHYq4kG9Ao/SsY5cdtCXmp9Y= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w= lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ludviglundgren-qbittorrent-cli-4dbbde7/internal/000077500000000000000000000000001522034161300222515ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/internal/config/000077500000000000000000000000001522034161300235165ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/internal/config/config.go000066400000000000000000000027001522034161300253110ustar00rootroot00000000000000package config import ( "errors" "log" "os" "path/filepath" "github.com/ludviglundgren/qbittorrent-cli/internal/domain" "github.com/mitchellh/go-homedir" "github.com/spf13/viper" ) var ( CfgFile string Config domain.AppConfig Qbit domain.QbitConfig Compare []domain.QbitConfig Reannounce domain.ReannounceSettings Rules domain.Rules Add domain.AddConfig ) // InitConfig initialize config func InitConfig() { if CfgFile != "" { // Use config file from the flag. viper.SetConfigFile(CfgFile) } else { // Find home directory. home, err := homedir.Dir() if err != nil { log.Fatal("could not read home dir: %w", err) } viper.SetConfigName(".qbt") // Search config in directories // call multiple times to add many search paths viper.AddConfigPath(".") // optionally look for config in the working directory viper.AddConfigPath(home) viper.AddConfigPath(filepath.Join(home, ".config", "qbt")) // windows path viper.AddConfigPath("$HOME/.config/qbt") } if err := viper.ReadInConfig(); err != nil { var ferr *viper.ConfigFileNotFoundError if errors.As(err, &ferr) { log.Printf("config file not found: err %q\n", ferr) } else { log.Printf("could not read config: err %q\n", err) } os.Exit(1) } if err := viper.Unmarshal(&Config); err != nil { os.Exit(1) } Qbit = Config.Qbit Compare = Config.Compare Reannounce = Config.Reannounce Rules = Config.Rules Add = Config.Add } ludviglundgren-qbittorrent-cli-4dbbde7/internal/domain/000077500000000000000000000000001522034161300235205ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/internal/domain/config.go000066400000000000000000000022171522034161300253160ustar00rootroot00000000000000package domain type QbitConfig struct { Addr string `mapstructure:"addr"` Host string `mapstructure:"host"` Port uint `mapstructure:"port"` Login string `mapstructure:"login"` Password string `mapstructure:"password"` APIKey string `mapstructure:"apikey"` BasicUser string `mapstructure:"basicUser"` BasicPass string `mapstructure:"basicPass"` } type ReannounceSettings struct { Enabled bool `mapstructure:"enabled"` Attempts int `mapstructure:"attempts"` Interval int `mapstructure:"interval"` } type Rules struct { Enabled bool `mapstructure:"enabled"` MaxActiveDownloads int `mapstructure:"max_active_downloads"` } type AddConfig struct { Sequential bool `mapstructure:"sequential"` FirstLastPiece bool `mapstructure:"first_last_piece"` } type AppConfig struct { Debug bool `mapstructure:"debug"` Qbit QbitConfig `mapstructure:"qbittorrent"` Reannounce ReannounceSettings `mapstructure:"reannounce"` Rules Rules `mapstructure:"rules"` Add AddConfig `mapstructure:"add"` Compare []QbitConfig `mapstructure:"compare"` } ludviglundgren-qbittorrent-cli-4dbbde7/internal/fs/000077500000000000000000000000001522034161300226615ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/internal/fs/copy.go000066400000000000000000000037271522034161300241730ustar00rootroot00000000000000package fs import ( "fmt" "io" "io/ioutil" "os" "path/filepath" ) // CopyFile copies the contents of the file named src to the file named // by dst. The file will be created if it does not already exist. If the // destination file exists, all it's contents will be replaced by the contents // of the source file. The file mode will be copied from the source and // the copied data is synced/flushed to stable storage. func CopyFile(src, dst string) (err error) { in, err := os.Open(src) if err != nil { return } defer in.Close() out, err := os.Create(dst) if err != nil { return } defer func() { if e := out.Close(); e != nil { err = e } }() _, err = io.Copy(out, in) if err != nil { return } err = out.Sync() if err != nil { return } si, err := os.Stat(src) if err != nil { return } err = os.Chmod(dst, si.Mode()) if err != nil { return } return } // CopyDir recursively copies a directory tree, attempting to preserve permissions. // Source directory must exist, destination directory must *not* exist. // Symlinks are ignored and skipped. func CopyDir(src string, dst string) (err error) { src = filepath.Clean(src) dst = filepath.Clean(dst) si, err := os.Stat(src) if err != nil { return err } if !si.IsDir() { return fmt.Errorf("source is not a directory") } _, err = os.Stat(dst) if err != nil && !os.IsNotExist(err) { return } if err == nil { return fmt.Errorf("destination already exists") } err = os.MkdirAll(dst, si.Mode()) if err != nil { return } entries, err := ioutil.ReadDir(src) if err != nil { return } for _, entry := range entries { srcPath := filepath.Join(src, entry.Name()) dstPath := filepath.Join(dst, entry.Name()) if entry.IsDir() { err = CopyDir(srcPath, dstPath) if err != nil { return } } else { // Skip symlinks. if entry.Mode()&os.ModeSymlink != 0 { continue } err = CopyFile(srcPath, dstPath) if err != nil { return } } } return } ludviglundgren-qbittorrent-cli-4dbbde7/internal/fs/dir.go000066400000000000000000000005211522034161300237640ustar00rootroot00000000000000package fs import "os" // MkDirIfNotExists check if export dir exists, if not then lets create it func MkDirIfNotExists(dir string) error { if _, err := os.Stat(dir); err != nil { if os.IsNotExist(err) { if err := os.MkdirAll(dir, os.ModePerm); err != nil { return err } return nil } return err } return nil } ludviglundgren-qbittorrent-cli-4dbbde7/internal/importer/000077500000000000000000000000001522034161300241125ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/internal/importer/deluge.go000066400000000000000000000122651522034161300257140ustar00rootroot00000000000000package importer import ( "log" "os" "path/filepath" "github.com/ludviglundgren/qbittorrent-cli/internal/fs" "github.com/ludviglundgren/qbittorrent-cli/pkg/qbittorrent" "github.com/anacrolix/torrent/metainfo" "github.com/pkg/errors" "github.com/zeebo/bencode" ) type Options struct { SourceDir string QbitDir string DryRun bool } type Importer interface { Import(opts Options) error } type DelugeImport struct{} func NewDelugeImporter() Importer { return &DelugeImport{} } func (di *DelugeImport) Import(opts Options) error { sourceDir := opts.SourceDir sourceDirInfo, err := os.Stat(sourceDir) if err != nil { if os.IsNotExist(err) { return errors.Errorf("source directory does not exist: %s", sourceDir) } return errors.Wrapf(err, "source directory error: %s\n", sourceDir) } if !sourceDirInfo.IsDir() { return errors.Errorf("source is a file, not a directory: %s\n", sourceDir) } if err := fs.MkDirIfNotExists(opts.QbitDir); err != nil { return errors.Wrapf(err, "qbit directory error: %s\n", opts.QbitDir) } resumeFilePath := filepath.Join(sourceDir, "torrents.fastresume") if _, err := os.Stat(resumeFilePath); os.IsNotExist(err) { log.Printf("Could not find deluge fastresume file: %s\n", resumeFilePath) return err } fastresumeFile, err := decodeFastresumeFile(resumeFilePath) if err != nil { log.Printf("Could not decode deluge fastresume file: %s\n", resumeFilePath) return err } matches, err := filepath.Glob(filepath.Join(sourceDir, "*.torrent")) if err != nil { return errors.Wrapf(err, "glob error: %s", matches) } totalJobs := len(matches) log.Printf("Total torrents to process: %d\n", totalJobs) positionNum := 0 for torrentID, value := range fastresumeFile { torrentNamePath := filepath.Join(sourceDir, torrentID+".torrent") torrentNamePathBak := filepath.Join(sourceDir, torrentID+".torrent.bak") // If a file exist in fastresume data but no .torrent file, skip if _, err = os.Stat(torrentNamePath); os.IsNotExist(err) { log.Printf("%s: skipping because %s not found in source directory\n", torrentID, torrentNamePath) continue } positionNum++ torrentOutFile := filepath.Join(opts.QbitDir, torrentID+".torrent") // If file already exists, skip if _, err = os.Stat(torrentOutFile); err == nil { log.Printf("(%d/%d) %s Torrent already exists, skipping\n", positionNum, totalJobs, torrentID) continue } var fastResume qbittorrent.Fastresume if err := bencode.DecodeString(value.(string), &fastResume); err != nil { log.Printf("Could not decode row %s. Continue\n", torrentID) continue } fastResume.TorrentFilePath = torrentNamePath if _, err = os.Stat(fastResume.TorrentFilePath); os.IsNotExist(err) { log.Printf("Could not find torrent file %s for %s\n", fastResume.TorrentFilePath, torrentID) return err } file, err := metainfo.LoadFromFile(torrentNamePath) if err != nil { return err } metaInfo, err := file.UnmarshalInfo() if err != nil { return err } if metaInfo.Files != nil { // valid QbtContentLayout = Original, Subfolder, NoSubfolder fastResume.QbtContentLayout = "Original" // legacy and should be removed sometime with 4.3.X fastResume.QbtHasRootFolder = 1 } else { fastResume.QbtContentLayout = "NoSubfolder" fastResume.QbtHasRootFolder = 0 } fastResume.QbtRatioLimit = -2000 fastResume.QbtSeedStatus = 1 fastResume.QbtSeedingTimeLimit = -2 fastResume.QbtName = "" fastResume.QbtSavePath = fastResume.SavePath fastResume.QbtQueuePosition = positionNum fastResume.AutoManaged = 0 fastResume.NumIncomplete = 0 fastResume.Paused = 0 fastResume.ConvertFilePriority(len(metaInfo.Files)) // fill pieces to set as completed fastResume.NumPieces = int64(metaInfo.NumPieces()) fastResume.FillPieces() // TODO handle replace paths if opts.DryRun { log.Printf("dry-run: (%d/%d) successfully imported: %s\n", positionNum, totalJobs, torrentID) continue } fastResumeOutFile := filepath.Join(opts.QbitDir, torrentID+".fastresume") if err = fastResume.Encode(fastResumeOutFile); err != nil { log.Printf("Could not create qBittorrent fastresume file %s error: %q\n", fastResumeOutFile, err) return err } if err = fs.CopyFile(fastResume.TorrentFilePath, torrentOutFile); err != nil { log.Printf("Could not copy qBittorrent torrent file %s error %q\n", torrentOutFile, err) return err } // Renaming the torrent file to .bak causes Deluge to skip the torrent when it restarts, // after which it will be removed from the .fastresume file. if err = os.Rename(torrentNamePath, torrentNamePathBak); err != nil { log.Printf("Could not move %s to %s error %q, continuing\n", torrentNamePath, torrentNamePathBak, err) } log.Printf("(%d/%d) successfully imported: %s %s\n", positionNum, totalJobs, torrentID, metaInfo.Name) } log.Printf("(%d/%d) successfully imported torrents!\n", positionNum, totalJobs) return nil } func decodeFastresumeFile(path string) (map[string]interface{}, error) { dat, err := os.ReadFile(path) if err != nil { return nil, err } var fastresumeFile map[string]interface{} if err := bencode.DecodeBytes(dat, &fastresumeFile); err != nil { return nil, err } return fastresumeFile, nil } ludviglundgren-qbittorrent-cli-4dbbde7/internal/importer/deluge_test.go000066400000000000000000000010711522034161300267440ustar00rootroot00000000000000package importer import "testing" func TestDelugeImport_Import(t *testing.T) { tests := []struct { name string opts Options wantErr bool }{ { name: "import_from_deluge", opts: Options{ SourceDir: "../../test/import/deluge/state/", QbitDir: "../../test/output/deluge", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { di := &DelugeImport{} if err := di.Import(tt.opts); (err != nil) != tt.wantErr { t.Errorf("Import() error = %v, wantErr %v", err, tt.wantErr) } }) } } ludviglundgren-qbittorrent-cli-4dbbde7/internal/importer/rtorrent.go000066400000000000000000000211141522034161300263170ustar00rootroot00000000000000package importer import ( "log" "os" "path" "path/filepath" "strconv" "strings" "time" "github.com/ludviglundgren/qbittorrent-cli/internal/fs" "github.com/ludviglundgren/qbittorrent-cli/pkg/qbittorrent" "github.com/ludviglundgren/qbittorrent-cli/pkg/torrent" "github.com/anacrolix/torrent/metainfo" "github.com/pkg/errors" "github.com/zeebo/bencode" ) type RTorrentImport struct{} func NewRTorrentImporter() Importer { return &RTorrentImport{} } var ( stateFileExtension = ".rtorrent" libtorrentStateFileExtension = ".libtorrent_resume" ) func (i *RTorrentImport) Import(opts Options) error { torrentsSessionDir := opts.SourceDir sourceDirInfo, err := os.Stat(torrentsSessionDir) if err != nil { if os.IsNotExist(err) { return errors.Errorf("source directory does not exist: %s", torrentsSessionDir) } return errors.Wrapf(err, "source directory error: %s", torrentsSessionDir) } if !sourceDirInfo.IsDir() { return errors.Errorf("source is a file, not a directory: %s", torrentsSessionDir) } matches, err := filepath.Glob(filepath.Join(torrentsSessionDir, "*.torrent")) if err != nil { return errors.Wrapf(err, "glob error: %s", torrentsSessionDir) } if len(matches) == 0 { log.Printf("Found 0 files to process in: %s\n", torrentsSessionDir) return nil } totalJobs := len(matches) log.Printf("Total torrents to process: %d\n", totalJobs) positionNum := 0 for _, match := range matches { positionNum++ torrentID := getTorrentFileName(match) torrentOutFile := filepath.Join(opts.QbitDir, torrentID+".torrent") // If file already exists, skip if _, err = os.Stat(torrentOutFile); err == nil { log.Printf("(%d/%d) %s Torrent already exists, skipping\n", positionNum, totalJobs, torrentOutFile) continue } if opts.DryRun { log.Printf("dry-run: (%d/%d) successfully imported: %s\n", positionNum, totalJobs, torrentID) continue } torrentFile, err := torrent.OpenDecodeRaw(match) if err != nil { log.Printf("Could not decode torrent file %s. Could not decode string %s. Continue\n", match, torrentID) continue } file, err := metainfo.LoadFromFile(match) if err != nil { return err } metaInfo, err := file.UnmarshalInfo() if err != nil { return err } // check for FILE.torrent.libtorrent_resume resumeFile, err := decodeRTorrentLibTorrentResumeFile(match + libtorrentStateFileExtension) if err != nil { return err } // check for FILE.torrent.rtorrent rtorrentFile, err := decodeRTorrentFile(match + stateFileExtension) if err != nil { return err } newFastResume := qbittorrent.Fastresume{ ActiveTime: getActiveTime(rtorrentFile.Custom.SeedingTime), AddedTime: strToIntClean(rtorrentFile.Custom.AddTime), Allocation: "sparse", ApplyIpFilter: 1, AutoManaged: 0, CompletedTime: rtorrentFile.TimestampFinished, DisableDHT: 0, DisableLSD: 0, DisablePEX: 0, DownloadRateLimit: -1, FileFormat: "libtorrent resume file", FileVersion: 1, FilePriority: []int{}, FinishedTime: int64(time.Since(time.Unix(rtorrentFile.TimestampFinished, 0)).Minutes()), LastDownload: 0, LastSeenComplete: rtorrentFile.TimestampFinished, LastUpload: 0, LibTorrentVersion: "1.2.11.0", MaxConnections: 16777215, MaxUploads: -1, NumComplete: 16777215, NumDownloaded: 16777215, NumIncomplete: 0, NumPieces: int64(metaInfo.NumPieces()), Paused: 0, Peers: "", Peers6: "", QbtCategory: "", QbtContentLayout: "Original", QbtFirstLastPiecePriority: 0, QbtName: "", QbtRatioLimit: -2000, QbtSavePath: rtorrentFile.Directory, QbtSeedStatus: 1, QbtSeedingTimeLimit: -2, QbtTags: []string{}, SavePath: rtorrentFile.Directory, SeedMode: 0, SeedingTime: 0, SequentialDownload: 0, ShareMode: 0, StopWhenReady: 0, SuperSeeding: 0, TotalDownloaded: rtorrentFile.TotalDownloaded, TotalUploaded: rtorrentFile.TotalUploaded, UploadMode: 0, UploadRateLimit: -1, UrlList: file.UrlList, TorrentFile: torrentFile, Path: rtorrentFile.Directory, } //if file.Info.Files != nil { if metaInfo.Files != nil { newFastResume.HasFiles = true // valid QbtContentLayout = Original, Subfolder, NoSubfolder newFastResume.QbtContentLayout = "Original" // legacy and should be removed sometime with 4.3.X newFastResume.QbtHasRootFolder = 1 // Fix savepath for torrents with subfolder // directory contains the whole torrent path, which gives error in qBit. // remove file.sourceDirInfo.name from full path in id.rtorrent directory newPath := strings.ReplaceAll(rtorrentFile.Directory, metaInfo.Name, "") newFastResume.Path = newPath newFastResume.SavePath = newPath newFastResume.QbtSavePath = newPath } else { // if only single file then use NoSubfolder newFastResume.HasFiles = false newFastResume.QbtContentLayout = "NoSubfolder" newFastResume.QbtHasRootFolder = 0 } // handle trackers newFastResume.Trackers = convertTrackers(*resumeFile) newFastResume.ConvertFilePriority(len(metaInfo.Files)) // fill pieces to set as completed newFastResume.FillPieces() // Set 20 byte SHA1 hash newFastResume.InfoHash = newFastResume.GetInfoHashSHA1() // copy torrent file fastResumeOutFile := filepath.Join(opts.QbitDir, torrentID+".fastresume") if err = newFastResume.Encode(fastResumeOutFile); err != nil { log.Printf("Could not create qBittorrent fastresume file %s error: %q\n", fastResumeOutFile, err) return err } if err = fs.CopyFile(match, torrentOutFile); err != nil { log.Printf("Could copy qBittorrent torrent file %s error %q\n", torrentOutFile, err) return err } log.Printf("(%d/%d) successfully imported: %s %s\n", positionNum, totalJobs, torrentID, metaInfo.Name) } log.Printf("(%d/%d) successfully imported torrents!\n", positionNum, totalJobs) return nil } // Takes id.rtorrent custom.seedingtime and converts to int64 func getActiveTime(t string) int64 { return int64(time.Since(time.Unix(strToIntClean(t), 0)).Seconds()) } // convertTrackers from rtorrent file spec to qBittorrent fastresume func convertTrackers(trackers RTorrentLibTorrentResumeFile) [][]string { var ret [][]string for url, status := range trackers.Trackers { // skip if dht if url == "dht://" { continue } if status["enabled"] == 1 { ret = append(ret, []string{url}) } } return ret } // getTorrentFileName from file. Removes file extension func getTorrentFileName(file string) string { _, fileName := filepath.Split(file) trimmed := strings.TrimSuffix(fileName, path.Ext(fileName)) toLower := strings.ToLower(trimmed) return toLower } func decodeRTorrentLibTorrentResumeFile(path string) (*RTorrentLibTorrentResumeFile, error) { dat, err := os.ReadFile(path) if err != nil { return nil, err } var torrentResumeFile RTorrentLibTorrentResumeFile if err := bencode.DecodeBytes(dat, &torrentResumeFile); err != nil { return nil, err } return &torrentResumeFile, nil } func decodeRTorrentFile(path string) (*RTorrentTorrentFile, error) { dat, err := os.ReadFile(path) if err != nil { return nil, err } var torrentFile RTorrentTorrentFile if err := bencode.DecodeBytes(dat, &torrentFile); err != nil { return nil, err } return &torrentFile, nil } // Clean and convert string to int from rtorrent.custom.addtime, seedingtime func strToIntClean(line string) int64 { if line == "" { return 0 } s := strings.TrimSuffix(line, "\n") i, err := strconv.ParseInt(s, 10, 64) if err != nil { return 0 } return i } type RTorrentLibTorrentResumeFile struct { Trackers map[string]map[string]int `bencode:"trackers"` } type RTorrentTorrentFile struct { Custom struct { AddTime string `bencode:"addtime"` SeedingTime string `bencode:"seedingtime"` } `bencode:"custom"` Directory string `bencode:"directory"` TotalDownloaded int64 `bencode:"total_downloaded"` TotalUploaded int64 `bencode:"total_uploaded"` TimestampFinished int64 `bencode:"timestamp.finished"` TimestampStarted int64 `bencode:"timestamp.started"` } ludviglundgren-qbittorrent-cli-4dbbde7/internal/importer/rtorrent_test.go000066400000000000000000000011001522034161300273470ustar00rootroot00000000000000package importer import ( "testing" ) func TestRTorrentImport_Import(t *testing.T) { tests := []struct { name string opts Options wantErr bool }{ { name: "import_from_rtorrent", opts: Options{ SourceDir: "../../test/import/sessions/", QbitDir: "../../test/output/rtorrent", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { i := &RTorrentImport{} if err := i.Import(tt.opts); (err != nil) != tt.wantErr { t.Errorf("Import() error = %v, wantErr %v", err, tt.wantErr) } }) } } ludviglundgren-qbittorrent-cli-4dbbde7/pkg/000077500000000000000000000000001522034161300212165ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/pkg/archive/000077500000000000000000000000001522034161300226375ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/pkg/archive/archive.go000066400000000000000000000025061522034161300246120ustar00rootroot00000000000000package archive import ( "archive/tar" "compress/gzip" "fmt" "io" "os" "path/filepath" ) func TarGzDirectory(source, target string) error { file, err := os.Create(target) if err != nil { return fmt.Errorf("failed to create tar.gz file: %w", err) } defer file.Close() // Create a new gzip writer gzipWriter := gzip.NewWriter(file) defer gzipWriter.Close() // Create a new tar writer tarWriter := tar.NewWriter(gzipWriter) defer tarWriter.Close() err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // Create a tar header from file info header, err := tar.FileInfoHeader(info, "") if err != nil { return err } // Preserve the directory structure header.Name, err = filepath.Rel(filepath.Dir(source), path) if err != nil { return err } // Write the header to the tar archive if err := tarWriter.WriteHeader(header); err != nil { return err } // If it's a file, write its content to the tar archive if !info.IsDir() { file, err := os.Open(path) if err != nil { return err } defer file.Close() if _, err := io.Copy(tarWriter, file); err != nil { return err } } return nil }) if err != nil { return fmt.Errorf("failed to add files to %s.tar.gz archive: %w", target, err) } return nil } ludviglundgren-qbittorrent-cli-4dbbde7/pkg/qbittorrent/000077500000000000000000000000001522034161300235735ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/pkg/qbittorrent/client.go000066400000000000000000000137571522034161300254150ustar00rootroot00000000000000package qbittorrent import ( "bytes" "crypto/tls" "fmt" "io" "log" "mime/multipart" "net/http" "net/http/cookiejar" "net/url" "os" "strings" "time" "github.com/pkg/errors" "golang.org/x/net/context" "golang.org/x/net/publicsuffix" ) const DefaultTimeout = 60 type Client struct { hostname string port uint addr string username string password string basicUser string basicPass string tlsSkipVerify bool http *http.Client } type Settings struct { Hostname string Port uint Addr string Username string Password string BasicUser string BasicPass string TLSSkipVerify bool } func NewClient(s Settings) *Client { jarOptions := &cookiejar.Options{PublicSuffixList: publicsuffix.List} //store cookies in jar jar, err := cookiejar.New(jarOptions) if err != nil { log.Fatal(err) } t := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: s.TLSSkipVerify, }, } httpClient := &http.Client{ Timeout: time.Second * DefaultTimeout, Jar: jar, Transport: t, } return &Client{ hostname: s.Hostname, port: s.Port, addr: s.Addr, username: s.Username, password: s.Password, basicUser: s.BasicUser, basicPass: s.BasicPass, tlsSkipVerify: s.TLSSkipVerify, http: httpClient, } } func (c *Client) get(endpoint string, values url.Values) (*http.Response, error) { return c.getCtx(context.TODO(), endpoint, values) } func (c *Client) getCtx(ctx context.Context, endpoint string, values url.Values) (*http.Response, error) { reqUrl, err := c.buildUrl(endpoint) if err != nil { return nil, errors.Wrap(err, "could not build url") } u, err := url.Parse(reqUrl) if err != nil { return nil, errors.Wrap(err, "could not parse url") } u.RawQuery = values.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, errors.Wrap(err, "could not build request") } // set basic auth c.setBasicAuth(req) resp, err := c.http.Do(req) if err != nil { return nil, errors.Wrap(err, "could not make request") } return resp, nil } func (c *Client) post(endpoint string, values url.Values) (*http.Response, error) { return c.postCtx(context.TODO(), endpoint, values) } func (c *Client) postCtx(ctx context.Context, endpoint string, values url.Values) (*http.Response, error) { reqUrl, err := c.buildUrl(endpoint) if err != nil { return nil, errors.Wrap(err, "could not build url for request") } req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqUrl, strings.NewReader(values.Encode())) if err != nil { return nil, errors.Wrap(err, "could not build request") } // add the content-type so qbittorrent knows what to expect req.Header.Add("Content-Type", "application/x-www-form-urlencoded") // set basic auth c.setBasicAuth(req) resp, err := c.http.Do(req) if err != nil { return nil, errors.Wrap(err, "could not make request") } defer resp.Body.Close() return resp, nil } func (c *Client) postFormCtx(ctx context.Context, endpoint string, form url.Values) (*http.Response, error) { reqUrl, err := c.buildUrl(endpoint) if err != nil { return nil, errors.Wrap(err, "could not build url for request") } req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqUrl, strings.NewReader(form.Encode())) if err != nil { return nil, errors.Wrap(err, "could not build request") } // add the content-type so qbittorrent knows what to expect req.Header.Add("Content-Type", "application/x-www-form-urlencoded") // set basic auth c.setBasicAuth(req) resp, err := c.http.Do(req) if err != nil { return nil, errors.Wrap(err, "could not make request") } defer resp.Body.Close() return resp, nil } func (c *Client) postFileCtx(ctx context.Context, endpoint string, fileName string, opts map[string]string) (*http.Response, error) { file, err := os.Open(fileName) if err != nil { return nil, errors.Wrapf(err, "could not open file: %s", fileName) } // Close the file later defer file.Close() // Buffer to store our request body as bytes var requestBody bytes.Buffer // Create a multipart writer multiPartWriter := multipart.NewWriter(&requestBody) // Initialize file field fileWriter, err := multiPartWriter.CreateFormFile("torrents", fileName) if err != nil { return nil, errors.Wrap(err, "could not initialize form file field") } // Copy the actual file content to the fields writer _, err = io.Copy(fileWriter, file) if err != nil { return nil, errors.Wrap(err, "could not copy file to writer") } // Populate other fields for key, val := range opts { fieldWriter, err := multiPartWriter.CreateFormField(key) if err != nil { return nil, errors.Wrapf(err, "could not create form field: %s", key) } if _, err := fieldWriter.Write([]byte(val)); err != nil { return nil, errors.Wrapf(err, "could not write form field: %s value: %s", key, val) } } // Close multipart writer multiPartWriter.Close() reqUrl, err := c.buildUrl(endpoint) if err != nil { return nil, errors.Wrap(err, "could not build url for request") } req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqUrl, &requestBody) if err != nil { return nil, errors.Wrap(err, "could not build request") } // Set correct content type req.Header.Set("Content-Type", multiPartWriter.FormDataContentType()) // set basic auth c.setBasicAuth(req) res, err := c.http.Do(req) if err != nil { return nil, errors.Wrap(err, "could not make request") } return res, nil } func (c *Client) buildUrl(endpoint string) (string, error) { reqUrl, err := url.JoinPath(c.addr, "/api/v2", endpoint) if err != nil { return "", errors.Wrap(err, "could not build addr") } if c.hostname != "" && c.port != 0 { reqUrl = fmt.Sprintf("http://%s:%d/api/v2/%s", c.hostname, c.port, endpoint) } return reqUrl, nil } func (c *Client) setBasicAuth(req *http.Request) { if c.basicUser != "" && c.basicPass != "" { req.SetBasicAuth(c.basicUser, c.basicPass) } } ludviglundgren-qbittorrent-cli-4dbbde7/pkg/qbittorrent/domain.go000066400000000000000000000146531522034161300254020ustar00rootroot00000000000000package qbittorrent type Torrent struct { AddedOn int `json:"added_on"` AmountLeft int `json:"amount_left"` AutoManaged bool `json:"auto_tmm"` Availability float32 `json:"availability"` Category string `json:"category"` Completed int `json:"completed"` CompletionOn int `json:"completion_on"` DlLimit int `json:"dl_limit"` DlSpeed int `json:"dlspeed"` Downloaded int `json:"downloaded"` DownloadedSession int `json:"downloaded_session"` ETA int `json:"eta"` FirstLastPiecePrio bool `json:"f_l_piece_prio"` ForceStart bool `json:"force_start"` Hash string `json:"hash"` LastActivity int `json:"last_activity"` MagnetURI string `json:"magnet_uri"` MaxRatio float32 `json:"max_ratio"` MaxSeedingTime int `json:"max_seeding_time"` Name string `json:"name"` NumComplete int `json:"num_complete"` NumIncomplete int `json:"num_incomplete"` NumSeeds int `json:"num_seeds"` Priority int `json:"priority"` Progress float32 `json:"progress"` Ratio float32 `json:"ratio"` RatioLimit float32 `json:"ratio_limit"` SavePath string `json:"save_path"` SeedingTimeLimit int `json:"seeding_time_limit"` SeenComplete int `json:"seen_complete"` SequentialDownload bool `json:"seq_dl"` Size int `json:"size"` State TorrentState `json:"state"` SuperSeeding bool `json:"super_seeding"` Tags string `json:"tags"` TimeActive int `json:"time_active"` TotalSize int `json:"total_size"` Tracker string `json:"tracker"` UpLimit int `json:"up_limit"` Uploaded int `json:"uploaded"` UploadedSession int `json:"uploaded_session"` UpSpeed int `json:"upspeed"` } type TorrentBasic struct { Category string `json:"category"` Downloaded int `json:"downloaded"` Hash string `json:"hash"` Name string `json:"name"` Progress float32 `json:"progress"` Ratio float32 `json:"ratio"` Size int `json:"size"` State TorrentState `json:"state"` Tags string `json:"tags"` Tracker string `json:"tracker"` Uploaded int `json:"uploaded"` } type TorrentTrackersResponse struct { Trackers []TorrentTracker `json:"trackers"` } type TorrentTracker struct { //Tier uint `json:"tier"` // can be both empty "" and int Url string `json:"url"` Status int `json:"status"` NumPeers int `json:"num_peers"` NumSeeds int `json:"num_seeds"` NumLeechers int `json:"num_leechers"` NumDownloaded int `json:"num_downloaded"` Message string `json:"msg"` } type TorrentState string const ( // Some error occurred, applies to paused torrents TorrentStateError TorrentState = "error" // Torrent data files is missing TorrentStateMissingFiles TorrentState = "missingFiles" // Torrent is being seeded and data is being transferred TorrentStateUploading TorrentState = "uploading" // Torrent is paused and has finished downloading TorrentStatePausedUp TorrentState = "pausedUP" // Queuing is enabled and torrent is queued for upload TorrentStateQueuedUp TorrentState = "queuedUP" // Torrent is being seeded, but no connection were made TorrentStateStalledUp TorrentState = "stalledUP" // Torrent has finished downloading and is being checked TorrentStateCheckingUp TorrentState = "checkingUP" // Torrent is forced to uploading and ignore queue limit TorrentStateForcedUp TorrentState = "forcedUP" // Torrent is allocating disk space for download TorrentStateAllocating TorrentState = "allocating" // Torrent is being downloaded and data is being transferred TorrentStateDownloading TorrentState = "downloading" // Torrent has just started downloading and is fetching metadata TorrentStateMetaDl TorrentState = "metaDL" // Torrent is paused and has NOT finished downloading TorrentStatePausedDl TorrentState = "pausedDL" // Queuing is enabled and torrent is queued for download TorrentStateQueuedDl TorrentState = "queuedDL" // Torrent is being downloaded, but no connection were made TorrentStateStalledDl TorrentState = "stalledDL" // Same as checkingUP, but torrent has NOT finished downloading TorrentStateCheckingDl TorrentState = "checkingDL" // Torrent is forced to downloading to ignore queue limit TorrentStateForceDl TorrentState = "forceDL" // Checking resume data on qBt startup TorrentStateCheckingResumeData TorrentState = "checkingResumeData" // Torrent is moving to another location TorrentStateMoving TorrentState = "moving" // Unknown status TorrentStateUnknown TorrentState = "unknown" ) // https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-list type TorrentFilter string const ( // Torrent is paused TorrentFilterAll TorrentFilter = "all" // Torrent is active TorrentFilterActive TorrentFilter = "active" // Torrent is inactive TorrentFilterInactive TorrentFilter = "inactive" // Torrent is completed TorrentFilterCompleted TorrentFilter = "completed" // Torrent is resumed TorrentFilterResumed TorrentFilter = "resumed" // Torrent is paused TorrentFilterPaused TorrentFilter = "paused" // Torrent is seeding TorrentFilterSeeding TorrentFilter = "seeding" // Torrent is stalled TorrentFilterStalled TorrentFilter = "stalled" // Torrent is being seeded and data is being transferred TorrentFilterUploading TorrentFilter = "uploading" // Torrent is being seeded, but no connection were made TorrentFilterStalledUploading TorrentFilter = "stalled_uploading" // Torrent is being downloaded and data is being transferred TorrentFilterDownloading TorrentFilter = "downloading" // Torrent is being downloaded, but no connection were made TorrentFilterStalledDownloading TorrentFilter = "stalled_downloading" TorrentFilterErrored TorrentFilter = "errored" ) type GetTorrentsRequest struct { Filter TorrentFilter Category string Tag string Hashes string Json bool } ludviglundgren-qbittorrent-cli-4dbbde7/pkg/qbittorrent/fastresume.go000066400000000000000000000214621522034161300263050ustar00rootroot00000000000000package qbittorrent import ( "bufio" "crypto/sha1" "fmt" "log" "os" "path/filepath" "strings" "github.com/zeebo/bencode" ) // Fastresume represents a qBittorrent fastresume file type Fastresume struct { ActiveTime int64 `bencode:"active_time"` AddedTime int64 `bencode:"added_time"` Allocation string `bencode:"allocation"` ApplyIpFilter int64 `bencode:"apply_ip_filter"` AnnounceToDht int64 `bencode:"announce_to_dht,omitempty"` AnnounceToLsd int64 `bencode:"announce_to_lsd,omitempty"` AnnounceToTrackers int64 `bencode:"announce_to_trackers,omitempty"` AutoManaged int64 `bencode:"auto_managed"` CompletedTime int64 `bencode:"completed_time"` DisableDHT int64 `bencode:"disable_dht"` DisableLSD int64 `bencode:"disable_lsd"` DisablePEX int64 `bencode:"disable_pex"` DownloadRateLimit int64 `bencode:"download_rate_limit"` FileFormat string `bencode:"file-format"` FileVersion int64 `bencode:"file-version"` FilePriority []int `bencode:"file_priority"` FileSizes [][]int64 `bencode:"file sizes,omitempty"` FinishedTime int64 `bencode:"finished_time"` HttpSeeds []string `bencode:"httpseeds"` InfoHash []byte `bencode:"info-hash"` LastDownload int64 `bencode:"last_download"` LastSeenComplete int64 `bencode:"last_seen_complete"` LastUpload int64 `bencode:"last_upload"` LibTorrentVersion string `bencode:"libtorrent-version"` MaxConnections int64 `bencode:"max_connections"` MaxUploads int64 `bencode:"max_uploads"` NumComplete int64 `bencode:"num_complete"` NumDownloaded int64 `bencode:"num_downloaded"` NumIncomplete int64 `bencode:"num_incomplete"` Paused int64 `bencode:"paused"` Pieces string `bencode:"pieces"` PiecePriority []byte `bencode:"piece_priority"` Peers string `bencode:"peers"` Peers6 string `bencode:"peers6"` QbtCategory string `bencode:"qBt-category"` QbtContentLayout string `bencode:"qBt-contentLayout"` QbtHasRootFolder int64 `bencode:"qBt-hasRootFolder"` QbtFirstLastPiecePriority int64 `bencode:"qBt-firstLastPiecePriority"` QbtName string `bencode:"qBt-name"` QbtRatioLimit int64 `bencode:"qBt-ratioLimit"` QbtSavePath string `bencode:"qBt-savePath"` QbtSeedStatus int64 `bencode:"qBt-seedStatus"` QbtSeedingTimeLimit int64 `bencode:"qBt-seedingTimeLimit"` QbtTags []string `bencode:"qBt-tags"` QbtQueuePosition int `bencode:"qBt-queuePosition,omitempty"` QbtTempPathDisabled int64 `bencode:"qBt-tempPathDisabled,omitempty"` SavePath string `bencode:"save_path"` SeedMode int64 `bencode:"seed_mode"` SeedingTime int64 `bencode:"seeding_time"` SequentialDownload int64 `bencode:"sequential_download"` ShareMode int64 `bencode:"share_mode"` StopWhenReady int64 `bencode:"stop_when_ready"` SuperSeeding int64 `bencode:"super_seeding"` TotalDownloaded int64 `bencode:"total_downloaded"` TotalUploaded int64 `bencode:"total_uploaded"` Trackers TrackerTiers `bencode:"trackers"` UploadMode int64 `bencode:"upload_mode"` UploadRateLimit int64 `bencode:"upload_rate_limit"` UrlList []string `bencode:"url-list"` Unfinished *[]interface{} `bencode:"unfinished,omitempty"` WithoutLabels bool `bencode:"-"` WithoutTags bool `bencode:"-"` HasFiles bool `bencode:"-"` TorrentFilePath string `bencode:"-"` TorrentFile map[string]interface{} `bencode:"-"` Path string `bencode:"-"` fileSizes int64 `bencode:"-"` sizeAndPrio [][]int64 `bencode:"-"` torrentFileList []string `bencode:"-"` NumPieces int64 `bencode:"-"` PieceLength int64 `bencode:"-"` MappedFiles []string `bencode:"mapped_files,omitempty"` } // TrackerTiers represents the libtorrent fastresume "trackers" field. // // qBittorrent/libtorrent normally writes a list of tiers, where each tier is a // list of tracker URLs ([][]string). Some clients and older versions instead // write a flat list of URLs ([]string) or even a single (whitespace separated) // string. The default decoder fails on those variants with // "Cannot store string into []string", which used to abort the whole // `torrent export` command. UnmarshalBencode normalizes all of these shapes into // the standard [][]string form so the file can still be processed. type TrackerTiers [][]string // UnmarshalBencode implements bencode.Unmarshaler. func (t *TrackerTiers) UnmarshalBencode(b []byte) error { // Standard format: a list of tiers ([][]string). var tiers [][]string if err := bencode.DecodeBytes(b, &tiers); err == nil { *t = tiers return nil } // Flat format: a single list of URLs ([]string). Wrap each URL in its own tier. var urls []string if err := bencode.DecodeBytes(b, &urls); err == nil { tiers = make([][]string, 0, len(urls)) for _, url := range urls { tiers = append(tiers, []string{url}) } *t = tiers return nil } // Single string, possibly whitespace separated. var single string if err := bencode.DecodeBytes(b, &single); err == nil { fields := strings.Fields(single) tiers = make([][]string, 0, len(fields)) for _, url := range fields { tiers = append(tiers, []string{url}) } *t = tiers return nil } return fmt.Errorf("could not decode trackers field: unsupported format %q", string(b)) } // Encode qBittorrent fastresume file func (fr *Fastresume) Encode(path string) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, os.ModePerm); err != nil { log.Printf("os create dir error: %v", err) return err } file, err := os.Create(path) if err != nil { log.Printf("os create error: %v", err) return err } defer file.Close() bufferedWriter := bufio.NewWriter(file) enc := bencode.NewEncoder(bufferedWriter) if err := enc.Encode(fr); err != nil { log.Printf("encode error: %v", err) return err } bufferedWriter.Flush() return nil } // ConvertFilePriority for each file set priority func (fr *Fastresume) ConvertFilePriority(numFiles int) { var newPrioList []int /* File priority: 0 Do not download 1 Normal 2 High */ for i := 0; i < numFiles; i++ { newPrioList = append(newPrioList, 1) } fr.FilePriority = newPrioList } // FillPieces set pieces as complete/done so fastresume doesn't recheck func (fr *Fastresume) FillPieces() { var pieces = make([]string, 0, fr.NumPieces) for i := int64(0); i < fr.NumPieces; i++ { pieces = append(pieces, "\x01") } fr.Pieces = strings.Join(pieces, "") } // GetInfoHashSHA1 returns a 20 byte hash func (fr *Fastresume) GetInfoHashSHA1() (hash []byte) { torInfo, _ := bencode.EncodeString(fr.TorrentFile["info"].(map[string]interface{})) h := sha1.New() _, _ = h.Write([]byte(torInfo)) ab := h.Sum(nil) return ab } //func (newstructure *NewTorrentStructure) GetTrackers(trackers interface{}) { // switch strct := trackers.(type) { // case []interface{}: // for _, st := range strct { // newstructure.GetTrackers(st) // } // case string: // for _, str := range strings.Fields(strct) { // newstructure.Trackers = append(newstructure.Trackers, []string{str}) // } // // } //} ludviglundgren-qbittorrent-cli-4dbbde7/pkg/qbittorrent/fastresume_test.go000066400000000000000000000052251522034161300273430ustar00rootroot00000000000000package qbittorrent import ( "reflect" "testing" "github.com/zeebo/bencode" ) func TestTrackerTiers_UnmarshalBencode(t *testing.T) { tests := []struct { name string input interface{} want [][]string wantErr bool }{ { name: "nested tiers (standard qBittorrent format)", input: [][]string{{"https://a/announce"}, {"udp://b:1337"}}, want: [][]string{{"https://a/announce"}, {"udp://b:1337"}}, }, { name: "nested tier with multiple urls", input: [][]string{{"https://a/announce", "https://a-backup/announce"}}, want: [][]string{{"https://a/announce", "https://a-backup/announce"}}, }, { // this is the shape that produced "Cannot store string into []string" name: "flat list of urls", input: []string{"https://a/announce", "udp://b:1337"}, want: [][]string{{"https://a/announce"}, {"udp://b:1337"}}, }, { name: "single url string", input: "https://a/announce", want: [][]string{{"https://a/announce"}}, }, { name: "single whitespace separated string", input: "https://a/announce udp://b:1337", want: [][]string{{"https://a/announce"}, {"udp://b:1337"}}, }, { name: "empty list", input: []string{}, want: nil, }, { name: "unsupported type", input: int64(5), wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { encoded, err := bencode.EncodeBytes(tt.input) if err != nil { t.Fatalf("could not encode test input: %v", err) } var got TrackerTiers err = bencode.DecodeBytes(encoded, &got) if (err != nil) != tt.wantErr { t.Fatalf("UnmarshalBencode() error = %v, wantErr %v", err, tt.wantErr) } if tt.wantErr { return } if !reflect.DeepEqual([][]string(got), tt.want) { t.Errorf("UnmarshalBencode() = %#v, want %#v", [][]string(got), tt.want) } }) } } // TestFastresume_DecodeFlatTrackers ensures a full fastresume that stores the // trackers field as a flat list of strings can still be decoded. This is the // exact failure reported in issue #135. func TestFastresume_DecodeFlatTrackers(t *testing.T) { raw := map[string]interface{}{ "save_path": "/downloads", "trackers": []string{"https://a/announce", "udp://b:1337"}, } encoded, err := bencode.EncodeBytes(raw) if err != nil { t.Fatalf("could not encode fastresume: %v", err) } var fr Fastresume if err := bencode.DecodeBytes(encoded, &fr); err != nil { t.Fatalf("could not decode fastresume with flat trackers: %v", err) } want := [][]string{{"https://a/announce"}, {"udp://b:1337"}} if !reflect.DeepEqual([][]string(fr.Trackers), want) { t.Errorf("Trackers = %#v, want %#v", [][]string(fr.Trackers), want) } } ludviglundgren-qbittorrent-cli-4dbbde7/pkg/qbittorrent/methods.go000066400000000000000000000241421522034161300255700ustar00rootroot00000000000000package qbittorrent import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "github.com/anacrolix/torrent/metainfo" "github.com/pkg/errors" "golang.org/x/net/context" ) func (c *Client) Login(ctx context.Context) error { v := url.Values{} v.Add("username", c.username) v.Add("password", c.password) resp, err := c.postCtx(ctx, "auth/login", v) if err != nil { return errors.Wrap(err, "login error") } if resp.StatusCode != http.StatusOK { // check for correct status code bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return err } return errors.New(fmt.Sprintf("login: unexpected response status: %d body: %s", resp.StatusCode, bodyBytes)) } // place cookies in jar for future requests if cookies := resp.Cookies(); len(cookies) > 0 { reqUrl, err := c.buildUrl("") if err != nil { return errors.Wrap(err, "login error, could not parse url for cookie") } cookieURL, _ := url.Parse(reqUrl) c.http.Jar.SetCookies(cookieURL, cookies) } return nil } func (c *Client) GetTorrentsWithFilters(ctx context.Context, req *GetTorrentsRequest) ([]Torrent, error) { v := url.Values{} if req.Filter != "" { v.Add("filter", string(req.Filter)) } else if req.Category != "" { v.Add("category", req.Category) } else if req.Tag != "" { v.Add("tag", req.Tag) } else if req.Hashes != "" { v.Add("hashes", req.Hashes) } resp, err := c.getCtx(ctx, "torrents/info", v) if err != nil { return nil, errors.Wrap(err, "could not fetch torrents") } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.Wrap(err, "could not read body") } if resp.StatusCode == http.StatusConflict { // qbit returns 409 if destination category does not exist return nil, errors.Errorf("category '%s' does not exist: body %s", req.Category, body) } var torrents []Torrent if err := json.Unmarshal(body, &torrents); err != nil { return nil, errors.Wrap(err, "could not unmarshal torrents list") } return torrents, nil } func (c *Client) GetTorrents(ctx context.Context) ([]Torrent, error) { resp, err := c.getCtx(ctx, "torrents/info", nil) if err != nil { return nil, errors.Wrap(err, "could not fetch torrents") } defer resp.Body.Close() body, readErr := io.ReadAll(resp.Body) if readErr != nil { return nil, errors.Wrap(err, "could not read body") } var torrents []Torrent if err := json.Unmarshal(body, &torrents); err != nil { return nil, errors.Wrap(err, "could not unmarshal torrents list") } return torrents, nil } func (c *Client) GetTorrentsRaw(ctx context.Context) (string, error) { resp, err := c.getCtx(ctx, "torrents/info", nil) if err != nil { return "", errors.Wrap(err, "could not fetch torrents") } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return "", errors.Wrap(err, "could not read body") } return string(data), nil } // GetTorrentsByPrefixes Search for torrents using provided prefixes; checks against either hashes, names, or both func (c *Client) GetTorrentsByPrefixes(ctx context.Context, terms []string, hashes bool, names bool) ([]Torrent, error) { torrents, err := c.GetTorrents(ctx) if err != nil { return nil, err } matchedTorrents := map[Torrent]bool{} for _, torrent := range torrents { if hashes { for _, targetHash := range terms { if strings.HasPrefix(torrent.Hash, targetHash) { matchedTorrents[torrent] = true break } } if matchedTorrents[torrent] { continue } } if names { for _, targetName := range terms { if strings.HasPrefix(torrent.Name, targetName) { matchedTorrents[torrent] = true break } } } } var foundTorrents []Torrent for torrent := range matchedTorrents { foundTorrents = append(foundTorrents, torrent) } return foundTorrents, nil } func (c *Client) GetTorrentTrackers(ctx context.Context, hash string) ([]TorrentTracker, error) { params := url.Values{} params.Add("hash", hash) resp, err := c.getCtx(ctx, "torrents/trackers", params) if err != nil { return nil, errors.Wrapf(err, "error fetching torrent trackers for: %s", hash) } defer resp.Body.Close() body, readErr := io.ReadAll(resp.Body) if readErr != nil { return nil, errors.Wrap(readErr, "could not read body") } var trackers []TorrentTracker if err := json.Unmarshal(body, &trackers); err != nil { return nil, errors.Wrapf(err, "could not unmarshal raw json: %s", body) } return trackers, nil } // AddTorrentFromFile add new torrent from torrent file func (c *Client) AddTorrentFromFile(ctx context.Context, file string, options map[string]string) (hash string, err error) { // Get meta info from file to find out the hash for later use t, err := metainfo.LoadFromFile(file) if err != nil { return "", errors.Wrapf(err, "could not open file: %s", file) } resp, err := c.postFileCtx(ctx, "torrents/add", file, options) if err != nil { return "", errors.Wrapf(err, "could not add torrent from file: %s", file) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return "", errors.Wrap(err, "could not read body") } return "", errors.New(fmt.Sprintf("unexpected response status: %d body: %s", resp.StatusCode, bodyBytes)) } return t.HashInfoBytes().HexString(), nil } func (c *Client) AddTorrentFromMagnet(ctx context.Context, magnetUri string, options map[string]string) (hash string, err error) { magnet, err := metainfo.ParseMagnetUri(magnetUri) if err != nil { return "", errors.Wrapf(err, "could not parse magnet URI: %s", magnetUri) } form := url.Values{} form.Add("urls", magnetUri) for key, val := range options { form.Add(key, val) } resp, err := c.postFormCtx(ctx, "torrents/add", form) if err != nil { return "", errors.Wrapf(err, "could not add torrent from magnet: %s", magnetUri) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return "", errors.Wrap(err, "could not read body") } return "", errors.New(fmt.Sprintf("unexpected response status: %d body: %s", resp.StatusCode, bodyBytes)) } return magnet.InfoHash.HexString(), nil } func (c *Client) DeleteTorrents(ctx context.Context, hashes []string, deleteFiles bool) error { form := url.Values{} // Add hashes together with | separator form.Add("hashes", strings.Join(hashes, "|")) // Only include the deleteFiles parameter if it's set to true if deleteFiles { form.Add("deleteFiles", "true") } resp, err := c.postFormCtx(ctx, "torrents/delete", form) if err != nil { return errors.Wrapf(err, "could not delete torrents by hashes: %v", hashes) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return errors.Wrap(err, "could not read body") } return errors.New(fmt.Sprintf("unexpected response status: %d body: %s", resp.StatusCode, bodyBytes)) } return nil } func (c *Client) ReAnnounceTorrents(ctx context.Context, hashes []string) error { v := url.Values{} // Add hashes together with | separator v.Add("hashes", strings.Join(hashes, "|")) resp, err := c.postCtx(ctx, "torrents/reannounce?"+v.Encode(), nil) if err != nil { return errors.Wrapf(err, "could not reannounce torrents by hashes: %v", hashes) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return errors.Wrap(err, "could not read body") } return errors.New(fmt.Sprintf("unexpected response status: %d body: %s", resp.StatusCode, bodyBytes)) } return nil } func (c *Client) Pause(ctx context.Context, hashes []string) error { form := url.Values{} // Add hashes together with | separator form.Add("hashes", strings.Join(hashes, "|")) resp, err := c.postFormCtx(ctx, "torrents/pause", form) if err != nil { return errors.Wrapf(err, "could not pause torrents: %v", hashes) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return errors.Wrap(err, "could not read body") } return errors.New(fmt.Sprintf("unexpected response status: %d body: %s", resp.StatusCode, bodyBytes)) } return nil } func (c *Client) Resume(ctx context.Context, hashes []string) error { form := url.Values{} // Add hashes together with | separator form.Add("hashes", strings.Join(hashes, "|")) resp, err := c.postFormCtx(ctx, "torrents/resume", form) if err != nil { return errors.Wrapf(err, "could not resume torrents: %v", hashes) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return errors.Wrap(err, "could not read body") } return errors.New(fmt.Sprintf("unexpected response status: %d body: %s", resp.StatusCode, bodyBytes)) } return nil } func (c *Client) SetCategory(ctx context.Context, hashes []string, category string) error { form := url.Values{} // Add hashes together with | separator form.Add("hashes", strings.Join(hashes, "|")) form.Add("category", category) resp, err := c.postFormCtx(ctx, "torrents/setCategory", form) if err != nil { return errors.Wrapf(err, "could not set category for torrents: %v", hashes) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return errors.Wrap(err, "could not read body") } return errors.New(fmt.Sprintf("unexpected response status: %d body: %s", resp.StatusCode, bodyBytes)) } return nil } func (c *Client) SetTag(ctx context.Context, hashes []string, tag string) error { form := url.Values{} // Add hashes together with | separator form.Add("hashes", strings.Join(hashes, "|")) form.Add("tags", tag) resp, err := c.postFormCtx(ctx, "torrents/addTags", form) if err != nil { return errors.Wrapf(err, "could not set tag for torrents: %v", hashes) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return errors.Wrap(err, "could not read body") } return errors.New(fmt.Sprintf("unexpected response status: %d body: %s", resp.StatusCode, bodyBytes)) } return nil } ludviglundgren-qbittorrent-cli-4dbbde7/pkg/torrent/000077500000000000000000000000001522034161300227135ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/pkg/torrent/torrent.go000066400000000000000000000043201522034161300247360ustar00rootroot00000000000000package torrent import ( "crypto/sha1" "encoding/hex" "io" "os" "github.com/zeebo/bencode" ) // TorrentInfo torrent meta info type TorrentInfo struct { Announce string `bencode:"announce"` AnnounceList [][]string `bencode:"announce-list"` CreatedBy string `bencode:"created by"` CreationDate int64 `bencode:"creation date"` Info struct { Length int64 `bencode:"length"` Name string `bencode:"name"` PieceLength int64 `bencode:"piece length"` Pieces string `bencode:"pieces"` Private bool `bencode:"private"` Files []TorrentInfoFile `bencode:"files"` } `bencode:"info"` UrlList []string `bencode:"url-list"` } type TorrentInfoFile struct { Length int64 `bencode:"length"` Path []string `bencode:"path"` } func Decode(path string) (*TorrentInfo, error) { dat, err := os.ReadFile(path) if err != nil { return nil, err } var torrent TorrentInfo if err := bencode.DecodeBytes(dat, &torrent); err != nil { return nil, err } return &torrent, nil } func OpenDecodeRaw(path string) (map[string]interface{}, error) { dat, err := os.ReadFile(path) if err != nil { return nil, err } var torrent map[string]interface{} if err := bencode.DecodeBytes(dat, &torrent); err != nil { return nil, err } return torrent, nil } func CopyFile(src string, dst string) error { originalFile, err := os.Open(src) if err != nil { return err } defer originalFile.Close() newFile, err := os.Create(dst) if err != nil { return err } defer newFile.Close() if _, err := io.Copy(newFile, originalFile); err != nil { return err } if err := newFile.Sync(); err != nil { return err } return nil } func CalculateInfoHash(torrent map[string]interface{}) (hash string) { t, _ := bencode.EncodeString(torrent["info"]) h := sha1.New() io.WriteString(h, t) hash = hex.EncodeToString(h.Sum(nil)) return hash } func GetName(torrent map[string]interface{}) (name string) { info, _ := torrent["info"].(map[string]interface{}) //info, _ := bencode.EncodeString(torrent["info"]) //h := sha1.New() //io.WriteString(h, t) //hash = hex.EncodeToString(h.Sum(nil)) return info["name"].(string) } ludviglundgren-qbittorrent-cli-4dbbde7/pkg/utils/000077500000000000000000000000001522034161300223565ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/pkg/utils/utils.go000066400000000000000000000013471522034161300240520ustar00rootroot00000000000000package utils import ( "fmt" "os/user" "path/filepath" "regexp" "strings" ) var hashRegex = regexp.MustCompile("^[a-fA-F0-9]{40}$") func ValidateHash(hashes []string) error { var invalid []string for _, hash := range hashes { if !hashRegex.MatchString(hash) { invalid = append(invalid, hash) } } if len(invalid) > 0 { return fmt.Errorf("invalid hashes: %s", strings.Join(invalid, ",")) } return nil } // ExpandTilde expands the ~ in the file path to the home directory func ExpandTilde(path string) (string, error) { if strings.HasPrefix(path, "~") { usr, err := user.Current() if err != nil { return "", err } homeDir := usr.HomeDir return filepath.Join(homeDir, path[1:]), nil } return path, nil } ludviglundgren-qbittorrent-cli-4dbbde7/pkg/utils/utils_test.go000066400000000000000000000014011522034161300251000ustar00rootroot00000000000000package utils import "testing" func TestValidateHash(t *testing.T) { type args struct { hashes []string } tests := []struct { name string args args want bool wantErr bool }{ {name: "ok", args: args{hashes: []string{"6957bf5272f5b994132458a557864e3ea747489f"}}, want: true, wantErr: false}, {name: "invalid", args: args{hashes: []string{"6957bf5272f5b994132458a557864e3ea747489"}}, want: false, wantErr: true}, {name: "invalid_2", args: args{hashes: []string{"11111"}}, want: false, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateHash(tt.args.hashes) if (err != nil) != tt.wantErr { t.Errorf("ValidateHash() error = %v, wantErr %v", err, tt.wantErr) return } }) } } ludviglundgren-qbittorrent-cli-4dbbde7/test/000077500000000000000000000000001522034161300214145ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/test/config/000077500000000000000000000000001522034161300226615ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/test/config/qBittorrent/000077500000000000000000000000001522034161300251765ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/test/config/qBittorrent/BT_backup/000077500000000000000000000000001522034161300270305ustar00rootroot000000000000005ba4939a00a9b21629a0ad7d376898b768d997a3.fastresume000066400000000000000000000026251522034161300363210ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/test/config/qBittorrent/BT_backupd11:active_timei1474e10:added_timei1645890736e10:allocation6:sparse15:apply_ip_filteri1e12:auto_managedi0e14:completed_timei1645890739e11:disable_dhti0e11:disable_lsdi0e11:disable_pexi0e19:download_rate_limiti-1e11:file-format22:libtorrent resume file12:file-versioni1e13:finished_timei1473e9:httpseedsle9:info-hash20:[¤“š©²) ­}7h˜·hÙ—£13:last_downloadi1645890739e18:last_seen_completei1645890739e11:last_uploadi0e18:libtorrent-version8:1.2.14.015:max_connectionsi100e11:max_uploadsi4e12:num_completei3e14:num_downloadedi16777215e14:num_incompletei0e6:pausedi1e5:peers12:PØÝmá¹Ë8üÉ6:peers60:6:pieces3:12:qBt-category5:ebook17:qBt-contentLayout8:Original26:qBt-firstLastPiecePriorityi0e8:qBt-name0:14:qBt-ratioLimiti-2000e12:qBt-savePath11:/downloads/14:qBt-seedStatusi1e20:qBt-seedingTimeLimiti-2e8:qBt-tagsl5:ebook6:activee9:save_path11:/downloads/9:seed_modei0e12:seeding_timei1473e19:sequential_downloadi0e10:share_modei0e15:stop_when_readyi0e13:super_seedingi0e16:total_downloadedi42310e14:total_uploadedi0e8:trackersll41:https://academictorrents.com/announce.phpel34:udp://tracker.coppersurfer.tk:6969el42:udp://tracker.opentrackr.org:1337/announceel44:udp://tracker.openbittorrent.com:80/announceee11:upload_modei0e17:upload_rate_limiti-1e8:url-listl93:https://web.archive.org/web/http://www.jmlr.org/papers/volume12/pedregosa11a/pedregosa11a.pdf65:http://www.jmlr.org/papers/volume12/pedregosa11a/pedregosa11a.pdfee5ba4939a00a9b21629a0ad7d376898b768d997a3.torrent000066400000000000000000000012331522034161300356320ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/test/config/qBittorrent/BT_backupd8:announce41:https://academictorrents.com/announce.php13:announce-listll41:https://academictorrents.com/announce.phpel34:udp://tracker.coppersurfer.tk:6969el42:udp://tracker.opentrackr.org:1337/announceel44:udp://tracker.openbittorrent.com:80/announceee10:created by23:py3createtorrent v0.9.513:creation datei1398649389e4:infod6:lengthi42310e4:name44:Scikit-learn: Machine Learning in Python.pdf12:piece lengthi16384e6:pieces60:lé3µÙM]d=ÅÆ"T[]�,*Æ«9Ë¢:­Ýho7Bd?vôCÉÑÙ©`Ƨ±ô%}ç�Ãh•°e8:url-listl65:http://www.jmlr.org/papers/volume12/pedregosa11a/pedregosa11a.pdf93:https://web.archive.org/web/http://www.jmlr.org/papers/volume12/pedregosa11a/pedregosa11a.pdfeeludviglundgren-qbittorrent-cli-4dbbde7/test/data/000077500000000000000000000000001522034161300223255ustar00rootroot000000000000001028296c5129f9291ae897a2234a74fab5122365.paxheader00006660000000000000000000000305152203416130020047xustar00rootroot00000000000000197 path=ludviglundgren-qbittorrent-cli-4dbbde7/test/data/MachineLearningforComputerSecurity (SpecialTopiconMachineLearningforComputerSecurity).pdf-3eced34cd948e7ea92f31ded3e0fd734274fee4a.torrent 1028296c5129f9291ae897a2234a74fab5122365.data000066400000000000000000000013651522034161300167140ustar00rootroot00000000000000d8:announce41:https://academictorrents.com/announce.php13:announce-listll41:https://academictorrents.com/announce.phpel34:udp://tracker.coppersurfer.tk:6969el42:udp://tracker.opentrackr.org:1337/announceel44:udp://tracker.openbittorrent.com:80/announceee10:created by23:py3createtorrent v0.9.513:creation datei1398649385e4:infod6:lengthi59935e4:name107:Machine Learning for Computer Security    (Special Topic on Machine Learning for Computer Security).pdf12:piece lengthi16384e6:pieces80:O]íŸlç;1 ÷j;N“¦K6h0ÙSiŒÿuWƒ>žÔ¸Ö–Ò!å äµÒÕ|�}Ã%¶}ø§žÔ¸Ö–Ò!å äµÒÕ|�}Ã%¶}ø§ÎÓLÙHçê’óí>×4'OîJ13:last_downloadi1614618322e18:last_seen_completei1614618322e11:last_uploadi0e18:libtorrent-version8:1.2.11.015:max_connectionsi16777215e11:max_uploadsi-1e12:num_completei3e14:num_downloadedi227e14:num_incompletei1e6:pausedi0e6:pieces4:9:save_path28:/home/user01/torrents/deluge9:seed_modei0e12:seeding_timei3420e19:sequential_downloadi0e10:share_modei0e15:stop_when_readyi0e13:super_seedingi0e16:total_downloadedi59935e14:total_uploadedi0e8:trackersll41:https://academictorrents.com/announce.phpel34:udp://tracker.coppersurfer.tk:6969el42:udp://tracker.opentrackr.org:1337/announceel44:udp://tracker.openbittorrent.com:80/announceee11:upload_modei0e17:upload_rate_limiti-1e8:url-listl96:https://web.archive.org/web/http://www.jmlr.org/papers/volume7/MLSEC-intro06a/MLSEC-intro06a.pdf68:http://www.jmlr.org/papers/volume7/MLSEC-intro06a/MLSEC-intro06a.pdfee40:5ba4939a00a9b21629a0ad7d376898b768d997a31225:d11:active_timei3643e10:added_timei1614618234e10:allocation6:sparse15:apply_ip_filteri1e12:auto_managedi1e14:completed_timei1614618236e11:disable_dhti0e11:disable_lsdi0e11:disable_pexi0e19:download_rate_limiti-1e11:file-format22:libtorrent resume file12:file-versioni1e13:finished_timei3641e9:httpseedsle9:info-hash20:[¤“š©²) ­}7h˜·hÙ—£13:last_downloadi1614618236e18:last_seen_completei1614618236e11:last_uploadi0e18:libtorrent-version8:1.2.11.015:max_connectionsi16777215e11:max_uploadsi-1e12:num_completei6e14:num_downloadedi3282e14:num_incompletei1e6:pausedi0e5:peers12:.‹Ì~|¹Ë8õ´6:peers60:6:pieces3:9:save_path28:/home/user01/torrents/deluge9:seed_modei0e12:seeding_timei3641e19:sequential_downloadi0e10:share_modei0e15:stop_when_readyi0e13:super_seedingi0e16:total_downloadedi42310e14:total_uploadedi0e8:trackersll41:https://academictorrents.com/announce.phpel34:udp://tracker.coppersurfer.tk:6969el42:udp://tracker.opentrackr.org:1337/announceel44:udp://tracker.openbittorrent.com:80/announceee11:upload_modei0e17:upload_rate_limiti-1e8:url-listl65:http://www.jmlr.org/papers/volume12/pedregosa11a/pedregosa11a.pdf93:https://web.archive.org/web/http://www.jmlr.org/papers/volume12/pedregosa11a/pedregosa11a.pdfeeeludviglundgren-qbittorrent-cli-4dbbde7/test/import/deluge/state/torrents.state000066400000000000000000000110011522034161300302260ustar00rootroot00000000000000€cdeluge.core.torrentmanager TorrentManagerState q)�q}qXtorrentsq]q(cdeluge.core.torrentmanager TorrentState q)�q}q(X torrent_idqX(5ba4939a00a9b21629a0ad7d376898b768d997a3q Xfilenameq Xtmpb2tckpbq.torrentq Xtrackersq ]q (}q(XurlqX)https://academictorrents.com/announce.phpqX trackeridqXqXtierqKX fail_limitqKXsourceqKXverifiedq‰XmessageqhX last_errorq}q(XvalueqKXcategoryqhuX next_announceqNX min_announceqNXscrape_incompleteqKXscrape_completeqKXscrape_downloadedq KXfailsq!KXupdatingq"‰X start_sentq#‰X complete_sentq$‰X endpointsq%]q&X send_statsq'‰u}q((Xurlq)X"udp://tracker.coppersurfer.tk:6969q*X trackeridq+hXtierq,KX fail_limitq-KXsourceq.KXverifiedq/‰Xmessageq0hX last_errorq1}q2(Xvalueq3KXcategoryq4huX next_announceq5NX min_announceq6NXscrape_incompleteq7KXscrape_completeq8KXscrape_downloadedq9KXfailsq:KXupdatingq;‰X start_sentq<‰X complete_sentq=‰X endpointsq>]q?X send_statsq@‰u}qA(XurlqBX*udp://tracker.opentrackr.org:1337/announceqCX trackeridqDhXtierqEKX fail_limitqFKXsourceqGKXverifiedqH‰XmessageqIhX last_errorqJ}qK(XvalueqLKXcategoryqMhuX next_announceqNNX min_announceqONXscrape_incompleteqPKXscrape_completeqQKXscrape_downloadedqRKXfailsqSKXupdatingqT‰X start_sentqU‰X complete_sentqV‰X endpointsqW]qXX send_statsqY‰u}qZ(Xurlq[X,udp://tracker.openbittorrent.com:80/announceq\X trackeridq]hXtierq^KX fail_limitq_KXsourceq`KXverifiedqa‰XmessageqbhX last_errorqc}qd(XvalueqeKXcategoryqfhuX next_announceqgNX min_announceqhNXscrape_incompleteqiKXscrape_completeqjKXscrape_downloadedqkKXfailsqlKXupdatingqm‰X start_sentqn‰X complete_sentqo‰X endpointsqp]qqX send_statsqr‰ueX storage_modeqsXsparseqtXpausedqu‰X save_pathqvX/home/user01/torrents/delugeqwXmax_connectionsqxJÿÿÿÿXmax_upload_slotsqyJÿÿÿÿXmax_upload_speedqzJÿÿÿÿXmax_download_speedq{JÿÿÿÿXprioritize_first_lastq|ˆXsequential_downloadq}‰Xfile_prioritiesq~]qKaXqueueq€JÿÿÿÿX auto_managedq�ˆX is_finishedq‚ˆX stop_ratioqƒG@X stop_at_ratioq„‰Xremove_at_ratioq…‰Xmove_completedq†‰Xmove_completed_pathq‡X/home/user01/DownloadsqˆXmagnetq‰NXownerqŠX localclientq‹XsharedqŒ‰X super_seedingq�‰XnameqŽhubh)�q�}q�(hX(3eced34cd948e7ea92f31ded3e0fd734274fee4aq‘h Xtmpyc3lrrhw.torrentq’h ]q“(}q”(Xurlq•X)https://academictorrents.com/announce.phpq–X trackeridq—hXtierq˜KX fail_limitq™KXsourceqšKXverifiedq›‰XmessageqœhX last_errorq�}qž(XvalueqŸKXcategoryq huX next_announceq¡NX min_announceq¢NXscrape_incompleteq£KXscrape_completeq¤KXscrape_downloadedq¥KXfailsq¦KXupdatingq§‰X start_sentq¨‰X complete_sentq©‰X endpointsqª]q«X send_statsq¬‰u}q­(Xurlq®X"udp://tracker.coppersurfer.tk:6969q¯X trackeridq°hXtierq±KX fail_limitq²KXsourceq³KXverifiedq´‰XmessageqµhX last_errorq¶}q·(Xvalueq¸KXcategoryq¹huX next_announceqºNX min_announceq»NXscrape_incompleteq¼KXscrape_completeq½KXscrape_downloadedq¾KXfailsq¿KXupdatingqÀ‰X start_sentqÁ‰X complete_sentq‰X endpointsqÃ]qÄX send_statsqʼnu}qÆ(XurlqÇX*udp://tracker.opentrackr.org:1337/announceqÈX trackeridqÉhXtierqÊKX fail_limitqËKXsourceqÌKXverifiedq͉XmessageqÎhX last_errorqÏ}qÐ(XvalueqÑKXcategoryqÒhuX next_announceqÓNX min_announceqÔNXscrape_incompleteqÕKXscrape_completeqÖKXscrape_downloadedq×KXfailsqØKXupdatingqÙ‰X start_sentqÚ‰X complete_sentqÛ‰X endpointsqÜ]qÝX send_statsqÞ‰u}qß(XurlqàX,udp://tracker.openbittorrent.com:80/announceqáX trackeridqâhXtierqãKX fail_limitqäKXsourceqåKXverifiedqæ‰XmessageqçhX last_errorqè}qé(XvalueqêKXcategoryqëhuX next_announceqìNX min_announceqíNXscrape_incompleteqîKXscrape_completeqïKXscrape_downloadedqðKXfailsqñKXupdatingqò‰X start_sentqó‰X complete_sentqô‰X endpointsqõ]qöX send_statsq÷‰uehsXsparseqøhu‰hvX/home/user01/torrents/delugeqùhxJÿÿÿÿhyJÿÿÿÿhzJÿÿÿÿh{Jÿÿÿÿh|ˆh}‰h~K…qúh€Jÿÿÿÿh�ˆh‚ˆhƒG@h„‰h…‰h†‰h‡X/home/user01/Downloadsqûh‰NhŠh‹hŒ‰h�‰hŽhubesb.ludviglundgren-qbittorrent-cli-4dbbde7/test/import/sessions/000077500000000000000000000000001522034161300245745ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/test/import/sessions/.gitkeep000066400000000000000000000000001522034161300262130ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/tools/000077500000000000000000000000001522034161300215755ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/tools/gen-docs/000077500000000000000000000000001522034161300232745ustar00rootroot00000000000000ludviglundgren-qbittorrent-cli-4dbbde7/tools/gen-docs/main.go000066400000000000000000000123431522034161300245520ustar00rootroot00000000000000// Command gen-docs generates Starlight-compatible markdown reference pages for // every qbt command using spf13/cobra's doc package. // // It is wired into `make docs` and verified in CI, so the committed docs under // docs/src/content/docs/commands always match the real command tree. // // Usage: // // go run ./tools/gen-docs [-out DIR] package main import ( "fmt" "log" "os" "path/filepath" "strings" "github.com/ludviglundgren/qbittorrent-cli/cmd" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" ) const defaultOutDir = "docs/src/content/docs/commands" func main() { log.SetFlags(0) outDir := defaultOutDir if len(os.Args) > 2 && os.Args[1] == "-out" { outDir = os.Args[2] } if err := run(outDir); err != nil { log.Fatalf("gen-docs: %v", err) } log.Printf("gen-docs: wrote command reference to %s", outDir) } func run(outDir string) error { rootCmd := cmd.NewRootCmd("dev", "none", "unknown") // Replace the auto-generated help command with a hidden one so it does not // show up as a "SEE ALSO" link to a page we never generate. rootCmd.SetHelpCommand(&cobra.Command{ Use: "no-help", Hidden: true, Run: func(*cobra.Command, []string) {}, }) // Disable the "Auto generated by spf13/cobra on " footer on every // command. The date would otherwise change on every run and break the CI // staleness check. disableAutoGenTag(rootCmd) // Map each generated filename to the command's short description so the // filePrepender (which only receives the filename) can emit it as // frontmatter. descriptions := map[string]string{} collectDescriptions(rootCmd, descriptions) if err := os.MkdirAll(outDir, 0o755); err != nil { return fmt.Errorf("create output dir: %w", err) } // Remove previously generated pages so renamed or removed commands do not // leave orphaned files behind. stale, err := filepath.Glob(filepath.Join(outDir, "*.md")) if err != nil { return fmt.Errorf("list stale docs: %w", err) } for _, f := range stale { if err := os.Remove(f); err != nil { return fmt.Errorf("remove stale doc %s: %w", f, err) } } filePrepender := func(filename string) string { base := filepath.Base(filename) name := strings.TrimSuffix(base, filepath.Ext(base)) // qbt_torrent_add title := strings.ReplaceAll(name, "_", " ") // qbt torrent add var b strings.Builder b.WriteString("---\n") b.WriteString("title: " + yamlString(title) + "\n") if desc := descriptions[base]; desc != "" { b.WriteString("description: " + yamlString(desc) + "\n") } // These pages are generated; hide the "Edit this page" link. b.WriteString("editUrl: false\n") b.WriteString("---\n\n") return b.String() } // cobra emits cross-links as "qbt_torrent_add.md". Every page lives in the // same directory and is served by Starlight at ".../commands//", so a // sibling page is one level up: "../qbt_torrent_add/". linkHandler := func(name string) string { return "../" + strings.TrimSuffix(name, ".md") + "/" } if err := doc.GenMarkdownTreeCustom(rootCmd, outDir, filePrepender, linkHandler); err != nil { return err } // Drop cobra's leading "## " heading from each page; Starlight // already renders the frontmatter title as the page heading, so it would be a // duplicate. The short description line below it is kept as the lead. generated, err := filepath.Glob(filepath.Join(outDir, "*.md")) if err != nil { return fmt.Errorf("list generated docs: %w", err) } for _, f := range generated { if err := stripCommandHeading(f); err != nil { return fmt.Errorf("post-process %s: %w", f, err) } } return nil } // stripCommandHeading removes the redundant "## " heading that // cobra writes at the top of the page body, leaving the frontmatter and the // rest of the content (including the short description lead) intact. func stripCommandHeading(path string) error { raw, err := os.ReadFile(path) if err != nil { return err } content := string(raw) const fence = "---\n" if !strings.HasPrefix(content, fence) { return nil } rel := strings.Index(content[len(fence):], "\n"+fence) if rel == -1 { return nil } headerEnd := len(fence) + rel + len("\n"+fence) header := content[:headerEnd] body := strings.TrimLeft(content[headerEnd:], "\n") if strings.HasPrefix(body, "## ") { if nl := strings.IndexByte(body, '\n'); nl != -1 { body = strings.TrimLeft(body[nl+1:], "\n") } } return os.WriteFile(path, []byte(header+"\n"+body), 0o644) } // disableAutoGenTag turns off the cobra auto-gen footer for cmd and all of its // descendants. func disableAutoGenTag(cmd *cobra.Command) { cmd.DisableAutoGenTag = true for _, c := range cmd.Commands() { disableAutoGenTag(c) } } // collectDescriptions walks the command tree and records each command's short // description keyed by the markdown filename cobra will generate for it. func collectDescriptions(cmd *cobra.Command, out map[string]string) { base := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".md" out[base] = cmd.Short for _, c := range cmd.Commands() { collectDescriptions(c, out) } } // yamlString quotes a value for safe use in YAML frontmatter. func yamlString(s string) string { s = strings.ReplaceAll(s, `\`, `\\`) s = strings.ReplaceAll(s, `"`, `\"`) return `"` + s + `"` }