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 @@
-
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,
+ },
+ },
+});