[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: nrjdalal\n"
  },
  {
    "path": ".github/workflows/dependencies.yml",
    "content": "name: Update Dependencies\n\non:\n  schedule:\n    - cron: \"0 0 * * *\"\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  update:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Update Dependencies\n        run: |\n          git config user.name 'github-actions[bot]'\n          git config user.email 'github-actions[bot]@users.noreply.github.com'\n          git pull origin main\n          git checkout -B update-dependencies\n          bun update\n          bun i\n          git add package.json bun.lock\n          git commit -m \"chore(deps): update dependencies\" || true\n          git push -f origin update-dependencies\n          curl -sL -X POST -H \"Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}\"  \"https://api.github.com/repos/$GITHUB_REPOSITORY/pulls\" \\\n            -d \"{\\\"title\\\": \\\"chore(deps): update dependencies\\\", \\\"head\\\": \\\"update-dependencies\\\", \\\"base\\\": \\\"main\\\", \\\"body\\\": \\\"Automatically generated PR to update dependencies.\\\"}\"\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release Package\n\non:\n  push:\n    branches:\n      - \"**\"\n  issue_comment:\n    types: [created]\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\npermissions:\n  id-token: write\n  contents: write\n  statuses: write\n  pull-requests: write\n\njobs:\n  test:\n    if: github.event_name == 'push'\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v6\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Install Dependencies\n        run: bun install --frozen-lockfile\n\n      - name: Test\n        run: bun run test\n\n  release:\n    if: |\n      always() && !failure() && !cancelled() && (\n        github.event_name == 'push' ||\n        github.event_name == 'issue_comment' && github.event.comment.user.login == github.repository_owner && contains(github.event.comment.body, 'release')\n      )\n    needs: [test]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Setup Node\n        uses: actions/setup-node@v6\n        with:\n          node-version: lts/*\n          registry-url: https://registry.npmjs.org\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Release Package\n        run: |\n          PACKAGE_NAME=$(bunx json -f package.json -a name)\n          npm view \"$PACKAGE_NAME\" 2>/dev/null || { echo \"$PACKAGE_NAME does not exist in the npm registry. Skipping publish.\"; exit 0; }\n          PACKAGE_VERSION=$(bunx json -f package.json -a version)\n          VERSIONS=$(npm view $PACKAGE_NAME dist-tags --json)\n          LATEST_VERSION=$(echo $VERSIONS | bunx json latest)\n          if [[ $GITHUB_EVENT_NAME == 'issue_comment' ]]; then\n            PR_NUMBER=$(echo \"${{ github.event.issue.pull_request.url }}\" | grep -o '[0-9]*$')\n            LATEST_COMMIT_SHA=$(curl -fsSL -H \"Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}\" \"https://api.github.com/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/commits\" | bunx json -a sha | tail -n 1)\n            git checkout $LATEST_COMMIT_SHA\n            TAG=\"test\"\n            RELEASE_VERSION=\"0.0.0-${LATEST_COMMIT_SHA:0:7}\"\n          elif [[ $PACKAGE_VERSION != $LATEST_VERSION ]]; then\n            RELEASE_VERSION=$PACKAGE_VERSION\n            HAS_TAG=$(echo $PACKAGE_VERSION | grep -o '[a-zA-Z]*' | head -n 1)\n            TAG=$([[ -n \"$HAS_TAG\" ]] && echo $HAS_TAG || echo \"latest\")\n          else\n            TAG=\"canary\"\n            RELEASE_VERSION=$(bunx semver $LATEST_VERSION -i minor)\n            TAGGED_VERSION=$(echo $VERSIONS | bunx json $TAG)\n            RELEASE_VERSION=$([[ $TAGGED_VERSION == $RELEASE_VERSION* ]] && bunx semver $TAGGED_VERSION -i prerelease || echo $RELEASE_VERSION-$TAG.0)\n          fi\n\n          bunx json -I -f package.json -e \"this.version=\\\"$RELEASE_VERSION\\\"\"\n          bun install --frozen-lockfile\n          bun run build\n          bunx json -I -f package.json -e 'delete this.scripts'\n          bunx json -I -f package.json -e 'delete this.commitlint'\n          bunx json -I -f package.json -e 'delete this.devDependencies'\n          npm publish --provenance --access public --no-git-checks --tag $TAG\n\n          PACKAGE_URL=\"https://www.npmjs.com/package/$PACKAGE_NAME/v/$RELEASE_VERSION\"\n          if [[ $GITHUB_EVENT_NAME == 'issue_comment' ]]; then\n            curl -fsSL -X DELETE -H \"Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}\" \"https://api.github.com/repos/$GITHUB_REPOSITORY/issues/comments/${{ github.event.comment.id }}\" >/dev/null \\\n              && echo \"🟢 Releasing comment deleted!\" || echo \"🔴 Failed to delete releasing comment.\"\n            curl -fsSL -X POST -H \"Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}\" \"https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" \\\n              -d \"{\\\"body\\\": \\\"Test package released - [\\`$PACKAGE_NAME@$RELEASE_VERSION\\`]($PACKAGE_URL)\\\"}\" >/dev/null \\\n              && echo \"🟢 Release comment to PR added!\" || echo \"🔴 Failed to add release comment to PR.\"\n          else\n            curl -fsSL -X POST -H \"Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}\" \"https://api.github.com/repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/comments\" \\\n              -d \"{\\\"body\\\": \\\"Package released - [\\`$PACKAGE_NAME@$RELEASE_VERSION\\`]($PACKAGE_URL)\\\"}\" >/dev/null \\\n              && echo \"🟢 Release comment added!\" || echo \"🔴 Failed to add release comment.\"\n            curl -fsSL -X POST -H \"Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}\" \"https://api.github.com/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA\" \\\n              -d \"{\\\"state\\\": \\\"success\\\", \\\"context\\\": \\\"Package released\\\", \\\"description\\\": \\\"$PACKAGE_NAME@$RELEASE_VERSION\\\", \\\"target_url\\\": \\\"$PACKAGE_URL\\\"}\" >/dev/null \\\n              && echo \"🟢 Release status updated!\" || echo \"🔴 Failed to update release status.\"\n          fi\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  push:\n    branches:\n      - \"**\"\n\njobs:\n  test:\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v6\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Install Dependencies\n        run: bun install --frozen-lockfile\n\n      - name: Test\n        run: bun run test\n"
  },
  {
    "path": ".gitignore",
    "content": "# os\n.DS_Store\n*.pem\n\n# logs\n/*-debug.log*\n/*-error.log*\n\n# lockfiles\nyarn.lock\npackage-lock.json\npnpm-lock.yaml\nbun.lockb\n\n# npm\nnode_modules/\n\n# build\ndist/\n\n# custom\n.test-artifacts/\n"
  },
  {
    "path": ".lintstagedrc.json",
    "content": "{\n  \"*\": [\"oxfmt --no-error-on-unmatched-pattern\", \"oxlint\"]\n}\n"
  },
  {
    "path": ".oxfmtrc.jsonc",
    "content": "{\n  \"$schema\": \"./node_modules/oxfmt/configuration_schema.json\",\n  \"semi\": false,\n  \"experimentalSortImports\": {},\n}\n"
  },
  {
    "path": ".oxlintrc.jsonc",
    "content": "{\n  \"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## v5.4.0 (2026-03-22)\n\n- **Local directory interactive mode** - browse local directories with `gitpick -i`\n  - `gitpick -i` browses cwd\n  - `gitpick -i target` browses cwd, copies selected to target\n  - `gitpick ./path -i target` browses a specific path\n- Uses `git ls-files` to respect `.gitignore` when in a git repo\n- Falls back to manual walk (skipping `.git` only) outside git repos\n- Preserves symlinks when copying (uses `lstat` + `symlink`)\n- Warns on symlink copy failures instead of swallowing silently\n- Errors on target-inside-source and missing source with explicit target\n\n## v5.3.0 (2026-03-22)\n\n- **File preview** - press enter on a file to view its content with line numbers and cursor navigation\n- **Syntax highlighting** - 38 languages via vendored `@speed-highlight/core` (zero runtime deps)\n- File sizes shown right-aligned in tree view (files and folders)\n- Full file path shown in preview header\n- Enter on symlink-to-folder jumps cursor to target and expands ancestors\n- Relative symlink paths resolved correctly for preview and selection\n- Ctrl-C works in preview mode\n- Terminal resize handled correctly in preview mode\n- ANSI-aware line truncation prevents color bleed\n\n## v5.2.0 (2026-03-22)\n\n- Symlinks counted separately and shown in yellow in footer\n- Selecting a symlink also selects its target file/folder\n- Deselecting all children auto-deselects parent folder\n- Press `.` toggles select all from anywhere\n- Show \"press . to select all\" when nothing selected, \"all selected\" with size when everything selected\n- Add interactive mode screenshot to README\n\n## v5.1.0 (2026-03-22)\n\n- Show symlinks in interactive picker (yellow with `->` target, matching dry-run tree)\n- Show \"all selected\" status when everything is selected\n- Scope interactive picker to subpath when using tree URLs (e.g. `owner/repo/tree/main/src -i`)\n- Re-render interactive picker on terminal resize\n\n## v5.0.0 (2026-03-22)\n\n- **Interactive mode (`-i` / `--interactive`)** - browse any repo's file tree in your terminal and cherry-pick exactly the files and folders you want\n  - Hierarchical tree view with expand/collapse, multi-select, select all\n  - Keyboard navigation: arrow keys, `j`/`k`, `h`/`l`, space, enter, `c` to confirm, `q` to quit\n  - Shows selection stats: folder/file count, total size, scroll position\n  - Dark gray background highlight on current line, `●`/`○` selection indicators\n  - Auto-expands all levels for small repos (<=30 entries), 2 levels for larger ones\n  - Respects `--dry-run`, `--overwrite`, `--recursive`, `--tree`\n  - Graceful TTY guard, SIGINT cleanup, alternate screen buffer\n  - Works with GitHub, GitLab, Bitbucket, public and private repos\n  - Zero new dependencies\n- Add interactive mode tests (TTY guard, help text)\n- Update README with interactive mode docs, table of contents, controls reference\n\n## v4.19.0 (2026-03-22)\n\n- **Environment variable token support** — `GITHUB_TOKEN`/`GH_TOKEN`, `GITLAB_TOKEN`, `BITBUCKET_TOKEN` auto-detected for private repos without embedding tokens in URLs\n- **SIGINT/SIGTERM temp dir cleanup** — signal handlers clean up active temp directories on process kill\n- **Non-TTY spinner suppression** — spinner animation completely suppressed in CI/piped output\n- **`--verbose` now includes stats** — file count with size, network/copy/total time breakdown\n- Non-blocking update notifier — checks npm registry in background, shows notice on next run\n\n## v4.18.0 (2026-03-22)\n\n- **Add `--quiet` / `-q` flag** — suppress all output except errors, ideal for CI pipelines and scripts\n- **Add `--verbose` flag** — show detailed clone info: strategy (shallow/full), source URL, target path, file count, duration\n- Update banner to two lines for readability\n- Add `dim` color formatter\n- Reorder README features list\n\n## v4.17.0 (2026-03-22)\n\n- **Add `--tree` flag** — display cloned file structure as a colored tree (like the `tree` command)\n  - Bold cyan for root directory, cyan for subdirs, yellow for symlinks, cyan/white for symlink targets\n  - Smart path header: `./` for cwd-relative, `~/` for home-relative, absolute otherwise\n  - Works with `--dry-run` (clones to temp dir, prints tree, cleans up)\n  - Consistent output for repos, folders, and blobs\n- `copyDir` now returns list of copied file paths\n- `cloneAction` returns `CloneResult` with files and duration\n- Fix oxlint warnings in test suite\n- Update package sizes in README\n\n## v4.16.2 (2026-03-21)\n\n- Add CLI help examples for `--dry-run`, GitLab and Bitbucket\n- Update README with package sizes, multi-host config examples, and verified adopters\n\n## v4.16.1 (2026-03-21)\n\n- Add `--dry-run`, GitLab and Bitbucket examples to CLI help output\n- Update README with package sizes and multi-host config examples\n\n## v4.16.0 (2026-03-21)\n\n- **Multi-host support** — clone from GitLab and Bitbucket in addition to GitHub\n- Add `--dry-run` / `-n` flag to preview what would be cloned without cloning\n- Add `/commit/` URL support — `owner/repo/commit/SHA` correctly extracts the commit SHA\n- Add `refs/remotes` and `refs/tags` support for raw URLs (in addition to `refs/heads`)\n- Preserve shorthand raw URL parsing (`owner/repo/refs/heads/branch/file`)\n- Migrate test suite from bash scripts to bun:test (106 tests across dry-run, clone, config, integrity)\n- Gate releases behind passing tests in CI\n- Fix `pull_request.url` reference in release workflow\n\n## v4.15.0 (2026-03-21)\n\n- **34KB → 19KB (43% smaller), zero dependencies**\n- Add `.gitpick.json` / `.gitpick.jsonc` config file support — pick multiple files, folders, branches in one command\n- Internalize all external dependencies (terminal-link, yocto-spinner, yoctocolors, nano-spawn, strip-json-comments)\n- Add symlink support in `copyDir`\n- Fix Windows blob/tree path handling (split on both `/` and `\\`, use `path.dirname`)\n- Add cross-platform CI (ubuntu, macos, windows)\n- Add PowerShell test suite for Windows backslash paths\n- Migrate from prettier to oxfmt, simple-git-hooks to lefthook, add oxlint\n- Move tests to `tests/` dir with cross-platform `tree.mjs` for file tree output\n\n## v4.14.0 (2026-03-21)\n\n- Add symlink support — `copyDir` now preserves symlinks instead of failing or following them\n- Migrate from prettier to oxfmt/oxlint\n- Migrate from simple-git-hooks to lefthook\n- Clean up release workflow\n\n## v4.13.0 (2026-03-21)\n\n- Update build tooling dependencies\n- Update package size in readme\n- Add package runner support in test script\n\n## v4.12.4 (2026-03-15)\n\n- Update dependency @types/node to v25\n- Update build tooling dependencies\n- Fix typo in related projects section\n\n## v4.12.3 (2025-10-22)\n\n- Documentation updates\n\n## v4.12.2 (2025-09-19)\n\n- Fix Twitter badge link in README\n\n## v4.12.1 (2025-09-19)\n\n- Documentation and package.json updates\n\n## v4.12.0 (2025-08-26)\n\n- Update dependencies\n\n## v4.11.0 – v4.11.3 (2025-05-07 – 2025-06-02)\n\n- Dependency updates\n- Documentation improvements\n\n## v4.10.0 – v4.10.2 (2025-04-06 – 2025-04-30)\n\n- Internal improvements\n- Dependency updates\n\n## v4.9.0 (2025-04-06)\n\n- Update external dependencies\n- Documentation improvements\n\n## v4.8.0 – v4.8.1 (2025-04-04)\n\n- CLI and documentation improvements\n\n## v4.7.0 – v4.7.1 (2025-04-04)\n\n- CLI improvements\n- Clone action refinements\n\n## v4.6.0 – v4.6.1 (2025-04-04)\n\n- Refactor clone action, URL transform, and time parsing utilities\n- CI workflow updates\n\n## v4.5.0 – v4.5.3 (2025-04-03)\n\n- External dependency management improvements\n- CLI refinements\n\n## v4.4.0 – v4.4.3 (2025-04-03)\n\n- Clone action improvements\n\n## v4.3.0 – v4.3.2 (2025-04-03)\n\n- Extend commit clone capability\n- External dependency updates\n\n## v4.2.0 – v4.2.1 (2025-04-03)\n\n- Major internal refactor\n- Test infrastructure improvements\n\n## v4.1.0 – v4.1.1 (2025-04-02)\n\n- Dependency updates\n- CI improvements\n\n## v4.0.0 (2025-04-02)\n\n- Major rewrite — new architecture with external vendored dependencies\n- Zero runtime dependencies\n- Faster cloning with shallow clone + sparse checkout\n- Support for shorthands (`owner/repo`), full URLs, and SSH URLs\n- Clone repos, trees, blobs, branches, commits, and raw content\n- Watch mode (`--watch`) for continuous syncing\n- Private repo support via PAT tokens\n- Windows long path support\n\n## v3.17.0 – v3.26.0 (2025-03-29 – 2025-03-30)\n\n- Fix Win32 long paths support (#6)\n- Logging improvements\n- Switch from inquirer/ora to clack\n- Add overwrite alias (`-o` for `--force`)\n- Documentation updates\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Neeraj Dalal\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# GitPick\n\n<!--  -->\n\n**Clone exactly what you need aka straightforward project scaffolding!**\n\n[![Twitter](https://img.shields.io/twitter/follow/nrjdalal_dev?label=%40nrjdalal_dev)](https://twitter.com/nrjdalal_dev)\n[![npm](https://img.shields.io/npm/v/gitpick?color=red&logo=npm)](https://www.npmjs.com/package/gitpick)\n[![downloads](https://img.shields.io/npm/dt/gitpick?color=red&logo=npm)](https://www.npmjs.com/package/gitpick)\n[![stars](https://img.shields.io/github/stars/nrjdalal/gitpick?color=blue)](https://github.com/nrjdalal/gitpick)\n\n📦 `Zero dependencies` / `Un/packed (~67/25kb)` / `Faster and more features` yet drop-in replacement for `degit`\n\n> #### Just `copy-and-paste` any GitHub, GitLab, Bitbucket or Codeberg URL - no editing required (shorthands work too) - to clone individual files, folders, branches, commits, raw content or even entire repositories without the `.git` directory.\n\nUnlike other tools that force you to tweak URLs or follow strict formats to clone files, folders, branches or commits GitPick works seamlessly with any URL.\n\n**You can also try [Interactive Mode](#-interactive-mode)**. Browse any repo right in your terminal. See every file, pick what you want, skip what you don't. Just `gitpick owner/repo -i` and you're in. No more guessing paths.\n\n<img width=\"400\" alt=\"GitPick Meme\" src=\"https://github.com/user-attachments/assets/180c3e5b-320c-48d7-aaf9-a7402e74c882\" />\n\n---\n\n### Table of Contents\n\n- [Some Examples](#-some-examples)\n- [Features](#-features)\n- [Quick Usage](#-quick-usage)\n- [Options](#-options)\n- [Interactive Mode](#-interactive-mode)\n- [Private Repos](#-private-repos)\n- [Config File](#-config-file)\n- [Install Globally](#-install-globally-optional)\n- [Used By](#-used-by)\n- [Related Projects](#-related-projects)\n- [Contributing](#-contributing)\n\n---\n\n## 📖 Some Examples\n\n### See [Quick Usage](#-quick-usage) for to learn more.\n\n```sh\n# interactive mode - browse and pick files/folders\nnpx gitpick owner/repo -i\nnpx gitpick https://github.com/owner/repo -i\n# clone a repo without .git\nnpx gitpick owner/repo\nnpx gitpick https://github.com/owner/repo\n# clone a folder aka tree\nnpx gitpick owner/repo/tree/main/path/to/folder\nnpx gitpick https://github.com/owner/repo/tree/main/path/to/folder\n# clone a file aka blob\nnpx gitpick owner/repo/blob/main/path/to/file\nnpx gitpick https://github.com/owner/repo/blob/main/path/to/file\n# clone a branch\nnpx gitpick owner/repo -b canary\nnpx gitpick https://github.com/owner/repo -b canary\nnpx gitpick owner/repo/tree/canary\nnpx gitpick https://github.com/owner/repo/tree/canary\n# clone a commit SHA\nnpx gitpick owner/repo -b cc8e93\nnpx gitpick https://github.com/owner/repo/commit/cc8e93\n# clone submodules\nnpx gitpick owner/repo -r\nnpx gitpick https://github.com/owner/repo -r\n# clone a private repo\nnpx gitpick https://<token>@github.com/owner/repo\n# clone from GitLab\nnpx gitpick https://gitlab.com/owner/repo\nnpx gitpick https://gitlab.com/owner/repo/-/tree/main/path/to/folder\n# clone from Bitbucket\nnpx gitpick https://bitbucket.org/owner/repo\nnpx gitpick https://bitbucket.org/owner/repo/src/main/path/to/folder\n# clone from Codeberg\nnpx gitpick https://codeberg.org/owner/repo\nnpx gitpick https://codeberg.org/owner/repo/src/branch/main/path/to/folder\n# dry run (preview without cloning)\nnpx gitpick owner/repo --dry-run\nnpx gitpick owner/repo -i --dry-run\n```\n\n---\n\n## ✨ Features\n\n- 🔍 Clone individual files or folders from GitHub, GitLab, Bitbucket and Codeberg\n- 🧠 Use shorthands `TanStack/router` or full URL's `https://github.com/TanStack/router`\n- ⚙️ Auto-detects branches and target directory (if not specified) like `git clone`\n- **🔥 Interactive mode** - browse and cherry-pick files/folders with `-i` | `--interactive`\n- 🔐 Seamlessly works with both public and private repositories using a PAT\n- 📦 Can easily clone all submodules with `-r` | `--recursive`\n- 🔎 Preview what would be cloned with `--dry-run` before cloning\n- 🌳 View cloned file structure as a colored tree with `--tree`\n- 🗑️ Overwrite or replace existing files without a prompt using `-o` | `--overwrite`\n- 🔁 Sync changes remotely with `--watch` using intervals (e.g., `15s`, `1m`, `1h`)\n- 🔇 Silent mode with `--quiet` for CI pipelines, debug mode with `--verbose`\n- 📋 Config file support (`.gitpick.json` / `.gitpick.jsonc`) for multi-path picks\n\n---\n\n## 🚀 Quick Usage\n\n```sh\nnpx gitpick <url/shorthand> [target] [options]\n```\n\n- [target] and [options] are optional, if not specified, GitPick fallbacks to the default behavior of `git clone`\n\nExamples:\n\n```sh\nnpx gitpick https://github.com/owner/repo           # repo without .git\nnpx gitpick owner/repo/tree/main/path/to/folder     # a folder aka tree\nnpx gitpick owner/repo/blob/main/path/to/file       # a file aka blob\n\nnpx gitpick <url/shorthand>                         # default git behavior\nnpx gitpick <url/shorthand> [target]                # with optional target\nnpx gitpick <url/shorthand> -b [branch/SHA]         # branch or commit SHA\nnpx gitpick <url/shorthand> -o                      # overwrite if exists\nnpx gitpick <url/shorthand> -r                      # clone submodules\nnpx gitpick <url/shorthand> -w 30s                  # sync every 30 seconds\nnpx gitpick <url/shorthand> --dry-run               # preview without cloning\nnpx gitpick https://<token>@github.com/owner/repo   # private repository\nnpx gitpick https://gitlab.com/owner/repo           # GitLab\nnpx gitpick https://bitbucket.org/owner/repo        # Bitbucket\nnpx gitpick https://codeberg.org/owner/repo         # Codeberg\n```\n\n<img width=\"720\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/ddbc41b4-bfc6-4287-bb85-eb949d723591\" />\n\n---\n\n## 🔧 Options\n\n```\n-b, --branch       Branch/SHA to clone\n-i, --interactive  Browse and pick files/folders interactively\n-n, --dry-run      Show what would be cloned without cloning\n-o, --overwrite    Skip overwrite prompt\n-r, --recursive    Clone submodules\n-w, --watch [time] Watch the repository and sync every [time]\n                   (e.g. 1h, 30m, 15s)\n    --tree         List copied files as a tree\n-q, --quiet        Suppress all output except errors\n    --verbose      Show detailed clone information\n-h, --help         display help for command\n-v, --version      display the version number\n```\n\n---\n\n## 🔥 Interactive Mode\n\n> **New in v5.0.** Browse any repository's file tree in your terminal and cherry-pick exactly the files and folders you want.\n\n```sh\nnpx gitpick owner/repo -i\nnpx gitpick owner/repo -i -b canary\nnpx gitpick https://github.com/owner/repo -i\nnpx gitpick https://gitlab.com/owner/repo -i\nnpx gitpick https://codeberg.org/owner/repo -i\n```\n\n<img width=\"720\" alt=\"Interactive Mode\" src=\"https://github.com/user-attachments/assets/9d6f4db7-ed84-4783-b815-0267719b3a52\" />\n\nNavigate with arrow keys, select with space, expand/collapse with enter, `.` to select all, `c` to confirm. Works with GitHub, GitLab, Bitbucket, Codeberg, public and private repos.\n\n---\n\n## 🔐 Private Repos\n\nUse a personal access token with read-only contents permission. Works with GitHub, GitLab, Bitbucket and Codeberg:\n\n```sh\nnpx gitpick https://<token>@github.com/owner/repo\nnpx gitpick https://<token>@gitlab.com/owner/repo\nnpx gitpick https://<token>@bitbucket.org/owner/repo\nnpx gitpick https://<token>@codeberg.org/owner/repo\n```\n\nOr use environment variables (recommended for CI):\n\n```sh\nexport GITHUB_TOKEN=ghp_xxxx    # or GH_TOKEN\nexport GITLAB_TOKEN=glpat-xxxx\nexport BITBUCKET_TOKEN=xxxx\nexport CODEBERG_TOKEN=xxxx\n\nnpx gitpick owner/private-repo  # token is picked up automatically\n```\n\nCreate a GitHub token 👉 [here](https://github.com/settings/personal-access-tokens/new) with `repo -> contents: read-only` permission.\n\n---\n\n## 📋 Config File\n\nCreate a `.gitpick.json` or `.gitpick.jsonc` in your project to pick multiple files/folders in one command:\n\n```jsonc\n// .gitpick.jsonc\n[\n  // clone a repo without .git\n  \"owner/repo\",\n  \"https://github.com/owner/repo\",\n  // clone a folder aka tree\n  \"owner/repo/tree/main/path/to/folder\",\n  \"https://github.com/owner/repo/tree/main/path/to/folder\",\n  // clone a file aka blob\n  \"owner/repo/blob/main/path/to/file\",\n  \"https://github.com/owner/repo/blob/main/path/to/file\",\n  // clone a branch\n  \"owner/repo -b canary\",\n  \"https://github.com/owner/repo -b canary\",\n  \"owner/repo/tree/canary\",\n  \"https://github.com/owner/repo/tree/canary\",\n  // clone a commit SHA\n  \"owner/repo -b cc8e93\",\n  \"https://github.com/owner/repo/commit/cc8e93\",\n  // clone submodules\n  \"owner/repo -r\",\n  \"https://github.com/owner/repo -r\",\n  // clone a private repo\n  \"https://<token>@github.com/owner/repo\",\n  // GitLab\n  \"https://gitlab.com/owner/repo\",\n  \"https://gitlab.com/owner/repo/-/tree/main/path/to/folder\",\n  // Bitbucket\n  \"https://bitbucket.org/owner/repo\",\n  \"https://bitbucket.org/owner/repo/src/main/path/to/folder\",\n  // Codeberg\n  \"https://codeberg.org/owner/repo\",\n  \"https://codeberg.org/owner/repo/src/branch/main/path/to/folder\",\n]\n```\n\nThen just run:\n\n```sh\nnpx gitpick\n```\n\nEach entry follows the same `<url> [target]` syntax as the CLI. All entries are cloned with `-o` (overwrite) by default.\n\n---\n\n## 📦 Install Globally (Optional)\n\n```sh\nnpm install -g gitpick\ngitpick <url/shorthand> [target] [options]\n```\n\n---\n\n## 🌍 Used By\n\n- **Major:** [Storybook](https://github.com/storybookjs/storybook), [TanStack Router](https://github.com/TanStack/router), [ElectricSQL](https://github.com/electric-sql/electric), [Alchemy](https://github.com/alchemy-run/alchemy), [Porto](https://github.com/ithacaxyz/porto), [oidc-spa](https://github.com/keycloakify/oidc-spa), [Fidely UI](https://github.com/fidely-ui/fidely-ui)\n- **Other:** [hono-better-auth](https://github.com/LovelessCodes/hono-better-auth), [vite-hono-ssr](https://github.com/Mirza-Glitch/vite-hono-ssr), [tanstack-start-cf](https://github.com/depsimon/tanstack-start-cf), [constructa-starter-min](https://github.com/instructa/constructa-starter-min), [tanstack-starter](https://github.com/enesdir/tanstack-starter), [react-shadcn-starter](https://github.com/aliadelelroby/react-shadcn-starter), [open-store](https://github.com/bang0711/open-store)\n\n---\n\n## 🛠 More Tools\n\nCheck out more projects at [github.com/nrjdalal](https://github.com/nrjdalal)\n\n## 🔗 Related Projects\n\n- [tiged](https://github.com/tiged/tiged) - community driven fork of degit\n- [giget](https://github.com/unjs/giget) - alternative approach\n\n[![Star History Chart](https://api.star-history.com/svg?repos=nrjdalal/gitpick,tiged/tiged,unjs/giget&type=timeline&logscale&legend=top-left)](https://www.star-history.com/#nrjdalal/gitpick&tiged/tiged&unjs/giget&type=timeline&logscale&legend=top-left)\n\n## 🤝 Contributing\n\nContributions welcome - any help is appreciated!\n\n- Fork the repo and create a branch (use descriptive names, e.g. feat/<name> or fix/<name>).\n- Make your changes, add tests if applicable, and run the checks:\n  - bun install\n  - bun test\n- Follow the existing code style and commit message conventions (use conventional commits: feat, fix, docs, chore).\n- Open a PR describing the change, motivation, and any migration notes; link related issues.\n- For breaking changes or large features, open an issue first to discuss the approach.\n- By contributing you agree to the MIT license and the project's Code of Conduct.\n\nThank you for helping improve GitPick!\n\n## 📄 License\n\nMIT – [LICENSE](LICENSE)\n"
  },
  {
    "path": "bin/external/nano-spawn.ts",
    "content": "// Trimmed from nano-spawn by Sindre Sorhus (https://github.com/sindresorhus/nano-spawn)\nimport { spawn as nodeSpawn, type SpawnOptions } from \"node:child_process\"\nimport { once } from \"node:events\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\nimport { fileURLToPath } from \"node:url\"\nimport { stripVTControlCharacters } from \"node:util\"\n\nclass SubprocessError extends Error {\n  name = \"SubprocessError\"\n  stdout = \"\"\n  stderr = \"\"\n  exitCode?: number\n}\n\nconst exeExtensions = [\".exe\", \".com\"]\n\nconst EXE_MEMO: Record<string, Promise<boolean>> = {}\nconst memoize =\n  (fn: (...args: string[]) => Promise<boolean>) =>\n  (...args: string[]) =>\n    (EXE_MEMO[args.join(\"\\0\")] ??= fn(...args))\n\nconst access = memoize(async (...args: string[]) => {\n  try {\n    await fs.access(args[0])\n    return true\n  } catch {\n    return false\n  }\n})\n\nconst mIsExe = memoize(async (file: string, cwd: string, PATH: string) => {\n  const parts = PATH.split(path.delimiter)\n    .filter(Boolean)\n    .map((part) => part.replace(/^\"(.*)\"$/, \"$1\"))\n  try {\n    await Promise.any(\n      [cwd, ...parts].flatMap((part) =>\n        exeExtensions.map((ext) => access(`${path.resolve(part, file)}${ext}`)),\n      ),\n    )\n  } catch {\n    return false\n  }\n  return true\n})\n\nconst shouldForceShell = async (file: string, options: SpawnOptions): Promise<boolean> =>\n  process.platform === \"win32\" &&\n  !options.shell &&\n  !exeExtensions.some((ext) => file.toLowerCase().endsWith(ext)) &&\n  !(await mIsExe(\n    file,\n    (options.cwd as string) ?? \".\",\n    (process.env.PATH || process.env.Path) ?? \"\",\n  ))\n\nconst escapeFile = (file: string) => file.replaceAll(/([()\\][%!^\"`<>&|;, *?])/g, \"^$1\")\nconst escapeArgument = (arg: string) =>\n  escapeFile(escapeFile(`\"${arg.replaceAll(/(\\\\*)\"/g, '$1$1\\\\\"').replace(/(\\\\*)$/, \"$1$1\")}\"`))\n\nconst getCommandPart = (part: string) =>\n  /[^\\w./-]/.test(part) ? `'${part.replaceAll(\"'\", \"'\\\\''\")}'` : part\n\nexport default async function spawn(\n  file: string,\n  args: string[] = [],\n  options: Record<string, any> = {},\n): Promise<{ stdout: string; stderr: string }> {\n  const {\n    stdin,\n    stdout: stdoutOpt,\n    stderr: stderrOpt,\n    stdio,\n    cwd: cwdOpt = \".\",\n    env: envOpt,\n    ...rest\n  } = options\n  const cwd = cwdOpt instanceof URL ? fileURLToPath(cwdOpt) : path.resolve(cwdOpt)\n  const env = envOpt ? { ...process.env, ...envOpt } : undefined\n  const resolvedStdio = stdio ?? [stdin, stdoutOpt, stderrOpt]\n\n  const command = [file, ...args]\n    .map((part) => getCommandPart(stripVTControlCharacters(part)))\n    .join(\" \")\n\n  if ([\"node\", \"node.exe\"].includes(file.toLowerCase())) {\n    file = process.execPath\n    args = [...process.execArgv.filter((flag) => !flag.startsWith(\"--inspect\")), ...args]\n  }\n\n  let spawnOpts: SpawnOptions = { ...rest, stdio: resolvedStdio, env, cwd }\n\n  if (await shouldForceShell(file, spawnOpts)) {\n    args = args.map((arg) => escapeArgument(arg))\n    file = escapeFile(file)\n    spawnOpts = { ...spawnOpts, shell: true }\n  }\n\n  if (spawnOpts.shell && args.length > 0) {\n    file = [file, ...args].join(\" \")\n    args = []\n  }\n\n  const instance = nodeSpawn(file, args, spawnOpts)\n\n  let stdoutData = \"\"\n  let stderrData = \"\"\n  if (instance.stdout) {\n    instance.stdout.setEncoding(\"utf8\")\n    instance.stdout.on(\"data\", (chunk: string) => (stdoutData += chunk))\n  }\n  if (instance.stderr) {\n    instance.stderr.setEncoding(\"utf8\")\n    instance.stderr.on(\"data\", (chunk: string) => (stderrData += chunk))\n  }\n\n  instance.once(\"error\", () => {})\n\n  try {\n    await once(instance, \"spawn\")\n  } catch (error) {\n    throw Object.assign(new SubprocessError(`Command failed: ${command}`, { cause: error }), {\n      stdout: stdoutData,\n      stderr: stderrData,\n    })\n  }\n\n  await once(instance, \"close\")\n\n  const trimOutput = (s: string) =>\n    s.at(-1) === \"\\n\" ? s.slice(0, s.at(-2) === \"\\r\" ? -2 : -1) : s\n\n  if (instance.exitCode && instance.exitCode > 0) {\n    throw Object.assign(\n      new SubprocessError(`Command failed with exit code ${instance.exitCode}: ${command}`),\n      {\n        stdout: trimOutput(stdoutData),\n        stderr: trimOutput(stderrData),\n        exitCode: instance.exitCode,\n      },\n    )\n  }\n\n  if (instance.signalCode) {\n    throw Object.assign(\n      new SubprocessError(`Command was terminated with ${instance.signalCode}: ${command}`),\n      { stdout: trimOutput(stdoutData), stderr: trimOutput(stderrData) },\n    )\n  }\n\n  return { stdout: trimOutput(stdoutData), stderr: trimOutput(stderrData) }\n}\n"
  },
  {
    "path": "bin/external/speed-highlight.ts",
    "content": "var te = Object.defineProperty\nvar d = (n) => (t) => {\n  var p = n[t]\n  if (p) return p()\n  throw new Error(\"Module not found in bundle: \" + t)\n}\nvar e = (n, t) => () => (n && (t = n((n = 0))), t)\nvar a = (n, t) => {\n  for (var p in t) te(n, p, { get: t[p], enumerable: !0 })\n}\nvar B = {}\na(B, { default: () => ee })\nvar ee,\n  G = e(() => {\n    ee = [\n      { type: \"cmnt\", match: /(;|#).*/gm },\n      { expand: \"str\" },\n      { expand: \"num\" },\n      { type: \"num\", match: /\\$[\\da-fA-F]*\\b/g },\n      { type: \"kwd\", match: /^[a-z]+\\s+[a-z.]+\\b/gm, sub: [{ type: \"func\", match: /^[a-z]+/g }] },\n      { type: \"kwd\", match: /^\\t*[a-z][a-z\\d]*\\b/gm },\n      { match: /%|\\$/g, type: \"oper\" },\n    ]\n  })\nvar H = {}\na(H, { default: () => I })\nvar k,\n  I,\n  N = e(() => {\n    ;((k = { type: \"var\", match: /\\$\\w+|\\${[^}]*}|\\$\\([^)]*\\)/g }),\n      (I = [\n        { sub: \"todo\", match: /#.*/g },\n        { type: \"str\", match: /([\"'])((?!\\1)[^\\r\\n\\\\]|\\\\[^])*\\1?/g, sub: [k] },\n        { type: \"oper\", match: /(?<=\\s|^)\\.*\\/[a-z/_.-]+/gi },\n        {\n          type: \"kwd\",\n          match:\n            /\\s-[a-zA-Z]+|$<|[&|;]+|\\b(unset|readonly|shift|export|if|fi|else|elif|while|do|done|for|until|case|esac|break|continue|exit|return|trap|wait|eval|exec|then|declare|enable|local|select|typeset|time|add|remove|install|update|delete)(?=\\s|$)/g,\n        },\n        { expand: \"num\" },\n        { type: \"func\", match: /(?<=(^|\\||\\&\\&|\\;)\\s*)[a-z_.-]+(?=\\s|$)/gim },\n        { type: \"bool\", match: /(?<=\\s|^)(true|false)(?=\\s|$)/g },\n        { type: \"oper\", match: /[=(){}<>!]+/g },\n        { type: \"var\", match: /(?<=\\s|^)[\\w_]+(?=\\s*=)/g },\n        k,\n      ]))\n  })\nvar _ = {}\na(_, { default: () => ae })\nvar ae,\n  z = e(() => {\n    ae = [\n      { match: /[^\\[\\->+.<\\]\\s].*/g, sub: \"todo\" },\n      { type: \"func\", match: /\\.+/g },\n      { type: \"kwd\", match: /[<>]+/g },\n      { type: \"oper\", match: /[+-]+/g },\n    ]\n  })\nvar Y = {}\na(Y, { default: () => ne })\nvar ne,\n  Z = e(() => {\n    ne = [\n      { match: /\\/\\/.*\\n?|\\/\\*((?!\\*\\/)[^])*(\\*\\/)?/g, sub: \"todo\" },\n      { expand: \"str\" },\n      { expand: \"num\" },\n      { type: \"kwd\", match: /#\\s*include (<.*>|\".*\")/g, sub: [{ type: \"str\", match: /(<|\").*/g }] },\n      {\n        match: /asm\\s*{[^}]*}/g,\n        sub: [\n          { type: \"kwd\", match: /^asm/g },\n          { match: /[^{}]*(?=}$)/g, sub: \"asm\" },\n        ],\n      },\n      {\n        type: \"kwd\",\n        match:\n          /\\*|&|#[a-z]+\\b|\\b(asm|auto|double|int|struct|break|else|long|switch|case|enum|register|typedef|char|extern|return|union|const|float|short|unsigned|continue|for|signed|void|default|goto|sizeof|volatile|do|if|static|while)\\b/g,\n      },\n      { type: \"oper\", match: /[/*+:?&|%^~=!,<>.^-]+/g },\n      { type: \"func\", match: /[a-zA-Z_][\\w_]*(?=\\s*\\()/g },\n      { type: \"class\", match: /\\b[A-Z][\\w_]*\\b/g },\n    ]\n  })\nvar X = {}\na(X, { default: () => se })\nvar se,\n  W = e(() => {\n    se = [\n      { match: /\\/\\*((?!\\*\\/)[^])*(\\*\\/)?/g, sub: \"todo\" },\n      { expand: \"str\" },\n      { type: \"kwd\", match: /@\\w+\\b|\\b(and|not|only|or)\\b|\\b[a-z-]+(?=[^{}]*{)/g },\n      { type: \"var\", match: /\\b[\\w-]+(?=\\s*:)|(::?|\\.)[\\w-]+(?=[^{}]*{)/g },\n      { type: \"func\", match: /#[\\w-]+(?=[^{}]*{)/g },\n      { type: \"num\", match: /#[\\da-f]{3,8}/g },\n      {\n        type: \"num\",\n        match: /\\d+(\\.\\d+)?(cm|mm|in|px|pt|pc|em|ex|ch|rem|vm|vh|vmin|vmax|%)?/g,\n        sub: [{ type: \"var\", match: /[a-z]+|%/g }],\n      },\n      {\n        match: /url\\([^)]*\\)/g,\n        sub: [\n          { type: \"func\", match: /url(?=\\()/g },\n          { type: \"str\", match: /[^()]+/g },\n        ],\n      },\n      { type: \"func\", match: /\\b[a-zA-Z]\\w*(?=\\s*\\()/g },\n      { type: \"num\", match: /\\b[a-z-]+\\b/g },\n    ]\n  })\nvar j = {}\na(j, { default: () => pe })\nvar pe,\n  K = e(() => {\n    pe = [{ expand: \"strDouble\" }, { type: \"oper\", match: /,/g }]\n  })\nvar V = {}\na(V, { default: () => A })\nvar A,\n  R = e(() => {\n    A = [\n      { type: \"deleted\", match: /^[-<].*/gm },\n      { type: \"insert\", match: /^[+>].*/gm },\n      { type: \"kwd\", match: /!.*/gm },\n      { type: \"section\", match: /^@@.*@@$|^\\d.*|^([*-+])\\1\\1.*/gm },\n    ]\n  })\nvar q = {}\na(q, { default: () => re })\nvar re,\n  Q = e(() => {\n    N()\n    re = [\n      {\n        type: \"kwd\",\n        match:\n          /^(FROM|RUN|CMD|LABEL|MAINTAINER|EXPOSE|ENV|ADD|COPY|ENTRYPOINT|VOLUME|USER|WORKDIR|ARG|ONBUILD|STOPSIGNAL|HEALTHCHECK|SHELL)\\b/gim,\n      },\n      ...I,\n    ]\n  })\nvar J = {}\na(J, { default: () => ce })\nvar ce,\n  tt = e(() => {\n    R()\n    ce = [\n      { match: /^#.*/gm, sub: \"todo\" },\n      { expand: \"str\" },\n      ...A,\n      { type: \"func\", match: /^(\\$ )?git(\\s.*)?$/gm },\n      { type: \"kwd\", match: /^commit \\w+$/gm },\n    ]\n  })\nvar et = {}\na(et, { default: () => me })\nvar me,\n  at = e(() => {\n    me = [\n      { match: /\\/\\/.*\\n?|\\/\\*((?!\\*\\/)[^])*(\\*\\/)?/g, sub: \"todo\" },\n      { expand: \"str\" },\n      { expand: \"num\" },\n      {\n        type: \"kwd\",\n        match:\n          /\\*|&|\\b(break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go|goto|if|import|interface|map|package|range|return|select|struct|switch|type|var)\\b/g,\n      },\n      { type: \"func\", match: /[a-zA-Z_][\\w_]*(?=\\s*\\()/g },\n      { type: \"class\", match: /\\b[A-Z][\\w_]*\\b/g },\n      { type: \"oper\", match: /[+\\-*\\/%&|^~=!<>.^-]+/g },\n    ]\n  })\nvar st = {}\na(st, { default: () => O, name: () => u, properties: () => i, xmlElement: () => l })\nvar nt,\n  oe,\n  u,\n  i,\n  l,\n  O,\n  x = e(() => {\n    ;((nt =\n      \":A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\"),\n      (oe = nt + \"\\\\-\\\\.0-9\\xB7\\u0300-\\u036F\\u203F-\\u2040\"),\n      (u = `[${nt}][${oe}]*`),\n      (i = `\\\\s*(\\\\s+${u}\\\\s*(=\\\\s*([^\"']\\\\S*|(\"|')(\\\\\\\\[^]|(?!\\\\4)[^])*\\\\4?)?)?\\\\s*)*`),\n      (l = {\n        match: RegExp(`<[/!?]?${u}${i}[/!?]?>`, \"g\"),\n        sub: [\n          {\n            type: \"var\",\n            match: RegExp(`^<[/!?]?${u}`, \"g\"),\n            sub: [{ type: \"oper\", match: /^<[\\/!?]?/g }],\n          },\n          {\n            type: \"str\",\n            match: /=\\s*([^\"']\\S*|(\"|')(\\\\[^]|(?!\\2)[^])*\\2?)/g,\n            sub: [{ type: \"oper\", match: /^=/g }],\n          },\n          { type: \"oper\", match: /[\\/!?]?>/g },\n          { type: \"class\", match: RegExp(u, \"g\") },\n        ],\n      }),\n      (O = [\n        { match: /<!--((?!-->)[^])*-->/g, sub: \"todo\" },\n        { type: \"class\", match: /<!\\[CDATA\\[[\\s\\S]*?\\]\\]>/gi },\n        l,\n        {\n          type: \"str\",\n          match: RegExp(`<\\\\?${u}([^?]|\\\\?[^?>])*\\\\?+>`, \"g\"),\n          sub: [\n            {\n              type: \"var\",\n              match: RegExp(`^<\\\\?${u}`, \"g\"),\n              sub: [{ type: \"oper\", match: /^<\\?/g }],\n            },\n            { type: \"oper\", match: /\\?+>$/g },\n          ],\n        },\n        { type: \"var\", match: /&(#x?)?[\\da-z]{1,8};/gi },\n      ]))\n  })\nvar pt = {}\na(pt, { default: () => le })\nvar le,\n  rt = e(() => {\n    x()\n    le = [\n      {\n        type: \"class\",\n        match: /<!DOCTYPE(\"[^\"]*\"|'[^']*'|[^\"'>])*>/gi,\n        sub: [\n          { type: \"str\", match: /\"[^\"]*\"|'[^']*'/g },\n          { type: \"oper\", match: /^<!|>$/g },\n          { type: \"var\", match: /DOCTYPE/gi },\n        ],\n      },\n      {\n        match: RegExp(`<style${i}>((?!</style>)[^])*</style\\\\s*>`, \"g\"),\n        sub: [\n          { match: RegExp(`^<style${i}>`, \"g\"), sub: l.sub },\n          { match: RegExp(`${l.match}|[^]*(?=</style\\\\s*>$)`, \"g\"), sub: \"css\" },\n          l,\n        ],\n      },\n      {\n        match: RegExp(`<script${i}>((?!<\\/script>)[^])*<\\/script\\\\s*>`, \"g\"),\n        sub: [\n          { match: RegExp(`^<script${i}>`, \"g\"), sub: l.sub },\n          { match: RegExp(`${l.match}|[^]*(?=<\\/script\\\\s*>$)`, \"g\"), sub: \"js\" },\n          l,\n        ],\n      },\n      ...O,\n    ]\n  })\nvar ue,\n  E,\n  b = e(() => {\n    ;((ue = [\n      [\"bash\", [/#!(\\/usr)?\\/bin\\/bash/g, 500], [/\\b(if|elif|then|fi|echo)\\b|\\$/g, 10]],\n      [\"html\", [/<\\/?[a-z-]+[^\\n>]*>/g, 10], [/^\\s+<!DOCTYPE\\s+html/g, 500]],\n      [\"http\", [/^(GET|HEAD|POST|PUT|DELETE|PATCH|HTTP)\\b/g, 500]],\n      [\n        \"js\",\n        [\n          /\\b(console|await|async|function|export|import|this|class|for|let|const|map|join|require|document|window)\\b/g,\n          10,\n        ],\n      ],\n      [\n        \"ts\",\n        [\n          /\\b(console|await|async|function|export|import|this|class|for|let|const|map|join|require|document|window|implements|interface|namespace)\\b/g,\n          10,\n        ],\n      ],\n      [\n        \"py\",\n        [\n          /\\b(def|print|await|async|class|and|or|lambda|import|from|self|asyncio|pass|True|False|None|__init__)\\b/g,\n          10,\n        ],\n      ],\n      [\"sql\", [/\\b(SELECT|INSERT|FROM)\\b/g, 50]],\n      [\"pl\", [/#!(\\/usr)?\\/bin\\/perl/g, 500], [/\\b(use|print)\\b|\\$/g, 10]],\n      [\"lua\", [/#!(\\/usr)?\\/bin\\/lua/g, 500]],\n      [\"make\", [/\\b(ifneq|endif|if|elif|then|fi|echo|.PHONY|^[a-z]+ ?:$)\\b|\\$/gm, 10]],\n      [\"uri\", [/https?:|mailto:|tel:|ftp:/g, 30]],\n      [\"css\", [/^(@import|@page|@media|(\\.|#)[a-z]+)/gm, 20]],\n      [\"diff\", [/^[+><-]/gm, 10], [/^@@ ?[-+,0-9 ]+ ?@@/gm, 25]],\n      [\"md\", [/^(>|\\t\\*|\\t\\d+.)/gm, 10], [/\\[.*\\](.*)/g, 10]],\n      [\"docker\", [/^(FROM|ENTRYPOINT|RUN)/gm, 500]],\n      [\"xml\", [/<\\/?[a-z-]+[^\\n>]*>/g, 10], [/^<\\?xml/g, 500]],\n      [\"c\", [/#include\\b|\\bprintf\\s+\\(/g, 100]],\n      [\"rs\", [/^\\s+(use|fn|mut|match)\\b/gm, 100]],\n      [\"go\", [/\\b(func|fmt|package)\\b/g, 100]],\n      [\"java\", [/^import\\s+java/gm, 500]],\n      [\"asm\", [/^(section|global main|extern|\\t(call|mov|ret))/gm, 100]],\n      [\"css\", [/^(@import|@page|@media|(\\.|#)[a-z]+)/gm, 20]],\n      [\"json\", [/\\b(true|false|null|\\{})\\b|\\\"[^\"]+\\\":/g, 10]],\n      [\"yaml\", [/^(\\s+)?[a-z][a-z0-9]*:/gim, 10]],\n    ]),\n      (E = (n) =>\n        ue\n          .map(([t, ...p]) => [t, p.reduce((r, [m, c]) => r + [...n.matchAll(m)].length * c, 0)])\n          .filter(([t, p]) => p > 20)\n          .sort((t, p) => p[1] - t[1])[0]?.[0] || \"plain\"))\n  })\nvar ct = {}\na(ct, { default: () => ie })\nvar ie,\n  mt = e(() => {\n    b()\n    ie = [\n      {\n        type: \"kwd\",\n        match: /^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH|PRI|SEARCH)\\b/gm,\n      },\n      { expand: \"str\" },\n      { type: \"section\", match: /\\bHTTP\\/[\\d.]+\\b/g },\n      { expand: \"num\" },\n      { type: \"oper\", match: /[,;:=]/g },\n      { type: \"var\", match: /[a-zA-Z][\\w-]*(?=:)/g },\n      { match: /\\n\\n[^]*/g, sub: E },\n    ]\n  })\nvar ot = {}\na(ot, { default: () => Ee })\nvar Ee,\n  lt = e(() => {\n    Ee = [\n      { match: /(^[ \\f\\t\\v]*)[#;].*/gm, sub: \"todo\" },\n      { type: \"var\", match: /.*(?==)/g },\n      { type: \"section\", match: /^\\s*\\[.+\\]\\s*$/gm },\n      { type: \"oper\", match: /=/g },\n      { type: \"str\", match: /.*/g },\n    ]\n  })\nvar ut = {}\na(ut, { default: () => he })\nvar he,\n  it = e(() => {\n    he = [\n      { match: /\\/\\/.*\\n?|\\/\\*((?!\\*\\/)[^])*(\\*\\/)?/g, sub: \"todo\" },\n      { expand: \"str\" },\n      { expand: \"num\" },\n      {\n        type: \"kwd\",\n        match:\n          /\\b(abstract|assert|boolean|break|byte|case|catch|char|class|continue|const|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|package|private|protected|public|requires|return|short|static|strictfp|super|switch|synchronized|this|throw|throws|transient|try|var|void|volatile|while)\\b/g,\n      },\n      { type: \"oper\", match: /[/*+:?&|%^~=!,<>.^-]+/g },\n      { type: \"func\", match: /[a-zA-Z_][\\w_]*(?=\\s*\\()/g },\n      { type: \"class\", match: /\\b[A-Z][\\w_]*\\b/g },\n    ]\n  })\nvar Et = {}\na(Et, { default: () => L })\nvar L,\n  S = e(() => {\n    L = [\n      { match: /\\/\\*\\*((?!\\*\\/)[^])*(\\*\\/)?/g, sub: \"jsdoc\" },\n      { match: /\\/\\/.*\\n?|\\/\\*((?!\\*\\/)[^])*(\\*\\/)?/g, sub: \"todo\" },\n      { expand: \"str\" },\n      { match: /`((?!`)[^]|\\\\[^])*`?/g, sub: \"js_template_literals\" },\n      {\n        type: \"kwd\",\n        match:\n          /=>|\\b(this|set|get|as|async|await|break|case|catch|class|const|constructor|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|if|implements|import|in|instanceof|interface|let|var|of|new|package|private|protected|public|return|static|super|switch|throw|throws|try|typeof|void|while|with|yield)\\b/g,\n      },\n      { match: /\\/((?!\\/)[^\\r\\n\\\\]|\\\\.)+\\/[dgimsuy]*/g, sub: \"regex\" },\n      { expand: \"num\" },\n      { type: \"num\", match: /\\b(NaN|null|undefined|[A-Z][A-Z_]*)\\b/g },\n      { type: \"bool\", match: /\\b(true|false)\\b/g },\n      { type: \"oper\", match: /[/*+:?&|%^~=!,<>.^-]+/g },\n      { type: \"class\", match: /\\b[A-Z][\\w_]*\\b/g },\n      {\n        type: \"func\",\n        match: /[a-zA-Z$_][\\w$_]*(?=\\s*((\\?\\.)?\\s*\\(|=\\s*(\\(?[\\w,{}\\[\\])]+\\)? =>|function\\b)))/g,\n      },\n    ]\n  })\nvar ht = {}\na(ht, { default: () => ge, type: () => de })\nvar ge,\n  de,\n  gt = e(() => {\n    ;((ge = [\n      {\n        match: new (class {\n          exec(n) {\n            let t = this.lastIndex,\n              p,\n              r = (m) => {\n                for (; ++t < n.length - 2; )\n                  if (n[t] == \"{\") r()\n                  else if (n[t] == \"}\") return\n              }\n            for (; t < n.length; ++t)\n              if (n[t - 1] != \"\\\\\" && n[t] == \"$\" && n[t + 1] == \"{\")\n                return (\n                  (p = t++), r(t), (this.lastIndex = t + 1), { index: p, 0: n.slice(p, t + 1) }\n                )\n            return null\n          }\n        })(),\n        sub: [\n          { type: \"kwd\", match: /^\\${|}$/g },\n          { match: /(?!^\\$|{)[^]+(?=}$)/g, sub: \"js\" },\n        ],\n      },\n    ]),\n      (de = \"str\"))\n  })\nvar dt = {}\na(dt, { default: () => C, type: () => be })\nvar C,\n  be,\n  w = e(() => {\n    ;((C = [\n      { type: \"err\", match: /\\b(TODO|FIXME|DEBUG|OPTIMIZE|WARNING|XXX|BUG)\\b/g },\n      { type: \"class\", match: /\\bIDEA\\b/g },\n      { type: \"insert\", match: /\\b(CHANGED|FIX|CHANGE)\\b/g },\n      { type: \"oper\", match: /\\bQUESTION\\b/g },\n    ]),\n      (be = \"cmnt\"))\n  })\nvar bt = {}\na(bt, { default: () => ye, type: () => Te })\nvar ye,\n  Te,\n  yt = e(() => {\n    w()\n    ;((ye = [\n      { type: \"kwd\", match: /@\\w+/g },\n      { type: \"class\", match: /{[\\w\\s|<>,.@\\[\\]]+}/g },\n      { type: \"var\", match: /\\[[\\w\\s=\"']+\\]/g },\n      ...C,\n    ]),\n      (Te = \"cmnt\"))\n  })\nvar Tt = {}\na(Tt, { default: () => fe })\nvar fe,\n  ft = e(() => {\n    fe = [\n      { type: \"var\", match: /((\"|')((?!\\2)[^\\r\\n\\\\]|\\\\[^])*\\2|[a-zA-Z]\\w*)(?=\\s*:)/g },\n      { expand: \"str\" },\n      { expand: \"num\" },\n      { type: \"num\", match: /\\bnull\\b/g },\n      { type: \"bool\", match: /\\b(true|false)\\b/g },\n    ]\n  })\nvar It = {}\na(It, { default: () => D })\nvar D,\n  U = e(() => {\n    b()\n    D = [\n      { type: \"cmnt\", match: /^>.*|(=|-)\\1+/gm },\n      { type: \"class\", match: /\\*\\*((?!\\*\\*).)*\\*\\*/g },\n      {\n        match: /```((?!```)[^])*\\n```/g,\n        sub: (n) => ({\n          type: \"kwd\",\n          sub: [\n            {\n              match: /\\n[^]*(?=```)/g,\n              sub:\n                n\n                  .split(`\n`)[0]\n                  .slice(3) || E(n),\n            },\n          ],\n        }),\n      },\n      { type: \"str\", match: /`[^`]*`/g },\n      { type: \"var\", match: /~~((?!~~).)*~~/g },\n      { type: \"kwd\", match: /\\b_\\S([^\\n]*?\\S)?_\\b|\\*\\S([^\\n]*?\\S)?\\*/g },\n      { type: \"kwd\", match: /^\\s*(\\*|\\d+\\.)\\s/gm },\n      {\n        type: \"func\",\n        match: /\\[[^\\]]*]\\([^)]*\\)|<[^>]*>/g,\n        sub: [{ type: \"oper\", match: /^\\[[^\\]]*]/g }],\n      },\n    ]\n  })\nvar Nt = {}\na(Nt, { default: () => Ie })\nvar Ie,\n  At = e(() => {\n    U()\n    b()\n    Ie = [\n      {\n        type: \"insert\",\n        match: /(leanpub-start-insert)((?!leanpub-end-insert)[^])*(leanpub-end-insert)?/g,\n        sub: [\n          { type: \"insert\", match: /leanpub-(start|end)-insert/g },\n          { match: /(?!leanpub-start-insert)((?!leanpub-end-insert)[^])*/g, sub: E },\n        ],\n      },\n      {\n        type: \"deleted\",\n        match: /(leanpub-start-delete)((?!leanpub-end-delete)[^])*(leanpub-end-delete)?/g,\n        sub: [\n          { type: \"deleted\", match: /leanpub-(start|end)-delete/g },\n          { match: /(?!leanpub-start-delete)((?!leanpub-end-delete)[^])*/g, sub: E },\n        ],\n      },\n      ...D,\n    ]\n  })\nvar Rt = {}\na(Rt, { default: () => Ne })\nvar Ne,\n  Ot = e(() => {\n    Ne = [\n      { type: \"cmnt\", match: /^#.*/gm },\n      { expand: \"strDouble\" },\n      { expand: \"num\" },\n      {\n        type: \"err\",\n        match:\n          /\\b(err(or)?|[a-z_-]*exception|warn|warning|failed|ko|invalid|not ?found|alert|fatal)\\b/gi,\n      },\n      { type: \"num\", match: /\\b(null|undefined)\\b/gi },\n      { type: \"bool\", match: /\\b(false|true|yes|no)\\b/gi },\n      { type: \"oper\", match: /\\.|,/g },\n    ]\n  })\nvar xt = {}\na(xt, { default: () => Ae })\nvar Ae,\n  Lt = e(() => {\n    Ae = [\n      { match: /^#!.*|--(\\[(=*)\\[((?!--\\]\\2\\])[^])*--\\]\\2\\]|.*)/g, sub: \"todo\" },\n      { expand: \"str\" },\n      {\n        type: \"kwd\",\n        match:\n          /\\b(and|break|do|else|elseif|end|for|function|if|in|local|not|or|repeat|return|then|until|while)\\b/g,\n      },\n      { type: \"bool\", match: /\\b(true|false|nil)\\b/g },\n      { type: \"oper\", match: /[+*/%^#=~<>:,.-]+/g },\n      { expand: \"num\" },\n      { type: \"func\", match: /[a-z_]+(?=\\s*[({])/g },\n    ]\n  })\nvar St = {}\na(St, { default: () => Re })\nvar Re,\n  Ct = e(() => {\n    Re = [\n      { match: /^\\s*#.*/gm, sub: \"todo\" },\n      { expand: \"str\" },\n      { type: \"oper\", match: /[${}()]+/g },\n      { type: \"class\", match: /.PHONY:/gm },\n      { type: \"section\", match: /^[\\w.]+:/gm },\n      { type: \"kwd\", match: /\\b(ifneq|endif)\\b/g },\n      { expand: \"num\" },\n      { type: \"var\", match: /[A-Z_]+(?=\\s*=)/g },\n      { match: /^.*$/gm, sub: \"bash\" },\n    ]\n  })\nvar wt = {}\na(wt, { default: () => Oe })\nvar Oe,\n  Dt = e(() => {\n    Oe = [\n      { match: /#.*/g, sub: \"todo\" },\n      { type: \"str\", match: /([\"'])(\\\\[^]|(?!\\1)[^])*\\1?/g },\n      { expand: \"num\" },\n      {\n        type: \"kwd\",\n        match:\n          /\\b(any|break|continue|default|delete|die|do|else|elsif|eval|for|foreach|given|goto|if|last|local|my|next|our|package|print|redo|require|return|say|state|sub|switch|undef|unless|until|use|when|while|not|and|or|xor)\\b/g,\n      },\n      { type: \"oper\", match: /[-+*/%~!&<>|=?,]+/g },\n      { type: \"func\", match: /[a-z_]+(?=\\s*\\()/g },\n    ]\n  })\nvar Ut = {}\na(Ut, { default: () => xe })\nvar xe,\n  Pt = e(() => {\n    xe = [{ expand: \"strDouble\" }]\n  })\nvar Ft = {}\na(Ft, { default: () => Le })\nvar Le,\n  Mt = e(() => {\n    Le = [\n      { match: /#.*/g, sub: \"todo\" },\n      {\n        type: \"str\",\n        match: /f(\"\"\"|''')(\\\\[^]|(?!\\1)[^])*\\1?|f(\"|')(\\\\[^]|(?!\\3).)*\\3?/gi,\n        sub: [\n          { type: \"var\", match: /{[^{}]*}/g, sub: [{ match: /(?!^{)[^]*(?=}$)/g, sub: \"py\" }] },\n        ],\n      },\n      { match: /(\"\"\"|''')(\\\\[^]|(?!\\1)[^])*\\1?/g, sub: \"todo\" },\n      { expand: \"str\" },\n      {\n        type: \"kwd\",\n        match:\n          /\\b(and|as|assert|break|class|continue|def|del|elif|else|except|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield)\\b/g,\n      },\n      { type: \"bool\", match: /\\b(False|True|None)\\b/g },\n      { expand: \"num\" },\n      { type: \"func\", match: /[a-z_]\\w*(?=\\s*\\()/gi },\n      { type: \"oper\", match: /[-/*+<>,=!&|^%]+/g },\n      { type: \"class\", match: /\\b[A-Z][\\w_]*\\b/g },\n    ]\n  })\nvar $t = {}\na($t, { default: () => Se, type: () => Ce })\nvar Se,\n  Ce,\n  vt = e(() => {\n    ;((Se = [\n      { match: /^(?!\\/).*/gm, sub: \"todo\" },\n      { type: \"num\", match: /\\[((?!\\])[^\\\\]|\\\\.)*\\]/g },\n      { type: \"kwd\", match: /\\||\\^|\\$|\\\\.|\\w+($|\\r|\\n)/g },\n      { type: \"var\", match: /\\*|\\+|\\{\\d+,\\d+\\}/g },\n    ]),\n      (Ce = \"oper\"))\n  })\nvar Bt = {}\na(Bt, { default: () => we })\nvar we,\n  Gt = e(() => {\n    we = [\n      { match: /\\/\\/.*\\n?|\\/\\*((?!\\*\\/)[^])*(\\*\\/)?/g, sub: \"todo\" },\n      { expand: \"str\" },\n      { expand: \"num\" },\n      {\n        type: \"kwd\",\n        match:\n          /\\b(as|break|const|continue|crate|else|enum|extern|false|fn|for|if|impl|in|let|loop|match|mod|move|mut|pub|ref|return|self|Self|static|struct|super|trait|true|type|unsafe|use|where|while|async|await|dyn|abstract|become|box|do|final|macro|override|priv|typeof|unsized|virtual|yield|try)\\b/g,\n      },\n      { type: \"oper\", match: /[/*+:?&|%^~=!,<>.^-]+/g },\n      { type: \"class\", match: /\\b[A-Z][\\w_]*\\b/g },\n      { type: \"func\", match: /[a-zA-Z_][\\w_]*(?=\\s*!?\\s*\\()/g },\n    ]\n  })\nvar kt = {}\na(kt, { default: () => De })\nvar De,\n  Ht = e(() => {\n    De = [\n      { match: /--.*\\n?|\\/\\*((?!\\*\\/)[^])*(\\*\\/)?/g, sub: \"todo\" },\n      { expand: \"str\" },\n      {\n        type: \"func\",\n        match:\n          /\\b(AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\\s*\\()/g,\n      },\n      {\n        type: \"kwd\",\n        match:\n          /\\b(ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:_INSERT|COL)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|kwdS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:S|ING)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|VAR(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\\b/g,\n      },\n      { type: \"num\", match: /\\.?\\d[\\d.oxa-fA-F-]*|\\bNULL\\b/g },\n      { type: \"bool\", match: /\\b(TRUE|FALSE)\\b/g },\n      {\n        type: \"oper\",\n        match:\n          /[-+*\\/=%^~]|&&?|\\|\\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\\b(?:AND|BETWEEN|DIV|IN|ILIKE|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\\b/g,\n      },\n      { type: \"var\", match: /@\\S+/g },\n    ]\n  })\nvar _t = {}\na(_t, { default: () => Ue })\nvar Ue,\n  zt = e(() => {\n    Ue = [\n      { match: /#.*/g, sub: \"todo\" },\n      { type: \"str\", match: /(\"\"\"|''')((?!\\1)[^]|\\\\[^])*\\1?/g },\n      { expand: \"str\" },\n      { type: \"section\", match: /^\\[.+\\]\\s*$/gm },\n      { type: \"num\", match: /\\b(inf|nan)\\b|\\d[\\d:ZT.-]*/g },\n      { expand: \"num\" },\n      { type: \"bool\", match: /\\b(true|false)\\b/g },\n      { type: \"oper\", match: /[+,.=-]/g },\n      { type: \"var\", match: /\\w+(?= \\=)/g },\n    ]\n  })\nvar Yt = {}\na(Yt, { default: () => Pe })\nvar Pe,\n  Zt = e(() => {\n    S()\n    Pe = [\n      { type: \"type\", match: /:\\s*(any|void|number|boolean|string|object|never|enum)\\b/g },\n      {\n        type: \"kwd\",\n        match:\n          /\\b(type|namespace|typedef|interface|public|private|protected|implements|declare|abstract|readonly)\\b/g,\n      },\n      ...L,\n    ]\n  })\nvar Xt = {}\na(Xt, { default: () => Fe })\nvar Fe,\n  Wt = e(() => {\n    Fe = [\n      { match: /^#.*/gm, sub: \"todo\" },\n      { type: \"class\", match: /^\\w+(?=:?)/gm },\n      { type: \"num\", match: /:\\d+/g },\n      { type: \"oper\", match: /[:/&?]|\\w+=/g },\n      { type: \"func\", match: /[.\\w]+@|#[\\w]+$/gm },\n      { type: \"var\", match: /\\w+\\.\\w+(\\.\\w+)*/g },\n    ]\n  })\nvar jt = {}\na(jt, { default: () => Me })\nvar Me,\n  Kt = e(() => {\n    Me = [\n      { match: /#.*/g, sub: \"todo\" },\n      { expand: \"str\" },\n      { type: \"str\", match: /(>|\\|)\\r?\\n((\\s[^\\n]*)?(\\r?\\n|$))*/g },\n      { type: \"type\", match: /!![a-z]+/g },\n      { type: \"bool\", match: /\\b(Yes|No)\\b/g },\n      { type: \"oper\", match: /[+:-]/g },\n      { expand: \"num\" },\n      { type: \"var\", match: /[a-zA-Z]\\w*(?=:)/g },\n    ]\n  })\nvar Vt = {}\na(Vt, { default: () => s })\nvar s,\n  y = e(() => {\n    s = {\n      black: \"\\x1B[30m\",\n      red: \"\\x1B[31m\",\n      green: \"\\x1B[32m\",\n      gray: \"\\x1B[90m\",\n      yellow: \"\\x1B[33m\",\n      blue: \"\\x1B[34m\",\n      magenta: \"\\x1B[35m\",\n      cyan: \"\\x1B[36m\",\n      white: \"\\x1B[37m\",\n    }\n  })\nvar qt = {}\na(qt, { default: () => ve })\nvar ve,\n  Qt = e(() => {\n    y()\n    ve = {\n      deleted: s.red,\n      var: s.red,\n      err: s.red,\n      kwd: s.magenta,\n      num: s.yellow,\n      class: s.yellow,\n      cmnt: s.gray,\n      insert: s.green,\n      str: s.green,\n      bool: s.cyan,\n      type: s.blue,\n      oper: s.blue,\n      section: s.magenta,\n      func: s.blue,\n    }\n  })\nvar M = {}\na(M, { default: () => Be })\nvar Be,\n  $ = e(() => {\n    y()\n    Be = {\n      deleted: s.red,\n      var: s.red,\n      err: s.red,\n      kwd: s.red,\n      num: s.yellow,\n      class: s.yellow,\n      cmnt: s.gray,\n      insert: s.green,\n      str: s.green,\n      bool: s.cyan,\n      type: s.blue,\n      oper: s.blue,\n      section: s.magenta,\n      func: s.magenta,\n    }\n  })\nvar v = {\n  num: { type: \"num\", match: /(\\.e?|\\b)\\d(e-|[\\d.oxa-fA-F_])*(\\.|\\b)/g },\n  str: { type: \"str\", match: /([\"'])(\\\\[^]|(?!\\1)[^\\r\\n\\\\])*\\1?/g },\n  strDouble: { type: \"str\", match: /\"((?!\")[^\\r\\n\\\\]|\\\\[^])*\"?/g },\n}\nvar $e = d({\n  \"./languages/asm.js\": () => Promise.resolve().then(() => (G(), B)),\n  \"./languages/bash.js\": () => Promise.resolve().then(() => (N(), H)),\n  \"./languages/bf.js\": () => Promise.resolve().then(() => (z(), _)),\n  \"./languages/c.js\": () => Promise.resolve().then(() => (Z(), Y)),\n  \"./languages/css.js\": () => Promise.resolve().then(() => (W(), X)),\n  \"./languages/csv.js\": () => Promise.resolve().then(() => (K(), j)),\n  \"./languages/diff.js\": () => Promise.resolve().then(() => (R(), V)),\n  \"./languages/docker.js\": () => Promise.resolve().then(() => (Q(), q)),\n  \"./languages/git.js\": () => Promise.resolve().then(() => (tt(), J)),\n  \"./languages/go.js\": () => Promise.resolve().then(() => (at(), et)),\n  \"./languages/html.js\": () => Promise.resolve().then(() => (rt(), pt)),\n  \"./languages/http.js\": () => Promise.resolve().then(() => (mt(), ct)),\n  \"./languages/ini.js\": () => Promise.resolve().then(() => (lt(), ot)),\n  \"./languages/java.js\": () => Promise.resolve().then(() => (it(), ut)),\n  \"./languages/js.js\": () => Promise.resolve().then(() => (S(), Et)),\n  \"./languages/js_template_literals.js\": () => Promise.resolve().then(() => (gt(), ht)),\n  \"./languages/jsdoc.js\": () => Promise.resolve().then(() => (yt(), bt)),\n  \"./languages/json.js\": () => Promise.resolve().then(() => (ft(), Tt)),\n  \"./languages/leanpub-md.js\": () => Promise.resolve().then(() => (At(), Nt)),\n  \"./languages/log.js\": () => Promise.resolve().then(() => (Ot(), Rt)),\n  \"./languages/lua.js\": () => Promise.resolve().then(() => (Lt(), xt)),\n  \"./languages/make.js\": () => Promise.resolve().then(() => (Ct(), St)),\n  \"./languages/md.js\": () => Promise.resolve().then(() => (U(), It)),\n  \"./languages/pl.js\": () => Promise.resolve().then(() => (Dt(), wt)),\n  \"./languages/plain.js\": () => Promise.resolve().then(() => (Pt(), Ut)),\n  \"./languages/py.js\": () => Promise.resolve().then(() => (Mt(), Ft)),\n  \"./languages/regex.js\": () => Promise.resolve().then(() => (vt(), $t)),\n  \"./languages/rs.js\": () => Promise.resolve().then(() => (Gt(), Bt)),\n  \"./languages/sql.js\": () => Promise.resolve().then(() => (Ht(), kt)),\n  \"./languages/todo.js\": () => Promise.resolve().then(() => (w(), dt)),\n  \"./languages/toml.js\": () => Promise.resolve().then(() => (zt(), _t)),\n  \"./languages/ts.js\": () => Promise.resolve().then(() => (Zt(), Yt)),\n  \"./languages/uri.js\": () => Promise.resolve().then(() => (Wt(), Xt)),\n  \"./languages/xml.js\": () => Promise.resolve().then(() => (x(), st)),\n  \"./languages/yaml.js\": () => Promise.resolve().then(() => (Kt(), jt)),\n})\nvar P = {}\nasync function F(n, t, p) {\n  try {\n    let r,\n      m,\n      c = {},\n      T,\n      o = [],\n      h = 0,\n      f = typeof t == \"string\" ? await (P[t] ?? (P[t] = $e(`./languages/${t}.js`))) : t,\n      g = [...(typeof t == \"string\" ? f.default : t.sub)]\n    for (; h < n.length; ) {\n      for (c.index = null, r = g.length; r-- > 0; ) {\n        if (((m = g[r].expand ? v[g[r].expand] : g[r]), o[r] === void 0 || o[r].match.index < h)) {\n          if (((m.match.lastIndex = h), (T = m.match.exec(n)), T === null)) {\n            ;(g.splice(r, 1), o.splice(r, 1))\n            continue\n          }\n          o[r] = { match: T, lastIndex: m.match.lastIndex }\n        }\n        o[r].match[0] &&\n          (o[r].match.index <= c.index || c.index === null) &&\n          (c = { part: m, index: o[r].match.index, match: o[r].match[0], end: o[r].lastIndex })\n      }\n      if (c.index === null) break\n      ;(p(n.slice(h, c.index), f.type),\n        (h = c.end),\n        c.part.sub\n          ? await F(\n              c.match,\n              typeof c.part.sub == \"string\"\n                ? c.part.sub\n                : typeof c.part.sub == \"function\"\n                  ? c.part.sub(c.match)\n                  : c.part,\n              p,\n            )\n          : p(c.match, c.part.type))\n    }\n    p(n.slice(h, n.length), f.type)\n  } catch {\n    p(n)\n  }\n}\nvar Ge = d({\n  \"./themes/atom-dark.js\": () => Promise.resolve().then(() => (Qt(), qt)),\n  \"./themes/default.js\": () => Promise.resolve().then(() => ($(), M)),\n  \"./themes/termcolor.js\": () => Promise.resolve().then(() => (y(), Vt)),\n})\nvar Jt = Promise.resolve().then(() => ($(), M)),\n  ke = async (n, t) => {\n    let p = \"\",\n      r = (await Jt).default\n    return (await F(n, t, (m, c) => (p += c ? `${r[c] ?? \"\"}${m}\\x1B[0m` : m)), p)\n  },\n  la = async (n, t) => console.log(await ke(n, t)),\n  ua = async (n) => (Jt = Ge(`./themes/${n}.js`))\nexport { ke as highlightText, la as printHighlight, ua as setTheme }\n"
  },
  {
    "path": "bin/external/strip-json-comments.ts",
    "content": "// From strip-json-comments by Sindre Sorhus (https://github.com/sindresorhus/strip-json-comments)\nconst singleComment = Symbol(\"singleComment\")\nconst multiComment = Symbol(\"multiComment\")\n\nconst stripWithWhitespace = (string: string, start: number, end?: number) =>\n  string.slice(start, end).replace(/[^ \\t\\r\\n]/g, \" \")\n\nconst isEscaped = (jsonString: string, quotePosition: number) => {\n  let index = quotePosition - 1\n  let backslashCount = 0\n  while (jsonString[index] === \"\\\\\") {\n    index -= 1\n    backslashCount += 1\n  }\n  return Boolean(backslashCount % 2)\n}\n\nexport default function stripJsonComments(jsonString: string) {\n  if (typeof jsonString !== \"string\") {\n    throw new TypeError(\n      `Expected argument \\`jsonString\\` to be a \\`string\\`, got \\`${typeof jsonString}\\``,\n    )\n  }\n\n  let isInsideString = false\n  let isInsideComment: symbol | false = false\n  let offset = 0\n  let buffer = \"\"\n  let result = \"\"\n  let commaIndex = -1\n\n  for (let index = 0; index < jsonString.length; index++) {\n    const currentCharacter = jsonString[index]\n    const nextCharacter = jsonString[index + 1]\n\n    if (!isInsideComment && currentCharacter === '\"') {\n      const escaped = isEscaped(jsonString, index)\n      if (!escaped) {\n        isInsideString = !isInsideString\n      }\n    }\n\n    if (isInsideString) {\n      continue\n    }\n\n    if (!isInsideComment && currentCharacter + nextCharacter === \"//\") {\n      buffer += jsonString.slice(offset, index)\n      offset = index\n      isInsideComment = singleComment\n      index++\n    } else if (isInsideComment === singleComment && currentCharacter + nextCharacter === \"\\r\\n\") {\n      index++\n      isInsideComment = false\n      buffer += stripWithWhitespace(jsonString, offset, index)\n      offset = index\n      continue\n    } else if (isInsideComment === singleComment && currentCharacter === \"\\n\") {\n      isInsideComment = false\n      buffer += stripWithWhitespace(jsonString, offset, index)\n      offset = index\n    } else if (!isInsideComment && currentCharacter + nextCharacter === \"/*\") {\n      buffer += jsonString.slice(offset, index)\n      offset = index\n      isInsideComment = multiComment\n      index++\n      continue\n    } else if (isInsideComment === multiComment && currentCharacter + nextCharacter === \"*/\") {\n      index++\n      isInsideComment = false\n      buffer += stripWithWhitespace(jsonString, offset, index + 1)\n      offset = index + 1\n      continue\n    } else if (!isInsideComment) {\n      if (commaIndex !== -1) {\n        if (currentCharacter === \"}\" || currentCharacter === \"]\") {\n          buffer += jsonString.slice(offset, index)\n          result += stripWithWhitespace(buffer, 0, 1) + buffer.slice(1)\n          buffer = \"\"\n          offset = index\n          commaIndex = -1\n        } else if (\n          currentCharacter !== \" \" &&\n          currentCharacter !== \"\\t\" &&\n          currentCharacter !== \"\\r\" &&\n          currentCharacter !== \"\\n\"\n        ) {\n          buffer += jsonString.slice(offset, index)\n          offset = index\n          commaIndex = -1\n        }\n      } else if (currentCharacter === \",\") {\n        result += buffer + jsonString.slice(offset, index)\n        buffer = \"\"\n        offset = index\n        commaIndex = index\n      }\n    }\n  }\n\n  const remaining =\n    isInsideComment === singleComment\n      ? stripWithWhitespace(jsonString, offset)\n      : jsonString.slice(offset)\n\n  return result + buffer + remaining\n}\n"
  },
  {
    "path": "bin/external/yocto-spinner.ts",
    "content": "// Trimmed from yocto-spinner by Sindre Sorhus (https://github.com/sindresorhus/yocto-spinner)\nimport process from \"node:process\"\nimport { stripVTControlCharacters } from \"node:util\"\n\nimport { cyan, green } from \"@/external/yoctocolors\"\n\nconst isUnicodeSupported =\n  process.platform !== \"win32\" ||\n  Boolean(process.env.WT_SESSION) ||\n  process.env.TERM_PROGRAM === \"vscode\"\n\nconst isInteractive = (stream: NodeJS.WriteStream) =>\n  Boolean(stream.isTTY && process.env.TERM !== \"dumb\" && !(\"CI\" in process.env))\n\nconst successSymbol = green(isUnicodeSupported ? \"✔\" : \"√\")\n\nconst frames = isUnicodeSupported\n  ? [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"]\n  : [\"-\", \"\\\\\", \"|\", \"/\"]\nconst interval = 80\n\nexport const spinner = (options: { text?: string; stream?: NodeJS.WriteStream } = {}) => {\n  let currentFrame = -1\n  let timer: ReturnType<typeof setInterval> | undefined\n  let text = options.text ?? \"\"\n  const stream = options.stream ?? process.stderr\n  const interactive = isInteractive(stream)\n  let lines = 0\n  let lastFrameTime = 0\n  let spinning = false\n\n  const write = (str: string) => stream.write(str)\n\n  const clear = () => {\n    if (!interactive || lines === 0) return\n    stream.cursorTo(0)\n    for (let i = 0; i < lines; i++) {\n      if (i > 0) stream.moveCursor(0, -1)\n      stream.clearLine(1)\n    }\n    lines = 0\n  }\n\n  const lineCount = (str: string) => {\n    const width = stream.columns ?? 80\n    const stripped = stripVTControlCharacters(str).split(\"\\n\")\n    let count = 0\n    for (const line of stripped) {\n      count += Math.max(1, Math.ceil(line.length / width))\n    }\n    return count\n  }\n\n  const render = () => {\n    if (!interactive) return\n    const now = Date.now()\n    if (currentFrame === -1 || now - lastFrameTime >= interval) {\n      currentFrame = ++currentFrame % frames.length\n      lastFrameTime = now\n    }\n    const frame = frames[currentFrame]\n    const string = `${cyan(frame)} ${text}`\n    clear()\n    write(string)\n    lines = lineCount(string)\n  }\n\n  return {\n    start(t: string) {\n      text = t\n      spinning = true\n      if (interactive) write(\"\\x1B[?25l\")\n      render()\n      if (interactive) {\n        timer = setInterval(render, interval)\n      }\n      return this\n    },\n    success(t: string) {\n      if (!spinning) return this\n      spinning = false\n      if (timer) {\n        clearInterval(timer)\n        timer = undefined\n      }\n      clear()\n      if (interactive) write(\"\\x1B[?25h\")\n      write(`${successSymbol} ${t ?? text}\\n`)\n      return this\n    },\n  }\n}\n"
  },
  {
    "path": "bin/external/yoctocolors.ts",
    "content": "// Trimmed from yoctocolors by Sindre Sorhus (https://github.com/sindresorhus/yoctocolors)\nimport tty from \"node:tty\"\n\nconst hasColors = tty?.WriteStream?.prototype?.hasColors?.() ?? false\n\nconst format = (open: number, close: number) => {\n  if (!hasColors) {\n    return (input: string) => input\n  }\n\n  const openCode = `\\u001B[${open}m`\n  const closeCode = `\\u001B[${close}m`\n\n  return (input: string) => {\n    const string = input + \"\"\n    let index = string.indexOf(closeCode)\n\n    if (index === -1) {\n      return openCode + string + closeCode\n    }\n\n    let result = openCode\n    let lastIndex = 0\n\n    const reopenOnNestedClose = close === 22\n    const replaceCode = (reopenOnNestedClose ? closeCode : \"\") + openCode\n\n    while (index !== -1) {\n      result += string.slice(lastIndex, index) + replaceCode\n      lastIndex = index + closeCode.length\n      index = string.indexOf(closeCode, lastIndex)\n    }\n\n    result += string.slice(lastIndex) + closeCode\n\n    return result\n  }\n}\n\nexport const bold = format(1, 22)\nexport const dim = format(2, 22)\nexport const red = format(31, 39)\nexport const green = format(32, 39)\nexport const yellow = format(33, 39)\nexport const cyan = format(36, 39)\n"
  },
  {
    "path": "bin/index.ts",
    "content": "#!/usr/bin/env node\nimport fs from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport { parseArgs } from \"node:util\"\n\nimport spawn from \"@/external/nano-spawn\"\nimport { spinner } from \"@/external/yocto-spinner\"\nimport { bold, cyan, green, red, yellow } from \"@/external/yoctocolors\"\nimport { cloneAction } from \"@/utils/clone-action\"\nimport { copyDir } from \"@/utils/copy-dir\"\nimport { type TreeEntry, interactivePicker } from \"@/utils/interactive-picker\"\nimport { parseTimeString } from \"@/utils/parse-time-string\"\nimport { configFromUrl } from \"@/utils/transform-url\"\nimport { notifyUpdate, scheduleUpdateCheck } from \"@/utils/update-notifier\"\nimport { useConfig } from \"@/utils/use-config\"\nimport { name, version } from \"~/package.json\"\n\nconst terminalLink = (text: string, url: string) => `\\x1b]8;;${url}\\x07${text}\\x1b]8;;\\x07`\n\nconst helpMessage = `\nWith ${bold(`${terminalLink(\"GitPick\", \"https://github.com/nrjdalal/gitpick\")}`)} clone specific directories or files from GitHub, GitLab, Bitbucket and Codeberg!\n\n  $ gitpick ${yellow(\"<url>\")} ${green(\"[target]\")} ${cyan(\"[options]\")}\n\n${bold(\"Hint:\")}\n  [target] and [options] are optional and if not specified,\n  GitPick fallbacks to the default behavior of \\`git clone\\`\n\n${bold(\"Arguments:\")}\n  ${yellow(\"url\")}                GitHub/GitLab/Bitbucket/Codeberg URL with path to file/folder/repository\n  ${green(\"target\")}             Directory to clone into (optional)\n\n${bold(\"Options:\")}\n  ${cyan(\"-b, --branch \")}      Branch/SHA to clone\n  ${cyan(\"-i, --interactive\")}  Browse and pick files/folders interactively\n  ${cyan(\"-n, --dry-run\")}      Show what would be cloned without cloning\n  ${cyan(\"-o, --overwrite\")}    Skip overwrite prompt\n  ${cyan(\"-r, --recursive\")}    Clone submodules\n  ${cyan(\"-w, --watch [time]\")} Watch the repository and sync every [time]\n                     (e.g. 1h, 30m, 15s)\n  ${cyan(\"    --tree\")}         List copied files as a tree\n  ${cyan(\"-q, --quiet\")}        Suppress all output except errors\n  ${cyan(\"    --verbose\")}      Show detailed clone information\n  ${cyan(\"-h, --help\")}         display help for command\n  ${cyan(\"-v, --version\")}      display the version number\n\n${bold(\"Examples:\")}\n  $ gitpick <url>\n  $ gitpick <url> [target]\n  $ gitpick <url> [target] -b [branch/SHA]\n  $ gitpick <url> [target] -w [time]\n  $ gitpick <url> [target] -b [branch/SHA] -w [time]\n  $ gitpick <url> --dry-run\n  $ gitpick https://gitlab.com/owner/repo\n  $ gitpick https://bitbucket.org/owner/repo\n  $ gitpick https://codeberg.org/owner/repo\n\n🚀 More awesome tools at ${cyan(\"https://github.com/nrjdalal\")}`\n\nconst displayPath = (targetPath: string) => {\n  const cwd = process.cwd()\n  const home = os.homedir()\n  const sep = path.sep\n  if (targetPath === cwd) return \".\"\n  if (targetPath.startsWith(cwd + sep))\n    return \"./\" + path.relative(cwd, targetPath).replaceAll(sep, \"/\")\n  if (targetPath.startsWith(home + sep))\n    return \"~/\" + path.relative(home, targetPath).replaceAll(sep, \"/\")\n  return targetPath\n}\n\nconst printTree = async (dir: string, prefix = \"\") => {\n  const entries = (await fs.promises.readdir(dir, { withFileTypes: true }))\n    .filter((e) => e.name !== \".git\")\n    .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: \"base\" }))\n\n  for (let i = 0; i < entries.length; i++) {\n    const entry = entries[i]\n    const last = i === entries.length - 1\n    const connector = last ? \"└── \" : \"├── \"\n    const entryPath = path.join(dir, entry.name)\n\n    if (entry.isSymbolicLink()) {\n      const linkTarget = await fs.promises.readlink(entryPath)\n      let resolvedIsDir = false\n      try {\n        resolvedIsDir = fs.statSync(entryPath).isDirectory()\n      } catch {}\n      process.stdout.write(\n        `${prefix}${connector}${yellow(entry.name)} -> ${resolvedIsDir ? cyan(linkTarget) : linkTarget}\\n`,\n      )\n    } else if (entry.isDirectory()) {\n      process.stdout.write(`${prefix}${connector}${cyan(entry.name)}\\n`)\n      await printTree(entryPath, `${prefix}${last ? \"    \" : \"│   \"}`)\n    } else {\n      process.stdout.write(`${prefix}${connector}${entry.name}\\n`)\n    }\n  }\n}\n\nconst parse: typeof parseArgs = (config) => {\n  try {\n    return parseArgs(config)\n  } catch (err: any) {\n    throw new Error(`Error parsing arguments: ${err.message}`)\n  }\n}\n\nconst main = async () => {\n  scheduleUpdateCheck()\n\n  try {\n    const { positionals, values } = parse({\n      allowPositionals: true,\n      options: {\n        branch: { type: \"string\", short: \"b\" },\n        \"dry-run\": { type: \"boolean\", short: \"n\" },\n        force: { type: \"boolean\", short: \"f\" },\n        help: { type: \"boolean\", short: \"h\" },\n        interactive: { type: \"boolean\", short: \"i\" },\n        quiet: { type: \"boolean\", short: \"q\" },\n        tree: { type: \"boolean\" },\n        verbose: { type: \"boolean\" },\n        overwrite: { type: \"boolean\", short: \"o\" },\n        recursive: { type: \"boolean\", short: \"r\" },\n        version: { type: \"boolean\", short: \"v\" },\n        watch: { type: \"string\", short: \"w\" },\n      },\n    })\n\n    if (!positionals.length) {\n      if (values.version) {\n        console.log(`\\n${name}@${version}`)\n        process.exit(0)\n      }\n\n      // `gitpick -i` with no args — browse cwd\n      if (values.interactive) {\n        positionals.push(\".\")\n      } else {\n        if (await useConfig()) process.exit(0)\n\n        console.log(helpMessage)\n        process.exit(0)\n      }\n    }\n\n    if (positionals[0] === \"clone\") {\n      positionals.shift()\n    }\n\n    let [url, target] = positionals\n\n    const options = {\n      branch: values.branch,\n      dryRun: values[\"dry-run\"],\n      force: values.force,\n      interactive: values.interactive,\n      quiet: values.quiet,\n      tree: values.tree,\n      verbose: values.verbose,\n      overwrite: values.overwrite,\n      recursive: values.recursive,\n      watch: values.watch,\n    }\n\n    // Local directory interactive mode — detect local paths or\n    // non-URL-like positionals when -i is set (e.g. `gitpick -i target`)\n    const isLocalPath =\n      url === \".\" ||\n      url.startsWith(\"./\") ||\n      url.startsWith(\"../\") ||\n      url.startsWith(\"/\") ||\n      url.startsWith(\"~/\") ||\n      (options.interactive &&\n        !url.includes(\"/\") &&\n        !url.startsWith(\"http\") &&\n        !url.startsWith(\"git@\"))\n\n    if (isLocalPath && options.interactive) {\n      // Single positional that doesn't exist — treat as target (e.g. `gitpick -i hello`)\n      // Only when no explicit target is given; with two args, a missing source is an error\n      if (\n        !fs.existsSync(path.resolve(url.startsWith(\"~/\") ? url.replace(\"~\", os.homedir()) : url))\n      ) {\n        if (target) {\n          throw new Error(`Directory not found: ${url}`)\n        }\n        target = url\n        url = \".\"\n      }\n      if (!process.stdout.isTTY) {\n        throw new Error(\"Interactive mode requires a TTY\")\n      }\n\n      const resolvedSource = path.resolve(\n        url.startsWith(\"~/\") ? url.replace(\"~\", os.homedir()) : url,\n      )\n\n      if (!fs.existsSync(resolvedSource)) {\n        throw new Error(`Directory not found: ${url}`)\n      }\n      if (!fs.statSync(resolvedSource).isDirectory()) {\n        throw new Error(`Not a directory: ${url}`)\n      }\n\n      const targetDir = target ? path.resolve(target) : null\n\n      const entries: TreeEntry[] = []\n\n      // Try git ls-files first (respects .gitignore)\n      let usedGit = false\n      try {\n        const result = await spawn(\n          \"git\",\n          [\"ls-files\", \"--cached\", \"--others\", \"--exclude-standard\"],\n          {\n            cwd: resolvedSource,\n          },\n        )\n        const files = result.stdout.trim().split(\"\\n\").filter(Boolean)\n        for (const file of files) {\n          const parts = file.split(\"/\")\n          // Add parent directories\n          for (let i = 1; i < parts.length; i++) {\n            const dirPath = parts.slice(0, i).join(\"/\")\n            if (!entries.some((e) => e.path === dirPath)) {\n              entries.push({ path: dirPath, type: \"tree\" })\n            }\n          }\n          const filePath = path.join(resolvedSource, file)\n          try {\n            const stat = await fs.promises.lstat(filePath)\n            if (stat.isSymbolicLink()) {\n              const linkTarget = await fs.promises.readlink(filePath)\n              let resolvedIsDir = false\n              try {\n                resolvedIsDir = (await fs.promises.stat(filePath)).isDirectory()\n              } catch {}\n              entries.push({\n                path: file,\n                type: \"symlink\",\n                linkTarget: resolvedIsDir ? linkTarget + \"/\" : linkTarget,\n              })\n            } else {\n              entries.push({ path: file, type: \"blob\", size: stat.size })\n            }\n          } catch {}\n        }\n        usedGit = true\n      } catch {}\n\n      // Fallback: walk directory manually (skip .git only)\n      if (!usedGit) {\n        async function walkLocal(dir: string, rel: string) {\n          let items\n          try {\n            items = await fs.promises.readdir(dir, { withFileTypes: true })\n          } catch {\n            return\n          }\n          for (const item of items) {\n            if (item.name === \".git\") continue\n            const itemRel = rel ? `${rel}/${item.name}` : item.name\n            const itemPath = path.join(dir, item.name)\n            if (item.isSymbolicLink()) {\n              const linkTarget = await fs.promises.readlink(itemPath)\n              let resolvedIsDir = false\n              try {\n                resolvedIsDir = (await fs.promises.stat(itemPath)).isDirectory()\n              } catch {}\n              entries.push({\n                path: itemRel,\n                type: \"symlink\",\n                linkTarget: resolvedIsDir ? linkTarget + \"/\" : linkTarget,\n              })\n            } else if (item.isDirectory()) {\n              entries.push({ path: itemRel, type: \"tree\" })\n              await walkLocal(itemPath, itemRel)\n            } else {\n              try {\n                const stat = await fs.promises.stat(itemPath)\n                entries.push({ path: itemRel, type: \"blob\", size: stat.size })\n              } catch {}\n            }\n          }\n        }\n        await walkLocal(resolvedSource, \"\")\n      }\n\n      if (!entries.length) {\n        console.log(yellow(\"\\nDirectory is empty.\"))\n        process.exit(0)\n      }\n\n      const selected = await interactivePicker(\n        entries,\n        `${displayPath(resolvedSource)}`,\n        resolvedSource,\n      )\n\n      if (!selected.length) {\n        console.log(\"\\nNo files selected.\")\n        process.exit(0)\n      }\n\n      if (options.dryRun) {\n        console.log(\n          `\\n${green(\"✔\")} Would pick ${selected.length} path${selected.length !== 1 ? \"s\" : \"\"}:`,\n        )\n        for (const sel of selected) console.log(`  ${sel}`)\n        console.log()\n        process.exit(0)\n      }\n\n      if (!targetDir) {\n        // No target - just list selected paths\n        console.log(\n          `\\n${green(\"✔\")} Selected ${selected.length} path${selected.length !== 1 ? \"s\" : \"\"}:`,\n        )\n        for (const sel of selected) console.log(`  ${sel}`)\n        console.log()\n        process.exit(0)\n      }\n\n      const resolvedTarget = path.resolve(targetDir)\n      if (resolvedSource === resolvedTarget) {\n        throw new Error(\"Source and target directories are the same\")\n      }\n      if (resolvedTarget.startsWith(resolvedSource + path.sep)) {\n        throw new Error(\"Target directory is inside the source directory\")\n      }\n\n      console.log(\n        `\\n${green(\"✔\")} Picking ${selected.length} selected path${selected.length !== 1 ? \"s\" : \"\"}...`,\n      )\n\n      options.overwrite = options.overwrite || options.force\n      if (fs.existsSync(targetDir) && !options.overwrite) {\n        if ((await fs.promises.readdir(targetDir)).length) {\n          console.log(\n            `${yellow(`\\nWarning: The target directory exists at ${green(target!)} and is not empty. Use ${cyan(\"-f\")} or ${cyan(\"-o\")} to overwrite.`)}`,\n          )\n          process.exit(1)\n        }\n      }\n\n      await fs.promises.mkdir(targetDir, { recursive: true })\n\n      let copiedFiles = 0\n      for (const sel of selected) {\n        const src = path.join(resolvedSource, sel)\n        const dest = path.join(targetDir, sel)\n        const lstat = await fs.promises.lstat(src).catch(() => null)\n        if (!lstat) continue\n\n        await fs.promises.mkdir(path.dirname(dest), { recursive: true })\n        if (lstat.isSymbolicLink()) {\n          const linkTarget = await fs.promises.readlink(src)\n          try {\n            await fs.promises.rm(dest, { force: true })\n            await fs.promises.symlink(linkTarget, dest)\n            copiedFiles++\n          } catch (err: any) {\n            console.log(yellow(`  Warning: failed to copy symlink ${sel}: ${err.message}`))\n          }\n        } else if (lstat.isDirectory()) {\n          await fs.promises.mkdir(dest, { recursive: true })\n          const files = await copyDir(src, dest)\n          copiedFiles += files.length\n        } else {\n          await fs.promises.copyFile(src, dest)\n          copiedFiles++\n        }\n      }\n\n      console.log(\n        green(\n          `✔ Copied ${copiedFiles} file${copiedFiles !== 1 ? \"s\" : \"\"} to ${displayPath(targetDir)}`,\n        ),\n      )\n      if (options.tree) {\n        process.stdout.write(`\\n${bold(cyan(displayPath(targetDir)))}\\n`)\n        await printTree(targetDir)\n        process.stdout.write(\"\\n\")\n      }\n      process.exit(0)\n    }\n\n    const silent = options.tree || options.quiet\n\n    if (!silent) {\n      console.log(\n        `\\nWith ${bold(`${terminalLink(\"GitPick\", \"https://github.com/nrjdalal/gitpick\")}`)} clone specific files, folders, branches,\\ncommits and much more from GitHub, GitLab, Bitbucket and Codeberg!`,\n      )\n    }\n\n    const config = await configFromUrl(url, {\n      branch: options.branch,\n      target,\n    })\n\n    if (config.type === \"blob\") {\n      const parts = config.target.split(/[/\\\\]/).filter((part) => part !== \"\")\n      let lastPart = parts[parts.length - 1]\n      if (lastPart !== \".\" && lastPart !== \"..\" && lastPart.includes(\".\")) {\n        parts.pop()\n      } else {\n        lastPart = config.path.split(\"/\").pop() || lastPart\n      }\n      config.target = [...parts, lastPart].join(\"/\")\n    }\n\n    if (!silent) {\n      console.info(\n        `\\n${green(\"✔\")} ${config.owner}/${config.repository} ${cyan(config.type + \":\" + config.branch)} ${\n          config.type === \"repository\"\n            ? `> ${green(config.target)}`\n            : `${!config.path.length ? \">\" : yellow(config.path) + \" >\"} ${green(config.target)}`\n        }`,\n      )\n    }\n\n    const targetPath = path.resolve(config.target)\n\n    if (options.interactive) {\n      if (!process.stdout.isTTY) {\n        throw new Error(\"Interactive mode requires a TTY\")\n      }\n\n      // Shallow clone to temp first\n      const tempDir = path.resolve(\n        os.tmpdir(),\n        `gitpick-interactive-${Date.now()}${Math.random().toString(16).slice(2, 6)}`,\n      )\n      const repoUrl = `https://${config.token ? config.token + \"@\" : \"\"}${config.host}/${config.owner}/${config.repository}.git`\n\n      const s = spinner()\n      s.start(`Fetching ${config.owner}/${config.repository}...`)\n\n      try {\n        await spawn(\"git\", [\n          \"clone\",\n          repoUrl,\n          tempDir,\n          \"--branch\",\n          config.branch,\n          \"--depth\",\n          \"1\",\n          \"--single-branch\",\n          ...(options.recursive ? [\"--recursive\"] : []),\n        ])\n      } catch {\n        await spawn(\"git\", [\n          \"clone\",\n          repoUrl,\n          tempDir,\n          ...(options.recursive ? [\"--recursive\"] : []),\n        ])\n        await spawn(\"git\", [\"checkout\", config.branch], { cwd: tempDir })\n      }\n\n      // Walk local tree to build entries (scoped to config.path if set)\n      const walkRoot = config.path ? path.join(tempDir, config.path) : tempDir\n      const entries: TreeEntry[] = []\n      async function walkDir(dir: string, rel: string) {\n        const items = await fs.promises.readdir(dir, { withFileTypes: true })\n        for (const item of items) {\n          if (item.name === \".git\") continue\n          const itemRel = rel ? `${rel}/${item.name}` : item.name\n          const itemPath = path.join(dir, item.name)\n          if (item.isSymbolicLink()) {\n            const linkTarget = await fs.promises.readlink(itemPath)\n            let resolvedIsDir = false\n            try {\n              resolvedIsDir = (await fs.promises.stat(itemPath)).isDirectory()\n            } catch {}\n            entries.push({\n              path: itemRel,\n              type: \"symlink\",\n              linkTarget: resolvedIsDir ? linkTarget + \"/\" : linkTarget,\n            })\n          } else if (item.isDirectory()) {\n            entries.push({ path: itemRel, type: \"tree\" })\n            await walkDir(itemPath, itemRel)\n          } else {\n            const stat = await fs.promises.stat(itemPath)\n            entries.push({ path: itemRel, type: \"blob\", size: stat.size })\n          }\n        }\n      }\n      await walkDir(walkRoot, \"\")\n\n      s.success(`Fetched ${config.owner}/${config.repository} (${entries.length} entries)`)\n\n      if (!entries.length) {\n        await fs.promises.rm(tempDir, { recursive: true, force: true })\n        console.log(yellow(\"\\nRepository has no files.\"))\n        process.exit(0)\n      }\n\n      const selected = await interactivePicker(\n        entries,\n        `${config.owner}/${config.repository} ${cyan(\"repository:\" + config.branch)} > ${green(config.target)}`,\n        walkRoot,\n      )\n\n      if (!selected.length) {\n        await fs.promises.rm(tempDir, { recursive: true, force: true })\n        console.log(\"\\nNo files selected.\")\n        process.exit(0)\n      }\n\n      // Dry run - just show what would be picked\n      if (options.dryRun) {\n        console.log(\n          `\\n${green(\"✔\")} Would pick ${selected.length} path${selected.length !== 1 ? \"s\" : \"\"}:`,\n        )\n        for (const sel of selected) console.log(`  ${sel}`)\n        await fs.promises.rm(tempDir, { recursive: true, force: true })\n        console.log()\n        notifyUpdate(version, false)\n        process.exit(0)\n      }\n\n      console.log(\n        `\\n${green(\"✔\")} Picking ${selected.length} selected path${selected.length !== 1 ? \"s\" : \"\"}...`,\n      )\n\n      // Overwrite guard\n      if (fs.existsSync(targetPath) && !options.overwrite) {\n        if ((await fs.promises.readdir(targetPath)).length) {\n          await fs.promises.rm(tempDir, { recursive: true, force: true })\n          console.log(\n            `${yellow(`\\nWarning: The target directory exists at ${green(config.target)} and is not empty. Use ${cyan(\"-f\")} or ${cyan(\"-o\")} to overwrite.`)}`,\n          )\n          process.exit(1)\n        }\n      }\n\n      await fs.promises.mkdir(targetPath, { recursive: true })\n\n      let copiedFiles = 0\n      for (const sel of selected) {\n        const src = path.join(walkRoot, sel)\n        const dest = path.join(targetPath, sel)\n        const stat = await fs.promises.stat(src).catch(() => null)\n        if (!stat) continue\n\n        if (stat.isDirectory()) {\n          await fs.promises.mkdir(dest, { recursive: true })\n          const files = await copyDir(src, dest)\n          copiedFiles += files.length\n        } else {\n          await fs.promises.mkdir(path.dirname(dest), { recursive: true })\n          await fs.promises.copyFile(src, dest)\n          copiedFiles++\n        }\n      }\n\n      await fs.promises.rm(tempDir, { recursive: true, force: true })\n\n      console.log(\n        green(\n          `✔ Copied ${copiedFiles} file${copiedFiles !== 1 ? \"s\" : \"\"} to ${displayPath(targetPath)}`,\n        ),\n      )\n      if (options.tree) {\n        process.stdout.write(`\\n${bold(cyan(displayPath(targetPath)))}\\n`)\n        await printTree(targetPath)\n        process.stdout.write(\"\\n\")\n      }\n      notifyUpdate(version, false)\n      process.exit(0)\n    }\n\n    const renderTree = async (clonedPath: string) => {\n      if (fs.statSync(clonedPath).isDirectory()) {\n        process.stdout.write(`${bold(cyan(displayPath(targetPath)))}\\n`)\n        await printTree(clonedPath)\n      } else {\n        process.stdout.write(`${bold(cyan(displayPath(path.dirname(targetPath))))}\\n`)\n        process.stdout.write(`└── ${path.basename(targetPath)}\\n`)\n      }\n      process.stdout.write(\"\\n\")\n    }\n\n    if (options.dryRun) {\n      if (options.tree) {\n        const tempTarget = path.resolve(\n          os.tmpdir(),\n          `gitpick-dry-${Date.now()}${Math.random().toString(16).slice(2, 6)}`,\n        )\n        try {\n          await cloneAction(config, options, tempTarget)\n          await renderTree(tempTarget)\n        } finally {\n          await fs.promises.rm(tempTarget, { recursive: true, force: true })\n        }\n      }\n      if (!silent) console.log()\n      notifyUpdate(version, silent)\n      process.exit(0)\n    }\n    options.overwrite = options.overwrite || options.force\n    if (options.watch) options.overwrite = true\n\n    if (fs.existsSync(targetPath) && !options.overwrite) {\n      if (config.type === \"blob\") {\n        console.log(\n          `${yellow(`\\nWarning: The target file exists at ${green(config.target)}. Use ${cyan(\"-f\")} or ${cyan(\"-o\")} to overwrite.`)}`,\n        )\n        process.exit(1)\n      }\n      if ((await fs.promises.readdir(targetPath)).length) {\n        console.log(\n          `${yellow(`\\nWarning: The target directory exists at ${green(config.target)} and is not empty. Use ${cyan(\"-f\")} or ${cyan(\"-o\")} to overwrite.`)}`,\n        )\n        process.exit(1)\n      }\n    }\n\n    if (options.watch) {\n      if (!silent)\n        console.log(`\\n👀 Watching every ${parseTimeString(options.watch) / 1000 + \"s\"}\\n`)\n      await cloneAction(config, options, targetPath)\n      if (options.tree) await renderTree(targetPath)\n      const watchInterval = parseTimeString(options.watch)\n      setInterval(async () => {\n        await cloneAction(config, options, targetPath)\n        if (options.tree) await renderTree(targetPath)\n      }, watchInterval)\n    } else {\n      await cloneAction(config, options, targetPath)\n      if (options.tree) await renderTree(targetPath)\n      notifyUpdate(version, silent)\n      process.exit(0)\n    }\n  } catch (err) {\n    if (err instanceof Error) {\n      console.log(bold(`\\n${red(\"Error: \")}`) + err.message)\n    } else {\n      console.log(bold(`${red(\"\\nUnexpected Error: \")}`) + JSON.stringify(err, null, 2))\n    }\n    process.exit(1)\n  }\n}\n\nmain()\n"
  },
  {
    "path": "bin/utils/clone-action.ts",
    "content": "import fs from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nimport spawn from \"@/external/nano-spawn\"\nimport { spinner } from \"@/external/yocto-spinner\"\nimport { cyan, dim } from \"@/external/yoctocolors\"\nimport { copyDir } from \"@/utils/copy-dir\"\n\nconst activeTempDirs = new Set<string>()\n\nfunction cleanupAndExit() {\n  for (const dir of activeTempDirs) {\n    try {\n      fs.rmSync(dir, { recursive: true, force: true })\n    } catch {}\n  }\n  process.exit(1)\n}\n\nprocess.on(\"SIGINT\", cleanupAndExit)\nprocess.on(\"SIGTERM\", cleanupAndExit)\n\nconst formatSize = (bytes: number) => {\n  if (bytes < 1024) return `${bytes} B`\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n\nexport type CloneResult = {\n  files: string[]\n  duration: number\n  networkTime: number\n  copyTime: number\n  totalSize: number\n  cloneStrategy: string\n}\n\nexport const cloneAction = async (\n  config: {\n    token: string\n    host: string\n    owner: string\n    repository: string\n    branch: string\n    type: string\n    path: string\n  },\n  options: {\n    recursive?: boolean\n    watch?: string\n    quiet?: boolean\n    tree?: boolean\n    verbose?: boolean\n  },\n  targetPath: string,\n): Promise<CloneResult> => {\n  const silent = options.tree || options.quiet\n  const verbose = options.verbose && !silent\n\n  if (process.platform === \"win32\") {\n    await spawn(\"git\", [\"config\", \"--global\", \"core.longpaths\", \"true\"])\n  }\n\n  const repoUrl = `https://${config.token ? config.token + \"@\" : config.token}${config.host}/${config.owner}/${config.repository}.git`\n  const displayUrl = `https://${config.host}/${config.owner}/${config.repository}.git`\n  const tempDir = path.resolve(\n    os.tmpdir(),\n    `${config.repository}-${Date.now()}${Math.random().toString(16).slice(2, 6)}`,\n  )\n\n  activeTempDirs.add(tempDir)\n\n  const s = spinner()\n  const start = performance.now()\n\n  if (!options.watch && !silent) {\n    s.start(\n      `Picking ${config.type}${config.type === \"repository\" ? \" without .git\" : \" from repository\"}...`,\n    )\n  }\n\n  let cloneStrategy = \"shallow\"\n  const networkStart = performance.now()\n\n  try {\n    await spawn(\"git\", [\n      \"clone\",\n      repoUrl,\n      tempDir,\n      \"--branch\",\n      config.branch,\n      \"--depth\",\n      \"1\",\n      \"--single-branch\",\n      ...(options.recursive ? [\"--recursive\"] : []),\n    ])\n  } catch {\n    cloneStrategy = \"full\"\n    await spawn(\"git\", [\"clone\", repoUrl, tempDir, ...(options.recursive ? [\"--recursive\"] : [])])\n    await spawn(\"git\", [\"checkout\", config.branch], { cwd: tempDir })\n  }\n\n  const networkTime = Number(((performance.now() - networkStart) / 1000).toFixed(2))\n\n  const sourcePath = path.resolve(tempDir, config.path)\n\n  const sourceStat = await fs.promises.stat(sourcePath)\n\n  let files: string[] = []\n  const copyStart = performance.now()\n\n  if (sourceStat.isDirectory()) {\n    await fs.promises.mkdir(targetPath, { recursive: true })\n    files = await copyDir(sourcePath, targetPath)\n  } else {\n    await fs.promises.mkdir(path.dirname(targetPath), {\n      recursive: true,\n    })\n    await fs.promises.copyFile(sourcePath, targetPath)\n    files = [path.basename(targetPath)]\n  }\n\n  const copyTime = Number(((performance.now() - copyStart) / 1000).toFixed(2))\n  const duration = Number(((performance.now() - start) / 1000).toFixed(2))\n\n  let totalSize = 0\n  for (const file of files) {\n    try {\n      const stat = await fs.promises.stat(path.join(targetPath, file))\n      totalSize += stat.size\n    } catch {\n      // single file (blob) — targetPath is the file itself\n      const stat = await fs.promises.stat(targetPath)\n      totalSize += stat.size\n      break\n    }\n  }\n\n  if (!silent) {\n    if (!options.watch) {\n      s.success(\n        `Picked ${config.type}${config.type === \"repository\" ? \" without .git\" : \" from repository\"} in ${duration} seconds.`,\n      )\n    } else console.log(\"- Synced at \" + new Date().toLocaleTimeString())\n  }\n\n  if (verbose) {\n    console.log(\n      dim(`  clone:    ${cloneStrategy} (depth=${cloneStrategy === \"shallow\" ? \"1\" : \"full\"})`),\n    )\n    console.log(dim(`  from:     ${displayUrl} @ ${cyan(config.branch)}`))\n    console.log(dim(`  to:       ${targetPath}`))\n    console.log(dim(`  files:    ${files.length} (${formatSize(totalSize)})`))\n    console.log(dim(`  network:  ${networkTime}s`))\n    console.log(dim(`  copy:     ${copyTime}s`))\n    console.log(dim(`  total:    ${duration}s`))\n  }\n\n  await fs.promises.rm(tempDir, { recursive: true, force: true })\n  activeTempDirs.delete(tempDir)\n\n  return { files, duration, networkTime, copyTime, totalSize, cloneStrategy }\n}\n"
  },
  {
    "path": "bin/utils/copy-dir.ts",
    "content": "import fs from \"node:fs\"\nimport path from \"node:path\"\n\nexport const copyDir = async (\n  src: string,\n  dest: string,\n  relativeTo?: string,\n): Promise<string[]> => {\n  const base = relativeTo ?? dest\n  const entries = await fs.promises.readdir(src, { withFileTypes: true })\n  await fs.promises.mkdir(dest, { recursive: true })\n\n  const files: string[] = []\n\n  for (const entry of entries) {\n    if (entry.name === \".git\") continue\n    const srcPath = path.join(src, entry.name)\n    const destPath = path.join(dest, entry.name)\n\n    if (entry.isDirectory()) {\n      files.push(...(await copyDir(srcPath, destPath, base)))\n    } else if (entry.isSymbolicLink()) {\n      const link = await fs.promises.readlink(srcPath)\n      await fs.promises.symlink(link, destPath)\n      files.push(path.relative(base, destPath))\n    } else {\n      await fs.promises.copyFile(srcPath, destPath)\n      files.push(path.relative(base, destPath))\n    }\n  }\n\n  return files\n}\n"
  },
  {
    "path": "bin/utils/get-default-branch.ts",
    "content": "import spawn from \"@/external/nano-spawn\"\n\nexport const getDefaultBranch = async (url: string) => {\n  const remotes = (await spawn(\"git\", [\"ls-remote\", url])).stdout\n  const headHash = remotes.match(/(.+)\\s+HEAD/)?.[1]\n  const branch = remotes.match(new RegExp(`${headHash}\\\\s+refs/heads/(.+)`))?.[1]\n  if (!branch) {\n    throw new Error(\"Could not determine default branch!\")\n  }\n  return branch\n}\n"
  },
  {
    "path": "bin/utils/interactive-picker.ts",
    "content": "import fs from \"node:fs\"\nimport path from \"node:path\"\n\nimport { highlightText } from \"@/external/speed-highlight\"\nimport { bold, cyan, dim, green, yellow } from \"@/external/yoctocolors\"\n\nconst EXT_TO_LANG: Record<string, string> = {\n  \".js\": \"js\",\n  \".mjs\": \"js\",\n  \".cjs\": \"js\",\n  \".jsx\": \"js\",\n  \".ts\": \"ts\",\n  \".mts\": \"ts\",\n  \".cts\": \"ts\",\n  \".tsx\": \"ts\",\n  \".json\": \"json\",\n  \".jsonc\": \"json\",\n  \".md\": \"md\",\n  \".mdx\": \"md\",\n  \".css\": \"css\",\n  \".scss\": \"css\",\n  \".html\": \"html\",\n  \".htm\": \"html\",\n  \".svelte\": \"html\",\n  \".vue\": \"html\",\n  \".xml\": \"xml\",\n  \".svg\": \"xml\",\n  \".yaml\": \"yaml\",\n  \".yml\": \"yaml\",\n  \".toml\": \"toml\",\n  \".py\": \"py\",\n  \".rs\": \"rs\",\n  \".go\": \"go\",\n  \".c\": \"c\",\n  \".h\": \"c\",\n  \".cpp\": \"c\",\n  \".hpp\": \"c\",\n  \".java\": \"java\",\n  \".sql\": \"sql\",\n  \".sh\": \"bash\",\n  \".bash\": \"bash\",\n  \".zsh\": \"bash\",\n  \".lua\": \"lua\",\n  \".pl\": \"pl\",\n  \".pm\": \"pl\",\n  \".rb\": \"py\", // ruby is close enough to py highlighting\n  \".diff\": \"diff\",\n  \".patch\": \"diff\",\n  \".ini\": \"ini\",\n  \".cfg\": \"ini\",\n  \".env\": \"ini\",\n  \".dockerfile\": \"docker\",\n  \".makefile\": \"make\",\n  \".csv\": \"csv\",\n  \".log\": \"log\",\n}\n\nfunction detectLang(filename: string): string {\n  const ext = path.extname(filename).toLowerCase()\n  if (ext) return EXT_TO_LANG[ext] || \"plain\"\n  const base = path.basename(filename).toLowerCase()\n  if (base === \"dockerfile\") return \"docker\"\n  if (base === \"makefile\") return \"make\"\n  if (base === \".gitignore\" || base === \".env\") return \"ini\"\n  return \"plain\"\n}\n\nexport type TreeEntry = {\n  path: string\n  type: \"blob\" | \"tree\" | \"symlink\"\n  size?: number\n  linkTarget?: string\n}\n\nconst stripAnsi = (s: string) => s.replace(/\\x1B\\[\\d+(?:;\\d+)*m/g, \"\")\n\nfunction truncateAnsi(s: string, maxWidth: number): string {\n  let visible = 0\n  let i = 0\n  while (i < s.length && visible < maxWidth) {\n    if (s[i] === \"\\x1B\" && s[i + 1] === \"[\") {\n      const end = s.indexOf(\"m\", i)\n      if (end !== -1) {\n        i = end + 1\n        continue\n      }\n    }\n    visible++\n    i++\n  }\n  return s.slice(0, i) + \"\\x1B[0m\"\n}\n\ntype TreeNode = {\n  name: string\n  path: string\n  type: \"blob\" | \"tree\" | \"symlink\"\n  size: number\n  linkTarget: string\n  children: TreeNode[]\n  expanded: boolean\n  selected: boolean\n  depth: number\n}\n\nfunction buildTree(entries: TreeEntry[]): TreeNode[] {\n  const root: TreeNode[] = []\n  const dirs = new Map<string, TreeNode>()\n\n  // Sort so directories come before files, then alphabetically\n  const sorted = [...entries].sort((a, b) => {\n    if (a.type !== b.type) return a.type === \"tree\" ? -1 : 1\n    return a.path.localeCompare(b.path, undefined, { sensitivity: \"base\" })\n  })\n\n  for (const entry of sorted) {\n    const parts = entry.path.split(\"/\")\n    const name = parts[parts.length - 1]\n    const node: TreeNode = {\n      name,\n      path: entry.path,\n      type: entry.type,\n      size: entry.size || 0,\n      linkTarget: entry.linkTarget || \"\",\n      children: [],\n      expanded: false,\n      selected: false,\n      depth: parts.length - 1,\n    }\n\n    if (entry.type === \"tree\") {\n      dirs.set(entry.path, node)\n    }\n\n    if (parts.length === 1) {\n      root.push(node)\n    } else {\n      const parentPath = parts.slice(0, -1).join(\"/\")\n      const parent = dirs.get(parentPath)\n      if (parent) {\n        parent.children.push(node)\n      } else {\n        // Parent directory wasn't in the tree entries - create implicit ones\n        let currentPath = \"\"\n        let currentList = root\n        for (let i = 0; i < parts.length - 1; i++) {\n          currentPath = currentPath ? currentPath + \"/\" + parts[i] : parts[i]\n          let dir = dirs.get(currentPath)\n          if (!dir) {\n            dir = {\n              name: parts[i],\n              path: currentPath,\n              type: \"tree\",\n              size: 0,\n              linkTarget: \"\",\n              children: [],\n              expanded: false,\n              selected: false,\n              depth: i,\n            }\n            dirs.set(currentPath, dir)\n            currentList.push(dir)\n          }\n          currentList = dir.children\n        }\n        currentList.push(node)\n      }\n    }\n  }\n\n  // Sort children: dirs first, then files, alphabetically within each group\n  function sortChildren(nodes: TreeNode[]) {\n    nodes.sort((a, b) => {\n      if (a.type !== b.type) return a.type === \"tree\" ? -1 : 1\n      return a.name.localeCompare(b.name, undefined, { sensitivity: \"base\" })\n    })\n    for (const n of nodes) {\n      if (n.children.length) sortChildren(n.children)\n    }\n  }\n  sortChildren(root)\n\n  // Calculate folder sizes from children\n  function calcSize(nodes: TreeNode[]): number {\n    let total = 0\n    for (const node of nodes) {\n      if (node.children.length) {\n        node.size = calcSize(node.children)\n      }\n      total += node.size\n    }\n    return total\n  }\n  calcSize(root)\n\n  return root\n}\n\ntype FlatItem = {\n  node: TreeNode\n  prefix: string\n  connector: string\n}\n\nfunction flatten(roots: TreeNode[]): FlatItem[] {\n  const items: FlatItem[] = []\n\n  function walk(nodes: TreeNode[], prefix: string) {\n    for (let i = 0; i < nodes.length; i++) {\n      const node = nodes[i]\n      const last = i === nodes.length - 1\n      const connector = last ? \"└── \" : \"├── \"\n      items.push({ node, prefix, connector })\n      if (node.type === \"tree\" && node.expanded) {\n        walk(node.children, prefix + (last ? \"    \" : \"│   \"))\n      }\n    }\n  }\n\n  walk(roots, \"\")\n  return items\n}\n\nfunction setSelected(node: TreeNode, value: boolean) {\n  node.selected = value\n  for (const child of node.children) {\n    setSelected(child, value)\n  }\n}\n\nfunction resolveSymlinkPath(symlinkPath: string, linkTarget: string): string {\n  const symlinkDir = symlinkPath.includes(\"/\") ? symlinkPath.split(\"/\").slice(0, -1).join(\"/\") : \"\"\n  const rawTarget = linkTarget.replace(/\\/$/, \"\")\n  const resolved = symlinkDir ? `${symlinkDir}/${rawTarget}` : rawTarget\n  const parts = resolved.split(\"/\")\n  const normalized: string[] = []\n  for (const p of parts) {\n    if (p === \"..\") normalized.pop()\n    else if (p !== \".\") normalized.push(p)\n  }\n  return normalized.join(\"/\")\n}\n\nfunction findNodeByPath(roots: TreeNode[], targetPath: string): TreeNode | null {\n  for (const node of roots) {\n    if (node.path === targetPath) return node\n    if (node.children.length) {\n      const found = findNodeByPath(node.children, targetPath)\n      if (found) return found\n    }\n  }\n  return null\n}\n\nfunction updateParentSelection(roots: TreeNode[]) {\n  function walk(nodes: TreeNode[]): void {\n    for (const node of nodes) {\n      if (node.children.length) {\n        walk(node.children)\n        node.selected = node.children.every((c) => c.selected)\n      }\n    }\n  }\n  walk(roots)\n}\n\nfunction collectSelected(nodes: TreeNode[]): string[] {\n  const paths: string[] = []\n\n  function walk(nodeList: TreeNode[]) {\n    for (const node of nodeList) {\n      if (node.selected) {\n        if (node.type === \"tree\") {\n          // If a whole directory is selected, add the dir path\n          // Only add the dir itself if ALL children are selected\n          const allChildrenSelected =\n            node.children.length > 0 && node.children.every((c) => c.selected)\n          if (allChildrenSelected || node.children.length === 0) {\n            paths.push(node.path)\n          } else {\n            walk(node.children)\n          }\n        } else {\n          paths.push(node.path)\n        }\n      } else if (node.type === \"tree\") {\n        // Dir not selected but maybe some children are\n        walk(node.children)\n      }\n    }\n  }\n\n  walk(nodes)\n  return paths\n}\n\nfunction countSelected(nodes: TreeNode[]): {\n  files: number\n  folders: number\n  symlinks: number\n  size: number\n} {\n  let files = 0\n  let folders = 0\n  let symlinks = 0\n  let size = 0\n\n  function walk(nodeList: TreeNode[]) {\n    for (const node of nodeList) {\n      if (node.selected) {\n        if (node.type === \"tree\") folders++\n        else if (node.type === \"symlink\") symlinks++\n        else {\n          files++\n          size += node.size\n        }\n      }\n      if (node.children.length) walk(node.children)\n    }\n  }\n\n  walk(nodes)\n  return { files, folders, symlinks, size }\n}\n\nconst formatSize = (bytes: number) => {\n  if (bytes < 1024) return `${bytes} B`\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n\nexport function interactivePicker(\n  entries: TreeEntry[],\n  label: string,\n  basePath?: string,\n): Promise<string[]> {\n  return new Promise((resolve) => {\n    const tree = buildTree(entries)\n    if (!tree.length) {\n      resolve([])\n      return\n    }\n\n    // Auto-expand: if ≤30 entries expand all, otherwise expand up to depth 1 (2 levels)\n    function expandToDepth(nodes: TreeNode[], maxDepth: number, currentDepth = 0) {\n      for (const node of nodes) {\n        if (node.type === \"tree\" && currentDepth <= maxDepth) {\n          node.expanded = true\n          expandToDepth(node.children, maxDepth, currentDepth + 1)\n        }\n      }\n    }\n\n    if (entries.length <= 30) {\n      expandToDepth(tree, Infinity)\n    } else {\n      expandToDepth(tree, 1)\n    }\n\n    let cursor = 0\n    let scrollOffset = 0\n    const stream = process.stdout\n    const stdin = process.stdin\n\n    const wasRaw = stdin.isRaw\n    stdin.setRawMode(true)\n    stdin.resume()\n\n    // Enter alternate screen, hide cursor\n    stream.write(\"\\x1B[?1049h\\x1B[?25l\")\n\n    function cleanup() {\n      stdin.setRawMode(wasRaw ?? false)\n      stdin.pause()\n      stdin.removeListener(\"data\", onKey)\n      stream.removeListener(\"resize\", onResize)\n      // Exit alternate screen, show cursor\n      stream.write(\"\\x1B[?25h\\x1B[?1049l\")\n    }\n\n    // Safety net for clean terminal restore\n    const onExit = () => {\n      stream.write(\"\\x1B[?25h\\x1B[?1049l\")\n    }\n    const onSigint = () => {\n      cleanup()\n      process.removeListener(\"exit\", onExit)\n      process.removeListener(\"SIGINT\", onSigint)\n      resolve([])\n      process.exit(0)\n    }\n    let inPreview = false\n    let previewRenderer: (() => void) | null = null\n    const onResize = () => {\n      if (inPreview && previewRenderer) previewRenderer()\n      else render()\n    }\n    process.on(\"exit\", onExit)\n    process.on(\"SIGINT\", onSigint)\n    stream.on(\"resize\", onResize)\n\n    function render() {\n      const rows = stream.rows || 24\n      const cols = stream.columns || 80\n      const headerLines = 3 // blank + label + blank\n      const dotRowLines = 1\n      const footerLines = 5\n      const treeViewportHeight = Math.max(1, rows - headerLines - dotRowLines - footerLines)\n\n      const items = flatten(tree)\n\n      // Scroll only applies to tree items (cursor > 0)\n      const treeCursor = cursor - 1 // -1 because 0 is dot row\n      if (treeCursor >= 0) {\n        if (treeCursor < scrollOffset) scrollOffset = treeCursor\n        if (treeCursor >= scrollOffset + treeViewportHeight)\n          scrollOffset = treeCursor - treeViewportHeight + 1\n      } else {\n        scrollOffset = 0\n      }\n      if (scrollOffset < 0) scrollOffset = 0\n\n      const visible = items.slice(scrollOffset, scrollOffset + treeViewportHeight)\n      const { files, folders, symlinks, size } = countSelected(tree)\n\n      // Build output\n      let out = \"\\x1B[H\\x1B[2J\" // cursor home + clear screen\n\n      // Header\n      out += `\\n  ${label}\\n\\n`\n\n      // \".\" select-all row (virtual row at index 0, tree items shift by 1)\n      const allSelected = tree.every((n) => n.selected)\n      const dotCursor = cursor === 0\n      const dotCheckbox = allSelected ? green(\"●\") : dim(\"○\")\n      let dotLine = `${dotCursor ? yellow(\">\") : \" \"} ${dotCheckbox} ${dim(\".\")}`\n      if (dotCursor) {\n        const pad = Math.max(0, cols - stripAnsi(dotLine).length)\n        dotLine = `\\x1B[48;5;236m${dotLine}${\" \".repeat(pad)}\\x1B[49m`\n      }\n      out += dotLine + \"\\n\"\n\n      // Tree items\n      for (let i = 0; i < visible.length; i++) {\n        const item = visible[i]\n        const idx = scrollOffset + i + 1 // +1 for the dot row\n        const isCursor = idx === cursor\n        const checkbox = item.node.selected ? green(\"●\") : dim(\"○\")\n        const nameStr =\n          item.node.type === \"tree\"\n            ? cyan(item.node.name + \"/\")\n            : item.node.type === \"symlink\"\n              ? yellow(item.node.name) +\n                dim(\" -> \") +\n                (item.node.linkTarget.endsWith(\"/\")\n                  ? cyan(item.node.linkTarget)\n                  : item.node.linkTarget)\n              : item.node.name\n        const expandIcon =\n          item.node.type === \"tree\" ? (item.node.expanded ? dim(\"▾ \") : dim(\"▸ \")) : \"  \"\n        const pointer = isCursor ? yellow(\">\") : \" \"\n        const leftPart = `${pointer} ${checkbox} ${dim(item.prefix)}${dim(item.connector)}${expandIcon}${nameStr}`\n        const sizeLabel =\n          item.node.size > 0 && item.node.type !== \"symlink\" ? dim(formatSize(item.node.size)) : \"\"\n        const leftLen = stripAnsi(leftPart).length\n        const sizeLen = stripAnsi(sizeLabel).length\n        const gap = Math.max(1, cols - leftLen - sizeLen - 1)\n        let line = sizeLabel ? `${leftPart}${\" \".repeat(gap)}${sizeLabel} ` : leftPart\n        if (isCursor) {\n          const pad = Math.max(0, cols - stripAnsi(line).length)\n          line = `\\x1B[48;5;236m${line}${\" \".repeat(pad)}\\x1B[49m`\n        }\n        out += line + \"\\n\"\n      }\n\n      // Pad remaining viewport\n      for (let i = visible.length; i < treeViewportHeight; i++) {\n        out += \"\\n\"\n      }\n\n      // Footer\n      out += \"\\n\"\n      const scrollInfo =\n        items.length > treeViewportHeight\n          ? dim(\n              ` • ${scrollOffset + 1}-${Math.min(scrollOffset + treeViewportHeight, items.length)}/${items.length}`,\n            )\n          : \"\"\n      let statusLine: string\n      if (allSelected) {\n        statusLine = `  all selected ${dim(\"•\")} ${dim(formatSize(size))}${scrollInfo}`\n      } else if (files + folders + symlinks > 0) {\n        const countParts: string[] = []\n        if (folders > 0) countParts.push(cyan(`${folders} folder${folders !== 1 ? \"s\" : \"\"}`))\n        if (files > 0) countParts.push(`${files} file${files !== 1 ? \"s\" : \"\"}`)\n        if (symlinks > 0) countParts.push(yellow(`${symlinks} symlink${symlinks !== 1 ? \"s\" : \"\"}`))\n        const metaParts: string[] = [countParts.join(\" \"), dim(formatSize(size))]\n        statusLine = `  ${metaParts.join(dim(\" • \"))}${scrollInfo}`\n      } else {\n        statusLine = dim(\"  press . to select all\") + scrollInfo\n      }\n      out += statusLine + \"\\n\"\n      out += \"\\n\"\n      const instructions = dim(\n        basePath\n          ? \"↑↓:navigate  enter:expand/preview  space:select  c:confirm  q:quit\"\n          : \"↑↓:navigate  enter:expand  space:select  c:confirm  q:quit\",\n      )\n      out += `  ${instructions}\\n`\n\n      stream.write(out)\n    }\n\n    async function showPreview(node: TreeNode) {\n      // Remove stdin listener immediately to prevent race during async highlight\n      stdin.removeListener(\"data\", onKey)\n\n      const resolvedPath =\n        node.type === \"symlink\" ? resolveSymlinkPath(node.path, node.linkTarget) : node.path\n      const filePath = path.join(basePath!, resolvedPath)\n      let content: string\n      try {\n        const stat = fs.statSync(filePath)\n        if (stat.isDirectory()) {\n          content = dim(\"(directory)\")\n        } else if (stat.size > 512 * 1024) {\n          content = dim(`(file too large: ${formatSize(stat.size)})`)\n        } else {\n          const raw = fs.readFileSync(filePath)\n          // Check if binary\n          if (raw.includes(0)) {\n            content = dim(`(binary file: ${formatSize(stat.size)})`)\n          } else {\n            const text = raw.toString(\"utf-8\")\n            const lang = detectLang(node.name)\n            if (lang !== \"plain\") {\n              try {\n                content = await highlightText(text, lang)\n              } catch {\n                content = text\n              }\n            } else {\n              content = text\n            }\n          }\n        }\n      } catch {\n        content = dim(\"(unable to read file)\")\n      }\n\n      let previewCursor = 0\n      let previewScrollOffset = 0\n      const lines = content.split(\"\\n\")\n      const lineNumWidth = String(lines.length).length\n\n      function renderPreview() {\n        const rows = stream.rows || 24\n        const cols = stream.columns || 80\n        const headerLines = 3\n        const footerLines = 3\n        const viewportHeight = Math.max(1, rows - headerLines - footerLines)\n\n        // Adjust scroll to follow cursor\n        if (previewCursor < previewScrollOffset) previewScrollOffset = previewCursor\n        if (previewCursor >= previewScrollOffset + viewportHeight)\n          previewScrollOffset = previewCursor - viewportHeight + 1\n        if (previewScrollOffset < 0) previewScrollOffset = 0\n\n        let out = \"\\x1B[H\\x1B[2J\"\n        const pathStr =\n          node.type === \"symlink\" ? yellow(node.path) + dim(\" -> \") + node.linkTarget : node.path\n        out += `\\n  ${bold(pathStr)} ${dim(\"•\")} ${dim(formatSize(node.size))}\\n\\n`\n\n        const visibleCount = Math.min(viewportHeight, lines.length - previewScrollOffset)\n        for (let i = 0; i < visibleCount; i++) {\n          const lineIdx = previewScrollOffset + i\n          const isCursorLine = lineIdx === previewCursor\n          const lineNum = dim(`  ${String(lineIdx + 1).padStart(lineNumWidth)}  `)\n          const lineContent = truncateAnsi(lines[lineIdx], cols - lineNumWidth - 5)\n          let line = `${lineNum}${lineContent}`\n          if (isCursorLine) {\n            // Strip trailing resets so bg extends fully\n            const cleaned = line.replace(/\\x1B\\[0m/g, \"\")\n            const pad = Math.max(0, cols - stripAnsi(cleaned).length)\n            line = `\\x1B[48;5;236m${cleaned}${\" \".repeat(pad)}\\x1B[0m`\n          }\n          out += line + \"\\n\"\n        }\n\n        // Pad remaining\n        for (let i = visibleCount; i < viewportHeight; i++) {\n          out += \"\\n\"\n        }\n\n        out += \"\\n\"\n        const scrollInfo =\n          lines.length > viewportHeight\n            ? dim(\n                `${previewScrollOffset + 1}-${Math.min(previewScrollOffset + viewportHeight, lines.length)}/${lines.length}`,\n              ) + dim(\" • \")\n            : \"\"\n        const previewInstructions = dim(\"↑↓:navigate  esc/q:back\")\n        out += `  ${scrollInfo}${previewInstructions}\\n`\n\n        stream.write(out)\n      }\n\n      function onPreviewKey(buf: Buffer) {\n        const key = buf.toString()\n\n        // Ctrl-C in preview\n        if (key === \"\\x03\") {\n          inPreview = false\n          previewRenderer = null\n          stdin.removeListener(\"data\", onPreviewKey)\n          cleanup()\n          process.removeListener(\"exit\", onExit)\n          process.removeListener(\"SIGINT\", onSigint)\n          resolve([])\n          return\n        }\n\n        if (key === \"\\x1B\" || key === \"q\" || key === \"Q\" || key === \"\\r\") {\n          inPreview = false\n          previewRenderer = null\n          stdin.removeListener(\"data\", onPreviewKey)\n          stdin.on(\"data\", onKey)\n          render()\n          return\n        }\n\n        if (key === \"\\x1B[A\" || key === \"k\") {\n          if (previewCursor > 0) previewCursor--\n        }\n        if (key === \"\\x1B[B\" || key === \"j\") {\n          if (previewCursor < lines.length - 1) previewCursor++\n        }\n\n        renderPreview()\n      }\n\n      inPreview = true\n      previewRenderer = renderPreview\n      stdin.on(\"data\", onPreviewKey)\n      renderPreview()\n    }\n\n    function onKey(buf: Buffer) {\n      const items = flatten(tree)\n      const totalRows = items.length + 1 // +1 for dot row\n      const key = buf.toString()\n\n      // Ctrl-C or q → quit\n      if (key === \"\\x03\" || key === \"q\" || key === \"Q\") {\n        cleanup()\n        process.removeListener(\"exit\", onExit)\n        process.removeListener(\"SIGINT\", onSigint)\n        resolve([])\n        return\n      }\n\n      // c → confirm\n      if (key === \"c\" || key === \"C\") {\n        cleanup()\n        process.removeListener(\"exit\", onExit)\n        process.removeListener(\"SIGINT\", onSigint)\n        resolve(collectSelected(tree))\n        return\n      }\n\n      // Arrow up\n      if (key === \"\\x1B[A\" || key === \"k\") {\n        if (cursor > 0) cursor--\n      }\n\n      // Arrow down\n      if (key === \"\\x1B[B\" || key === \"j\") {\n        if (cursor < totalRows - 1) cursor++\n      }\n\n      // Space or Enter on dot row, or . anywhere → toggle select all\n      if ((cursor === 0 && (key === \" \" || key === \"\\r\")) || key === \".\") {\n        const allSelected = tree.every((n) => n.selected)\n        for (const node of tree) setSelected(node, !allSelected)\n      }\n\n      // Space → toggle selection (tree items)\n      if (key === \" \" && cursor > 0) {\n        const item = items[cursor - 1]\n        if (item) {\n          const newValue = !item.node.selected\n          setSelected(item.node, newValue)\n          // If symlink selected, also select the target (but don't deselect it)\n          if (newValue && item.node.type === \"symlink\" && item.node.linkTarget) {\n            const targetPath = resolveSymlinkPath(item.node.path, item.node.linkTarget)\n            const targetNode = findNodeByPath(tree, targetPath)\n            if (targetNode) setSelected(targetNode, true)\n          }\n          updateParentSelection(tree)\n        }\n      }\n\n      // Enter → expand/collapse directory, preview file, or jump to symlink target\n      if (key === \"\\r\" && cursor > 0) {\n        const item = items[cursor - 1]\n        if (item && item.node.type === \"tree\") {\n          item.node.expanded = !item.node.expanded\n        } else if (item && item.node.type === \"symlink\" && item.node.linkTarget.endsWith(\"/\")) {\n          // Symlink to folder - resolve relative path and jump to target\n          const targetPath = resolveSymlinkPath(item.node.path, item.node.linkTarget)\n          // Expand all ancestors so target is visible\n          const pathParts = targetPath.split(\"/\")\n          for (let pi = 1; pi <= pathParts.length; pi++) {\n            const ancestorPath = pathParts.slice(0, pi).join(\"/\")\n            const ancestor = findNodeByPath(tree, ancestorPath)\n            if (ancestor && ancestor.type === \"tree\") ancestor.expanded = true\n          }\n          const targetNode = findNodeByPath(tree, targetPath)\n          if (targetNode) {\n            if (targetNode.type === \"tree\") targetNode.expanded = true\n            const updatedItems = flatten(tree)\n            const targetIdx = updatedItems.findIndex((fi) => fi.node === targetNode)\n            if (targetIdx >= 0) cursor = targetIdx + 1 // +1 for dot row\n          }\n        } else if (\n          item &&\n          basePath &&\n          (item.node.type === \"blob\" || item.node.type === \"symlink\")\n        ) {\n          showPreview(item.node)\n          return\n        }\n      }\n\n      // Right arrow / l → expand directory\n      if ((key === \"\\x1B[C\" || key === \"l\") && cursor > 0) {\n        const item = items[cursor - 1]\n        if (item && item.node.type === \"tree\") {\n          item.node.expanded = true\n        }\n      }\n\n      // Left arrow / h → collapse directory\n      if ((key === \"\\x1B[D\" || key === \"h\") && cursor > 0) {\n        const item = items[cursor - 1]\n        if (item && item.node.type === \"tree\") {\n          item.node.expanded = false\n        }\n      }\n\n      render()\n    }\n\n    stdin.on(\"data\", onKey)\n    render()\n  })\n}\n"
  },
  {
    "path": "bin/utils/parse-time-string.ts",
    "content": "export function parseTimeString(timeString: string | number): number {\n  if (typeof timeString === \"number\" || /^\\d+$/.test(timeString)) {\n    return typeof timeString === \"number\" ? timeString : parseInt(timeString, 10)\n  }\n\n  const regex = /(\\d+)([hms])/g\n  let totalMilliseconds = 0\n  let match: RegExpExecArray | null\n\n  while ((match = regex.exec(timeString)) !== null) {\n    const value = parseInt(match[1], 10)\n    const unit = match[2] as \"h\" | \"m\" | \"s\"\n\n    switch (unit) {\n      case \"h\":\n        totalMilliseconds += value * 3600000\n        break\n      case \"m\":\n        totalMilliseconds += value * 60000\n        break\n      case \"s\":\n        totalMilliseconds += value * 1000\n        break\n    }\n  }\n\n  return totalMilliseconds\n}\n"
  },
  {
    "path": "bin/utils/transform-url.ts",
    "content": "import { getDefaultBranch } from \"@/utils/get-default-branch\"\n\ntype Host = \"github.com\" | \"gitlab.com\" | \"bitbucket.org\" | \"codeberg.org\"\n\nconst PREFIXES: { prefix: string; host: Host }[] = [\n  { prefix: \"git@github.com:\", host: \"github.com\" },\n  { prefix: \"https://github.com/\", host: \"github.com\" },\n  { prefix: \"https://raw.githubusercontent.com/\", host: \"github.com\" },\n  { prefix: \"git@gitlab.com:\", host: \"gitlab.com\" },\n  { prefix: \"https://gitlab.com/\", host: \"gitlab.com\" },\n  { prefix: \"git@bitbucket.org:\", host: \"bitbucket.org\" },\n  { prefix: \"https://bitbucket.org/\", host: \"bitbucket.org\" },\n  { prefix: \"git@codeberg.org:\", host: \"codeberg.org\" },\n  { prefix: \"https://codeberg.org/\", host: \"codeberg.org\" },\n]\n\nexport async function configFromUrl(\n  url: string,\n  {\n    branch,\n    target,\n  }: {\n    branch?: string | null\n    target?: string | null\n  },\n) {\n  const tokenRegex = /^https:\\/\\/([^@]+)@(github\\.com|gitlab\\.com|bitbucket\\.org|codeberg\\.org)/\n  const tokenMatch = url.match(tokenRegex)\n\n  let token = \"\"\n  if (tokenMatch) {\n    token = tokenMatch[1]\n    url = url.replace(`${tokenMatch[1]}@`, \"\")\n  } else {\n    const hostTokens: Record<Host, string> = {\n      \"github.com\": process.env.GITHUB_TOKEN || process.env.GH_TOKEN || \"\",\n      \"gitlab.com\": process.env.GITLAB_TOKEN || \"\",\n      \"bitbucket.org\": process.env.BITBUCKET_TOKEN || \"\",\n      \"codeberg.org\": process.env.CODEBERG_TOKEN || \"\",\n    }\n    // Detect host early to pick the right env var\n    for (const { prefix, host: h } of PREFIXES) {\n      if (url.startsWith(prefix)) {\n        token = hostTokens[h]\n        break\n      }\n    }\n    // Shorthand (no prefix) defaults to github.com\n    if (!token && !url.startsWith(\"https://\") && !url.startsWith(\"git@\")) {\n      token = hostTokens[\"github.com\"]\n    }\n  }\n\n  let host: Host = \"github.com\"\n\n  for (const { prefix, host: h } of PREFIXES) {\n    if (url.startsWith(prefix)) {\n      host = h\n      url = url.replace(prefix, \"\")\n      break\n    }\n  }\n\n  const split = url.split(\"/\")\n  const owner = split[0]\n  const repository = split[1]?.endsWith(\".git\") ? split[1].slice(0, -4) : split[1]\n\n  const repoUrl = `https://${token ? token + \"@\" : token}${host}/${owner}/${repository}`\n\n  let type: string\n  let resolvedBranch: string\n  let resolvedPath: string\n\n  if (host === \"github.com\") {\n    if (split[2] === \"refs\" && [\"heads\", \"tags\"].includes(split[3])) {\n      type = \"raw\"\n      resolvedBranch = branch || split[4]\n      resolvedPath = split.slice(5).join(\"/\")\n    } else if (split[2] === \"refs\" && split[3] === \"remotes\") {\n      type = \"raw\"\n      resolvedBranch = branch || `${split[4]}/${split[5]}`\n      resolvedPath = split.slice(6).join(\"/\")\n    } else if (split[2] === \"blob\") {\n      type = \"blob\"\n      resolvedBranch = branch || split[3]\n      resolvedPath = split.slice(4).join(\"/\")\n    } else if (split[2] === \"tree\") {\n      type = \"tree\"\n      resolvedBranch = branch || split[3]\n      resolvedPath = split.slice(4).join(\"/\")\n    } else if (split[2] === \"commit\") {\n      type = \"repository\"\n      resolvedBranch = branch || split[3]\n      resolvedPath = \"\"\n    } else {\n      type = \"repository\"\n      resolvedBranch = branch || (await getDefaultBranch(repoUrl))\n      resolvedPath = \"\"\n    }\n  } else if (host === \"gitlab.com\") {\n    if (split[2] === \"-\" && split[3] === \"blob\") {\n      type = \"blob\"\n      resolvedBranch = branch || split[4]\n      resolvedPath = split.slice(5).join(\"/\")\n    } else if (split[2] === \"-\" && split[3] === \"tree\") {\n      type = \"tree\"\n      resolvedBranch = branch || split[4]\n      resolvedPath = split.slice(5).join(\"/\")\n    } else {\n      type = \"repository\"\n      resolvedBranch = branch || (await getDefaultBranch(repoUrl))\n      resolvedPath = \"\"\n    }\n  } else if (host === \"bitbucket.org\") {\n    // bitbucket.org — uses /src/branch/path for both files and dirs\n    if (split[2] === \"src\") {\n      type = \"tree\"\n      resolvedBranch = branch || split[3]\n      resolvedPath = split.slice(4).join(\"/\")\n    } else {\n      type = \"repository\"\n      resolvedBranch = branch || (await getDefaultBranch(repoUrl))\n      resolvedPath = \"\"\n    }\n  } else if (host === \"codeberg.org\") {\n    // codeberg.org — /src/<branch|tag|commit>/<ref>/path for files+dirs,\n    // /raw/... and /media/... for direct file (blob) links\n    if (\n      [\"src\", \"raw\", \"media\"].includes(split[2]) &&\n      [\"branch\", \"tag\", \"commit\"].includes(split[3])\n    ) {\n      type = split[2] === \"src\" ? \"tree\" : \"blob\"\n      resolvedBranch = branch || split[4]\n      resolvedPath = split.slice(5).join(\"/\")\n    } else {\n      type = \"repository\"\n      resolvedBranch = branch || (await getDefaultBranch(repoUrl))\n      resolvedPath = \"\"\n    }\n  } else {\n    const _exhaustive: never = host\n    throw new Error(`Unsupported host: ${_exhaustive}`)\n  }\n\n  const resolvedTarget = target\n    ? target\n    : type === \"blob\"\n      ? \".\"\n      : resolvedPath.split(\"/\").pop() || repository\n\n  return {\n    token,\n    host,\n    owner,\n    repository,\n    type,\n    branch: resolvedBranch,\n    path: resolvedPath,\n    target: resolvedTarget,\n  }\n}\n"
  },
  {
    "path": "bin/utils/update-notifier.ts",
    "content": "import fs from \"node:fs\"\nimport https from \"node:https\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nimport { bold, cyan, dim, yellow } from \"@/external/yoctocolors\"\n\nconst CACHE_DIR = path.join(os.homedir(), \".cache\", \"gitpick\")\nconst CACHE_FILE = path.join(CACHE_DIR, \"update-check.json\")\nconst CHECK_INTERVAL = 24 * 60 * 60 * 1000 // 24 hours\n\ntype UpdateCache = {\n  lastCheck: number\n  latestVersion: string\n}\n\nfunction readCache(): UpdateCache | null {\n  try {\n    return JSON.parse(fs.readFileSync(CACHE_FILE, \"utf-8\"))\n  } catch {\n    return null\n  }\n}\n\nfunction writeCache(cache: UpdateCache) {\n  try {\n    fs.mkdirSync(CACHE_DIR, { recursive: true })\n    fs.writeFileSync(CACHE_FILE, JSON.stringify(cache))\n  } catch {}\n}\n\nfunction fetchLatestVersion(): Promise<string | null> {\n  return new Promise((resolve) => {\n    const req = https.get(\n      \"https://registry.npmjs.org/gitpick/latest\",\n      { headers: { Accept: \"application/json\" }, timeout: 3000 },\n      (res) => {\n        if (res.statusCode !== 200) {\n          res.resume()\n          return resolve(null)\n        }\n        let data = \"\"\n        res.on(\"data\", (chunk) => (data += chunk))\n        res.on(\"end\", () => {\n          try {\n            resolve(JSON.parse(data).version || null)\n          } catch {\n            resolve(null)\n          }\n        })\n      },\n    )\n    req.on(\"error\", () => resolve(null))\n    req.on(\"timeout\", () => {\n      req.destroy()\n      resolve(null)\n    })\n  })\n}\n\nfunction isNewer(latest: string, current: string): boolean {\n  const l = latest.split(\".\").map(Number)\n  const c = current.split(\".\").map(Number)\n  for (let i = 0; i < 3; i++) {\n    if ((l[i] || 0) > (c[i] || 0)) return true\n    if ((l[i] || 0) < (c[i] || 0)) return false\n  }\n  return false\n}\n\n/** Print update notice if a cached newer version is known. */\nexport function notifyUpdate(currentVersion: string, silent: boolean) {\n  if (silent) return\n  const cache = readCache()\n  if (cache && isNewer(cache.latestVersion, currentVersion)) {\n    console.log(\n      dim(\n        `\\n  Update available: ${yellow(currentVersion)} → ${cyan(bold(cache.latestVersion))}` +\n          `\\n  Run ${cyan(\"npm i -g gitpick\")} to update\\n`,\n      ),\n    )\n  }\n}\n\n/** Background check — fetch latest version and cache it. Does not block. */\nexport function scheduleUpdateCheck() {\n  const cache = readCache()\n  if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL) return\n\n  setTimeout(async () => {\n    const latest = await fetchLatestVersion()\n    if (latest) {\n      writeCache({ lastCheck: Date.now(), latestVersion: latest })\n    }\n  }, 0)\n}\n"
  },
  {
    "path": "bin/utils/use-config.ts",
    "content": "import fs from \"node:fs\"\nimport path from \"node:path\"\n\nimport spawn from \"@/external/nano-spawn\"\nimport stripJsonComments from \"@/external/strip-json-comments\"\n\nconst configFiles = [\".gitpick.json\", \".gitpick.jsonc\"]\n\nexport const useConfig = async () => {\n  let configPath: string | undefined\n  for (const file of configFiles) {\n    const resolved = path.resolve(file)\n    if (fs.existsSync(resolved)) {\n      configPath = resolved\n      break\n    }\n  }\n\n  if (!configPath) return false\n\n  const content = await fs.promises.readFile(configPath, \"utf-8\")\n  const entries = JSON.parse(stripJsonComments(content))\n\n  if (!Array.isArray(entries) || !entries.every((e: unknown) => typeof e === \"string\")) {\n    throw new Error(`${path.basename(configPath)} must be an array of strings`)\n  }\n\n  for (const entry of entries) {\n    await spawn(process.argv[0], [...process.argv.slice(1), ...entry.split(/\\s+/), \"-o\"], {\n      stdio: \"inherit\",\n    })\n  }\n\n  return true\n}\n"
  },
  {
    "path": "lefthook.yml",
    "content": "pre-commit:\n  piped: true\n  commands:\n    lint-staged:\n      run: bunx lint-staged --verbose\n      stage_fixed: true\n\ncommit-msg:\n  commands:\n    commitlint:\n      run: bunx commitlint --edit {1}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"gitpick\",\n  \"version\": \"5.4.1\",\n  \"description\": \"Clone exactly what you need aka straightforward project scaffolding!\",\n  \"keywords\": [\n    \"clone\",\n    \"degit\",\n    \"directory\",\n    \"file\",\n    \"folder\",\n    \"git\",\n    \"github\",\n    \"repository\",\n    \"scaffolding\",\n    \"template\",\n    \"url\"\n  ],\n  \"homepage\": \"https://github.com/nrjdalal/gitpick#readme\",\n  \"bugs\": \"https://github.com/nrjdalal/gitpick/issues\",\n  \"license\": \"MIT\",\n  \"author\": {\n    \"name\": \"Neeraj Dalal\",\n    \"email\": \"admin@nrjdalal.com\",\n    \"url\": \"https://nrjdalal.com\"\n  },\n  \"repository\": \"nrjdalal/gitpick\",\n  \"funding\": \"https://github.com/sponsors/nrjdalal\",\n  \"bin\": {\n    \"degit\": \"./dist/index.mjs\",\n    \"gitpick\": \"./dist/index.mjs\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"tsdown\",\n    \"dev\": \"tsdown --watch\",\n    \"format\": \"oxfmt\",\n    \"format:check\": \"oxfmt --check\",\n    \"prepare\": \"bunx lefthook install\",\n    \"test\": \"tsdown && bun test tests/cli.test.ts\"\n  },\n  \"devDependencies\": {\n    \"@commitlint/cli\": \"^20.5.0\",\n    \"@commitlint/config-conventional\": \"^20.5.0\",\n    \"@types/node\": \"^25.5.0\",\n    \"lefthook\": \"^2.1.4\",\n    \"lint-staged\": \"^16.4.0\",\n    \"oxfmt\": \"^0.41.0\",\n    \"oxlint\": \"^1.56.0\",\n    \"sort-package-json\": \"^3.6.1\",\n    \"tsdown\": \"^0.21.4\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"commitlint\": {\n    \"extends\": [\n      \"@commitlint/config-conventional\"\n    ]\n  }\n}\n"
  },
  {
    "path": "tests/cli.test.ts",
    "content": "import { beforeAll, describe, expect, it } from \"bun:test\"\nimport {\n  copyFileSync,\n  existsSync,\n  lstatSync,\n  mkdirSync,\n  readFileSync,\n  readlinkSync,\n  readdirSync,\n  rmSync,\n  writeFileSync,\n} from \"node:fs\"\nimport { join, resolve } from \"node:path\"\n\nconst CLI = [\"node\", resolve(\"dist/index.mjs\")]\nconst ARTIFACTS = \".test-artifacts\"\n\n// --- helpers ---\n\nfunction stripAnsi(s: string) {\n  // eslint-disable-next-line no-control-regex\n  return s.replace(/\\x1b\\[[0-9;]*m/g, \"\").replace(/\\x1b\\]8;;[^\\x07]*\\x07/g, \"\")\n}\n\nasync function run(args: string[], cwd?: string) {\n  const proc = Bun.spawn([...CLI, ...args], {\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n    cwd,\n  })\n  const [stdout, stderr] = await Promise.all([\n    new Response(proc.stdout).text(),\n    new Response(proc.stderr).text(),\n  ])\n  return { output: stdout + stderr, exitCode: await proc.exited }\n}\n\nfunction parseLine(output: string) {\n  return (\n    stripAnsi(output)\n      .split(\"\\n\")\n      .find((l) => l.includes(\"✔\")) || \"\"\n  )\n}\n\nfunction tree(dir: string, prefix = \"\"): string {\n  const entries = readdirSync(dir, { withFileTypes: true })\n    .filter((e) => e.name !== \".git\")\n    .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: \"base\" }))\n  let out = \"\"\n  entries.forEach((e, i) => {\n    const last = i === entries.length - 1\n    const connector = last ? \"└── \" : \"├── \"\n    if (e.isSymbolicLink()) {\n      out += `${prefix}${connector}${e.name} -> ${readlinkSync(join(dir, e.name))}\\n`\n    } else if (e.isDirectory()) {\n      out += `${prefix}${connector}${e.name}\\n`\n      out += tree(join(dir, e.name), `${prefix}${last ? \"    \" : \"│   \"}`)\n    } else {\n      out += `${prefix}${connector}${e.name}\\n`\n    }\n  })\n  return out\n}\n\nfunction getTree(dir: string) {\n  if (!existsSync(dir)) return \"\"\n  if (!lstatSync(dir).isDirectory()) return \"(file)\"\n  return tree(dir).trimEnd()\n}\n\n// --- tree snapshots (sorted case-insensitive for bun's readdirSync) ---\n\nconst TREE_REPO_MAIN = [\n  \"├── file.txt\",\n  \"├── folder\",\n  \"│   ├── deep\",\n  \"│   │   └── file.txt\",\n  \"│   └── nested.txt\",\n  \"├── README.md\",\n  \"├── symdir -> folder\",\n  \"└── symlink.txt -> file.txt\",\n].join(\"\\n\")\n\nconst TREE_REPO_DEV = [\n  \"├── dev.txt\",\n  \"├── file.txt\",\n  \"├── folder\",\n  \"│   ├── deep\",\n  \"│   │   └── file.txt\",\n  \"│   └── nested.txt\",\n  \"├── README.md\",\n  \"├── symdir -> folder\",\n  \"└── symlink.txt -> file.txt\",\n].join(\"\\n\")\n\nconst TREE_FOLDER = [\"├── deep\", \"│   └── file.txt\", \"└── nested.txt\"].join(\"\\n\")\nconst TREE_FOLDER_DEEP = \"└── file.txt\"\nconst TREE_BLOB_FILE = \"└── file.txt\"\nconst TREE_BLOB_NESTED = \"└── nested.txt\"\nconst TREE_BLOB_README = \"└── README.md\"\n\nconst TREE_GITLAB_REPO = [\n  \"├── .gitlab-ci.yml\",\n  \"├── public\",\n  \"│   ├── index.html\",\n  \"│   └── style.css\",\n  \"└── README.md\",\n].join(\"\\n\")\n\nconst TREE_GITLAB_PUBLIC = [\"├── index.html\", \"└── style.css\"].join(\"\\n\")\n\n// --- test counter for unique artifact dirs ---\n\nlet n = 0\nfunction target() {\n  return join(ARTIFACTS, \"cli\", String(++n))\n}\n\n// --- high-level clone helper ---\n\nasync function cloneAndExpect(\n  args: string[],\n  expectedOutput: string,\n  expectedTree: string,\n  customTarget?: string,\n) {\n  const t = customTarget ? join(ARTIFACTS, \"cli\", customTarget) : target()\n  if (existsSync(t) && !customTarget) rmSync(t, { recursive: true, force: true })\n\n  const { output, exitCode } = await run([\"clone\", ...args, t])\n  expect(parseLine(output)).toContain(expectedOutput)\n  expect(exitCode).toBe(0)\n\n  if (expectedTree === \"(file)\") {\n    expect(lstatSync(t).isFile()).toBe(true)\n  } else if (expectedTree) {\n    expect(getTree(t)).toBe(expectedTree)\n  } else {\n    expect(existsSync(t)).toBe(true)\n  }\n}\n\n// --- setup ---\n\nbeforeAll(() => {\n  rmSync(join(ARTIFACTS, \"cli\"), { recursive: true, force: true })\n  rmSync(join(ARTIFACTS, \"config\"), { recursive: true, force: true })\n  mkdirSync(join(ARTIFACTS, \"cli\"), { recursive: true })\n})\n\n// =====================================================================\n// DRY-RUN TESTS\n// =====================================================================\n\ndescribe(\"dry-run — URL parsing without cloning\", () => {\n  async function dryRun(args: string[], expected: string) {\n    const { output, exitCode } = await run([...args, \"--dry-run\"])\n    expect(exitCode).toBe(0)\n    expect(parseLine(output)).toContain(expected)\n  }\n\n  // repo\n  it(\n    \"shorthand repo\",\n    () => dryRun([\"nrjdalal/picksuite\"], \"nrjdalal/picksuite repository:main > picksuite\"),\n    30000,\n  )\n  it(\n    \"full URL repo\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite\"],\n        \"nrjdalal/picksuite repository:main > picksuite\",\n      ),\n    30000,\n  )\n\n  // tree\n  it(\n    \"shorthand tree\",\n    () =>\n      dryRun(\n        [\"nrjdalal/picksuite/tree/main/folder\"],\n        \"nrjdalal/picksuite tree:main folder > folder\",\n      ),\n    30000,\n  )\n  it(\n    \"full URL tree\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite/tree/main/folder\"],\n        \"nrjdalal/picksuite tree:main folder > folder\",\n      ),\n    30000,\n  )\n  it(\n    \"nested tree\",\n    () =>\n      dryRun(\n        [\"nrjdalal/picksuite/tree/main/folder/deep\"],\n        \"nrjdalal/picksuite tree:main folder/deep > deep\",\n      ),\n    30000,\n  )\n  it(\n    \"full URL nested tree\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite/tree/main/folder/deep\"],\n        \"nrjdalal/picksuite tree:main folder/deep > deep\",\n      ),\n    30000,\n  )\n\n  // blob\n  it(\n    \"shorthand blob\",\n    () =>\n      dryRun(\n        [\"nrjdalal/picksuite/blob/main/file.txt\"],\n        \"nrjdalal/picksuite blob:main file.txt > ./file.txt\",\n      ),\n    30000,\n  )\n  it(\n    \"full URL blob\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite/blob/main/file.txt\"],\n        \"nrjdalal/picksuite blob:main file.txt > ./file.txt\",\n      ),\n    30000,\n  )\n  it(\n    \"nested blob\",\n    () =>\n      dryRun(\n        [\"nrjdalal/picksuite/blob/main/folder/nested.txt\"],\n        \"nrjdalal/picksuite blob:main folder/nested.txt > ./nested.txt\",\n      ),\n    30000,\n  )\n  it(\n    \"deep nested blob\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite/blob/main/folder/deep/file.txt\"],\n        \"nrjdalal/picksuite blob:main folder/deep/file.txt > ./file.txt\",\n      ),\n    30000,\n  )\n\n  // branch\n  it(\n    \"-b dev\",\n    () =>\n      dryRun([\"nrjdalal/picksuite\", \"-b\", \"dev\"], \"nrjdalal/picksuite repository:dev > picksuite\"),\n    30000,\n  )\n  it(\n    \"full URL -b dev\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite\", \"-b\", \"dev\"],\n        \"nrjdalal/picksuite repository:dev > picksuite\",\n      ),\n    30000,\n  )\n  it(\n    \"tree/dev\",\n    () => dryRun([\"nrjdalal/picksuite/tree/dev\"], \"nrjdalal/picksuite tree:dev > picksuite\"),\n    30000,\n  )\n  it(\n    \"full URL tree/dev\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite/tree/dev\"],\n        \"nrjdalal/picksuite tree:dev > picksuite\",\n      ),\n    30000,\n  )\n\n  // commit SHA\n  it(\n    \"-b SHA\",\n    () =>\n      dryRun(\n        [\"nrjdalal/picksuite\", \"-b\", \"8af536b\"],\n        \"nrjdalal/picksuite repository:8af536b > picksuite\",\n      ),\n    30000,\n  )\n  it(\n    \"/commit/ URL\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite/commit/8af536b\"],\n        \"nrjdalal/picksuite repository:8af536b > picksuite\",\n      ),\n    30000,\n  )\n\n  // submodules\n  it(\n    \"-r shorthand\",\n    () => dryRun([\"nrjdalal/picksuite\", \"-r\"], \"nrjdalal/picksuite repository:main > picksuite\"),\n    30000,\n  )\n  it(\n    \"-r full URL\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite\", \"-r\"],\n        \"nrjdalal/picksuite repository:main > picksuite\",\n      ),\n    30000,\n  )\n\n  // token\n  it(\n    \"token URL\",\n    () =>\n      dryRun(\n        [\"https://fake_token@github.com/nrjdalal/picksuite\"],\n        \"nrjdalal/picksuite repository:main > picksuite\",\n      ),\n    30000,\n  )\n\n  // git@ and .git\n  it(\n    \"git@\",\n    () =>\n      dryRun(\n        [\"git@github.com:nrjdalal/picksuite.git\"],\n        \"nrjdalal/picksuite repository:main > picksuite\",\n      ),\n    30000,\n  )\n  it(\n    \".git suffix\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite.git\"],\n        \"nrjdalal/picksuite repository:main > picksuite\",\n      ),\n    30000,\n  )\n  it(\n    \"git@ tree\",\n    () =>\n      dryRun(\n        [\"git@github.com:nrjdalal/picksuite.git/tree/main/folder\"],\n        \"nrjdalal/picksuite tree:main folder > folder\",\n      ),\n    30000,\n  )\n  it(\n    \".git tree\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite.git/tree/main/folder\"],\n        \"nrjdalal/picksuite tree:main folder > folder\",\n      ),\n    30000,\n  )\n\n  // branch override\n  it(\n    \"branch override\",\n    () =>\n      dryRun(\n        [\"nrjdalal/picksuite/tree/dev/folder\", \"-b\", \"main\"],\n        \"nrjdalal/picksuite tree:main folder > folder\",\n      ),\n    30000,\n  )\n  it(\n    \"branch override URL\",\n    () =>\n      dryRun(\n        [\"https://github.com/nrjdalal/picksuite/tree/dev/folder\", \"-b\", \"main\"],\n        \"nrjdalal/picksuite tree:main folder > folder\",\n      ),\n    30000,\n  )\n\n  // custom target\n  it(\n    \"custom target repo\",\n    () => dryRun([\"nrjdalal/picksuite\", \"my-dir\"], \"nrjdalal/picksuite repository:main > my-dir\"),\n    30000,\n  )\n  it(\n    \"custom target tree\",\n    () =>\n      dryRun(\n        [\"nrjdalal/picksuite/tree/main/folder\", \"my-folder\"],\n        \"nrjdalal/picksuite tree:main folder > my-folder\",\n      ),\n    30000,\n  )\n\n  // raw URL\n  it(\n    \"raw URL refs/heads\",\n    () =>\n      dryRun(\n        [\"https://raw.githubusercontent.com/nrjdalal/picksuite/refs/heads/main/file.txt\"],\n        \"nrjdalal/picksuite raw:main file.txt > file.txt\",\n      ),\n    30000,\n  )\n  it(\n    \"raw URL refs/tags\",\n    () =>\n      dryRun(\n        [\"https://raw.githubusercontent.com/nrjdalal/picksuite/refs/tags/main/file.txt\"],\n        \"nrjdalal/picksuite raw:main file.txt > file.txt\",\n      ),\n    30000,\n  )\n  it(\n    \"raw shorthand refs/heads\",\n    () =>\n      dryRun(\n        [\"nrjdalal/picksuite/refs/heads/main/file.txt\"],\n        \"nrjdalal/picksuite raw:main file.txt > file.txt\",\n      ),\n    30000,\n  )\n  it(\n    \"raw URL refs/remotes\",\n    () =>\n      dryRun(\n        [\"https://raw.githubusercontent.com/nrjdalal/picksuite/refs/remotes/origin/main/file.txt\"],\n        \"nrjdalal/picksuite raw:origin/main file.txt > file.txt\",\n      ),\n    30000,\n  )\n\n  // gitlab\n  it(\n    \"gitlab repo\",\n    () =>\n      dryRun(\n        [\"https://gitlab.com/pages/plain-html\", \"-b\", \"main\"],\n        \"pages/plain-html repository:main > plain-html\",\n      ),\n    30000,\n  )\n  it(\n    \"gitlab tree\",\n    () =>\n      dryRun(\n        [\"https://gitlab.com/pages/plain-html/-/tree/main/public\"],\n        \"pages/plain-html tree:main public > public\",\n      ),\n    30000,\n  )\n  it(\n    \"gitlab blob\",\n    () =>\n      dryRun(\n        [\"https://gitlab.com/pages/plain-html/-/blob/main/README.md\"],\n        \"pages/plain-html blob:main README.md > ./README.md\",\n      ),\n    30000,\n  )\n\n  // bitbucket\n  it(\n    \"bitbucket repo\",\n    () =>\n      dryRun(\n        [\"https://bitbucket.org/snakeyaml/snakeyaml\", \"-b\", \"master\"],\n        \"snakeyaml/snakeyaml repository:master > snakeyaml\",\n      ),\n    30000,\n  )\n  it(\n    \"bitbucket src path\",\n    () =>\n      dryRun(\n        [\"https://bitbucket.org/snakeyaml/snakeyaml/src/master/src\"],\n        \"snakeyaml/snakeyaml tree:master src > src\",\n      ),\n    30000,\n  )\n\n  // codeberg\n  it(\n    \"codeberg repo\",\n    () =>\n      dryRun(\n        [\"https://codeberg.org/Codeberg/avatars\", \"-b\", \"main\"],\n        \"Codeberg/avatars repository:main > avatars\",\n      ),\n    30000,\n  )\n  it(\n    \"codeberg src/branch path\",\n    () =>\n      dryRun(\n        [\"https://codeberg.org/Codeberg/avatars/src/branch/main/example\"],\n        \"Codeberg/avatars tree:main example > example\",\n      ),\n    30000,\n  )\n  it(\n    \"codeberg src/tag path\",\n    () =>\n      dryRun(\n        [\"https://codeberg.org/Codeberg/avatars/src/tag/v1.0.0/example\"],\n        \"Codeberg/avatars tree:v1.0.0 example > example\",\n      ),\n    30000,\n  )\n  it(\n    \"codeberg src/commit path\",\n    () =>\n      dryRun(\n        [\n          \"https://codeberg.org/Codeberg/avatars/src/commit/c86887927797ce57a7e4666494903a4e9b1e901c/example\",\n        ],\n        \"Codeberg/avatars tree:c86887927797ce57a7e4666494903a4e9b1e901c example > example\",\n      ),\n    30000,\n  )\n  it(\n    \"codeberg raw/branch file\",\n    () =>\n      dryRun(\n        [\"https://codeberg.org/Codeberg/avatars/raw/branch/main/README.md\"],\n        \"Codeberg/avatars blob:main README.md > ./README.md\",\n      ),\n    30000,\n  )\n  it(\n    \"codeberg media/branch file\",\n    () =>\n      dryRun(\n        [\"https://codeberg.org/Codeberg/avatars/media/branch/main/README.md\"],\n        \"Codeberg/avatars blob:main README.md > ./README.md\",\n      ),\n    30000,\n  )\n  it(\n    \"codeberg git@ repo\",\n    () =>\n      dryRun(\n        [\"git@codeberg.org:Codeberg/avatars\", \"-b\", \"main\"],\n        \"Codeberg/avatars repository:main > avatars\",\n      ),\n    30000,\n  )\n  it(\n    \"codeberg git@ src path\",\n    () =>\n      dryRun(\n        [\"git@codeberg.org:Codeberg/avatars/src/branch/main/example\"],\n        \"Codeberg/avatars tree:main example > example\",\n      ),\n    30000,\n  )\n})\n\n// =====================================================================\n// CLI CLONE TESTS\n// =====================================================================\n\ndescribe(\"default — gitpick <url/shorthand>\", () => {\n  it(\n    \"shorthand repo\",\n    () =>\n      cloneAndExpect([\"nrjdalal/picksuite\"], \"nrjdalal/picksuite repository:main\", TREE_REPO_MAIN),\n    30000,\n  )\n  it(\n    \"full URL repo\",\n    () =>\n      cloneAndExpect(\n        [\"https://github.com/nrjdalal/picksuite\"],\n        \"nrjdalal/picksuite repository:main\",\n        TREE_REPO_MAIN,\n      ),\n    30000,\n  )\n  it(\n    \"git@ repo\",\n    () =>\n      cloneAndExpect(\n        [\"git@github.com:nrjdalal/picksuite.git\"],\n        \"nrjdalal/picksuite repository:main\",\n        TREE_REPO_MAIN,\n      ),\n    30000,\n  )\n  it(\n    \".git suffix repo\",\n    () =>\n      cloneAndExpect(\n        [\"https://github.com/nrjdalal/picksuite.git\"],\n        \"nrjdalal/picksuite repository:main\",\n        TREE_REPO_MAIN,\n      ),\n    30000,\n  )\n\n  it(\n    \"shorthand tree\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/tree/main/folder\"],\n        \"nrjdalal/picksuite tree:main folder\",\n        TREE_FOLDER,\n      ),\n    30000,\n  )\n  it(\n    \"full URL tree\",\n    () =>\n      cloneAndExpect(\n        [\"https://github.com/nrjdalal/picksuite/tree/main/folder\"],\n        \"nrjdalal/picksuite tree:main folder\",\n        TREE_FOLDER,\n      ),\n    30000,\n  )\n  it(\n    \"git@ tree\",\n    () =>\n      cloneAndExpect(\n        [\"git@github.com:nrjdalal/picksuite.git/tree/main/folder\"],\n        \"nrjdalal/picksuite tree:main folder\",\n        TREE_FOLDER,\n      ),\n    30000,\n  )\n  it(\n    \".git suffix tree\",\n    () =>\n      cloneAndExpect(\n        [\"https://github.com/nrjdalal/picksuite.git/tree/main/folder\"],\n        \"nrjdalal/picksuite tree:main folder\",\n        TREE_FOLDER,\n      ),\n    30000,\n  )\n  it(\n    \"nested tree\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/tree/main/folder/deep\"],\n        \"nrjdalal/picksuite tree:main folder/deep\",\n        TREE_FOLDER_DEEP,\n      ),\n    30000,\n  )\n\n  it(\n    \"shorthand blob\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/blob/main/file.txt\"],\n        \"nrjdalal/picksuite blob:main file.txt\",\n        TREE_BLOB_FILE,\n      ),\n    30000,\n  )\n  it(\n    \"full URL blob\",\n    () =>\n      cloneAndExpect(\n        [\"https://github.com/nrjdalal/picksuite/blob/main/file.txt\"],\n        \"nrjdalal/picksuite blob:main file.txt\",\n        TREE_BLOB_FILE,\n      ),\n    30000,\n  )\n  it(\n    \"nested blob\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/blob/main/folder/nested.txt\"],\n        \"nrjdalal/picksuite blob:main folder/nested.txt\",\n        TREE_BLOB_NESTED,\n      ),\n    30000,\n  )\n  it(\n    \"deep nested blob\",\n    () =>\n      cloneAndExpect(\n        [\"https://github.com/nrjdalal/picksuite/blob/main/folder/deep/file.txt\"],\n        \"nrjdalal/picksuite blob:main folder/deep/file.txt\",\n        TREE_BLOB_FILE,\n      ),\n    30000,\n  )\n})\n\ndescribe(\"no prefix — gitpick <url> (without clone keyword)\", () => {\n  async function noPrefixClone(args: string[], expectedOutput: string, expectedTree: string) {\n    const t = target()\n    if (existsSync(t)) rmSync(t, { recursive: true, force: true })\n\n    const { output, exitCode } = await run([...args, t])\n    expect(parseLine(output)).toContain(expectedOutput)\n    expect(exitCode).toBe(0)\n\n    if (expectedTree === \"(file)\") {\n      expect(lstatSync(t).isFile()).toBe(true)\n    } else if (expectedTree) {\n      expect(getTree(t)).toBe(expectedTree)\n    } else {\n      expect(existsSync(t)).toBe(true)\n    }\n  }\n\n  it(\n    \"repo without clone prefix\",\n    () =>\n      noPrefixClone([\"nrjdalal/picksuite\"], \"nrjdalal/picksuite repository:main\", TREE_REPO_MAIN),\n    30000,\n  )\n  it(\n    \"tree without clone prefix\",\n    () =>\n      noPrefixClone(\n        [\"nrjdalal/picksuite/tree/main/folder\"],\n        \"nrjdalal/picksuite tree:main folder\",\n        TREE_FOLDER,\n      ),\n    30000,\n  )\n  it(\n    \"blob without clone prefix\",\n    () =>\n      noPrefixClone(\n        [\"nrjdalal/picksuite/blob/main/file.txt\"],\n        \"nrjdalal/picksuite blob:main file.txt\",\n        TREE_BLOB_FILE,\n      ),\n    30000,\n  )\n})\n\ndescribe(\"target — gitpick <url> [target]\", () => {\n  it(\n    \"repo → custom dir\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite\"],\n        \"nrjdalal/picksuite repository:main\",\n        TREE_REPO_MAIN,\n        \"my-repo\",\n      ),\n    30000,\n  )\n  it(\n    \"tree → custom dir\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/tree/main/folder\"],\n        \"nrjdalal/picksuite tree:main folder\",\n        TREE_FOLDER,\n        \"my-folder\",\n      ),\n    30000,\n  )\n  it(\n    \"blob → custom dir\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/blob/main/file.txt\"],\n        \"nrjdalal/picksuite blob:main file.txt\",\n        TREE_BLOB_FILE,\n        \"my-blob\",\n      ),\n    30000,\n  )\n\n  it(\n    \"repo → nested path\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite\"],\n        \"nrjdalal/picksuite repository:main\",\n        TREE_REPO_MAIN,\n        \"nested/path/repo\",\n      ),\n    30000,\n  )\n  it(\n    \"tree → nested path\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/tree/main/folder\"],\n        \"nrjdalal/picksuite tree:main folder\",\n        TREE_FOLDER,\n        \"nested/path/folder\",\n      ),\n    30000,\n  )\n  it(\n    \"blob → nested path\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/blob/main/file.txt\"],\n        \"nrjdalal/picksuite blob:main file.txt\",\n        TREE_BLOB_FILE,\n        \"nested/path/blob\",\n      ),\n    30000,\n  )\n\n  it(\n    \"blob → renamed.txt\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/blob/main/file.txt\"],\n        \"nrjdalal/picksuite blob:main file.txt\",\n        \"(file)\",\n        \"renamed.txt\",\n      ),\n    30000,\n  )\n  it(\n    \"blob → nested renamed\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/blob/main/file.txt\"],\n        \"nrjdalal/picksuite blob:main file.txt\",\n        \"(file)\",\n        \"some/path/renamed.txt\",\n      ),\n    30000,\n  )\n})\n\ndescribe(\"branch — gitpick <url> -b [branch/SHA]\", () => {\n  it(\n    \"-b dev shorthand\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite\", \"-b\", \"dev\"],\n        \"nrjdalal/picksuite repository:dev\",\n        TREE_REPO_DEV,\n      ),\n    30000,\n  )\n  it(\n    \"-b dev full URL\",\n    () =>\n      cloneAndExpect(\n        [\"https://github.com/nrjdalal/picksuite\", \"-b\", \"dev\"],\n        \"nrjdalal/picksuite repository:dev\",\n        TREE_REPO_DEV,\n      ),\n    30000,\n  )\n  it(\n    \"tree/dev\",\n    () =>\n      cloneAndExpect([\"nrjdalal/picksuite/tree/dev\"], \"nrjdalal/picksuite tree:dev\", TREE_REPO_DEV),\n    30000,\n  )\n  it(\n    \"tree/dev URL\",\n    () =>\n      cloneAndExpect(\n        [\"https://github.com/nrjdalal/picksuite/tree/dev\"],\n        \"nrjdalal/picksuite tree:dev\",\n        TREE_REPO_DEV,\n      ),\n    30000,\n  )\n\n  it(\n    \"-b short SHA\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite\", \"-b\", \"8af536b\"],\n        \"nrjdalal/picksuite repository:8af536b\",\n        TREE_REPO_MAIN,\n      ),\n    30000,\n  )\n  it(\n    \"/commit/ URL\",\n    () =>\n      cloneAndExpect(\n        [\"https://github.com/nrjdalal/picksuite/commit/8af536b\"],\n        \"nrjdalal/picksuite repository:8af536b\",\n        TREE_REPO_MAIN,\n      ),\n    30000,\n  )\n  it(\n    \"-b full SHA\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite\", \"-b\", \"8af536b29d38630301fc47f2e088c41248d41932\"],\n        \"nrjdalal/picksuite repository:8af536b29d38630301fc47f2e088c41248d41932\",\n        TREE_REPO_MAIN,\n      ),\n    30000,\n  )\n\n  it(\n    \"branch override\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/tree/dev/folder\", \"-b\", \"main\"],\n        \"nrjdalal/picksuite tree:main folder\",\n        TREE_FOLDER,\n      ),\n    30000,\n  )\n  it(\n    \"branch override URL\",\n    () =>\n      cloneAndExpect(\n        [\"https://github.com/nrjdalal/picksuite/tree/dev/folder\", \"-b\", \"main\"],\n        \"nrjdalal/picksuite tree:main folder\",\n        TREE_FOLDER,\n      ),\n    30000,\n  )\n\n  it(\n    \"blob from dev branch\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite/blob/dev/dev.txt\"],\n        \"nrjdalal/picksuite blob:dev dev.txt\",\n        \"\",\n      ),\n    30000,\n  )\n})\n\ndescribe(\"overwrite — gitpick <url> -o / -f\", () => {\n  it(\"-o re-clone\", async () => {\n    const t = target()\n    await run([\"clone\", \"nrjdalal/picksuite/tree/main/folder\", \"-o\", t])\n    const { output, exitCode } = await run([\n      \"clone\",\n      \"nrjdalal/picksuite/tree/main/folder\",\n      \"-o\",\n      t,\n    ])\n    expect(exitCode).toBe(0)\n    expect(parseLine(output)).toContain(\"nrjdalal/picksuite tree:main folder\")\n    expect(getTree(t)).toBe(TREE_FOLDER)\n  }, 60000)\n\n  it(\"-f re-clone\", async () => {\n    const t = target()\n    await run([\"clone\", \"nrjdalal/picksuite/tree/main/folder\", \"-f\", t])\n    const { output, exitCode } = await run([\n      \"clone\",\n      \"nrjdalal/picksuite/tree/main/folder\",\n      \"-f\",\n      t,\n    ])\n    expect(exitCode).toBe(0)\n    expect(parseLine(output)).toContain(\"nrjdalal/picksuite tree:main folder\")\n    expect(getTree(t)).toBe(TREE_FOLDER)\n  }, 60000)\n})\n\ndescribe(\"overwrite rejection\", () => {\n  it(\"rejects non-empty dir without -o\", async () => {\n    const t = target()\n    mkdirSync(t, { recursive: true })\n    writeFileSync(join(t, \"existing.txt\"), \"existing\")\n\n    const { output, exitCode } = await run([\"clone\", \"nrjdalal/picksuite\", t])\n    expect(exitCode).toBe(1)\n    expect(stripAnsi(output)).toContain(\"not empty\")\n  }, 30000)\n\n  it(\"rejects existing blob without -o\", async () => {\n    const t = target()\n    mkdirSync(t, { recursive: true })\n    writeFileSync(join(t, \"file.txt\"), \"existing\")\n\n    const { output, exitCode } = await run([\"clone\", \"nrjdalal/picksuite/blob/main/file.txt\", t])\n    expect(exitCode).toBe(1)\n    expect(stripAnsi(output)).toContain(\"target file exists\")\n  }, 30000)\n\n  it(\"allows clone into empty dir\", async () => {\n    const t = target()\n    mkdirSync(t, { recursive: true })\n\n    const { exitCode } = await run([\"clone\", \"nrjdalal/picksuite\", t])\n    expect(exitCode).toBe(0)\n    expect(getTree(t)).toBe(TREE_REPO_MAIN)\n  }, 30000)\n})\n\ndescribe(\"recursive — gitpick <url> -r\", () => {\n  it(\n    \"-r shorthand\",\n    () =>\n      cloneAndExpect(\n        [\"nrjdalal/picksuite\", \"-r\"],\n        \"nrjdalal/picksuite repository:main\",\n        TREE_REPO_MAIN,\n      ),\n    30000,\n  )\n  it(\n    \"-r full URL\",\n    () =>\n      cloneAndExpect(\n        [\"https://github.com/nrjdalal/picksuite\", \"-r\"],\n        \"nrjdalal/picksuite repository:main\",\n        TREE_REPO_MAIN,\n      ),\n    30000,\n  )\n})\n\ndescribe(\"raw URL\", () => {\n  it(\n    \"raw.githubusercontent.com\",\n    () =>\n      cloneAndExpect(\n        [\"https://raw.githubusercontent.com/nrjdalal/picksuite/refs/heads/main/file.txt\"],\n        \"nrjdalal/picksuite raw:main file.txt\",\n        \"(file)\",\n      ),\n    30000,\n  )\n})\n\ndescribe(\"token URL\", () => {\n  it(\n    \"public repo with token\",\n    () =>\n      cloneAndExpect(\n        [\"https://fake_token@github.com/nrjdalal/picksuite\"],\n        \"nrjdalal/picksuite repository:main\",\n        TREE_REPO_MAIN,\n      ),\n    30000,\n  )\n})\n\ndescribe(\"gitlab\", () => {\n  it(\n    \"repo\",\n    () =>\n      cloneAndExpect(\n        [\"https://gitlab.com/pages/plain-html\", \"-b\", \"main\"],\n        \"pages/plain-html repository:main\",\n        TREE_GITLAB_REPO,\n      ),\n    30000,\n  )\n  it(\n    \"tree\",\n    () =>\n      cloneAndExpect(\n        [\"https://gitlab.com/pages/plain-html/-/tree/main/public\"],\n        \"pages/plain-html tree:main public\",\n        TREE_GITLAB_PUBLIC,\n      ),\n    30000,\n  )\n  it(\n    \"blob\",\n    () =>\n      cloneAndExpect(\n        [\"https://gitlab.com/pages/plain-html/-/blob/main/README.md\"],\n        \"pages/plain-html blob:main README.md\",\n        TREE_BLOB_README,\n      ),\n    30000,\n  )\n})\n\n// =====================================================================\n// CLI FLAGS\n// =====================================================================\n\ndescribe(\"CLI flags\", () => {\n  it(\"--version\", async () => {\n    const { output, exitCode } = await run([\"--version\"])\n    expect(exitCode).toBe(0)\n    expect(stripAnsi(output)).toContain(\"gitpick@\")\n  })\n\n  it(\"--help (no args)\", async () => {\n    const { output, exitCode } = await run([])\n    expect(exitCode).toBe(0)\n    expect(stripAnsi(output)).toContain(\"clone specific directories or files\")\n  })\n\n  it(\"--dry-run exits 0 without cloning\", async () => {\n    const { output, exitCode } = await run([\"nrjdalal/picksuite\", \"--dry-run\"])\n    expect(exitCode).toBe(0)\n    expect(parseLine(output)).toContain(\"nrjdalal/picksuite repository:main\")\n  }, 30000)\n})\n\n// =====================================================================\n// INTEGRITY\n// =====================================================================\n\ndescribe(\"integrity — .git exclusion, symlinks, content\", () => {\n  // references first clone (test #1 in \"default\" section)\n  const repoDir = join(ARTIFACTS, \"cli\", \"1\")\n\n  it(\".git excluded\", () => {\n    expect(existsSync(join(repoDir, \".git\"))).toBe(false)\n  })\n\n  it(\"symlink.txt is a symlink\", () => {\n    expect(lstatSync(join(repoDir, \"symlink.txt\")).isSymbolicLink()).toBe(true)\n  })\n\n  it(\"symdir is a symlink\", () => {\n    expect(lstatSync(join(repoDir, \"symdir\")).isSymbolicLink()).toBe(true)\n  })\n\n  it(\"symlink.txt → file.txt\", () => {\n    expect(readlinkSync(join(repoDir, \"symlink.txt\"))).toBe(\"file.txt\")\n  })\n\n  it(\"symdir → folder\", () => {\n    expect(readlinkSync(join(repoDir, \"symdir\"))).toBe(\"folder\")\n  })\n\n  it(\"file.txt content\", () => {\n    expect(readFileSync(join(repoDir, \"file.txt\"), \"utf-8\").trim()).toBe(\"root file\")\n  })\n\n  it(\"folder/nested.txt content\", () => {\n    expect(readFileSync(join(repoDir, \"folder/nested.txt\"), \"utf-8\").trim()).toBe(\"nested file\")\n  })\n\n  it(\"folder/deep/file.txt content\", () => {\n    expect(readFileSync(join(repoDir, \"folder/deep/file.txt\"), \"utf-8\").trim()).toBe(\"deep file\")\n  })\n})\n\n// =====================================================================\n// CONFIG FILE\n// =====================================================================\n\ndescribe(\"config — .gitpick.jsonc\", () => {\n  const configDir = join(ARTIFACTS, \"config\")\n\n  const CONFIG_TREES: Record<number, string> = {\n    1: TREE_REPO_MAIN,\n    2: TREE_REPO_MAIN,\n    3: TREE_REPO_MAIN,\n    4: TREE_REPO_MAIN,\n    5: TREE_FOLDER,\n    6: TREE_FOLDER,\n    7: TREE_FOLDER,\n    8: TREE_FOLDER,\n    9: TREE_BLOB_FILE,\n    10: TREE_REPO_DEV,\n    11: TREE_REPO_DEV,\n    12: TREE_REPO_MAIN,\n    13: TREE_FOLDER,\n    14: TREE_REPO_MAIN,\n    15: TREE_GITLAB_REPO,\n    16: TREE_GITLAB_PUBLIC,\n  }\n\n  beforeAll(async () => {\n    rmSync(configDir, { recursive: true, force: true })\n    mkdirSync(configDir, { recursive: true })\n    copyFileSync(\"tests/fixtures.jsonc\", join(configDir, \".gitpick.jsonc\"))\n\n    // run gitpick with no args in the config dir to trigger config mode\n    const { exitCode } = await run([], resolve(configDir))\n    expect(exitCode).toBe(0)\n  }, 600000)\n\n  for (let i = 1; i <= 16; i++) {\n    it(`entry #${i}`, () => {\n      const dir = join(configDir, String(i))\n      expect(existsSync(dir)).toBe(true)\n      expect(readdirSync(dir).length).toBeGreaterThan(0)\n\n      const expected = CONFIG_TREES[i]\n      if (expected) {\n        expect(getTree(dir)).toBe(expected)\n      }\n    })\n  }\n})\n\n// =====================================================================\n// TREE OUTPUT\n// =====================================================================\n\ndescribe(\"--tree output\", () => {\n  function parseTreeOutput(output: string) {\n    const stripped = stripAnsi(output).trim()\n    const lines = stripped.split(\"\\n\")\n    return { header: lines[0], tree: lines.slice(1).join(\"\\n\") }\n  }\n\n  const fwd = (s: string) => s.replaceAll(\"\\\\\", \"/\")\n\n  it(\"clone tree shows header and tree\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\n      \"clone\",\n      \"nrjdalal/picksuite/tree/main/folder\",\n      t,\n      \"--tree\",\n    ])\n    expect(exitCode).toBe(0)\n    const { header, tree } = parseTreeOutput(output)\n    expect(header).toContain(fwd(t))\n    expect(tree).toBe(TREE_FOLDER)\n  }, 30000)\n\n  it(\"clone repo shows header and full tree\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\"clone\", \"nrjdalal/picksuite\", t, \"--tree\"])\n    expect(exitCode).toBe(0)\n    const { header, tree } = parseTreeOutput(output)\n    expect(header).toContain(fwd(t))\n    expect(tree).toBe(TREE_REPO_MAIN)\n  }, 30000)\n\n  it(\"no human-readable output with --tree\", async () => {\n    const t = target()\n    const { output } = await run([\"clone\", \"nrjdalal/picksuite/tree/main/folder\", t, \"--tree\"])\n    expect(stripAnsi(output)).not.toContain(\"GitPick\")\n    expect(stripAnsi(output)).not.toContain(\"✔\")\n    expect(stripAnsi(output)).not.toContain(\"Picked\")\n  }, 30000)\n\n  it(\"dry-run tree shows header and tree without leaving files\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\n      \"nrjdalal/picksuite/tree/main/folder\",\n      t,\n      \"--dry-run\",\n      \"--tree\",\n    ])\n    expect(exitCode).toBe(0)\n    const { header, tree } = parseTreeOutput(output)\n    expect(header).toContain(fwd(t))\n    expect(tree).toBe(TREE_FOLDER)\n    expect(existsSync(resolve(t))).toBe(false)\n  }, 30000)\n\n  it(\"dry-run repo shows header and full tree\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\"nrjdalal/picksuite\", t, \"--dry-run\", \"--tree\"])\n    expect(exitCode).toBe(0)\n    const { header, tree } = parseTreeOutput(output)\n    expect(header).toContain(fwd(t))\n    expect(tree).toBe(TREE_REPO_MAIN)\n  }, 30000)\n\n  it(\"header uses ./ for relative paths\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\n      \"clone\",\n      \"nrjdalal/picksuite/tree/main/folder\",\n      t,\n      \"--tree\",\n    ])\n    expect(exitCode).toBe(0)\n    const { header } = parseTreeOutput(output)\n    expect(header.startsWith(\"./\")).toBe(true)\n  }, 30000)\n\n  it(\"blob shows parent dir header and file node\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\n      \"clone\",\n      \"nrjdalal/picksuite/blob/main/file.txt\",\n      t,\n      \"--tree\",\n    ])\n    expect(exitCode).toBe(0)\n    const { header, tree } = parseTreeOutput(output)\n    expect(header).toContain(fwd(join(ARTIFACTS, \"cli\")))\n    expect(tree).toBe(\"└── file.txt\")\n  }, 30000)\n\n  it(\"dry-run blob shows parent dir header and file node\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\n      \"nrjdalal/picksuite/blob/main/file.txt\",\n      t,\n      \"--dry-run\",\n      \"--tree\",\n    ])\n    expect(exitCode).toBe(0)\n    const { header, tree } = parseTreeOutput(output)\n    expect(header).toContain(fwd(join(ARTIFACTS, \"cli\")))\n    expect(tree).toBe(\"└── file.txt\")\n  }, 30000)\n})\n\n// =====================================================================\n// QUIET & VERBOSE\n// =====================================================================\n\ndescribe(\"--quiet output\", () => {\n  it(\"suppresses all output on clone\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\n      \"clone\",\n      \"nrjdalal/picksuite/tree/main/folder\",\n      t,\n      \"-q\",\n    ])\n    expect(exitCode).toBe(0)\n    expect(output.trim()).toBe(\"\")\n  }, 30000)\n\n  it(\"suppresses all output on dry-run\", async () => {\n    const { output, exitCode } = await run([\"nrjdalal/picksuite\", \"--dry-run\", \"-q\"])\n    expect(exitCode).toBe(0)\n    expect(output.trim()).toBe(\"\")\n  }, 30000)\n})\n\ndescribe(\"--verbose output\", () => {\n  it(\"shows clone metadata and stats on success\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\n      \"clone\",\n      \"nrjdalal/picksuite/tree/main/folder\",\n      t,\n      \"--verbose\",\n    ])\n    expect(exitCode).toBe(0)\n    const stripped = stripAnsi(output)\n    expect(stripped).toContain(\"clone:\")\n    expect(stripped).toContain(\"shallow\")\n    expect(stripped).toContain(\"from:\")\n    expect(stripped).toContain(\"picksuite.git\")\n    expect(stripped).toContain(\"files:\")\n    expect(stripped).toContain(\"network:\")\n    expect(stripped).toContain(\"copy:\")\n    expect(stripped).toContain(\"total:\")\n    expect(stripped).toMatch(/\\d+ B|\\d+\\.\\d+ KB|\\d+\\.\\d+ MB/)\n  }, 30000)\n\n  it(\"reports full clone strategy for SHA\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\n      \"clone\",\n      \"nrjdalal/picksuite\",\n      \"-b\",\n      \"8af536b\",\n      t,\n      \"--verbose\",\n    ])\n    expect(exitCode).toBe(0)\n    const stripped = stripAnsi(output)\n    expect(stripped).toContain(\"full (depth=full)\")\n  }, 30000)\n})\n\ndescribe(\"--quiet / --verbose interactions\", () => {\n  it(\"--quiet with --tree shows tree only, no banner or spinner\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\n      \"clone\",\n      \"nrjdalal/picksuite/tree/main/folder\",\n      t,\n      \"-q\",\n      \"--tree\",\n    ])\n    expect(exitCode).toBe(0)\n    const stripped = stripAnsi(output)\n    // tree output still shows (--tree takes precedence for its own output)\n    expect(stripped).not.toContain(\"GitPick\")\n    expect(stripped).not.toContain(\"Picked\")\n    expect(stripped).not.toContain(\"✔\")\n  }, 30000)\n\n  it(\"--verbose with --tree shows tree but no verbose metadata\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\n      \"clone\",\n      \"nrjdalal/picksuite/tree/main/folder\",\n      t,\n      \"--verbose\",\n      \"--tree\",\n    ])\n    expect(exitCode).toBe(0)\n    const stripped = stripAnsi(output)\n    // --tree silences verbose output\n    expect(stripped).not.toContain(\"clone:\")\n    expect(stripped).not.toContain(\"from:\")\n    // but tree still renders\n    expect(stripped).toContain(\"deep\")\n    expect(stripped).toContain(\"nested.txt\")\n  }, 30000)\n\n  it(\"--quiet dry-run produces no output\", async () => {\n    const { output, exitCode } = await run([\n      \"nrjdalal/picksuite/tree/main/folder\",\n      \"--dry-run\",\n      \"-q\",\n    ])\n    expect(exitCode).toBe(0)\n    expect(output.trim()).toBe(\"\")\n  }, 30000)\n\n  it(\"--verbose dry-run shows info line but no clone metadata\", async () => {\n    const { output, exitCode } = await run([\"nrjdalal/picksuite\", \"--dry-run\", \"--verbose\"])\n    expect(exitCode).toBe(0)\n    const stripped = stripAnsi(output)\n    expect(stripped).toContain(\"picksuite\")\n    // no clone metadata since nothing was cloned\n    expect(stripped).not.toContain(\"clone:\")\n    expect(stripped).not.toContain(\"duration:\")\n  }, 30000)\n})\n\n// =====================================================================\n// ENV VAR TOKENS\n// =====================================================================\n\ndescribe(\"env var token support\", () => {\n  it(\"GITHUB_TOKEN is used for shorthand URLs\", async () => {\n    const proc = Bun.spawn([...CLI, \"nrjdalal/picksuite\", \"--dry-run\"], {\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: { ...process.env, GITHUB_TOKEN: \"fake_github_token\" },\n    })\n    const stdout = await new Response(proc.stdout).text()\n    const exitCode = await proc.exited\n    expect(exitCode).toBe(0)\n    expect(parseLine(stdout)).toContain(\"nrjdalal/picksuite\")\n  }, 30000)\n\n  it(\"GH_TOKEN fallback works\", async () => {\n    const proc = Bun.spawn([...CLI, \"nrjdalal/picksuite\", \"--dry-run\"], {\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: { ...process.env, GITHUB_TOKEN: \"\", GH_TOKEN: \"fake_gh_token\" },\n    })\n    const stdout = await new Response(proc.stdout).text()\n    const exitCode = await proc.exited\n    expect(exitCode).toBe(0)\n    expect(parseLine(stdout)).toContain(\"nrjdalal/picksuite\")\n  }, 30000)\n\n  it(\"URL token takes precedence over env var\", async () => {\n    const proc = Bun.spawn(\n      [...CLI, \"https://fake_url_token@github.com/nrjdalal/picksuite\", \"--dry-run\"],\n      {\n        stdout: \"pipe\",\n        stderr: \"pipe\",\n        env: { ...process.env, GITHUB_TOKEN: \"fake_env_token\" },\n      },\n    )\n    const stdout = await new Response(proc.stdout).text()\n    const exitCode = await proc.exited\n    expect(exitCode).toBe(0)\n    expect(parseLine(stdout)).toContain(\"nrjdalal/picksuite\")\n  }, 30000)\n})\n\n// =====================================================================\n// NON-TTY SPINNER\n// =====================================================================\n\ndescribe(\"non-TTY spinner suppression\", () => {\n  it(\"no spinner frames in piped output\", async () => {\n    const t = target()\n    const { output, exitCode } = await run([\"clone\", \"nrjdalal/picksuite/tree/main/folder\", t])\n    expect(exitCode).toBe(0)\n    const stripped = stripAnsi(output)\n    // Bun.spawn captures stdout as pipe (non-TTY), so spinner should be suppressed\n    for (const frame of [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"]) {\n      expect(stripped).not.toContain(frame)\n    }\n    // But success message should still appear\n    expect(stripped).toContain(\"Picked\")\n  }, 30000)\n})\n\n// =====================================================================\n// SIGINT CLEANUP\n// =====================================================================\n\ndescribe(\"SIGINT temp dir cleanup\", () => {\n  // SIGINT is a POSIX signal — Windows does not deliver it to child processes\n  it.skipIf(process.platform === \"win32\")(\n    \"cleans up temp dir on SIGINT\",\n    async () => {\n      const { readdirSync } = await import(\"node:fs\")\n      const { tmpdir } = await import(\"node:os\")\n\n      // Snapshot temp dirs before\n      const before = new Set(readdirSync(tmpdir()).filter((d) => d.startsWith(\"picksuite-\")))\n\n      const proc = Bun.spawn(\n        [...CLI, \"clone\", \"nrjdalal/picksuite\", \"/tmp/gitpick-sigint-test\", \"-o\"],\n        { stdout: \"pipe\", stderr: \"pipe\" },\n      )\n\n      // Poll until a new temp dir appears (max 10s)\n      let newTempDir: string | null = null\n      for (let i = 0; i < 100; i++) {\n        await new Promise((r) => setTimeout(r, 100))\n        const current = readdirSync(tmpdir()).filter(\n          (d) => d.startsWith(\"picksuite-\") && !before.has(d),\n        )\n        if (current.length > 0) {\n          newTempDir = current[0]\n          break\n        }\n      }\n\n      // If we found it, kill and verify cleanup\n      if (newTempDir) {\n        proc.kill(\"SIGINT\")\n        await proc.exited\n\n        const after = readdirSync(tmpdir()).filter(\n          (d) => d.startsWith(\"picksuite-\") && !before.has(d),\n        )\n        expect(after).toHaveLength(0)\n      } else {\n        // Clone finished before we could catch the temp dir — still valid, just skip assertion\n        await proc.exited\n      }\n    },\n    30000,\n  )\n})\n\n// ---------------------------------------------------------------------------\n// Interactive mode\n// ---------------------------------------------------------------------------\ndescribe(\"interactive mode\", () => {\n  it(\"should error on non-TTY with -i flag\", async () => {\n    const { output, exitCode } = await run([\"nrjdalal/gitpick\", \"-i\", \"-b\", \"main\"])\n    expect(exitCode).not.toBe(0)\n    expect(stripAnsi(output)).toContain(\"Interactive mode requires a TTY\")\n  })\n\n  it(\"should show -i in help text\", async () => {\n    const { output } = await run([\"--help\"])\n    expect(stripAnsi(output)).toContain(\"-i, --interactive\")\n  })\n})\n"
  },
  {
    "path": "tests/fixtures.jsonc",
    "content": "// Example config, run tests using `bun run test:config`\n[\n  // repo picks\n  \"nrjdalal/picksuite 1\",\n  \"https://github.com/nrjdalal/picksuite 2\",\n  \"git@github.com:nrjdalal/picksuite.git 3\",\n  \"https://github.com/nrjdalal/picksuite.git 4\",\n  // tree picks\n  \"nrjdalal/picksuite/tree/main/folder 5\",\n  \"https://github.com/nrjdalal/picksuite/tree/main/folder 6\",\n  \"git@github.com:nrjdalal/picksuite.git/tree/main/folder 7\",\n  \"https://github.com/nrjdalal/picksuite.git/tree/main/folder 8\",\n  // blob pick\n  \"nrjdalal/picksuite/blob/main/file.txt 9\",\n  // branch picks\n  \"nrjdalal/picksuite -b dev 10\",\n  \"nrjdalal/picksuite/tree/dev 11\",\n  // commit SHA\n  \"nrjdalal/picksuite -b 8af536b 12\",\n  // branch override\n  \"nrjdalal/picksuite/tree/dev/folder -b main 13\",\n  // symlink support (full repo has symlinks)\n  \"nrjdalal/picksuite -r 14\",\n  // gitlab\n  \"https://gitlab.com/pages/plain-html -b main 15\",\n  \"https://gitlab.com/pages/plain-html/-/tree/main/public 16\",\n]\n"
  },
  {
    "path": "tests/tree.mjs",
    "content": "import { readdirSync, readlinkSync, statSync } from \"node:fs\"\nimport { join } from \"node:path\"\n\nfunction tree(dir, prefix = \"\") {\n  const entries = readdirSync(dir, { withFileTypes: true }).filter((e) => e.name !== \".git\")\n  entries.forEach((e, i) => {\n    const last = i === entries.length - 1\n    const connector = last ? \"└── \" : \"├── \"\n    if (e.isSymbolicLink()) {\n      const target = readlinkSync(join(dir, e.name))\n      console.log(`${prefix}${connector}${e.name} -> ${target}`)\n    } else if (e.isDirectory()) {\n      console.log(`${prefix}${connector}${e.name}`)\n      tree(join(dir, e.name), `${prefix}${last ? \"    \" : \"│   \"}`)\n    } else {\n      console.log(`${prefix}${connector}${e.name}`)\n    }\n  })\n}\n\nconst target = process.argv[2]\nif (!target) process.exit(1)\nconst stat = statSync(target, { throwIfNoEntry: false })\nif (stat?.isDirectory()) {\n  console.log(target)\n  tree(target)\n} else {\n  console.log(`${target} (file)`)\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"paths\": {\n      \"~/*\": [\"./*\"],\n      \"@/*\": [\"./bin/*\"]\n    }\n  },\n  \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "tsdown.config.ts",
    "content": "import { defineConfig } from \"tsdown\"\n\nexport default defineConfig({\n  entry: [\"bin/index.ts\"],\n  minify: true,\n})\n"
  },
  {
    "path": "types/package.json.d.ts",
    "content": "declare module \"~/package.json\" {\n  export const name: string\n  export const version: string\n  export const author: {\n    name: string\n    email: string\n    url: string\n  }\n}\n"
  }
]