├── .commitlintrc.js ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── config.yml ├── actions │ ├── create-check │ │ └── action.yml │ └── install-latest-npm │ │ └── action.yml ├── dependabot.yml ├── matchers │ └── tap.json ├── settings.yml └── workflows │ ├── audit.yml │ ├── ci-release.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── post-dependabot.yml │ ├── pull-request.yml │ ├── release-integration.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .release-please-manifest.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── lib ├── escape.js └── index.js ├── package.json ├── release-please-config.json └── test ├── escape.js ├── index.js ├── open.js └── shell.js /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | module.exports = { 4 | extends: ['@commitlint/config-conventional'], 5 | rules: { 6 | 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']], 7 | 'header-max-length': [2, 'always', 80], 8 | 'subject-case': [0], 9 | 'body-max-line-length': [0], 10 | 'footer-max-line-length': [0], 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | 'use strict' 4 | 5 | const { readdirSync: readdir } = require('fs') 6 | 7 | const localConfigs = readdir(__dirname) 8 | .filter((file) => file.startsWith('.eslintrc.local.')) 9 | .map((file) => `./${file}`) 10 | 11 | module.exports = { 12 | root: true, 13 | ignorePatterns: [ 14 | 'tap-testdir*/', 15 | ], 16 | extends: [ 17 | '@npmcli', 18 | ...localConfigs, 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | * @npm/cli-team 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Bug 4 | description: File a bug/issue 5 | title: "[BUG] " 6 | labels: [ Bug, Needs Triage ] 7 | 8 | body: 9 | - type: checkboxes 10 | attributes: 11 | label: Is there an existing issue for this? 12 | description: Please [search here](./issues) to see if an issue already exists for your problem. 13 | options: 14 | - label: I have searched the existing issues 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Current Behavior 19 | description: A clear & concise description of what you're experiencing. 20 | validations: 21 | required: false 22 | - type: textarea 23 | attributes: 24 | label: Expected Behavior 25 | description: A clear & concise description of what you expected to happen. 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: Steps To Reproduce 31 | description: Steps to reproduce the behavior. 32 | value: | 33 | 1. In this environment... 34 | 2. With this config... 35 | 3. Run '...' 36 | 4. See error... 37 | validations: 38 | required: false 39 | - type: textarea 40 | attributes: 41 | label: Environment 42 | description: | 43 | examples: 44 | - **npm**: 7.6.3 45 | - **Node**: 13.14.0 46 | - **OS**: Ubuntu 20.04 47 | - **platform**: Macbook Pro 48 | value: | 49 | - npm: 50 | - Node: 51 | - OS: 52 | - platform: 53 | validations: 54 | required: false 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | blank_issues_enabled: true 4 | -------------------------------------------------------------------------------- /.github/actions/create-check/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Create Check' 4 | inputs: 5 | name: 6 | required: true 7 | token: 8 | required: true 9 | sha: 10 | required: true 11 | check-name: 12 | default: '' 13 | outputs: 14 | check-id: 15 | value: ${{ steps.create-check.outputs.check_id }} 16 | runs: 17 | using: "composite" 18 | steps: 19 | - name: Get Workflow Job 20 | uses: actions/github-script@v7 21 | id: workflow 22 | env: 23 | JOB_NAME: "${{ inputs.name }}" 24 | SHA: "${{ inputs.sha }}" 25 | with: 26 | result-encoding: string 27 | script: | 28 | const { repo: { owner, repo}, runId, serverUrl } = context 29 | const { JOB_NAME, SHA } = process.env 30 | 31 | const job = await github.rest.actions.listJobsForWorkflowRun({ 32 | owner, 33 | repo, 34 | run_id: runId, 35 | per_page: 100 36 | }).then(r => r.data.jobs.find(j => j.name.endsWith(JOB_NAME))) 37 | 38 | return [ 39 | `This check is assosciated with ${serverUrl}/${owner}/${repo}/commit/${SHA}.`, 40 | 'Run logs:', 41 | job?.html_url || `could not be found for a job ending with: "${JOB_NAME}"`, 42 | ].join(' ') 43 | - name: Create Check 44 | uses: LouisBrunner/checks-action@v1.6.0 45 | id: create-check 46 | with: 47 | token: ${{ inputs.token }} 48 | sha: ${{ inputs.sha }} 49 | status: in_progress 50 | name: ${{ inputs.check-name || inputs.name }} 51 | output: | 52 | {"summary":"${{ steps.workflow.outputs.result }}"} 53 | -------------------------------------------------------------------------------- /.github/actions/install-latest-npm/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Install Latest npm' 4 | description: 'Install the latest version of npm compatible with the Node version' 5 | inputs: 6 | node: 7 | description: 'Current Node version' 8 | required: true 9 | runs: 10 | using: "composite" 11 | steps: 12 | # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows 13 | - name: Update Windows npm 14 | if: | 15 | runner.os == 'Windows' && ( 16 | startsWith(inputs.node, 'v10.') || 17 | startsWith(inputs.node, 'v12.') || 18 | startsWith(inputs.node, 'v14.') 19 | ) 20 | shell: cmd 21 | run: | 22 | curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz 23 | tar xf npm-7.5.4.tgz 24 | cd package 25 | node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz 26 | cd .. 27 | rmdir /s /q package 28 | - name: Install Latest npm 29 | shell: bash 30 | env: 31 | NODE_VERSION: ${{ inputs.node }} 32 | working-directory: ${{ runner.temp }} 33 | run: | 34 | MATCH="" 35 | SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") 36 | 37 | echo "node@$NODE_VERSION" 38 | 39 | for SPEC in ${SPECS[@]}; do 40 | ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') 41 | echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" 42 | 43 | if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then 44 | MATCH=$SPEC 45 | echo "Found compatible version: npm@$MATCH" 46 | break 47 | fi 48 | done 49 | 50 | if [ -z $MATCH ]; then 51 | echo "Could not find a compatible version of npm for node@$NODE_VERSION" 52 | exit 1 53 | fi 54 | 55 | npm i --prefer-online --no-fund --no-audit -g npm@$MATCH 56 | - name: npm Version 57 | shell: bash 58 | run: npm -v 59 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: npm 7 | directory: / 8 | schedule: 9 | interval: daily 10 | target-branch: "main" 11 | allow: 12 | - dependency-type: direct 13 | versioning-strategy: increase-if-necessary 14 | commit-message: 15 | prefix: deps 16 | prefix-development: chore 17 | labels: 18 | - "Dependencies" 19 | open-pull-requests-limit: 10 20 | -------------------------------------------------------------------------------- /.github/matchers/tap.json: -------------------------------------------------------------------------------- 1 | { 2 | "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.", 3 | "problemMatcher": [ 4 | { 5 | "owner": "tap", 6 | "pattern": [ 7 | { 8 | "regexp": "^\\s*not ok \\d+ - (.*)", 9 | "message": 1 10 | }, 11 | { 12 | "regexp": "^\\s*---" 13 | }, 14 | { 15 | "regexp": "^\\s*at:" 16 | }, 17 | { 18 | "regexp": "^\\s*line:\\s*(\\d+)", 19 | "line": 1 20 | }, 21 | { 22 | "regexp": "^\\s*column:\\s*(\\d+)", 23 | "column": 1 24 | }, 25 | { 26 | "regexp": "^\\s*file:\\s*(.*)", 27 | "file": 1 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | repository: 4 | allow_merge_commit: false 5 | allow_rebase_merge: true 6 | allow_squash_merge: true 7 | squash_merge_commit_title: PR_TITLE 8 | squash_merge_commit_message: PR_BODY 9 | delete_branch_on_merge: true 10 | enable_automated_security_fixes: true 11 | enable_vulnerability_alerts: true 12 | 13 | branches: 14 | - name: main 15 | protection: 16 | required_status_checks: null 17 | enforce_admins: true 18 | block_creations: true 19 | required_pull_request_reviews: 20 | required_approving_review_count: 1 21 | require_code_owner_reviews: true 22 | require_last_push_approval: true 23 | dismiss_stale_reviews: true 24 | restrictions: 25 | apps: [] 26 | users: [] 27 | teams: [ "cli-team" ] 28 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Audit 4 | 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 9 | - cron: "0 8 * * 1" 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | audit: 16 | name: Audit Dependencies 17 | if: github.repository_owner == 'npm' 18 | runs-on: ubuntu-latest 19 | defaults: 20 | run: 21 | shell: bash 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Setup Git User 26 | run: | 27 | git config --global user.email "npm-cli+bot@github.com" 28 | git config --global user.name "npm CLI robot" 29 | - name: Setup Node 30 | uses: actions/setup-node@v4 31 | id: node 32 | with: 33 | node-version: 22.x 34 | check-latest: contains('22.x', '.x') 35 | - name: Install Latest npm 36 | uses: ./.github/actions/install-latest-npm 37 | with: 38 | node: ${{ steps.node.outputs.node-version }} 39 | - name: Install Dependencies 40 | run: npm i --ignore-scripts --no-audit --no-fund --package-lock 41 | - name: Run Production Audit 42 | run: npm audit --omit=dev 43 | - name: Run Full Audit 44 | run: npm audit --audit-level=none 45 | -------------------------------------------------------------------------------- /.github/workflows/ci-release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI - Release 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | ref: 9 | required: true 10 | type: string 11 | default: main 12 | workflow_call: 13 | inputs: 14 | ref: 15 | required: true 16 | type: string 17 | check-sha: 18 | required: true 19 | type: string 20 | 21 | permissions: 22 | contents: read 23 | checks: write 24 | 25 | jobs: 26 | lint-all: 27 | name: Lint All 28 | if: github.repository_owner == 'npm' 29 | runs-on: ubuntu-latest 30 | defaults: 31 | run: 32 | shell: bash 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | with: 37 | ref: ${{ inputs.ref }} 38 | - name: Setup Git User 39 | run: | 40 | git config --global user.email "npm-cli+bot@github.com" 41 | git config --global user.name "npm CLI robot" 42 | - name: Create Check 43 | id: create-check 44 | if: ${{ inputs.check-sha }} 45 | uses: ./.github/actions/create-check 46 | with: 47 | name: "Lint All" 48 | token: ${{ secrets.GITHUB_TOKEN }} 49 | sha: ${{ inputs.check-sha }} 50 | - name: Setup Node 51 | uses: actions/setup-node@v4 52 | id: node 53 | with: 54 | node-version: 22.x 55 | check-latest: contains('22.x', '.x') 56 | - name: Install Latest npm 57 | uses: ./.github/actions/install-latest-npm 58 | with: 59 | node: ${{ steps.node.outputs.node-version }} 60 | - name: Install Dependencies 61 | run: npm i --ignore-scripts --no-audit --no-fund 62 | - name: Lint 63 | run: npm run lint --ignore-scripts 64 | - name: Post Lint 65 | run: npm run postlint --ignore-scripts 66 | - name: Conclude Check 67 | uses: LouisBrunner/checks-action@v1.6.0 68 | if: steps.create-check.outputs.check-id && always() 69 | with: 70 | token: ${{ secrets.GITHUB_TOKEN }} 71 | conclusion: ${{ job.status }} 72 | check_id: ${{ steps.create-check.outputs.check-id }} 73 | 74 | test-all: 75 | name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 76 | if: github.repository_owner == 'npm' 77 | strategy: 78 | fail-fast: false 79 | matrix: 80 | platform: 81 | - name: Linux 82 | os: ubuntu-latest 83 | shell: bash 84 | - name: macOS 85 | os: macos-latest 86 | shell: bash 87 | - name: macOS 88 | os: macos-13 89 | shell: bash 90 | - name: Windows 91 | os: windows-latest 92 | shell: cmd 93 | node-version: 94 | - 18.17.0 95 | - 18.x 96 | - 20.5.0 97 | - 20.x 98 | - 22.x 99 | exclude: 100 | - platform: { name: macOS, os: macos-13, shell: bash } 101 | node-version: 18.17.0 102 | - platform: { name: macOS, os: macos-13, shell: bash } 103 | node-version: 18.x 104 | - platform: { name: macOS, os: macos-13, shell: bash } 105 | node-version: 20.5.0 106 | - platform: { name: macOS, os: macos-13, shell: bash } 107 | node-version: 20.x 108 | - platform: { name: macOS, os: macos-13, shell: bash } 109 | node-version: 22.x 110 | runs-on: ${{ matrix.platform.os }} 111 | defaults: 112 | run: 113 | shell: ${{ matrix.platform.shell }} 114 | steps: 115 | - name: Checkout 116 | uses: actions/checkout@v4 117 | with: 118 | ref: ${{ inputs.ref }} 119 | - name: Setup Git User 120 | run: | 121 | git config --global user.email "npm-cli+bot@github.com" 122 | git config --global user.name "npm CLI robot" 123 | - name: Create Check 124 | id: create-check 125 | if: ${{ inputs.check-sha }} 126 | uses: ./.github/actions/create-check 127 | with: 128 | name: "Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" 129 | token: ${{ secrets.GITHUB_TOKEN }} 130 | sha: ${{ inputs.check-sha }} 131 | - name: Setup Node 132 | uses: actions/setup-node@v4 133 | id: node 134 | with: 135 | node-version: ${{ matrix.node-version }} 136 | check-latest: contains(matrix.node-version, '.x') 137 | - name: Install Latest npm 138 | uses: ./.github/actions/install-latest-npm 139 | with: 140 | node: ${{ steps.node.outputs.node-version }} 141 | - name: Install Dependencies 142 | run: npm i --ignore-scripts --no-audit --no-fund 143 | - name: Add Problem Matcher 144 | run: echo "::add-matcher::.github/matchers/tap.json" 145 | - name: Test 146 | run: npm test --ignore-scripts 147 | - name: Conclude Check 148 | uses: LouisBrunner/checks-action@v1.6.0 149 | if: steps.create-check.outputs.check-id && always() 150 | with: 151 | token: ${{ secrets.GITHUB_TOKEN }} 152 | conclusion: ${{ job.status }} 153 | check_id: ${{ steps.create-check.outputs.check-id }} 154 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | schedule: 12 | # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 13 | - cron: "0 9 * * 1" 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | lint: 20 | name: Lint 21 | if: github.repository_owner == 'npm' 22 | runs-on: ubuntu-latest 23 | defaults: 24 | run: 25 | shell: bash 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Setup Git User 30 | run: | 31 | git config --global user.email "npm-cli+bot@github.com" 32 | git config --global user.name "npm CLI robot" 33 | - name: Setup Node 34 | uses: actions/setup-node@v4 35 | id: node 36 | with: 37 | node-version: 22.x 38 | check-latest: contains('22.x', '.x') 39 | - name: Install Latest npm 40 | uses: ./.github/actions/install-latest-npm 41 | with: 42 | node: ${{ steps.node.outputs.node-version }} 43 | - name: Install Dependencies 44 | run: npm i --ignore-scripts --no-audit --no-fund 45 | - name: Lint 46 | run: npm run lint --ignore-scripts 47 | - name: Post Lint 48 | run: npm run postlint --ignore-scripts 49 | 50 | test: 51 | name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 52 | if: github.repository_owner == 'npm' 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | platform: 57 | - name: Linux 58 | os: ubuntu-latest 59 | shell: bash 60 | - name: macOS 61 | os: macos-latest 62 | shell: bash 63 | - name: macOS 64 | os: macos-13 65 | shell: bash 66 | - name: Windows 67 | os: windows-latest 68 | shell: cmd 69 | node-version: 70 | - 18.17.0 71 | - 18.x 72 | - 20.5.0 73 | - 20.x 74 | - 22.x 75 | exclude: 76 | - platform: { name: macOS, os: macos-13, shell: bash } 77 | node-version: 18.17.0 78 | - platform: { name: macOS, os: macos-13, shell: bash } 79 | node-version: 18.x 80 | - platform: { name: macOS, os: macos-13, shell: bash } 81 | node-version: 20.5.0 82 | - platform: { name: macOS, os: macos-13, shell: bash } 83 | node-version: 20.x 84 | - platform: { name: macOS, os: macos-13, shell: bash } 85 | node-version: 22.x 86 | runs-on: ${{ matrix.platform.os }} 87 | defaults: 88 | run: 89 | shell: ${{ matrix.platform.shell }} 90 | steps: 91 | - name: Checkout 92 | uses: actions/checkout@v4 93 | - name: Setup Git User 94 | run: | 95 | git config --global user.email "npm-cli+bot@github.com" 96 | git config --global user.name "npm CLI robot" 97 | - name: Setup Node 98 | uses: actions/setup-node@v4 99 | id: node 100 | with: 101 | node-version: ${{ matrix.node-version }} 102 | check-latest: contains(matrix.node-version, '.x') 103 | - name: Install Latest npm 104 | uses: ./.github/actions/install-latest-npm 105 | with: 106 | node: ${{ steps.node.outputs.node-version }} 107 | - name: Install Dependencies 108 | run: npm i --ignore-scripts --no-audit --no-fund 109 | - name: Add Problem Matcher 110 | run: echo "::add-matcher::.github/matchers/tap.json" 111 | - name: Test 112 | run: npm test --ignore-scripts 113 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CodeQL 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | schedule: 13 | # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 14 | - cron: "0 10 * * 1" 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | analyze: 21 | name: Analyze 22 | runs-on: ubuntu-latest 23 | permissions: 24 | actions: read 25 | contents: read 26 | security-events: write 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Setup Git User 31 | run: | 32 | git config --global user.email "npm-cli+bot@github.com" 33 | git config --global user.name "npm CLI robot" 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | with: 37 | languages: javascript 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | -------------------------------------------------------------------------------- /.github/workflows/post-dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Post Dependabot 4 | 5 | on: pull_request 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | template-oss: 12 | name: template-oss 13 | if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | shell: bash 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.event.pull_request.head.ref }} 23 | - name: Setup Git User 24 | run: | 25 | git config --global user.email "npm-cli+bot@github.com" 26 | git config --global user.name "npm CLI robot" 27 | - name: Setup Node 28 | uses: actions/setup-node@v4 29 | id: node 30 | with: 31 | node-version: 22.x 32 | check-latest: contains('22.x', '.x') 33 | - name: Install Latest npm 34 | uses: ./.github/actions/install-latest-npm 35 | with: 36 | node: ${{ steps.node.outputs.node-version }} 37 | - name: Install Dependencies 38 | run: npm i --ignore-scripts --no-audit --no-fund 39 | - name: Fetch Dependabot Metadata 40 | id: metadata 41 | uses: dependabot/fetch-metadata@v1 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | # Dependabot can update multiple directories so we output which directory 46 | # it is acting on so we can run the command for the correct root or workspace 47 | - name: Get Dependabot Directory 48 | if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') 49 | id: flags 50 | run: | 51 | dependabot_dir="${{ steps.metadata.outputs.directory }}" 52 | if [[ "$dependabot_dir" == "/" || "$dependabot_dir" == "/main" ]]; then 53 | echo "workspace=-iwr" >> $GITHUB_OUTPUT 54 | else 55 | # strip leading slash from directory so it works as a 56 | # a path to the workspace flag 57 | echo "workspace=-w ${dependabot_dir#/}" >> $GITHUB_OUTPUT 58 | fi 59 | 60 | - name: Apply Changes 61 | if: steps.flags.outputs.workspace 62 | id: apply 63 | run: | 64 | npm run template-oss-apply ${{ steps.flags.outputs.workspace }} 65 | if [[ `git status --porcelain` ]]; then 66 | echo "changes=true" >> $GITHUB_OUTPUT 67 | fi 68 | # This only sets the conventional commit prefix. This workflow can't reliably determine 69 | # what the breaking change is though. If a BREAKING CHANGE message is required then 70 | # this PR check will fail and the commit will be amended with stafftools 71 | if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then 72 | prefix='feat!' 73 | else 74 | prefix='chore' 75 | fi 76 | echo "message=$prefix: postinstall for dependabot template-oss PR" >> $GITHUB_OUTPUT 77 | 78 | # This step will fail if template-oss has made any workflow updates. It is impossible 79 | # for a workflow to update other workflows. In the case it does fail, we continue 80 | # and then try to apply only a portion of the changes in the next step 81 | - name: Push All Changes 82 | if: steps.apply.outputs.changes 83 | id: push 84 | continue-on-error: true 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | run: | 88 | git commit -am "${{ steps.apply.outputs.message }}" 89 | git push 90 | 91 | # If the previous step failed, then reset the commit and remove any workflow changes 92 | # and attempt to commit and push again. This is helpful because we will have a commit 93 | # with the correct prefix that we can then --amend with @npmcli/stafftools later. 94 | - name: Push All Changes Except Workflows 95 | if: steps.apply.outputs.changes && steps.push.outcome == 'failure' 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | run: | 99 | git reset HEAD~ 100 | git checkout HEAD -- .github/workflows/ 101 | git clean -fd .github/workflows/ 102 | git commit -am "${{ steps.apply.outputs.message }}" 103 | git push 104 | 105 | # Check if all the necessary template-oss changes were applied. Since we continued 106 | # on errors in one of the previous steps, this check will fail if our follow up 107 | # only applied a portion of the changes and we need to followup manually. 108 | # 109 | # Note that this used to run `lint` and `postlint` but that will fail this action 110 | # if we've also shipped any linting changes separate from template-oss. We do 111 | # linting in another action, so we want to fail this one only if there are 112 | # template-oss changes that could not be applied. 113 | - name: Check Changes 114 | if: steps.apply.outputs.changes 115 | run: | 116 | npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check 117 | 118 | - name: Fail on Breaking Change 119 | if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') 120 | run: | 121 | echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'" 122 | echo "for more information on how to fix this with a BREAKING CHANGE footer." 123 | exit 1 124 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Pull Request 4 | 5 | on: 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - edited 11 | - synchronize 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | commitlint: 18 | name: Lint Commits 19 | if: github.repository_owner == 'npm' 20 | runs-on: ubuntu-latest 21 | defaults: 22 | run: 23 | shell: bash 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | - name: Setup Git User 30 | run: | 31 | git config --global user.email "npm-cli+bot@github.com" 32 | git config --global user.name "npm CLI robot" 33 | - name: Setup Node 34 | uses: actions/setup-node@v4 35 | id: node 36 | with: 37 | node-version: 22.x 38 | check-latest: contains('22.x', '.x') 39 | - name: Install Latest npm 40 | uses: ./.github/actions/install-latest-npm 41 | with: 42 | node: ${{ steps.node.outputs.node-version }} 43 | - name: Install Dependencies 44 | run: npm i --ignore-scripts --no-audit --no-fund 45 | - name: Run Commitlint on Commits 46 | id: commit 47 | continue-on-error: true 48 | run: npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }} 49 | - name: Run Commitlint on PR Title 50 | if: steps.commit.outcome == 'failure' 51 | env: 52 | PR_TITLE: ${{ github.event.pull_request.title }} 53 | run: echo "$PR_TITLE" | npx --offline commitlint -V 54 | -------------------------------------------------------------------------------- /.github/workflows/release-integration.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release Integration 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | releases: 9 | required: true 10 | type: string 11 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 12 | workflow_call: 13 | inputs: 14 | releases: 15 | required: true 16 | type: string 17 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 18 | secrets: 19 | PUBLISH_TOKEN: 20 | required: true 21 | 22 | permissions: 23 | contents: read 24 | id-token: write 25 | 26 | jobs: 27 | publish: 28 | name: Publish 29 | runs-on: ubuntu-latest 30 | defaults: 31 | run: 32 | shell: bash 33 | permissions: 34 | id-token: write 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | with: 39 | ref: ${{ fromJSON(inputs.releases)[0].tagName }} 40 | - name: Setup Git User 41 | run: | 42 | git config --global user.email "npm-cli+bot@github.com" 43 | git config --global user.name "npm CLI robot" 44 | - name: Setup Node 45 | uses: actions/setup-node@v4 46 | id: node 47 | with: 48 | node-version: 22.x 49 | check-latest: contains('22.x', '.x') 50 | - name: Install Latest npm 51 | uses: ./.github/actions/install-latest-npm 52 | with: 53 | node: ${{ steps.node.outputs.node-version }} 54 | - name: Install Dependencies 55 | run: npm i --ignore-scripts --no-audit --no-fund 56 | - name: Set npm authToken 57 | run: npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN} 58 | - name: Publish 59 | env: 60 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 61 | RELEASES: ${{ inputs.releases }} 62 | run: | 63 | EXIT_CODE=0 64 | 65 | for release in $(echo $RELEASES | jq -r '.[] | @base64'); do 66 | PUBLISH_TAG=$(echo "$release" | base64 --decode | jq -r .publishTag) 67 | npm publish --provenance --tag="$PUBLISH_TAG" 68 | STATUS=$? 69 | if [[ "$STATUS" -eq 1 ]]; then 70 | EXIT_CODE=$STATUS 71 | fi 72 | done 73 | 74 | exit $EXIT_CODE 75 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | checks: write 14 | 15 | jobs: 16 | release: 17 | outputs: 18 | pr: ${{ steps.release.outputs.pr }} 19 | pr-branch: ${{ steps.release.outputs.pr-branch }} 20 | pr-number: ${{ steps.release.outputs.pr-number }} 21 | pr-sha: ${{ steps.release.outputs.pr-sha }} 22 | releases: ${{ steps.release.outputs.releases }} 23 | comment-id: ${{ steps.create-comment.outputs.comment-id || steps.update-comment.outputs.comment-id }} 24 | check-id: ${{ steps.create-check.outputs.check-id }} 25 | name: Release 26 | if: github.repository_owner == 'npm' 27 | runs-on: ubuntu-latest 28 | defaults: 29 | run: 30 | shell: bash 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Git User 35 | run: | 36 | git config --global user.email "npm-cli+bot@github.com" 37 | git config --global user.name "npm CLI robot" 38 | - name: Setup Node 39 | uses: actions/setup-node@v4 40 | id: node 41 | with: 42 | node-version: 22.x 43 | check-latest: contains('22.x', '.x') 44 | - name: Install Latest npm 45 | uses: ./.github/actions/install-latest-npm 46 | with: 47 | node: ${{ steps.node.outputs.node-version }} 48 | - name: Install Dependencies 49 | run: npm i --ignore-scripts --no-audit --no-fund 50 | - name: Release Please 51 | id: release 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | run: npx --offline template-oss-release-please --branch="${{ github.ref_name }}" --backport="" --defaultTag="latest" 55 | - name: Create Release Manager Comment Text 56 | if: steps.release.outputs.pr-number 57 | uses: actions/github-script@v7 58 | id: comment-text 59 | with: 60 | result-encoding: string 61 | script: | 62 | const { runId, repo: { owner, repo } } = context 63 | const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) 64 | return['## Release Manager', `Release workflow run: ${workflow.html_url}`].join('\n\n') 65 | - name: Find Release Manager Comment 66 | uses: peter-evans/find-comment@v2 67 | if: steps.release.outputs.pr-number 68 | id: found-comment 69 | with: 70 | issue-number: ${{ steps.release.outputs.pr-number }} 71 | comment-author: 'github-actions[bot]' 72 | body-includes: '## Release Manager' 73 | - name: Create Release Manager Comment 74 | id: create-comment 75 | if: steps.release.outputs.pr-number && !steps.found-comment.outputs.comment-id 76 | uses: peter-evans/create-or-update-comment@v3 77 | with: 78 | issue-number: ${{ steps.release.outputs.pr-number }} 79 | body: ${{ steps.comment-text.outputs.result }} 80 | - name: Update Release Manager Comment 81 | id: update-comment 82 | if: steps.release.outputs.pr-number && steps.found-comment.outputs.comment-id 83 | uses: peter-evans/create-or-update-comment@v3 84 | with: 85 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 86 | body: ${{ steps.comment-text.outputs.result }} 87 | edit-mode: 'replace' 88 | - name: Create Check 89 | id: create-check 90 | uses: ./.github/actions/create-check 91 | if: steps.release.outputs.pr-sha 92 | with: 93 | name: "Release" 94 | token: ${{ secrets.GITHUB_TOKEN }} 95 | sha: ${{ steps.release.outputs.pr-sha }} 96 | 97 | update: 98 | needs: release 99 | outputs: 100 | sha: ${{ steps.commit.outputs.sha }} 101 | check-id: ${{ steps.create-check.outputs.check-id }} 102 | name: Update - Release 103 | if: github.repository_owner == 'npm' && needs.release.outputs.pr 104 | runs-on: ubuntu-latest 105 | defaults: 106 | run: 107 | shell: bash 108 | steps: 109 | - name: Checkout 110 | uses: actions/checkout@v4 111 | with: 112 | fetch-depth: 0 113 | ref: ${{ needs.release.outputs.pr-branch }} 114 | - name: Setup Git User 115 | run: | 116 | git config --global user.email "npm-cli+bot@github.com" 117 | git config --global user.name "npm CLI robot" 118 | - name: Setup Node 119 | uses: actions/setup-node@v4 120 | id: node 121 | with: 122 | node-version: 22.x 123 | check-latest: contains('22.x', '.x') 124 | - name: Install Latest npm 125 | uses: ./.github/actions/install-latest-npm 126 | with: 127 | node: ${{ steps.node.outputs.node-version }} 128 | - name: Install Dependencies 129 | run: npm i --ignore-scripts --no-audit --no-fund 130 | - name: Create Release Manager Checklist Text 131 | id: comment-text 132 | env: 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | run: npm exec --offline -- template-oss-release-manager --pr="${{ needs.release.outputs.pr-number }}" --backport="" --defaultTag="latest" --publish 135 | - name: Append Release Manager Comment 136 | uses: peter-evans/create-or-update-comment@v3 137 | with: 138 | comment-id: ${{ needs.release.outputs.comment-id }} 139 | body: ${{ steps.comment-text.outputs.result }} 140 | edit-mode: 'append' 141 | - name: Run Post Pull Request Actions 142 | env: 143 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 144 | run: npm run rp-pull-request --ignore-scripts --if-present -- --pr="${{ needs.release.outputs.pr-number }}" --commentId="${{ needs.release.outputs.comment-id }}" 145 | - name: Commit 146 | id: commit 147 | env: 148 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 149 | run: | 150 | git commit --all --amend --no-edit || true 151 | git push --force-with-lease 152 | echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 153 | - name: Create Check 154 | id: create-check 155 | uses: ./.github/actions/create-check 156 | with: 157 | name: "Update - Release" 158 | check-name: "Release" 159 | token: ${{ secrets.GITHUB_TOKEN }} 160 | sha: ${{ steps.commit.outputs.sha }} 161 | - name: Conclude Check 162 | uses: LouisBrunner/checks-action@v1.6.0 163 | with: 164 | token: ${{ secrets.GITHUB_TOKEN }} 165 | conclusion: ${{ job.status }} 166 | check_id: ${{ needs.release.outputs.check-id }} 167 | 168 | ci: 169 | name: CI - Release 170 | needs: [ release, update ] 171 | if: needs.release.outputs.pr 172 | uses: ./.github/workflows/ci-release.yml 173 | with: 174 | ref: ${{ needs.release.outputs.pr-branch }} 175 | check-sha: ${{ needs.update.outputs.sha }} 176 | 177 | post-ci: 178 | needs: [ release, update, ci ] 179 | name: Post CI - Release 180 | if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() 181 | runs-on: ubuntu-latest 182 | defaults: 183 | run: 184 | shell: bash 185 | steps: 186 | - name: Get CI Conclusion 187 | id: conclusion 188 | run: | 189 | result="" 190 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 191 | result="failure" 192 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 193 | result="cancelled" 194 | else 195 | result="success" 196 | fi 197 | echo "result=$result" >> $GITHUB_OUTPUT 198 | - name: Conclude Check 199 | uses: LouisBrunner/checks-action@v1.6.0 200 | with: 201 | token: ${{ secrets.GITHUB_TOKEN }} 202 | conclusion: ${{ steps.conclusion.outputs.result }} 203 | check_id: ${{ needs.update.outputs.check-id }} 204 | 205 | post-release: 206 | needs: release 207 | outputs: 208 | comment-id: ${{ steps.create-comment.outputs.comment-id }} 209 | name: Post Release - Release 210 | if: github.repository_owner == 'npm' && needs.release.outputs.releases 211 | runs-on: ubuntu-latest 212 | defaults: 213 | run: 214 | shell: bash 215 | steps: 216 | - name: Create Release PR Comment Text 217 | id: comment-text 218 | uses: actions/github-script@v7 219 | env: 220 | RELEASES: ${{ needs.release.outputs.releases }} 221 | with: 222 | result-encoding: string 223 | script: | 224 | const releases = JSON.parse(process.env.RELEASES) 225 | const { runId, repo: { owner, repo } } = context 226 | const issue_number = releases[0].prNumber 227 | const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}` 228 | 229 | return [ 230 | '## Release Workflow\n', 231 | ...releases.map(r => `- \`${r.pkgName}@${r.version}\` ${r.url}`), 232 | `- Workflow run: :arrows_counterclockwise: ${runUrl}`, 233 | ].join('\n') 234 | - name: Create Release PR Comment 235 | id: create-comment 236 | uses: peter-evans/create-or-update-comment@v3 237 | with: 238 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 239 | body: ${{ steps.comment-text.outputs.result }} 240 | 241 | release-integration: 242 | needs: release 243 | name: Release Integration 244 | if: needs.release.outputs.releases 245 | uses: ./.github/workflows/release-integration.yml 246 | permissions: 247 | contents: read 248 | id-token: write 249 | secrets: 250 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 251 | with: 252 | releases: ${{ needs.release.outputs.releases }} 253 | 254 | post-release-integration: 255 | needs: [ release, release-integration, post-release ] 256 | name: Post Release Integration - Release 257 | if: github.repository_owner == 'npm' && needs.release.outputs.releases && always() 258 | runs-on: ubuntu-latest 259 | defaults: 260 | run: 261 | shell: bash 262 | steps: 263 | - name: Get Post Release Conclusion 264 | id: conclusion 265 | run: | 266 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 267 | result="x" 268 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 269 | result="heavy_multiplication_x" 270 | else 271 | result="white_check_mark" 272 | fi 273 | echo "result=$result" >> $GITHUB_OUTPUT 274 | - name: Find Release PR Comment 275 | uses: peter-evans/find-comment@v2 276 | id: found-comment 277 | with: 278 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 279 | comment-author: 'github-actions[bot]' 280 | body-includes: '## Release Workflow' 281 | - name: Create Release PR Comment Text 282 | id: comment-text 283 | if: steps.found-comment.outputs.comment-id 284 | uses: actions/github-script@v7 285 | env: 286 | RESULT: ${{ steps.conclusion.outputs.result }} 287 | BODY: ${{ steps.found-comment.outputs.comment-body }} 288 | with: 289 | result-encoding: string 290 | script: | 291 | const { RESULT, BODY } = process.env 292 | const body = [BODY.replace(/(Workflow run: :)[a-z_]+(:)/, `$1${RESULT}$2`)] 293 | if (RESULT !== 'white_check_mark') { 294 | body.push(':rotating_light::rotating_light::rotating_light:') 295 | body.push([ 296 | '@npm/cli-team: The post-release workflow failed for this release.', 297 | 'Manual steps may need to be taken after examining the workflow output.' 298 | ].join(' ')) 299 | body.push(':rotating_light::rotating_light::rotating_light:') 300 | } 301 | return body.join('\n\n').trim() 302 | - name: Update Release PR Comment 303 | if: steps.comment-text.outputs.result 304 | uses: peter-evans/create-or-update-comment@v3 305 | with: 306 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 307 | body: ${{ steps.comment-text.outputs.result }} 308 | edit-mode: 'replace' 309 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | # ignore everything in the root 4 | /* 5 | 6 | !**/.gitignore 7 | !/.commitlintrc.js 8 | !/.eslint.config.js 9 | !/.eslintrc.js 10 | !/.eslintrc.local.* 11 | !/.git-blame-ignore-revs 12 | !/.github/ 13 | !/.gitignore 14 | !/.npmrc 15 | !/.prettierignore 16 | !/.prettierrc.js 17 | !/.release-please-manifest.json 18 | !/bin/ 19 | !/CHANGELOG* 20 | !/CODE_OF_CONDUCT.md 21 | !/CONTRIBUTING.md 22 | !/docs/ 23 | !/lib/ 24 | !/LICENSE* 25 | !/map.js 26 | !/package.json 27 | !/README* 28 | !/release-please-config.json 29 | !/scripts/ 30 | !/SECURITY.md 31 | !/tap-snapshots/ 32 | !/test/ 33 | !/tsconfig.json 34 | tap-testdir*/ 35 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ; This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | package-lock=false 4 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "8.0.2" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [8.0.2](https://github.com/npm/promise-spawn/compare/v8.0.1...v8.0.2) (2024-10-18) 4 | ### Bug Fixes 5 | * [`5ecf301`](https://github.com/npm/promise-spawn/commit/5ecf3016c8252a35f7b84fe7e63ca1f97e3ce6d7) [#128](https://github.com/npm/promise-spawn/pull/128) open URL in browser on WSL (#128) (@mbtools, @wraithgar) 6 | ### Chores 7 | * [`ef4ba09`](https://github.com/npm/promise-spawn/commit/ef4ba0994104927011160f1ef4e7256335e16544) [#127](https://github.com/npm/promise-spawn/pull/127) bump @npmcli/template-oss from 4.23.3 to 4.23.4 (#127) (@dependabot[bot], @npm-cli-bot) 8 | 9 | ## [8.0.1](https://github.com/npm/promise-spawn/compare/v8.0.0...v8.0.1) (2024-10-02) 10 | ### Dependencies 11 | * [`606edd3`](https://github.com/npm/promise-spawn/commit/606edd301ac120f4bc4e710846b2581659f12eaf) [#124](https://github.com/npm/promise-spawn/pull/124) bump `which@5.0.0` 12 | 13 | ## [8.0.0](https://github.com/npm/promise-spawn/compare/v7.0.2...v8.0.0) (2024-09-03) 14 | ### ⚠️ BREAKING CHANGES 15 | * `@npmcli/promise-spawn` now supports node `^18.17.0 || >=20.5.0` 16 | ### Bug Fixes 17 | * [`eeaf662`](https://github.com/npm/promise-spawn/commit/eeaf66200f401ff3c53d99c152c56b0e9b20fab2) [#122](https://github.com/npm/promise-spawn/pull/122) align to npm 10 node engine range (@hashtagchris) 18 | ### Chores 19 | * [`392dc76`](https://github.com/npm/promise-spawn/commit/392dc767061f672fc44063227a9454511cffd2ec) [#122](https://github.com/npm/promise-spawn/pull/122) run template-oss-apply (@hashtagchris) 20 | * [`9ed7cbe`](https://github.com/npm/promise-spawn/commit/9ed7cbeac0089ae0a4386cb5a2bab83c733d46bd) [#120](https://github.com/npm/promise-spawn/pull/120) bump @npmcli/eslint-config from 4.0.5 to 5.0.0 (@dependabot[bot]) 21 | * [`63cc0e6`](https://github.com/npm/promise-spawn/commit/63cc0e6636cd99699ec953b5943003ceed938f4e) [#121](https://github.com/npm/promise-spawn/pull/121) postinstall for dependabot template-oss PR (@hashtagchris) 22 | * [`a784586`](https://github.com/npm/promise-spawn/commit/a784586feba3d93f03abcc1c49563b0d546e6407) [#121](https://github.com/npm/promise-spawn/pull/121) bump @npmcli/template-oss from 4.23.1 to 4.23.3 (@dependabot[bot]) 23 | 24 | ## [7.0.2](https://github.com/npm/promise-spawn/compare/v7.0.1...v7.0.2) (2024-05-04) 25 | 26 | ### Bug Fixes 27 | 28 | * [`4912015`](https://github.com/npm/promise-spawn/commit/491201572c19d4f85c2461df9e05638f6d5397a2) [#102](https://github.com/npm/promise-spawn/pull/102) reject with error from parent context on close (#102) (@lukekarrys) 29 | 30 | ### Chores 31 | 32 | * [`09872d7`](https://github.com/npm/promise-spawn/commit/09872d77491cf40c0b7702bf2acb426c8a55eeb7) [#105](https://github.com/npm/promise-spawn/pull/105) linting: no-unused-vars (@lukekarrys) 33 | * [`70f0eb7`](https://github.com/npm/promise-spawn/commit/70f0eb7329adf97fdacb4a01ee656dbde1653634) [#105](https://github.com/npm/promise-spawn/pull/105) bump @npmcli/template-oss to 4.22.0 (@lukekarrys) 34 | * [`82ae2a7`](https://github.com/npm/promise-spawn/commit/82ae2a704bc01758492cd791255d415c36e4cf0b) [#105](https://github.com/npm/promise-spawn/pull/105) postinstall for dependabot template-oss PR (@lukekarrys) 35 | * [`2855879`](https://github.com/npm/promise-spawn/commit/2855879bc22b3a1b6b25762bc4816799839e0a92) [#104](https://github.com/npm/promise-spawn/pull/104) bump @npmcli/template-oss from 4.21.3 to 4.21.4 (@dependabot[bot]) 36 | 37 | ## [7.0.1](https://github.com/npm/promise-spawn/compare/v7.0.0...v7.0.1) (2023-12-21) 38 | 39 | ### Bug Fixes 40 | 41 | * [`46fad5a`](https://github.com/npm/promise-spawn/commit/46fad5a1dec6fe7ad182373d9c0a651d18ff3231) [#98](https://github.com/npm/promise-spawn/pull/98) parse `options.env` more similarly to `process.env` (#98) (@thecodrr) 42 | 43 | ### Chores 44 | 45 | * [`d3ba687`](https://github.com/npm/promise-spawn/commit/d3ba6875797c87ca4c044dbff9a8c5de849cbcca) [#97](https://github.com/npm/promise-spawn/pull/97) postinstall for dependabot template-oss PR (@lukekarrys) 46 | * [`cf18492`](https://github.com/npm/promise-spawn/commit/cf1849244ba7e8f0b3e51752a86ddb097ddc8c74) [#97](https://github.com/npm/promise-spawn/pull/97) bump @npmcli/template-oss from 4.21.1 to 4.21.3 (@dependabot[bot]) 47 | * [`c72524e`](https://github.com/npm/promise-spawn/commit/c72524e4c4f58965ee7b64ea5cc981a7fb649889) [#95](https://github.com/npm/promise-spawn/pull/95) postinstall for dependabot template-oss PR (@lukekarrys) 48 | * [`8102197`](https://github.com/npm/promise-spawn/commit/810219764b55cd98f9e9f66f767e0a10afbd6b73) [#95](https://github.com/npm/promise-spawn/pull/95) bump @npmcli/template-oss from 4.19.0 to 4.21.1 (@dependabot[bot]) 49 | * [`3d54f38`](https://github.com/npm/promise-spawn/commit/3d54f38ef9e21ab527adcf5e9db71a19ae6c9663) [#76](https://github.com/npm/promise-spawn/pull/76) postinstall for dependabot template-oss PR (@lukekarrys) 50 | * [`ca63a18`](https://github.com/npm/promise-spawn/commit/ca63a18479877f4964706c0417a36deddfaf9ff4) [#76](https://github.com/npm/promise-spawn/pull/76) bump @npmcli/template-oss from 4.18.1 to 4.19.0 (@dependabot[bot]) 51 | * [`e3e359f`](https://github.com/npm/promise-spawn/commit/e3e359f1362bc8e9b05b1623c656bb47df685ae2) [#74](https://github.com/npm/promise-spawn/pull/74) postinstall for dependabot template-oss PR (@lukekarrys) 52 | * [`cc8e9c9`](https://github.com/npm/promise-spawn/commit/cc8e9c94d311723fbf3dbee7f2d7371f95578e25) [#74](https://github.com/npm/promise-spawn/pull/74) bump @npmcli/template-oss from 4.18.0 to 4.18.1 (@dependabot[bot]) 53 | 54 | ## [7.0.0](https://github.com/npm/promise-spawn/compare/v6.0.2...v7.0.0) (2023-08-30) 55 | 56 | ### ⚠️ BREAKING CHANGES 57 | 58 | * support for node 14 has been removed 59 | 60 | ### Bug Fixes 61 | 62 | * [`bc0bb5f`](https://github.com/npm/promise-spawn/commit/bc0bb5f6183743b4253608275b1dbf7b9cc67f6c) [#71](https://github.com/npm/promise-spawn/pull/71) drop node14 support (@wraithgar) 63 | 64 | ### Dependencies 65 | 66 | * [`e8606c7`](https://github.com/npm/promise-spawn/commit/e8606c7d0b068cd3d67b6f0bdc7605609a1dc321) [#71](https://github.com/npm/promise-spawn/pull/71) bump which from 3.0.1 to 4.0.0 67 | 68 | ## [6.0.2](https://github.com/npm/promise-spawn/compare/v6.0.1...v6.0.2) (2022-12-12) 69 | 70 | ### Bug Fixes 71 | 72 | * [`38f272a`](https://github.com/npm/promise-spawn/commit/38f272ab994c8896e5c36efa96c5d1ec0ece3161) [#56](https://github.com/npm/promise-spawn/pull/56) correctly identify more wsl distributions, closes npm/cli#5903 (#56) (@nlf) 73 | 74 | ## [6.0.1](https://github.com/npm/promise-spawn/compare/v6.0.0...v6.0.1) (2022-11-01) 75 | 76 | ### Dependencies 77 | 78 | * [`b9b7a78`](https://github.com/npm/promise-spawn/commit/b9b7a788abc5cdc0b63be3f4d241ad723ef82676) [#50](https://github.com/npm/promise-spawn/pull/50) `which@3.0.0` (#50) 79 | 80 | ## [6.0.0](https://github.com/npm/promise-spawn/compare/v5.0.0...v6.0.0) (2022-11-01) 81 | 82 | ### ⚠️ BREAKING CHANGES 83 | 84 | * stdout and stderr will now be returned as strings by default, for buffers set stdioString to false 85 | * when the `shell` option is set provided arguments will automatically be escaped 86 | 87 | ### Features 88 | 89 | * [`6ab90b8`](https://github.com/npm/promise-spawn/commit/6ab90b886751c6c060bb8e4e05962185b41b648d) [#48](https://github.com/npm/promise-spawn/pull/48) switch stdioString default to true (#48) (@nlf) 90 | * [`a854057`](https://github.com/npm/promise-spawn/commit/a854057456532fd9cfe1b38d88bc367760139ae1) [#47](https://github.com/npm/promise-spawn/pull/47) add open method for using system default apps to open arguments (#47) (@nlf) 91 | * [`723fc32`](https://github.com/npm/promise-spawn/commit/723fc3200958c4b7b98328ee02269506fba253ba) [#44](https://github.com/npm/promise-spawn/pull/44) implement argument escaping when the `shell` option is set (@nlf) 92 | 93 | ## [5.0.0](https://github.com/npm/promise-spawn/compare/v4.0.0...v5.0.0) (2022-10-26) 94 | 95 | ### ⚠️ BREAKING CHANGES 96 | 97 | * leading and trailing whitespace is no longer preserved when stdioStrings is set 98 | * this module no longer attempts to infer a uid and gid for processes 99 | 100 | ### Features 101 | 102 | * [`422e1b6`](https://github.com/npm/promise-spawn/commit/422e1b6005baa7ca3d5cd70180e3fbea0cf07dd9) [#40](https://github.com/npm/promise-spawn/pull/40) remove infer-owner (#40) (@nlf, @wraithgar) 103 | 104 | ### Bug Fixes 105 | 106 | * [`0f3dc07`](https://github.com/npm/promise-spawn/commit/0f3dc07469226faec67550ebebad9abdfd5b63a9) [#42](https://github.com/npm/promise-spawn/pull/42) trim stdio strings before returning when stdioStrings is set (#42) (@nlf) 107 | 108 | ## [4.0.0](https://github.com/npm/promise-spawn/compare/v3.0.0...v4.0.0) (2022-10-10) 109 | 110 | ### ⚠️ BREAKING CHANGES 111 | 112 | * `@npmcli/promise-spawn` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0` 113 | 114 | ### Features 115 | 116 | * [`4fba970`](https://github.com/npm/promise-spawn/commit/4fba970efe7ad586cd3c4a817fc10d364dee7421) [#29](https://github.com/npm/promise-spawn/pull/29) postinstall for dependabot template-oss PR (@lukekarrys) 117 | 118 | ## [3.0.0](https://github.com/npm/promise-spawn/compare/v2.0.1...v3.0.0) (2022-04-05) 119 | 120 | 121 | ### ⚠ BREAKING CHANGES 122 | 123 | * this will drop support for node 10 and non-LTS versions of node 12 and node 14 124 | 125 | ### Bug Fixes 126 | 127 | * put infer-owner back in ([#12](https://github.com/npm/promise-spawn/issues/12)) ([cb4a487](https://github.com/npm/promise-spawn/commit/cb4a4879e00deb6f5527d5b193a1d647a28a1cb4)) 128 | 129 | 130 | ### Dependencies 131 | 132 | * @npmcli/template-oss@3.2.2 ([#10](https://github.com/npm/promise-spawn/issues/10)) ([ad35767](https://github.com/npm/promise-spawn/commit/ad357670149ad5ab7993002ea8a82bc85f9deeaa)) 133 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | All interactions in this repo are covered by the [npm Code of 4 | Conduct](https://docs.npmjs.com/policies/conduct) 5 | 6 | The npm cli team may, at its own discretion, moderate, remove, or edit 7 | any interactions such as pull requests, issues, and comments. 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | # Contributing 4 | 5 | ## Code of Conduct 6 | 7 | All interactions in the **npm** organization on GitHub are considered to be covered by our standard [Code of Conduct](https://docs.npmjs.com/policies/conduct). 8 | 9 | ## Reporting Bugs 10 | 11 | Before submitting a new bug report please search for an existing or similar report. 12 | 13 | Use one of our existing issue templates if you believe you've come across a unique problem. 14 | 15 | Duplicate issues, or issues that don't use one of our templates may get closed without a response. 16 | 17 | ## Pull Request Conventions 18 | 19 | ### Commits 20 | 21 | We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 22 | 23 | When opening a pull request please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: 24 | 25 | - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. 26 | - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. 27 | - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. 28 | - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be *no* change to the version of the package when it is next published (as the commit does not affect the published version). 29 | 30 | ### Test Coverage 31 | 32 | Pull requests made against this repo will run `npm test` automatically. Please make sure tests pass locally before submitting a PR. 33 | 34 | Every new feature or bug fix should come with a corresponding test or tests that validate the solutions. Testing also reports on code coverage and will fail if code coverage drops. 35 | 36 | ### Linting 37 | 38 | Linting is also done automatically once tests pass. `npm run lintfix` will fix most linting errors automatically. 39 | 40 | Please make sure linting passes before submitting a PR. 41 | 42 | ## What _not_ to contribute? 43 | 44 | ### Dependencies 45 | 46 | It should be noted that our team does not accept third-party dependency updates/PRs. If you submit a PR trying to update our dependencies we will close it with or without a reference to these contribution guidelines. 47 | 48 | ### Tools/Automation 49 | 50 | Our core team is responsible for the maintenance of the tooling/automation in this project and we ask contributors to not make changes to these when contributing (e.g. `.github/*`, `.eslintrc.json`, `.licensee.json`). Most of those files also have a header at the top to remind folks they are automatically generated. Pull requests that alter these will not be accepted. 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) npm, Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE NPM DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE NPM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, 12 | OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, 13 | DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 14 | ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 15 | SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @npmcli/promise-spawn 2 | 3 | Spawn processes the way the npm cli likes to do. Give it some options, 4 | it'll give you a Promise that resolves or rejects based on the results of 5 | the execution. 6 | 7 | ## USAGE 8 | 9 | ```js 10 | const promiseSpawn = require('@npmcli/promise-spawn') 11 | 12 | promiseSpawn('ls', [ '-laF', 'some/dir/*.js' ], { 13 | cwd: '/tmp/some/path', // defaults to process.cwd() 14 | stdioString: true, // stdout/stderr as strings rather than buffers 15 | stdio: 'pipe', // any node spawn stdio arg is valid here 16 | // any other arguments to node child_process.spawn can go here as well, 17 | }, { 18 | extra: 'things', 19 | to: 'decorate', 20 | the: 'result', 21 | }).then(result => { 22 | // {code === 0, signal === null, stdout, stderr, and all the extras} 23 | console.log('ok!', result) 24 | }).catch(er => { 25 | // er has all the same properties as the result, set appropriately 26 | console.error('failed!', er) 27 | }) 28 | ``` 29 | 30 | ## API 31 | 32 | ### `promiseSpawn(cmd, args, opts, extra)` -> `Promise` 33 | 34 | Run the command, return a Promise that resolves/rejects based on the 35 | process result. 36 | 37 | Result or error will be decorated with the properties in the `extra` 38 | object. You can use this to attach some helpful info about _why_ the 39 | command is being run, if it makes sense for your use case. 40 | 41 | If `stdio` is set to anything other than `'inherit'`, then the result/error 42 | will be decorated with `stdout` and `stderr` values. If `stdioString` is 43 | set to `true`, these will be strings. Otherwise they will be Buffer 44 | objects. 45 | 46 | Returned promise is decorated with the `stdin` stream if the process is set 47 | to pipe from `stdin`. Writing to this stream writes to the `stdin` of the 48 | spawned process. 49 | 50 | #### Options 51 | 52 | - `stdioString` Boolean, default `true`. Return stdout/stderr output as 53 | strings rather than buffers. 54 | - `cwd` String, default `process.cwd()`. Current working directory for 55 | running the script. Also the argument to `infer-owner` to determine 56 | effective uid/gid when run as root on Unix systems. 57 | - `shell` Boolean or String. If false, no shell is used during spawn. If true, 58 | the system default shell is used. If a String, that specific shell is used. 59 | When a shell is used, the given command runs from within that shell by 60 | concatenating the command and its escaped arguments and running the result. 61 | This option is _not_ passed through to `child_process.spawn`. 62 | - Any other options for `child_process.spawn` can be passed as well. 63 | 64 | ### `promiseSpawn.open(arg, opts, extra)` -> `Promise` 65 | 66 | Use the operating system to open `arg` with a default program. This is useful 67 | for things like opening the user's default browser to a specific URL. 68 | 69 | Depending on the platform in use this will use `start` (win32), `open` (darwin) 70 | or `xdg-open` (everything else). In the case of Windows Subsystem for Linux we 71 | use the default win32 behavior as it is much more predictable to open the arg 72 | using the host operating system. 73 | 74 | #### Options 75 | 76 | Options are identical to `promiseSpawn` except for the following: 77 | 78 | - `command` String, the command to use to open the file in question. Default is 79 | one of `start`, `open` or `xdg-open` depending on platform in use. 80 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 4 | 5 | If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. 6 | 7 | If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [opensource-security@github.com](mailto:opensource-security@github.com). 8 | 9 | If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award. 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 12 | 13 | Thanks for helping make GitHub safe for everyone. 14 | -------------------------------------------------------------------------------- /lib/escape.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // eslint-disable-next-line max-len 4 | // this code adapted from: https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ 5 | const cmd = (input, doubleEscape) => { 6 | if (!input.length) { 7 | return '""' 8 | } 9 | 10 | let result 11 | if (!/[ \t\n\v"]/.test(input)) { 12 | result = input 13 | } else { 14 | result = '"' 15 | for (let i = 0; i <= input.length; ++i) { 16 | let slashCount = 0 17 | while (input[i] === '\\') { 18 | ++i 19 | ++slashCount 20 | } 21 | 22 | if (i === input.length) { 23 | result += '\\'.repeat(slashCount * 2) 24 | break 25 | } 26 | 27 | if (input[i] === '"') { 28 | result += '\\'.repeat(slashCount * 2 + 1) 29 | result += input[i] 30 | } else { 31 | result += '\\'.repeat(slashCount) 32 | result += input[i] 33 | } 34 | } 35 | result += '"' 36 | } 37 | 38 | // and finally, prefix shell meta chars with a ^ 39 | result = result.replace(/[ !%^&()<>|"]/g, '^$&') 40 | if (doubleEscape) { 41 | result = result.replace(/[ !%^&()<>|"]/g, '^$&') 42 | } 43 | 44 | return result 45 | } 46 | 47 | const sh = (input) => { 48 | if (!input.length) { 49 | return `''` 50 | } 51 | 52 | if (!/[\t\n\r "#$&'()*;<>?\\`|~]/.test(input)) { 53 | return input 54 | } 55 | 56 | // replace single quotes with '\'' and wrap the whole result in a fresh set of quotes 57 | const result = `'${input.replace(/'/g, `'\\''`)}'` 58 | // if the input string already had single quotes around it, clean those up 59 | .replace(/^(?:'')+(?!$)/, '') 60 | .replace(/\\'''/g, `\\'`) 61 | 62 | return result 63 | } 64 | 65 | module.exports = { 66 | cmd, 67 | sh, 68 | } 69 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { spawn } = require('child_process') 4 | const os = require('os') 5 | const which = require('which') 6 | 7 | const escape = require('./escape.js') 8 | 9 | // 'extra' object is for decorating the error a bit more 10 | const promiseSpawn = (cmd, args, opts = {}, extra = {}) => { 11 | if (opts.shell) { 12 | return spawnWithShell(cmd, args, opts, extra) 13 | } 14 | 15 | let resolve, reject 16 | const promise = new Promise((_resolve, _reject) => { 17 | resolve = _resolve 18 | reject = _reject 19 | }) 20 | 21 | // Create error here so we have a more useful stack trace when rejecting 22 | const closeError = new Error('command failed') 23 | 24 | const stdout = [] 25 | const stderr = [] 26 | 27 | const getResult = (result) => ({ 28 | cmd, 29 | args, 30 | ...result, 31 | ...stdioResult(stdout, stderr, opts), 32 | ...extra, 33 | }) 34 | const rejectWithOpts = (er, erOpts) => { 35 | const resultError = getResult(erOpts) 36 | reject(Object.assign(er, resultError)) 37 | } 38 | 39 | const proc = spawn(cmd, args, opts) 40 | promise.stdin = proc.stdin 41 | promise.process = proc 42 | 43 | proc.on('error', rejectWithOpts) 44 | 45 | if (proc.stdout) { 46 | proc.stdout.on('data', c => stdout.push(c)) 47 | proc.stdout.on('error', rejectWithOpts) 48 | } 49 | 50 | if (proc.stderr) { 51 | proc.stderr.on('data', c => stderr.push(c)) 52 | proc.stderr.on('error', rejectWithOpts) 53 | } 54 | 55 | proc.on('close', (code, signal) => { 56 | if (code || signal) { 57 | rejectWithOpts(closeError, { code, signal }) 58 | } else { 59 | resolve(getResult({ code, signal })) 60 | } 61 | }) 62 | 63 | return promise 64 | } 65 | 66 | const spawnWithShell = (cmd, args, opts, extra) => { 67 | let command = opts.shell 68 | // if shell is set to true, we use a platform default. we can't let the core 69 | // spawn method decide this for us because we need to know what shell is in use 70 | // ahead of time so that we can escape arguments properly. we don't need coverage here. 71 | if (command === true) { 72 | // istanbul ignore next 73 | command = process.platform === 'win32' ? process.env.ComSpec : 'sh' 74 | } 75 | 76 | const options = { ...opts, shell: false } 77 | const realArgs = [] 78 | let script = cmd 79 | 80 | // first, determine if we're in windows because if we are we need to know if we're 81 | // running an .exe or a .cmd/.bat since the latter requires extra escaping 82 | const isCmd = /(?:^|\\)cmd(?:\.exe)?$/i.test(command) 83 | if (isCmd) { 84 | let doubleEscape = false 85 | 86 | // find the actual command we're running 87 | let initialCmd = '' 88 | let insideQuotes = false 89 | for (let i = 0; i < cmd.length; ++i) { 90 | const char = cmd.charAt(i) 91 | if (char === ' ' && !insideQuotes) { 92 | break 93 | } 94 | 95 | initialCmd += char 96 | if (char === '"' || char === "'") { 97 | insideQuotes = !insideQuotes 98 | } 99 | } 100 | 101 | let pathToInitial 102 | try { 103 | pathToInitial = which.sync(initialCmd, { 104 | path: (options.env && findInObject(options.env, 'PATH')) || process.env.PATH, 105 | pathext: (options.env && findInObject(options.env, 'PATHEXT')) || process.env.PATHEXT, 106 | }).toLowerCase() 107 | } catch (err) { 108 | pathToInitial = initialCmd.toLowerCase() 109 | } 110 | 111 | doubleEscape = pathToInitial.endsWith('.cmd') || pathToInitial.endsWith('.bat') 112 | for (const arg of args) { 113 | script += ` ${escape.cmd(arg, doubleEscape)}` 114 | } 115 | realArgs.push('/d', '/s', '/c', script) 116 | options.windowsVerbatimArguments = true 117 | } else { 118 | for (const arg of args) { 119 | script += ` ${escape.sh(arg)}` 120 | } 121 | realArgs.push('-c', script) 122 | } 123 | 124 | return promiseSpawn(command, realArgs, options, extra) 125 | } 126 | 127 | // open a file with the default application as defined by the user's OS 128 | const open = (_args, opts = {}, extra = {}) => { 129 | const options = { ...opts, shell: true } 130 | const args = [].concat(_args) 131 | 132 | let platform = process.platform 133 | // process.platform === 'linux' may actually indicate WSL, if that's the case 134 | // open the argument with sensible-browser which is pre-installed 135 | // In WSL, set the default browser using, for example, 136 | // export BROWSER="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" 137 | // or 138 | // export BROWSER="/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" 139 | // To permanently set the default browser, add the appropriate entry to your shell's 140 | // RC file, e.g. .bashrc or .zshrc. 141 | if (platform === 'linux' && os.release().toLowerCase().includes('microsoft')) { 142 | platform = 'wsl' 143 | if (!process.env.BROWSER) { 144 | return Promise.reject( 145 | new Error('Set the BROWSER environment variable to your desired browser.')) 146 | } 147 | } 148 | 149 | let command = options.command 150 | if (!command) { 151 | if (platform === 'win32') { 152 | // spawnWithShell does not do the additional os.release() check, so we 153 | // have to force the shell here to make sure we treat WSL as windows. 154 | options.shell = process.env.ComSpec 155 | // also, the start command accepts a title so to make sure that we don't 156 | // accidentally interpret the first arg as the title, we stick an empty 157 | // string immediately after the start command 158 | command = 'start ""' 159 | } else if (platform === 'wsl') { 160 | command = 'sensible-browser' 161 | } else if (platform === 'darwin') { 162 | command = 'open' 163 | } else { 164 | command = 'xdg-open' 165 | } 166 | } 167 | 168 | return spawnWithShell(command, args, options, extra) 169 | } 170 | promiseSpawn.open = open 171 | 172 | const isPipe = (stdio = 'pipe', fd) => { 173 | if (stdio === 'pipe' || stdio === null) { 174 | return true 175 | } 176 | 177 | if (Array.isArray(stdio)) { 178 | return isPipe(stdio[fd], fd) 179 | } 180 | 181 | return false 182 | } 183 | 184 | const stdioResult = (stdout, stderr, { stdioString = true, stdio }) => { 185 | const result = { 186 | stdout: null, 187 | stderr: null, 188 | } 189 | 190 | // stdio is [stdin, stdout, stderr] 191 | if (isPipe(stdio, 1)) { 192 | result.stdout = Buffer.concat(stdout) 193 | if (stdioString) { 194 | result.stdout = result.stdout.toString().trim() 195 | } 196 | } 197 | 198 | if (isPipe(stdio, 2)) { 199 | result.stderr = Buffer.concat(stderr) 200 | if (stdioString) { 201 | result.stderr = result.stderr.toString().trim() 202 | } 203 | } 204 | 205 | return result 206 | } 207 | 208 | // case insensitive lookup in an object 209 | const findInObject = (obj, key) => { 210 | key = key.toLowerCase() 211 | for (const objKey of Object.keys(obj).sort()) { 212 | if (objKey.toLowerCase() === key) { 213 | return obj[objKey] 214 | } 215 | } 216 | } 217 | 218 | module.exports = promiseSpawn 219 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@npmcli/promise-spawn", 3 | "version": "8.0.2", 4 | "files": [ 5 | "bin/", 6 | "lib/" 7 | ], 8 | "main": "./lib/index.js", 9 | "description": "spawn processes the way the npm cli likes to do", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/npm/promise-spawn.git" 13 | }, 14 | "author": "GitHub Inc.", 15 | "license": "ISC", 16 | "scripts": { 17 | "test": "tap", 18 | "snap": "tap", 19 | "lint": "npm run eslint", 20 | "lintfix": "npm run eslint -- --fix", 21 | "posttest": "npm run lint", 22 | "postsnap": "npm run lintfix --", 23 | "postlint": "template-oss-check", 24 | "template-oss-apply": "template-oss-apply --force", 25 | "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"" 26 | }, 27 | "tap": { 28 | "check-coverage": true, 29 | "nyc-arg": [ 30 | "--exclude", 31 | "tap-snapshots/**" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "@npmcli/eslint-config": "^5.0.0", 36 | "@npmcli/template-oss": "4.24.3", 37 | "spawk": "^1.7.1", 38 | "tap": "^16.0.1" 39 | }, 40 | "engines": { 41 | "node": "^18.17.0 || >=20.5.0" 42 | }, 43 | "templateOSS": { 44 | "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", 45 | "version": "4.24.3", 46 | "publish": true 47 | }, 48 | "dependencies": { 49 | "which": "^5.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "group-pull-request-title-pattern": "chore: release ${version}", 3 | "pull-request-title-pattern": "chore: release${component} ${version}", 4 | "changelog-sections": [ 5 | { 6 | "type": "feat", 7 | "section": "Features", 8 | "hidden": false 9 | }, 10 | { 11 | "type": "fix", 12 | "section": "Bug Fixes", 13 | "hidden": false 14 | }, 15 | { 16 | "type": "docs", 17 | "section": "Documentation", 18 | "hidden": false 19 | }, 20 | { 21 | "type": "deps", 22 | "section": "Dependencies", 23 | "hidden": false 24 | }, 25 | { 26 | "type": "chore", 27 | "section": "Chores", 28 | "hidden": true 29 | } 30 | ], 31 | "packages": { 32 | ".": { 33 | "package-name": "" 34 | } 35 | }, 36 | "prerelease-type": "pre.0" 37 | } 38 | -------------------------------------------------------------------------------- /test/escape.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { writeFileSync: writeFile } = require('fs') 4 | const { join } = require('path') 5 | const t = require('tap') 6 | const promiseSpawn = require('../lib/index.js') 7 | 8 | const escape = require('../lib/escape.js') 9 | const isWindows = process.platform === 'win32' 10 | 11 | t.test('sh', (t) => { 12 | const expectations = [ 13 | ['', `''`], 14 | ['test', 'test'], 15 | ['test words', `'test words'`], 16 | ['$1', `'$1'`], 17 | ['"$1"', `'"$1"'`], 18 | [`'$1'`, `\\''$1'\\'`], 19 | ['\\$1', `'\\$1'`], 20 | ['--arg="$1"', `'--arg="$1"'`], 21 | ['--arg=npm exec -c "$1"', `'--arg=npm exec -c "$1"'`], 22 | [`--arg=npm exec -c '$1'`, `'--arg=npm exec -c '\\''$1'\\'`], 23 | [`'--arg=npm exec -c "$1"'`, `\\''--arg=npm exec -c "$1"'\\'`], 24 | ] 25 | 26 | for (const [input, expectation] of expectations) { 27 | t.equal(escape.sh(input), expectation, 28 | `expected to escape \`${input}\` to \`${expectation}\``) 29 | } 30 | 31 | t.test('integration', { skip: isWindows && 'posix only' }, async (t) => { 32 | for (const [input] of expectations) { 33 | const p = await promiseSpawn('node', ['-p', 'process.argv[1]', '--', input], 34 | { shell: true, stdioString: true }) 35 | const stdout = p.stdout.trim() 36 | t.equal(stdout, input, `expected \`${stdout}\` to equal \`${input}\``) 37 | } 38 | 39 | t.end() 40 | }) 41 | 42 | t.end() 43 | }) 44 | 45 | t.test('cmd', (t) => { 46 | const expectations = [ 47 | ['', '""'], 48 | ['test', 'test'], 49 | ['%PATH%', '^%PATH^%'], 50 | ['%PATH%', '^^^%PATH^^^%', true], 51 | ['"%PATH%"', '^"\\^"^%PATH^%\\^"^"'], 52 | ['"%PATH%"', '^^^"\\^^^"^^^%PATH^^^%\\^^^"^^^"', true], 53 | [`'%PATH%'`, `'^%PATH^%'`], 54 | [`'%PATH%'`, `'^^^%PATH^^^%'`, true], 55 | ['\\%PATH%', '\\^%PATH^%'], 56 | ['\\%PATH%', '\\^^^%PATH^^^%', true], 57 | ['--arg="%PATH%"', '^"--arg=\\^"^%PATH^%\\^"^"'], 58 | ['--arg="%PATH%"', '^^^"--arg=\\^^^"^^^%PATH^^^%\\^^^"^^^"', true], 59 | ['--arg=npm exec -c "%PATH%"', '^"--arg=npm^ exec^ -c^ \\^"^%PATH^%\\^"^"'], 60 | ['--arg=npm exec -c "%PATH%"', 61 | '^^^"--arg=npm^^^ exec^^^ -c^^^ \\^^^"^^^%PATH^^^%\\^^^"^^^"', true], 62 | [`--arg=npm exec -c '%PATH%'`, `^"--arg=npm^ exec^ -c^ '^%PATH^%'^"`], 63 | [`--arg=npm exec -c '%PATH%'`, `^^^"--arg=npm^^^ exec^^^ -c^^^ '^^^%PATH^^^%'^^^"`, true], 64 | [`'--arg=npm exec -c "%PATH%"'`, `^"'--arg=npm^ exec^ -c^ \\^"^%PATH^%\\^"'^"`], 65 | [`'--arg=npm exec -c "%PATH%"'`, 66 | `^^^"'--arg=npm^^^ exec^^^ -c^^^ \\^^^"^^^%PATH^^^%\\^^^"'^^^"`, true], 67 | ['"C:\\Program Files\\test.bat"', '^"\\^"C:\\Program^ Files\\test.bat\\^"^"'], 68 | ['"C:\\Program Files\\test.bat"', '^^^"\\^^^"C:\\Program^^^ Files\\test.bat\\^^^"^^^"', true], 69 | ['"C:\\Program Files\\test%.bat"', '^"\\^"C:\\Program^ Files\\test^%.bat\\^"^"'], 70 | ['"C:\\Program Files\\test%.bat"', 71 | '^^^"\\^^^"C:\\Program^^^ Files\\test^^^%.bat\\^^^"^^^"', true], 72 | ['% % %', '^"^%^ ^%^ ^%^"'], 73 | ['% % %', '^^^"^^^%^^^ ^^^%^^^ ^^^%^^^"', true], 74 | ['hello^^^^^^', 'hello^^^^^^^^^^^^'], 75 | ['hello^^^^^^', 'hello^^^^^^^^^^^^^^^^^^^^^^^^', true], 76 | ['hello world', '^"hello^ world^"'], 77 | ['hello world', '^^^"hello^^^ world^^^"', true], 78 | ['hello"world', '^"hello\\^"world^"'], 79 | ['hello"world', '^^^"hello\\^^^"world^^^"', true], 80 | ['hello""world', '^"hello\\^"\\^"world^"'], 81 | ['hello""world', '^^^"hello\\^^^"\\^^^"world^^^"', true], 82 | ['hello\\world', 'hello\\world'], 83 | ['hello\\world', 'hello\\world', true], 84 | ['hello\\\\world', 'hello\\\\world'], 85 | ['hello\\\\world', 'hello\\\\world', true], 86 | ['hello\\"world', '^"hello\\\\\\^"world^"'], 87 | ['hello\\"world', '^^^"hello\\\\\\^^^"world^^^"', true], 88 | ['hello\\\\"world', '^"hello\\\\\\\\\\^"world^"'], 89 | ['hello\\\\"world', '^^^"hello\\\\\\\\\\^^^"world^^^"', true], 90 | ['hello world\\', '^"hello^ world\\\\^"'], 91 | ['hello world\\', '^^^"hello^^^ world\\\\^^^"', true], 92 | ['hello %PATH%', '^"hello^ ^%PATH^%^"'], 93 | ['hello %PATH%', '^^^"hello^^^ ^^^%PATH^^^%^^^"', true], 94 | ] 95 | 96 | for (const [input, expectation, double] of expectations) { 97 | const msg = `expected to${double ? ' double' : ''} escape \`${input}\` to \`${expectation}\`` 98 | t.equal(escape.cmd(input, double), expectation, msg) 99 | } 100 | 101 | t.test('integration', { skip: !isWindows && 'Windows only' }, async (t) => { 102 | const dir = t.testdir() 103 | const shimFile = join(dir, 'shim.cmd') 104 | const shim = `@echo off\nnode -p process.argv[1] -- %*` 105 | writeFile(shimFile, shim) 106 | 107 | const spawnOpts = { shell: true, stdioString: true } 108 | for (const [input,, double] of expectations) { 109 | const p = double 110 | ? await promiseSpawn(shimFile, [input], spawnOpts) 111 | : await promiseSpawn('node', ['-p', 'process.argv[1]', '--', input], spawnOpts) 112 | t.equal(p.stdout, input, `expected \`${p.stdout}\` to equal \`${input}\``) 113 | } 114 | 115 | t.end() 116 | }) 117 | 118 | t.end() 119 | }) 120 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const spawk = require('spawk') 4 | const t = require('tap') 5 | 6 | const promiseSpawn = require('../lib/index.js') 7 | 8 | spawk.preventUnmatched() 9 | t.afterEach(() => { 10 | spawk.clean() 11 | }) 12 | 13 | t.test('defaults to returning strings', async (t) => { 14 | const proc = spawk.spawn('pass', [], {}) 15 | .stdout(Buffer.from('OK\n')) 16 | 17 | const result = await promiseSpawn('pass', []) 18 | t.hasStrict(result, { 19 | code: 0, 20 | signal: undefined, 21 | stdout: 'OK', 22 | stderr: '', 23 | }) 24 | 25 | t.ok(proc.called) 26 | }) 27 | 28 | t.test('extra context is returned', async (t) => { 29 | const proc = spawk.spawn('pass', [], {}) 30 | .stdout(Buffer.from('OK\n')) 31 | 32 | const result = await promiseSpawn('pass', [], {}, { extra: 'property' }) 33 | t.hasStrict(result, { 34 | code: 0, 35 | signal: undefined, 36 | stdout: 'OK', 37 | stderr: '', 38 | extra: 'property', 39 | }) 40 | 41 | t.ok(proc.called) 42 | }) 43 | 44 | t.test('stdioString false returns buffers', async (t) => { 45 | const proc = spawk.spawn('pass', [], {}) 46 | .stdout(Buffer.from('OK\n')) 47 | 48 | const result = await promiseSpawn('pass', [], { stdioString: false }) 49 | t.hasStrict(result, { 50 | code: 0, 51 | signal: undefined, 52 | stdout: Buffer.from('OK\n'), 53 | stderr: Buffer.from(''), 54 | }) 55 | 56 | t.ok(proc.called) 57 | }) 58 | 59 | t.test('stdout and stderr are null when stdio is inherit', async (t) => { 60 | const proc = spawk.spawn('pass', [], { stdio: 'inherit' }) 61 | .stdout(Buffer.from('OK\n')) 62 | 63 | const result = await promiseSpawn('pass', [], { stdio: 'inherit' }) 64 | t.hasStrict(result, { 65 | code: 0, 66 | signal: undefined, 67 | stdout: null, 68 | stderr: null, 69 | }) 70 | 71 | t.ok(proc.called) 72 | }) 73 | 74 | t.test('stdout and stderr are null when stdio is inherit and stdioString is false', async (t) => { 75 | const proc = spawk.spawn('pass', [], { stdio: 'inherit' }) 76 | .stdout(Buffer.from('OK\n')) 77 | 78 | const result = await promiseSpawn('pass', [], { stdio: 'inherit', stdioString: false }) 79 | t.hasStrict(result, { 80 | code: 0, 81 | signal: undefined, 82 | stdout: null, 83 | stderr: null, 84 | }) 85 | 86 | t.ok(proc.called) 87 | }) 88 | 89 | t.test('stdout is null when stdio is [pipe, inherit, pipe]', async (t) => { 90 | const proc = spawk.spawn('pass', [], { stdio: ['pipe', 'inherit', 'pipe'] }) 91 | .stdout(Buffer.from('OK\n')) 92 | 93 | const result = await promiseSpawn('pass', [], { stdio: ['pipe', 'inherit', 'pipe'] }) 94 | t.hasStrict(result, { 95 | code: 0, 96 | signal: undefined, 97 | stdout: null, 98 | stderr: '', 99 | }) 100 | 101 | t.ok(proc.called) 102 | }) 103 | 104 | t.test('stderr is null when stdio is [pipe, pipe, inherit]', async (t) => { 105 | const proc = spawk.spawn('pass', [], { stdio: ['pipe', 'pipe', 'inherit'] }) 106 | .stdout(Buffer.from('OK\n')) 107 | 108 | const result = await promiseSpawn('pass', [], { stdio: ['pipe', 'pipe', 'inherit'] }) 109 | t.hasStrict(result, { 110 | code: 0, 111 | signal: undefined, 112 | stdout: 'OK', 113 | stderr: null, 114 | }) 115 | 116 | t.ok(proc.called) 117 | }) 118 | 119 | t.test('exposes stdin', async (t) => { 120 | const proc = spawk.spawn('stdin', [], {}) 121 | const p = promiseSpawn('stdin', []) 122 | process.nextTick(() => { 123 | p.process.stdin.pipe(p.process.stdout) 124 | p.stdin.end('hello') 125 | }) 126 | 127 | const result = await p 128 | t.hasStrict(result, { 129 | code: 0, 130 | signal: undefined, 131 | stdout: 'hello', 132 | stderr: '', 133 | }) 134 | 135 | t.ok(proc.called) 136 | }) 137 | 138 | t.test('exposes process', async (t) => { 139 | const proc = spawk.spawn('proc', [], {}) 140 | .exitOnSignal('SIGFAKE') 141 | 142 | const p = promiseSpawn('proc', []) 143 | process.nextTick(() => p.process.kill('SIGFAKE')) 144 | 145 | // there are no signals in windows, so we expect a different result 146 | if (process.platform === 'win32') { 147 | await t.rejects(p, { 148 | code: 1, 149 | signal: undefined, 150 | stdout: '', 151 | stderr: '', 152 | }) 153 | } else { 154 | await t.rejects(p, { 155 | code: null, 156 | signal: 'SIGFAKE', 157 | stdout: '', 158 | stderr: '', 159 | }) 160 | } 161 | 162 | t.ok(proc.called) 163 | }) 164 | 165 | t.test('rejects when spawn errors', async (t) => { 166 | const proc = spawk.spawn('notfound', [], {}) 167 | .spawnError(new Error('command not found')) 168 | 169 | await t.rejects(promiseSpawn('notfound', []), { 170 | message: 'command not found', 171 | stdout: '', 172 | stderr: '', 173 | }) 174 | 175 | t.ok(proc.called) 176 | }) 177 | 178 | t.test('spawn error includes extra', async (t) => { 179 | const proc = spawk.spawn('notfound', [], {}) 180 | .spawnError(new Error('command not found')) 181 | 182 | await t.rejects(promiseSpawn('notfound', [], {}, { extra: 'property' }), { 183 | message: 'command not found', 184 | stdout: '', 185 | stderr: '', 186 | extra: 'property', 187 | }) 188 | 189 | t.ok(proc.called) 190 | }) 191 | 192 | t.test('spawn error respects stdioString', async (t) => { 193 | const proc = spawk.spawn('notfound', [], {}) 194 | .spawnError(new Error('command not found')) 195 | 196 | await t.rejects(promiseSpawn('notfound', [], { stdioString: false }), { 197 | message: 'command not found', 198 | stdout: Buffer.from(''), 199 | stderr: Buffer.from(''), 200 | }) 201 | 202 | t.ok(proc.called) 203 | }) 204 | 205 | t.test('spawn error respects stdio as inherit', async (t) => { 206 | const proc = spawk.spawn('notfound', [], { stdio: 'inherit' }) 207 | .spawnError(new Error('command not found')) 208 | 209 | await t.rejects(promiseSpawn('notfound', [], { stdio: 'inherit' }), { 210 | message: 'command not found', 211 | stdout: null, 212 | stderr: null, 213 | }) 214 | 215 | t.ok(proc.called) 216 | }) 217 | 218 | t.test('rejects when command fails', async (t) => { 219 | const proc = spawk.spawn('fail', [], {}) 220 | .stderr(Buffer.from('Error!\n')) 221 | .exit(1) 222 | 223 | await t.rejects(promiseSpawn('fail', []), { 224 | message: 'command failed', 225 | code: 1, 226 | stdout: '', 227 | stderr: 'Error!', 228 | }) 229 | 230 | t.ok(proc.called) 231 | }) 232 | 233 | t.test('failed command returns extra', async (t) => { 234 | const proc = spawk.spawn('fail', [], {}) 235 | .stderr(Buffer.from('Error!\n')) 236 | .exit(1) 237 | 238 | await t.rejects(promiseSpawn('fail', [], {}, { extra: 'property' }), { 239 | message: 'command failed', 240 | code: 1, 241 | stdout: '', 242 | stderr: 'Error!', 243 | extra: 'property', 244 | }) 245 | 246 | t.ok(proc.called) 247 | }) 248 | 249 | t.test('failed command respects stdioString', async (t) => { 250 | const proc = spawk.spawn('fail', [], {}) 251 | .stderr(Buffer.from('Error!\n')) 252 | .exit(1) 253 | 254 | await t.rejects(promiseSpawn('fail', [], { stdioString: false }), { 255 | message: 'command failed', 256 | code: 1, 257 | stdout: Buffer.from(''), 258 | stderr: Buffer.from('Error!\n'), 259 | }) 260 | 261 | t.ok(proc.called) 262 | }) 263 | 264 | t.test('failed command respects stdio as inherit', async (t) => { 265 | const proc = spawk.spawn('fail', [], { stdio: 'inherit' }) 266 | .stderr(Buffer.from('Error!\n')) 267 | .exit(1) 268 | 269 | await t.rejects(promiseSpawn('fail', [], { stdio: 'inherit' }), { 270 | message: 'command failed', 271 | code: 1, 272 | stdout: null, 273 | stderr: null, 274 | }) 275 | 276 | t.ok(proc.called) 277 | }) 278 | 279 | t.test('rejects when signal kills child', async (t) => { 280 | const proc = spawk.spawn('signal', [], {}) 281 | .signal('SIGFAKE') 282 | 283 | const p = promiseSpawn('signal', []) 284 | // there are no signals in windows, so we expect a different result 285 | if (process.platform === 'win32') { 286 | await t.rejects(p, { 287 | code: 1, 288 | signal: undefined, 289 | stdout: '', 290 | stderr: '', 291 | }) 292 | } else { 293 | await t.rejects(p, { 294 | code: null, 295 | signal: 'SIGFAKE', 296 | stdout: '', 297 | stderr: '', 298 | }) 299 | } 300 | 301 | t.ok(proc.called) 302 | }) 303 | 304 | t.test('signal death includes extra', async (t) => { 305 | const proc = spawk.spawn('signal', [], {}) 306 | .signal('SIGFAKE') 307 | 308 | const p = promiseSpawn('signal', [], {}, { extra: 'property' }) 309 | // there are no signals in windows, so we expect a different result 310 | if (process.platform === 'win32') { 311 | await t.rejects(p, { 312 | code: 1, 313 | signal: undefined, 314 | stdout: '', 315 | stderr: '', 316 | extra: 'property', 317 | }) 318 | } else { 319 | await t.rejects(p, { 320 | code: null, 321 | signal: 'SIGFAKE', 322 | stdout: '', 323 | stderr: '', 324 | extra: 'property', 325 | }) 326 | } 327 | 328 | t.ok(proc.called) 329 | }) 330 | 331 | t.test('signal death respects stdioString', async (t) => { 332 | const proc = spawk.spawn('signal', [], {}) 333 | .signal('SIGFAKE') 334 | 335 | const p = promiseSpawn('signal', [], { stdioString: false }) 336 | // there are no signals in windows, so we expect a different result 337 | if (process.platform === 'win32') { 338 | await t.rejects(p, { 339 | code: 1, 340 | signal: undefined, 341 | stdout: Buffer.from(''), 342 | stderr: Buffer.from(''), 343 | }) 344 | } else { 345 | await t.rejects(p, { 346 | code: null, 347 | signal: 'SIGFAKE', 348 | stdout: Buffer.from(''), 349 | stderr: Buffer.from(''), 350 | }) 351 | } 352 | 353 | t.ok(proc.called) 354 | }) 355 | 356 | t.test('signal death respects stdio as inherit', async (t) => { 357 | const proc = spawk.spawn('signal', [], { stdio: 'inherit' }) 358 | .signal('SIGFAKE') 359 | 360 | const p = promiseSpawn('signal', [], { stdio: 'inherit' }) 361 | // there are no signals in windows, so we expect a different result 362 | if (process.platform === 'win32') { 363 | await t.rejects(p, { 364 | code: 1, 365 | signal: undefined, 366 | stdout: null, 367 | stderr: null, 368 | }) 369 | } else { 370 | await t.rejects(p, { 371 | code: null, 372 | signal: 'SIGFAKE', 373 | stdout: null, 374 | stderr: null, 375 | }) 376 | } 377 | 378 | t.ok(proc.called) 379 | }) 380 | 381 | t.test('rejects when stdout errors', async (t) => { 382 | const proc = spawk.spawn('stdout-err', [], {}) 383 | 384 | const p = promiseSpawn('stdout-err', []) 385 | process.nextTick(() => p.process.stdout.emit('error', new Error('stdout err'))) 386 | 387 | await t.rejects(p, { 388 | message: 'stdout err', 389 | code: null, 390 | signal: undefined, 391 | stdout: '', 392 | stderr: '', 393 | }) 394 | 395 | t.ok(proc.called) 396 | }) 397 | 398 | t.test('rejects when stderr errors', async (t) => { 399 | const proc = spawk.spawn('stderr-err', [], {}) 400 | 401 | const p = promiseSpawn('stderr-err', []) 402 | process.nextTick(() => p.process.stderr.emit('error', new Error('stderr err'))) 403 | 404 | await t.rejects(p, { 405 | message: 'stderr err', 406 | code: null, 407 | signal: undefined, 408 | stdout: '', 409 | stderr: '', 410 | }) 411 | 412 | t.ok(proc.called) 413 | }) 414 | -------------------------------------------------------------------------------- /test/open.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const spawk = require('spawk') 4 | const t = require('tap') 5 | const os = require('node:os') 6 | 7 | const promiseSpawn = require('../lib/index.js') 8 | 9 | spawk.preventUnmatched() 10 | t.afterEach(() => { 11 | spawk.clean() 12 | }) 13 | 14 | const isWSL = process.platform === 'linux' && os.release().toLowerCase().includes('microsoft') 15 | 16 | t.test('process.platform === win32', (t) => { 17 | const comSpec = process.env.ComSpec 18 | const platformDesc = Object.getOwnPropertyDescriptor(process, 'platform') 19 | process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe' 20 | Object.defineProperty(process, 'platform', { ...platformDesc, value: 'win32' }) 21 | t.teardown(() => { 22 | process.env.ComSpec = comSpec 23 | Object.defineProperty(process, 'platform', platformDesc) 24 | }) 25 | 26 | t.test('uses start with a shell', async (t) => { 27 | const proc = spawk.spawn('C:\\Windows\\System32\\cmd.exe', 28 | ['/d', '/s', '/c', 'start "" https://google.com'], 29 | { shell: false }) 30 | 31 | const result = await promiseSpawn.open('https://google.com') 32 | t.hasStrict(result, { 33 | code: 0, 34 | signal: undefined, 35 | }) 36 | 37 | t.ok(proc.called) 38 | }) 39 | 40 | t.test('ignores shell = false', async (t) => { 41 | const proc = spawk.spawn('C:\\Windows\\System32\\cmd.exe', 42 | ['/d', '/s', '/c', 'start "" https://google.com'], 43 | { shell: false }) 44 | 45 | const result = await promiseSpawn.open('https://google.com', { shell: false }) 46 | t.hasStrict(result, { 47 | code: 0, 48 | signal: undefined, 49 | }) 50 | 51 | t.ok(proc.called) 52 | }) 53 | 54 | t.test('respects opts.command', async (t) => { 55 | const proc = spawk.spawn('C:\\Windows\\System32\\cmd.exe', 56 | ['/d', '/s', '/c', 'browser https://google.com'], 57 | { shell: false }) 58 | 59 | const result = await promiseSpawn.open('https://google.com', { command: 'browser' }) 60 | t.hasStrict(result, { 61 | code: 0, 62 | signal: undefined, 63 | }) 64 | 65 | t.ok(proc.called) 66 | }) 67 | 68 | t.end() 69 | }) 70 | 71 | t.test('process.platform === darwin', (t) => { 72 | const platformDesc = Object.getOwnPropertyDescriptor(process, 'platform') 73 | Object.defineProperty(process, 'platform', { ...platformDesc, value: 'darwin' }) 74 | t.teardown(() => { 75 | Object.defineProperty(process, 'platform', platformDesc) 76 | }) 77 | 78 | t.test('uses open with a shell', async (t) => { 79 | const proc = spawk.spawn('sh', ['-c', 'open https://google.com'], { shell: false }) 80 | 81 | const result = await promiseSpawn.open('https://google.com') 82 | t.hasStrict(result, { 83 | code: 0, 84 | signal: undefined, 85 | }) 86 | 87 | t.ok(proc.called) 88 | }) 89 | 90 | t.test('ignores shell = false', async (t) => { 91 | const proc = spawk.spawn('sh', ['-c', 'open https://google.com'], { shell: false }) 92 | 93 | const result = await promiseSpawn.open('https://google.com', { shell: false }) 94 | t.hasStrict(result, { 95 | code: 0, 96 | signal: undefined, 97 | }) 98 | 99 | t.ok(proc.called) 100 | }) 101 | 102 | t.test('respects opts.command', async (t) => { 103 | const proc = spawk.spawn('sh', ['-c', 'browser https://google.com'], { shell: false }) 104 | 105 | const result = await promiseSpawn.open('https://google.com', { command: 'browser' }) 106 | t.hasStrict(result, { 107 | code: 0, 108 | signal: undefined, 109 | }) 110 | 111 | t.ok(proc.called) 112 | }) 113 | 114 | t.end() 115 | }) 116 | 117 | t.test('process.platform === linux', (t) => { 118 | const platformDesc = Object.getOwnPropertyDescriptor(process, 'platform') 119 | Object.defineProperty(process, 'platform', { ...platformDesc, value: 'linux' }) 120 | t.teardown(() => { 121 | Object.defineProperty(process, 'platform', platformDesc) 122 | }) 123 | 124 | // xdg-open is not installed in WSL by default 125 | t.test('uses xdg-open in a shell', { skip: isWSL }, async (t) => { 126 | const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false }) 127 | 128 | const result = await promiseSpawn.open('https://google.com') 129 | t.hasStrict(result, { 130 | code: 0, 131 | signal: undefined, 132 | }) 133 | 134 | t.ok(proc.called) 135 | }) 136 | 137 | // xdg-open is not installed in WSL by default 138 | t.test('ignores shell = false', { skip: isWSL }, async (t) => { 139 | const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false }) 140 | 141 | const result = await promiseSpawn.open('https://google.com', { shell: false }) 142 | t.hasStrict(result, { 143 | code: 0, 144 | signal: undefined, 145 | }) 146 | 147 | t.ok(proc.called) 148 | }) 149 | 150 | t.test('respects opts.command', async (t) => { 151 | const proc = spawk.spawn('sh', ['-c', 'browser https://google.com'], { shell: false }) 152 | 153 | const result = await promiseSpawn.open('https://google.com', { command: 'browser' }) 154 | t.hasStrict(result, { 155 | code: 0, 156 | signal: undefined, 157 | }) 158 | 159 | t.ok(proc.called) 160 | }) 161 | 162 | t.test('when os.release() includes Microsoft treats as WSL', async (t) => { 163 | const promiseSpawnMock = t.mock('../lib/index.js', { 164 | os: { 165 | release: () => 'Microsoft', 166 | }, 167 | }) 168 | const browser = process.env.BROWSER 169 | process.env.BROWSER = '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe' 170 | 171 | const proc = spawk.spawn('sh', ['-c', 'sensible-browser https://google.com'], { shell: false }) 172 | 173 | const result = await promiseSpawnMock.open('https://google.com') 174 | t.hasStrict(result, { 175 | code: 0, 176 | signal: undefined, 177 | }) 178 | 179 | t.teardown(() => { 180 | process.env.BROWSER = browser 181 | }) 182 | 183 | t.ok(proc.called) 184 | }) 185 | 186 | t.test('when os.release() includes microsoft treats as WSL', async (t) => { 187 | const promiseSpawnMock = t.mock('../lib/index.js', { 188 | os: { 189 | release: () => 'microsoft', 190 | }, 191 | }) 192 | const browser = process.env.BROWSER 193 | process.env.BROWSER = '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe' 194 | 195 | const proc = spawk.spawn('sh', ['-c', 'sensible-browser https://google.com'], { shell: false }) 196 | 197 | const result = await promiseSpawnMock.open('https://google.com') 198 | t.hasStrict(result, { 199 | code: 0, 200 | signal: undefined, 201 | }) 202 | 203 | t.teardown(() => { 204 | process.env.BROWSER = browser 205 | }) 206 | 207 | t.ok(proc.called) 208 | }) 209 | 210 | t.test('fails on WSL if BROWSER is not set', async (t) => { 211 | const promiseSpawnMock = t.mock('../lib/index.js', { 212 | os: { 213 | release: () => 'microsoft', 214 | }, 215 | }) 216 | const browser = process.env.BROWSER 217 | delete process.env.BROWSER 218 | 219 | const proc = spawk.spawn('sh', ['-c', 'sensible-browser https://google.com'], { shell: false }) 220 | 221 | await t.rejects(promiseSpawnMock.open('https://google.com'), { 222 | message: 'Set the BROWSER environment variable to your desired browser.', 223 | }) 224 | 225 | t.teardown(() => { 226 | process.env.BROWSER = browser 227 | }) 228 | 229 | t.notOk(proc.called) 230 | }) 231 | 232 | t.end() 233 | }) 234 | 235 | // this covers anything that is not win32, darwin or linux 236 | t.test('process.platform === freebsd', (t) => { 237 | const platformDesc = Object.getOwnPropertyDescriptor(process, 'platform') 238 | Object.defineProperty(process, 'platform', { ...platformDesc, value: 'freebsd' }) 239 | t.teardown(() => { 240 | Object.defineProperty(process, 'platform', platformDesc) 241 | }) 242 | 243 | t.test('uses xdg-open with a shell', async (t) => { 244 | const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false }) 245 | 246 | const result = await promiseSpawn.open('https://google.com') 247 | t.hasStrict(result, { 248 | code: 0, 249 | signal: undefined, 250 | }) 251 | 252 | t.ok(proc.called) 253 | }) 254 | 255 | t.test('ignores shell = false', async (t) => { 256 | const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false }) 257 | 258 | const result = await promiseSpawn.open('https://google.com', { shell: false }) 259 | t.hasStrict(result, { 260 | code: 0, 261 | signal: undefined, 262 | }) 263 | 264 | t.ok(proc.called) 265 | }) 266 | 267 | t.test('respects opts.command', async (t) => { 268 | const proc = spawk.spawn('sh', ['-c', 'browser https://google.com'], { shell: false }) 269 | 270 | const result = await promiseSpawn.open('https://google.com', { command: 'browser' }) 271 | t.hasStrict(result, { 272 | code: 0, 273 | signal: undefined, 274 | }) 275 | 276 | t.ok(proc.called) 277 | }) 278 | 279 | t.end() 280 | }) 281 | -------------------------------------------------------------------------------- /test/shell.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const spawk = require('spawk') 4 | const t = require('tap') 5 | 6 | const promiseSpawn = require('../lib/index.js') 7 | 8 | spawk.preventUnmatched() 9 | t.afterEach(() => { 10 | spawk.clean() 11 | }) 12 | 13 | t.test('sh', (t) => { 14 | t.test('runs in shell', async (t) => { 15 | const proc = spawk.spawn('sh', ['-c', 'echo hello'], { shell: false }) 16 | .stdout(Buffer.from('hello\n')) 17 | 18 | const result = await promiseSpawn('echo', ['hello'], { shell: 'sh' }) 19 | t.hasStrict(result, { 20 | code: 0, 21 | signal: undefined, 22 | stdout: 'hello', 23 | stderr: '', 24 | }) 25 | 26 | t.ok(proc.called) 27 | }) 28 | 29 | t.test('escapes arguments', async (t) => { 30 | const proc = spawk.spawn('sh', ['-c', 'echo \'hello world\''], { shell: false }) 31 | .stdout(Buffer.from('hello\n')) 32 | 33 | const result = await promiseSpawn('echo', ['hello world'], { shell: 'sh' }) 34 | t.hasStrict(result, { 35 | code: 0, 36 | signal: undefined, 37 | stdout: 'hello', 38 | stderr: '', 39 | }) 40 | 41 | t.ok(proc.called) 42 | }) 43 | 44 | t.end() 45 | }) 46 | 47 | t.test('cmd', (t) => { 48 | t.test('runs in shell', async (t) => { 49 | const proc = spawk.spawn('cmd.exe', ['/d', '/s', '/c', 'echo hello'], { 50 | shell: false, 51 | windowsVerbatimArguments: true, 52 | }) 53 | .stdout(Buffer.from('hello\n')) 54 | 55 | const result = await promiseSpawn('echo', ['hello'], { shell: 'cmd.exe' }) 56 | t.hasStrict(result, { 57 | code: 0, 58 | signal: undefined, 59 | stdout: 'hello', 60 | stderr: '', 61 | }) 62 | 63 | t.ok(proc.called) 64 | }) 65 | 66 | t.test('works when initial cmd is wrapped in quotes', async (t) => { 67 | const proc = spawk.spawn('cmd.exe', ['/d', '/s', '/c', '"echo" hello'], { 68 | shell: false, 69 | windowsVerbatimArguments: true, 70 | }) 71 | .stdout(Buffer.from('hello\n')) 72 | 73 | const result = await promiseSpawn('"echo"', ['hello'], { shell: 'cmd.exe' }) 74 | t.hasStrict(result, { 75 | code: 0, 76 | signal: undefined, 77 | stdout: 'hello', 78 | stderr: '', 79 | }) 80 | 81 | t.ok(proc.called) 82 | }) 83 | 84 | t.test('works when initial cmd has a space and is wrapped in quotes', async (t) => { 85 | const proc = spawk.spawn('cmd.exe', ['/d', '/s', '/c', '"two words" hello'], { 86 | shell: false, 87 | windowsVerbatimArguments: true, 88 | }) 89 | .stdout(Buffer.from('hello\n')) 90 | 91 | const result = await promiseSpawn('"two words"', ['hello'], { shell: 'cmd.exe' }) 92 | t.hasStrict(result, { 93 | code: 0, 94 | signal: undefined, 95 | stdout: 'hello', 96 | stderr: '', 97 | }) 98 | 99 | t.ok(proc.called) 100 | }) 101 | 102 | t.test('works when initial cmd is more than one command', async (t) => { 103 | const proc = spawk.spawn('cmd.exe', ['/d', '/s', '/c', 'one two three hello'], { 104 | shell: false, 105 | windowsVerbatimArguments: true, 106 | }) 107 | .stdout(Buffer.from('hello\n')) 108 | 109 | const result = await promiseSpawn('one two three', ['hello'], { shell: 'cmd.exe' }) 110 | t.hasStrict(result, { 111 | code: 0, 112 | signal: undefined, 113 | stdout: 'hello', 114 | stderr: '', 115 | }) 116 | 117 | t.ok(proc.called) 118 | }) 119 | 120 | t.test('escapes when cmd is a .exe', async (t) => { 121 | const promiseSpawnMock = t.mock('../lib/index.js', { 122 | which: { 123 | sync: (key) => { 124 | t.equal(key, 'dir') 125 | return 'dir.exe' 126 | }, 127 | }, 128 | }) 129 | 130 | const proc = spawk.spawn('cmd.exe', ['/d', '/s', '/c', 'dir ^"with^ spaces^"'], { 131 | shell: false, 132 | windowsVerbatimArguments: true, 133 | }) 134 | 135 | const result = await promiseSpawnMock('dir', ['with spaces'], { shell: 'cmd.exe' }) 136 | t.hasStrict(result, { 137 | code: 0, 138 | signal: undefined, 139 | stdout: '', 140 | stderr: '', 141 | }) 142 | 143 | t.ok(proc.called) 144 | }) 145 | 146 | t.test('double escapes when cmd is a .cmd', async (t) => { 147 | const promiseSpawnMock = t.mock('../lib/index.js', { 148 | which: { 149 | sync: (key) => { 150 | t.equal(key, 'dir') 151 | return 'dir.cmd' 152 | }, 153 | }, 154 | }) 155 | 156 | const proc = spawk.spawn('cmd.exe', ['/d', '/s', '/c', 'dir ^^^"with^^^ spaces^^^"'], { 157 | shell: false, 158 | windowsVerbatimArguments: true, 159 | }) 160 | 161 | const result = await promiseSpawnMock('dir', ['with spaces'], { shell: 'cmd.exe' }) 162 | t.hasStrict(result, { 163 | code: 0, 164 | signal: undefined, 165 | stdout: '', 166 | stderr: '', 167 | }) 168 | 169 | t.ok(proc.called) 170 | }) 171 | 172 | t.test('which respects provided env PATH/PATHEXT', async (t) => { 173 | const PATH = 'C:\\Windows\\System32' 174 | const PATHEXT = 'EXE' 175 | 176 | const promiseSpawnMock = t.mock('../lib/index.js', { 177 | which: { 178 | sync: (key, opts) => { 179 | t.equal(key, 'dir') 180 | t.equal(opts.path, PATH) 181 | t.equal(opts.pathext, PATHEXT) 182 | return 'dir.exe' 183 | }, 184 | }, 185 | }) 186 | 187 | const proc = spawk.spawn('cmd.exe', ['/d', '/s', '/c', 'dir ^"with^ spaces^"'], { 188 | shell: false, 189 | windowsVerbatimArguments: true, 190 | }) 191 | 192 | const result = await promiseSpawnMock('dir', ['with spaces'], { 193 | env: { 194 | PATH, 195 | PATHEXT, 196 | }, 197 | shell: 'cmd.exe', 198 | }) 199 | t.hasStrict(result, { 200 | code: 0, 201 | signal: undefined, 202 | stdout: '', 203 | stderr: '', 204 | }) 205 | 206 | t.ok(proc.called) 207 | }) 208 | 209 | t.test('which respects variant casing for provided env PATH/PATHEXT', async (t) => { 210 | const PATH = 'C:\\Windows\\System32' 211 | const PATHEXT = 'EXE' 212 | 213 | const promiseSpawnMock = t.mock('../lib/index.js', { 214 | which: { 215 | sync: (key, opts) => { 216 | t.equal(key, 'dir') 217 | t.equal(opts.path, PATH) 218 | t.equal(opts.pathext, PATHEXT) 219 | return 'dir.exe' 220 | }, 221 | }, 222 | }) 223 | 224 | const proc = spawk.spawn('cmd.exe', ['/d', '/s', '/c', 'dir ^"with^ spaces^"'], { 225 | shell: false, 226 | windowsVerbatimArguments: true, 227 | }) 228 | 229 | const result = await promiseSpawnMock('dir', ['with spaces'], { 230 | env: { 231 | pAtH: PATH, 232 | pathEXT: PATHEXT, 233 | }, 234 | shell: 'cmd.exe', 235 | }) 236 | t.hasStrict(result, { 237 | code: 0, 238 | signal: undefined, 239 | stdout: '', 240 | stderr: '', 241 | }) 242 | 243 | t.ok(proc.called) 244 | }) 245 | 246 | t.end() 247 | }) 248 | --------------------------------------------------------------------------------