diff --git a/.commitlintrc.js b/.commitlintrc.js
new file mode 100644
index 0000000..5b0b1a5
--- /dev/null
+++ b/.commitlintrc.js
@@ -0,0 +1,10 @@
+/* This file is automatically added by @npmcli/template-oss. Do not edit. */
+
+module.exports = {
+ extends: ['@commitlint/config-conventional'],
+ rules: {
+ 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']],
+ 'header-max-length': [2, 'always', 80],
+ 'subject-case': [0, 'always', ['lower-case', 'sentence-case', 'start-case']],
+ },
+}
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..5db9f81
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,17 @@
+/* This file is automatically added by @npmcli/template-oss. Do not edit. */
+
+'use strict'
+
+const { readdirSync: readdir } = require('fs')
+
+const localConfigs = readdir(__dirname)
+ .filter((file) => file.startsWith('.eslintrc.local.'))
+ .map((file) => `./${file}`)
+
+module.exports = {
+ root: true,
+ extends: [
+ '@npmcli',
+ ...localConfigs,
+ ],
+}
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..2c54b0d
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,3 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+* @npm/cli-team
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
new file mode 100644
index 0000000..d043192
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -0,0 +1,54 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Bug
+description: File a bug/issue
+title: "[BUG]
"
+labels: [ Bug, Needs Triage ]
+
+body:
+ - type: checkboxes
+ attributes:
+ label: Is there an existing issue for this?
+ description: Please [search here](./issues) to see if an issue already exists for your problem.
+ options:
+ - label: I have searched the existing issues
+ required: true
+ - type: textarea
+ attributes:
+ label: Current Behavior
+ description: A clear & concise description of what you're experiencing.
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Expected Behavior
+ description: A clear & concise description of what you expected to happen.
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Steps To Reproduce
+ description: Steps to reproduce the behavior.
+ value: |
+ 1. In this environment...
+ 2. With this config...
+ 3. Run '...'
+ 4. See error...
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Environment
+ description: |
+ examples:
+ - **npm**: 7.6.3
+ - **Node**: 13.14.0
+ - **OS**: Ubuntu 20.04
+ - **platform**: Macbook Pro
+ value: |
+ - npm:
+ - Node:
+ - OS:
+ - platform:
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..d640909
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,3 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+blank_issues_enabled: true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..8da2a45
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,17 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+version: 2
+
+updates:
+ - package-ecosystem: npm
+ directory: /
+ schedule:
+ interval: daily
+ allow:
+ - dependency-type: direct
+ versioning-strategy: increase-if-necessary
+ commit-message:
+ prefix: deps
+ prefix-development: chore
+ labels:
+ - "Dependencies"
diff --git a/.github/matchers/tap.json b/.github/matchers/tap.json
new file mode 100644
index 0000000..2c81ea9
--- /dev/null
+++ b/.github/matchers/tap.json
@@ -0,0 +1,32 @@
+{
+ "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.",
+ "problemMatcher": [
+ {
+ "owner": "tap",
+ "pattern": [
+ {
+ "regexp": "^\\s*not ok \\d+ - (.*)",
+ "message": 1
+ },
+ {
+ "regexp": "^\\s*---"
+ },
+ {
+ "regexp": "^\\s*at:"
+ },
+ {
+ "regexp": "^\\s*line:\\s*(\\d+)",
+ "line": 1
+ },
+ {
+ "regexp": "^\\s*column:\\s*(\\d+)",
+ "column": 1
+ },
+ {
+ "regexp": "^\\s*file:\\s*(.*)",
+ "file": 1
+ }
+ ]
+ }
+ ]
+}
diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml
new file mode 100644
index 0000000..62892f9
--- /dev/null
+++ b/.github/workflows/audit.yml
@@ -0,0 +1,39 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Audit
+
+on:
+ workflow_dispatch:
+ schedule:
+ # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1
+ - cron: "0 8 * * 1"
+
+jobs:
+ audit:
+ name: Audit Dependencies
+ if: github.repository_owner == 'npm'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Setup Git User
+ run: |
+ git config --global user.email "npm-cli+bot@github.com"
+ git config --global user.name "npm CLI robot"
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
+ - name: Install npm@latest
+ run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+ - name: npm Version
+ run: npm -v
+ - name: Install Dependencies
+ run: npm i --ignore-scripts --no-audit --no-fund --package-lock
+ - name: Run Production Audit
+ run: npm audit --omit=dev
+ - name: Run Full Audit
+ run: npm audit --audit-level=none
diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml
new file mode 100644
index 0000000..6e80aa6
--- /dev/null
+++ b/.github/workflows/ci-release.yml
@@ -0,0 +1,216 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: CI - Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ ref:
+ required: true
+ type: string
+ default: main
+ workflow_call:
+ inputs:
+ ref:
+ required: true
+ type: string
+ check-sha:
+ required: true
+ type: string
+
+jobs:
+ lint-all:
+ name: Lint All
+ if: github.repository_owner == 'npm'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Get Workflow Job
+ uses: actions/github-script@v6
+ if: inputs.check-sha
+ id: check-output
+ env:
+ JOB_NAME: "Lint All"
+ MATRIX_NAME: ""
+ with:
+ script: |
+ const { owner, repo } = context.repo
+
+ const { data } = await github.rest.actions.listJobsForWorkflowRun({
+ owner,
+ repo,
+ run_id: context.runId,
+ per_page: 100
+ })
+
+ const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
+ const job = data.jobs.find(j => j.name.endsWith(jobName))
+ const jobUrl = job?.html_url
+
+ const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}`
+
+ let summary = `This check is assosciated with ${shaUrl}\n\n`
+
+ if (jobUrl) {
+ summary += `For run logs, click here: ${jobUrl}`
+ } else {
+ summary += `Run logs could not be found for a job with name: "${jobName}"`
+ }
+
+ return { summary }
+ - name: Create Check
+ uses: LouisBrunner/checks-action@v1.3.1
+ id: check
+ if: inputs.check-sha
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ status: in_progress
+ name: Lint All
+ sha: ${{ inputs.check-sha }}
+ output: ${{ steps.check-output.outputs.result }}
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ inputs.ref }}
+ - name: Setup Git User
+ run: |
+ git config --global user.email "npm-cli+bot@github.com"
+ git config --global user.name "npm CLI robot"
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
+ - name: Install npm@latest
+ run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+ - name: npm Version
+ run: npm -v
+ - name: Install Dependencies
+ run: npm i --ignore-scripts --no-audit --no-fund
+ - name: Lint
+ run: npm run lint --ignore-scripts
+ - name: Post Lint
+ run: npm run postlint --ignore-scripts
+ - name: Conclude Check
+ uses: LouisBrunner/checks-action@v1.3.1
+ if: steps.check.outputs.check_id && always()
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ conclusion: ${{ job.status }}
+ check_id: ${{ steps.check.outputs.check_id }}
+
+ test-all:
+ name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
+ if: github.repository_owner == 'npm'
+ strategy:
+ fail-fast: false
+ matrix:
+ platform:
+ - name: Linux
+ os: ubuntu-latest
+ shell: bash
+ - name: macOS
+ os: macos-latest
+ shell: bash
+ - name: Windows
+ os: windows-latest
+ shell: cmd
+ node-version:
+ - 14.17.0
+ - 14.x
+ - 16.13.0
+ - 16.x
+ - 18.0.0
+ - 18.x
+ runs-on: ${{ matrix.platform.os }}
+ defaults:
+ run:
+ shell: ${{ matrix.platform.shell }}
+ steps:
+ - name: Get Workflow Job
+ uses: actions/github-script@v6
+ if: inputs.check-sha
+ id: check-output
+ env:
+ JOB_NAME: "Test All"
+ MATRIX_NAME: " - ${{ matrix.platform.name }} - ${{ matrix.node-version }}"
+ with:
+ script: |
+ const { owner, repo } = context.repo
+
+ const { data } = await github.rest.actions.listJobsForWorkflowRun({
+ owner,
+ repo,
+ run_id: context.runId,
+ per_page: 100
+ })
+
+ const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
+ const job = data.jobs.find(j => j.name.endsWith(jobName))
+ const jobUrl = job?.html_url
+
+ const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}`
+
+ let summary = `This check is assosciated with ${shaUrl}\n\n`
+
+ if (jobUrl) {
+ summary += `For run logs, click here: ${jobUrl}`
+ } else {
+ summary += `Run logs could not be found for a job with name: "${jobName}"`
+ }
+
+ return { summary }
+ - name: Create Check
+ uses: LouisBrunner/checks-action@v1.3.1
+ id: check
+ if: inputs.check-sha
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ status: in_progress
+ name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
+ sha: ${{ inputs.check-sha }}
+ output: ${{ steps.check-output.outputs.result }}
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ inputs.ref }}
+ - name: Setup Git User
+ run: |
+ git config --global user.email "npm-cli+bot@github.com"
+ git config --global user.name "npm CLI robot"
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: Update Windows npm
+ # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
+ if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.'))
+ run: |
+ curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
+ tar xf npm-7.5.4.tgz
+ cd package
+ node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
+ cd ..
+ rmdir /s /q package
+ - name: Install npm@7
+ if: startsWith(matrix.node-version, '10.')
+ run: npm i --prefer-online --no-fund --no-audit -g npm@7
+ - name: Install npm@latest
+ if: ${{ !startsWith(matrix.node-version, '10.') }}
+ run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+ - name: npm Version
+ run: npm -v
+ - name: Install Dependencies
+ run: npm i --ignore-scripts --no-audit --no-fund
+ - name: Add Problem Matcher
+ run: echo "::add-matcher::.github/matchers/tap.json"
+ - name: Test
+ run: npm test --ignore-scripts
+ - name: Conclude Check
+ uses: LouisBrunner/checks-action@v1.3.1
+ if: steps.check.outputs.check_id && always()
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ conclusion: ${{ job.status }}
+ check_id: ${{ steps.check.outputs.check_id }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9cc149d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,107 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: CI
+
+on:
+ workflow_dispatch:
+ pull_request:
+ push:
+ branches:
+ - main
+ - latest
+ schedule:
+ # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1
+ - cron: "0 9 * * 1"
+
+jobs:
+ lint:
+ name: Lint
+ if: github.repository_owner == 'npm'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Setup Git User
+ run: |
+ git config --global user.email "npm-cli+bot@github.com"
+ git config --global user.name "npm CLI robot"
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
+ - name: Install npm@latest
+ run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+ - name: npm Version
+ run: npm -v
+ - name: Install Dependencies
+ run: npm i --ignore-scripts --no-audit --no-fund
+ - name: Lint
+ run: npm run lint --ignore-scripts
+ - name: Post Lint
+ run: npm run postlint --ignore-scripts
+
+ test:
+ name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
+ if: github.repository_owner == 'npm'
+ strategy:
+ fail-fast: false
+ matrix:
+ platform:
+ - name: Linux
+ os: ubuntu-latest
+ shell: bash
+ - name: macOS
+ os: macos-latest
+ shell: bash
+ - name: Windows
+ os: windows-latest
+ shell: cmd
+ node-version:
+ - 14.17.0
+ - 14.x
+ - 16.13.0
+ - 16.x
+ - 18.0.0
+ - 18.x
+ runs-on: ${{ matrix.platform.os }}
+ defaults:
+ run:
+ shell: ${{ matrix.platform.shell }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Setup Git User
+ run: |
+ git config --global user.email "npm-cli+bot@github.com"
+ git config --global user.name "npm CLI robot"
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: Update Windows npm
+ # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
+ if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.'))
+ run: |
+ curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
+ tar xf npm-7.5.4.tgz
+ cd package
+ node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
+ cd ..
+ rmdir /s /q package
+ - name: Install npm@7
+ if: startsWith(matrix.node-version, '10.')
+ run: npm i --prefer-online --no-fund --no-audit -g npm@7
+ - name: Install npm@latest
+ if: ${{ !startsWith(matrix.node-version, '10.') }}
+ run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+ - name: npm Version
+ run: npm -v
+ - name: Install Dependencies
+ run: npm i --ignore-scripts --no-audit --no-fund
+ - name: Add Problem Matcher
+ run: echo "::add-matcher::.github/matchers/tap.json"
+ - name: Test
+ run: npm test --ignore-scripts
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..66b9498
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,38 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: CodeQL
+
+on:
+ push:
+ branches:
+ - main
+ - latest
+ pull_request:
+ branches:
+ - main
+ - latest
+ schedule:
+ # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1
+ - cron: "0 10 * * 1"
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Setup Git User
+ run: |
+ git config --global user.email "npm-cli+bot@github.com"
+ git config --global user.name "npm CLI robot"
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: javascript
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/post-dependabot.yml b/.github/workflows/post-dependabot.yml
new file mode 100644
index 0000000..19902bd
--- /dev/null
+++ b/.github/workflows/post-dependabot.yml
@@ -0,0 +1,121 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Post Dependabot
+
+on: pull_request
+
+permissions:
+ contents: write
+
+jobs:
+ template-oss:
+ name: template-oss
+ if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.ref }}
+ - name: Setup Git User
+ run: |
+ git config --global user.email "npm-cli+bot@github.com"
+ git config --global user.name "npm CLI robot"
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
+ - name: Install npm@latest
+ run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+ - name: npm Version
+ run: npm -v
+ - name: Install Dependencies
+ run: npm i --ignore-scripts --no-audit --no-fund
+ - name: Fetch Dependabot Metadata
+ id: metadata
+ uses: dependabot/fetch-metadata@v1
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+
+ # Dependabot can update multiple directories so we output which directory
+ # it is acting on so we can run the command for the correct root or workspace
+ - name: Get Dependabot Directory
+ if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss')
+ id: flags
+ run: |
+ dependabot_dir="${{ steps.metadata.outputs.directory }}"
+ if [[ "$dependabot_dir" == "/" ]]; then
+ echo "::set-output name=workspace::-iwr"
+ else
+ # strip leading slash from directory so it works as a
+ # a path to the workspace flag
+ echo "::set-output name=workspace::-w ${dependabot_dir#/}"
+ fi
+
+ - name: Apply Changes
+ if: steps.flags.outputs.workspace
+ id: apply
+ run: |
+ npm run template-oss-apply ${{ steps.flags.outputs.workspace }}
+ if [[ `git status --porcelain` ]]; then
+ echo "::set-output name=changes::true"
+ fi
+ # This only sets the conventional commit prefix. This workflow can't reliably determine
+ # what the breaking change is though. If a BREAKING CHANGE message is required then
+ # this PR check will fail and the commit will be amended with stafftools
+ if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then
+ prefix='feat!'
+ else
+ prefix='chore'
+ fi
+ echo "::set-output name=message::$prefix: postinstall for dependabot template-oss PR"
+
+ # This step will fail if template-oss has made any workflow updates. It is impossible
+ # for a workflow to update other workflows. In the case it does fail, we continue
+ # and then try to apply only a portion of the changes in the next step
+ - name: Push All Changes
+ if: steps.apply.outputs.changes
+ id: push
+ continue-on-error: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ git commit -am "${{ steps.apply.outputs.message }}"
+ git push
+
+ # If the previous step failed, then reset the commit and remove any workflow changes
+ # and attempt to commit and push again. This is helpful because we will have a commit
+ # with the correct prefix that we can then --amend with @npmcli/stafftools later.
+ - name: Push All Changes Except Workflows
+ if: steps.apply.outputs.changes && steps.push.outcome == 'failure'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ git reset HEAD~
+ git checkout HEAD -- .github/workflows/
+ git clean -fd .github/workflows/
+ git commit -am "${{ steps.apply.outputs.message }}"
+ git push
+
+ # Check if all the necessary template-oss changes were applied. Since we continued
+ # on errors in one of the previous steps, this check will fail if our follow up
+ # only applied a portion of the changes and we need to followup manually.
+ #
+ # Note that this used to run `lint` and `postlint` but that will fail this action
+ # if we've also shipped any linting changes separate from template-oss. We do
+ # linting in another action, so we want to fail this one only if there are
+ # template-oss changes that could not be applied.
+ - name: Check Changes
+ if: steps.apply.outputs.changes
+ run: |
+ npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check
+
+ - name: Fail on Breaking Change
+ if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!')
+ run: |
+ echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'"
+ echo "for more information on how to fix this with a BREAKING CHANGE footer."
+ exit 1
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
new file mode 100644
index 0000000..1a1d1ee
--- /dev/null
+++ b/.github/workflows/pull-request.yml
@@ -0,0 +1,48 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Pull Request
+
+on:
+ pull_request:
+ types:
+ - opened
+ - reopened
+ - edited
+ - synchronize
+
+jobs:
+ commitlint:
+ name: Lint Commits
+ if: github.repository_owner == 'npm'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Setup Git User
+ run: |
+ git config --global user.email "npm-cli+bot@github.com"
+ git config --global user.name "npm CLI robot"
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
+ - name: Install npm@latest
+ run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+ - name: npm Version
+ run: npm -v
+ - name: Install Dependencies
+ run: npm i --ignore-scripts --no-audit --no-fund
+ - name: Run Commitlint on Commits
+ id: commit
+ continue-on-error: true
+ run: |
+ npx --offline commitlint -V --from origin/${{ github.base_ref }} --to ${{ github.event.pull_request.head.sha }}
+ - name: Run Commitlint on PR Title
+ if: steps.commit.outcome == 'failure'
+ run: |
+ echo ${{ github.event.pull_request.title }} | npx --offline commitlint -V
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..15d37cb
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,299 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Release
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ - latest
+ - release/v*
+
+permissions:
+ contents: write
+ pull-requests: write
+ checks: write
+
+jobs:
+ release:
+ outputs:
+ pr: ${{ steps.release.outputs.pr }}
+ releases: ${{ steps.release.outputs.releases }}
+ release-flags: ${{ steps.release.outputs.release-flags }}
+ branch: ${{ steps.release.outputs.pr-branch }}
+ pr-number: ${{ steps.release.outputs.pr-number }}
+ comment-id: ${{ steps.pr-comment.outputs.result }}
+ check-id: ${{ steps.check.outputs.check_id }}
+ name: Release
+ if: github.repository_owner == 'npm'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Setup Git User
+ run: |
+ git config --global user.email "npm-cli+bot@github.com"
+ git config --global user.name "npm CLI robot"
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
+ - name: Install npm@latest
+ run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+ - name: npm Version
+ run: npm -v
+ - name: Install Dependencies
+ run: npm i --ignore-scripts --no-audit --no-fund
+ - name: Release Please
+ id: release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ npx --offline template-oss-release-please ${{ github.ref_name }} ${{ github.event_name }}
+ - name: Post Pull Request Comment
+ if: steps.release.outputs.pr-number
+ uses: actions/github-script@v6
+ id: pr-comment
+ env:
+ PR_NUMBER: ${{ steps.release.outputs.pr-number }}
+ REF_NAME: ${{ github.ref_name }}
+ with:
+ script: |
+ const { REF_NAME, PR_NUMBER } = process.env
+ const repo = { owner: context.repo.owner, repo: context.repo.repo }
+ const issue = { ...repo, issue_number: PR_NUMBER }
+
+ const { data: workflow } = await github.rest.actions.getWorkflowRun({ ...repo, run_id: context.runId })
+
+ let body = '## Release Manager\n\n'
+
+ const comments = await github.paginate(github.rest.issues.listComments, issue)
+ let commentId = comments?.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id
+
+ body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Rerun for This Release\n\n`
+ body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`main\`. `
+ body += `To force CI to rerun, run this command:\n\n`
+ body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME}\n\`\`\``
+
+ if (commentId) {
+ await github.rest.issues.updateComment({ ...repo, comment_id: commentId, body })
+ } else {
+ const { data: comment } = await github.rest.issues.createComment({ ...issue, body })
+ commentId = comment?.id
+ }
+
+ return commentId
+ - name: Get Workflow Job
+ uses: actions/github-script@v6
+ if: steps.release.outputs.pr-sha
+ id: check-output
+ env:
+ JOB_NAME: "Release"
+ MATRIX_NAME: ""
+ with:
+ script: |
+ const { owner, repo } = context.repo
+
+ const { data } = await github.rest.actions.listJobsForWorkflowRun({
+ owner,
+ repo,
+ run_id: context.runId,
+ per_page: 100
+ })
+
+ const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
+ const job = data.jobs.find(j => j.name.endsWith(jobName))
+ const jobUrl = job?.html_url
+
+ const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.release.outputs.pr-sha }}`
+
+ let summary = `This check is assosciated with ${shaUrl}\n\n`
+
+ if (jobUrl) {
+ summary += `For run logs, click here: ${jobUrl}`
+ } else {
+ summary += `Run logs could not be found for a job with name: "${jobName}"`
+ }
+
+ return { summary }
+ - name: Create Check
+ uses: LouisBrunner/checks-action@v1.3.1
+ id: check
+ if: steps.release.outputs.pr-sha
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ status: in_progress
+ name: Release
+ sha: ${{ steps.release.outputs.pr-sha }}
+ output: ${{ steps.check-output.outputs.result }}
+
+ update:
+ needs: release
+ outputs:
+ sha: ${{ steps.commit.outputs.sha }}
+ check-id: ${{ steps.check.outputs.check_id }}
+ name: Update - Release
+ if: github.repository_owner == 'npm' && needs.release.outputs.pr
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ ref: ${{ needs.release.outputs.branch }}
+ - name: Setup Git User
+ run: |
+ git config --global user.email "npm-cli+bot@github.com"
+ git config --global user.name "npm CLI robot"
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
+ - name: Install npm@latest
+ run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+ - name: npm Version
+ run: npm -v
+ - name: Install Dependencies
+ run: npm i --ignore-scripts --no-audit --no-fund
+ - name: Run Post Pull Request Actions
+ env:
+ RELEASE_PR_NUMBER: ${{ needs.release.outputs.pr-number }}
+ RELEASE_COMMENT_ID: ${{ needs.release.outputs.comment-id }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ npm exec --offline -- template-oss-release-manager
+ npm run rp-pull-request --ignore-scripts --if-present
+ - name: Commit
+ id: commit
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ git commit --all --amend --no-edit || true
+ git push --force-with-lease
+ echo "::set-output name=sha::$(git rev-parse HEAD)"
+ - name: Get Workflow Job
+ uses: actions/github-script@v6
+ if: steps.commit.outputs.sha
+ id: check-output
+ env:
+ JOB_NAME: "Update - Release"
+ MATRIX_NAME: ""
+ with:
+ script: |
+ const { owner, repo } = context.repo
+
+ const { data } = await github.rest.actions.listJobsForWorkflowRun({
+ owner,
+ repo,
+ run_id: context.runId,
+ per_page: 100
+ })
+
+ const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
+ const job = data.jobs.find(j => j.name.endsWith(jobName))
+ const jobUrl = job?.html_url
+
+ const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.commit.outputs.sha }}`
+
+ let summary = `This check is assosciated with ${shaUrl}\n\n`
+
+ if (jobUrl) {
+ summary += `For run logs, click here: ${jobUrl}`
+ } else {
+ summary += `Run logs could not be found for a job with name: "${jobName}"`
+ }
+
+ return { summary }
+ - name: Create Check
+ uses: LouisBrunner/checks-action@v1.3.1
+ id: check
+ if: steps.commit.outputs.sha
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ status: in_progress
+ name: Release
+ sha: ${{ steps.commit.outputs.sha }}
+ output: ${{ steps.check-output.outputs.result }}
+ - name: Conclude Check
+ uses: LouisBrunner/checks-action@v1.3.1
+ if: needs.release.outputs.check-id && always()
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ conclusion: ${{ job.status }}
+ check_id: ${{ needs.release.outputs.check-id }}
+
+ ci:
+ name: CI - Release
+ needs: [ release, update ]
+ if: needs.release.outputs.pr
+ uses: ./.github/workflows/ci-release.yml
+ with:
+ ref: ${{ needs.release.outputs.branch }}
+ check-sha: ${{ needs.update.outputs.sha }}
+
+ post-ci:
+ needs: [ release, update, ci ]
+ name: Post CI - Release
+ if: github.repository_owner == 'npm' && needs.release.outputs.pr && always()
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Get Needs Result
+ id: needs-result
+ run: |
+ result=""
+ if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
+ result="failure"
+ elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
+ result="cancelled"
+ else
+ result="success"
+ fi
+ echo "::set-output name=result::$result"
+ - name: Conclude Check
+ uses: LouisBrunner/checks-action@v1.3.1
+ if: needs.update.outputs.check-id && always()
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ conclusion: ${{ steps.needs-result.outputs.result }}
+ check_id: ${{ needs.update.outputs.check-id }}
+
+ post-release:
+ needs: release
+ name: Post Release - Release
+ if: github.repository_owner == 'npm' && needs.release.outputs.releases
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Setup Git User
+ run: |
+ git config --global user.email "npm-cli+bot@github.com"
+ git config --global user.name "npm CLI robot"
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
+ - name: Install npm@latest
+ run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+ - name: npm Version
+ run: npm -v
+ - name: Install Dependencies
+ run: npm i --ignore-scripts --no-audit --no-fund
+ - name: Run Post Release Actions
+ env:
+ RELEASES: ${{ needs.release.outputs.releases }}
+ run: |
+ npm run rp-release --ignore-scripts --if-present ${{ join(fromJSON(needs.release.outputs.release-flags), ' ') }}
diff --git a/.gitignore b/.gitignore
index 2d8d77e..0ec3c84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,28 @@
-.nyc_output/
-coverage/
-node_modules/
-package-lock.json
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+# ignore everything in the root
+/*
+
+# keep these
+!**/.gitignore
+!/.commitlintrc.js
+!/.eslintrc.js
+!/.eslintrc.local.*
+!/.github/
+!/.gitignore
+!/.npmrc
+!/.release-please-manifest.json
+!/bin/
+!/CHANGELOG*
+!/CODE_OF_CONDUCT.md
+!/docs/
+!/lib/
+!/LICENSE*
+!/map.js
+!/package.json
+!/README*
+!/release-please-config.json
+!/scripts/
+!/SECURITY.md
+!/tap-snapshots/
+!/test/
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..529f93e
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,3 @@
+; This file is automatically added by @npmcli/template-oss. Do not edit.
+
+package-lock=false
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..47fb725
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "2.0.2"
+}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index a4b0658..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-language: node_js
-
-node_js:
- - node
- - 12
- - 10
- - 8
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fb1f20..991bf8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,4 @@
-# Changes
-
+# Changelog
## 2.0.2
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..167043c
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,7 @@
+
+
+All interactions in this repo are covered by the [npm Code of
+Conduct](https://docs.npmjs.com/policies/conduct)
+
+The npm cli team may, at its own discretion, moderate, remove, or edit
+any interactions such as pull requests, issues, and comments.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index e88ff8f..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1 +0,0 @@
-Please consider signing [the neveragain.tech pledge](http://neveragain.tech/)
diff --git a/README.md b/README.md
index cd83350..a6e86ab 100644
--- a/README.md
+++ b/README.md
@@ -9,30 +9,24 @@ needed when the PATH changes.
## USAGE
```javascript
-var which = require('which')
+const which = require('which')
// async usage
-which('node', function (er, resolvedPath) {
- // er is returned if no "node" is found on the PATH
- // if it is found, then the absolute path to the exec is returned
-})
+// rejects if not found
+const resolved = await which('node')
-// or promise
-which('node').then(resolvedPath => { ... }).catch(er => { ... not found ... })
+// if nothrow option is used, returns null if not found
+const resolvedOrNull = await which('node', { nothrow: true })
// sync usage
// throws if not found
-var resolved = which.sync('node')
+const resolved = which.sync('node')
// if nothrow option is used, returns null if not found
-resolved = which.sync('node', {nothrow: true})
+const resolvedOrNull = which.sync('node', { nothrow: true })
// Pass options to override the PATH and PATHEXT environment vars.
-which('node', { path: someOtherPath }, function (er, resolved) {
- if (er)
- throw er
- console.log('found at %j', resolved)
-})
+await which('node', { path: someOtherPath, pathExt: somePathExt })
```
## CLI USAGE
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..a93106d
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,3 @@
+
+
+Please send vulnerability reports through [hackerone](https://hackerone.com/github).
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index b5fac87..0000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-environment:
- matrix:
- - nodejs_version: '6'
- - nodejs_version: '4'
-install:
- - ps: Install-Product node $env:nodejs_version
- - set CI=true
- - npm -g install npm@latest
- - set PATH=%APPDATA%\npm;%PATH%
- - npm install
-matrix:
- fast_finish: true
-build: off
-version: '{build}'
-shallow_clone: true
-clone_depth: 1
-test_script:
- - npm test
diff --git a/bin/node-which b/bin/node-which
deleted file mode 100755
index 7cee372..0000000
--- a/bin/node-which
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env node
-var which = require("../")
-if (process.argv.length < 3)
- usage()
-
-function usage () {
- console.error('usage: which [-as] program ...')
- process.exit(1)
-}
-
-var all = false
-var silent = false
-var dashdash = false
-var args = process.argv.slice(2).filter(function (arg) {
- if (dashdash || !/^-/.test(arg))
- return true
-
- if (arg === '--') {
- dashdash = true
- return false
- }
-
- var flags = arg.substr(1).split('')
- for (var f = 0; f < flags.length; f++) {
- var flag = flags[f]
- switch (flag) {
- case 's':
- silent = true
- break
- case 'a':
- all = true
- break
- default:
- console.error('which: illegal option -- ' + flag)
- usage()
- }
- }
- return false
-})
-
-process.exit(args.reduce(function (pv, current) {
- try {
- var f = which.sync(current, { all: all })
- if (all)
- f = f.join('\n')
- if (!silent)
- console.log(f)
- return pv;
- } catch (e) {
- return 1;
- }
-}, 0))
diff --git a/bin/which.js b/bin/which.js
new file mode 100755
index 0000000..6df16f2
--- /dev/null
+++ b/bin/which.js
@@ -0,0 +1,52 @@
+#!/usr/bin/env node
+
+const which = require('../lib')
+const argv = process.argv.slice(2)
+
+const usage = (err) => {
+ if (err) {
+ console.error(`which: ${err}`)
+ }
+ console.error('usage: which [-as] program ...')
+ process.exit(1)
+}
+
+if (!argv.length) {
+ return usage()
+}
+
+let dashdash = false
+const [commands, flags] = argv.reduce((acc, arg) => {
+ if (dashdash || arg === '--') {
+ dashdash = true
+ return acc
+ }
+
+ if (!/^-/.test(arg)) {
+ acc[0].push(arg)
+ return acc
+ }
+
+ for (const flag of arg.slice(1).split('')) {
+ if (flag === 's') {
+ acc[1].silent = true
+ } else if (flag === 'a') {
+ acc[1].all = true
+ } else {
+ usage(`illegal option -- ${flag}`)
+ }
+ }
+
+ return acc
+}, [[], {}])
+
+for (const command of commands) {
+ try {
+ const res = which.sync(command, { all: flags.all })
+ if (!flags.silent) {
+ console.log([].concat(res).join('\n'))
+ }
+ } catch (err) {
+ process.exitCode = 1
+ }
+}
diff --git a/gen-changelog.sh b/gen-changelog.sh
deleted file mode 100644
index 360e54a..0000000
--- a/gen-changelog.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-(
- echo '# Changes'
- echo ''
- git log --first-parent --pretty=format:'%s' \
- | grep -v '^update changelog' \
- | perl -p -e 's/^((v?[0-9]+\.?)+)$/\n## \1\n/g' \
- | perl -p -e 's/^([^#\s].*)$/* \1/g'
-)> CHANGELOG.md
diff --git a/lib/index.js b/lib/index.js
new file mode 100644
index 0000000..8de3388
--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1,110 @@
+const isexe = require('isexe')
+const { join, delimiter, sep, posix } = require('path')
+
+const isWindows = process.platform === 'win32'
+
+// used to check for slashed in commands passed in. always checks for the posix
+// seperator on all platforms, and checks for the current separator when not on
+// a posix platform. don't use the isWindows check for this since that is mocked
+// in tests but we still need the code to actually work when called. that is also
+// why it is ignored from coverage.
+/* istanbul ignore next */
+const rSlash = new RegExp(`[${posix.sep}${sep === posix.sep ? '' : sep}]`.replace(/(\\)/g, '\\$1'))
+const rRel = new RegExp(`^\\.${rSlash.source}`)
+
+const getNotFoundError = (cmd) =>
+ Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' })
+
+const getPathInfo = (cmd, {
+ path: optPath = process.env.PATH,
+ pathExt: optPathExt = process.env.PATHEXT,
+ delimiter: optDelimiter = delimiter,
+}) => {
+ // If it has a slash, then we don't bother searching the pathenv.
+ // just check the file itself, and that's it.
+ const pathEnv = cmd.match(rSlash) ? [''] : [
+ // windows always checks the cwd first
+ ...(isWindows ? [process.cwd()] : []),
+ ...(optPath || /* istanbul ignore next: very unusual */ '').split(optDelimiter),
+ ]
+
+ if (isWindows) {
+ const pathExtExe = optPathExt || ['.EXE', '.CMD', '.BAT', '.COM'].join(optDelimiter)
+ const pathExt = pathExtExe.split(optDelimiter)
+ if (cmd.includes('.') && pathExt[0] !== '') {
+ pathExt.unshift('')
+ }
+ return { pathEnv, pathExt, pathExtExe }
+ }
+
+ return { pathEnv, pathExt: [''] }
+}
+
+const getPathPart = (raw, cmd) => {
+ const pathPart = /^".*"$/.test(raw) ? raw.slice(1, -1) : raw
+ const prefix = !pathPart && rRel.test(cmd) ? cmd.slice(0, 2) : ''
+ return prefix + join(pathPart, cmd)
+}
+
+const which = async (cmd, opt = {}) => {
+ const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt)
+ const found = []
+
+ for (const envPart of pathEnv) {
+ const p = getPathPart(envPart, cmd)
+
+ for (const ext of pathExt) {
+ const withExt = p + ext
+ const is = await isexe(withExt, { pathExt: pathExtExe, ignoreErrors: true })
+ if (is) {
+ if (!opt.all) {
+ return withExt
+ }
+ found.push(withExt)
+ }
+ }
+ }
+
+ if (opt.all && found.length) {
+ return found
+ }
+
+ if (opt.nothrow) {
+ return null
+ }
+
+ throw getNotFoundError(cmd)
+}
+
+const whichSync = (cmd, opt = {}) => {
+ const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt)
+ const found = []
+
+ for (const pathEnvPart of pathEnv) {
+ const p = getPathPart(pathEnvPart, cmd)
+
+ for (const ext of pathExt) {
+ const withExt = p + ext
+ const is = isexe.sync(withExt, { pathExt: pathExtExe, ignoreErrors: true })
+ if (is) {
+ if (!opt.all) {
+ return withExt
+ }
+ found.push(withExt)
+ }
+ }
+ }
+
+ if (opt.all && found.length) {
+ return found
+ }
+
+ if (opt.nothrow) {
+ return null
+ }
+
+ throw getNotFoundError(cmd)
+}
+
+module.exports = which
+which.sync = whichSync
diff --git a/package.json b/package.json
index dcbf044..92d44b2 100644
--- a/package.json
+++ b/package.json
@@ -1,43 +1,50 @@
{
- "author": "Isaac Z. Schlueter (http://blog.izs.me)",
+ "author": "GitHub Inc.",
"name": "which",
"description": "Like which(1) unix command. Find the first instance of an executable in the PATH.",
"version": "2.0.2",
"repository": {
"type": "git",
- "url": "git://github.com/isaacs/node-which.git"
+ "url": "https://github.com/npm/node-which.git"
},
- "main": "which.js",
+ "main": "lib/index.js",
"bin": {
- "node-which": "./bin/node-which"
+ "node-which": "./bin/which.js"
},
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"devDependencies": {
- "mkdirp": "^0.5.0",
- "rimraf": "^2.6.2",
- "tap": "^16.0.1"
+ "@npmcli/eslint-config": "^4.0.0",
+ "@npmcli/template-oss": "4.8.0",
+ "tap": "^16.3.0"
},
"scripts": {
"test": "tap",
- "preversion": "npm test",
- "postversion": "npm publish",
- "prepublishOnly": "npm run changelog",
- "prechangelog": "bash gen-changelog.sh",
- "changelog": "git add CHANGELOG.md",
- "postchangelog": "git commit -m 'update changelog - '${npm_package_version}",
- "postpublish": "git push origin --follow-tags"
+ "lint": "eslint \"**/*.js\"",
+ "postlint": "template-oss-check",
+ "template-oss-apply": "template-oss-apply --force",
+ "lintfix": "npm run lint -- --fix",
+ "snap": "tap",
+ "posttest": "npm run lint"
},
"files": [
- "which.js",
- "bin/node-which"
+ "bin/",
+ "lib/"
],
"tap": {
- "check-coverage": true
+ "check-coverage": true,
+ "nyc-arg": [
+ "--exclude",
+ "tap-snapshots/**"
+ ]
},
"engines": {
- "node": ">= 8"
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ },
+ "templateOSS": {
+ "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
+ "version": "4.8.0"
}
}
diff --git a/release-please-config.json b/release-please-config.json
new file mode 100644
index 0000000..73d1e35
--- /dev/null
+++ b/release-please-config.json
@@ -0,0 +1,36 @@
+{
+ "exclude-packages-from-root": true,
+ "group-pull-request-title-pattern": "chore: release ${version}",
+ "pull-request-title-pattern": "chore: release${component} ${version}",
+ "changelog-sections": [
+ {
+ "type": "feat",
+ "section": "Features",
+ "hidden": false
+ },
+ {
+ "type": "fix",
+ "section": "Bug Fixes",
+ "hidden": false
+ },
+ {
+ "type": "docs",
+ "section": "Documentation",
+ "hidden": false
+ },
+ {
+ "type": "deps",
+ "section": "Dependencies",
+ "hidden": false
+ },
+ {
+ "type": "chore",
+ "hidden": true
+ }
+ ],
+ "packages": {
+ ".": {
+ "package-name": ""
+ }
+ }
+}
diff --git a/test/basic.js b/test/basic.js
deleted file mode 100644
index c656fa5..0000000
--- a/test/basic.js
+++ /dev/null
@@ -1,193 +0,0 @@
-var t = require('tap')
-var fs = require('fs')
-var rimraf = require('rimraf')
-var mkdirp = require('mkdirp')
-const fixdir = `'/fixture-${(+process.env.TAP_CHILD_ID || 0)}`
-var fixture = `${__dirname}/${fixdir}`
-var which = require('../which.js')
-var path = require('path')
-
-var isWindows = process.platform === 'win32' ||
- process.env.OSTYPE === 'cygwin' ||
- process.env.OSTYPE === 'msys'
-
-var skip = { skip: isWindows ? 'not relevant on windows' : false }
-
-t.test('setup', function (t) {
- rimraf.sync(fixture)
- mkdirp.sync(fixture)
- fs.writeFileSync(fixture + '/foo.sh', 'echo foo\n')
- t.end()
-})
-
-t.test('does not find missed', function(t) {
- t.plan(3)
-
- t.rejects(which(fixture + '/foobar.sh'), {
- code: 'ENOENT',
- })
-
- t.throws(function () {
- which.sync(fixture + '/foobar.sh')
- }, {code: 'ENOENT'})
-
- t.equal(which.sync(fixture + '/foobar.sh', {nothrow:true}), null)
-})
-
-t.test('does not find non-executable', skip, function (t) {
- t.plan(2)
-
- t.test('absolute', function (t) {
- t.plan(3)
- which(fixture + '/foo.sh', function (er) {
- t.type(er, Error)
- t.equal(er.code, 'ENOENT')
- })
-
- t.throws(function () {
- which.sync(fixture + '/foo.sh')
- }, {code: 'ENOENT'})
- })
-
- t.test('with path', function (t) {
- t.plan(3)
- which('foo.sh', { path: fixture }, function (er) {
- t.type(er, Error)
- t.equal(er.code, 'ENOENT')
- })
-
- t.throws(function () {
- which.sync('foo.sh', { path: fixture })
- }, {code: 'ENOENT'})
- })
-})
-
-t.test('make executable', function (t) {
- fs.chmodSync(fixture + '/foo.sh', '0755')
- t.end()
-})
-
-t.test('find when executable', function (t) {
- var opt = { pathExt: '.sh' }
- var expect = path.resolve(fixture, 'foo.sh').toLowerCase()
- var PATH = process.env.PATH
-
- t.test('absolute', function (t) {
- runTest(fixture + '/foo.sh', t)
- })
-
- t.test('with process.env.PATH', function (t) {
- process.env.PATH = fixture
- runTest('foo.sh', t)
- })
-
- t.test('with pathExt', {
- skip: isWindows ? false : 'Only for Windows'
- }, function (t) {
- var pe = process.env.PATHEXT
- process.env.PATHEXT = '.SH'
- process.env.PATH = fixture
-
- t.test('foo.sh', function (t) {
- process.env.PATH = fixture
- runTest('foo.sh', t)
- })
- t.test('foo', function (t) {
- process.env.PATH = fixture
- runTest('foo', t)
- })
- t.test('replace', function (t) {
- process.env.PATHEXT = pe
- t.end()
- })
- t.end()
- })
-
- t.test('with path opt', function (t) {
- opt.path = fixture
- runTest('foo.sh', t)
- })
-
- t.test('relative path', function (t) {
- var opt = { pathExt: '.sh' }
- var expect = path.join(`test/${fixdir}/foo.sh`)
- t.plan(3)
-
- t.test('no ./', function (t) {
- t.plan(2)
- var actual = which.sync(`test/${fixdir}/foo.sh`, opt)
- t.equal(actual, expect)
- which(`test/${fixdir}/foo.sh`, opt, function (er, actual) {
- if (er)
- throw er
- t.equal(actual, expect)
- })
- })
-
- t.test('with ./', function (t) {
- t.plan(2)
- expect = './' + expect
- var actual = which.sync(`./test/${fixdir}/foo.sh`, opt)
- t.equal(actual, expect)
- which(`./test/${fixdir}/foo.sh`, opt, function (er, actual) {
- if (er)
- throw er
- t.equal(actual, expect)
- })
- })
-
- t.test('with ../', function (t) {
- t.plan(2)
- var dir = path.basename(process.cwd())
- expect = path.join('..', dir, `test/${fixdir}/foo.sh`)
- var actual = which.sync(expect, opt)
- t.equal(actual, expect)
- which(expect, opt, function (er, actual) {
- if (er)
- throw er
- t.equal(actual, expect)
- })
- })
- })
-
- function runTest(exec, t) {
- t.plan(2)
-
- var found = which.sync(exec, opt).toLowerCase()
- t.equal(found, expect)
-
- which(exec, opt, function (er, found) {
- if (er)
- throw er
- t.equal(found.toLowerCase(), expect)
- t.end()
- process.env.PATH = PATH
- })
- }
-
- t.end()
-})
-
-t.test('find all', t => {
- mkdirp.sync(`${fixture}/all/a`)
- mkdirp.sync(`${fixture}/all/b`)
- fs.writeFileSync(`${fixture}/all/a/x.cmd`, 'exec me')
- fs.writeFileSync(`${fixture}/all/b/x.cmd`, 'exec me')
- fs.chmodSync(`${fixture}/all/a/x.cmd`, 0o755)
- fs.chmodSync(`${fixture}/all/b/x.cmd`, 0o755)
- const opt = {
- path: `${fixture}/all/a:"${fixture}/all/b"`,
- colon: ':',
- all: true,
- }
- const allsync = which.sync('x.cmd', opt)
- t.same(allsync, [`${fixture}/all/a/x.cmd`, `${fixture}/all/b/x.cmd`])
- return which('x.cmd', opt).then(all => {
- t.same(all, [`${fixture}/all/a/x.cmd`, `${fixture}/all/b/x.cmd`])
- })
-})
-
-t.test('clean', function (t) {
- rimraf.sync(fixture)
- t.end()
-})
diff --git a/test/bin.js b/test/bin.js
index fccd24c..b8b9a34 100644
--- a/test/bin.js
+++ b/test/bin.js
@@ -1,125 +1,118 @@
-var t = require('tap')
-var spawn = require('child_process').spawn
-var node = process.execPath
-var bin = require.resolve('../bin/node-which')
+const t = require('tap')
+const spawn = require('child_process').spawn
-function which (args, extraPath, cb) {
- if (typeof extraPath === 'function')
- cb = extraPath, extraPath = null
+const node = process.execPath
+const bin = require.resolve('../bin/which.js')
+
+function which (args, extraPath) {
+ const options = {}
- var options = {}
if (extraPath) {
- var sep = process.platform === 'win32' ? ';' : ':'
- var p = process.env.PATH + sep + extraPath
+ const sep = process.platform === 'win32' ? ';' : ':'
+ const p = process.env.PATH + sep + extraPath
options.env = Object.keys(process.env).reduce(function (env, k) {
- if (!k.match(/^path$/i))
+ if (!k.match(/^path$/i)) {
env[k] = process.env[k]
+ }
return env
}, { PATH: p })
}
- var out = ''
- var err = ''
- var child = spawn(node, [bin].concat(args), options)
- child.stdout.on('data', function (c) {
- out += c
- })
- child.stderr.on('data', function (c) {
- err += c
- })
- child.on('close', function (code, signal) {
- cb(code, signal, out.trim(), err.trim())
+ return new Promise((res) => {
+ let out = ''
+ let err = ''
+ const child = spawn(node, [bin].concat(args).filter(Boolean), options)
+ child.stdout.on('data', (c) => out += c)
+ child.stderr.on('data', (c) => err += c)
+ child.on('close', (code, signal) => {
+ out = out.trim()
+ err = err.trim()
+ res({ code, signal, out, err })
+ })
})
}
-t.test('finds node', function (t) {
- which('node', function (code, signal, out, err) {
- t.equal(signal, null)
- t.equal(code, 0)
- t.equal(err, '')
- t.match(out, /[\\\/]node(\.exe)?$/i)
- t.end()
- })
+t.test('finds node', async (t) => {
+ const { code, signal, out, err } = await which('node')
+ t.equal(signal, null)
+ t.equal(code, 0)
+ t.equal(err, '')
+ t.match(out, /[\\/]node(\.exe)?$/i)
})
-t.test('does not find flergyderp', function (t) {
- which('flergyderp', function (code, signal, out, err) {
- t.equal(signal, null)
- t.equal(code, 1)
- t.equal(err, '')
- t.match(out, '')
- t.end()
- })
+t.test('does not find flergyderp', async (t) => {
+ const { code, signal, out, err } = await which('flergyderp')
+ t.equal(signal, null)
+ t.equal(code, 1)
+ t.equal(err, '')
+ t.match(out, '')
})
-t.test('finds node and tap', function (t) {
- which(['node', 'tap'], function (code, signal, out, err) {
- t.equal(signal, null)
- t.equal(code, 0)
- t.equal(err, '')
- t.match(out.split(/[\r\n]+/), [
- /[\\\/]node(\.exe)?$/i,
- /[\\\/]tap(\.cmd)?$/i
- ])
- t.end()
- })
+t.test('finds node and tap', async (t) => {
+ const { code, signal, out, err } = await which(['node', 'tap'])
+ t.equal(signal, null)
+ t.equal(code, 0)
+ t.equal(err, '')
+ t.match(out.split(/[\r\n]+/), [
+ /[\\/]node(\.exe)?$/i,
+ /[\\/]tap(\.cmd)?$/i,
+ ])
})
-t.test('finds node and tap, but not flergyderp', function (t) {
- which(['node', 'flergyderp', 'tap'], function (code, signal, out, err) {
- t.equal(signal, null)
- t.equal(code, 1)
- t.equal(err, '')
- t.match(out.split(/[\r\n]+/), [
- /[\\\/]node(\.exe)?$/i,
- /[\\\/]tap(\.cmd)?$/i
- ])
- t.end()
- })
+t.test('finds node and tap, but not flergyderp', async (t) => {
+ const { code, signal, out, err } = await which(['node', 'flergyderp', 'tap'])
+ t.equal(signal, null)
+ t.equal(code, 1)
+ t.equal(err, '')
+ t.match(out.split(/[\r\n]+/), [
+ /[\\/]node(\.exe)?$/i,
+ /[\\/]tap(\.cmd)?$/i,
+ ])
})
-t.test('cli flags', function (t) {
- var p = require('path').dirname(bin)
- var cases = [ '-a', '-s', '-as', '-sa' ]
- t.plan(cases.length)
- cases.forEach(function (c) {
- t.test(c, function (t) {
- which(['which', c], p, function (code, signal, out, err) {
- t.equal(signal, null)
- t.equal(code, 0)
- t.equal(err, '')
- if (/s/.test(c))
- t.equal(out, '', 'should be silent')
- else if (/a/.test(c)) {
- out = out.split(/[\r\n]+/)
- var opt = { actual: out }
- if (process.platform === 'win32') {
- opt.skip = 'windows does not have builtin "which"'
- }
- t.ok(out.length > 0, 'should have a result', opt)
+t.test('cli flags', async (t) => {
+ const p = require('path').dirname(bin)
+
+ for (const c of ['-a', '-s', '-as', '-sa']) {
+ t.test(c, async (t) => {
+ let { code, signal, out, err } = await which(['which', c], p)
+ t.equal(signal, null)
+ t.equal(code, 0)
+ t.equal(err, '')
+ if (/s/.test(c)) {
+ t.equal(out, '', 'should be silent')
+ } else if (/a/.test(c)) {
+ out = out.split(/[\r\n]+/)
+ const opt = { actual: out }
+ if (process.platform === 'win32') {
+ opt.skip = 'windows does not have builtin "which"'
}
- t.end()
- })
+ t.ok(out.length > 0, 'should have a result', opt)
+ }
})
- })
+ }
})
-t.test('shows usage', function (t) {
- which([], function (code, signal, out, err) {
- t.equal(signal, null)
- t.equal(code, 1)
- t.equal(err, 'usage: which [-as] program ...')
- t.equal(out, '')
- t.end()
- })
+t.test('shows usage', async (t) => {
+ const { code, signal, out, err } = await which()
+ t.equal(signal, null)
+ t.equal(code, 1)
+ t.equal(err, 'usage: which [-as] program ...')
+ t.equal(out, '')
})
-t.test('complains about unknown flag', function (t) {
- which(['node', '-sax'], function (code, signal, out, err) {
- t.equal(signal, null)
- t.equal(code, 1)
- t.equal(out, '')
- t.equal(err, 'which: illegal option -- x\nusage: which [-as] program ...')
- t.end()
- })
+t.test('complains about unknown flag', async (t) => {
+ const { code, signal, out, err } = await which(['node', '-sax'])
+ t.equal(signal, null)
+ t.equal(code, 1)
+ t.equal(out, '')
+ t.equal(err, 'which: illegal option -- x\nusage: which [-as] program ...')
+})
+
+t.test('anything after -- is ignored', async (t) => {
+ const { code, signal, out, err } = await which(['node', '--', '--anything-goes-here'])
+ t.equal(signal, null)
+ t.equal(code, 0)
+ t.equal(err, '')
+ t.match(out, /[\\/]node(\.exe)?$/i)
})
diff --git a/test/index.js b/test/index.js
new file mode 100644
index 0000000..555b498
--- /dev/null
+++ b/test/index.js
@@ -0,0 +1,175 @@
+
+const t = require('tap')
+const fs = require('fs')
+const { basename, join, relative, sep, delimiter } = require('path')
+const isWindows = process.platform === 'win32'
+
+const ENV_VARS = { PATH: process.env.PATH, PATHEXT: process.env.PATHEXT }
+const PLATFORM = Object.getOwnPropertyDescriptor(process, 'platform')
+
+const runTest = async (t, exec, expect, { platforms = ['posix', 'win32'], ..._opt } = {}) => {
+ t.teardown(() => {
+ for (const [k, v] of Object.entries(ENV_VARS)) {
+ if (v) {
+ process.env[k] = v
+ } else {
+ delete process.env[k]
+ }
+ }
+ })
+
+ for (const platform of platforms) {
+ t.test(`${t.name} - ${platform}`, async t => {
+ Object.defineProperty(process, 'platform', { ...PLATFORM, value: platform })
+
+ t.teardown(() => {
+ Object.defineProperty(process, 'platform', PLATFORM)
+ })
+
+ // pass in undefined if there are no opts to test default argß
+ const opt = Object.keys(_opt).length ? { ..._opt } : undefined
+
+ // if we are actually on windows but testing posix we have to
+ // mock isexe since that has special windows detection inside
+ // of it. this is mostly to get 100% coverage on windows
+ const mocks = {}
+ if (isWindows && platform === 'posix') {
+ const isexe = async (p) => [].concat(expect).includes(p)
+ isexe.sync = (p) => [].concat(expect).includes(p)
+ mocks.isexe = isexe
+ }
+
+ const which = t.mock('..', mocks)
+ if (expect?.code) {
+ await t.rejects(() => which(exec, opt), expect, 'async rejects')
+ t.throws(() => which.sync(exec, opt), expect, 'sync throws')
+ } else {
+ t.strictSame(await which(exec, opt), expect, 'async')
+ t.strictSame(which.sync(exec, opt), expect, 'sync')
+ }
+ })
+ }
+}
+
+t.test('does not find missed', async (t) => {
+ const fixture = t.testdir()
+ const cmd = join(fixture, 'foobar.sh')
+
+ t.test('throw', async t => {
+ await runTest(t, cmd, { code: 'ENOENT' })
+ })
+ t.test('nothrow', async t => {
+ await runTest(t, cmd, null, { nothrow: true })
+ })
+})
+
+t.test('does not find non-executable', async (t) => {
+ const dir = t.testdir({ 'foo.sh': 'echo foo\n' })
+ const foo = join(dir, 'foo.sh')
+
+ t.test('absolute', async (t) => {
+ await runTest(t, foo, { code: 'ENOENT' })
+ })
+
+ t.test('with path', async (t) => {
+ await runTest(t, basename(foo), { code: 'ENOENT' }, { path: dir })
+ })
+})
+
+t.test('find when executable', async t => {
+ const fixture = t.testdir({ 'foo.sh': 'echo foo\n' })
+ const foo = join(fixture, 'foo.sh')
+ fs.chmodSync(foo, '0755')
+
+ // windows needs to explicitly look for .sh files by default
+ const opts = isWindows ? { pathExt: '.sh' } : {}
+
+ t.test('absolute', async (t) => {
+ await runTest(t, foo, foo, opts)
+ })
+
+ t.test('with process.env.PATH', async (t) => {
+ process.env.PATH = fixture
+ await runTest(t, basename(foo), foo, opts)
+ })
+
+ t.test('with path opt', async (t) => {
+ await runTest(t, basename(foo), foo, { ...opts, path: fixture })
+ })
+
+ t.test('no ./', async (t) => {
+ const rel = relative(process.cwd(), foo)
+ await runTest(t, rel, rel, opts)
+ })
+
+ t.test('with ./', async (t) => {
+ const rel = `.${sep}${relative(process.cwd(), foo)}`
+ await runTest(t, rel, rel, opts)
+ })
+
+ t.test('with ../', async (t) => {
+ const dir = basename(process.cwd())
+ const rel = join('..', dir, relative(process.cwd(), foo))
+ await runTest(t, rel, rel, opts)
+ })
+})
+
+t.test('find all', async t => {
+ const cmdName = 'x.cmd'
+ const fixture = t.testdir({
+ all: {
+ a: { [cmdName]: 'exec me' },
+ b: { [cmdName]: 'exec me' },
+ },
+ })
+ const dirs = [
+ join(fixture, 'all', 'a'),
+ join(fixture, 'all', 'b'),
+ ]
+ const cmds = dirs.map(dir => {
+ const cmd = join(dir, cmdName)
+ fs.chmodSync(cmd, 0o755)
+ return cmd
+ })
+ await runTest(t, cmdName, cmds, {
+ all: true,
+ path: dirs.map((dir, index) => index % 2 ? dir : `"${dir}"`).join(delimiter),
+ })
+})
+
+t.test('pathExt', async (t) => {
+ const fixture = t.testdir({ 'foo.sh': 'echo foo\n' })
+ const foo = join(fixture, 'foo.sh')
+ fs.chmodSync(foo, '0755')
+
+ const pathExt = '.sh'
+ const opts = { platforms: ['win32'] }
+
+ t.test('foo.sh - env vars', async (t) => {
+ process.env.PATHEXT = pathExt
+ process.env.PATH = fixture
+ await runTest(t, basename(foo), foo, opts)
+ })
+
+ t.test('foo.sh - opts', async (t) => {
+ await runTest(t, basename(foo), foo, { ...opts, path: fixture, pathExt })
+ })
+
+ t.test('foo - env vars', async (t) => {
+ process.env.PATHEXT = pathExt
+ process.env.PATH = fixture
+ await runTest(t, basename(foo, '.sh'), foo, opts)
+ })
+
+ t.test('foo - opts', async (t) => {
+ await runTest(t, basename(foo, '.sh'), foo, { ...opts, path: fixture, pathExt })
+ })
+
+ t.test('foo - no pathext', async (t) => {
+ await runTest(t, basename(foo, '.sh'), { code: 'ENOENT' }, {
+ ...opts,
+ path: fixture,
+ pathExt: '',
+ })
+ })
+})
diff --git a/test/windows.js b/test/windows.js
deleted file mode 100644
index 1d5e429..0000000
--- a/test/windows.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// pretend to be Windows.
-if (process.platform === 'win32') {
- var t = require('tap')
- t.plan(0, 'already on windows')
- process.exit(0)
-}
-
-process.env.Path = process.env.PATH.split(':').join(';')
-process.env.OSTYPE = 'cygwin'
-require('./basic.js')
diff --git a/which.js b/which.js
deleted file mode 100644
index 82afffd..0000000
--- a/which.js
+++ /dev/null
@@ -1,125 +0,0 @@
-const isWindows = process.platform === 'win32' ||
- process.env.OSTYPE === 'cygwin' ||
- process.env.OSTYPE === 'msys'
-
-const path = require('path')
-const COLON = isWindows ? ';' : ':'
-const isexe = require('isexe')
-
-const getNotFoundError = (cmd) =>
- Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' })
-
-const getPathInfo = (cmd, opt) => {
- const colon = opt.colon || COLON
-
- // If it has a slash, then we don't bother searching the pathenv.
- // just check the file itself, and that's it.
- const pathEnv = cmd.match(/\//) || isWindows && cmd.match(/\\/) ? ['']
- : (
- [
- // windows always checks the cwd first
- ...(isWindows ? [process.cwd()] : []),
- ...(opt.path || process.env.PATH ||
- /* istanbul ignore next: very unusual */ '').split(colon),
- ]
- )
- const pathExtExe = isWindows
- ? opt.pathExt || process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM'
- : ''
- const pathExt = isWindows ? pathExtExe.split(colon) : ['']
-
- if (isWindows) {
- if (cmd.indexOf('.') !== -1 && pathExt[0] !== '')
- pathExt.unshift('')
- }
-
- return {
- pathEnv,
- pathExt,
- pathExtExe,
- }
-}
-
-const which = (cmd, opt, cb) => {
- if (typeof opt === 'function') {
- cb = opt
- opt = {}
- }
- if (!opt)
- opt = {}
-
- const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt)
- const found = []
-
- const step = i => new Promise((resolve, reject) => {
- if (i === pathEnv.length)
- return opt.all && found.length ? resolve(found)
- : reject(getNotFoundError(cmd))
-
- const ppRaw = pathEnv[i]
- const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw
-
- const pCmd = path.join(pathPart, cmd)
- const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd
- : pCmd
-
- resolve(subStep(p, i, 0))
- })
-
- const subStep = (p, i, ii) => new Promise((resolve, reject) => {
- if (ii === pathExt.length)
- return resolve(step(i + 1))
- const ext = pathExt[ii]
- isexe(p + ext, { pathExt: pathExtExe }, (er, is) => {
- if (!er && is) {
- if (opt.all)
- found.push(p + ext)
- else
- return resolve(p + ext)
- }
- return resolve(subStep(p, i, ii + 1))
- })
- })
-
- return cb ? step(0).then(res => cb(null, res), cb) : step(0)
-}
-
-const whichSync = (cmd, opt) => {
- opt = opt || {}
-
- const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt)
- const found = []
-
- for (let i = 0; i < pathEnv.length; i ++) {
- const ppRaw = pathEnv[i]
- const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw
-
- const pCmd = path.join(pathPart, cmd)
- const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd
- : pCmd
-
- for (let j = 0; j < pathExt.length; j ++) {
- const cur = p + pathExt[j]
- try {
- const is = isexe.sync(cur, { pathExt: pathExtExe })
- if (is) {
- if (opt.all)
- found.push(cur)
- else
- return cur
- }
- } catch (ex) {}
- }
- }
-
- if (opt.all && found.length)
- return found
-
- if (opt.nothrow)
- return null
-
- throw getNotFoundError(cmd)
-}
-
-module.exports = which
-which.sync = whichSync