diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fecd537..90dca55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,34 +9,34 @@ on: jobs: build: runs-on: ubuntu-latest - + strategy: matrix: node-version: [18, 20] - + steps: - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: 0 # Fetch full history for semantic-release - + fetch-depth: 0 # Fetch full history for semantic-release + - name: Install pnpm uses: pnpm/action-setup@v4 with: version: latest run_install: false - + - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'pnpm' - + cache: "pnpm" + - name: Get pnpm store directory shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - + - name: Setup pnpm cache uses: actions/cache@v4 with: @@ -44,13 +44,13 @@ jobs: key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - + - name: Install dependencies run: pnpm install --frozen-lockfile - + - name: Build TypeScript run: pnpm run compile - + - name: Check for build artifacts run: | if [ ! -d "lib" ]; then @@ -63,31 +63,31 @@ jobs: runs-on: ubuntu-latest needs: build if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' - + steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - + - name: Install pnpm uses: pnpm/action-setup@v4 with: version: latest run_install: false - + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - cache: 'pnpm' - + cache: "pnpm" + - name: Get pnpm store directory shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - + - name: Setup pnpm cache uses: actions/cache@v4 with: @@ -95,15 +95,15 @@ jobs: key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - + - name: Install dependencies run: pnpm install --frozen-lockfile - + - name: Build TypeScript run: pnpm run compile - + - name: Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: pnpm run semantic-release \ No newline at end of file + run: pnpm run semantic-release diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..cb2c84d --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint-staged diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..9663b58 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,8 @@ +{ + "*.{ts,js}": [ + "dprint fmt" + ], + "*.{json,md,yml,yaml,toml}": [ + "dprint fmt" + ] +} diff --git a/CLAUDE.md b/CLAUDE.md index f6fdcfc..c8c57db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,116 +1,17 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +GitHub CLI tool for cleaning up merged PR branches. -## Project Overview -Ghouls is a GitHub CLI tool for cleaning up pull request branches. It identifies and deletes both remote and local branches that have been merged but not cleaned up. +## Commands -## Development Commands - -### Build ```bash -pnpm compile # Compiles TypeScript to JavaScript in lib/ directory -``` +pnpm compile # Build +pnpm test # Test +pnpm install # Install deps -### Installation -```bash -pnpm add -g ghouls # Install globally -pnpm install # Install dependencies -``` - -### Testing -The project uses Vitest for comprehensive unit testing. - -```bash -pnpm test # Run all tests -pnpm test:watch # Run tests in watch mode -pnpm test:coverage # Generate coverage reports +ghouls remote [--dry-run] [owner/repo] # Clean remote branches +ghouls local [--dry-run] [owner/repo] # Clean local branches +ghouls all [--dry-run] [owner/repo] # Clean both ``` -### TypeScript Compiler -The project uses strict TypeScript configuration with: -- Target: ES2022 -- Module: ES2022 -- Output directory: `./lib` -- Strict type checking enabled -- No unused locals/parameters allowed - -## Architecture - -### Core Components - -1. **OctokitPlus** (`src/OctokitPlus.ts`): Wrapper around GitHub's Octokit API client - - Handles GitHub API operations for references and pull requests - - Provides async iterator pattern for paginated results - - Key methods: `getReference()`, `deleteReference()`, `getPullRequests()` - -2. **CLI Entry Point** (`src/cli.ts`): Main command-line interface using yargs - - Registers available commands (`remote`, `local`, and `all`) - - Handles unhandled promise rejections - -3. **Remote Command** (`src/commands/PrunePullRequests.ts`): Remote branch cleanup - - Iterates through closed pull requests - - Checks if branch SHA matches PR merge state - - Deletes remote branches that have been merged (with --dry-run option) - - Shows progress bar during operation - -4. **Local Command** (`src/commands/PruneLocalBranches.ts`): Local branch cleanup - - Scans local branches for safe deletion candidates - - Verifies local branch SHA matches PR head SHA before deletion - - Protects current branch and branches with unpushed commits - - Includes comprehensive safety checks and dry-run mode - - Shows detailed analysis and progress during operation - -5. **All Command** (`src/commands/PruneAll.ts`): Combined branch cleanup - - Executes remote pruning first, then local pruning - - Continues with local cleanup even if remote fails - - Provides combined summary of both operations - - Supports --dry-run and --force flags for both phases - - Ensures maximum cleanup efficiency in a single command - -6. **Utilities** (`src/utils/`): - - `createOctokitPlus.ts`: Factory for creating authenticated Octokit instances - - `ownerAndRepoMatch.ts`: Validates PR head/base repository matching - - `localGitOperations.ts`: Local git operations (list branches, get status, delete branches) - - `branchSafetyChecks.ts`: Safety validation for branch deletion - - `getGitRemote.ts`: Git remote URL parsing and repository detection - -### Authentication -Ghouls uses GitHub CLI authentication exclusively. Users must have the GitHub CLI (`gh`) installed and authenticated with `gh auth login`. The tool automatically uses the existing GitHub CLI authentication credentials. - -### Command Usage -```bash -# Remote branch cleanup -ghouls remote [--dry-run] [owner/repo] - -# Local branch cleanup -ghouls local [--dry-run] [owner/repo] - -# Combined cleanup (both remote and local) -ghouls all [--dry-run] [owner/repo] -``` - -All commands support repository auto-detection from git remotes when run within a git repository. - -## AI Team Configuration (autogenerated by team-configurator, 2025-08-01) - -**Important: YOU MUST USE subagents when available for the task.** - -### Technology Stack Detected -- Language: TypeScript with strict type checking (ES2022 target) -- Runtime: Node.js (>=18.0.0) -- CLI Framework: yargs for command-line interface -- GitHub API: @octokit/rest for GitHub API interactions -- Build System: TypeScript compiler with pnpm package manager -- Package Management: pnpm with semantic-release -- Test Framework: Vitest with comprehensive unit tests - -### AI Team Assignments - -| Task | Agent | Notes | -|------|-------|-------| -| Code reviews and quality assurance | code-reviewer | Required for all PRs and feature changes | -| Performance optimization and profiling | performance-optimizer | Essential for CLI tool responsiveness | -| Backend development and API integration | backend-developer | Handles GitHub API integration and CLI logic | -| API design and GitHub integration specs | api-architect | Designs interfaces for GitHub API wrapper | -| Documentation updates and maintenance | documentation-specialist | Maintains README, API docs, and user guides | \ No newline at end of file +Uses GitHub CLI auth (`gh auth login`). TypeScript/Node.js/pnpm project. diff --git a/README.md b/README.md index ff8621e..9b6085b 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,16 @@ Ghouls Logo - The ghouls can help you. # Breaking Changes ## v2.0.0 + - **Command names have changed:** - `prunePullRequests` → `remote` - `pruneLocalBranches` → `local` - + If you have scripts using the old commands, please update them to use the new shorter names. # Getting started @@ -100,6 +100,7 @@ For other platforms and more installation options, visit: https://cli.github.com Safely deletes remote branches that have been merged via pull requests. Run from within a git repository (auto-detects repo): + ```bash ghouls remote --dry-run ``` @@ -107,6 +108,7 @@ ghouls remote --dry-run The auto-detection feature works with both github.com and GitHub Enterprise repositories, automatically detecting the repository owner/name from the remote URL. Or specify a repository explicitly: + ```bash ghouls remote --dry-run myorg/myrepo ``` @@ -125,11 +127,13 @@ $ ghouls remote myorg/myrepo Safely deletes local branches that have been merged via pull requests. This command includes comprehensive safety checks to protect important branches and work in progress. Run from within a git repository (auto-detects repo): + ```bash ghouls local --dry-run ``` Or specify a repository explicitly: + ```bash ghouls local --dry-run myorg/myrepo ``` @@ -181,11 +185,13 @@ Summary: The `all` command combines both remote and local branch cleanup in a single operation, running them in sequence for maximum efficiency. Run from within a git repository (auto-detects repo): + ```bash ghouls all --dry-run ``` Or specify a repository explicitly: + ```bash ghouls all --dry-run myorg/myrepo ``` @@ -193,6 +199,7 @@ ghouls all --dry-run myorg/myrepo ### Execution Order The command executes in two phases: + 1. **Remote cleanup**: Deletes merged remote branches first 2. **Local cleanup**: Then deletes corresponding local branches @@ -236,18 +243,21 @@ Local cleanup: ✅ Success The project uses Vitest for comprehensive unit testing. ### Run tests + ```bash pnpm test ``` ### Run tests in watch mode + ```bash pnpm test:watch ``` ### Generate coverage reports + ```bash pnpm test:coverage ``` -The test suite includes comprehensive unit tests covering all core functionality, utilities, and edge cases. \ No newline at end of file +The test suite includes comprehensive unit tests covering all core functionality, utilities, and edge cases. diff --git a/dprint.json b/dprint.json new file mode 100644 index 0000000..11d0d12 --- /dev/null +++ b/dprint.json @@ -0,0 +1,36 @@ +{ + "typescript": { + "lineWidth": 120, + "indentWidth": 2 + }, + "json": { + }, + "markdown": { + }, + "toml": { + }, + "malva": { + }, + "markup": { + }, + "yaml": { + }, + "excludes": [ + "**/node_modules", + "**/*-lock.json", + "pnpm-lock.yaml", + "coverage/**/*", + "lib/**/*", + ".claude/settings.local.json", + ".claude/settings.json" + ], + "plugins": [ + "https://plugins.dprint.dev/typescript-0.95.10.wasm", + "https://plugins.dprint.dev/json-0.20.0.wasm", + "https://plugins.dprint.dev/markdown-0.19.0.wasm", + "https://plugins.dprint.dev/toml-0.7.0.wasm", + "https://plugins.dprint.dev/g-plane/malva-v0.14.1.wasm", + "https://plugins.dprint.dev/g-plane/markup_fmt-v0.23.1.wasm", + "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm" + ] +} diff --git a/package.json b/package.json index 484a50e..8be11a3 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,15 @@ "engines": { "node": ">=18.0.0" }, + "exports": {}, "scripts": { "compile": "tsc", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", "prepublishOnly": "pnpm run compile", - "semantic-release": "semantic-release" + "semantic-release": "semantic-release", + "prepare": "husky" }, "bin": { "ghouls": "./bin/ghouls" @@ -26,10 +28,12 @@ "dependencies": { "@octokit/rest": "^20.1.2", "execa": "^9.6.0", + "find-up": "^7.0.0", "inquirer": "^12.9.0", "progress": "^2.0.3", "source-map-support": "^0.5.21", - "yargs": "^18.0.0" + "yargs": "^18.0.0", + "zod": "^4.0.17" }, "devDependencies": { "@types/inquirer": "^9.0.8", @@ -39,10 +43,11 @@ "@types/which": "^3.0.4", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", - "c8": "^10.1.3", - "prettier": "^3.6.2", + "dprint": "^0.50.1", + "husky": "^9.1.7", + "knip": "^5.62.0", + "lint-staged": "^16.1.5", "semantic-release": "^24.2.7", - "ts-node": "^10.9.2", "typescript": "^5.9.2", "vitest": "^3.2.4" }, @@ -54,4 +59,4 @@ "type": "git", "url": "https://github.com/ericanderson/ghouls.git" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29e4ea1..7785869 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: execa: specifier: ^9.6.0 version: 9.6.0 + find-up: + specifier: ^7.0.0 + version: 7.0.0 inquirer: specifier: ^12.9.0 version: 12.9.0(@types/node@22.17.0) @@ -26,6 +29,9 @@ importers: yargs: specifier: ^18.0.0 version: 18.0.0 + zod: + specifier: ^4.0.17 + version: 4.0.17 devDependencies: '@types/inquirer': specifier: ^9.0.8 @@ -48,24 +54,27 @@ importers: '@vitest/ui': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) - c8: - specifier: ^10.1.3 - version: 10.1.3 - prettier: - specifier: ^3.6.2 - version: 3.6.2 + dprint: + specifier: ^0.50.1 + version: 0.50.1 + husky: + specifier: ^9.1.7 + version: 9.1.7 + knip: + specifier: ^5.62.0 + version: 5.62.0(@types/node@22.17.0)(typescript@5.9.2) + lint-staged: + specifier: ^16.1.5 + version: 16.1.5 semantic-release: specifier: ^24.2.7 version: 24.2.7(typescript@5.9.2) - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@22.17.0)(typescript@5.9.2) typescript: specifier: ^5.9.2 version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.17.0)(@vitest/ui@3.2.4) + version: 3.2.4(@types/node@22.17.0)(@vitest/ui@3.2.4)(jiti@2.5.1)(yaml@2.8.1) packages: @@ -102,9 +111,59 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} + '@dprint/darwin-arm64@0.50.1': + resolution: {integrity: sha512-NNKf3dxXn567pd/hpCVLHLbC0dI7s3YvQnUEwjRTOAQVMp6O7/ME+Tg1RPGsDP1IB+Y2fIYSM4qmG02zQrqjAQ==} + cpu: [arm64] + os: [darwin] + + '@dprint/darwin-x64@0.50.1': + resolution: {integrity: sha512-PcY75U3UC/0CLOxWzE0zZJZ2PxzUM5AX2baYL1ovgDGCfqO1H0hINiyxfx/8ncGgPojWBkLs+zrcFiGnXx7BQg==} + cpu: [x64] + os: [darwin] + + '@dprint/linux-arm64-glibc@0.50.1': + resolution: {integrity: sha512-q0TOGy9FsoSKsEQ4sIMKyFweF5M8rW1S5OfwJDNRR2TU2riWByU9TKYIZUzg53iuwYKRypr/kJ5kdbl516afRQ==} + cpu: [arm64] + os: [linux] + + '@dprint/linux-arm64-musl@0.50.1': + resolution: {integrity: sha512-XRtxN2cA9rc06WFzzVPDIZYGGLmUXqpVf3F0XhhHV77ikQLJZ5reF4xBOQ+0HjJ/zy8W/HzuGDAHedWyCrRf9g==} + cpu: [arm64] + os: [linux] + + '@dprint/linux-riscv64-glibc@0.50.1': + resolution: {integrity: sha512-vAk/eYhSjA3LJ/yuYgxkHamiK8+m6YdqVBO/Ka+i16VxyjQyOdcMKBkrLCIqSxgyXd6b8raf9wM59HJbaIpoOg==} + cpu: [riscv64] + os: [linux] + + '@dprint/linux-x64-glibc@0.50.1': + resolution: {integrity: sha512-EpW5KLekaq4hXmKBWWtfBgZ244S4C+vFmMOd1YaGi8+f0hmPTJzVWLdIgpO2ZwfPQ5iycaVI/JS514PQmXPOvg==} + cpu: [x64] + os: [linux] + + '@dprint/linux-x64-musl@0.50.1': + resolution: {integrity: sha512-assISBbaKKL8LkjrIy/5tpE157MVW6HbyIKAjTtg3tPNM3lDn1oH3twuGtK9WBsN/VoEP3QMZVauolcUJT/VOg==} + cpu: [x64] + os: [linux] + + '@dprint/win32-arm64@0.50.1': + resolution: {integrity: sha512-ZeaRMQYoFjrsO3lvI1SqzDWDGH1GGXWmNSeXvcFuAf2OgYQJWMBlLotCKiHNJ3uyYneoyhTg2tv9QkApNkZV4Q==} + cpu: [arm64] + os: [win32] + + '@dprint/win32-x64@0.50.1': + resolution: {integrity: sha512-pMm8l/hRZ9zYylKw/yCaYkSV3btYB9UyMDbWqyxNthkQ1gckWrk17VTI6WjwwQuHD4Iaz5JgAYLS36hlUzWkxA==} + cpu: [x64] + os: [win32] + + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} '@esbuild/aix-ppc64@0.25.8': resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} @@ -404,8 +463,8 @@ packages: '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@napi-rs/wasm-runtime@1.0.3': + resolution: {integrity: sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -519,6 +578,101 @@ packages: '@octokit/types@14.1.0': resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@oxc-resolver/binding-android-arm-eabi@11.6.1': + resolution: {integrity: sha512-Ma/kg29QJX1Jzelv0Q/j2iFuUad1WnjgPjpThvjqPjpOyLjCUaiFCCnshhmWjyS51Ki1Iol3fjf1qAzObf8GIA==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.6.1': + resolution: {integrity: sha512-xjL/FKKc5p8JkFWiH7pJWSzsewif3fRf1rw2qiRxRvq1uIa6l7Zoa14Zq2TNWEsqDjdeOrlJtfWiPNRnevK0oQ==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.6.1': + resolution: {integrity: sha512-u0yrJ3NHE0zyCjiYpIyz4Vmov21MA0yFKbhHgixDU/G6R6nvC8ZpuSFql3+7C8ttAK9p8WpqOGweepfcilH5Bw==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.6.1': + resolution: {integrity: sha512-2lox165h1EhzxcC8edUy0znXC/hnAbUPaMpYKVlzLpB2AoYmgU4/pmofFApj+axm2FXpNamjcppld8EoHo06rw==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.6.1': + resolution: {integrity: sha512-F45MhEQ7QbHfsvZtVNuA/9obu3il7QhpXYmCMfxn7Zt9nfAOw4pQ8hlS5DroHVp3rW35u9F7x0sixk/QEAi3qQ==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.6.1': + resolution: {integrity: sha512-r+3+MTTl0tD4NoWbfTIItAxJvuyIU7V0fwPDXrv7Uj64vZ3OYaiyV+lVaeU89Bk/FUUQxeUpWBwdKNKHjyRNQw==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.6.1': + resolution: {integrity: sha512-TBTZ63otsWZ72Z8ZNK2JVS0HW1w9zgOixJTFDNrYPUUW1pXGa28KAjQ1yGawj242WLAdu3lwdNIWtkxeO2BLxQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.6.1': + resolution: {integrity: sha512-SjwhNynjSG2yMdyA0f7wz7Yvo3ppejO+ET7n2oiI7ApCXrwxMzeRWjBzQt+oVWr2HzVOfaEcDS9rMtnR83ulig==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.6.1': + resolution: {integrity: sha512-f4EMidK6rosInBzPMnJ0Ri4RttFCvvLNUNDFUBtELW/MFkBwPTDlvbsmW0u0Mk/ruBQ2WmRfOZ6tT62kWMcX2Q==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.6.1': + resolution: {integrity: sha512-1umENVKeUsrWnf5IlF/6SM7DCv8G6CoKI2LnYR6qhZuLYDPS4PBZ0Jow3UDV9Rtbv5KRPcA3/uXjI88ntWIcOQ==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.6.1': + resolution: {integrity: sha512-Hjyp1FRdJhsEpIxsZq5VcDuFc8abC0Bgy8DWEa31trCKoTz7JqA7x3E2dkFbrAKsEFmZZ0NvuG5Ip3oIRARhow==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.6.1': + resolution: {integrity: sha512-ODJOJng6f3QxpAXhLel3kyWs8rPsJeo9XIZHzA7p//e+5kLMDU7bTVk4eZnUHuxsqsB8MEvPCicJkKCEuur5Ag==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.6.1': + resolution: {integrity: sha512-hCzRiLhqe1ZOpHTsTGKp7gnMJRORlbCthawBueer2u22RVAka74pV/+4pP1tqM07mSlQn7VATuWaDw9gCl+cVg==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.6.1': + resolution: {integrity: sha512-JansPD8ftOzMYIC3NfXJ68tt63LEcIAx44Blx6BAd7eY880KX7A0KN3hluCrelCz5aQkPaD95g8HBiJmKaEi2w==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.6.1': + resolution: {integrity: sha512-R78ES1rd4z2x5NrFPtSWb/ViR1B8wdl+QN2X8DdtoYcqZE/4tvWtn9ZTCXMEzUp23tchJ2wUB+p6hXoonkyLpA==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-wasm32-wasi@11.6.1': + resolution: {integrity: sha512-qAR3tYIf3afkij/XYunZtlz3OH2Y4ni10etmCFIJB5VRGsqJyI6Hl+2dXHHGJNwbwjXjSEH/KWJBpVroF3TxBw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.6.1': + resolution: {integrity: sha512-QqygWygIuemGkaBA48POOTeinbVvlamqh6ucm8arGDGz/mB5O00gXWxed12/uVrYEjeqbMkla/CuL3fjL3EKvw==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.6.1': + resolution: {integrity: sha512-N2+kkWwt/bk0JTCxhPuK8t8JMp3nd0n2OhwOkU8KO4a7roAJEa4K1SZVjMv5CqUIr5sx2CxtXRBoFDiORX5oBg==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.6.1': + resolution: {integrity: sha512-DfMg3cU9bJUbN62Prbp4fGCtLgexuwyEaQGtZAp8xmi1Ii26uflOGx0FJkFTF6lVMSFoIRFvIL8gsw5/ZdHrMw==} + cpu: [x64] + os: [win32] + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -681,17 +835,8 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.10.0': + resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -705,9 +850,6 @@ packages: '@types/inquirer@9.0.8': resolution: {integrity: sha512-CgPD5kFGWsb8HJ5K7rfWlifao87m4ph8uioU7OTncJevmE/VLIqAAjfQtko578JZg7/f69K4FgqYym3gNr7DeA==} - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - '@types/node@22.17.0': resolution: {integrity: sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==} @@ -769,15 +911,6 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -817,9 +950,6 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -858,16 +988,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - c8@10.1.3: - resolution: {integrity: sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - monocart-coverage-reports: ^2 - peerDependenciesMeta: - monocart-coverage-reports: - optional: true - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -892,6 +1012,10 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chalk@5.6.0: + resolution: {integrity: sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -907,6 +1031,10 @@ packages: resolution: {integrity: sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==} engines: {node: '>=14.16'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + cli-highlight@2.1.11: resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} engines: {node: '>=8.0.0', npm: '>=5.0.0'} @@ -916,6 +1044,10 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -944,6 +1076,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@14.0.0: + resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + engines: {node: '>=20'} + compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} @@ -972,9 +1111,6 @@ packages: resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} engines: {node: '>=12'} - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -987,9 +1123,6 @@ packages: typescript: optional: true - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1018,10 +1151,6 @@ packages: deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1030,6 +1159,10 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} + dprint@0.50.1: + resolution: {integrity: sha512-s+kUyQp2rGpwsM3vVmXySOY3v1NjYyRpKfQZdP4rfNTz6zQuICSO6nqIXNm3YdK1MwNFR/EXSFMuE1YPuulhow==} + hasBin: true + duplexer2@0.1.4: resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} @@ -1086,6 +1219,9 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -1112,6 +1248,9 @@ packages: fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fdir@6.4.6: resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: @@ -1143,9 +1282,9 @@ packages: resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} engines: {node: '>=4'} - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} find-versions@6.0.0: resolution: {integrity: sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==} @@ -1158,6 +1297,11 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + formatly@0.2.4: + resolution: {integrity: sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA==} + engines: {node: '>=18.3.0'} + hasBin: true + from2@2.3.0: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} @@ -1266,6 +1410,11 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -1323,6 +1472,14 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1384,6 +1541,10 @@ packages: resolution: {integrity: sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==} engines: {node: '>= 0.6.0'} + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1403,9 +1564,30 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + knip@5.62.0: + resolution: {integrity: sha512-hfTUVzmrMNMT1khlZfAYmBABeehwWUUrizLQoLamoRhSFkygsGIXWx31kaWKBgEaIVL77T3Uz7IxGvSw+CvQ6A==} + engines: {node: '>=18.18.0'} + hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4' + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lint-staged@16.1.5: + resolution: {integrity: sha512-uAeQQwByI6dfV7wpt/gVqg+jAPaSp8WwOA8kKC/dv1qw14oGpnpAisY65ibGHUGDUv0rYaZ8CAJZ/1U8hUvC2A==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.1: + resolution: {integrity: sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==} + engines: {node: '>=20.0.0'} + load-json-file@4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} @@ -1414,9 +1596,9 @@ packages: resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} engines: {node: '>=4'} - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} @@ -1436,6 +1618,10 @@ packages: lodash.uniqby@4.7.0: resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + loupe@3.2.0: resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} @@ -1452,9 +1638,6 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - marked-terminal@7.3.0: resolution: {integrity: sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==} engines: {node: '>=16.0.0'} @@ -1490,6 +1673,10 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -1515,11 +1702,20 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nano-spawn@1.0.2: + resolution: {integrity: sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==} + engines: {node: '>=20.17'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-postinstall@0.3.3: + resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -1631,10 +1827,17 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + oxc-resolver@11.6.1: + resolution: {integrity: sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==} + p-each-series@3.0.0: resolution: {integrity: sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==} engines: {node: '>=12'} @@ -1651,17 +1854,17 @@ packages: resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} engines: {node: '>=4'} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} p-locate@2.0.0: resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} engines: {node: '>=4'} - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} p-map@7.0.3: resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==} @@ -1711,9 +1914,9 @@ packages: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} @@ -1753,6 +1956,11 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + pify@3.0.0: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} @@ -1765,11 +1973,6 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} - engines: {node: '>=14'} - hasBin: true - pretty-ms@9.2.0: resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} engines: {node: '>=18'} @@ -1818,10 +2021,17 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup@4.46.2: resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1892,6 +2102,18 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + smol-toml@1.4.2: + resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1930,6 +2152,10 @@ packages: stream-combiner2@1.1.1: resolution: {integrity: sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1969,6 +2195,10 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} + strip-json-comments@5.0.2: + resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==} + engines: {node: '>=14.16'} + strip-literal@3.0.0: resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} @@ -2052,20 +2282,6 @@ packages: resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==} engines: {node: '>= 0.4'} - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2131,13 +2347,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} - validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -2214,6 +2423,10 @@ packages: jsdom: optional: true + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2254,6 +2467,11 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -2278,13 +2496,9 @@ packages: resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} yoctocolors-cjs@2.1.2: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} @@ -2294,6 +2508,18 @@ packages: resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} engines: {node: '>=18'} + zod-validation-error@3.5.3: + resolution: {integrity: sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.0.17: + resolution: {integrity: sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==} + snapshots: '@ampproject/remapping@2.3.0': @@ -2325,9 +2551,48 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@cspotcode/source-map-support@0.8.1': + '@dprint/darwin-arm64@0.50.1': + optional: true + + '@dprint/darwin-x64@0.50.1': + optional: true + + '@dprint/linux-arm64-glibc@0.50.1': + optional: true + + '@dprint/linux-arm64-musl@0.50.1': + optional: true + + '@dprint/linux-riscv64-glibc@0.50.1': + optional: true + + '@dprint/linux-x64-glibc@0.50.1': + optional: true + + '@dprint/linux-x64-musl@0.50.1': + optional: true + + '@dprint/win32-arm64@0.50.1': + optional: true + + '@dprint/win32-x64@0.50.1': + optional: true + + '@emnapi/core@1.4.5': + dependencies: + '@emnapi/wasi-threads': 1.0.4 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.5': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.4': dependencies: - '@jridgewell/trace-mapping': 0.3.9 + tslib: 2.8.1 + optional: true '@esbuild/aix-ppc64@0.25.8': optional: true @@ -2548,10 +2813,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.4 - '@jridgewell/trace-mapping@0.3.9': + '@napi-rs/wasm-runtime@1.0.3': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@tybys/wasm-util': 0.10.0 + optional: true '@nodelib/fs.scandir@2.1.5': dependencies: @@ -2687,6 +2954,65 @@ snapshots: dependencies: '@octokit/openapi-types': 25.1.0 + '@oxc-resolver/binding-android-arm-eabi@11.6.1': + optional: true + + '@oxc-resolver/binding-android-arm64@11.6.1': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.6.1': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.6.1': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.6.1': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.6.1': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.6.1': + dependencies: + '@napi-rs/wasm-runtime': 1.0.3 + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.6.1': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.6.1': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.6.1': + optional: true + '@pkgjs/parseargs@0.11.0': optional: true @@ -2843,13 +3169,10 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@tsconfig/node10@1.0.11': {} - - '@tsconfig/node12@1.0.11': {} - - '@tsconfig/node14@1.0.3': {} - - '@tsconfig/node16@1.0.4': {} + '@tybys/wasm-util@0.10.0': + dependencies: + tslib: 2.8.1 + optional: true '@types/chai@5.2.2': dependencies: @@ -2864,8 +3187,6 @@ snapshots: '@types/through': 0.0.33 rxjs: 7.8.2 - '@types/istanbul-lib-coverage@2.0.6': {} - '@types/node@22.17.0': dependencies: undici-types: 6.21.0 @@ -2901,7 +3222,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.17.0)(@vitest/ui@3.2.4) + vitest: 3.2.4(@types/node@22.17.0)(@vitest/ui@3.2.4)(jiti@2.5.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -2913,13 +3234,13 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.0.6(@types/node@22.17.0))': + '@vitest/mocker@3.2.4(vite@7.0.6(@types/node@22.17.0)(jiti@2.5.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 7.0.6(@types/node@22.17.0) + vite: 7.0.6(@types/node@22.17.0)(jiti@2.5.1)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -2950,7 +3271,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.17.0)(@vitest/ui@3.2.4) + vitest: 3.2.4(@types/node@22.17.0)(@vitest/ui@3.2.4)(jiti@2.5.1)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -2958,12 +3279,6 @@ snapshots: loupe: 3.2.0 tinyrainbow: 2.0.0 - acorn-walk@8.3.4: - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - agent-base@7.1.4: {} aggregate-error@5.0.0: @@ -2995,8 +3310,6 @@ snapshots: any-promise@1.3.0: {} - arg@4.1.3: {} - argparse@2.0.1: {} argv-formatter@1.0.0: {} @@ -3029,20 +3342,6 @@ snapshots: buffer-from@1.1.2: {} - c8@10.1.3: - dependencies: - '@bcoe/v8-coverage': 1.0.2 - '@istanbuljs/schema': 0.1.3 - find-up: 5.0.0 - foreground-child: 3.3.1 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.1.7 - test-exclude: 7.0.1 - v8-to-istanbul: 9.3.0 - yargs: 17.7.2 - yargs-parser: 21.1.1 - cac@6.7.14: {} callsites@3.1.0: {} @@ -3068,6 +3367,8 @@ snapshots: chalk@5.4.1: {} + chalk@5.6.0: {} + char-regex@1.0.2: {} chardet@0.7.0: {} @@ -3078,6 +3379,10 @@ snapshots: dependencies: escape-string-regexp: 5.0.0 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + cli-highlight@2.1.11: dependencies: chalk: 4.1.2 @@ -3093,6 +3398,11 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + cli-width@4.1.0: {} cliui@7.0.4: @@ -3125,6 +3435,10 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + + commander@14.0.0: {} + compare-func@2.0.0: dependencies: array-ify: 1.0.0 @@ -3154,8 +3468,6 @@ snapshots: convert-hrtime@5.0.0: {} - convert-source-map@2.0.0: {} - core-util-is@1.0.3: {} cosmiconfig@9.0.0(typescript@5.9.2): @@ -3167,8 +3479,6 @@ snapshots: optionalDependencies: typescript: 5.9.2 - create-require@1.1.1: {} - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3189,8 +3499,6 @@ snapshots: deprecation@2.3.1: {} - diff@4.0.2: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -3199,6 +3507,18 @@ snapshots: dependencies: is-obj: 2.0.0 + dprint@0.50.1: + optionalDependencies: + '@dprint/darwin-arm64': 0.50.1 + '@dprint/darwin-x64': 0.50.1 + '@dprint/linux-arm64-glibc': 0.50.1 + '@dprint/linux-arm64-musl': 0.50.1 + '@dprint/linux-riscv64-glibc': 0.50.1 + '@dprint/linux-x64-glibc': 0.50.1 + '@dprint/linux-x64-musl': 0.50.1 + '@dprint/win32-arm64': 0.50.1 + '@dprint/win32-x64': 0.50.1 + duplexer2@0.1.4: dependencies: readable-stream: 2.3.8 @@ -3267,6 +3587,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + eventemitter3@5.0.1: {} + execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -3316,6 +3638,10 @@ snapshots: dependencies: reusify: 1.1.0 + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + fdir@6.4.6(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -3340,10 +3666,11 @@ snapshots: dependencies: locate-path: 2.0.0 - find-up@5.0.0: + find-up@7.0.0: dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 find-versions@6.0.0: dependencies: @@ -3357,6 +3684,10 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + formatly@0.2.4: + dependencies: + fd-package-json: 2.0.0 + from2@2.3.0: dependencies: inherits: 2.0.4 @@ -3468,6 +3799,8 @@ snapshots: human-signals@8.0.1: {} + husky@9.1.7: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -3519,6 +3852,12 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -3576,6 +3915,8 @@ snapshots: java-properties@1.0.2: {} + jiti@2.5.1: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -3594,8 +3935,52 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + knip@5.62.0(@types/node@22.17.0)(typescript@5.9.2): + dependencies: + '@nodelib/fs.walk': 1.2.8 + '@types/node': 22.17.0 + fast-glob: 3.3.3 + formatly: 0.2.4 + jiti: 2.5.1 + js-yaml: 4.1.0 + minimist: 1.2.8 + oxc-resolver: 11.6.1 + picocolors: 1.1.1 + picomatch: 4.0.3 + smol-toml: 1.4.2 + strip-json-comments: 5.0.2 + typescript: 5.9.2 + zod: 3.25.76 + zod-validation-error: 3.5.3(zod@3.25.76) + + lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} + lint-staged@16.1.5: + dependencies: + chalk: 5.6.0 + commander: 14.0.0 + debug: 4.4.1 + lilconfig: 3.1.3 + listr2: 9.0.1 + micromatch: 4.0.8 + nano-spawn: 1.0.2 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.1 + transitivePeerDependencies: + - supports-color + + listr2@9.0.1: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + load-json-file@4.0.0: dependencies: graceful-fs: 4.2.11 @@ -3608,9 +3993,9 @@ snapshots: p-locate: 2.0.0 path-exists: 3.0.0 - locate-path@6.0.0: + locate-path@7.2.0: dependencies: - p-locate: 5.0.0 + p-locate: 6.0.0 lodash-es@4.17.21: {} @@ -3624,6 +4009,14 @@ snapshots: lodash.uniqby@4.7.0: {} + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + loupe@3.2.0: {} lru-cache@10.4.3: {} @@ -3642,8 +4035,6 @@ snapshots: dependencies: semver: 7.7.2 - make-error@1.3.6: {} - marked-terminal@7.3.0(marked@15.0.12): dependencies: ansi-escapes: 7.0.0 @@ -3672,6 +4063,8 @@ snapshots: mimic-fn@4.0.0: {} + mimic-function@5.0.1: {} + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -3692,8 +4085,12 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nano-spawn@1.0.2: {} + nanoid@3.3.11: {} + napi-postinstall@0.3.3: {} + neo-async@2.6.2: {} nerf-dart@1.0.0: {} @@ -3734,8 +4131,36 @@ snapshots: dependencies: mimic-fn: 4.0.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + os-tmpdir@1.0.2: {} + oxc-resolver@11.6.1: + dependencies: + napi-postinstall: 0.3.3 + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.6.1 + '@oxc-resolver/binding-android-arm64': 11.6.1 + '@oxc-resolver/binding-darwin-arm64': 11.6.1 + '@oxc-resolver/binding-darwin-x64': 11.6.1 + '@oxc-resolver/binding-freebsd-x64': 11.6.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.6.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.6.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.6.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.6.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.6.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.6.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.6.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.6.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.6.1 + '@oxc-resolver/binding-linux-x64-musl': 11.6.1 + '@oxc-resolver/binding-wasm32-wasi': 11.6.1 + '@oxc-resolver/binding-win32-arm64-msvc': 11.6.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.6.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.6.1 + p-each-series@3.0.0: {} p-filter@4.1.0: @@ -3748,17 +4173,17 @@ snapshots: dependencies: p-try: 1.0.0 - p-limit@3.1.0: + p-limit@4.0.0: dependencies: - yocto-queue: 0.1.0 + yocto-queue: 1.2.1 p-locate@2.0.0: dependencies: p-limit: 1.3.0 - p-locate@5.0.0: + p-locate@6.0.0: dependencies: - p-limit: 3.1.0 + p-limit: 4.0.0 p-map@7.0.3: {} @@ -3802,7 +4227,7 @@ snapshots: path-exists@3.0.0: {} - path-exists@4.0.0: {} + path-exists@5.0.0: {} path-key@3.1.1: {} @@ -3827,6 +4252,8 @@ snapshots: picomatch@4.0.3: {} + pidtree@0.6.0: {} + pify@3.0.0: {} pkg-conf@2.1.0: @@ -3840,8 +4267,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.6.2: {} - pretty-ms@9.2.0: dependencies: parse-ms: 4.0.0 @@ -3895,8 +4320,15 @@ snapshots: resolve-from@5.0.0: {} + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + reusify@1.1.0: {} + rfdc@1.4.1: {} + rollup@4.46.2: dependencies: '@types/estree': 1.0.8 @@ -4008,6 +4440,18 @@ snapshots: slash@5.1.0: {} + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + smol-toml@1.4.2: {} + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -4046,6 +4490,8 @@ snapshots: duplexer2: 0.1.4 readable-stream: 2.3.8 + string-argv@0.3.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -4084,6 +4530,8 @@ snapshots: strip-json-comments@2.0.1: {} + strip-json-comments@5.0.2: {} + strip-literal@3.0.0: dependencies: js-tokens: 9.0.1 @@ -4165,24 +4613,6 @@ snapshots: traverse@0.6.8: {} - ts-node@10.9.2(@types/node@22.17.0)(typescript@5.9.2): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 22.17.0 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.9.2 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - tslib@2.8.1: {} type-fest@0.21.3: {} @@ -4220,26 +4650,18 @@ snapshots: util-deprecate@1.0.2: {} - v8-compile-cache-lib@3.0.1: {} - - v8-to-istanbul@9.3.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.29 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 - validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite-node@3.2.4(@types/node@22.17.0): + vite-node@3.2.4(@types/node@22.17.0)(jiti@2.5.1)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.0.6(@types/node@22.17.0) + vite: 7.0.6(@types/node@22.17.0)(jiti@2.5.1)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -4254,7 +4676,7 @@ snapshots: - tsx - yaml - vite@7.0.6(@types/node@22.17.0): + vite@7.0.6(@types/node@22.17.0)(jiti@2.5.1)(yaml@2.8.1): dependencies: esbuild: 0.25.8 fdir: 6.4.6(picomatch@4.0.3) @@ -4265,12 +4687,14 @@ snapshots: optionalDependencies: '@types/node': 22.17.0 fsevents: 2.3.3 + jiti: 2.5.1 + yaml: 2.8.1 - vitest@3.2.4(@types/node@22.17.0)(@vitest/ui@3.2.4): + vitest@3.2.4(@types/node@22.17.0)(@vitest/ui@3.2.4)(jiti@2.5.1)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.0.6(@types/node@22.17.0)) + '@vitest/mocker': 3.2.4(vite@7.0.6(@types/node@22.17.0)(jiti@2.5.1)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4288,8 +4712,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.0.6(@types/node@22.17.0) - vite-node: 3.2.4(@types/node@22.17.0) + vite: 7.0.6(@types/node@22.17.0)(jiti@2.5.1)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.17.0)(jiti@2.5.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.17.0 @@ -4308,6 +4732,8 @@ snapshots: - tsx - yaml + walk-up-path@4.0.0: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -4349,6 +4775,8 @@ snapshots: y18n@5.0.8: {} + yaml@2.8.1: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} @@ -4384,10 +4812,16 @@ snapshots: y18n: 5.0.8 yargs-parser: 22.0.0 - yn@3.1.1: {} - - yocto-queue@0.1.0: {} + yocto-queue@1.2.1: {} yoctocolors-cjs@2.1.2: {} yoctocolors@2.1.1: {} + + zod-validation-error@3.5.3(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} + + zod@4.0.17: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..f7fff18 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - dprint diff --git a/src/OctokitPlus.ts b/src/OctokitPlus.ts index 9aac013..5f5a2b2 100644 --- a/src/OctokitPlus.ts +++ b/src/OctokitPlus.ts @@ -61,7 +61,7 @@ export class OctokitPlus { .getRef({ repo: prRef.repo.name, owner: prRef.repo.owner.login, - ref: `heads/${prRef.ref}` + ref: `heads/${prRef.ref}`, }) .catch(convert404); @@ -80,19 +80,20 @@ export class OctokitPlus { return this.octokit.rest.git.deleteRef({ owner: prRef.repo.owner.login, repo: prRef.repo.name, - ref: `heads/${prRef.ref}` + ref: `heads/${prRef.ref}`, }); } public async *getPullRequests(opts: Parameters[0]) { - for await (const { data: pullRequests } of this.octokit.paginate.iterator( - this.octokit.rest.pulls.list, - opts - )) { + for await ( + const { data: pullRequests } of this.octokit.paginate.iterator( + this.octokit.rest.pulls.list, + opts, + ) + ) { for (const pr of pullRequests) { yield pr as PullRequest; } } } } - diff --git a/src/cli.ts b/src/cli.ts index de04053..7d4b9df 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,9 +1,9 @@ +import sourceMapSupport from "source-map-support"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import { prunePullRequestsCommand } from "./commands/PrunePullRequests.js"; -import { pruneLocalBranchesCommand } from "./commands/PruneLocalBranches.js"; import { pruneAllCommand } from "./commands/PruneAll.js"; -import sourceMapSupport from "source-map-support"; +import { pruneLocalBranchesCommand } from "./commands/PruneLocalBranches.js"; +import { prunePullRequestsCommand } from "./commands/PrunePullRequests.js"; sourceMapSupport.install(); export default function cli() { diff --git a/src/commands/PruneAll.test.ts b/src/commands/PruneAll.test.ts index 3a3ee04..85fa84a 100644 --- a/src/commands/PruneAll.test.ts +++ b/src/commands/PruneAll.test.ts @@ -1,25 +1,25 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { pruneAllCommand } from './PruneAll.js'; -import { createOctokitPlus } from '../utils/createOctokitPlus.js'; -import { getGitRemote } from '../utils/getGitRemote.js'; -import { isGitRepository } from '../utils/localGitOperations.js'; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createOctokitPlus } from "../utils/createOctokitPlus.js"; +import { getGitRemote } from "../utils/getGitRemote.js"; +import { isGitRepository } from "../utils/localGitOperations.js"; +import { pruneAllCommand } from "./PruneAll.js"; // Mock all dependencies -vi.mock('../utils/createOctokitPlus.js'); -vi.mock('../utils/getGitRemote.js'); -vi.mock('../utils/localGitOperations.js'); +vi.mock("../utils/createOctokitPlus.js"); +vi.mock("../utils/getGitRemote.js"); +vi.mock("../utils/localGitOperations.js"); // Mock the individual command modules -vi.mock('./PrunePullRequests.js', () => ({ +vi.mock("./PrunePullRequests.js", () => ({ prunePullRequestsCommand: { - handler: vi.fn() - } + handler: vi.fn(), + }, })); -vi.mock('./PruneLocalBranches.js', () => ({ +vi.mock("./PruneLocalBranches.js", () => ({ pruneLocalBranchesCommand: { - handler: vi.fn() - } + handler: vi.fn(), + }, })); const mockedCreateOctokitPlus = vi.mocked(createOctokitPlus); @@ -27,27 +27,29 @@ const mockedGetGitRemote = vi.mocked(getGitRemote); const mockedIsGitRepository = vi.mocked(isGitRepository); // Import after mocking -import { prunePullRequestsCommand } from './PrunePullRequests.js'; -import { pruneLocalBranchesCommand } from './PruneLocalBranches.js'; +import { pruneLocalBranchesCommand } from "./PruneLocalBranches.js"; +import { prunePullRequestsCommand } from "./PrunePullRequests.js"; -describe('PruneAll', () => { +describe("PruneAll", () => { let consoleLogSpy: ReturnType; let consoleErrorSpy: ReturnType; let processExitSpy: any; beforeEach(() => { - consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - processExitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { - throw new Error('process.exit'); - }) as any); - + consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + processExitSpy = vi.spyOn(process, "exit").mockImplementation( + (() => { + throw new Error("process.exit"); + }) as any, + ); + // Reset all mocks vi.clearAllMocks(); - + // Setup default mocks mockedIsGitRepository.mockReturnValue(true); - mockedGetGitRemote.mockReturnValue({ owner: 'testowner', repo: 'testrepo', host: 'github.com' }); + mockedGetGitRemote.mockReturnValue({ owner: "testowner", repo: "testrepo", host: "github.com" }); mockedCreateOctokitPlus.mockReturnValue({} as any); }); @@ -57,27 +59,27 @@ describe('PruneAll', () => { processExitSpy.mockRestore(); }); - describe('Repository Detection', () => { - it('should use provided repo argument', async () => { + describe("Repository Detection", () => { + it("should use provided repo argument", async () => { const args = { - repo: { owner: 'customowner', repo: 'customrepo' }, + repo: { owner: "customowner", repo: "customrepo" }, dryRun: false, - force: false + force: false, }; await pruneAllCommand.handler!(args); expect(prunePullRequestsCommand.handler).toHaveBeenCalledWith({ ...args, - repo: { owner: 'customowner', repo: 'customrepo' } + repo: { owner: "customowner", repo: "customrepo" }, }); expect(pruneLocalBranchesCommand.handler).toHaveBeenCalledWith({ ...args, - repo: { owner: 'customowner', repo: 'customrepo' } + repo: { owner: "customowner", repo: "customrepo" }, }); }); - it('should detect repo from git remote when no repo provided', async () => { + it("should detect repo from git remote when no repo provided", async () => { const args = { dryRun: false, force: false }; await pruneAllCommand.handler!(args); @@ -85,74 +87,76 @@ describe('PruneAll', () => { expect(mockedGetGitRemote).toHaveBeenCalled(); expect(prunePullRequestsCommand.handler).toHaveBeenCalledWith({ ...args, - repo: { owner: 'testowner', repo: 'testrepo' } + repo: { owner: "testowner", repo: "testrepo" }, }); expect(pruneLocalBranchesCommand.handler).toHaveBeenCalledWith({ ...args, - repo: { owner: 'testowner', repo: 'testrepo' } + repo: { owner: "testowner", repo: "testrepo" }, }); }); - it('should throw error when not in git repo and no repo provided', async () => { + it("should throw error when not in git repo and no repo provided", async () => { mockedIsGitRepository.mockReturnValue(false); const args = { dryRun: false, force: false }; await expect(pruneAllCommand.handler!(args)).rejects.toThrow( - 'This command must be run from within a git repository or specify owner/repo.' + "This command must be run from within a git repository or specify owner/repo.", ); }); - it('should throw error when git remote detection fails', async () => { + it("should throw error when git remote detection fails", async () => { mockedGetGitRemote.mockReturnValue(null); const args = { dryRun: false, force: false }; await expect(pruneAllCommand.handler!(args)).rejects.toThrow( - 'No repo specified and unable to detect from git remote' + "No repo specified and unable to detect from git remote", ); }); }); - describe('Command Execution', () => { - it('should execute both commands successfully', async () => { + describe("Command Execution", () => { + it("should execute both commands successfully", async () => { const args = { dryRun: false, force: false }; await pruneAllCommand.handler!(args); expect(prunePullRequestsCommand.handler).toHaveBeenCalledTimes(1); expect(pruneLocalBranchesCommand.handler).toHaveBeenCalledTimes(1); - expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('All cleanup operations completed successfully!')); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining("All cleanup operations completed successfully!"), + ); }); - it('should pass through dry-run flag to both commands', async () => { + it("should pass through dry-run flag to both commands", async () => { const args = { dryRun: true, force: false }; await pruneAllCommand.handler!(args); expect(prunePullRequestsCommand.handler).toHaveBeenCalledWith( - expect.objectContaining({ dryRun: true }) + expect.objectContaining({ dryRun: true }), ); expect(pruneLocalBranchesCommand.handler).toHaveBeenCalledWith( - expect.objectContaining({ dryRun: true }) + expect.objectContaining({ dryRun: true }), ); }); - it('should pass through force flag to both commands', async () => { + it("should pass through force flag to both commands", async () => { const args = { dryRun: false, force: true }; await pruneAllCommand.handler!(args); expect(prunePullRequestsCommand.handler).toHaveBeenCalledWith( - expect.objectContaining({ force: true }) + expect.objectContaining({ force: true }), ); expect(pruneLocalBranchesCommand.handler).toHaveBeenCalledWith( - expect.objectContaining({ force: true }) + expect.objectContaining({ force: true }), ); }); }); - describe('Error Handling', () => { - it('should continue with local cleanup when remote fails', async () => { - vi.mocked(prunePullRequestsCommand.handler!).mockRejectedValueOnce(new Error('Remote error')); + describe("Error Handling", () => { + it("should continue with local cleanup when remote fails", async () => { + vi.mocked(prunePullRequestsCommand.handler!).mockRejectedValueOnce(new Error("Remote error")); const args = { dryRun: false, force: false }; try { @@ -162,12 +166,12 @@ describe('PruneAll', () => { } expect(pruneLocalBranchesCommand.handler).toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Remote cleanup failed: Remote error')); - expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Cleanup completed with some errors')); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Remote cleanup failed: Remote error")); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Cleanup completed with some errors")); }); - it('should handle local cleanup failure', async () => { - vi.mocked(pruneLocalBranchesCommand.handler!).mockRejectedValueOnce(new Error('Local error')); + it("should handle local cleanup failure", async () => { + vi.mocked(pruneLocalBranchesCommand.handler!).mockRejectedValueOnce(new Error("Local error")); const args = { dryRun: false, force: false }; try { @@ -176,72 +180,72 @@ describe('PruneAll', () => { // Expected process.exit } - expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Local cleanup failed: Local error')); - expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Cleanup completed with some errors')); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Local cleanup failed: Local error")); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Cleanup completed with some errors")); }); - it('should exit with error code 1 when both commands fail', async () => { - vi.mocked(prunePullRequestsCommand.handler!).mockRejectedValueOnce(new Error('Remote error')); - vi.mocked(pruneLocalBranchesCommand.handler!).mockRejectedValueOnce(new Error('Local error')); + it("should exit with error code 1 when both commands fail", async () => { + vi.mocked(prunePullRequestsCommand.handler!).mockRejectedValueOnce(new Error("Remote error")); + vi.mocked(pruneLocalBranchesCommand.handler!).mockRejectedValueOnce(new Error("Local error")); const args = { dryRun: false, force: false }; try { await pruneAllCommand.handler!(args); } catch (e) { - expect(e).toEqual(new Error('process.exit')); + expect(e).toEqual(new Error("process.exit")); } expect(processExitSpy).toHaveBeenCalledWith(1); - expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Both cleanup operations failed!')); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Both cleanup operations failed!")); }); - it('should exit with code 0 on partial success', async () => { - vi.mocked(prunePullRequestsCommand.handler!).mockRejectedValueOnce(new Error('Remote error')); + it("should exit with code 0 on partial success", async () => { + vi.mocked(prunePullRequestsCommand.handler!).mockRejectedValueOnce(new Error("Remote error")); const args = { dryRun: false, force: false }; try { await pruneAllCommand.handler!(args); } catch (e) { - expect(e).toEqual(new Error('process.exit')); + expect(e).toEqual(new Error("process.exit")); } expect(processExitSpy).toHaveBeenCalledWith(0); }); }); - describe('Command Configuration', () => { - it('should have correct command definition', () => { - expect(pruneAllCommand.command).toBe('all [--dry-run] [--force] [repo]'); - expect(pruneAllCommand.describe).toBe('Delete both remote and local merged branches'); + describe("Command Configuration", () => { + it("should have correct command definition", () => { + expect(pruneAllCommand.command).toBe("all [--dry-run] [--force] [repo]"); + expect(pruneAllCommand.describe).toBe("Delete both remote and local merged branches"); }); - it('should validate repo format correctly', () => { + it("should validate repo format correctly", () => { const builder = pruneAllCommand.builder as any; const yargsMock = { env: vi.fn().mockReturnThis(), option: vi.fn().mockReturnThis(), - positional: vi.fn().mockReturnThis() + positional: vi.fn().mockReturnThis(), }; builder(yargsMock); const positionalCall = yargsMock.positional.mock.calls.find( - (call: any[]) => call[0] === 'repo' + (call: any[]) => call[0] === "repo", ); expect(positionalCall).toBeDefined(); const coerce = positionalCall![1].coerce; // Test valid repo format - expect(coerce('owner/repo')).toEqual({ owner: 'owner', repo: 'repo' }); + expect(coerce("owner/repo")).toEqual({ owner: "owner", repo: "repo" }); // Test invalid formats - expect(() => coerce('invalid')).toThrow('Repository must be in the format'); - expect(() => coerce('owner/')).toThrow('Repository must be in the format'); - expect(() => coerce('/repo')).toThrow('Repository must be in the format'); - expect(() => coerce('-owner/repo')).toThrow('Invalid owner name'); + expect(() => coerce("invalid")).toThrow("Repository must be in the format"); + expect(() => coerce("owner/")).toThrow("Repository must be in the format"); + expect(() => coerce("/repo")).toThrow("Repository must be in the format"); + expect(() => coerce("-owner/repo")).toThrow("Invalid owner name"); // Note: GitHub actually allows repos to start with hyphens, dots, or underscores - expect(coerce('owner/-repo')).toEqual({ owner: 'owner', repo: '-repo' }); + expect(coerce("owner/-repo")).toEqual({ owner: "owner", repo: "-repo" }); }); }); -}); \ No newline at end of file +}); diff --git a/src/commands/PruneAll.ts b/src/commands/PruneAll.ts index 8f57173..5e50abb 100644 --- a/src/commands/PruneAll.ts +++ b/src/commands/PruneAll.ts @@ -21,7 +21,9 @@ export const pruneAllCommand: CommandModule = { // Try to get from git remote const gitRemote = getGitRemote(); if (!gitRemote) { - throw new Error("No repo specified and unable to detect from git remote. Please run from a git repository or specify owner/repo."); + throw new Error( + "No repo specified and unable to detect from git remote. Please run from a git repository or specify owner/repo.", + ); } owner = gitRemote.owner; repo = gitRemote.repo; @@ -45,7 +47,7 @@ export const pruneAllCommand: CommandModule = { const { prunePullRequestsCommand } = await import("./PrunePullRequests.js"); await prunePullRequestsCommand.handler!({ ...args, - repo: { owner, repo } + repo: { owner, repo }, }); remoteSuccess = true; } catch (error) { @@ -60,7 +62,7 @@ export const pruneAllCommand: CommandModule = { const { pruneLocalBranchesCommand } = await import("./PruneLocalBranches.js"); await pruneLocalBranchesCommand.handler!({ ...args, - repo: { owner, repo } + repo: { owner, repo }, }); localSuccess = true; } catch (error) { @@ -92,11 +94,11 @@ export const pruneAllCommand: CommandModule = { .env() .option("dry-run", { type: "boolean", - description: "Perform a dry run (show what would be deleted)" + description: "Perform a dry run (show what would be deleted)", }) .option("force", { type: "boolean", - description: "Skip interactive mode and delete all safe branches automatically" + description: "Skip interactive mode and delete all safe branches automatically", }) .positional("repo", { type: "string", @@ -104,26 +106,30 @@ export const pruneAllCommand: CommandModule = { if (!s) { return undefined; } - + // Validate repo string format (owner/repo) const parts = s.split("/"); if (parts.length !== 2 || !parts[0] || !parts[1]) { throw new Error("Repository must be in the format 'owner/repo'"); } - + // Validate owner and repo names (GitHub naming rules) const ownerRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/; const repoRegex = /^[a-zA-Z0-9._-]+$/; - + if (!ownerRegex.test(parts[0])) { - throw new Error("Invalid owner name. Must contain only alphanumeric characters and hyphens, and cannot start or end with a hyphen."); + throw new Error( + "Invalid owner name. Must contain only alphanumeric characters and hyphens, and cannot start or end with a hyphen.", + ); } - + if (!repoRegex.test(parts[1])) { - throw new Error("Invalid repository name. Must contain only alphanumeric characters, dots, underscores, and hyphens."); + throw new Error( + "Invalid repository name. Must contain only alphanumeric characters, dots, underscores, and hyphens.", + ); } - + return { owner: parts[0], repo: parts[1] }; - } - }) -}; \ No newline at end of file + }, + }), +}; diff --git a/src/commands/PruneLocalBranches.test.ts b/src/commands/PruneLocalBranches.test.ts index e11dfd4..8f4c650 100644 --- a/src/commands/PruneLocalBranches.test.ts +++ b/src/commands/PruneLocalBranches.test.ts @@ -1,25 +1,20 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { pruneLocalBranchesCommand } from './PruneLocalBranches.js'; -import { createOctokitPlus } from '../utils/createOctokitPlus.js'; -import { getGitRemote } from '../utils/getGitRemote.js'; -import { - getLocalBranches, - getCurrentBranch, - deleteLocalBranch, - isGitRepository -} from '../utils/localGitOperations.js'; -import { filterSafeBranches } from '../utils/branchSafetyChecks.js'; -import type { LocalBranch } from '../utils/localGitOperations.js'; -import type { PullRequest, OctokitPlus } from '../OctokitPlus.js'; -import inquirer from 'inquirer'; +import inquirer from "inquirer"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OctokitPlus, PullRequest } from "../OctokitPlus.js"; +import { filterSafeBranches } from "../utils/branchSafetyChecks.js"; +import { createOctokitPlus } from "../utils/createOctokitPlus.js"; +import { getGitRemote } from "../utils/getGitRemote.js"; +import { deleteLocalBranch, getCurrentBranch, getLocalBranches, isGitRepository } from "../utils/localGitOperations.js"; +import type { LocalBranch } from "../utils/localGitOperations.js"; +import { pruneLocalBranchesCommand } from "./PruneLocalBranches.js"; // Mock all dependencies -vi.mock('../../src/utils/createOctokitPlus.js'); -vi.mock('../../src/utils/getGitRemote.js'); -vi.mock('../../src/utils/localGitOperations.js'); -vi.mock('../../src/utils/branchSafetyChecks.js'); -vi.mock('progress'); -vi.mock('inquirer'); +vi.mock("../../src/utils/createOctokitPlus.js"); +vi.mock("../../src/utils/getGitRemote.js"); +vi.mock("../../src/utils/localGitOperations.js"); +vi.mock("../../src/utils/branchSafetyChecks.js"); +vi.mock("progress"); +vi.mock("inquirer"); const mockedCreateOctokitPlus = vi.mocked(createOctokitPlus); const mockedGetGitRemote = vi.mocked(getGitRemote); @@ -30,7 +25,7 @@ const mockedIsGitRepository = vi.mocked(isGitRepository); const mockedFilterSafeBranches = vi.mocked(filterSafeBranches); const mockedInquirer = vi.mocked(inquirer); -describe('PruneLocalBranches', () => { +describe("PruneLocalBranches", () => { let mockOctokitPlus: OctokitPlus; let consoleLogSpy: ReturnType; let consoleErrorSpy: ReturnType; @@ -38,47 +33,52 @@ describe('PruneLocalBranches', () => { const createLocalBranch = (name: string, sha: string, isCurrent: boolean = false): LocalBranch => ({ name, sha, - isCurrent + isCurrent, }); - const createPullRequest = (number: number, headRef: string, headSha: string, mergeCommitSha?: string): PullRequest => ({ + const createPullRequest = ( + number: number, + headRef: string, + headSha: string, + mergeCommitSha?: string, + ): PullRequest => ({ id: 123 + number, number, - user: { login: 'user' }, - state: 'closed', + user: { login: "user" }, + state: "closed", head: { label: `user:${headRef}`, ref: headRef, sha: headSha, repo: { - name: 'test-repo', - owner: { login: 'user' }, - fork: false - } + name: "test-repo", + owner: { login: "user" }, + fork: false, + }, }, base: { - label: 'user:main', - ref: 'main', - sha: 'base-sha', + label: "user:main", + ref: "main", + sha: "base-sha", repo: { - name: 'test-repo', - owner: { login: 'user' }, - fork: false - } + name: "test-repo", + owner: { login: "user" }, + fork: false, + }, }, - merge_commit_sha: mergeCommitSha || null + merge_commit_sha: mergeCommitSha || null, }); beforeEach(() => { vi.clearAllMocks(); // Mock console methods - consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); // Mock inquirer.prompt to auto-select all branches by default (mockedInquirer.prompt as any).mockImplementation(async (questions: any) => { - if (Array.isArray(questions) && questions[0]?.type === 'checkbox') { + if (Array.isArray(questions) && questions[0]?.type === "checkbox") { // Return all checked choices const choices = questions[0].choices; const selectedValues = choices @@ -90,16 +90,16 @@ describe('PruneLocalBranches', () => { }); // Mock process.stderr.isTTY - Object.defineProperty(process.stderr, 'isTTY', { + Object.defineProperty(process.stderr, "isTTY", { value: false, - configurable: true + configurable: true, }); // Setup mock OctokitPlus mockOctokitPlus = { getPullRequests: vi.fn(), getReference: vi.fn(), - deleteReference: vi.fn() + deleteReference: vi.fn(), } as any; mockedCreateOctokitPlus.mockReturnValue(mockOctokitPlus); @@ -112,132 +112,132 @@ describe('PruneLocalBranches', () => { consoleErrorSpy.mockRestore(); }); - describe('command configuration', () => { - it('should have correct command definition', () => { - expect(pruneLocalBranchesCommand.command).toBe('local [--dry-run] [--force] [repo]'); - expect(pruneLocalBranchesCommand.describe).toBe('Delete merged local branches from pull requests'); + describe("command configuration", () => { + it("should have correct command definition", () => { + expect(pruneLocalBranchesCommand.command).toBe("local [--dry-run] [--force] [repo]"); + expect(pruneLocalBranchesCommand.describe).toBe("Delete merged local branches from pull requests"); }); - it('should configure yargs builder correctly', () => { + it("should configure yargs builder correctly", () => { const mockYargs = { env: vi.fn().mockReturnThis(), option: vi.fn().mockReturnThis(), - positional: vi.fn().mockReturnThis() + positional: vi.fn().mockReturnThis(), }; (pruneLocalBranchesCommand.builder as any)(mockYargs); expect(mockYargs.env).toHaveBeenCalled(); - expect(mockYargs.option).toHaveBeenCalledWith('dry-run', expect.any(Object)); - expect(mockYargs.option).toHaveBeenCalledWith('force', expect.any(Object)); - expect(mockYargs.positional).toHaveBeenCalledWith('repo', expect.any(Object)); + expect(mockYargs.option).toHaveBeenCalledWith("dry-run", expect.any(Object)); + expect(mockYargs.option).toHaveBeenCalledWith("force", expect.any(Object)); + expect(mockYargs.positional).toHaveBeenCalledWith("repo", expect.any(Object)); }); }); - describe('repo string validation', () => { - it('should parse valid repo string', () => { + describe("repo string validation", () => { + it("should parse valid repo string", () => { const mockYargs = { env: vi.fn().mockReturnThis(), option: vi.fn().mockReturnThis(), positional: vi.fn((key, config) => { - if (key === 'repo' && config.coerce) { - const result = config.coerce('owner/repo'); - expect(result).toEqual({ owner: 'owner', repo: 'repo' }); + if (key === "repo" && config.coerce) { + const result = config.coerce("owner/repo"); + expect(result).toEqual({ owner: "owner", repo: "repo" }); } return mockYargs; - }) + }), }; (pruneLocalBranchesCommand.builder as any)(mockYargs); }); - it('should handle undefined repo string', () => { + it("should handle undefined repo string", () => { const mockYargs = { env: vi.fn().mockReturnThis(), option: vi.fn().mockReturnThis(), positional: vi.fn((key, config) => { - if (key === 'repo' && config.coerce) { + if (key === "repo" && config.coerce) { const result = config.coerce(undefined); expect(result).toBeUndefined(); } return mockYargs; - }) + }), }; (pruneLocalBranchesCommand.builder as any)(mockYargs); }); - it('should reject invalid repo format', () => { + it("should reject invalid repo format", () => { const mockYargs = { env: vi.fn().mockReturnThis(), option: vi.fn().mockReturnThis(), positional: vi.fn((key, config) => { - if (key === 'repo' && config.coerce) { - expect(() => config.coerce('invalid')).toThrow('Repository must be in the format \'owner/repo\''); - expect(() => config.coerce('owner/')).toThrow('Repository must be in the format \'owner/repo\''); - expect(() => config.coerce('/repo')).toThrow('Repository must be in the format \'owner/repo\''); + if (key === "repo" && config.coerce) { + expect(() => config.coerce("invalid")).toThrow("Repository must be in the format 'owner/repo'"); + expect(() => config.coerce("owner/")).toThrow("Repository must be in the format 'owner/repo'"); + expect(() => config.coerce("/repo")).toThrow("Repository must be in the format 'owner/repo'"); } return mockYargs; - }) + }), }; (pruneLocalBranchesCommand.builder as any)(mockYargs); }); - it('should validate owner name format', () => { + it("should validate owner name format", () => { const mockYargs = { env: vi.fn().mockReturnThis(), option: vi.fn().mockReturnThis(), positional: vi.fn((key, config) => { - if (key === 'repo' && config.coerce) { - expect(() => config.coerce('-invalid/repo')).toThrow('Invalid owner name'); - expect(() => config.coerce('invalid-/repo')).toThrow('Invalid owner name'); - expect(() => config.coerce('in@valid/repo')).toThrow('Invalid owner name'); + if (key === "repo" && config.coerce) { + expect(() => config.coerce("-invalid/repo")).toThrow("Invalid owner name"); + expect(() => config.coerce("invalid-/repo")).toThrow("Invalid owner name"); + expect(() => config.coerce("in@valid/repo")).toThrow("Invalid owner name"); } return mockYargs; - }) + }), }; (pruneLocalBranchesCommand.builder as any)(mockYargs); }); - it('should validate repo name format', () => { + it("should validate repo name format", () => { const mockYargs = { env: vi.fn().mockReturnThis(), option: vi.fn().mockReturnThis(), positional: vi.fn((key, config) => { - if (key === 'repo' && config.coerce) { - expect(() => config.coerce('owner/in@valid')).toThrow('Invalid repository name'); - expect(() => config.coerce('owner/in valid')).toThrow('Invalid repository name'); + if (key === "repo" && config.coerce) { + expect(() => config.coerce("owner/in@valid")).toThrow("Invalid repository name"); + expect(() => config.coerce("owner/in valid")).toThrow("Invalid repository name"); } return mockYargs; - }) + }), }; (pruneLocalBranchesCommand.builder as any)(mockYargs); }); }); - describe('handler execution', () => { - it('should throw error when not in git repository', async () => { + describe("handler execution", () => { + it("should throw error when not in git repository", async () => { mockedIsGitRepository.mockReturnValue(false); - await expect(pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: 'ghouls' })).rejects.toThrow( - 'This command must be run from within a git repository.' + await expect(pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: "ghouls" })).rejects.toThrow( + "This command must be run from within a git repository.", ); }); - it('should use provided repo when available', async () => { - const branches = [createLocalBranch('feature-1', 'abc123')]; + it("should use provided repo when available", async () => { + const branches = [createLocalBranch("feature-1", "abc123")]; mockedGetLocalBranches.mockReturnValue(branches); - mockedGetCurrentBranch.mockReturnValue('main'); + mockedGetCurrentBranch.mockReturnValue("main"); mockedFilterSafeBranches.mockReturnValue([ - { branch: branches[0], safetyCheck: { safe: true }, matchingPR: undefined } + { branch: branches[0], safetyCheck: { safe: true }, matchingPR: undefined }, ]); // Mock async generator for getPullRequests - const mockPullRequests = [createPullRequest(1, 'feature-1', 'abc123', 'merge-sha')]; - const asyncGenerator = (async function* () { + const mockPullRequests = [createPullRequest(1, "feature-1", "abc123", "merge-sha")]; + const asyncGenerator = (async function*() { for (const pr of mockPullRequests) { yield pr; } @@ -245,304 +245,303 @@ describe('PruneLocalBranches', () => { (mockOctokitPlus.getPullRequests as any).mockImplementation(() => asyncGenerator); const args = { - repo: { owner: 'test-owner', repo: 'test-repo' }, + repo: { owner: "test-owner", repo: "test-repo" }, dryRun: true, _: [], - $0: 'ghouls' + $0: "ghouls", }; await pruneLocalBranchesCommand.handler!(args); expect(mockOctokitPlus.getPullRequests).toHaveBeenCalledWith({ - repo: 'test-repo', - owner: 'test-owner', + repo: "test-repo", + owner: "test-owner", per_page: 100, - state: 'closed', - sort: 'updated', - direction: 'desc' + state: "closed", + sort: "updated", + direction: "desc", }); }); - it('should use git remote when repo not provided', async () => { - mockedGetGitRemote.mockReturnValue({ owner: 'remote-owner', repo: 'remote-repo', host: 'github.com' }); - - const branches = [createLocalBranch('feature-1', 'abc123')]; + it("should use git remote when repo not provided", async () => { + mockedGetGitRemote.mockReturnValue({ owner: "remote-owner", repo: "remote-repo", host: "github.com" }); + + const branches = [createLocalBranch("feature-1", "abc123")]; mockedGetLocalBranches.mockReturnValue(branches); - mockedGetCurrentBranch.mockReturnValue('main'); + mockedGetCurrentBranch.mockReturnValue("main"); mockedFilterSafeBranches.mockReturnValue([ - { branch: branches[0], safetyCheck: { safe: true }, matchingPR: undefined } + { branch: branches[0], safetyCheck: { safe: true }, matchingPR: undefined }, ]); // Mock async generator for getPullRequests - const mockPullRequests = [createPullRequest(1, 'feature-1', 'abc123', 'merge-sha')]; - const asyncGenerator = (async function* () { + const mockPullRequests = [createPullRequest(1, "feature-1", "abc123", "merge-sha")]; + const asyncGenerator = (async function*() { for (const pr of mockPullRequests) { yield pr; } })(); (mockOctokitPlus.getPullRequests as any).mockImplementation(() => asyncGenerator); - const args = { dryRun: false, _: [], $0: 'ghouls' }; + const args = { dryRun: false, _: [], $0: "ghouls" }; await pruneLocalBranchesCommand.handler!(args); expect(mockedGetGitRemote).toHaveBeenCalled(); expect(mockOctokitPlus.getPullRequests).toHaveBeenCalledWith({ - repo: 'remote-repo', - owner: 'remote-owner', + repo: "remote-repo", + owner: "remote-owner", per_page: 100, - state: 'closed', - sort: 'updated', - direction: 'desc' + state: "closed", + sort: "updated", + direction: "desc", }); }); - it('should throw error when no repo provided and no git remote', async () => { + it("should throw error when no repo provided and no git remote", async () => { mockedGetGitRemote.mockReturnValue(null); - await expect(pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: 'ghouls' })).rejects.toThrow( - 'No repo specified and unable to detect from git remote. Please run from a git repository or specify owner/repo.' + await expect(pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: "ghouls" })).rejects.toThrow( + "No repo specified and unable to detect from git remote. Please run from a git repository or specify owner/repo.", ); }); - it('should handle empty local branches', async () => { + it("should handle empty local branches", async () => { mockedGetLocalBranches.mockReturnValue([]); - mockedGetCurrentBranch.mockReturnValue('main'); - mockedGetGitRemote.mockReturnValue({ owner: 'owner', repo: 'repo', host: 'github.com' }); + mockedGetCurrentBranch.mockReturnValue("main"); + mockedGetGitRemote.mockReturnValue({ owner: "owner", repo: "repo", host: "github.com" }); // Mock async generator for getPullRequests - const asyncGenerator = (async function* () {})(); + const asyncGenerator = (async function*() {})(); (mockOctokitPlus.getPullRequests as any).mockImplementation(() => asyncGenerator); - await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: 'ghouls' }); + await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: "ghouls" }); - expect(consoleLogSpy).toHaveBeenCalledWith('No local branches found.'); + expect(consoleLogSpy).toHaveBeenCalledWith("No local branches found."); }); - it('should handle no safe branches to delete', async () => { + it("should handle no safe branches to delete", async () => { const branches = [ - createLocalBranch('main', 'abc123', true), - createLocalBranch('develop', 'def456') + createLocalBranch("main", "abc123", true), + createLocalBranch("develop", "def456"), ]; mockedGetLocalBranches.mockReturnValue(branches); - mockedGetCurrentBranch.mockReturnValue('main'); - mockedGetGitRemote.mockReturnValue({ owner: 'owner', repo: 'repo', host: 'github.com' }); + mockedGetCurrentBranch.mockReturnValue("main"); + mockedGetGitRemote.mockReturnValue({ owner: "owner", repo: "repo", host: "github.com" }); // All branches are unsafe mockedFilterSafeBranches.mockReturnValue([ - { branch: branches[0], safetyCheck: { safe: false, reason: 'current branch' }, matchingPR: undefined }, - { branch: branches[1], safetyCheck: { safe: false, reason: 'protected branch' }, matchingPR: undefined } + { branch: branches[0], safetyCheck: { safe: false, reason: "current branch" }, matchingPR: undefined }, + { branch: branches[1], safetyCheck: { safe: false, reason: "protected branch" }, matchingPR: undefined }, ]); // Mock async generator for getPullRequests - const asyncGenerator = (async function* () {})(); + const asyncGenerator = (async function*() {})(); (mockOctokitPlus.getPullRequests as any).mockImplementation(() => asyncGenerator); - await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: 'ghouls' }); + await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: "ghouls" }); - expect(consoleLogSpy).toHaveBeenCalledWith('\nNo branches are safe to delete.'); - expect(consoleLogSpy).toHaveBeenCalledWith('\nSkipping unsafe branches:'); - expect(consoleLogSpy).toHaveBeenCalledWith(' - main (current branch)'); - expect(consoleLogSpy).toHaveBeenCalledWith(' - develop (protected branch)'); + expect(consoleLogSpy).toHaveBeenCalledWith("\nNo branches are safe to delete."); + expect(consoleLogSpy).toHaveBeenCalledWith("\nSkipping unsafe branches:"); + expect(consoleLogSpy).toHaveBeenCalledWith(" - main (current branch)"); + expect(consoleLogSpy).toHaveBeenCalledWith(" - develop (protected branch)"); }); - it('should delete safe branches in non-dry-run mode', async () => { - const branches = [createLocalBranch('feature-1', 'abc123'), createLocalBranch('feature-2', 'def456')]; - const pr1 = createPullRequest(1, 'feature-1', 'abc123', 'merge-sha-1'); - const pr2 = createPullRequest(2, 'feature-2', 'def456', 'merge-sha-2'); - + it("should delete safe branches in non-dry-run mode", async () => { + const branches = [createLocalBranch("feature-1", "abc123"), createLocalBranch("feature-2", "def456")]; + const pr1 = createPullRequest(1, "feature-1", "abc123", "merge-sha-1"); + const pr2 = createPullRequest(2, "feature-2", "def456", "merge-sha-2"); + mockedGetLocalBranches.mockReturnValue(branches); - mockedGetCurrentBranch.mockReturnValue('main'); - mockedGetGitRemote.mockReturnValue({ owner: 'owner', repo: 'repo', host: 'github.com' }); + mockedGetCurrentBranch.mockReturnValue("main"); + mockedGetGitRemote.mockReturnValue({ owner: "owner", repo: "repo", host: "github.com" }); mockedFilterSafeBranches.mockReturnValue([ { branch: branches[0], safetyCheck: { safe: true }, matchingPR: pr1 }, - { branch: branches[1], safetyCheck: { safe: true }, matchingPR: pr2 } + { branch: branches[1], safetyCheck: { safe: true }, matchingPR: pr2 }, ]); // Mock async generator for getPullRequests const mockPullRequests = [pr1, pr2]; - const asyncGenerator = (async function* () { + const asyncGenerator = (async function*() { for (const pr of mockPullRequests) { yield pr; } })(); (mockOctokitPlus.getPullRequests as any).mockImplementation(() => asyncGenerator); - await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: 'ghouls' }); + await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: "ghouls" }); - expect(mockedDeleteLocalBranch).toHaveBeenCalledWith('feature-1'); - expect(mockedDeleteLocalBranch).toHaveBeenCalledWith('feature-2'); - expect(consoleLogSpy).toHaveBeenCalledWith('Deleted: feature-1 (#1)'); - expect(consoleLogSpy).toHaveBeenCalledWith('Deleted: feature-2 (#2)'); + expect(mockedDeleteLocalBranch).toHaveBeenCalledWith("feature-1"); + expect(mockedDeleteLocalBranch).toHaveBeenCalledWith("feature-2"); + expect(consoleLogSpy).toHaveBeenCalledWith("Deleted: feature-1 (#1)"); + expect(consoleLogSpy).toHaveBeenCalledWith("Deleted: feature-2 (#2)"); }); - it('should simulate deletion in dry-run mode', async () => { - const branches = [createLocalBranch('feature-1', 'abc123')]; - const pr1 = createPullRequest(1, 'feature-1', 'abc123', 'merge-sha-1'); - + it("should simulate deletion in dry-run mode", async () => { + const branches = [createLocalBranch("feature-1", "abc123")]; + const pr1 = createPullRequest(1, "feature-1", "abc123", "merge-sha-1"); + mockedGetLocalBranches.mockReturnValue(branches); - mockedGetCurrentBranch.mockReturnValue('main'); - mockedGetGitRemote.mockReturnValue({ owner: 'owner', repo: 'repo', host: 'github.com' }); + mockedGetCurrentBranch.mockReturnValue("main"); + mockedGetGitRemote.mockReturnValue({ owner: "owner", repo: "repo", host: "github.com" }); mockedFilterSafeBranches.mockReturnValue([ - { branch: branches[0], safetyCheck: { safe: true }, matchingPR: pr1 } + { branch: branches[0], safetyCheck: { safe: true }, matchingPR: pr1 }, ]); // Mock async generator for getPullRequests - const asyncGenerator = (async function* () { + const asyncGenerator = (async function*() { yield pr1; })(); (mockOctokitPlus.getPullRequests as any).mockImplementation(() => asyncGenerator); - await pruneLocalBranchesCommand.handler!({ dryRun: true, _: [], $0: 'ghouls' }); + await pruneLocalBranchesCommand.handler!({ dryRun: true, _: [], $0: "ghouls" }); expect(mockedDeleteLocalBranch).not.toHaveBeenCalled(); - expect(consoleLogSpy).toHaveBeenCalledWith('[DRY RUN] Would delete: feature-1 (#1)'); - expect(consoleLogSpy).toHaveBeenCalledWith(' Would delete: 1 branch'); + expect(consoleLogSpy).toHaveBeenCalledWith("[DRY RUN] Would delete: feature-1 (#1)"); + expect(consoleLogSpy).toHaveBeenCalledWith(" Would delete: 1 branch"); }); - it('should handle branches without matching PRs', async () => { - const branches = [createLocalBranch('feature-no-pr', 'abc123')]; - + it("should handle branches without matching PRs", async () => { + const branches = [createLocalBranch("feature-no-pr", "abc123")]; + mockedGetLocalBranches.mockReturnValue(branches); - mockedGetCurrentBranch.mockReturnValue('main'); - mockedGetGitRemote.mockReturnValue({ owner: 'owner', repo: 'repo', host: 'github.com' }); + mockedGetCurrentBranch.mockReturnValue("main"); + mockedGetGitRemote.mockReturnValue({ owner: "owner", repo: "repo", host: "github.com" }); mockedFilterSafeBranches.mockReturnValue([ - { branch: branches[0], safetyCheck: { safe: true }, matchingPR: undefined } + { branch: branches[0], safetyCheck: { safe: true }, matchingPR: undefined }, ]); // Mock async generator for getPullRequests (no PRs) - const asyncGenerator = (async function* () {})(); + const asyncGenerator = (async function*() {})(); (mockOctokitPlus.getPullRequests as any).mockImplementation(() => asyncGenerator); - await pruneLocalBranchesCommand.handler!({ dryRun: true, _: [], $0: 'ghouls' }); + await pruneLocalBranchesCommand.handler!({ dryRun: true, _: [], $0: "ghouls" }); - expect(consoleLogSpy).toHaveBeenCalledWith('[DRY RUN] Would delete: feature-no-pr (no PR)'); + expect(consoleLogSpy).toHaveBeenCalledWith("[DRY RUN] Would delete: feature-no-pr (no PR)"); }); - it('should handle deletion errors', async () => { - const branches = [createLocalBranch('feature-1', 'abc123')]; - const pr1 = createPullRequest(1, 'feature-1', 'abc123', 'merge-sha-1'); - + it("should handle deletion errors", async () => { + const branches = [createLocalBranch("feature-1", "abc123")]; + const pr1 = createPullRequest(1, "feature-1", "abc123", "merge-sha-1"); + mockedGetLocalBranches.mockReturnValue(branches); - mockedGetCurrentBranch.mockReturnValue('main'); - mockedGetGitRemote.mockReturnValue({ owner: 'owner', repo: 'repo', host: 'github.com' }); + mockedGetCurrentBranch.mockReturnValue("main"); + mockedGetGitRemote.mockReturnValue({ owner: "owner", repo: "repo", host: "github.com" }); mockedFilterSafeBranches.mockReturnValue([ - { branch: branches[0], safetyCheck: { safe: true }, matchingPR: pr1 } + { branch: branches[0], safetyCheck: { safe: true }, matchingPR: pr1 }, ]); mockedDeleteLocalBranch.mockImplementation(() => { - throw new Error('Git deletion failed'); + throw new Error("Git deletion failed"); }); // Mock async generator for getPullRequests - const asyncGenerator = (async function* () { + const asyncGenerator = (async function*() { yield pr1; })(); (mockOctokitPlus.getPullRequests as any).mockImplementation(() => asyncGenerator); - await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: 'ghouls' }); + await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: "ghouls" }); - expect(consoleLogSpy).toHaveBeenCalledWith('Error deleting feature-1: Git deletion failed'); - expect(consoleLogSpy).toHaveBeenCalledWith(' Successfully deleted: 0 branches'); - expect(consoleLogSpy).toHaveBeenCalledWith(' Errors: 1'); + expect(consoleLogSpy).toHaveBeenCalledWith("Error deleting feature-1: Git deletion failed"); + expect(consoleLogSpy).toHaveBeenCalledWith(" Successfully deleted: 0 branches"); + expect(consoleLogSpy).toHaveBeenCalledWith(" Errors: 1"); }); - it('should display progress information', async () => { + it("should display progress information", async () => { const branches = [ - createLocalBranch('feature-1', 'abc123'), - createLocalBranch('feature-2', 'def456'), - createLocalBranch('main', 'ghi789', true) + createLocalBranch("feature-1", "abc123"), + createLocalBranch("feature-2", "def456"), + createLocalBranch("main", "ghi789", true), ]; - const pr1 = createPullRequest(1, 'feature-1', 'abc123', 'merge-sha-1'); - + const pr1 = createPullRequest(1, "feature-1", "abc123", "merge-sha-1"); + mockedGetLocalBranches.mockReturnValue(branches); - mockedGetCurrentBranch.mockReturnValue('main'); - mockedGetGitRemote.mockReturnValue({ owner: 'owner', repo: 'repo', host: 'github.com' }); + mockedGetCurrentBranch.mockReturnValue("main"); + mockedGetGitRemote.mockReturnValue({ owner: "owner", repo: "repo", host: "github.com" }); mockedFilterSafeBranches.mockReturnValue([ { branch: branches[0], safetyCheck: { safe: true }, matchingPR: pr1 }, { branch: branches[1], safetyCheck: { safe: true }, matchingPR: undefined }, - { branch: branches[2], safetyCheck: { safe: false, reason: 'current branch' }, matchingPR: undefined } + { branch: branches[2], safetyCheck: { safe: false, reason: "current branch" }, matchingPR: undefined }, ]); // Mock async generator for getPullRequests - const asyncGenerator = (async function* () { + const asyncGenerator = (async function*() { yield pr1; })(); (mockOctokitPlus.getPullRequests as any).mockImplementation(() => asyncGenerator); - await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: 'ghouls' }); + await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: "ghouls" }); - expect(consoleLogSpy).toHaveBeenCalledWith('Found 3 local branches'); - expect(consoleLogSpy).toHaveBeenCalledWith('Found 1 merged pull requests'); - expect(consoleLogSpy).toHaveBeenCalledWith(' Safe to delete: 2'); - expect(consoleLogSpy).toHaveBeenCalledWith(' Unsafe to delete: 1'); - expect(consoleLogSpy).toHaveBeenCalledWith(' Skipped (unsafe): 1'); + expect(consoleLogSpy).toHaveBeenCalledWith("Found 3 local branches"); + expect(consoleLogSpy).toHaveBeenCalledWith("Found 1 merged pull requests"); + expect(consoleLogSpy).toHaveBeenCalledWith(" Safe to delete: 2"); + expect(consoleLogSpy).toHaveBeenCalledWith(" Unsafe to delete: 1"); + expect(consoleLogSpy).toHaveBeenCalledWith(" Skipped (unsafe): 1"); }); - it('should only process merged PRs', async () => { - const branches = [createLocalBranch('feature-1', 'abc123')]; - const mergedPR = createPullRequest(1, 'feature-1', 'abc123', 'merge-sha-1'); - const closedPR = createPullRequest(2, 'feature-2', 'def456'); // No merge commit SHA - + it("should only process merged PRs", async () => { + const branches = [createLocalBranch("feature-1", "abc123")]; + const mergedPR = createPullRequest(1, "feature-1", "abc123", "merge-sha-1"); + const closedPR = createPullRequest(2, "feature-2", "def456"); // No merge commit SHA + mockedGetLocalBranches.mockReturnValue(branches); - mockedGetCurrentBranch.mockReturnValue('main'); - mockedGetGitRemote.mockReturnValue({ owner: 'owner', repo: 'repo', host: 'github.com' }); + mockedGetCurrentBranch.mockReturnValue("main"); + mockedGetGitRemote.mockReturnValue({ owner: "owner", repo: "repo", host: "github.com" }); mockedFilterSafeBranches.mockReturnValue([ - { branch: branches[0], safetyCheck: { safe: true }, matchingPR: mergedPR } + { branch: branches[0], safetyCheck: { safe: true }, matchingPR: mergedPR }, ]); // Mock async generator for getPullRequests - includes both merged and closed PRs - const asyncGenerator = (async function* () { + const asyncGenerator = (async function*() { yield mergedPR; yield closedPR; })(); (mockOctokitPlus.getPullRequests as any).mockImplementation(() => asyncGenerator); - await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: 'ghouls' }); + await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: "ghouls" }); // Should only count the merged PR - expect(consoleLogSpy).toHaveBeenCalledWith('Found 1 merged pull requests'); + expect(consoleLogSpy).toHaveBeenCalledWith("Found 1 merged pull requests"); }); - it('should use progress bar when TTY is available', async () => { + it("should use progress bar when TTY is available", async () => { // Mock TTY as true - Object.defineProperty(process.stderr, 'isTTY', { + Object.defineProperty(process.stderr, "isTTY", { value: true, - configurable: true + configurable: true, }); - const branches = [createLocalBranch('feature-1', 'abc123')]; - const pr1 = createPullRequest(1, 'feature-1', 'abc123', 'merge-sha-1'); - + const branches = [createLocalBranch("feature-1", "abc123")]; + const pr1 = createPullRequest(1, "feature-1", "abc123", "merge-sha-1"); + mockedGetLocalBranches.mockReturnValue(branches); - mockedGetCurrentBranch.mockReturnValue('main'); - mockedGetGitRemote.mockReturnValue({ owner: 'owner', repo: 'repo', host: 'github.com' }); + mockedGetCurrentBranch.mockReturnValue("main"); + mockedGetGitRemote.mockReturnValue({ owner: "owner", repo: "repo", host: "github.com" }); mockedFilterSafeBranches.mockReturnValue([ - { branch: branches[0], safetyCheck: { safe: true }, matchingPR: pr1 } + { branch: branches[0], safetyCheck: { safe: true }, matchingPR: pr1 }, ]); // Mock async generator for getPullRequests - const asyncGenerator = (async function* () { + const asyncGenerator = (async function*() { yield pr1; })(); (mockOctokitPlus.getPullRequests as any).mockImplementation(() => asyncGenerator); - await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: 'ghouls' }); + await pruneLocalBranchesCommand.handler!({ dryRun: false, _: [], $0: "ghouls" }); // When TTY is available, the code uses a progress bar // Verify the regular console.log calls still happen (for non-progress messages) - expect(consoleLogSpy).toHaveBeenCalledWith('\nScanning for local branches that can be safely deleted...'); - expect(consoleLogSpy).toHaveBeenCalledWith('Found 1 local branches'); - expect(consoleLogSpy).toHaveBeenCalledWith('\nDeleting 1 branch:'); - expect(consoleLogSpy).toHaveBeenCalledWith(' Successfully deleted: 1 branch'); + expect(consoleLogSpy).toHaveBeenCalledWith("\nScanning for local branches that can be safely deleted..."); + expect(consoleLogSpy).toHaveBeenCalledWith("Found 1 local branches"); + expect(consoleLogSpy).toHaveBeenCalledWith("\nDeleting 1 branch:"); + expect(consoleLogSpy).toHaveBeenCalledWith(" Successfully deleted: 1 branch"); }); }); }); - diff --git a/src/commands/PruneLocalBranches.ts b/src/commands/PruneLocalBranches.ts index 1c32b0f..a8bbb4f 100644 --- a/src/commands/PruneLocalBranches.ts +++ b/src/commands/PruneLocalBranches.ts @@ -1,16 +1,11 @@ +import inquirer from "inquirer"; +import ProgressBar from "progress"; import type { CommandModule } from "yargs"; +import { OctokitPlus, PullRequest } from "../OctokitPlus.js"; +import { filterSafeBranches } from "../utils/branchSafetyChecks.js"; import { createOctokitPlus } from "../utils/createOctokitPlus.js"; -import ProgressBar from "progress"; -import { PullRequest, OctokitPlus } from "../OctokitPlus.js"; import { getGitRemote } from "../utils/getGitRemote.js"; -import { - getLocalBranches, - getCurrentBranch, - deleteLocalBranch, - isGitRepository -} from "../utils/localGitOperations.js"; -import { filterSafeBranches } from "../utils/branchSafetyChecks.js"; -import inquirer from "inquirer"; +import { deleteLocalBranch, getCurrentBranch, getLocalBranches, isGitRepository } from "../utils/localGitOperations.js"; export const pruneLocalBranchesCommand: CommandModule = { handler: async (args: any) => { @@ -30,7 +25,9 @@ export const pruneLocalBranchesCommand: CommandModule = { // Try to get from git remote const gitRemote = getGitRemote(); if (!gitRemote) { - throw new Error("No repo specified and unable to detect from git remote. Please run from a git repository or specify owner/repo."); + throw new Error( + "No repo specified and unable to detect from git remote. Please run from a git repository or specify owner/repo.", + ); } owner = gitRemote.owner; repo = gitRemote.repo; @@ -41,7 +38,7 @@ export const pruneLocalBranchesCommand: CommandModule = { args.dryRun, args.force, owner, - repo + repo, ); await pruneLocalBranches.perform(); @@ -53,11 +50,11 @@ export const pruneLocalBranchesCommand: CommandModule = { .env() .option("dry-run", { type: "boolean", - description: "Perform a dry run (show what would be deleted)" + description: "Perform a dry run (show what would be deleted)", }) .option("force", { type: "boolean", - description: "Skip interactive mode and delete all safe branches automatically" + description: "Skip interactive mode and delete all safe branches automatically", }) .positional("repo", { type: "string", @@ -65,28 +62,32 @@ export const pruneLocalBranchesCommand: CommandModule = { if (!s) { return undefined; } - + // Validate repo string format (owner/repo) const parts = s.split("/"); if (parts.length !== 2 || !parts[0] || !parts[1]) { throw new Error("Repository must be in the format 'owner/repo'"); } - + // Validate owner and repo names (GitHub naming rules) const ownerRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/; const repoRegex = /^[a-zA-Z0-9._-]+$/; - + if (!ownerRegex.test(parts[0])) { - throw new Error("Invalid owner name. Must contain only alphanumeric characters and hyphens, and cannot start or end with a hyphen."); + throw new Error( + "Invalid owner name. Must contain only alphanumeric characters and hyphens, and cannot start or end with a hyphen.", + ); } - + if (!repoRegex.test(parts[1])) { - throw new Error("Invalid repository name. Must contain only alphanumeric characters, dots, underscores, and hyphens."); + throw new Error( + "Invalid repository name. Must contain only alphanumeric characters, dots, underscores, and hyphens.", + ); } - + return { owner: parts[0], repo: parts[1] }; - } - }) + }, + }), }; class PruneLocalBranches { @@ -95,18 +96,18 @@ class PruneLocalBranches { private dryRun: boolean, private force: boolean, private owner: string, - private repo: string + private repo: string, ) {} public async perform() { console.log(`\nScanning for local branches that can be safely deleted...`); - + // Get all local branches const localBranches = getLocalBranches(); const currentBranch = getCurrentBranch(); - + console.log(`Found ${localBranches.length} local branches`); - + if (localBranches.length === 0) { console.log("No local branches found."); return; @@ -141,27 +142,27 @@ class PruneLocalBranches { // Get branches to delete based on mode let branchesToDelete = safeBranches; - + if (!this.force && !this.dryRun) { // Interactive mode const choices = safeBranches.map(({ branch, matchingPR }) => { - const prInfo = matchingPR ? `PR #${matchingPR.number}` : 'no PR'; - const lastCommit = branch.lastCommitDate ? new Date(branch.lastCommitDate).toLocaleDateString() : 'unknown'; + const prInfo = matchingPR ? `PR #${matchingPR.number}` : "no PR"; + const lastCommit = branch.lastCommitDate ? new Date(branch.lastCommitDate).toLocaleDateString() : "unknown"; return { name: `${branch.name} (${prInfo}, last commit: ${lastCommit})`, value: branch.name, - checked: true + checked: true, }; }); const { selectedBranches } = await inquirer.prompt([ { - type: 'checkbox', - name: 'selectedBranches', - message: 'Select branches to delete:', + type: "checkbox", + name: "selectedBranches", + message: "Select branches to delete:", choices, - pageSize: 20 - } + pageSize: 20, + }, ]); if (selectedBranches.length === 0) { @@ -169,23 +170,25 @@ class PruneLocalBranches { return; } - branchesToDelete = safeBranches.filter(({ branch }) => - selectedBranches.includes(branch.name) - ); + branchesToDelete = safeBranches.filter(({ branch }) => selectedBranches.includes(branch.name)); } // Show what will be deleted - console.log(`\n${this.dryRun ? 'Would delete' : 'Deleting'} ${branchesToDelete.length} branch${branchesToDelete.length === 1 ? '' : 'es'}:`); - + console.log( + `\n${this.dryRun ? "Would delete" : "Deleting"} ${branchesToDelete.length} branch${ + branchesToDelete.length === 1 ? "" : "es" + }:`, + ); + // Use progress bar only if we have a TTY, otherwise use simple logging const isTTY = process.stderr.isTTY; let bar: ProgressBar | null = null; - + if (isTTY) { bar = new ProgressBar(":bar :branch (:current/:total)", { total: branchesToDelete.length, width: 30, - stream: process.stderr + stream: process.stderr, }); } @@ -193,8 +196,8 @@ class PruneLocalBranches { let errorCount = 0; for (const { branch, matchingPR } of branchesToDelete) { - const prInfo = matchingPR ? `#${matchingPR.number}` : 'no PR'; - + const prInfo = matchingPR ? `#${matchingPR.number}` : "no PR"; + if (bar) { bar.update(deletedCount + errorCount, { branch: `${branch.name} (${prInfo})` }); } @@ -236,15 +239,15 @@ class PruneLocalBranches { // Summary console.log(`\nSummary:`); if (this.dryRun) { - console.log(` Would delete: ${deletedCount} branch${deletedCount === 1 ? '' : 'es'}`); + console.log(` Would delete: ${deletedCount} branch${deletedCount === 1 ? "" : "es"}`); } else { - console.log(` Successfully deleted: ${deletedCount} branch${deletedCount === 1 ? '' : 'es'}`); + console.log(` Successfully deleted: ${deletedCount} branch${deletedCount === 1 ? "" : "es"}`); } - + if (errorCount > 0) { console.log(` Errors: ${errorCount}`); } - + console.log(` Skipped (unsafe): ${unsafeBranches.length}`); } @@ -257,7 +260,7 @@ class PruneLocalBranches { per_page: 100, state: "closed", sort: "updated", - direction: "desc" + direction: "desc", }); for await (const pr of pullRequests) { @@ -269,4 +272,4 @@ class PruneLocalBranches { return mergedPRs; } -} \ No newline at end of file +} diff --git a/src/commands/PrunePullRequests.ts b/src/commands/PrunePullRequests.ts index becc394..b2fe671 100644 --- a/src/commands/PrunePullRequests.ts +++ b/src/commands/PrunePullRequests.ts @@ -1,10 +1,10 @@ +import inquirer from "inquirer"; +import ProgressBar from "progress"; import type { CommandModule } from "yargs"; +import { OctokitPlus, PullRequest } from "../OctokitPlus.js"; import { createOctokitPlus } from "../utils/createOctokitPlus.js"; -import ProgressBar from "progress"; -import { PullRequest, OctokitPlus } from "../OctokitPlus.js"; -import { ownerAndRepoMatch } from "../utils/ownerAndRepoMatch.js"; import { getGitRemote } from "../utils/getGitRemote.js"; -import inquirer from "inquirer"; +import { ownerAndRepoMatch } from "../utils/ownerAndRepoMatch.js"; export const prunePullRequestsCommand: CommandModule = { handler: async (args: any) => { @@ -19,7 +19,9 @@ export const prunePullRequestsCommand: CommandModule = { // Try to get from git remote const gitRemote = getGitRemote(); if (!gitRemote) { - throw new Error("No repo specified and unable to detect from git remote. Please run from a git repository or specify owner/repo."); + throw new Error( + "No repo specified and unable to detect from git remote. Please run from a git repository or specify owner/repo.", + ); } owner = gitRemote.owner; repo = gitRemote.repo; @@ -30,7 +32,7 @@ export const prunePullRequestsCommand: CommandModule = { args.dryRun, args.force, owner, - repo + repo, ); await prunePullRequest.perform(); @@ -42,11 +44,11 @@ export const prunePullRequestsCommand: CommandModule = { .env() .option("dry-run", { type: "boolean", - description: "Perform a dry run (show what would be deleted)" + description: "Perform a dry run (show what would be deleted)", }) .option("force", { type: "boolean", - description: "Skip interactive mode and delete all merged branches automatically" + description: "Skip interactive mode and delete all merged branches automatically", }) .positional("repo", { type: "string", @@ -54,28 +56,32 @@ export const prunePullRequestsCommand: CommandModule = { if (!s) { return undefined; } - + // Validate repo string format (owner/repo) const parts = s.split("/"); if (parts.length !== 2 || !parts[0] || !parts[1]) { throw new Error("Repository must be in the format 'owner/repo'"); } - + // Validate owner and repo names (GitHub naming rules) const ownerRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/; const repoRegex = /^[a-zA-Z0-9._-]+$/; - + if (!ownerRegex.test(parts[0])) { - throw new Error("Invalid owner name. Must contain only alphanumeric characters and hyphens, and cannot start or end with a hyphen."); + throw new Error( + "Invalid owner name. Must contain only alphanumeric characters and hyphens, and cannot start or end with a hyphen.", + ); } - + if (!repoRegex.test(parts[1])) { - throw new Error("Invalid repository name. Must contain only alphanumeric characters, dots, underscores, and hyphens."); + throw new Error( + "Invalid repository name. Must contain only alphanumeric characters, dots, underscores, and hyphens.", + ); } - + return { owner: parts[0], repo: parts[1] }; - } - }) + }, + }), }; interface BranchToDelete { @@ -89,15 +95,15 @@ class PrunePullRequest { private dryRun: boolean, private force: boolean, private owner: string, - private repo: string + private repo: string, ) {} public async perform() { console.log("\nScanning for remote branches that can be safely deleted..."); - + // First collect all branches that can be deleted const branchesToDelete = await this.collectDeletableBranches(); - + if (branchesToDelete.length === 0) { console.log("\nNo branches found that can be safely deleted."); return; @@ -107,26 +113,26 @@ class PrunePullRequest { // Get branches to delete based on mode let selectedBranches = branchesToDelete; - + if (!this.force && !this.dryRun) { // Interactive mode const choices = branchesToDelete.map(({ ref, pr }) => { - const mergeDate = pr.merged_at ? new Date(pr.merged_at).toLocaleDateString() : 'unknown'; + const mergeDate = pr.merged_at ? new Date(pr.merged_at).toLocaleDateString() : "unknown"; return { - name: `${ref} (PR #${pr.number}: ${pr.title || 'No title'}, merged: ${mergeDate})`, + name: `${ref} (PR #${pr.number}: ${pr.title || "No title"}, merged: ${mergeDate})`, value: ref, - checked: true + checked: true, }; }); const { selected } = await inquirer.prompt([ { - type: 'checkbox', - name: 'selected', - message: 'Select remote branches to delete:', + type: "checkbox", + name: "selected", + message: "Select remote branches to delete:", choices, - pageSize: 20 - } + pageSize: 20, + }, ]); if (selected.length === 0) { @@ -134,17 +140,19 @@ class PrunePullRequest { return; } - selectedBranches = branchesToDelete.filter(({ ref }) => - selected.includes(ref) - ); + selectedBranches = branchesToDelete.filter(({ ref }) => selected.includes(ref)); } // Delete selected branches - console.log(`\n${this.dryRun ? 'Would delete' : 'Deleting'} ${selectedBranches.length} branch${selectedBranches.length === 1 ? '' : 'es'}:`); - + console.log( + `\n${this.dryRun ? "Would delete" : "Deleting"} ${selectedBranches.length} branch${ + selectedBranches.length === 1 ? "" : "es" + }:`, + ); + const bar = new ProgressBar(":bar :branch (:current/:total)", { total: selectedBranches.length, - width: 30 + width: 30, }); let deletedCount = 0; @@ -173,11 +181,11 @@ class PrunePullRequest { // Summary console.log(`\nSummary:`); if (this.dryRun) { - console.log(` Would delete: ${deletedCount} branch${deletedCount === 1 ? '' : 'es'}`); + console.log(` Would delete: ${deletedCount} branch${deletedCount === 1 ? "" : "es"}`); } else { - console.log(` Successfully deleted: ${deletedCount} branch${deletedCount === 1 ? '' : 'es'}`); + console.log(` Successfully deleted: ${deletedCount} branch${deletedCount === 1 ? "" : "es"}`); } - + if (errorCount > 0) { console.log(` Errors: ${errorCount}`); } @@ -185,14 +193,14 @@ class PrunePullRequest { private async collectDeletableBranches(): Promise { const branchesToDelete: BranchToDelete[] = []; - + const pullRequests = this.octokitPlus.getPullRequests({ repo: this.repo, owner: this.owner, per_page: 100, state: "closed", sort: "updated", - direction: "desc" + direction: "desc", }); for await (const pr of pullRequests) { @@ -212,7 +220,7 @@ class PrunePullRequest { branchesToDelete.push({ ref: `heads/${pr.head.ref}`, - pr + pr, }); } diff --git a/src/test/setup.ts b/src/test/setup.ts index 6314235..0495956 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1,30 +1,32 @@ -import { expect } from 'vitest'; +import { expect } from "vitest"; // Global test setup and utilities /** * Helper to create a mock execa result with default values */ -export function createMockExecaResult(overrides: Partial<{ - stdout: string; - stderr: string; - exitCode: number; - failed: boolean; - timedOut: boolean; - command: string; - killed: boolean; -}>) { +export function createMockExecaResult( + overrides: Partial<{ + stdout: string; + stderr: string; + exitCode: number; + failed: boolean; + timedOut: boolean; + command: string; + killed: boolean; + }>, +) { return { - stdout: '', - stderr: '', + stdout: "", + stderr: "", exitCode: 0, - command: overrides.command || 'mock-command', - escapedCommand: overrides.command || 'mock-command', + command: overrides.command || "mock-command", + escapedCommand: overrides.command || "mock-command", failed: false, timedOut: false, isCanceled: false, killed: false, - ...overrides + ...overrides, } as any; } @@ -37,8 +39,8 @@ export function expectGhCliTimeout(mockFn: any, timeout: number) { expect.anything(), expect.objectContaining({ timeout, - reject: false - }) + reject: false, + }), ); } @@ -51,7 +53,7 @@ export function expectGitTimeout(mockFn: any, timeout: number) { expect.anything(), expect.objectContaining({ timeout, - reject: false - }) + reject: false, + }), ); -} \ No newline at end of file +} diff --git a/src/types/yargs.d.ts b/src/types/yargs.d.ts index efb7a9e..fb9e86b 100644 --- a/src/types/yargs.d.ts +++ b/src/types/yargs.d.ts @@ -1,4 +1,4 @@ -declare module 'yargs' { +declare module "yargs" { export interface CommandModule { command?: string | string[]; describe?: string | false; @@ -19,6 +19,6 @@ declare module 'yargs' { export default yargs; } -declare module 'yargs/helpers' { +declare module "yargs/helpers" { export function hideBin(argv: string[]): string[]; -} \ No newline at end of file +} diff --git a/src/utils/branchSafetyChecks.test.ts b/src/utils/branchSafetyChecks.test.ts index 2e416da..83d5dcb 100644 --- a/src/utils/branchSafetyChecks.test.ts +++ b/src/utils/branchSafetyChecks.test.ts @@ -1,17 +1,14 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { - isBranchSafeToDelete, - filterSafeBranches -} from './branchSafetyChecks.js'; -import { getBranchStatus } from './localGitOperations.js'; -import type { LocalBranch } from './localGitOperations.js'; -import type { PullRequest } from '../OctokitPlus.js'; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { PullRequest } from "../OctokitPlus.js"; +import { filterSafeBranches, isBranchSafeToDelete } from "./branchSafetyChecks.js"; +import type { LocalBranch } from "./localGitOperations.js"; +import { getBranchStatus } from "./localGitOperations.js"; // Mock localGitOperations -vi.mock('../../src/utils/localGitOperations.js'); +vi.mock("../../src/utils/localGitOperations.js"); const mockedGetBranchStatus = vi.mocked(getBranchStatus); -describe('branchSafetyChecks', () => { +describe("branchSafetyChecks", () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -20,500 +17,574 @@ describe('branchSafetyChecks', () => { vi.restoreAllMocks(); }); - describe('isBranchSafeToDelete', () => { + describe("isBranchSafeToDelete", () => { const createLocalBranch = (name: string, sha: string, isCurrent: boolean = false): LocalBranch => ({ name, sha, - isCurrent + isCurrent, }); const createPullRequest = (headSha: string, mergeCommitSha?: string): PullRequest => ({ id: 123, number: 1, - user: { login: 'user' }, - state: 'closed', + user: { login: "user" }, + state: "closed", head: { - label: 'user:feature-branch', - ref: 'feature-branch', + label: "user:feature-branch", + ref: "feature-branch", sha: headSha, repo: { - name: 'test-repo', - owner: { login: 'user' }, - fork: false - } + name: "test-repo", + owner: { login: "user" }, + fork: false, + }, }, base: { - label: 'user:main', - ref: 'main', - sha: 'base-sha', + label: "user:main", + ref: "main", + sha: "base-sha", repo: { - name: 'test-repo', - owner: { login: 'user' }, - fork: false - } + name: "test-repo", + owner: { login: "user" }, + fork: false, + }, }, - merge_commit_sha: mergeCommitSha || null + merge_commit_sha: mergeCommitSha || null, }); - describe('current branch checks', () => { - it('should not allow deleting current branch (isCurrent=true)', () => { - const branch = createLocalBranch('main', 'abc123', true); + describe("current branch checks", () => { + it("should not allow deleting current branch (isCurrent=true)", () => { + const branch = createLocalBranch("main", "abc123", true); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'develop'); + const result = isBranchSafeToDelete(branch, "develop"); expect(result).toEqual({ safe: false, - reason: 'current branch' + reason: "current branch", }); }); - it('should not allow deleting branch matching current branch name', () => { - const branch = createLocalBranch('main', 'abc123', false); + it("should not allow deleting branch matching current branch name", () => { + const branch = createLocalBranch("main", "abc123", false); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main'); + const result = isBranchSafeToDelete(branch, "main"); expect(result).toEqual({ safe: false, - reason: 'current branch' + reason: "current branch", }); }); }); - describe('protected branch checks', () => { - const protectedBranches = ['main', 'master', 'develop', 'dev', 'staging', 'production', 'prod']; + describe("protected branch checks", () => { + const protectedBranches = ["main", "master", "develop", "dev", "staging", "production", "prod"]; protectedBranches.forEach(branchName => { it(`should not allow deleting protected branch: ${branchName}`, () => { - const branch = createLocalBranch(branchName, 'abc123'); + const branch = createLocalBranch(branchName, "abc123"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'other-branch'); + const result = isBranchSafeToDelete(branch, "other-branch"); expect(result).toEqual({ safe: false, - reason: 'protected branch' + reason: "protected branch", }); }); it(`should not allow deleting protected branch with different case: ${branchName.toUpperCase()}`, () => { - const branch = createLocalBranch(branchName.toUpperCase(), 'abc123'); + const branch = createLocalBranch(branchName.toUpperCase(), "abc123"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'other-branch'); + const result = isBranchSafeToDelete(branch, "other-branch"); expect(result).toEqual({ safe: false, - reason: 'protected branch' + reason: "protected branch", }); }); }); - it('should allow deleting non-protected branches', () => { - const branch = createLocalBranch('feature/test', 'abc123'); + it("should allow deleting non-protected branches", () => { + const branch = createLocalBranch("feature/test", "abc123"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main'); + const result = isBranchSafeToDelete(branch, "main"); expect(result).toEqual({ safe: true }); }); }); - describe('release and hotfix branch checks', () => { + describe("release and hotfix branch checks", () => { const releaseBranches = [ - 'release/v1.0.0', - 'release/1.0', - 'release/v2.1.3', - 'release/2024.1', - 'RELEASE/V1.0.0', // Test case insensitive - 'release-v1.0.0', - 'release-1.0', - 'release-v2.1.3', - 'release-2024.1', - 'RELEASE-V1.0.0', // Test case insensitive - 'hotfix/urgent-bug', - 'hotfix/v1.0.1', - 'hotfix/security-patch', - 'HOTFIX/URGENT-BUG' // Test case insensitive + "release/v1.0.0", + "release/1.0", + "release/v2.1.3", + "release/2024.1", + "RELEASE/V1.0.0", // Test case insensitive + "release-v1.0.0", + "release-1.0", + "release-v2.1.3", + "release-2024.1", + "RELEASE-V1.0.0", // Test case insensitive + "hotfix/urgent-bug", + "hotfix/v1.0.1", + "hotfix/security-patch", + "HOTFIX/URGENT-BUG", // Test case insensitive ]; releaseBranches.forEach(branchName => { it(`should not allow deleting release/hotfix branch: ${branchName}`, () => { - const branch = createLocalBranch(branchName, 'abc123'); + const branch = createLocalBranch(branchName, "abc123"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main'); + const result = isBranchSafeToDelete(branch, "main"); expect(result).toEqual({ safe: false, - reason: 'release/hotfix branch' + reason: "release/hotfix branch", }); }); }); const nonReleaseBranches = [ - 'feature/release-notes', // Contains "release" but not a release branch - 'bugfix/hotfix-issue', // Contains "hotfix" but not a hotfix branch - 'release', // Just "release" without separator - 'hotfix', // Just "hotfix" without separator - 'releases/v1.0.0', // Plural "releases" - 'hotfixes/v1.0.1', // Plural "hotfixes" - 'pre-release/v1.0.0', // Has prefix before "release" - 'my-hotfix/urgent' // Has prefix before "hotfix" + "feature/release-notes", // Contains "release" but not a release branch + "bugfix/hotfix-issue", // Contains "hotfix" but not a hotfix branch + "release", // Just "release" without separator + "hotfix", // Just "hotfix" without separator + "releases/v1.0.0", // Plural "releases" + "hotfixes/v1.0.1", // Plural "hotfixes" + "pre-release/v1.0.0", // Has prefix before "release" + "my-hotfix/urgent", // Has prefix before "hotfix" ]; nonReleaseBranches.forEach(branchName => { it(`should allow deleting non-release branch: ${branchName}`, () => { - const branch = createLocalBranch(branchName, 'abc123'); + const branch = createLocalBranch(branchName, "abc123"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main'); + const result = isBranchSafeToDelete(branch, "main"); expect(result).toEqual({ safe: true }); }); }); }); - describe('PR SHA matching checks', () => { - it('should not allow deleting when PR head SHA does not match branch SHA', () => { - const branch = createLocalBranch('feature-branch', 'abc123'); - const pr = createPullRequest('different-sha', 'merge-sha'); + describe("release and hotfix branch checks", () => { + const releaseBranches = [ + "release/v1.0.0", + "release/1.0", + "release/v2.1.3", + "release/2024.1", + "RELEASE/V1.0.0", // Test case insensitive + "release-v1.0.0", + "release-1.0", + "release-v2.1.3", + "release-2024.1", + "RELEASE-V1.0.0", // Test case insensitive + "hotfix/urgent-bug", + "hotfix/v1.0.1", + "hotfix/security-patch", + "HOTFIX/URGENT-BUG", // Test case insensitive + ]; + + releaseBranches.forEach(branchName => { + it(`should not allow deleting release/hotfix branch: ${branchName}`, () => { + const branch = createLocalBranch(branchName, "abc123"); + mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); + + const result = isBranchSafeToDelete(branch, "main"); + + expect(result).toEqual({ + safe: false, + reason: "release/hotfix branch", + }); + }); + }); + + const nonReleaseBranches = [ + "feature/release-notes", // Contains "release" but not a release branch + "bugfix/hotfix-issue", // Contains "hotfix" but not a hotfix branch + "release", // Just "release" without separator + "hotfix", // Just "hotfix" without separator + "releases/v1.0.0", // Plural "releases" + "hotfixes/v1.0.1", // Plural "hotfixes" + "pre-release/v1.0.0", // Has prefix before "release" + "my-hotfix/urgent", // Has prefix before "hotfix" + ]; + + nonReleaseBranches.forEach(branchName => { + it(`should allow deleting non-release branch: ${branchName}`, () => { + const branch = createLocalBranch(branchName, "abc123"); + mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); + + const result = isBranchSafeToDelete(branch, "main"); + + expect(result).toEqual({ safe: true }); + }); + }); + }); + + describe("PR SHA matching checks", () => { + it("should not allow deleting when PR head SHA does not match branch SHA", () => { + const branch = createLocalBranch("feature-branch", "abc123"); + const pr = createPullRequest("different-sha", "merge-sha"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main', pr); + const result = isBranchSafeToDelete(branch, "main", pr); expect(result).toEqual({ safe: false, - reason: 'SHA mismatch with PR head' + reason: "SHA mismatch with PR head", }); }); - it('should not allow deleting when PR was not merged', () => { - const branch = createLocalBranch('feature-branch', 'abc123'); - const pr = createPullRequest('abc123'); // No merge commit SHA + it("should not allow deleting when PR was not merged", () => { + const branch = createLocalBranch("feature-branch", "abc123"); + const pr = createPullRequest("abc123"); // No merge commit SHA mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main', pr); + const result = isBranchSafeToDelete(branch, "main", pr); expect(result).toEqual({ safe: false, - reason: 'PR was not merged' + reason: "PR was not merged", }); }); - it('should allow deleting when PR head SHA matches and PR was merged', () => { - const branch = createLocalBranch('feature-branch', 'abc123'); - const pr = createPullRequest('abc123', 'merge-sha'); + it("should allow deleting when PR head SHA matches and PR was merged", () => { + const branch = createLocalBranch("feature-branch", "abc123"); + const pr = createPullRequest("abc123", "merge-sha"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main', pr); + const result = isBranchSafeToDelete(branch, "main", pr); expect(result).toEqual({ safe: true }); }); }); - describe('unpushed commits checks', () => { - it('should not allow deleting when branch has unpushed commits', () => { - const branch = createLocalBranch('feature-branch', 'abc123'); + describe("unpushed commits checks", () => { + it("should not allow deleting when branch has unpushed commits", () => { + const branch = createLocalBranch("feature-branch", "abc123"); mockedGetBranchStatus.mockReturnValue({ ahead: 2, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main'); + const result = isBranchSafeToDelete(branch, "main"); expect(result).toEqual({ safe: false, - reason: '2 unpushed commits' + reason: "2 unpushed commits", }); }); - it('should handle singular unpushed commit message', () => { - const branch = createLocalBranch('feature-branch', 'abc123'); + it("should handle singular unpushed commit message", () => { + const branch = createLocalBranch("feature-branch", "abc123"); mockedGetBranchStatus.mockReturnValue({ ahead: 1, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main'); + const result = isBranchSafeToDelete(branch, "main"); expect(result).toEqual({ safe: false, - reason: '1 unpushed commit' + reason: "1 unpushed commit", }); }); - it('should allow deleting when no unpushed commits', () => { - const branch = createLocalBranch('feature-branch', 'abc123'); + it("should allow deleting when no unpushed commits", () => { + const branch = createLocalBranch("feature-branch", "abc123"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 2 }); - const result = isBranchSafeToDelete(branch, 'main'); + const result = isBranchSafeToDelete(branch, "main"); expect(result).toEqual({ safe: true }); }); - it('should allow deleting when branch status is null', () => { - const branch = createLocalBranch('feature-branch', 'abc123'); + it("should allow deleting when branch status is null", () => { + const branch = createLocalBranch("feature-branch", "abc123"); mockedGetBranchStatus.mockReturnValue(null); - const result = isBranchSafeToDelete(branch, 'main'); + const result = isBranchSafeToDelete(branch, "main"); expect(result).toEqual({ safe: true }); }); }); - describe('combined scenarios', () => { - it('should prioritize current branch check over other checks', () => { - const branch = createLocalBranch('main', 'abc123', true); - const pr = createPullRequest('abc123', 'merge-sha'); + describe("combined scenarios", () => { + it("should prioritize current branch check over other checks", () => { + const branch = createLocalBranch("main", "abc123", true); + const pr = createPullRequest("abc123", "merge-sha"); mockedGetBranchStatus.mockReturnValue({ ahead: 5, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'develop', pr); + const result = isBranchSafeToDelete(branch, "develop", pr); expect(result).toEqual({ safe: false, - reason: 'current branch' + reason: "current branch", }); }); - it('should prioritize protected branch check over PR checks', () => { - const branch = createLocalBranch('main', 'abc123'); - const pr = createPullRequest('abc123', 'merge-sha'); + it("should prioritize protected branch check over PR checks", () => { + const branch = createLocalBranch("main", "abc123"); + const pr = createPullRequest("abc123", "merge-sha"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'develop', pr); + const result = isBranchSafeToDelete(branch, "develop", pr); expect(result).toEqual({ safe: false, - reason: 'protected branch' + reason: "protected branch", }); }); - it('should prioritize release/hotfix branch check over PR checks', () => { - const branch = createLocalBranch('release/v1.0.0', 'abc123'); - const pr = createPullRequest('abc123', 'merge-sha'); + it("should prioritize release/hotfix branch check over PR checks", () => { + const branch = createLocalBranch("release/v1.0.0", "abc123"); + const pr = createPullRequest("abc123", "merge-sha"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main', pr); + const result = isBranchSafeToDelete(branch, "main", pr); expect(result).toEqual({ safe: false, - reason: 'release/hotfix branch' + reason: "release/hotfix branch", }); }); - it('should check PR SHA before unpushed commits', () => { - const branch = createLocalBranch('feature-branch', 'abc123'); - const pr = createPullRequest('different-sha', 'merge-sha'); + it("should prioritize release/hotfix branch check over PR checks", () => { + const branch = createLocalBranch("release/v1.0.0", "abc123"); + const pr = createPullRequest("abc123", "merge-sha"); + mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); + + const result = isBranchSafeToDelete(branch, "main", pr); + + expect(result).toEqual({ + safe: false, + reason: "release/hotfix branch", + }); + }); + + it("should check PR SHA before unpushed commits", () => { + const branch = createLocalBranch("feature-branch", "abc123"); + const pr = createPullRequest("different-sha", "merge-sha"); mockedGetBranchStatus.mockReturnValue({ ahead: 2, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main', pr); + const result = isBranchSafeToDelete(branch, "main", pr); expect(result).toEqual({ safe: false, - reason: 'SHA mismatch with PR head' + reason: "SHA mismatch with PR head", }); }); - it('should check merged status before unpushed commits', () => { - const branch = createLocalBranch('feature-branch', 'abc123'); - const pr = createPullRequest('abc123'); // Not merged + it("should check merged status before unpushed commits", () => { + const branch = createLocalBranch("feature-branch", "abc123"); + const pr = createPullRequest("abc123"); // Not merged mockedGetBranchStatus.mockReturnValue({ ahead: 2, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main', pr); + const result = isBranchSafeToDelete(branch, "main", pr); expect(result).toEqual({ safe: false, - reason: 'PR was not merged' + reason: "PR was not merged", }); }); - it('should allow deletion when all checks pass', () => { - const branch = createLocalBranch('feature-branch', 'abc123'); - const pr = createPullRequest('abc123', 'merge-sha'); + it("should allow deletion when all checks pass", () => { + const branch = createLocalBranch("feature-branch", "abc123"); + const pr = createPullRequest("abc123", "merge-sha"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main', pr); + const result = isBranchSafeToDelete(branch, "main", pr); expect(result).toEqual({ safe: true }); }); - it('should allow deletion without PR when all other checks pass', () => { - const branch = createLocalBranch('feature-branch', 'abc123'); + it("should allow deletion without PR when all other checks pass", () => { + const branch = createLocalBranch("feature-branch", "abc123"); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = isBranchSafeToDelete(branch, 'main'); + const result = isBranchSafeToDelete(branch, "main"); expect(result).toEqual({ safe: true }); }); }); }); - describe('filterSafeBranches', () => { + describe("filterSafeBranches", () => { const createLocalBranch = (name: string, sha: string, isCurrent: boolean = false): LocalBranch => ({ name, sha, - isCurrent + isCurrent, }); const createPullRequest = (headRef: string, headSha: string, mergeCommitSha?: string): PullRequest => ({ id: 123, number: 1, - user: { login: 'user' }, - state: 'closed', + user: { login: "user" }, + state: "closed", head: { label: `user:${headRef}`, ref: headRef, sha: headSha, repo: { - name: 'test-repo', - owner: { login: 'user' }, - fork: false - } + name: "test-repo", + owner: { login: "user" }, + fork: false, + }, }, base: { - label: 'user:main', - ref: 'main', - sha: 'base-sha', + label: "user:main", + ref: "main", + sha: "base-sha", repo: { - name: 'test-repo', - owner: { login: 'user' }, - fork: false - } + name: "test-repo", + owner: { login: "user" }, + fork: false, + }, }, - merge_commit_sha: mergeCommitSha || null + merge_commit_sha: mergeCommitSha || null, }); - it('should filter branches with safety checks', () => { + it("should filter branches with safety checks", () => { const branches = [ - createLocalBranch('main', 'abc123', true), - createLocalBranch('feature-1', 'def456'), - createLocalBranch('feature-2', 'ghi789') + createLocalBranch("main", "abc123", true), + createLocalBranch("feature-1", "def456"), + createLocalBranch("feature-2", "ghi789"), ]; const mergedPRs = new Map([ - ['feature-1', createPullRequest('feature-1', 'def456', 'merge-sha-1')], - ['feature-2', createPullRequest('feature-2', 'ghi789', 'merge-sha-2')] + ["feature-1", createPullRequest("feature-1", "def456", "merge-sha-1")], + ["feature-2", createPullRequest("feature-2", "ghi789", "merge-sha-2")], ]); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = filterSafeBranches(branches, 'main', mergedPRs); + const result = filterSafeBranches(branches, "main", mergedPRs); expect(result).toHaveLength(3); - + // Check main branch (unsafe - current) expect(result[0]).toEqual({ branch: branches[0], - safetyCheck: { safe: false, reason: 'current branch' }, - matchingPR: undefined + safetyCheck: { safe: false, reason: "current branch" }, + matchingPR: undefined, }); // Check feature-1 (safe) expect(result[1]).toEqual({ branch: branches[1], safetyCheck: { safe: true }, - matchingPR: mergedPRs.get('feature-1') + matchingPR: mergedPRs.get("feature-1"), }); // Check feature-2 (safe) expect(result[2]).toEqual({ branch: branches[2], safetyCheck: { safe: true }, - matchingPR: mergedPRs.get('feature-2') + matchingPR: mergedPRs.get("feature-2"), }); }); - it('should handle branches without matching PRs', () => { + it("should handle branches without matching PRs", () => { const branches = [ - createLocalBranch('feature-1', 'def456'), - createLocalBranch('feature-2', 'ghi789') + createLocalBranch("feature-1", "def456"), + createLocalBranch("feature-2", "ghi789"), ]; const mergedPRs = new Map([ - ['feature-1', createPullRequest('feature-1', 'def456', 'merge-sha-1')] + ["feature-1", createPullRequest("feature-1", "def456", "merge-sha-1")], ]); mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = filterSafeBranches(branches, 'main', mergedPRs); + const result = filterSafeBranches(branches, "main", mergedPRs); expect(result).toHaveLength(2); - + // Check feature-1 (has PR) expect(result[0]).toEqual({ branch: branches[0], safetyCheck: { safe: true }, - matchingPR: mergedPRs.get('feature-1') + matchingPR: mergedPRs.get("feature-1"), }); // Check feature-2 (no PR) expect(result[1]).toEqual({ branch: branches[1], safetyCheck: { safe: true }, - matchingPR: undefined + matchingPR: undefined, }); }); - it('should handle empty branches array', () => { - const result = filterSafeBranches([], 'main', new Map()); + it("should handle empty branches array", () => { + const result = filterSafeBranches([], "main", new Map()); expect(result).toEqual([]); }); - it('should handle empty merged PRs map', () => { + it("should handle empty merged PRs map", () => { const branches = [ - createLocalBranch('feature-1', 'def456') + createLocalBranch("feature-1", "def456"), ]; mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - const result = filterSafeBranches(branches, 'main'); + const result = filterSafeBranches(branches, "main"); expect(result).toHaveLength(1); expect(result[0]).toEqual({ branch: branches[0], safetyCheck: { safe: true }, - matchingPR: undefined + matchingPR: undefined, }); }); - it('should handle mixed safe and unsafe branches', () => { + it("should handle mixed safe and unsafe branches", () => { const branches = [ - createLocalBranch('main', 'abc123'), - createLocalBranch('develop', 'def456'), - createLocalBranch('release/v1.0.0', 'mno345'), - createLocalBranch('feature-safe', 'ghi789'), - createLocalBranch('feature-unpushed', 'jkl012') + createLocalBranch("main", "abc123"), + createLocalBranch("develop", "def456"), + createLocalBranch("release/v1.0.0", "mno345"), + createLocalBranch("feature-safe", "ghi789"), + createLocalBranch("feature-unpushed", "jkl012"), ]; const mergedPRs = new Map([ - ['feature-safe', createPullRequest('feature-safe', 'ghi789', 'merge-sha')], - ['feature-unpushed', createPullRequest('feature-unpushed', 'jkl012', 'merge-sha')] + ["feature-safe", createPullRequest("feature-safe", "ghi789", "merge-sha")], + ["feature-unpushed", createPullRequest("feature-unpushed", "jkl012", "merge-sha")], ]); mockedGetBranchStatus.mockImplementation((branchName) => { - if (branchName === 'feature-unpushed') { + if (branchName === "feature-unpushed") { return { ahead: 3, behind: 0 }; } return { ahead: 0, behind: 0 }; }); - const result = filterSafeBranches(branches, 'other', mergedPRs); + const result = filterSafeBranches(branches, "other", mergedPRs); expect(result).toHaveLength(5); - + // main - protected expect(result[0].safetyCheck).toEqual({ safe: false, - reason: 'protected branch' + reason: "protected branch", }); // develop - protected expect(result[1].safetyCheck).toEqual({ safe: false, - reason: 'protected branch' + reason: "protected branch", + }); + + // release/v1.0.0 - release branch + expect(result[2].safetyCheck).toEqual({ + safe: false, + reason: "release/hotfix branch", }); // release/v1.0.0 - release branch expect(result[2].safetyCheck).toEqual({ safe: false, - reason: 'release/hotfix branch' + reason: "release/hotfix branch", }); // feature-safe - safe @@ -522,23 +593,23 @@ describe('branchSafetyChecks', () => { // feature-unpushed - has unpushed commits expect(result[4].safetyCheck).toEqual({ safe: false, - reason: '3 unpushed commits' + reason: "3 unpushed commits", }); }); - it('should call getBranchStatus for each branch', () => { + it("should call getBranchStatus for each branch", () => { const branches = [ - createLocalBranch('feature-1', 'def456'), - createLocalBranch('feature-2', 'ghi789') + createLocalBranch("feature-1", "def456"), + createLocalBranch("feature-2", "ghi789"), ]; mockedGetBranchStatus.mockReturnValue({ ahead: 0, behind: 0 }); - filterSafeBranches(branches, 'main', new Map()); + filterSafeBranches(branches, "main", new Map()); expect(mockedGetBranchStatus).toHaveBeenCalledTimes(2); - expect(mockedGetBranchStatus).toHaveBeenCalledWith('feature-1'); - expect(mockedGetBranchStatus).toHaveBeenCalledWith('feature-2'); + expect(mockedGetBranchStatus).toHaveBeenCalledWith("feature-1"); + expect(mockedGetBranchStatus).toHaveBeenCalledWith("feature-2"); }); }); }); diff --git a/src/utils/branchSafetyChecks.ts b/src/utils/branchSafetyChecks.ts index 070ce35..39d969b 100644 --- a/src/utils/branchSafetyChecks.ts +++ b/src/utils/branchSafetyChecks.ts @@ -1,5 +1,5 @@ -import { LocalBranch, getBranchStatus } from "./localGitOperations.js"; import { PullRequest } from "../OctokitPlus.js"; +import { getBranchStatus, LocalBranch } from "./localGitOperations.js"; export interface SafetyCheckResult { safe: boolean; @@ -12,13 +12,13 @@ export interface SafetyCheckResult { export function isBranchSafeToDelete( branch: LocalBranch, currentBranch: string, - matchingPR?: PullRequest + matchingPR?: PullRequest, ): SafetyCheckResult { // Never delete the current branch if (branch.isCurrent || branch.name === currentBranch) { return { safe: false, - reason: "current branch" + reason: "current branch", }; } @@ -27,7 +27,7 @@ export function isBranchSafeToDelete( if (protectedBranches.includes(branch.name.toLowerCase())) { return { safe: false, - reason: "protected branch" + reason: "protected branch", }; } @@ -51,7 +51,7 @@ export function isBranchSafeToDelete( if (branch.sha !== matchingPR.head.sha) { return { safe: false, - reason: "SHA mismatch with PR head" + reason: "SHA mismatch with PR head", }; } @@ -59,7 +59,7 @@ export function isBranchSafeToDelete( if (!matchingPR.merge_commit_sha) { return { safe: false, - reason: "PR was not merged" + reason: "PR was not merged", }; } } @@ -69,7 +69,7 @@ export function isBranchSafeToDelete( if (branchStatus && branchStatus.ahead > 0) { return { safe: false, - reason: `${branchStatus.ahead} unpushed commit${branchStatus.ahead === 1 ? '' : 's'}` + reason: `${branchStatus.ahead} unpushed commit${branchStatus.ahead === 1 ? "" : "s"}`, }; } @@ -82,16 +82,16 @@ export function isBranchSafeToDelete( export function filterSafeBranches( branches: LocalBranch[], currentBranch: string, - mergedPRs: Map = new Map() + mergedPRs: Map = new Map(), ): Array<{ branch: LocalBranch; safetyCheck: SafetyCheckResult; matchingPR?: PullRequest }> { return branches.map(branch => { const matchingPR = mergedPRs.get(branch.name); const safetyCheck = isBranchSafeToDelete(branch, currentBranch, matchingPR); - + return { branch, safetyCheck, - matchingPR + matchingPR, }; }); -} \ No newline at end of file +} diff --git a/src/utils/createOctokitPlus.ts b/src/utils/createOctokitPlus.ts index c139046..21478ba 100644 --- a/src/utils/createOctokitPlus.ts +++ b/src/utils/createOctokitPlus.ts @@ -1,7 +1,7 @@ import { Octokit } from "@octokit/rest"; import { OctokitPlus } from "../OctokitPlus.js"; -import { getGhToken } from "./getGhToken.js"; import { getGhBaseUrl } from "./getGhBaseUrl.js"; +import { getGhToken } from "./getGhToken.js"; import { detectGhCliError, formatGhCliError } from "./ghCliErrorHandler.js"; export function createOctokitPlus() { @@ -35,7 +35,7 @@ export function createOctokitPlus() { const octokit = new Octokit({ baseUrl, - auth: token + auth: token, }); return new OctokitPlus(octokit); diff --git a/src/utils/getGhBaseUrl.test.ts b/src/utils/getGhBaseUrl.test.ts index 3b5f6cf..59ef01a 100644 --- a/src/utils/getGhBaseUrl.test.ts +++ b/src/utils/getGhBaseUrl.test.ts @@ -1,14 +1,14 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { execaSync } from 'execa'; -import { getGhBaseUrl } from './getGhBaseUrl.js'; -import { createMockExecaResult } from '../test/setup.js'; +import { execaSync } from "execa"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createMockExecaResult } from "../test/setup.js"; +import { getGhBaseUrl } from "./getGhBaseUrl.js"; // Mock execa -vi.mock('execa'); +vi.mock("execa"); const mockedExecaSync = vi.mocked(execaSync); -describe('getGhBaseUrl', () => { +describe("getGhBaseUrl", () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -17,30 +17,30 @@ describe('getGhBaseUrl', () => { vi.restoreAllMocks(); }); - it('should return GitHub.com API URL when logged in to github.com', () => { + it("should return GitHub.com API URL when logged in to github.com", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: '✓ Logged in to github.com account awesome-dude', + stdout: "", + stderr: "✓ Logged in to github.com account awesome-dude", exitCode: 0, - command: 'gh auth status' + command: "gh auth status", })); const result = getGhBaseUrl(); - expect(result).toBe('https://api.github.com'); - expect(mockedExecaSync).toHaveBeenCalledWith('gh', ['auth', 'status'], { + expect(result).toBe("https://api.github.com"); + expect(mockedExecaSync).toHaveBeenCalledWith("gh", ["auth", "status"], { timeout: 10000, - reject: false + reject: false, }); }); - it('should return enterprise API URL when logged in to enterprise host', () => { - const enterpriseHost = 'github.enterprise.com'; + it("should return enterprise API URL when logged in to enterprise host", () => { + const enterpriseHost = "github.enterprise.com"; mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', + stdout: "", stderr: `✓ Logged in to ${enterpriseHost} account user`, exitCode: 0, - command: 'gh auth status' + command: "gh auth status", })); const result = getGhBaseUrl(); @@ -48,13 +48,13 @@ describe('getGhBaseUrl', () => { expect(result).toBe(`https://${enterpriseHost}/api/v3`); }); - it('should handle "Active account on" format', () => { - const enterpriseHost = 'github.company.com'; + it("should handle \"Active account on\" format", () => { + const enterpriseHost = "github.company.com"; mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', + stdout: "", stderr: `Active account on ${enterpriseHost} (user123)`, exitCode: 0, - command: 'gh auth status' + command: "gh auth status", })); const result = getGhBaseUrl(); @@ -62,120 +62,120 @@ describe('getGhBaseUrl', () => { expect(result).toBe(`https://${enterpriseHost}/api/v3`); }); - it('should parse host from stdout when stderr is empty', () => { + it("should parse host from stdout when stderr is empty", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '✓ Logged in to github.com account awesome-dude', - stderr: '', + stdout: "✓ Logged in to github.com account awesome-dude", + stderr: "", exitCode: 0, - command: 'gh auth status' + command: "gh auth status", })); const result = getGhBaseUrl(); - expect(result).toBe('https://api.github.com'); + expect(result).toBe("https://api.github.com"); }); - it('should handle multiline output with host on separate line', () => { + it("should handle multiline output with host on separate line", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', + stdout: "", stderr: `github.enterprise.com ✓ Logged in to github.enterprise.com account user`, exitCode: 0, - command: 'gh auth status' + command: "gh auth status", })); const result = getGhBaseUrl(); - expect(result).toBe('https://github.enterprise.com/api/v3'); + expect(result).toBe("https://github.enterprise.com/api/v3"); }); - it('should default to github.com when no host match found', () => { + it("should default to github.com when no host match found", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: 'No auth status found', + stdout: "", + stderr: "No auth status found", exitCode: 1, - command: 'gh auth status', - failed: true + command: "gh auth status", + failed: true, })); const result = getGhBaseUrl(); - expect(result).toBe('https://api.github.com'); + expect(result).toBe("https://api.github.com"); }); - it('should default to github.com when both stdout and stderr are empty', () => { + it("should default to github.com when both stdout and stderr are empty", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: '', + stdout: "", + stderr: "", exitCode: 0, - command: 'gh auth status' + command: "gh auth status", })); const result = getGhBaseUrl(); - expect(result).toBe('https://api.github.com'); + expect(result).toBe("https://api.github.com"); }); - it('should throw when gh command is not found', () => { + it("should throw when gh command is not found", () => { const mockResult = createMockExecaResult({ - stdout: '', - stderr: 'gh: command not found', + stdout: "", + stderr: "gh: command not found", exitCode: 127, - command: 'gh auth status', - failed: true + command: "gh auth status", + failed: true, }); mockedExecaSync.mockReturnValue(mockResult); expect(() => getGhBaseUrl()).toThrow(); }); - it('should throw when execaSync throws an exception', () => { + it("should throw when execaSync throws an exception", () => { mockedExecaSync.mockImplementation(() => { - throw new Error('Command failed'); + throw new Error("Command failed"); }); - expect(() => getGhBaseUrl()).toThrow('Command failed'); + expect(() => getGhBaseUrl()).toThrow("Command failed"); }); - it('should handle timeout correctly', () => { + it("should handle timeout correctly", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: '', + stdout: "", + stderr: "", exitCode: 124, - command: 'gh auth status', + command: "gh auth status", failed: true, - timedOut: true + timedOut: true, })); const result = getGhBaseUrl(); - expect(result).toBe('https://api.github.com'); - expect(mockedExecaSync).toHaveBeenCalledWith('gh', ['auth', 'status'], { + expect(result).toBe("https://api.github.com"); + expect(mockedExecaSync).toHaveBeenCalledWith("gh", ["auth", "status"], { timeout: 10000, - reject: false + reject: false, }); }); - it('should handle case insensitive host matching', () => { + it("should handle case insensitive host matching", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: '✓ LOGGED IN TO github.com account user', + stdout: "", + stderr: "✓ LOGGED IN TO github.com account user", exitCode: 0, - command: 'gh auth status' + command: "gh auth status", })); const result = getGhBaseUrl(); - expect(result).toBe('https://api.github.com'); + expect(result).toBe("https://api.github.com"); }); - it('should handle complex enterprise domain names', () => { - const enterpriseHost = 'git.internal.company-name.co.uk'; + it("should handle complex enterprise domain names", () => { + const enterpriseHost = "git.internal.company-name.co.uk"; mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', + stdout: "", stderr: `✓ Logged in to ${enterpriseHost} account user`, exitCode: 0, - command: 'gh auth status' + command: "gh auth status", })); const result = getGhBaseUrl(); @@ -183,16 +183,16 @@ describe('getGhBaseUrl', () => { expect(result).toBe(`https://${enterpriseHost}/api/v3`); }); - it('should handle output with additional text after host', () => { + it("should handle output with additional text after host", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: '✓ Logged in to github.com account awesome-dude (keyring)', + stdout: "", + stderr: "✓ Logged in to github.com account awesome-dude (keyring)", exitCode: 0, - command: 'gh auth status' + command: "gh auth status", })); const result = getGhBaseUrl(); - expect(result).toBe('https://api.github.com'); + expect(result).toBe("https://api.github.com"); }); -}); \ No newline at end of file +}); diff --git a/src/utils/getGhBaseUrl.ts b/src/utils/getGhBaseUrl.ts index f1cd078..44f99f5 100644 --- a/src/utils/getGhBaseUrl.ts +++ b/src/utils/getGhBaseUrl.ts @@ -3,7 +3,7 @@ import { execaSync } from "execa"; export function getGhBaseUrl(): string { const result = execaSync("gh", ["auth", "status"], { timeout: 10000, // 10 second timeout - reject: false + reject: false, }); // Check if the command failed due to gh not being installed @@ -13,24 +13,24 @@ export function getGhBaseUrl(): string { // gh auth status outputs to stderr, so check both stdout and stderr const hostsOutput = result.stderr || result.stdout || ""; - - // Extract the host from the output (looking for lines like "github.com" or custom enterprise hosts) - // Look for patterns like "✓ Logged in to github.com" or similar - const hostMatch = hostsOutput.match(/(?:Logged in to|Active account on)\s+([^\s\n]+)/i) || - hostsOutput.match(/^\s*([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\s*$/m); - - if (hostMatch && hostMatch[1]) { - const host = hostMatch[1]; - - // If it's github.com, return the API URL - if (host === "github.com") { - return "https://api.github.com"; - } - - // For GitHub Enterprise, construct the API URL - return `https://${host}/api/v3`; + + // Extract the host from the output (looking for lines like "github.com" or custom enterprise hosts) + // Look for patterns like "✓ Logged in to github.com" or similar + const hostMatch = hostsOutput.match(/(?:Logged in to|Active account on)\s+([^\s\n]+)/i) + || hostsOutput.match(/^\s*([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\s*$/m); + + if (hostMatch && hostMatch[1]) { + const host = hostMatch[1]; + + // If it's github.com, return the API URL + if (host === "github.com") { + return "https://api.github.com"; } - - // Default to github.com - return "https://api.github.com"; -} \ No newline at end of file + + // For GitHub Enterprise, construct the API URL + return `https://${host}/api/v3`; + } + + // Default to github.com + return "https://api.github.com"; +} diff --git a/src/utils/getGhToken.test.ts b/src/utils/getGhToken.test.ts index d615f62..2c86fa1 100644 --- a/src/utils/getGhToken.test.ts +++ b/src/utils/getGhToken.test.ts @@ -1,14 +1,14 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { execaSync } from 'execa'; -import { getGhToken } from './getGhToken.js'; -import { createMockExecaResult } from '../test/setup.js'; +import { execaSync } from "execa"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createMockExecaResult } from "../test/setup.js"; +import { getGhToken } from "./getGhToken.js"; // Mock execa -vi.mock('execa'); +vi.mock("execa"); const mockedExecaSync = vi.mocked(execaSync); -describe('getGhToken', () => { +describe("getGhToken", () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -17,31 +17,31 @@ describe('getGhToken', () => { vi.restoreAllMocks(); }); - it('should return token when gh auth token succeeds', () => { - const mockToken = 'ghp_1234567890abcdef'; + it("should return token when gh auth token succeeds", () => { + const mockToken = "ghp_1234567890abcdef"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: mockToken, - stderr: '', + stderr: "", exitCode: 0, - command: 'gh auth token' + command: "gh auth token", })); const result = getGhToken(); expect(result).toBe(mockToken); - expect(mockedExecaSync).toHaveBeenCalledWith('gh', ['auth', 'token'], { + expect(mockedExecaSync).toHaveBeenCalledWith("gh", ["auth", "token"], { timeout: 10000, - reject: false + reject: false, }); }); - it('should return trimmed token when stdout has whitespace', () => { - const mockToken = 'ghp_1234567890abcdef'; + it("should return trimmed token when stdout has whitespace", () => { + const mockToken = "ghp_1234567890abcdef"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: ` ${mockToken} \n`, - stderr: '', + stderr: "", exitCode: 0, - command: 'gh auth token' + command: "gh auth token", })); const result = getGhToken(); @@ -49,12 +49,12 @@ describe('getGhToken', () => { expect(result).toBe(mockToken); }); - it('should return null when stdout is empty', () => { + it("should return null when stdout is empty", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: '', + stdout: "", + stderr: "", exitCode: 0, - command: 'gh auth token' + command: "gh auth token", })); const result = getGhToken(); @@ -62,12 +62,12 @@ describe('getGhToken', () => { expect(result).toBe(null); }); - it('should return null when stdout is only whitespace', () => { + it("should return null when stdout is only whitespace", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: ' \n\t ', - stderr: '', + stdout: " \n\t ", + stderr: "", exitCode: 0, - command: 'gh auth token' + command: "gh auth token", })); const result = getGhToken(); @@ -75,12 +75,12 @@ describe('getGhToken', () => { expect(result).toBe(null); }); - it('should return null when stdout is undefined', () => { + it("should return null when stdout is undefined", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: undefined as any, - stderr: '', + stderr: "", exitCode: 0, - command: 'gh auth token' + command: "gh auth token", })); const result = getGhToken(); @@ -88,55 +88,55 @@ describe('getGhToken', () => { expect(result).toBe(null); }); - it('should throw when gh command fails', () => { + it("should throw when gh command fails", () => { const mockResult = createMockExecaResult({ - stdout: '', - stderr: 'gh: command not found', + stdout: "", + stderr: "gh: command not found", exitCode: 127, - command: 'gh auth token', - failed: true + command: "gh auth token", + failed: true, }); mockedExecaSync.mockReturnValue(mockResult); expect(() => getGhToken()).toThrow(); }); - it('should throw when execaSync throws an exception', () => { + it("should throw when execaSync throws an exception", () => { mockedExecaSync.mockImplementation(() => { - throw new Error('Command failed'); + throw new Error("Command failed"); }); - expect(() => getGhToken()).toThrow('Command failed'); + expect(() => getGhToken()).toThrow("Command failed"); }); - it('should throw when gh is not authenticated', () => { + it("should throw when gh is not authenticated", () => { const mockResult = createMockExecaResult({ - stdout: '', - stderr: 'gh: To get started with GitHub CLI, please run: gh auth login', + stdout: "", + stderr: "gh: To get started with GitHub CLI, please run: gh auth login", exitCode: 1, - command: 'gh auth token', - failed: true + command: "gh auth token", + failed: true, }); mockedExecaSync.mockReturnValue(mockResult); expect(() => getGhToken()).toThrow(); }); - it('should throw when timeout occurs', () => { + it("should throw when timeout occurs", () => { const mockResult = createMockExecaResult({ - stdout: '', - stderr: '', + stdout: "", + stderr: "", exitCode: 124, - command: 'gh auth token', + command: "gh auth token", failed: true, - timedOut: true + timedOut: true, }); mockedExecaSync.mockReturnValue(mockResult); expect(() => getGhToken()).toThrow(); - expect(mockedExecaSync).toHaveBeenCalledWith('gh', ['auth', 'token'], { + expect(mockedExecaSync).toHaveBeenCalledWith("gh", ["auth", "token"], { timeout: 10000, - reject: false + reject: false, }); }); -}); \ No newline at end of file +}); diff --git a/src/utils/getGhToken.ts b/src/utils/getGhToken.ts index 01fffd9..900ef8e 100644 --- a/src/utils/getGhToken.ts +++ b/src/utils/getGhToken.ts @@ -3,7 +3,7 @@ import { execaSync } from "execa"; export function getGhToken(): string | null { const result = execaSync("gh", ["auth", "token"], { timeout: 10000, // 10 second timeout - reject: false + reject: false, }); // Check if the command failed @@ -14,4 +14,4 @@ export function getGhToken(): string | null { const token = result.stdout?.trim(); return token || null; -} \ No newline at end of file +} diff --git a/src/utils/getGhUsername.test.ts b/src/utils/getGhUsername.test.ts index d7331b5..4060fb5 100644 --- a/src/utils/getGhUsername.test.ts +++ b/src/utils/getGhUsername.test.ts @@ -1,14 +1,14 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { execaSync } from 'execa'; -import { getGhUsername } from './getGhUsername.js'; -import { createMockExecaResult } from '../test/setup.js'; +import { execaSync } from "execa"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createMockExecaResult } from "../test/setup.js"; +import { getGhUsername } from "./getGhUsername.js"; // Mock execa -vi.mock('execa'); +vi.mock("execa"); const mockedExecaSync = vi.mocked(execaSync); -describe('getGhUsername', () => { +describe("getGhUsername", () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -17,31 +17,31 @@ describe('getGhUsername', () => { vi.restoreAllMocks(); }); - it('should return username when gh api user succeeds', () => { - const mockUsername = 'awesome-dude'; + it("should return username when gh api user succeeds", () => { + const mockUsername = "awesome-dude"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: mockUsername, - stderr: '', + stderr: "", exitCode: 0, - command: 'gh api user --jq .login' + command: "gh api user --jq .login", })); const result = getGhUsername(); expect(result).toBe(mockUsername); - expect(mockedExecaSync).toHaveBeenCalledWith('gh', ['api', 'user', '--jq', '.login'], { + expect(mockedExecaSync).toHaveBeenCalledWith("gh", ["api", "user", "--jq", ".login"], { timeout: 10000, - reject: false + reject: false, }); }); - it('should return trimmed username when stdout has whitespace', () => { - const mockUsername = 'awesome-dude'; + it("should return trimmed username when stdout has whitespace", () => { + const mockUsername = "awesome-dude"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: ` ${mockUsername} \n`, - stderr: '', + stderr: "", exitCode: 0, - command: 'gh api user --jq .login' + command: "gh api user --jq .login", })); const result = getGhUsername(); @@ -49,12 +49,12 @@ describe('getGhUsername', () => { expect(result).toBe(mockUsername); }); - it('should return null when stdout is empty', () => { + it("should return null when stdout is empty", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: '', + stdout: "", + stderr: "", exitCode: 0, - command: 'gh api user --jq .login' + command: "gh api user --jq .login", })); const result = getGhUsername(); @@ -62,12 +62,12 @@ describe('getGhUsername', () => { expect(result).toBe(null); }); - it('should return null when stdout is only whitespace', () => { + it("should return null when stdout is only whitespace", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: ' \n\t ', - stderr: '', + stdout: " \n\t ", + stderr: "", exitCode: 0, - command: 'gh api user --jq .login' + command: "gh api user --jq .login", })); const result = getGhUsername(); @@ -75,12 +75,12 @@ describe('getGhUsername', () => { expect(result).toBe(null); }); - it('should return null when stdout is undefined', () => { + it("should return null when stdout is undefined", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: undefined as any, - stderr: '', + stderr: "", exitCode: 0, - command: 'gh api user --jq .login' + command: "gh api user --jq .login", })); const result = getGhUsername(); @@ -88,81 +88,81 @@ describe('getGhUsername', () => { expect(result).toBe(null); }); - it('should throw when gh command fails', () => { + it("should throw when gh command fails", () => { const mockResult = createMockExecaResult({ - stdout: '', - stderr: 'gh: command not found', + stdout: "", + stderr: "gh: command not found", exitCode: 127, - command: 'gh api user --jq .login', - failed: true + command: "gh api user --jq .login", + failed: true, }); mockedExecaSync.mockReturnValue(mockResult); expect(() => getGhUsername()).toThrow(); }); - it('should throw when execaSync throws an exception', () => { + it("should throw when execaSync throws an exception", () => { mockedExecaSync.mockImplementation(() => { - throw new Error('Command failed'); + throw new Error("Command failed"); }); - expect(() => getGhUsername()).toThrow('Command failed'); + expect(() => getGhUsername()).toThrow("Command failed"); }); - it('should throw when gh is not authenticated', () => { + it("should throw when gh is not authenticated", () => { const mockResult = createMockExecaResult({ - stdout: '', - stderr: 'gh: To get started with GitHub CLI, please run: gh auth login', + stdout: "", + stderr: "gh: To get started with GitHub CLI, please run: gh auth login", exitCode: 1, - command: 'gh api user --jq .login', - failed: true + command: "gh api user --jq .login", + failed: true, }); mockedExecaSync.mockReturnValue(mockResult); expect(() => getGhUsername()).toThrow(); }); - it('should throw when API request fails', () => { + it("should throw when API request fails", () => { const mockResult = createMockExecaResult({ - stdout: '', - stderr: 'HTTP 401: Unauthorized (https://api.github.com/user)', + stdout: "", + stderr: "HTTP 401: Unauthorized (https://api.github.com/user)", exitCode: 1, - command: 'gh api user --jq .login', - failed: true + command: "gh api user --jq .login", + failed: true, }); mockedExecaSync.mockReturnValue(mockResult); expect(() => getGhUsername()).toThrow(); }); - it('should throw when timeout occurs', () => { + it("should throw when timeout occurs", () => { const mockResult = createMockExecaResult({ - stdout: '', - stderr: '', + stdout: "", + stderr: "", exitCode: 124, - command: 'gh api user --jq .login', + command: "gh api user --jq .login", failed: true, - timedOut: true + timedOut: true, }); mockedExecaSync.mockReturnValue(mockResult); expect(() => getGhUsername()).toThrow(); - expect(mockedExecaSync).toHaveBeenCalledWith('gh', ['api', 'user', '--jq', '.login'], { + expect(mockedExecaSync).toHaveBeenCalledWith("gh", ["api", "user", "--jq", ".login"], { timeout: 10000, - reject: false + reject: false, }); }); - it('should throw when jq parsing errors occur', () => { + it("should throw when jq parsing errors occur", () => { const mockResult = createMockExecaResult({ - stdout: '', - stderr: 'jq: error: Invalid JSON', + stdout: "", + stderr: "jq: error: Invalid JSON", exitCode: 1, - command: 'gh api user --jq .login', - failed: true + command: "gh api user --jq .login", + failed: true, }); mockedExecaSync.mockReturnValue(mockResult); expect(() => getGhUsername()).toThrow(); }); -}); \ No newline at end of file +}); diff --git a/src/utils/getGhUsername.ts b/src/utils/getGhUsername.ts index cee9de3..d3567bd 100644 --- a/src/utils/getGhUsername.ts +++ b/src/utils/getGhUsername.ts @@ -3,7 +3,7 @@ import { execaSync } from "execa"; export function getGhUsername(): string | null { const result = execaSync("gh", ["api", "user", "--jq", ".login"], { timeout: 10000, // 10 second timeout - reject: false + reject: false, }); // Check if the command failed @@ -14,4 +14,4 @@ export function getGhUsername(): string | null { const username = result.stdout?.trim(); return username || null; -} \ No newline at end of file +} diff --git a/src/utils/getGitRemote.test.ts b/src/utils/getGitRemote.test.ts index 8bd66fb..6e0060b 100644 --- a/src/utils/getGitRemote.test.ts +++ b/src/utils/getGitRemote.test.ts @@ -1,14 +1,14 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { execaSync } from 'execa'; -import { getGitRemote } from './getGitRemote.js'; -import { createMockExecaResult } from '../test/setup.js'; +import { execaSync } from "execa"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createMockExecaResult } from "../test/setup.js"; +import { getGitRemote } from "./getGitRemote.js"; // Mock execa -vi.mock('execa'); +vi.mock("execa"); const mockedExecaSync = vi.mocked(execaSync); -describe('getGitRemote', () => { +describe("getGitRemote", () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -17,142 +17,142 @@ describe('getGitRemote', () => { vi.restoreAllMocks(); }); - it('should parse HTTPS GitHub URL correctly', () => { - const httpsUrl = 'https://github.com/awesome-dude/ghouls.git'; + it("should parse HTTPS GitHub URL correctly", () => { + const httpsUrl = "https://github.com/awesome-dude/ghouls.git"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: httpsUrl, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); expect(result).toEqual({ - owner: 'awesome-dude', - repo: 'ghouls', - host: 'github.com' + owner: "awesome-dude", + repo: "ghouls", + host: "github.com", }); - expect(mockedExecaSync).toHaveBeenCalledWith('git', ['remote', 'get-url', 'origin'], { + expect(mockedExecaSync).toHaveBeenCalledWith("git", ["remote", "get-url", "origin"], { timeout: 5000, - reject: false + reject: false, }); }); - it('should parse HTTPS GitHub URL without .git suffix', () => { - const httpsUrl = 'https://github.com/awesome-dude/ghouls'; + it("should parse HTTPS GitHub URL without .git suffix", () => { + const httpsUrl = "https://github.com/awesome-dude/ghouls"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: httpsUrl, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); expect(result).toEqual({ - owner: 'awesome-dude', - repo: 'ghouls', - host: 'github.com' + owner: "awesome-dude", + repo: "ghouls", + host: "github.com", }); }); - it('should parse SSH GitHub URL correctly', () => { - const sshUrl = 'git@github.com:awesome-dude/ghouls.git'; + it("should parse SSH GitHub URL correctly", () => { + const sshUrl = "git@github.com:awesome-dude/ghouls.git"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: sshUrl, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); expect(result).toEqual({ - owner: 'awesome-dude', - repo: 'ghouls', - host: 'github.com' + owner: "awesome-dude", + repo: "ghouls", + host: "github.com", }); }); - it('should parse SSH GitHub URL without .git suffix', () => { - const sshUrl = 'git@github.com:awesome-dude/ghouls'; + it("should parse SSH GitHub URL without .git suffix", () => { + const sshUrl = "git@github.com:awesome-dude/ghouls"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: sshUrl, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); expect(result).toEqual({ - owner: 'awesome-dude', - repo: 'ghouls', - host: 'github.com' + owner: "awesome-dude", + repo: "ghouls", + host: "github.com", }); }); - it('should handle repository names with dashes and underscores', () => { - const httpsUrl = 'https://github.com/some-user/my_awesome-repo.git'; + it("should handle repository names with dashes and underscores", () => { + const httpsUrl = "https://github.com/some-user/my_awesome-repo.git"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: httpsUrl, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); expect(result).toEqual({ - owner: 'some-user', - repo: 'my_awesome-repo', - host: 'github.com' + owner: "some-user", + repo: "my_awesome-repo", + host: "github.com", }); }); - it('should handle organization names with dots', () => { - const httpsUrl = 'https://github.com/some.org/repo.git'; + it("should handle organization names with dots", () => { + const httpsUrl = "https://github.com/some.org/repo.git"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: httpsUrl, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); expect(result).toEqual({ - owner: 'some.org', - repo: 'repo', - host: 'github.com' + owner: "some.org", + repo: "repo", + host: "github.com", }); }); - it('should trim whitespace from remote URL', () => { - const httpsUrl = 'https://github.com/awesome-dude/ghouls.git'; + it("should trim whitespace from remote URL", () => { + const httpsUrl = "https://github.com/awesome-dude/ghouls.git"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: ` ${httpsUrl} \n`, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); expect(result).toEqual({ - owner: 'awesome-dude', - repo: 'ghouls', - host: 'github.com' + owner: "awesome-dude", + repo: "ghouls", + host: "github.com", }); }); - it('should return null when stdout is empty', () => { + it("should return null when stdout is empty", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: '', + stdout: "", + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); @@ -160,12 +160,12 @@ describe('getGitRemote', () => { expect(result).toBe(null); }); - it('should return null when stdout is only whitespace', () => { + it("should return null when stdout is only whitespace", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: ' \n\t ', - stderr: '', + stdout: " \n\t ", + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); @@ -173,12 +173,12 @@ describe('getGitRemote', () => { expect(result).toBe(null); }); - it('should return null when stdout is undefined', () => { + it("should return null when stdout is undefined", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: undefined as any, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); @@ -186,31 +186,31 @@ describe('getGitRemote', () => { expect(result).toBe(null); }); - it('should parse non-GitHub URLs (e.g., GitLab)', () => { - const gitlabUrl = 'https://gitlab.com/user/repo.git'; + it("should parse non-GitHub URLs (e.g., GitLab)", () => { + const gitlabUrl = "https://gitlab.com/user/repo.git"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: gitlabUrl, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); expect(result).toEqual({ - owner: 'user', - repo: 'repo', - host: 'gitlab.com' + owner: "user", + repo: "repo", + host: "gitlab.com", }); }); - it('should return null for malformed GitHub URLs', () => { - const malformedUrl = 'https://github.com/incomplete'; + it("should return null for malformed GitHub URLs", () => { + const malformedUrl = "https://github.com/incomplete"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: malformedUrl, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); @@ -218,13 +218,13 @@ describe('getGitRemote', () => { expect(result).toBe(null); }); - it('should return null when git command fails', () => { + it("should return null when git command fails", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: 'fatal: not a git repository', + stdout: "", + stderr: "fatal: not a git repository", exitCode: 128, - command: 'git remote get-url origin', - failed: true + command: "git remote get-url origin", + failed: true, })); const result = getGitRemote(); @@ -232,13 +232,13 @@ describe('getGitRemote', () => { expect(result).toBe(null); }); - it('should return null when origin remote does not exist', () => { + it("should return null when origin remote does not exist", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: 'fatal: No such remote \'origin\'', + stdout: "", + stderr: "fatal: No such remote 'origin'", exitCode: 128, - command: 'git remote get-url origin', - failed: true + command: "git remote get-url origin", + failed: true, })); const result = getGitRemote(); @@ -246,9 +246,9 @@ describe('getGitRemote', () => { expect(result).toBe(null); }); - it('should return null when execaSync throws an exception', () => { + it("should return null when execaSync throws an exception", () => { mockedExecaSync.mockImplementation(() => { - throw new Error('Command failed'); + throw new Error("Command failed"); }); const result = getGitRemote(); @@ -256,32 +256,32 @@ describe('getGitRemote', () => { expect(result).toBe(null); }); - it('should handle timeout correctly', () => { + it("should handle timeout correctly", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - stderr: '', + stdout: "", + stderr: "", exitCode: 124, - command: 'git remote get-url origin', + command: "git remote get-url origin", failed: true, - timedOut: true + timedOut: true, })); const result = getGitRemote(); expect(result).toBe(null); - expect(mockedExecaSync).toHaveBeenCalledWith('git', ['remote', 'get-url', 'origin'], { + expect(mockedExecaSync).toHaveBeenCalledWith("git", ["remote", "get-url", "origin"], { timeout: 5000, - reject: false + reject: false, }); }); - it('should handle URLs with additional path components', () => { - const urlWithPath = 'https://github.com/awesome-dude/ghouls.git/some/path'; + it("should handle URLs with additional path components", () => { + const urlWithPath = "https://github.com/awesome-dude/ghouls.git/some/path"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: urlWithPath, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); @@ -290,13 +290,13 @@ describe('getGitRemote', () => { expect(result).toBe(null); }); - it('should handle SSH URLs with different formats', () => { - const sshUrl = 'ssh://git@github.com:22/awesome-dude/ghouls.git'; + it("should handle SSH URLs with different formats", () => { + const sshUrl = "ssh://git@github.com:22/awesome-dude/ghouls.git"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: sshUrl, - stderr: '', + stderr: "", exitCode: 0, - command: 'git remote get-url origin' + command: "git remote get-url origin", })); const result = getGitRemote(); @@ -304,4 +304,4 @@ describe('getGitRemote', () => { // Should not match the ssh:// format, only git@ format expect(result).toBe(null); }); -}); \ No newline at end of file +}); diff --git a/src/utils/getGitRemote.ts b/src/utils/getGitRemote.ts index f106fd5..f21da32 100644 --- a/src/utils/getGitRemote.ts +++ b/src/utils/getGitRemote.ts @@ -7,12 +7,12 @@ export interface GitRemoteInfo { } export function parseGitRemote(remoteUrl: string): GitRemoteInfo | null { - if (!remoteUrl || typeof remoteUrl !== 'string') { + if (!remoteUrl || typeof remoteUrl !== "string") { return null; } const trimmedUrl = remoteUrl.trim(); - + if (!trimmedUrl) { return null; } @@ -20,31 +20,31 @@ export function parseGitRemote(remoteUrl: string): GitRemoteInfo | null { // Parse Git URLs (both HTTPS and SSH formats) // HTTPS: https://github.com/owner/repo.git or https://github.company.com/owner/repo.git // SSH: git@github.com:owner/repo.git or git@github.company.com:owner/repo.git - + let match: RegExpMatchArray | null = null; - + // Try HTTPS format - matches any domain match = trimmedUrl.match(/https:\/\/([^/]+)\/([^/]+)\/([^/]+?)(\.git)?$/); - + if (match && match[1] && match[2] && match[3]) { return { owner: match[2], repo: match[3], - host: match[1] + host: match[1], }; } - + // Try SSH format - matches any domain match = trimmedUrl.match(/git@([^:]+):([^/]+)\/([^/]+?)(\.git)?$/); - + if (match && match[1] && match[2] && match[3]) { return { owner: match[2], repo: match[3], - host: match[1] + host: match[1], }; } - + return null; } @@ -53,7 +53,7 @@ export function getGitRemote(): GitRemoteInfo | null { // Get the remote URL for origin const { stdout } = execaSync("git", ["remote", "get-url", "origin"], { timeout: 5000, // 5 second timeout - reject: false + reject: false, }); if (!stdout) { @@ -64,4 +64,4 @@ export function getGitRemote(): GitRemoteInfo | null { } catch (error) { return null; } -} \ No newline at end of file +} diff --git a/src/utils/ghCliErrorHandler.test.ts b/src/utils/ghCliErrorHandler.test.ts index b87acd5..ee277bb 100644 --- a/src/utils/ghCliErrorHandler.test.ts +++ b/src/utils/ghCliErrorHandler.test.ts @@ -1,23 +1,23 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { ExecaSyncError } from 'execa'; +import { ExecaSyncError } from "execa"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { detectGhCliError, - isGhNotInstalledError, - isGhNotAuthenticatedError, - getGhInstallationInstructions, + formatGhCliError, getGhAuthenticationInstructions, - formatGhCliError -} from './ghCliErrorHandler.js'; + getGhInstallationInstructions, + isGhNotAuthenticatedError, + isGhNotInstalledError, +} from "./ghCliErrorHandler.js"; // Mock the os module -vi.mock('os', () => ({ - platform: vi.fn() +vi.mock("os", () => ({ + platform: vi.fn(), })); -import { platform } from 'os'; +import { platform } from "os"; const mockedPlatform = vi.mocked(platform); -describe('ghCliErrorHandler', () => { +describe("ghCliErrorHandler", () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -26,288 +26,288 @@ describe('ghCliErrorHandler', () => { vi.restoreAllMocks(); }); - describe('isGhNotInstalledError', () => { - it('should detect exit code 127 as not installed', () => { + describe("isGhNotInstalledError", () => { + it("should detect exit code 127 as not installed", () => { const error = { exitCode: 127, - stderr: '', - stdout: '' + stderr: "", + stdout: "", } as ExecaSyncError; expect(isGhNotInstalledError(error)).toBe(true); }); - it('should detect "command not found" in stderr', () => { + it("should detect \"command not found\" in stderr", () => { const error = { exitCode: 1, - stderr: 'gh: command not found', - stdout: '' + stderr: "gh: command not found", + stdout: "", } as ExecaSyncError; expect(isGhNotInstalledError(error)).toBe(true); }); - it('should detect "not found" in stderr', () => { + it("should detect \"not found\" in stderr", () => { const error = { exitCode: 1, - stderr: 'bash: gh: not found', - stdout: '' + stderr: "bash: gh: not found", + stdout: "", } as ExecaSyncError; expect(isGhNotInstalledError(error)).toBe(true); }); - it('should detect "cannot find" in stderr', () => { + it("should detect \"cannot find\" in stderr", () => { const error = { exitCode: 1, - stderr: 'Cannot find gh executable', - stdout: '' + stderr: "Cannot find gh executable", + stdout: "", } as ExecaSyncError; expect(isGhNotInstalledError(error)).toBe(true); }); - it('should detect ENOENT error code', () => { + it("should detect ENOENT error code", () => { const error = { - code: 'ENOENT', + code: "ENOENT", exitCode: undefined, - stderr: '', - stdout: '' + stderr: "", + stdout: "", } as ExecaSyncError & { code: string }; expect(isGhNotInstalledError(error)).toBe(true); }); - it('should return false for other errors', () => { + it("should return false for other errors", () => { const error = { exitCode: 1, - stderr: 'Some other error', - stdout: '' + stderr: "Some other error", + stdout: "", } as ExecaSyncError; expect(isGhNotInstalledError(error)).toBe(false); }); }); - describe('isGhNotAuthenticatedError', () => { - it('should detect "gh auth login" message', () => { + describe("isGhNotAuthenticatedError", () => { + it("should detect \"gh auth login\" message", () => { const error = { exitCode: 1, - stderr: 'gh: To get started with GitHub CLI, please run: gh auth login', - stdout: '' + stderr: "gh: To get started with GitHub CLI, please run: gh auth login", + stdout: "", } as ExecaSyncError; expect(isGhNotAuthenticatedError(error)).toBe(true); }); - it('should detect "not authenticated" message', () => { + it("should detect \"not authenticated\" message", () => { const error = { exitCode: 1, - stderr: 'Error: Not authenticated', - stdout: '' + stderr: "Error: Not authenticated", + stdout: "", } as ExecaSyncError; expect(isGhNotAuthenticatedError(error)).toBe(true); }); - it('should detect "no github token" message', () => { + it("should detect \"no github token\" message", () => { const error = { exitCode: 1, - stderr: 'No GitHub token found', - stdout: '' + stderr: "No GitHub token found", + stdout: "", } as ExecaSyncError; expect(isGhNotAuthenticatedError(error)).toBe(true); }); - it('should detect "please authenticate" message', () => { + it("should detect \"please authenticate\" message", () => { const error = { exitCode: 1, - stderr: 'Please authenticate first', - stdout: '' + stderr: "Please authenticate first", + stdout: "", } as ExecaSyncError; expect(isGhNotAuthenticatedError(error)).toBe(true); }); - it('should detect auth token command failure', () => { + it("should detect auth token command failure", () => { const error = { exitCode: 1, - command: 'gh auth token', - stderr: '', - stdout: '' + command: "gh auth token", + stderr: "", + stdout: "", } as ExecaSyncError; expect(isGhNotAuthenticatedError(error)).toBe(true); }); - it('should check both stdout and stderr', () => { + it("should check both stdout and stderr", () => { const error = { exitCode: 1, - stderr: '', - stdout: 'Please run gh auth login' + stderr: "", + stdout: "Please run gh auth login", } as ExecaSyncError; expect(isGhNotAuthenticatedError(error)).toBe(true); }); - it('should be case insensitive', () => { + it("should be case insensitive", () => { const error = { exitCode: 1, - stderr: 'GH AUTH LOGIN required', - stdout: '' + stderr: "GH AUTH LOGIN required", + stdout: "", } as ExecaSyncError; expect(isGhNotAuthenticatedError(error)).toBe(true); }); - it('should return false for other errors', () => { + it("should return false for other errors", () => { const error = { exitCode: 1, - stderr: 'Network timeout', - stdout: '' + stderr: "Network timeout", + stdout: "", } as ExecaSyncError; expect(isGhNotAuthenticatedError(error)).toBe(false); }); }); - describe('detectGhCliError', () => { - it('should detect not installed error', () => { + describe("detectGhCliError", () => { + it("should detect not installed error", () => { // Mock the platform to ensure consistent test results - mockedPlatform.mockReturnValue('linux'); - + mockedPlatform.mockReturnValue("linux"); + const error = { exitCode: 127, - stderr: 'gh: command not found', - stdout: '' + stderr: "gh: command not found", + stdout: "", } as ExecaSyncError; const result = detectGhCliError(error); expect(result).not.toBeNull(); - expect(result?.type).toBe('not-installed'); - expect(result?.message).toBe('GitHub CLI (gh) is not installed.'); - expect(result?.instructions).toContain('To install GitHub CLI on Linux:'); + expect(result?.type).toBe("not-installed"); + expect(result?.message).toBe("GitHub CLI (gh) is not installed."); + expect(result?.instructions).toContain("To install GitHub CLI on Linux:"); }); - it('should detect not authenticated error', () => { + it("should detect not authenticated error", () => { const error = { exitCode: 1, - stderr: 'gh: To get started with GitHub CLI, please run: gh auth login', - stdout: '' + stderr: "gh: To get started with GitHub CLI, please run: gh auth login", + stdout: "", } as ExecaSyncError; const result = detectGhCliError(error); expect(result).not.toBeNull(); - expect(result?.type).toBe('not-authenticated'); - expect(result?.message).toBe('GitHub CLI is not authenticated.'); - expect(result?.instructions).toContain('To authenticate with GitHub:'); + expect(result?.type).toBe("not-authenticated"); + expect(result?.message).toBe("GitHub CLI is not authenticated."); + expect(result?.instructions).toContain("To authenticate with GitHub:"); }); - it('should return null for unknown errors', () => { + it("should return null for unknown errors", () => { const error = { exitCode: 1, - stderr: 'Some random error', - stdout: '' + stderr: "Some random error", + stdout: "", } as ExecaSyncError; const result = detectGhCliError(error); expect(result).toBeNull(); }); - it('should return null for non-object errors', () => { + it("should return null for non-object errors", () => { expect(detectGhCliError(null)).toBeNull(); expect(detectGhCliError(undefined)).toBeNull(); - expect(detectGhCliError('string error')).toBeNull(); + expect(detectGhCliError("string error")).toBeNull(); expect(detectGhCliError(123)).toBeNull(); }); }); - describe('getGhInstallationInstructions', () => { - it('should show Windows-specific instructions on Windows', () => { - mockedPlatform.mockReturnValue('win32'); + describe("getGhInstallationInstructions", () => { + it("should show Windows-specific instructions on Windows", () => { + mockedPlatform.mockReturnValue("win32"); const instructions = getGhInstallationInstructions(); - expect(instructions).toContain('To install GitHub CLI on Windows:'); - expect(instructions).toContain('winget install --id GitHub.cli'); - expect(instructions).toContain('choco install gh'); - expect(instructions).not.toContain('brew install'); - expect(instructions).not.toContain('apt install'); + expect(instructions).toContain("To install GitHub CLI on Windows:"); + expect(instructions).toContain("winget install --id GitHub.cli"); + expect(instructions).toContain("choco install gh"); + expect(instructions).not.toContain("brew install"); + expect(instructions).not.toContain("apt install"); }); - it('should show macOS-specific instructions on macOS', () => { - mockedPlatform.mockReturnValue('darwin'); + it("should show macOS-specific instructions on macOS", () => { + mockedPlatform.mockReturnValue("darwin"); const instructions = getGhInstallationInstructions(); - expect(instructions).toContain('To install GitHub CLI on macOS:'); - expect(instructions).toContain('brew install gh'); - expect(instructions).toContain('sudo port install gh'); - expect(instructions).not.toContain('winget install'); - expect(instructions).not.toContain('apt install'); + expect(instructions).toContain("To install GitHub CLI on macOS:"); + expect(instructions).toContain("brew install gh"); + expect(instructions).toContain("sudo port install gh"); + expect(instructions).not.toContain("winget install"); + expect(instructions).not.toContain("apt install"); }); - it('should show Linux instructions on Linux', () => { - mockedPlatform.mockReturnValue('linux'); + it("should show Linux instructions on Linux", () => { + mockedPlatform.mockReturnValue("linux"); const instructions = getGhInstallationInstructions(); - expect(instructions).toContain('To install GitHub CLI on Linux:'); - expect(instructions).toContain('sudo apt install gh'); - expect(instructions).toContain('sudo dnf install gh'); - expect(instructions).toContain('sudo pacman -S github-cli'); - expect(instructions).not.toContain('winget install'); - expect(instructions).not.toContain('brew install'); + expect(instructions).toContain("To install GitHub CLI on Linux:"); + expect(instructions).toContain("sudo apt install gh"); + expect(instructions).toContain("sudo dnf install gh"); + expect(instructions).toContain("sudo pacman -S github-cli"); + expect(instructions).not.toContain("winget install"); + expect(instructions).not.toContain("brew install"); }); - it('should include link to README for other platforms', () => { - mockedPlatform.mockReturnValue('linux'); + it("should include link to README for other platforms", () => { + mockedPlatform.mockReturnValue("linux"); const instructions = getGhInstallationInstructions(); - expect(instructions).toContain('https://github.com/ericanderson/ghouls#installing-github-cli'); + expect(instructions).toContain("https://github.com/ericanderson/ghouls#installing-github-cli"); }); - it('should include GitHub CLI website link', () => { - mockedPlatform.mockReturnValue('darwin'); + it("should include GitHub CLI website link", () => { + mockedPlatform.mockReturnValue("darwin"); const instructions = getGhInstallationInstructions(); - expect(instructions).toContain('https://cli.github.com/'); + expect(instructions).toContain("https://cli.github.com/"); }); - it('should handle unknown platforms', () => { - mockedPlatform.mockReturnValue('freebsd' as any); + it("should handle unknown platforms", () => { + mockedPlatform.mockReturnValue("freebsd" as any); const instructions = getGhInstallationInstructions(); - expect(instructions).toContain('To install GitHub CLI on your platform'); - expect(instructions).toContain('https://cli.github.com/'); + expect(instructions).toContain("To install GitHub CLI on your platform"); + expect(instructions).toContain("https://cli.github.com/"); }); }); - describe('getGhAuthenticationInstructions', () => { - it('should include authentication steps', () => { + describe("getGhAuthenticationInstructions", () => { + it("should include authentication steps", () => { const instructions = getGhAuthenticationInstructions(); - expect(instructions).toContain('gh auth login'); - expect(instructions).toContain('Choose GitHub.com or GitHub Enterprise Server'); - expect(instructions).toContain('Login with a web browser (recommended)'); - expect(instructions).toContain('Paste an authentication token'); + expect(instructions).toContain("gh auth login"); + expect(instructions).toContain("Choose GitHub.com or GitHub Enterprise Server"); + expect(instructions).toContain("Login with a web browser (recommended)"); + expect(instructions).toContain("Paste an authentication token"); }); - it('should include documentation link', () => { + it("should include documentation link", () => { const instructions = getGhAuthenticationInstructions(); - expect(instructions).toContain('https://cli.github.com/manual/gh_auth_login'); + expect(instructions).toContain("https://cli.github.com/manual/gh_auth_login"); }); }); - describe('formatGhCliError', () => { - it('should return instructions when available', () => { + describe("formatGhCliError", () => { + it("should return instructions when available", () => { const error = { - type: 'not-installed' as const, - message: 'Not installed', - instructions: 'Detailed instructions here' + type: "not-installed" as const, + message: "Not installed", + instructions: "Detailed instructions here", }; - expect(formatGhCliError(error)).toBe('Detailed instructions here'); + expect(formatGhCliError(error)).toBe("Detailed instructions here"); }); - it('should return message when instructions not available', () => { + it("should return message when instructions not available", () => { const error = { - type: 'unknown' as const, - message: 'Unknown error occurred' + type: "unknown" as const, + message: "Unknown error occurred", }; - expect(formatGhCliError(error)).toBe('Unknown error occurred'); + expect(formatGhCliError(error)).toBe("Unknown error occurred"); }); }); -}); \ No newline at end of file +}); diff --git a/src/utils/ghCliErrorHandler.ts b/src/utils/ghCliErrorHandler.ts index 2007c13..b0fa8e9 100644 --- a/src/utils/ghCliErrorHandler.ts +++ b/src/utils/ghCliErrorHandler.ts @@ -14,22 +14,22 @@ export function detectGhCliError(error: unknown): GhCliError | null { // Check if it's an ExecaSyncError const execaError = error as ExecaSyncError; - + // Check for gh not installed if (isGhNotInstalledError(execaError)) { return { type: "not-installed", message: "GitHub CLI (gh) is not installed.", - instructions: getGhInstallationInstructions() + instructions: getGhInstallationInstructions(), }; } // Check for gh not authenticated if (isGhNotAuthenticatedError(execaError)) { return { - type: "not-authenticated", + type: "not-authenticated", message: "GitHub CLI is not authenticated.", - instructions: getGhAuthenticationInstructions() + instructions: getGhAuthenticationInstructions(), }; } @@ -43,10 +43,12 @@ export function isGhNotInstalledError(error: ExecaSyncError): boolean { } // Check stderr for common "command not found" messages - const stderr = typeof error.stderr === 'string' ? error.stderr.toLowerCase() : ""; - if (stderr.includes("command not found") || - stderr.includes("not found") || - stderr.includes("cannot find")) { + const stderr = typeof error.stderr === "string" ? error.stderr.toLowerCase() : ""; + if ( + stderr.includes("command not found") + || stderr.includes("not found") + || stderr.includes("cannot find") + ) { return true; } @@ -59,16 +61,18 @@ export function isGhNotInstalledError(error: ExecaSyncError): boolean { } export function isGhNotAuthenticatedError(error: ExecaSyncError): boolean { - const stderr = typeof error.stderr === 'string' ? error.stderr : ""; - const stdout = typeof error.stdout === 'string' ? error.stdout : ""; + const stderr = typeof error.stderr === "string" ? error.stderr : ""; + const stdout = typeof error.stdout === "string" ? error.stdout : ""; const combined = `${stderr} ${stdout}`.toLowerCase(); // Check for authentication-related messages - if (combined.includes("gh auth login") || - combined.includes("not authenticated") || - combined.includes("no github token") || - combined.includes("please authenticate") || - combined.includes("to get started with github cli")) { + if ( + combined.includes("gh auth login") + || combined.includes("not authenticated") + || combined.includes("no github token") + || combined.includes("please authenticate") + || combined.includes("to get started with github cli") + ) { return true; } @@ -160,4 +164,4 @@ export function formatGhCliError(error: GhCliError): string { return error.instructions; } return error.message; -} \ No newline at end of file +} diff --git a/src/utils/localGitOperations.test.ts b/src/utils/localGitOperations.test.ts index c0d2888..9b4a024 100644 --- a/src/utils/localGitOperations.test.ts +++ b/src/utils/localGitOperations.test.ts @@ -1,19 +1,19 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { execaSync } from 'execa'; +import { execaSync } from "execa"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createMockExecaResult, expectGitTimeout } from "../test/setup.js"; import { - getLocalBranches, - getCurrentBranch, - getBranchStatus, deleteLocalBranch, - isGitRepository -} from './localGitOperations.js'; -import { createMockExecaResult, expectGitTimeout } from '../test/setup.js'; + getBranchStatus, + getCurrentBranch, + getLocalBranches, + isGitRepository, +} from "./localGitOperations.js"; // Mock execa -vi.mock('execa'); +vi.mock("execa"); const mockedExecaSync = vi.mocked(execaSync); -describe('localGitOperations', () => { +describe("localGitOperations", () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -22,36 +22,37 @@ describe('localGitOperations', () => { vi.restoreAllMocks(); }); - describe('getLocalBranches', () => { - it('should return local branches with correct format', () => { - const mockOutput = 'main|abc123|*|2024-01-01 10:00:00 -0500\nfeature/test|def456||2024-01-02 11:00:00 -0500\ndevelop|ghi789||2024-01-03 12:00:00 -0500'; + describe("getLocalBranches", () => { + it("should return local branches with correct format", () => { + const mockOutput = + "main|abc123|*|2024-01-01 10:00:00 -0500\nfeature/test|def456||2024-01-02 11:00:00 -0500\ndevelop|ghi789||2024-01-03 12:00:00 -0500"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: mockOutput, - command: 'git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)' + command: "git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)", })); const result = getLocalBranches(); expect(mockedExecaSync).toHaveBeenCalledWith( - 'git', - ['branch', '-v', '--format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)'], + "git", + ["branch", "-v", "--format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)"], { timeout: 10000, - reject: false - } + reject: false, + }, ); expect(result).toEqual([ - { name: 'main', sha: 'abc123', isCurrent: true, lastCommitDate: '2024-01-01 10:00:00 -0500' }, - { name: 'feature/test', sha: 'def456', isCurrent: false, lastCommitDate: '2024-01-02 11:00:00 -0500' }, - { name: 'develop', sha: 'ghi789', isCurrent: false, lastCommitDate: '2024-01-03 12:00:00 -0500' } + { name: "main", sha: "abc123", isCurrent: true, lastCommitDate: "2024-01-01 10:00:00 -0500" }, + { name: "feature/test", sha: "def456", isCurrent: false, lastCommitDate: "2024-01-02 11:00:00 -0500" }, + { name: "develop", sha: "ghi789", isCurrent: false, lastCommitDate: "2024-01-03 12:00:00 -0500" }, ]); }); - it('should return empty array when no stdout', () => { + it("should return empty array when no stdout", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - command: 'git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)' + stdout: "", + command: "git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)", })); const result = getLocalBranches(); @@ -59,66 +60,68 @@ describe('localGitOperations', () => { expect(result).toEqual([]); }); - it('should filter out empty lines', () => { - const mockOutput = 'main|abc123|*|2024-01-01 10:00:00 -0500\n\n\nfeature/test|def456||2024-01-02 11:00:00 -0500\n\n'; + it("should filter out empty lines", () => { + const mockOutput = + "main|abc123|*|2024-01-01 10:00:00 -0500\n\n\nfeature/test|def456||2024-01-02 11:00:00 -0500\n\n"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: mockOutput, - command: 'git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)' + command: "git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)", })); const result = getLocalBranches(); expect(result).toEqual([ - { name: 'main', sha: 'abc123', isCurrent: true, lastCommitDate: '2024-01-01 10:00:00 -0500' }, - { name: 'feature/test', sha: 'def456', isCurrent: false, lastCommitDate: '2024-01-02 11:00:00 -0500' } + { name: "main", sha: "abc123", isCurrent: true, lastCommitDate: "2024-01-01 10:00:00 -0500" }, + { name: "feature/test", sha: "def456", isCurrent: false, lastCommitDate: "2024-01-02 11:00:00 -0500" }, ]); }); - it('should handle branches with spaces in names', () => { - const mockOutput = 'feature branch|abc123||2024-01-01 10:00:00 -0500\ntest-branch|def456|*|2024-01-02 11:00:00 -0500'; + it("should handle branches with spaces in names", () => { + const mockOutput = + "feature branch|abc123||2024-01-01 10:00:00 -0500\ntest-branch|def456|*|2024-01-02 11:00:00 -0500"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: mockOutput, - command: 'git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)' + command: "git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)", })); const result = getLocalBranches(); expect(result).toEqual([ - { name: 'feature branch', sha: 'abc123', isCurrent: false, lastCommitDate: '2024-01-01 10:00:00 -0500' }, - { name: 'test-branch', sha: 'def456', isCurrent: true, lastCommitDate: '2024-01-02 11:00:00 -0500' } + { name: "feature branch", sha: "abc123", isCurrent: false, lastCommitDate: "2024-01-01 10:00:00 -0500" }, + { name: "test-branch", sha: "def456", isCurrent: true, lastCommitDate: "2024-01-02 11:00:00 -0500" }, ]); }); - it('should throw error for malformed git output', () => { - const mockOutput = 'invalid-format-line\nmain|abc123'; + it("should throw error for malformed git output", () => { + const mockOutput = "invalid-format-line\nmain|abc123"; mockedExecaSync.mockReturnValue(createMockExecaResult({ stdout: mockOutput, - command: 'git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)' + command: "git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)", })); - expect(() => getLocalBranches()).toThrow('Unexpected git branch output format: invalid-format-line'); + expect(() => getLocalBranches()).toThrow("Unexpected git branch output format: invalid-format-line"); }); - it('should throw error when git command fails', () => { + it("should throw error when git command fails", () => { mockedExecaSync.mockImplementation(() => { - throw new Error('git command failed'); + throw new Error("git command failed"); }); - expect(() => getLocalBranches()).toThrow('Failed to get local branches: git command failed'); + expect(() => getLocalBranches()).toThrow("Failed to get local branches: git command failed"); }); - it('should handle non-Error exceptions', () => { + it("should handle non-Error exceptions", () => { mockedExecaSync.mockImplementation(() => { - throw 'string error'; + throw "string error"; }); - expect(() => getLocalBranches()).toThrow('Failed to get local branches: string error'); + expect(() => getLocalBranches()).toThrow("Failed to get local branches: string error"); }); - it('should use correct timeout for git command', () => { + it("should use correct timeout for git command", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: 'main|abc123|*|2024-01-01 10:00:00 -0500', - command: 'git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)' + stdout: "main|abc123|*|2024-01-01 10:00:00 -0500", + command: "git branch -v --format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)", })); getLocalBranches(); @@ -127,61 +130,61 @@ describe('localGitOperations', () => { }); }); - describe('getCurrentBranch', () => { - it('should return current branch name', () => { + describe("getCurrentBranch", () => { + it("should return current branch name", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: 'main', - command: 'git branch --show-current' + stdout: "main", + command: "git branch --show-current", })); const result = getCurrentBranch(); expect(mockedExecaSync).toHaveBeenCalledWith( - 'git', - ['branch', '--show-current'], + "git", + ["branch", "--show-current"], { timeout: 5000, - reject: false - } + reject: false, + }, ); - expect(result).toBe('main'); + expect(result).toBe("main"); }); - it('should trim whitespace from branch name', () => { + it("should trim whitespace from branch name", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: ' feature/test \n', - command: 'git branch --show-current' + stdout: " feature/test \n", + command: "git branch --show-current", })); const result = getCurrentBranch(); - expect(result).toBe('feature/test'); + expect(result).toBe("feature/test"); }); - it('should return empty string when no current branch', () => { + it("should return empty string when no current branch", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - command: 'git branch --show-current' + stdout: "", + command: "git branch --show-current", })); const result = getCurrentBranch(); - expect(result).toBe(''); + expect(result).toBe(""); }); - it('should throw error when git command fails', () => { + it("should throw error when git command fails", () => { mockedExecaSync.mockImplementation(() => { - throw new Error('git command failed'); + throw new Error("git command failed"); }); - expect(() => getCurrentBranch()).toThrow('Failed to get current branch: git command failed'); + expect(() => getCurrentBranch()).toThrow("Failed to get current branch: git command failed"); }); - it('should use correct timeout for git command', () => { + it("should use correct timeout for git command", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: 'main', - command: 'git branch --show-current' + stdout: "main", + command: "git branch --show-current", })); getCurrentBranch(); @@ -190,255 +193,256 @@ describe('localGitOperations', () => { }); }); - describe('getBranchStatus', () => { - it('should return branch status when upstream exists', () => { + describe("getBranchStatus", () => { + it("should return branch status when upstream exists", () => { // Mock upstream check mockedExecaSync.mockReturnValueOnce(createMockExecaResult({ - stdout: 'origin/feature-branch', - command: 'git rev-parse --abbrev-ref feature-branch@{upstream}' + stdout: "origin/feature-branch", + command: "git rev-parse --abbrev-ref feature-branch@{upstream}", })); // Mock status check mockedExecaSync.mockReturnValueOnce(createMockExecaResult({ - stdout: '2\t3', - command: 'git rev-list --count --left-right origin/feature-branch...feature-branch' + stdout: "2\t3", + command: "git rev-list --count --left-right origin/feature-branch...feature-branch", })); - const result = getBranchStatus('feature-branch'); + const result = getBranchStatus("feature-branch"); expect(mockedExecaSync).toHaveBeenCalledTimes(2); - expect(mockedExecaSync).toHaveBeenNthCalledWith(1, - 'git', - ['rev-parse', '--abbrev-ref', 'feature-branch@{upstream}'], - { - timeout: 5000, - reject: false - } - ); - expect(mockedExecaSync).toHaveBeenNthCalledWith(2, - 'git', - ['rev-list', '--count', '--left-right', 'origin/feature-branch...feature-branch'], - { - timeout: 5000, - reject: false - } - ); + expect(mockedExecaSync).toHaveBeenNthCalledWith(1, "git", [ + "rev-parse", + "--abbrev-ref", + "feature-branch@{upstream}", + ], { + timeout: 5000, + reject: false, + }); + expect(mockedExecaSync).toHaveBeenNthCalledWith(2, "git", [ + "rev-list", + "--count", + "--left-right", + "origin/feature-branch...feature-branch", + ], { + timeout: 5000, + reject: false, + }); expect(result).toEqual({ behind: 2, - ahead: 3 + ahead: 3, }); }); - it('should return zero ahead/behind when no upstream', () => { + it("should return zero ahead/behind when no upstream", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '', - command: 'git rev-parse --abbrev-ref feature-branch@{upstream}' + stdout: "", + command: "git rev-parse --abbrev-ref feature-branch@{upstream}", })); - const result = getBranchStatus('feature-branch'); + const result = getBranchStatus("feature-branch"); expect(mockedExecaSync).toHaveBeenCalledTimes(1); expect(result).toEqual({ ahead: 0, - behind: 0 + behind: 0, }); }); - it('should handle malformed rev-list output', () => { + it("should handle malformed rev-list output", () => { mockedExecaSync.mockReturnValueOnce(createMockExecaResult({ - stdout: 'origin/feature-branch', - command: 'git rev-parse --abbrev-ref feature-branch@{upstream}' + stdout: "origin/feature-branch", + command: "git rev-parse --abbrev-ref feature-branch@{upstream}", })); mockedExecaSync.mockReturnValueOnce(createMockExecaResult({ - stdout: 'invalid-format', - command: 'git rev-list --count --left-right origin/feature-branch...feature-branch' + stdout: "invalid-format", + command: "git rev-list --count --left-right origin/feature-branch...feature-branch", })); - const result = getBranchStatus('feature-branch'); + const result = getBranchStatus("feature-branch"); expect(result).toBeNull(); }); - it('should handle zero counts correctly', () => { + it("should handle zero counts correctly", () => { mockedExecaSync.mockReturnValueOnce(createMockExecaResult({ - stdout: 'origin/feature-branch', - command: 'git rev-parse --abbrev-ref feature-branch@{upstream}' + stdout: "origin/feature-branch", + command: "git rev-parse --abbrev-ref feature-branch@{upstream}", })); mockedExecaSync.mockReturnValueOnce(createMockExecaResult({ - stdout: '0\t0', - command: 'git rev-list --count --left-right origin/feature-branch...feature-branch' + stdout: "0\t0", + command: "git rev-list --count --left-right origin/feature-branch...feature-branch", })); - const result = getBranchStatus('feature-branch'); + const result = getBranchStatus("feature-branch"); expect(result).toEqual({ behind: 0, - ahead: 0 + ahead: 0, }); }); - it('should handle invalid number parsing', () => { + it("should handle invalid number parsing", () => { mockedExecaSync.mockReturnValueOnce(createMockExecaResult({ - stdout: 'origin/feature-branch', - command: 'git rev-parse --abbrev-ref feature-branch@{upstream}' + stdout: "origin/feature-branch", + command: "git rev-parse --abbrev-ref feature-branch@{upstream}", })); mockedExecaSync.mockReturnValueOnce(createMockExecaResult({ - stdout: 'abc\tdef', - command: 'git rev-list --count --left-right origin/feature-branch...feature-branch' + stdout: "abc\tdef", + command: "git rev-list --count --left-right origin/feature-branch...feature-branch", })); - const result = getBranchStatus('feature-branch'); + const result = getBranchStatus("feature-branch"); expect(result).toEqual({ behind: 0, - ahead: 0 + ahead: 0, }); }); - it('should return null when git command throws', () => { + it("should return null when git command throws", () => { mockedExecaSync.mockImplementation(() => { - throw new Error('git command failed'); + throw new Error("git command failed"); }); - const result = getBranchStatus('feature-branch'); + const result = getBranchStatus("feature-branch"); expect(result).toBeNull(); }); - it('should handle upstream with special characters', () => { + it("should handle upstream with special characters", () => { mockedExecaSync.mockReturnValueOnce(createMockExecaResult({ - stdout: 'origin/feature/special-chars', - command: 'git rev-parse --abbrev-ref feature-branch@{upstream}' + stdout: "origin/feature/special-chars", + command: "git rev-parse --abbrev-ref feature-branch@{upstream}", })); mockedExecaSync.mockReturnValueOnce(createMockExecaResult({ - stdout: '1\t2', - command: 'git rev-list --count --left-right origin/feature/special-chars...feature-branch' + stdout: "1\t2", + command: "git rev-list --count --left-right origin/feature/special-chars...feature-branch", })); - const result = getBranchStatus('feature-branch'); + const result = getBranchStatus("feature-branch"); expect(result).toEqual({ behind: 1, - ahead: 2 + ahead: 2, }); }); }); - describe('deleteLocalBranch', () => { - it('should delete branch with -d flag by default', () => { + describe("deleteLocalBranch", () => { + it("should delete branch with -d flag by default", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - command: 'git branch -d feature-branch' + command: "git branch -d feature-branch", })); - deleteLocalBranch('feature-branch'); + deleteLocalBranch("feature-branch"); expect(mockedExecaSync).toHaveBeenCalledWith( - 'git', - ['branch', '-d', 'feature-branch'], + "git", + ["branch", "-d", "feature-branch"], { - timeout: 10000 - } + timeout: 10000, + }, ); }); - it('should delete branch with -D flag when force is true', () => { + it("should delete branch with -D flag when force is true", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - command: 'git branch -D feature-branch' + command: "git branch -D feature-branch", })); - deleteLocalBranch('feature-branch', true); + deleteLocalBranch("feature-branch", true); expect(mockedExecaSync).toHaveBeenCalledWith( - 'git', - ['branch', '-D', 'feature-branch'], + "git", + ["branch", "-D", "feature-branch"], { - timeout: 10000 - } + timeout: 10000, + }, ); }); - it('should handle branch names with special characters', () => { + it("should handle branch names with special characters", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - command: 'git branch -d feature/test-branch' + command: "git branch -d feature/test-branch", })); - deleteLocalBranch('feature/test-branch'); + deleteLocalBranch("feature/test-branch"); expect(mockedExecaSync).toHaveBeenCalledWith( - 'git', - ['branch', '-d', 'feature/test-branch'], + "git", + ["branch", "-d", "feature/test-branch"], { - timeout: 10000 - } + timeout: 10000, + }, ); }); - it('should throw error when git command fails', () => { + it("should throw error when git command fails", () => { mockedExecaSync.mockImplementation(() => { - throw new Error('branch deletion failed'); + throw new Error("branch deletion failed"); }); - expect(() => deleteLocalBranch('feature-branch')).toThrow( - 'Failed to delete branch feature-branch: branch deletion failed' + expect(() => deleteLocalBranch("feature-branch")).toThrow( + "Failed to delete branch feature-branch: branch deletion failed", ); }); - it('should handle non-Error exceptions', () => { + it("should handle non-Error exceptions", () => { mockedExecaSync.mockImplementation(() => { - throw 'string error'; + throw "string error"; }); - expect(() => deleteLocalBranch('feature-branch')).toThrow( - 'Failed to delete branch feature-branch: string error' + expect(() => deleteLocalBranch("feature-branch")).toThrow( + "Failed to delete branch feature-branch: string error", ); }); - it('should use correct timeout for git command', () => { + it("should use correct timeout for git command", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - command: 'git branch -d feature-branch' + command: "git branch -d feature-branch", })); - deleteLocalBranch('feature-branch'); + deleteLocalBranch("feature-branch"); expect(mockedExecaSync).toHaveBeenCalledWith( - 'git', - ['branch', '-d', 'feature-branch'], + "git", + ["branch", "-d", "feature-branch"], { - timeout: 10000 - } + timeout: 10000, + }, ); }); }); - describe('isGitRepository', () => { - it('should return true when in git repository', () => { + describe("isGitRepository", () => { + it("should return true when in git repository", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '.git', - command: 'git rev-parse --git-dir' + stdout: ".git", + command: "git rev-parse --git-dir", })); const result = isGitRepository(); expect(mockedExecaSync).toHaveBeenCalledWith( - 'git', - ['rev-parse', '--git-dir'], + "git", + ["rev-parse", "--git-dir"], { timeout: 5000, - reject: false - } + reject: false, + }, ); expect(result).toBe(true); }); - it('should return false when not in git repository', () => { + it("should return false when not in git repository", () => { mockedExecaSync.mockImplementation(() => { - throw new Error('not a git repository'); + throw new Error("not a git repository"); }); const result = isGitRepository(); @@ -446,9 +450,9 @@ describe('localGitOperations', () => { expect(result).toBe(false); }); - it('should return false when git command fails for any reason', () => { + it("should return false when git command fails for any reason", () => { mockedExecaSync.mockImplementation(() => { - throw 'any error'; + throw "any error"; }); const result = isGitRepository(); @@ -456,10 +460,10 @@ describe('localGitOperations', () => { expect(result).toBe(false); }); - it('should use correct timeout for git command', () => { + it("should use correct timeout for git command", () => { mockedExecaSync.mockReturnValue(createMockExecaResult({ - stdout: '.git', - command: 'git rev-parse --git-dir' + stdout: ".git", + command: "git rev-parse --git-dir", })); isGitRepository(); diff --git a/src/utils/localGitOperations.ts b/src/utils/localGitOperations.ts index cfb903b..298ce3d 100644 --- a/src/utils/localGitOperations.ts +++ b/src/utils/localGitOperations.ts @@ -18,9 +18,13 @@ export interface BranchStatus { export function getLocalBranches(): LocalBranch[] { try { // Get all local branches with their SHAs, current branch indicator, and commit date - const { stdout } = execaSync("git", ["branch", "-v", "--format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)"], { + const { stdout } = execaSync("git", [ + "branch", + "-v", + "--format=%(refname:short)|%(objectname)|%(HEAD)|%(committerdate:iso)", + ], { timeout: 10000, - reject: false + reject: false, }); if (!stdout) { @@ -35,12 +39,12 @@ export function getLocalBranches(): LocalBranch[] { if (parts.length !== 4) { throw new Error(`Unexpected git branch output format: ${line}`); } - + return { name: parts[0].trim(), sha: parts[1].trim(), isCurrent: parts[2].trim() === "*", - lastCommitDate: parts[3].trim() + lastCommitDate: parts[3].trim(), }; }); } catch (error) { @@ -55,7 +59,7 @@ export function getCurrentBranch(): string { try { const { stdout } = execaSync("git", ["branch", "--show-current"], { timeout: 5000, - reject: false + reject: false, }); return stdout.trim(); @@ -72,7 +76,7 @@ export function getBranchStatus(branchName: string): BranchStatus | null { // First check if the branch has an upstream const { stdout: upstreamResult } = execaSync("git", ["rev-parse", "--abbrev-ref", `${branchName}@{upstream}`], { timeout: 5000, - reject: false + reject: false, }); if (!upstreamResult) { @@ -85,7 +89,7 @@ export function getBranchStatus(branchName: string): BranchStatus | null { // Get ahead/behind status const { stdout } = execaSync("git", ["rev-list", "--count", "--left-right", `${upstream}...${branchName}`], { timeout: 5000, - reject: false + reject: false, }); const parts = stdout.trim().split("\t"); @@ -95,7 +99,7 @@ export function getBranchStatus(branchName: string): BranchStatus | null { return { behind: parseInt(parts[0], 10) || 0, - ahead: parseInt(parts[1], 10) || 0 + ahead: parseInt(parts[1], 10) || 0, }; } catch (error) { // If we can't determine status, assume it's not safe to delete @@ -110,7 +114,7 @@ export function deleteLocalBranch(branchName: string, force: boolean = false): v try { const args = ["branch", force ? "-D" : "-d", branchName]; execaSync("git", args, { - timeout: 10000 + timeout: 10000, }); } catch (error) { throw new Error(`Failed to delete branch ${branchName}: ${error instanceof Error ? error.message : String(error)}`); @@ -124,10 +128,10 @@ export function isGitRepository(): boolean { try { execaSync("git", ["rev-parse", "--git-dir"], { timeout: 5000, - reject: false + reject: false, }); return true; } catch { return false; } -} \ No newline at end of file +} diff --git a/src/utils/ownerAndRepoMatch.test.ts b/src/utils/ownerAndRepoMatch.test.ts index 6092510..b357323 100644 --- a/src/utils/ownerAndRepoMatch.test.ts +++ b/src/utils/ownerAndRepoMatch.test.ts @@ -1,159 +1,159 @@ -import { describe, it, expect } from 'vitest'; -import { ownerAndRepoMatch } from './ownerAndRepoMatch.js'; -import { PullRequestReference } from '../OctokitPlus.js'; +import { describe, expect, it } from "vitest"; +import { PullRequestReference } from "../OctokitPlus.js"; +import { ownerAndRepoMatch } from "./ownerAndRepoMatch.js"; -describe('ownerAndRepoMatch', () => { +describe("ownerAndRepoMatch", () => { const createPullRequestReference = (owner: string, repo: string): PullRequestReference => ({ label: `${owner}:branch`, - ref: 'branch', - sha: 'abc123', + ref: "branch", + sha: "abc123", repo: { name: repo, owner: { login: owner }, - fork: false - } + fork: false, + }, }); - it('should return true when owner and repo match', () => { - const refA = createPullRequestReference('octocat', 'hello-world'); - const refB = createPullRequestReference('octocat', 'hello-world'); + it("should return true when owner and repo match", () => { + const refA = createPullRequestReference("octocat", "hello-world"); + const refB = createPullRequestReference("octocat", "hello-world"); expect(ownerAndRepoMatch(refA, refB)).toBeTruthy(); }); - it('should return false when owners do not match', () => { - const refA = createPullRequestReference('octocat', 'hello-world'); - const refB = createPullRequestReference('github', 'hello-world'); + it("should return false when owners do not match", () => { + const refA = createPullRequestReference("octocat", "hello-world"); + const refB = createPullRequestReference("github", "hello-world"); expect(ownerAndRepoMatch(refA, refB)).toBeFalsy(); }); - it('should return false when repos do not match', () => { - const refA = createPullRequestReference('octocat', 'hello-world'); - const refB = createPullRequestReference('octocat', 'goodbye-world'); + it("should return false when repos do not match", () => { + const refA = createPullRequestReference("octocat", "hello-world"); + const refB = createPullRequestReference("octocat", "goodbye-world"); expect(ownerAndRepoMatch(refA, refB)).toBeFalsy(); }); - it('should return false when both owner and repo do not match', () => { - const refA = createPullRequestReference('octocat', 'hello-world'); - const refB = createPullRequestReference('github', 'goodbye-world'); + it("should return false when both owner and repo do not match", () => { + const refA = createPullRequestReference("octocat", "hello-world"); + const refB = createPullRequestReference("github", "goodbye-world"); expect(ownerAndRepoMatch(refA, refB)).toBeFalsy(); }); - it('should return false when first reference has no repo', () => { + it("should return false when first reference has no repo", () => { const refA: PullRequestReference = { - label: 'octocat:branch', - ref: 'branch', - sha: 'abc123', - repo: null + label: "octocat:branch", + ref: "branch", + sha: "abc123", + repo: null, }; - const refB = createPullRequestReference('octocat', 'hello-world'); + const refB = createPullRequestReference("octocat", "hello-world"); expect(ownerAndRepoMatch(refA, refB)).toBeFalsy(); }); - it('should return false when second reference has no repo', () => { - const refA = createPullRequestReference('octocat', 'hello-world'); + it("should return false when second reference has no repo", () => { + const refA = createPullRequestReference("octocat", "hello-world"); const refB: PullRequestReference = { - label: 'octocat:branch', - ref: 'branch', - sha: 'abc123', - repo: null + label: "octocat:branch", + ref: "branch", + sha: "abc123", + repo: null, }; expect(ownerAndRepoMatch(refA, refB)).toBeFalsy(); }); - it('should return false when both references have no repo', () => { + it("should return false when both references have no repo", () => { const refA: PullRequestReference = { - label: 'octocat:branch', - ref: 'branch', - sha: 'abc123', - repo: null + label: "octocat:branch", + ref: "branch", + sha: "abc123", + repo: null, }; const refB: PullRequestReference = { - label: 'github:branch', - ref: 'branch', - sha: 'def456', - repo: null + label: "github:branch", + ref: "branch", + sha: "def456", + repo: null, }; expect(ownerAndRepoMatch(refA, refB)).toBeFalsy(); }); - it('should handle case sensitivity correctly', () => { - const refA = createPullRequestReference('OctoCat', 'Hello-World'); - const refB = createPullRequestReference('octocat', 'hello-world'); + it("should handle case sensitivity correctly", () => { + const refA = createPullRequestReference("OctoCat", "Hello-World"); + const refB = createPullRequestReference("octocat", "hello-world"); // GitHub usernames and repo names are case-insensitive in URLs but // the API returns them with original casing, so exact match is expected expect(ownerAndRepoMatch(refA, refB)).toBeFalsy(); }); - it('should handle special characters in owner and repo names', () => { - const refA = createPullRequestReference('octo-cat_123', 'hello.world-repo'); - const refB = createPullRequestReference('octo-cat_123', 'hello.world-repo'); + it("should handle special characters in owner and repo names", () => { + const refA = createPullRequestReference("octo-cat_123", "hello.world-repo"); + const refB = createPullRequestReference("octo-cat_123", "hello.world-repo"); expect(ownerAndRepoMatch(refA, refB)).toBeTruthy(); }); - it('should handle empty strings in owner login', () => { + it("should handle empty strings in owner login", () => { const refA: PullRequestReference = { - label: ':branch', - ref: 'branch', - sha: 'abc123', + label: ":branch", + ref: "branch", + sha: "abc123", repo: { - name: 'hello-world', - owner: { login: '' }, - fork: false - } + name: "hello-world", + owner: { login: "" }, + fork: false, + }, }; - const refB = createPullRequestReference('octocat', 'hello-world'); + const refB = createPullRequestReference("octocat", "hello-world"); expect(ownerAndRepoMatch(refA, refB)).toBeFalsy(); }); - it('should handle empty strings in repo name', () => { + it("should handle empty strings in repo name", () => { const refA: PullRequestReference = { - label: 'octocat:branch', - ref: 'branch', - sha: 'abc123', + label: "octocat:branch", + ref: "branch", + sha: "abc123", repo: { - name: '', - owner: { login: 'octocat' }, - fork: false - } + name: "", + owner: { login: "octocat" }, + fork: false, + }, }; - const refB = createPullRequestReference('octocat', 'hello-world'); + const refB = createPullRequestReference("octocat", "hello-world"); expect(ownerAndRepoMatch(refA, refB)).toBeFalsy(); }); - it('should work with forked repositories', () => { + it("should work with forked repositories", () => { const refA: PullRequestReference = { - label: 'octocat:branch', - ref: 'branch', - sha: 'abc123', + label: "octocat:branch", + ref: "branch", + sha: "abc123", repo: { - name: 'hello-world', - owner: { login: 'octocat' }, - fork: true - } + name: "hello-world", + owner: { login: "octocat" }, + fork: true, + }, }; const refB: PullRequestReference = { - label: 'octocat:branch', - ref: 'branch', - sha: 'def456', + label: "octocat:branch", + ref: "branch", + sha: "def456", repo: { - name: 'hello-world', - owner: { login: 'octocat' }, - fork: false - } + name: "hello-world", + owner: { login: "octocat" }, + fork: false, + }, }; // Fork status doesn't affect the match expect(ownerAndRepoMatch(refA, refB)).toBeTruthy(); }); -}); \ No newline at end of file +}); diff --git a/src/utils/ownerAndRepoMatch.ts b/src/utils/ownerAndRepoMatch.ts index 3af8287..c2e77e2 100644 --- a/src/utils/ownerAndRepoMatch.ts +++ b/src/utils/ownerAndRepoMatch.ts @@ -2,12 +2,12 @@ import { PullRequestReference } from "../OctokitPlus.js"; export function ownerAndRepoMatch( a: PullRequestReference, - b: PullRequestReference + b: PullRequestReference, ) { return ( - a.repo && - b.repo && - a.repo.owner.login === b.repo.owner.login && - a.repo.name === b.repo.name + a.repo + && b.repo + && a.repo.owner.login === b.repo.owner.login + && a.repo.name === b.repo.name ); } diff --git a/src/utils/parseGitRemote.test.ts b/src/utils/parseGitRemote.test.ts index 8f7b5b7..23fa4fd 100644 --- a/src/utils/parseGitRemote.test.ts +++ b/src/utils/parseGitRemote.test.ts @@ -1,175 +1,175 @@ -import { describe, it, expect } from 'vitest'; -import { parseGitRemote } from './getGitRemote.js'; +import { describe, expect, it } from "vitest"; +import { parseGitRemote } from "./getGitRemote.js"; -describe('parseGitRemote', () => { - describe('HTTPS URL parsing', () => { - it('should parse standard GitHub.com HTTPS URL with .git', () => { - const result = parseGitRemote('https://github.com/owner/repo.git'); - expect(result).toEqual({ owner: 'owner', repo: 'repo', host: 'github.com' }); +describe("parseGitRemote", () => { + describe("HTTPS URL parsing", () => { + it("should parse standard GitHub.com HTTPS URL with .git", () => { + const result = parseGitRemote("https://github.com/owner/repo.git"); + expect(result).toEqual({ owner: "owner", repo: "repo", host: "github.com" }); }); - it('should parse standard GitHub.com HTTPS URL without .git', () => { - const result = parseGitRemote('https://github.com/owner/repo'); - expect(result).toEqual({ owner: 'owner', repo: 'repo', host: 'github.com' }); + it("should parse standard GitHub.com HTTPS URL without .git", () => { + const result = parseGitRemote("https://github.com/owner/repo"); + expect(result).toEqual({ owner: "owner", repo: "repo", host: "github.com" }); }); - it('should parse GitHub Enterprise HTTPS URL with .git', () => { - const result = parseGitRemote('https://github.enterprise.com/owner/repo.git'); - expect(result).toEqual({ owner: 'owner', repo: 'repo', host: 'github.enterprise.com' }); + it("should parse GitHub Enterprise HTTPS URL with .git", () => { + const result = parseGitRemote("https://github.enterprise.com/owner/repo.git"); + expect(result).toEqual({ owner: "owner", repo: "repo", host: "github.enterprise.com" }); }); - it('should parse GitHub Enterprise HTTPS URL without .git', () => { - const result = parseGitRemote('https://github.enterprise.com/owner/repo'); - expect(result).toEqual({ owner: 'owner', repo: 'repo', host: 'github.enterprise.com' }); + it("should parse GitHub Enterprise HTTPS URL without .git", () => { + const result = parseGitRemote("https://github.enterprise.com/owner/repo"); + expect(result).toEqual({ owner: "owner", repo: "repo", host: "github.enterprise.com" }); }); - it('should parse custom domain HTTPS URL', () => { - const result = parseGitRemote('https://git.company.internal/team/project.git'); - expect(result).toEqual({ owner: 'team', repo: 'project', host: 'git.company.internal' }); + it("should parse custom domain HTTPS URL", () => { + const result = parseGitRemote("https://git.company.internal/team/project.git"); + expect(result).toEqual({ owner: "team", repo: "project", host: "git.company.internal" }); }); - it('should handle repos with hyphens and underscores in HTTPS URLs', () => { - const result = parseGitRemote('https://github.com/my-org/my_awesome-repo.git'); - expect(result).toEqual({ owner: 'my-org', repo: 'my_awesome-repo', host: 'github.com' }); + it("should handle repos with hyphens and underscores in HTTPS URLs", () => { + const result = parseGitRemote("https://github.com/my-org/my_awesome-repo.git"); + expect(result).toEqual({ owner: "my-org", repo: "my_awesome-repo", host: "github.com" }); }); - it('should handle numeric owner and repo names in HTTPS URLs', () => { - const result = parseGitRemote('https://github.com/user123/repo456.git'); - expect(result).toEqual({ owner: 'user123', repo: 'repo456', host: 'github.com' }); + it("should handle numeric owner and repo names in HTTPS URLs", () => { + const result = parseGitRemote("https://github.com/user123/repo456.git"); + expect(result).toEqual({ owner: "user123", repo: "repo456", host: "github.com" }); }); - it('should handle subdomain with port in HTTPS URLs', () => { - const result = parseGitRemote('https://git.company.com:8080/team/project.git'); - expect(result).toEqual({ owner: 'team', repo: 'project', host: 'git.company.com:8080' }); + it("should handle subdomain with port in HTTPS URLs", () => { + const result = parseGitRemote("https://git.company.com:8080/team/project.git"); + expect(result).toEqual({ owner: "team", repo: "project", host: "git.company.com:8080" }); }); }); - describe('SSH URL parsing', () => { - it('should parse standard GitHub.com SSH URL with .git', () => { - const result = parseGitRemote('git@github.com:owner/repo.git'); - expect(result).toEqual({ owner: 'owner', repo: 'repo', host: 'github.com' }); + describe("SSH URL parsing", () => { + it("should parse standard GitHub.com SSH URL with .git", () => { + const result = parseGitRemote("git@github.com:owner/repo.git"); + expect(result).toEqual({ owner: "owner", repo: "repo", host: "github.com" }); }); - it('should parse standard GitHub.com SSH URL without .git', () => { - const result = parseGitRemote('git@github.com:owner/repo'); - expect(result).toEqual({ owner: 'owner', repo: 'repo', host: 'github.com' }); + it("should parse standard GitHub.com SSH URL without .git", () => { + const result = parseGitRemote("git@github.com:owner/repo"); + expect(result).toEqual({ owner: "owner", repo: "repo", host: "github.com" }); }); - it('should parse GitHub Enterprise SSH URL with .git', () => { - const result = parseGitRemote('git@github.enterprise.com:owner/repo.git'); - expect(result).toEqual({ owner: 'owner', repo: 'repo', host: 'github.enterprise.com' }); + it("should parse GitHub Enterprise SSH URL with .git", () => { + const result = parseGitRemote("git@github.enterprise.com:owner/repo.git"); + expect(result).toEqual({ owner: "owner", repo: "repo", host: "github.enterprise.com" }); }); - it('should parse GitHub Enterprise SSH URL without .git', () => { - const result = parseGitRemote('git@github.enterprise.com:owner/repo'); - expect(result).toEqual({ owner: 'owner', repo: 'repo', host: 'github.enterprise.com' }); + it("should parse GitHub Enterprise SSH URL without .git", () => { + const result = parseGitRemote("git@github.enterprise.com:owner/repo"); + expect(result).toEqual({ owner: "owner", repo: "repo", host: "github.enterprise.com" }); }); - it('should parse custom domain SSH URL', () => { - const result = parseGitRemote('git@git.company.internal:team/project.git'); - expect(result).toEqual({ owner: 'team', repo: 'project', host: 'git.company.internal' }); + it("should parse custom domain SSH URL", () => { + const result = parseGitRemote("git@git.company.internal:team/project.git"); + expect(result).toEqual({ owner: "team", repo: "project", host: "git.company.internal" }); }); - it('should handle repos with hyphens and underscores in SSH URLs', () => { - const result = parseGitRemote('git@github.com:my-org/my_awesome-repo.git'); - expect(result).toEqual({ owner: 'my-org', repo: 'my_awesome-repo', host: 'github.com' }); + it("should handle repos with hyphens and underscores in SSH URLs", () => { + const result = parseGitRemote("git@github.com:my-org/my_awesome-repo.git"); + expect(result).toEqual({ owner: "my-org", repo: "my_awesome-repo", host: "github.com" }); }); - it('should handle numeric owner and repo names in SSH URLs', () => { - const result = parseGitRemote('git@github.com:user123/repo456.git'); - expect(result).toEqual({ owner: 'user123', repo: 'repo456', host: 'github.com' }); + it("should handle numeric owner and repo names in SSH URLs", () => { + const result = parseGitRemote("git@github.com:user123/repo456.git"); + expect(result).toEqual({ owner: "user123", repo: "repo456", host: "github.com" }); }); - it('should return null for SSH URL with custom port (not supported format)', () => { + it("should return null for SSH URL with custom port (not supported format)", () => { // SSH URLs with ports use ssh://git@host:port/path format, not git@host:port/path - const result = parseGitRemote('git@git.company.com:2222/team/project.git'); + const result = parseGitRemote("git@git.company.com:2222/team/project.git"); expect(result).toBeNull(); }); }); - describe('edge cases and error handling', () => { - it('should return null for empty string', () => { - const result = parseGitRemote(''); + describe("edge cases and error handling", () => { + it("should return null for empty string", () => { + const result = parseGitRemote(""); expect(result).toBeNull(); }); - it('should return null for whitespace-only string', () => { - const result = parseGitRemote(' '); + it("should return null for whitespace-only string", () => { + const result = parseGitRemote(" "); expect(result).toBeNull(); }); - it('should return null for undefined input', () => { + it("should return null for undefined input", () => { const result = parseGitRemote(undefined as any); expect(result).toBeNull(); }); - it('should return null for null input', () => { + it("should return null for null input", () => { const result = parseGitRemote(null as any); expect(result).toBeNull(); }); - it('should return null for non-string input', () => { + it("should return null for non-string input", () => { const result = parseGitRemote(123 as any); expect(result).toBeNull(); }); - it('should handle URLs with leading/trailing whitespace', () => { - const result = parseGitRemote(' https://github.com/owner/repo.git '); - expect(result).toEqual({ owner: 'owner', repo: 'repo', host: 'github.com' }); + it("should handle URLs with leading/trailing whitespace", () => { + const result = parseGitRemote(" https://github.com/owner/repo.git "); + expect(result).toEqual({ owner: "owner", repo: "repo", host: "github.com" }); }); - it('should return null for invalid HTTPS URL format', () => { - const result = parseGitRemote('https://github.com/owner'); + it("should return null for invalid HTTPS URL format", () => { + const result = parseGitRemote("https://github.com/owner"); expect(result).toBeNull(); }); - it('should return null for invalid SSH URL format', () => { - const result = parseGitRemote('git@github.com:owner'); + it("should return null for invalid SSH URL format", () => { + const result = parseGitRemote("git@github.com:owner"); expect(result).toBeNull(); }); - it('should return null for malformed URL', () => { - const result = parseGitRemote('not-a-valid-url'); + it("should return null for malformed URL", () => { + const result = parseGitRemote("not-a-valid-url"); expect(result).toBeNull(); }); - it('should return null for HTTP (not HTTPS) URL', () => { - const result = parseGitRemote('http://github.com/owner/repo.git'); + it("should return null for HTTP (not HTTPS) URL", () => { + const result = parseGitRemote("http://github.com/owner/repo.git"); expect(result).toBeNull(); }); - it('should return null for FTP URL', () => { - const result = parseGitRemote('ftp://github.com/owner/repo.git'); + it("should return null for FTP URL", () => { + const result = parseGitRemote("ftp://github.com/owner/repo.git"); expect(result).toBeNull(); }); - it('should return null for URL with too many path segments', () => { - const result = parseGitRemote('https://github.com/owner/repo/extra/path.git'); + it("should return null for URL with too many path segments", () => { + const result = parseGitRemote("https://github.com/owner/repo/extra/path.git"); expect(result).toBeNull(); }); - it('should return null for SSH URL without colon', () => { - const result = parseGitRemote('git@github.com/owner/repo.git'); + it("should return null for SSH URL without colon", () => { + const result = parseGitRemote("git@github.com/owner/repo.git"); expect(result).toBeNull(); }); }); - describe('backwards compatibility', () => { - it('should maintain compatibility with existing github.com URLs', () => { + describe("backwards compatibility", () => { + it("should maintain compatibility with existing github.com URLs", () => { // These are the original test cases that should continue to work - const githubHttps = parseGitRemote('https://github.com/facebook/react.git'); - expect(githubHttps).toEqual({ owner: 'facebook', repo: 'react', host: 'github.com' }); + const githubHttps = parseGitRemote("https://github.com/facebook/react.git"); + expect(githubHttps).toEqual({ owner: "facebook", repo: "react", host: "github.com" }); - const githubSsh = parseGitRemote('git@github.com:facebook/react.git'); - expect(githubSsh).toEqual({ owner: 'facebook', repo: 'react', host: 'github.com' }); + const githubSsh = parseGitRemote("git@github.com:facebook/react.git"); + expect(githubSsh).toEqual({ owner: "facebook", repo: "react", host: "github.com" }); }); - it('should handle real-world repository examples', () => { + it("should handle real-world repository examples", () => { const examples = [ - 'https://github.com/microsoft/vscode.git', - 'git@github.com:nodejs/node.git', - 'https://github.com/vercel/next.js.git', - 'git@github.com:facebook/react.git' + "https://github.com/microsoft/vscode.git", + "git@github.com:nodejs/node.git", + "https://github.com/vercel/next.js.git", + "git@github.com:facebook/react.git", ]; examples.forEach(url => { @@ -182,13 +182,13 @@ describe('parseGitRemote', () => { }); }); - describe('GitHub Enterprise specific tests', () => { - it('should parse enterprise URLs with various domain patterns', () => { + describe("GitHub Enterprise specific tests", () => { + it("should parse enterprise URLs with various domain patterns", () => { const enterpriseUrls = [ - 'https://github.company.com/team/project.git', - 'git@github.enterprise.io:org/repo.git', - 'https://git.internal.corp/dev/app.git', - 'git@code.company.net:department/service.git' + "https://github.company.com/team/project.git", + "git@github.enterprise.io:org/repo.git", + "https://git.internal.corp/dev/app.git", + "git@code.company.net:department/service.git", ]; enterpriseUrls.forEach(url => { @@ -200,12 +200,12 @@ describe('parseGitRemote', () => { }); }); - it('should correctly extract owner and repo from enterprise URLs', () => { - const result1 = parseGitRemote('https://github.mycompany.com/platform-team/core-service.git'); - expect(result1).toEqual({ owner: 'platform-team', repo: 'core-service', host: 'github.mycompany.com' }); + it("should correctly extract owner and repo from enterprise URLs", () => { + const result1 = parseGitRemote("https://github.mycompany.com/platform-team/core-service.git"); + expect(result1).toEqual({ owner: "platform-team", repo: "core-service", host: "github.mycompany.com" }); - const result2 = parseGitRemote('git@git.enterprise.local:backend/user-api.git'); - expect(result2).toEqual({ owner: 'backend', repo: 'user-api', host: 'git.enterprise.local' }); + const result2 = parseGitRemote("git@git.enterprise.local:backend/user-api.git"); + expect(result2).toEqual({ owner: "backend", repo: "user-api", host: "git.enterprise.local" }); }); }); }); diff --git a/tsconfig.json b/tsconfig.json index 7ce80bf..45011eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,4 +28,4 @@ "node_modules", "lib" ] -} \ No newline at end of file +} diff --git a/vitest.config.ts b/vitest.config.ts index 6e0eb89..9411dcc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,34 +1,34 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - environment: 'node', + environment: "node", globals: true, - setupFiles: ['./src/test/setup.ts'], - include: ['src/**/*.test.ts', 'src/**/*.spec.ts'], - exclude: ['lib/**', 'node_modules/**', 'dist/**'], + setupFiles: ["./src/test/setup.ts"], + include: ["src/**/*.test.ts", "src/**/*.spec.ts"], + exclude: ["lib/**", "node_modules/**", "dist/**"], coverage: { - provider: 'v8', - reporter: ['text', 'html', 'lcov'], - include: ['src/**/*.ts'], - exclude: ['src/**/*.test.ts', 'src/**/*.spec.ts'], + provider: "v8", + reporter: ["text", "html", "lcov"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.test.ts", "src/**/*.spec.ts"], thresholds: { global: { branches: 80, functions: 80, lines: 80, - statements: 80 - } - } + statements: 80, + }, + }, }, typecheck: { - enabled: true - } + enabled: true, + }, }, resolve: { alias: { // Handle ESM imports with .js extensions - '~': new URL('./src', import.meta.url).pathname - } - } -}); \ No newline at end of file + "~": new URL("./src", import.meta.url).pathname, + }, + }, +});