diff --git a/README.md b/README.md index bf7fc72..d77223c 100644 --- a/README.md +++ b/README.md @@ -72,15 +72,7 @@ For safety, unregistered directories inside the main worktree are refused. worktree-remove ``` -Example: - -```bash -# remove a single worktree -worktree-remove feature/login-form - -# remove multiple worktrees at once -worktree-remove feature/login-form feature/signup -``` +The targeting modes above map onto concrete invocations; see [Examples](#examples) for the full command list. Detached HEAD example: @@ -94,13 +86,6 @@ Sibling directory name example: worktree-remove my-app-test-29 ``` -Current worktree example: - -```bash -# remove the worktree you are currently in (no need to know its name) -worktree-remove . --yes -``` - ## Options - `-i, --interactive` interactively select worktrees to remove (multi-select) diff --git a/package.json b/package.json index ce2416b..369cee2 100644 --- a/package.json +++ b/package.json @@ -65,17 +65,17 @@ }, "devDependencies": { "@total-typescript/ts-reset": "^0.6.1", - "@types/node": "^25.9.2", + "@types/node": "^25.9.3", "@typescript/native-preview": "beta", "@vitest/coverage-v8": "^4.1.8", - "fta-check": "^1.5.3", + "fta-check": "^1.5.4", "fta-cli": "^3.0.0", "knip": "^6.16.1", "oxfmt": "^0.54.0", "oxlint": "^1.69.0", - "oxlint-config-j4k": "2.7.2", + "oxlint-config-j4k": "2.7.4", "oxlint-tsgolint": "^0.23.0", - "semantic-release": "^25.0.3", + "semantic-release": "^25.0.5", "vitest": "^4.1.8" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0cb55b..99e32b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 15.0.0(commander@15.0.0) '@inquirer/prompts': specifier: ^8.5.2 - version: 8.5.2(@types/node@25.9.2) + version: 8.5.2(@types/node@25.9.3) chalk: specifier: ^5.6.2 version: 5.6.2 @@ -31,8 +31,8 @@ importers: specifier: ^0.6.1 version: 0.6.1 '@types/node': - specifier: ^25.9.2 - version: 25.9.2 + specifier: ^25.9.3 + version: 25.9.3 '@typescript/native-preview': specifier: beta version: 7.0.0-dev.20260421.2 @@ -40,8 +40,8 @@ importers: specifier: ^4.1.8 version: 4.1.8(vitest@4.1.8) fta-check: - specifier: ^1.5.3 - version: 1.5.3(fta-cli@3.0.0) + specifier: ^1.5.4 + version: 1.5.4(fta-cli@3.0.0) fta-cli: specifier: ^3.0.0 version: 3.0.0 @@ -55,17 +55,17 @@ importers: specifier: ^1.69.0 version: 1.69.0(oxlint-tsgolint@0.23.0) oxlint-config-j4k: - specifier: 2.7.2 - version: 2.7.2 + specifier: 2.7.4 + version: 2.7.4 oxlint-tsgolint: specifier: ^0.23.0 version: 0.23.0 semantic-release: - specifier: ^25.0.3 - version: 25.0.3(typescript@6.0.3) + specifier: ^25.0.5 + version: 25.0.5(typescript@6.0.3) vitest: specifier: ^4.1.8 - version: 4.1.8(@types/node@25.9.2)(@vitest/coverage-v8@4.1.8)(vite@7.3.1(@types/node@25.9.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.8(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(vite@7.3.1(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0)) packages: @@ -429,8 +429,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -1010,8 +1010,8 @@ packages: resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} engines: {node: '>=12.22.0'} - '@pnpm/npm-conf@3.0.2': - resolution: {integrity: sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==} + '@pnpm/npm-conf@3.0.3': + resolution: {integrity: sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==} engines: {node: '>=12'} '@rollup/rollup-android-arm-eabi@4.61.1': @@ -1229,8 +1229,8 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/node@25.9.2': - resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==} + '@types/node@25.9.3': + resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -1360,8 +1360,8 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-v8-to-istanbul@1.0.3: - resolution: {integrity: sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==} + ast-v8-to-istanbul@1.0.4: + resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==} before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} @@ -1660,8 +1660,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - fta-check@1.5.3: - resolution: {integrity: sha512-VSqv+1K9AJju+LM40GXuX2iWKJKkXb8fX189Njx0LybkHefW5954Ue1gyX0npGdGpOskNKMxl8xyP63/ClG6tg==} + fta-check@1.5.4: + resolution: {integrity: sha512-xT18rXFhRiJu/nKM+0fifg3uz5IfqEHCq2BLRyqUD7r/LGBDqJr7GvTgjm2umF9gp5ma4r/Ib4gRJsE6fcOoMg==} engines: {node: '>=24.0.0'} hasBin: true peerDependencies: @@ -2066,8 +2066,8 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} - npm@11.16.0: - resolution: {integrity: sha512-A74XL8OxmcegZDMWPkWb5bEQppg8HdYwW3rBD2sPoS4UQHVajfaxBkqyzLeJ3wR0kZ+5xoTjItxXaF7eIXUsyw==} + npm@11.17.0: + resolution: {integrity: sha512-PurxiZexEHDTE4SSaLI3ZrnbAGiZfeyUcQcxcP5D+hfytNAze/D1IzDuInTn9XVLIbAQUnQuSPXJx02LHjLvQw==} engines: {node: ^20.17.0 || >=22.9.0} hasBin: true bundledDependencies: @@ -2141,8 +2141,8 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - obug@2.1.2: - resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} once@1.4.0: @@ -2180,8 +2180,8 @@ packages: vite-plus: optional: true - oxlint-config-j4k@2.7.2: - resolution: {integrity: sha512-Yyn8cL6Ju9q4MxhRB+yH314xbxqI+oCY0s+6bHz85Rls/gfCGTvq0hwCVT/XkEJBbCbnpPR7TsE9eUvx2Ee3Fg==} + oxlint-config-j4k@2.7.4: + resolution: {integrity: sha512-DY/jpbMY4sRHM/ZlW0zOBd8JmgpUINkL3S7GaLbqXCL6zAmWOhJt0GTrUSMpJUIm+R4xl3SB05psH/E0eI4gqQ==} engines: {node: '>=24.0.0'} oxlint-tsgolint@0.23.0: @@ -2425,8 +2425,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - semantic-release@25.0.3: - resolution: {integrity: sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==} + semantic-release@25.0.5: + resolution: {integrity: sha512-mn61SUJwtM8ThrWn2WmgLVpwVJeG/hPSupua1psdMoufmwRIPyvRLkRkL0JDXkP67OntlLWUYnBnfVc8EDO3/g==} engines: {node: ^22.14.0 || >= 24.10.0} hasBin: true @@ -2434,8 +2434,8 @@ packages: resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} engines: {node: '>=12'} - semver@7.8.3: - resolution: {integrity: sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==} + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} hasBin: true @@ -3023,122 +3023,122 @@ snapshots: '@inquirer/ansi@2.0.7': {} - '@inquirer/checkbox@5.2.1(@types/node@25.9.2)': + '@inquirer/checkbox@5.2.1(@types/node@25.9.3)': dependencies: '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@25.9.2) + '@inquirer/core': 11.2.1(@types/node@25.9.3) '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.2) + '@inquirer/type': 4.0.7(@types/node@25.9.3) optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 - '@inquirer/confirm@6.1.1(@types/node@25.9.2)': + '@inquirer/confirm@6.1.1(@types/node@25.9.3)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.2) - '@inquirer/type': 4.0.7(@types/node@25.9.2) + '@inquirer/core': 11.2.1(@types/node@25.9.3) + '@inquirer/type': 4.0.7(@types/node@25.9.3) optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 - '@inquirer/core@11.2.1(@types/node@25.9.2)': + '@inquirer/core@11.2.1(@types/node@25.9.3)': dependencies: '@inquirer/ansi': 2.0.7 '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.2) + '@inquirer/type': 4.0.7(@types/node@25.9.3) cli-width: 4.1.0 fast-wrap-ansi: 0.2.2 mute-stream: 3.0.0 signal-exit: 4.1.0 optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 - '@inquirer/editor@5.2.2(@types/node@25.9.2)': + '@inquirer/editor@5.2.2(@types/node@25.9.3)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.2) - '@inquirer/external-editor': 3.0.3(@types/node@25.9.2) - '@inquirer/type': 4.0.7(@types/node@25.9.2) + '@inquirer/core': 11.2.1(@types/node@25.9.3) + '@inquirer/external-editor': 3.0.3(@types/node@25.9.3) + '@inquirer/type': 4.0.7(@types/node@25.9.3) optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 - '@inquirer/expand@5.1.1(@types/node@25.9.2)': + '@inquirer/expand@5.1.1(@types/node@25.9.3)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.2) - '@inquirer/type': 4.0.7(@types/node@25.9.2) + '@inquirer/core': 11.2.1(@types/node@25.9.3) + '@inquirer/type': 4.0.7(@types/node@25.9.3) optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 - '@inquirer/external-editor@3.0.3(@types/node@25.9.2)': + '@inquirer/external-editor@3.0.3(@types/node@25.9.3)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 '@inquirer/figures@2.0.7': {} - '@inquirer/input@5.1.2(@types/node@25.9.2)': + '@inquirer/input@5.1.2(@types/node@25.9.3)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.2) - '@inquirer/type': 4.0.7(@types/node@25.9.2) + '@inquirer/core': 11.2.1(@types/node@25.9.3) + '@inquirer/type': 4.0.7(@types/node@25.9.3) optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 - '@inquirer/number@4.1.1(@types/node@25.9.2)': + '@inquirer/number@4.1.1(@types/node@25.9.3)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.2) - '@inquirer/type': 4.0.7(@types/node@25.9.2) + '@inquirer/core': 11.2.1(@types/node@25.9.3) + '@inquirer/type': 4.0.7(@types/node@25.9.3) optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 - '@inquirer/password@5.1.1(@types/node@25.9.2)': + '@inquirer/password@5.1.1(@types/node@25.9.3)': dependencies: '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@25.9.2) - '@inquirer/type': 4.0.7(@types/node@25.9.2) + '@inquirer/core': 11.2.1(@types/node@25.9.3) + '@inquirer/type': 4.0.7(@types/node@25.9.3) optionalDependencies: - '@types/node': 25.9.2 - - '@inquirer/prompts@8.5.2(@types/node@25.9.2)': - dependencies: - '@inquirer/checkbox': 5.2.1(@types/node@25.9.2) - '@inquirer/confirm': 6.1.1(@types/node@25.9.2) - '@inquirer/editor': 5.2.2(@types/node@25.9.2) - '@inquirer/expand': 5.1.1(@types/node@25.9.2) - '@inquirer/input': 5.1.2(@types/node@25.9.2) - '@inquirer/number': 4.1.1(@types/node@25.9.2) - '@inquirer/password': 5.1.1(@types/node@25.9.2) - '@inquirer/rawlist': 5.3.1(@types/node@25.9.2) - '@inquirer/search': 4.2.1(@types/node@25.9.2) - '@inquirer/select': 5.2.1(@types/node@25.9.2) + '@types/node': 25.9.3 + + '@inquirer/prompts@8.5.2(@types/node@25.9.3)': + dependencies: + '@inquirer/checkbox': 5.2.1(@types/node@25.9.3) + '@inquirer/confirm': 6.1.1(@types/node@25.9.3) + '@inquirer/editor': 5.2.2(@types/node@25.9.3) + '@inquirer/expand': 5.1.1(@types/node@25.9.3) + '@inquirer/input': 5.1.2(@types/node@25.9.3) + '@inquirer/number': 4.1.1(@types/node@25.9.3) + '@inquirer/password': 5.1.1(@types/node@25.9.3) + '@inquirer/rawlist': 5.3.1(@types/node@25.9.3) + '@inquirer/search': 4.2.1(@types/node@25.9.3) + '@inquirer/select': 5.2.1(@types/node@25.9.3) optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 - '@inquirer/rawlist@5.3.1(@types/node@25.9.2)': + '@inquirer/rawlist@5.3.1(@types/node@25.9.3)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.2) - '@inquirer/type': 4.0.7(@types/node@25.9.2) + '@inquirer/core': 11.2.1(@types/node@25.9.3) + '@inquirer/type': 4.0.7(@types/node@25.9.3) optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 - '@inquirer/search@4.2.1(@types/node@25.9.2)': + '@inquirer/search@4.2.1(@types/node@25.9.3)': dependencies: - '@inquirer/core': 11.2.1(@types/node@25.9.2) + '@inquirer/core': 11.2.1(@types/node@25.9.3) '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.2) + '@inquirer/type': 4.0.7(@types/node@25.9.3) optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 - '@inquirer/select@5.2.1(@types/node@25.9.2)': + '@inquirer/select@5.2.1(@types/node@25.9.3)': dependencies: '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@25.9.2) + '@inquirer/core': 11.2.1(@types/node@25.9.3) '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@25.9.2) + '@inquirer/type': 4.0.7(@types/node@25.9.3) optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 - '@inquirer/type@4.0.7(@types/node@25.9.2)': + '@inquirer/type@4.0.7(@types/node@25.9.3)': optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 '@jridgewell/resolve-uri@3.1.2': {} @@ -3149,7 +3149,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 @@ -3280,7 +3280,7 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@oxc-parser/binding-win32-arm64-msvc@0.133.0': @@ -3346,7 +3346,7 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@oxc-resolver/binding-win32-arm64-msvc@11.20.0': @@ -3493,7 +3493,7 @@ snapshots: dependencies: graceful-fs: 4.2.10 - '@pnpm/npm-conf@3.0.2': + '@pnpm/npm-conf@3.0.3': dependencies: '@pnpm/config.env-replace': 1.1.0 '@pnpm/network.ca-file': 1.0.2 @@ -3576,7 +3576,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.3(typescript@6.0.3))': + '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.5(typescript@6.0.3))': dependencies: conventional-changelog-angular: 8.3.1 conventional-changelog-writer: 8.4.0 @@ -3586,13 +3586,13 @@ snapshots: import-from-esm: 2.0.0 lodash-es: 4.18.1 micromatch: 4.0.8 - semantic-release: 25.0.3(typescript@6.0.3) + semantic-release: 25.0.5(typescript@6.0.3) transitivePeerDependencies: - supports-color '@semantic-release/error@4.0.0': {} - '@semantic-release/github@12.0.8(semantic-release@25.0.3(typescript@6.0.3))': + '@semantic-release/github@12.0.8(semantic-release@25.0.5(typescript@6.0.3))': dependencies: '@octokit/core': 7.0.6 '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) @@ -3608,7 +3608,7 @@ snapshots: lodash-es: 4.18.1 mime: 4.1.0 p-filter: 4.1.0 - semantic-release: 25.0.3(typescript@6.0.3) + semantic-release: 25.0.5(typescript@6.0.3) tinyglobby: 0.2.17 undici: 7.27.2 url-join: 5.0.0 @@ -3616,7 +3616,7 @@ snapshots: - kerberos - supports-color - '@semantic-release/npm@13.1.5(semantic-release@25.0.3(typescript@6.0.3))': + '@semantic-release/npm@13.1.5(semantic-release@25.0.5(typescript@6.0.3))': dependencies: '@actions/core': 3.0.1 '@semantic-release/error': 4.0.0 @@ -3627,15 +3627,15 @@ snapshots: lodash-es: 4.18.1 nerf-dart: 1.0.0 normalize-url: 9.0.1 - npm: 11.16.0 + npm: 11.17.0 rc: 1.2.8 read-pkg: 10.1.0 registry-auth-token: 5.1.1 - semantic-release: 25.0.3(typescript@6.0.3) - semver: 7.8.3 + semantic-release: 25.0.5(typescript@6.0.3) + semver: 7.8.4 tempy: 3.2.0 - '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.3(typescript@6.0.3))': + '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.5(typescript@6.0.3))': dependencies: conventional-changelog-angular: 8.3.1 conventional-changelog-writer: 8.4.0 @@ -3645,7 +3645,7 @@ snapshots: import-from-esm: 2.0.0 lodash-es: 4.18.1 read-package-up: 11.0.0 - semantic-release: 25.0.3(typescript@6.0.3) + semantic-release: 25.0.5(typescript@6.0.3) transitivePeerDependencies: - supports-color @@ -3683,7 +3683,7 @@ snapshots: '@types/estree@1.0.9': {} - '@types/node@25.9.2': + '@types/node@25.9.3': dependencies: undici-types: 7.24.6 @@ -3724,15 +3724,15 @@ snapshots: dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.8 - ast-v8-to-istanbul: 1.0.3 + ast-v8-to-istanbul: 1.0.4 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 magicast: 0.5.3 - obug: 2.1.2 + obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.8(@types/node@25.9.2)(@vitest/coverage-v8@4.1.8)(vite@7.3.1(@types/node@25.9.2)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.8(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(vite@7.3.1(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/expect@4.1.8': dependencies: @@ -3743,13 +3743,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.8(vite@7.3.1(@types/node@25.9.2)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.8(vite@7.3.1(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.9.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 7.3.1(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.8': dependencies: @@ -3810,7 +3810,7 @@ snapshots: assertion-error@2.0.1: {} - ast-v8-to-istanbul@1.0.3: + ast-v8-to-istanbul@1.0.4: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 @@ -3918,7 +3918,7 @@ snapshots: conventional-commits-filter: 5.0.0 handlebars: 4.7.9 meow: 13.2.0 - semver: 7.8.3 + semver: 7.8.4 conventional-commits-filter@5.0.0: {} @@ -4141,7 +4141,7 @@ snapshots: fsevents@2.3.3: optional: true - fta-check@1.5.3(fta-cli@3.0.0): + fta-check@1.5.4(fta-cli@3.0.0): dependencies: '@commander-js/extra-typings': 14.0.0(commander@14.0.3) commander: 14.0.3 @@ -4424,7 +4424,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.8.3 + semver: 7.8.4 marked-terminal@7.3.0(marked@15.0.12): dependencies: @@ -4492,13 +4492,13 @@ snapshots: normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 - semver: 7.8.3 + semver: 7.8.4 validate-npm-package-license: 3.0.4 normalize-package-data@8.0.0: dependencies: hosted-git-info: 9.0.3 - semver: 7.8.3 + semver: 7.8.4 validate-npm-package-license: 3.0.4 normalize-url@9.0.1: {} @@ -4516,11 +4516,11 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 - npm@11.16.0: {} + npm@11.17.0: {} object-assign@4.1.1: {} - obug@2.1.2: {} + obug@2.1.3: {} once@1.4.0: dependencies: @@ -4607,7 +4607,7 @@ snapshots: '@oxfmt/binding-win32-ia32-msvc': 0.54.0 '@oxfmt/binding-win32-x64-msvc': 0.54.0 - oxlint-config-j4k@2.7.2: {} + oxlint-config-j4k@2.7.4: {} oxlint-tsgolint@0.23.0: optionalDependencies: @@ -4812,7 +4812,7 @@ snapshots: registry-auth-token@5.1.1: dependencies: - '@pnpm/npm-conf': 3.0.2 + '@pnpm/npm-conf': 3.0.3 require-directory@2.1.1: {} @@ -4863,13 +4863,13 @@ snapshots: safer-buffer@2.1.2: {} - semantic-release@25.0.3(typescript@6.0.3): + semantic-release@25.0.5(typescript@6.0.3): dependencies: - '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.3(typescript@6.0.3)) + '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.5(typescript@6.0.3)) '@semantic-release/error': 4.0.0 - '@semantic-release/github': 12.0.8(semantic-release@25.0.3(typescript@6.0.3)) - '@semantic-release/npm': 13.1.5(semantic-release@25.0.3(typescript@6.0.3)) - '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.3(typescript@6.0.3)) + '@semantic-release/github': 12.0.8(semantic-release@25.0.5(typescript@6.0.3)) + '@semantic-release/npm': 13.1.5(semantic-release@25.0.5(typescript@6.0.3)) + '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.5(typescript@6.0.3)) aggregate-error: 5.0.0 cosmiconfig: 9.0.2(typescript@6.0.3) debug: 4.4.3 @@ -4890,7 +4890,7 @@ snapshots: p-reduce: 3.0.0 read-package-up: 12.0.0 resolve-from: 5.0.0 - semver: 7.8.3 + semver: 7.8.4 signale: 1.4.0 yargs: 18.0.0 transitivePeerDependencies: @@ -4900,7 +4900,7 @@ snapshots: semver-regex@4.0.5: {} - semver@7.8.3: {} + semver@7.8.4: {} shebang-command@2.0.0: dependencies: @@ -5133,7 +5133,7 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite@7.3.1(@types/node@25.9.2)(jiti@2.7.0)(yaml@2.9.0): + vite@7.3.1(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) @@ -5142,15 +5142,15 @@ snapshots: rollup: 4.61.1 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 fsevents: 2.3.3 jiti: 2.7.0 yaml: 2.9.0 - vitest@4.1.8(@types/node@25.9.2)(@vitest/coverage-v8@4.1.8)(vite@7.3.1(@types/node@25.9.2)(jiti@2.7.0)(yaml@2.9.0)): + vitest@4.1.8(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(vite@7.3.1(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@7.3.1(@types/node@25.9.2)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.8(vite@7.3.1(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.8 '@vitest/runner': 4.1.8 '@vitest/snapshot': 4.1.8 @@ -5159,7 +5159,7 @@ snapshots: es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 - obug: 2.1.2 + obug: 2.1.3 pathe: 2.0.3 picomatch: 4.0.4 std-env: 4.1.0 @@ -5167,10 +5167,10 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@25.9.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 7.3.1(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 '@vitest/coverage-v8': 4.1.8(vitest@4.1.8) transitivePeerDependencies: - msw diff --git a/src/cli.ts b/src/cli.ts old mode 100755 new mode 100644 index 9c11354..498fc0a --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env -S node --experimental-strip-types - import { Command } from "@commander-js/extra-typings"; import chalk from "chalk"; import packageJson from "../package.json" with { type: "json" }; @@ -61,7 +59,8 @@ const program = new Command() output.error( "Interactive selection is disabled in non-interactive mode. Provide a target or re-run without CI/--no-interactive.", ); - process.exit(1); + process.exitCode = 1; + return; } if (options.interactive && targets.length > 0) { @@ -74,13 +73,14 @@ const program = new Command() if (targets.length === 0 && !interactiveSelection) { output.error("Missing target worktree. Provide a target or use --interactive."); - process.exit(1); + process.exitCode = 1; + return; } const selectedTargets = interactiveSelection ? await selectWorktrees(output) : targets; if (selectedTargets.length === 0) { - process.exit(0); + return; } await removeBatch(selectedTargets, { @@ -90,7 +90,7 @@ const program = new Command() } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); console.error(chalk.red("Error:"), message); - process.exit(1); + process.exitCode = 1; } }); diff --git a/src/cli/remove-batch.ts b/src/cli/remove-batch.ts index 6e50d89..c4b1b68 100644 --- a/src/cli/remove-batch.ts +++ b/src/cli/remove-batch.ts @@ -10,6 +10,7 @@ import { prepareCwdSwitch } from "./handle-cwd-switch.js"; import type { OutputWriter } from "./output-writer.js"; import { prefixOutput } from "./output-writer.js"; import { reportBatchResults } from "./report-batch-results.js"; +import type { BatchResultEntry } from "./report-batch-results.js"; import { resolveBatchTargets, type ResolvedTarget } from "./resolve-batch-targets.js"; import { getWorktreeInfo } from "../git/get-worktree-info.js"; import { exitWithMessage } from "../git/git-helpers.js"; @@ -34,7 +35,8 @@ export async function removeBatch(targets: string[], options: BatchOptions): Pro worktrees, }); - if (resolved.length === 0) { + const [firstTarget] = resolved; + if (!firstTarget) { exitWithMessage("No valid targets to remove."); } @@ -65,14 +67,9 @@ export async function removeBatch(targets: string[], options: BatchOptions): Pro // Phase 2: Confirmation const isSingleTarget = resolved.length === 1; - const firstTarget = resolved[0]; - - // For a single target, show the same compact prompt as the old single-target flow. - // For multiple targets, show a numbered list. - const confirmationMessage = - isSingleTarget && firstTarget - ? formatSingleConfirmation(firstTarget) - : formatBatchConfirmation(resolved); + const confirmationMessage = isSingleTarget + ? formatSingleConfirmation(firstTarget) + : formatBatchConfirmation(resolved); const performCwdSwitch = prepareCwdSwitch({ targets: resolved.map((r) => ({ @@ -100,49 +97,47 @@ export async function removeBatch(targets: string[], options: BatchOptions): Pro // Phase 3: Remove targets in parallel (concurrency 4) const limit = pLimit(4); - const results = await Promise.allSettled( - resolved.map((r) => - limit(() => - performWorktreeRemoval({ - status: r.displayInfo.status, - targetDirectoryName: r.displayInfo.targetDirectoryName, - targetPath: r.targetPath, - mainPath, - registeredPath: r.registeredPath, - directoryExistedInitially: r.directoryExists, - policy, - output: isSingleTarget ? output : prefixOutput(output, r.displayInfo.targetDirectoryName), - // Suppress the interactive trash-failure prompt for multi-target - // batches to prevent concurrent readline races on stdin. The user - // already confirmed removal in Phase 2; if trash fails without - // --force, the target is reported as failed instead of silently - // proceeding with a destructive git unregister. - skipTrashFailurePrompt: !isSingleTarget, - }), - ), + const settled = await Promise.all( + resolved.map((target) => + limit(async (): Promise => { + const name = target.displayInfo.targetDirectoryName; + try { + const value = await performWorktreeRemoval({ + status: target.displayInfo.status, + targetDirectoryName: name, + targetPath: target.targetPath, + mainPath, + registeredPath: target.registeredPath, + directoryExistedInitially: target.directoryExists, + policy, + output: isSingleTarget ? output : prefixOutput(output, name), + // Suppress the interactive trash-failure prompt for multi-target + // batches to prevent concurrent readline races on stdin. The user + // already confirmed removal in Phase 2; if trash fails without + // --force, the target is reported as failed instead of silently + // proceeding with a destructive git unregister. + skipTrashFailurePrompt: !isSingleTarget, + }); + return { name, result: { status: "fulfilled", value } }; + } catch (error) { + return { name, result: { status: "rejected", reason: error } }; + } + }), ), ); // Phase 4: Report summary (skip for single targets — performWorktreeRemoval // already reports its own outcome, but still check exit code) if (isSingleTarget) { - const singleResult = results[0]; - if ( - !singleResult || - singleResult.status === "rejected" || - singleResult.value.status === "failed" - ) { - exitWithMessage( - `Failed to remove '${firstTarget?.displayInfo.targetDirectoryName ?? "target"}'.`, - ); + const failed = settled.some( + (entry) => entry.result.status === "rejected" || entry.result.value.status === "failed", + ); + if (failed) { + exitWithMessage(`Failed to remove '${firstTarget.displayInfo.targetDirectoryName}'.`); } // "cancelled" — user declined a prompt; exit cleanly (code 0) } else { - const entries = results.map((result, index) => ({ - name: resolved[index]?.displayInfo.targetDirectoryName ?? "unknown target", - result, - })); - reportBatchResults(entries, { dryRun: policy.dryRun, output }); + reportBatchResults(settled, { dryRun: policy.dryRun, output }); } } diff --git a/src/cli/report-batch-results.ts b/src/cli/report-batch-results.ts index f9e4a06..e6c945f 100644 --- a/src/cli/report-batch-results.ts +++ b/src/cli/report-batch-results.ts @@ -2,7 +2,7 @@ import type { PerformWorktreeRemovalResult } from "../worktree/perform-worktree- import { exitWithMessage } from "../git/git-helpers.js"; import type { OutputWriter } from "./output-writer.js"; -interface BatchResultEntry { +export interface BatchResultEntry { name: string; result: PromiseSettledResult; } @@ -12,12 +12,12 @@ export function reportBatchResults( options: { dryRun: boolean; output: OutputWriter }, ): void { const { dryRun, output } = options; - const succeeded: string[] = []; + let succeededCount = 0; const failed: string[] = []; for (const { name, result } of entries) { if (result.status === "fulfilled" && result.value.status === "ok") { - succeeded.push(name); + succeededCount += 1; } else { failed.push(name); if (result.status === "rejected") { @@ -31,7 +31,7 @@ export function reportBatchResults( } const verb = dryRun ? "Would remove" : "Removed"; - output.warn(`${verb} ${succeeded.length} of ${entries.length} worktrees.`); + output.warn(`${verb} ${succeededCount} of ${entries.length} worktrees.`); if (failed.length > 0) { exitWithMessage(`Failed: ${failed.join(", ")}`); } diff --git a/src/fs/check-directory-exists.test.ts b/src/fs/check-directory-exists.test.ts index 0c8b214..d0584d2 100644 --- a/src/fs/check-directory-exists.test.ts +++ b/src/fs/check-directory-exists.test.ts @@ -1,34 +1,31 @@ -import { describe, it, expect, vi } from "vitest"; -import type { Stats } from "node:fs"; -import * as fs from "node:fs/promises"; +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; import { directoryExists } from "./check-directory-exists.js"; -vi.mock("node:fs/promises"); - describe("directoryExists", () => { - it("should return true for an existing directory", async () => { - vi.mocked(fs.stat).mockResolvedValueOnce({ - isDirectory: () => true, - } as Stats); + let tempDir: string; - const result = await directoryExists("/path/to/existing/dir"); - expect(result).toBe(true); - expect(fs.stat).toHaveBeenCalledWith("/path/to/existing/dir"); + beforeAll(async () => { + tempDir = await mkdtemp(path.join(tmpdir(), "worktree-remove-dir-exists-")); + await mkdir(path.join(tempDir, "subdir")); + await writeFile(path.join(tempDir, "file.txt"), "content"); }); - it("should return false for a non-existent path", async () => { - vi.mocked(fs.stat).mockRejectedValueOnce(new Error("ENOENT")); + afterAll(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); - const result = await directoryExists("/path/to/non-existent"); - expect(result).not.toBe(true); + it("returns true for an existing directory", async () => { + await expect(directoryExists(path.join(tempDir, "subdir"))).resolves.toBe(true); }); - it("should return false for a file (not a directory)", async () => { - vi.mocked(fs.stat).mockResolvedValueOnce({ - isDirectory: () => false, - } as Stats); + it("returns false for a file", async () => { + await expect(directoryExists(path.join(tempDir, "file.txt"))).resolves.toBe(false); + }); - const result = await directoryExists("/path/to/file.txt"); - expect(result).not.toBe(true); + it("returns false for a non-existent path", async () => { + await expect(directoryExists(path.join(tempDir, "does-not-exist"))).resolves.toBe(false); }); }); diff --git a/src/fs/check-directory-exists.ts b/src/fs/check-directory-exists.ts index 936b3ed..8f42547 100644 --- a/src/fs/check-directory-exists.ts +++ b/src/fs/check-directory-exists.ts @@ -1,7 +1,3 @@ -/** - * Check if a directory exists - */ - import * as fs from "node:fs/promises"; export async function directoryExists(path: string): Promise { diff --git a/src/fs/trash-directory.ts b/src/fs/trash-directory.ts index 40d117d..6c3a1b1 100644 --- a/src/fs/trash-directory.ts +++ b/src/fs/trash-directory.ts @@ -1,7 +1,3 @@ -/** - * Move a directory to the system trash - */ - import trash from "trash"; type TrashResult = { ok: true } | { ok: false; reason: string }; diff --git a/src/git/check-uncommitted-changes.ts b/src/git/check-uncommitted-changes.ts index aa3be1b..b5a62e7 100644 --- a/src/git/check-uncommitted-changes.ts +++ b/src/git/check-uncommitted-changes.ts @@ -1,7 +1,3 @@ -/** - * Check if a worktree has uncommitted changes - */ - import { git } from "./git-helpers.js"; export function hasUncommittedChanges(worktreePath: string): boolean { diff --git a/src/git/get-worktree-info.ts b/src/git/get-worktree-info.ts index 3464519..3fe9a5b 100644 --- a/src/git/get-worktree-info.ts +++ b/src/git/get-worktree-info.ts @@ -1,7 +1,3 @@ -/** - * Get worktree information from git - */ - import { git, exitWithMessage } from "./git-helpers.js"; import { normalizeGitPath } from "./normalize-git-path.js"; import { parseWorktreeListPorcelain } from "./parse-worktree-list.js"; diff --git a/src/git/git-helpers.ts b/src/git/git-helpers.ts index d29b1e4..2a2bedf 100644 --- a/src/git/git-helpers.ts +++ b/src/git/git-helpers.ts @@ -61,30 +61,6 @@ export function git(...arguments_: [...string[], { cwd?: string }] | string[]): return result.stdout.trim(); } -/** - * Normalize a Git branch reference to a simple branch name. - * - * This helper trims surrounding whitespace and removes common prefixes so the - * resulting string matches the short local branch name used by refs under - * `refs/heads/`. - * - * Behavior: - * - Trims surrounding whitespace. - * - Strips `refs/heads/`. - * - Strips `refs/remotes/origin/`. - * - Strips `remotes/origin/`. - * - Strips `origin/`. - * - * It deliberately does not lowercase or modify path separators so branch names - * like `feature/subtask` remain intact. - * - * Examples: - * - normalizeBranchName("feature/login") → "feature/login" - * - normalizeBranchName(" origin/feature/login ") → "feature/login" - * - normalizeBranchName("refs/heads/feature/login") → "feature/login" - * - normalizeBranchName("refs/remotes/origin/feature/login") → "feature/login" - * - normalizeBranchName("remotes/origin/feature/login") → "feature/login" - */ export function normalizeBranchName(name: string): string { const trimmed = name.trim(); return trimmed @@ -94,20 +70,12 @@ export function normalizeBranchName(name: string): string { .replace(/^origin\//u, ""); } -/** - * Print an error message and exit the process with status 1. - */ export function exitWithMessage(message: string): never { console.error(chalk.red(message)); // eslint-disable-next-line unicorn/no-process-exit process.exit(1); } -/** - * Prompts user for yes/no confirmation - * @param message The message to display to the user - * @returns Promise - true if user confirms, false otherwise - */ export async function confirm(message: string): Promise { const rl = readline.createInterface({ input: process.stdin,