├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------