├── .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
└── index.js
├── package.json
├── release-please-config.json
└── test
├── backup-file copy.js
├── basic.js
├── buffer.js
├── conditional.js
├── error-file.js
├── exports.js
├── fixtures
├── basic.fixture.js
├── conditional.fixture.js
├── exports.fixture.js
├── fn.fixture.js
├── prompts.fixture.js
├── setup.js
├── simple.fixture.js
└── validate.fixture.js
├── fn.js
├── prompts.js
├── simple.js
└── validate.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 | ".": "2.0.0"
3 | }
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [2.0.0](https://github.com/npm/promzard/compare/v1.0.2...v2.0.0) (2024-09-25)
4 | ### ⚠️ BREAKING CHANGES
5 | * `promzard` now supports node `^18.17.0 || >=20.5.0`
6 | ### Bug Fixes
7 | * [`d679fa2`](https://github.com/npm/promzard/commit/d679fa22a5771001b22f489bf381e97d5f050351) [#91](https://github.com/npm/promzard/pull/91) align to npm 10 node engine range (@reggi)
8 | ### Dependencies
9 | * [`c389d6b`](https://github.com/npm/promzard/commit/c389d6bac7ddbe8d01aab36f1a65a3f68fc4369e) [#91](https://github.com/npm/promzard/pull/91) `read@4.0.0`
10 | ### Chores
11 | * [`df7ffed`](https://github.com/npm/promzard/commit/df7ffedea73d4c6395488517e83910174c943b74) [#91](https://github.com/npm/promzard/pull/91) run template-oss-apply (@reggi)
12 | * [`9a50db4`](https://github.com/npm/promzard/commit/9a50db4a0f3471dde91bec1d80ad885a41422725) [#89](https://github.com/npm/promzard/pull/89) bump @npmcli/eslint-config from 4.0.5 to 5.0.0 (@dependabot[bot])
13 | * [`ddb9e70`](https://github.com/npm/promzard/commit/ddb9e709c6ec30e3f3c3d25f712459ddd18b181f) [#90](https://github.com/npm/promzard/pull/90) postinstall for dependabot template-oss PR (@hashtagchris)
14 | * [`142f948`](https://github.com/npm/promzard/commit/142f948f18bec13a4211e889c0aaabcca3972c48) [#90](https://github.com/npm/promzard/pull/90) bump @npmcli/template-oss from 4.23.1 to 4.23.3 (@dependabot[bot])
15 |
16 | ## [1.0.2](https://github.com/npm/promzard/compare/v1.0.1...v1.0.2) (2024-05-04)
17 |
18 | ### Bug Fixes
19 |
20 | * [`1c79729`](https://github.com/npm/promzard/commit/1c797292cf9584e98f7a957463d245add8125a95) [#79](https://github.com/npm/promzard/pull/79) linting: no-console (@lukekarrys)
21 |
22 | ### Chores
23 |
24 | * [`376bde4`](https://github.com/npm/promzard/commit/376bde43e1b74a727607d7372722e84ec79a33ba) [#79](https://github.com/npm/promzard/pull/79) bump @npmcli/template-oss to 4.22.0 (@lukekarrys)
25 | * [`8bcec44`](https://github.com/npm/promzard/commit/8bcec4442d2916cb120ef1bcb47361dcf1774bfb) [#79](https://github.com/npm/promzard/pull/79) postinstall for dependabot template-oss PR (@lukekarrys)
26 | * [`cba412d`](https://github.com/npm/promzard/commit/cba412d02e6b748e3be09dc6ba16fc51bc372688) [#77](https://github.com/npm/promzard/pull/77) bump @npmcli/template-oss from 4.21.3 to 4.21.4 (@dependabot[bot])
27 |
28 | ## [1.0.1](https://github.com/npm/promzard/compare/v1.0.0...v1.0.1) (2024-04-01)
29 |
30 | ### Dependencies
31 |
32 | * [`445759b`](https://github.com/npm/promzard/commit/445759ba776c649436cd1f8ceed15765204a8f34) [#70](https://github.com/npm/promzard/pull/70) bump read from 2.1.0 to 3.0.1 (#70) (@dependabot[bot], @lukekarrys)
33 |
34 | ### Chores
35 |
36 | * [`47e194d`](https://github.com/npm/promzard/commit/47e194d58da2d2d8835931b1420b41dce42f411b) [#76](https://github.com/npm/promzard/pull/76) turn on autopublish (#76) (@lukekarrys)
37 | * [`70d4ae9`](https://github.com/npm/promzard/commit/70d4ae9fe75560c580597b5f601911f2d36adc86) [#72](https://github.com/npm/promzard/pull/72) postinstall for dependabot template-oss PR (@lukekarrys)
38 | * [`f21c0f6`](https://github.com/npm/promzard/commit/f21c0f69ddbcc18754322536bb799a8fdaaa0e38) [#72](https://github.com/npm/promzard/pull/72) bump @npmcli/template-oss from 4.21.1 to 4.21.3 (@dependabot[bot])
39 | * [`5fb3f70`](https://github.com/npm/promzard/commit/5fb3f70f1a4224c81113593f8487cb566d9fa3ec) [#67](https://github.com/npm/promzard/pull/67) postinstall for dependabot template-oss PR (@lukekarrys)
40 | * [`d051eb5`](https://github.com/npm/promzard/commit/d051eb5c7f2c6d672281b7066a01972f5f95ed21) [#67](https://github.com/npm/promzard/pull/67) bump @npmcli/template-oss from 4.19.0 to 4.21.1 (@dependabot[bot])
41 | * [`4ddf313`](https://github.com/npm/promzard/commit/4ddf313a261649c15ac86ce555c903d5e13454a3) [#48](https://github.com/npm/promzard/pull/48) postinstall for dependabot template-oss PR (@lukekarrys)
42 | * [`d11c8c1`](https://github.com/npm/promzard/commit/d11c8c12d266614ce9aee79c0357ca5d2c87940d) [#48](https://github.com/npm/promzard/pull/48) bump @npmcli/template-oss from 4.18.1 to 4.19.0 (@dependabot[bot])
43 | * [`64c2ec8`](https://github.com/npm/promzard/commit/64c2ec8bee72d5dbc71e639de3c24074fbed0042) [#47](https://github.com/npm/promzard/pull/47) postinstall for dependabot template-oss PR (@lukekarrys)
44 | * [`d236976`](https://github.com/npm/promzard/commit/d236976a98fcbb14ae2c2d397acb958204156c8f) [#47](https://github.com/npm/promzard/pull/47) bump @npmcli/template-oss from 4.18.0 to 4.18.1 (@dependabot[bot])
45 | * [`70f916a`](https://github.com/npm/promzard/commit/70f916aabd8960e1bc22561e10d618545de21c42) [#46](https://github.com/npm/promzard/pull/46) postinstall for dependabot template-oss PR (@lukekarrys)
46 | * [`66c0af6`](https://github.com/npm/promzard/commit/66c0af6f1e2ac5e25cafa5e6b9b7176ae3eae66b) [#46](https://github.com/npm/promzard/pull/46) bump @npmcli/template-oss from 4.17.0 to 4.18.0 (@dependabot[bot])
47 | * [`3abd0eb`](https://github.com/npm/promzard/commit/3abd0eb9490b1cd2f0c2be100e6f262da3517d13) [#45](https://github.com/npm/promzard/pull/45) postinstall for dependabot template-oss PR (@lukekarrys)
48 | * [`9c481e7`](https://github.com/npm/promzard/commit/9c481e7e2d03a7a37eaa84d90682fd528d88793e) [#45](https://github.com/npm/promzard/pull/45) bump @npmcli/template-oss from 4.15.1 to 4.17.0 (@dependabot[bot])
49 | * [`2167b18`](https://github.com/npm/promzard/commit/2167b182da6b80035eb60bf0c2638fd4fca85559) [#44](https://github.com/npm/promzard/pull/44) postinstall for dependabot template-oss PR (@lukekarrys)
50 | * [`a923fd4`](https://github.com/npm/promzard/commit/a923fd4d235920f5d95407c6699582a5a085b79b) [#44](https://github.com/npm/promzard/pull/44) bump @npmcli/template-oss from 4.14.1 to 4.15.1 (@dependabot[bot])
51 | * [`fbb70ca`](https://github.com/npm/promzard/commit/fbb70ca952175184db7fb6c2e5e43d8e7c8cf464) [#43](https://github.com/npm/promzard/pull/43) bump @npmcli/template-oss from 4.12.1 to 4.14.1 (#43) (@dependabot[bot], @npm-cli-bot, @nlf)
52 | * [`95695cd`](https://github.com/npm/promzard/commit/95695cd2432a030efa5313682155d8eea36fe814) [#42](https://github.com/npm/promzard/pull/42) bump @npmcli/template-oss from 4.12.0 to 4.12.1 (#42) (@dependabot[bot], @npm-cli-bot)
53 | * [`be54ca0`](https://github.com/npm/promzard/commit/be54ca04b6661ffbd947f6ee029898bab1610f8f) [#41](https://github.com/npm/promzard/pull/41) postinstall for dependabot template-oss PR (@lukekarrys)
54 | * [`41e457b`](https://github.com/npm/promzard/commit/41e457b70a6d849cfa0b612e58afc50b89c127eb) [#41](https://github.com/npm/promzard/pull/41) bump @npmcli/template-oss from 4.11.4 to 4.12.0 (@dependabot[bot])
55 | * [`e89243d`](https://github.com/npm/promzard/commit/e89243dfc2f9e6c483987eaf32acfdb3a2f5833b) [#40](https://github.com/npm/promzard/pull/40) postinstall for dependabot template-oss PR (@lukekarrys)
56 | * [`24f9830`](https://github.com/npm/promzard/commit/24f983072f2bfac4608735ce61204426296eed89) [#40](https://github.com/npm/promzard/pull/40) bump @npmcli/template-oss from 4.11.3 to 4.11.4 (@dependabot[bot])
57 | * [`5413040`](https://github.com/npm/promzard/commit/5413040ee8c1d3c7caf089e499abbaee8aa50cbe) [#39](https://github.com/npm/promzard/pull/39) postinstall for dependabot template-oss PR (@lukekarrys)
58 | * [`0840251`](https://github.com/npm/promzard/commit/08402519c6f5dd47d548221f2b7f5a4f8982c6b3) [#39](https://github.com/npm/promzard/pull/39) bump @npmcli/template-oss from 4.11.0 to 4.11.3 (@dependabot[bot])
59 |
60 | ## [1.0.0](https://github.com/npm/promzard/compare/v0.3.0...v1.0.0) (2022-12-15)
61 |
62 | ### ⚠️ BREAKING CHANGES
63 |
64 | * refactor module
65 | - all async interfaces are promise only and no longer accept a callback
66 | - the `PromZard` class is no longer an event emitter
67 | - the `load` method must be manually called after creating a class
68 |
69 | ### Features
70 |
71 | * [`d37e442`](https://github.com/npm/promzard/commit/d37e4422075eda27a3951e8ab2f3d9f4f265a122) refactor (@lukekarrys)
72 |
73 | ### Bug Fixes
74 |
75 | * [`3c113db`](https://github.com/npm/promzard/commit/3c113db7a1ce0f8787ec0bc98bc3b1353eeaf109) [#38](https://github.com/npm/promzard/pull/38) 100 test coverage (#38) (@lukekarrys)
76 |
77 | ### Dependencies
78 |
79 | * [`9f2b9aa`](https://github.com/npm/promzard/commit/9f2b9aaa058472b61e4538cb4e0866b3ebfd48ff) `read@2.0.0`
80 |
--------------------------------------------------------------------------------
/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) Isaac Z. Schlueter
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 AUTHOR DISCLAIMS ALL WARRANTIES
10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # promzard
2 |
3 | A prompting wizard for building files from specialized PromZard modules.
4 | Used by `npm init`.
5 |
6 | A reimplementation of @SubStack's
7 | [prompter](https://github.com/substack/node-prompter), which does not
8 | use AST traversal.
9 |
10 | From another point of view, it's a reimplementation of
11 | [@Marak](https://github.com/marak)'s
12 | [wizard](https://github.com/Marak/wizard) which doesn't use schemas.
13 |
14 | The goal is a nice drop-in enhancement for `npm init`.
15 |
16 | ## Usage
17 |
18 | ```javascript
19 | const promzard = require('promzard')
20 | const data = await promzard(inputFile, optionalContextAdditions, options)
21 | ```
22 |
23 | In the `inputFile` you can have something like this:
24 |
25 | ```javascript
26 | const fs = require('fs/promises')
27 | module.exports = {
28 | "greeting": prompt("Who shall you greet?", "world", (who) => `Hello, ${who}`),
29 | "filename": __filename,
30 | "directory": async () => {
31 | const entries = await fs.readdir(__dirname)
32 | return entries.map(e => `entry: ${e}`)
33 | }
34 | }
35 | ```
36 |
37 | When run, promzard will display the prompts and resolve the async
38 | functions in order, and then either give you an error, or the resolved
39 | data, ready to be dropped into a JSON file or some other place.
40 |
41 |
42 | ### promzard(inputFile, ctx, options)
43 |
44 | The inputFile is just a node module. You can require() things, set
45 | module.exports, etc. Whatever that module exports is the result, and it
46 | is walked over to call any functions as described below.
47 |
48 | The only caveat is that you must give PromZard the full absolute path
49 | to the module (you can get this via Node's `require.resolve`.) Also,
50 | the `prompt` function is injected into the context object, so watch out.
51 |
52 | Whatever you put in that `ctx` will of course also be available in the
53 | module. You can get quite fancy with this, passing in existing configs
54 | and so on.
55 |
56 | #### options.backupFile
57 |
58 | Use the `backupFile` option as a fallback when `inputFile` fails to be read.
59 |
60 | ### Class: promzard.PromZard(file, ctx, options).load()
61 |
62 | Just like the `promzard` function, but the class that makes it
63 | all happen. The `load` method returns a promise which will resolve
64 | to the resolved data or throw with an error.
65 |
66 | ### prompt(...)
67 |
68 | In the promzard input module, you can call the `prompt` function.
69 | This prompts the user to input some data. The arguments are interpreted
70 | based on type:
71 |
72 | 1. `string` The first string encountered is the prompt. The second is
73 | the default value.
74 | 2. `function` A transformer function which receives the data and returns
75 | something else. More than meets the eye.
76 | 3. `object` The `prompt` member is the prompt, the `default` member is
77 | the default value, and the `transform` is the transformer.
78 |
79 | Whatever the final value is, that's what will be put on the resulting
80 | object.
81 |
82 | ### Functions
83 |
84 | If there are any functions on the promzard input module's exports, then
85 | promzard will await each of them. This way, your module
86 | can do asynchronous actions if necessary to validate or ascertain
87 | whatever needs verification.
88 |
89 | The functions are called in the context of the ctx object.
90 |
91 | In the async function, you can also call prompt() and return the result
92 | of the prompt.
93 |
94 | For example, this works fine in a promzard module:
95 |
96 | ```js
97 | exports.asyncPrompt = async function () {
98 | const st = await fs.stat(someFile)
99 | // if there's an error, no prompt, just error
100 | // otherwise prompt and use the actual file size as the default
101 | return prompt('file size', st.size)
102 | }
103 | ```
104 |
105 | You can also return other async functions in the async function
106 | callback. Though that's a bit silly, it could be a handy way to reuse
107 | functionality in some cases.
108 |
109 | ### Sync vs Async
110 |
111 | The `prompt()` function is not synchronous, though it appears that way.
112 | It just returns a token that is swapped out when the data object is
113 | walked over asynchronously later, and returns a token.
114 |
115 | For that reason, prompt() calls whose results don't end up on the data
116 | object are never shown to the user. For example, this will only prompt
117 | once:
118 |
119 | ```
120 | exports.promptThreeTimes = prompt('prompt me once', 'shame on you')
121 | exports.promptThreeTimes = prompt('prompt me twice', 'um....')
122 | exports.promptThreeTimes = prompt('you cant prompt me again')
123 | ```
124 |
125 | ### Isn't this exactly the sort of 'looks sync' that you said was bad about other libraries?
126 |
127 | Yeah, sorta. I wouldn't use promzard for anything more complicated than
128 | a wizard that spits out prompts to set up a config file or something.
129 | Maybe there are other use cases I haven't considered.
130 |
--------------------------------------------------------------------------------
/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/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs/promises')
2 | const { runInThisContext } = require('vm')
3 | const { promisify } = require('util')
4 | const { randomBytes } = require('crypto')
5 | const { Module } = require('module')
6 | const { dirname, basename } = require('path')
7 | const { read } = require('read')
8 |
9 | const files = {}
10 |
11 | class PromZard {
12 | #file = null
13 | #backupFile = null
14 | #ctx = null
15 | #unique = randomBytes(8).toString('hex')
16 | #prompts = []
17 |
18 | constructor (file, ctx = {}, options = {}) {
19 | this.#file = file
20 | this.#ctx = ctx
21 | this.#backupFile = options.backupFile
22 | }
23 |
24 | static async promzard (file, ctx, options) {
25 | const pz = new PromZard(file, ctx, options)
26 | return pz.load()
27 | }
28 |
29 | static async fromBuffer (buf, ctx, options) {
30 | let filename = 0
31 | do {
32 | filename = '\0' + Math.random()
33 | } while (files[filename])
34 | files[filename] = buf
35 | const ret = await PromZard.promzard(filename, ctx, options)
36 | delete files[filename]
37 | return ret
38 | }
39 |
40 | async load () {
41 | if (files[this.#file]) {
42 | return this.#loaded()
43 | }
44 |
45 | try {
46 | files[this.#file] = await fs.readFile(this.#file, 'utf8')
47 | } catch (er) {
48 | if (er && this.#backupFile) {
49 | this.#file = this.#backupFile
50 | this.#backupFile = null
51 | return this.load()
52 | }
53 | throw er
54 | }
55 |
56 | return this.#loaded()
57 | }
58 |
59 | async #loaded () {
60 | const mod = new Module(this.#file, module)
61 | mod.loaded = true
62 | mod.filename = this.#file
63 | mod.id = this.#file
64 | mod.paths = Module._nodeModulePaths(dirname(this.#file))
65 |
66 | this.#ctx.prompt = this.#makePrompt()
67 | this.#ctx.__filename = this.#file
68 | this.#ctx.__dirname = dirname(this.#file)
69 | this.#ctx.__basename = basename(this.#file)
70 | this.#ctx.module = mod
71 | this.#ctx.require = (p) => mod.require(p)
72 | this.#ctx.require.resolve = (p) => Module._resolveFilename(p, mod)
73 | this.#ctx.exports = mod.exports
74 |
75 | const body = `(function(${Object.keys(this.#ctx).join(', ')}) { ${files[this.#file]}\n })`
76 | runInThisContext(body, this.#file).apply(this.#ctx, Object.values(this.#ctx))
77 | this.#ctx.res = mod.exports
78 |
79 | return this.#walk()
80 | }
81 |
82 | #makePrompt () {
83 | return (...args) => {
84 | let p, d, t
85 | for (let i = 0; i < args.length; i++) {
86 | const a = args[i]
87 | if (typeof a === 'string') {
88 | if (p) {
89 | d = a
90 | } else {
91 | p = a
92 | }
93 | } else if (typeof a === 'function') {
94 | t = a
95 | } else if (a && typeof a === 'object') {
96 | p = a.prompt || p
97 | d = a.default || d
98 | t = a.transform || t
99 | }
100 | }
101 | try {
102 | return `${this.#unique}-${this.#prompts.length}`
103 | } finally {
104 | this.#prompts.push([p, d, t])
105 | }
106 | }
107 | }
108 |
109 | async #walk (o = this.#ctx.res) {
110 | const keys = Object.keys(o)
111 |
112 | const len = keys.length
113 | let i = 0
114 |
115 | while (i < len) {
116 | const k = keys[i]
117 | const v = o[k]
118 | i++
119 |
120 | if (v && typeof v === 'object') {
121 | o[k] = await this.#walk(v)
122 | continue
123 | }
124 |
125 | if (v && typeof v === 'string' && v.startsWith(this.#unique)) {
126 | const n = +v.slice(this.#unique.length + 1)
127 |
128 | // default to the key
129 | // default to the ctx value, if there is one
130 | const [prompt = k, def = this.#ctx[k], tx] = this.#prompts[n]
131 |
132 | try {
133 | o[k] = await this.#prompt(prompt, def, tx)
134 | } catch (er) {
135 | if (er.notValid) {
136 | // eslint-disable-next-line no-console
137 | console.log(er.message)
138 | i--
139 | } else {
140 | throw er
141 | }
142 | }
143 | continue
144 | }
145 |
146 | if (typeof v === 'function') {
147 | // XXX: remove v.length check to remove cb from functions
148 | // would be a breaking change for `npm init`
149 | // XXX: if cb is no longer an argument then this.#ctx should
150 | // be passed in to allow arrow fns to be used and still access ctx
151 | const fn = v.length ? promisify(v) : v
152 | o[k] = await fn.call(this.#ctx)
153 | // back up so that we process this one again.
154 | // this is because it might return a prompt() call in the cb.
155 | i--
156 | continue
157 | }
158 | }
159 |
160 | return o
161 | }
162 |
163 | async #prompt (prompt, def, tx) {
164 | const res = await read({ prompt: prompt + ':', default: def }).then((r) => tx ? tx(r) : r)
165 | // XXX: remove this to require throwing an error instead of
166 | // returning it. would be a breaking change for `npm init`
167 | if (res instanceof Error && res.notValid) {
168 | throw res
169 | }
170 | return res
171 | }
172 | }
173 |
174 | module.exports = PromZard.promzard
175 | module.exports.fromBuffer = PromZard.fromBuffer
176 | module.exports.PromZard = PromZard
177 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "GitHub Inc.",
3 | "name": "promzard",
4 | "description": "prompting wizardly",
5 | "version": "2.0.0",
6 | "repository": {
7 | "url": "git+https://github.com/npm/promzard.git",
8 | "type": "git"
9 | },
10 | "dependencies": {
11 | "read": "^4.0.0"
12 | },
13 | "devDependencies": {
14 | "@npmcli/eslint-config": "^5.0.0",
15 | "@npmcli/template-oss": "4.24.3",
16 | "tap": "^16.3.0"
17 | },
18 | "main": "lib/index.js",
19 | "scripts": {
20 | "test": "tap",
21 | "lint": "npm run eslint",
22 | "postlint": "template-oss-check",
23 | "template-oss-apply": "template-oss-apply --force",
24 | "lintfix": "npm run eslint -- --fix",
25 | "snap": "tap",
26 | "posttest": "npm run lint",
27 | "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\""
28 | },
29 | "license": "ISC",
30 | "files": [
31 | "bin/",
32 | "lib/"
33 | ],
34 | "engines": {
35 | "node": "^18.17.0 || >=20.5.0"
36 | },
37 | "templateOSS": {
38 | "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
39 | "version": "4.24.3",
40 | "publish": true
41 | },
42 | "tap": {
43 | "jobs": 1,
44 | "test-ignore": "fixtures/",
45 | "nyc-arg": [
46 | "--exclude",
47 | "tap-snapshots/**"
48 | ]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/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/backup-file copy.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 | const { setup, child, isChild, getFixture } = require('./fixtures/setup')
3 |
4 | if (isChild()) {
5 | return child('file does not exist', { tmpdir: '/tmp' }, { backupFile: getFixture('simple') })
6 | }
7 |
8 | t.test('backup file', async (t) => {
9 | const output = await setup(__filename, ['', '55', 'no'])
10 |
11 | t.same(JSON.parse(output), {
12 | a: 3,
13 | b: '!2b',
14 | c: {
15 | x: 55,
16 | y: '/tmp/y/file.txt',
17 | },
18 | error: 'no',
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/test/basic.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 | const { setup, child, isChild } = require('./fixtures/setup')
3 |
4 | if (isChild()) {
5 | return child(__filename, { basename: 'node-example' })
6 | }
7 |
8 | t.test('run the example', async (t) => {
9 | const output = await setup(__filename, [
10 | 'testing description',
11 | 'test-entry.js',
12 | 'fugazi function waiting room',
13 | ])
14 |
15 | t.same(JSON.parse(output), {
16 | name: 'example',
17 | version: '0.0.0',
18 | description: 'testing description',
19 | main: 'test-entry.js',
20 | resolved: 'index.js',
21 | directories: {
22 | example: 'example',
23 | test: 'test',
24 | },
25 | dependencies: {},
26 | devDependencies: {
27 | tap: '~0.2.5',
28 | },
29 | scripts: {
30 | test: 'tap test/*.js',
31 | },
32 | repository: {
33 | type: 'git',
34 | url: 'git://github.com/substack/node-example.git',
35 | },
36 | homepage: 'https://github.com/substack/node-example',
37 | keywords: [
38 | 'fugazi',
39 | 'function',
40 | 'waiting',
41 | 'room',
42 | ],
43 | author: {
44 | name: 'James Halliday',
45 | email: 'mail@substack.net',
46 | url: 'http://substack.net',
47 | },
48 | license: 'MIT',
49 | engine: {
50 | node: '>=0.6',
51 | },
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/test/buffer.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 | const { setup, childBuffer, isChild } = require('./fixtures/setup')
3 |
4 | if (isChild()) {
5 | return childBuffer('basic', { basename: 'node-example' })
6 | }
7 |
8 | t.test('run the example', async (t) => {
9 | const output = await setup(__filename, [
10 | 'testing description',
11 | 'test-entry.js',
12 | 'fugazi function waiting room',
13 | ])
14 |
15 | t.same(JSON.parse(output), {
16 | name: 'example',
17 | version: '0.0.0',
18 | description: 'testing description',
19 | main: 'test-entry.js',
20 | resolved: 'error',
21 | directories: {
22 | example: 'example',
23 | test: 'test',
24 | },
25 | dependencies: {},
26 | devDependencies: {
27 | tap: '~0.2.5',
28 | },
29 | scripts: {
30 | test: 'tap test/*.js',
31 | },
32 | repository: {
33 | type: 'git',
34 | url: 'git://github.com/substack/node-example.git',
35 | },
36 | homepage: 'https://github.com/substack/node-example',
37 | keywords: [
38 | 'fugazi',
39 | 'function',
40 | 'waiting',
41 | 'room',
42 | ],
43 | author: {
44 | name: 'James Halliday',
45 | email: 'mail@substack.net',
46 | url: 'http://substack.net',
47 | },
48 | license: 'MIT',
49 | engine: {
50 | node: '>=0.6',
51 | },
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/test/conditional.js:
--------------------------------------------------------------------------------
1 |
2 | const t = require('tap')
3 | const { setup: _setup, child, isChild } = require('./fixtures/setup')
4 |
5 | if (isChild()) {
6 | return child(__filename)
7 | }
8 |
9 | const setup = (...args) => _setup(__filename, args).then(JSON.parse)
10 |
11 | t.test('conditional', async (t) => {
12 | t.same(await setup(''), {})
13 | t.same(await setup('a'), {})
14 | t.same(await setup('git', ''), {})
15 | t.same(await setup('git', 'http'), {
16 | repository: {
17 | type: 'git',
18 | url: 'http',
19 | },
20 | })
21 | t.same(await setup('svn', 'http'), {
22 | repository: {
23 | type: 'svn',
24 | url: 'http',
25 | },
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/test/error-file.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 | const { setup, child, isChild } = require('./fixtures/setup')
3 |
4 | if (isChild()) {
5 | return child('file does not exist')
6 | }
7 |
8 | t.test('backup file', async (t) => {
9 | t.match(await setup(__filename), 'ENOENT')
10 | })
11 |
--------------------------------------------------------------------------------
/test/exports.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 | const { setup, child, isChild } = require('./fixtures/setup')
3 |
4 | if (isChild()) {
5 | return child(__filename, { tmpdir: '/tmp' })
6 | }
7 |
8 | t.test('exports', async (t) => {
9 | const output = await setup(__filename, ['', '55'])
10 |
11 | t.same(JSON.parse(output), {
12 | a: 3,
13 | b: '!2b',
14 | c: {
15 | x: 55,
16 | y: '/tmp/y/file.txt',
17 | },
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/test/fixtures/basic.fixture.js:
--------------------------------------------------------------------------------
1 | /* globals prompt, basename */
2 |
3 | const fs = require('fs/promises')
4 | const path = require('path')
5 |
6 | module.exports = {
7 | name: basename.replace(/^node-/, ''),
8 | version: '0.0.0',
9 | description: async () => {
10 | const value = await fs.readFile('README.markdown', 'utf8')
11 | .then((src) => src.split('\n')
12 | .find((l) => /\s+/.test(l) && l.trim() !== basename.replace(/^node-/, ''))
13 | .trim()
14 | .replace(/^./, c => c.toLowerCase())
15 | .replace(/\.$/, ''))
16 | .catch(() => null)
17 | return prompt('description', value)
18 | },
19 | main: prompt('entry point', 'index.js'),
20 | resolved: () => {
21 | try {
22 | return path.basename(require.resolve('../../'))
23 | } catch {
24 | return 'error'
25 | }
26 | },
27 | bin: async function () {
28 | const exists = await fs.stat('bin/cmd.js')
29 | .then(() => true)
30 | .catch(() => false)
31 | return exists ? {
32 | [basename.replace(/^node-/, '')]: 'bin/cmd.js',
33 | } : undefined
34 | },
35 | directories: {
36 | example: 'example',
37 | test: 'test',
38 | },
39 | dependencies: {},
40 | devDependencies: {
41 | tap: '~0.2.5',
42 | },
43 | scripts: {
44 | test: 'tap test/*.js',
45 | },
46 | repository: {
47 | type: 'git',
48 | url: 'git://github.com/substack/' + basename + '.git',
49 | },
50 | homepage: 'https://github.com/substack/' + basename,
51 | keywords: prompt((s) => s.split(/\s+/)),
52 | author: {
53 | name: 'James Halliday',
54 | email: 'mail@substack.net',
55 | url: 'http://substack.net',
56 | },
57 | license: 'MIT',
58 | engine: { node: '>=0.6' },
59 | }
60 |
--------------------------------------------------------------------------------
/test/fixtures/conditional.fixture.js:
--------------------------------------------------------------------------------
1 | /* globals prompt */
2 |
3 | module.exports = {
4 | repository: {
5 | type: prompt('repo type'),
6 | url () {
7 | if (['git', 'svn'].includes(this.res.repository.type)) {
8 | return prompt(`${this.res.repository.type} url`)
9 | }
10 | },
11 | },
12 | // this name of this doesnt matter, just that it comes last
13 | '' () {
14 | if (!this.res.repository.type || !this.res.repository.url) {
15 | delete this.res.repository
16 | }
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/test/fixtures/exports.fixture.js:
--------------------------------------------------------------------------------
1 | /* globals prompt, tmpdir */
2 |
3 | exports.a = 1 + 2
4 | exports.b = prompt('To be or not to be?', '!2b')
5 | exports.c = {}
6 | exports.c.x = prompt()
7 | exports.c.y = tmpdir + '/y/file.txt'
8 |
--------------------------------------------------------------------------------
/test/fixtures/fn.fixture.js:
--------------------------------------------------------------------------------
1 | /* globals prompt, tmpdir */
2 |
3 | const fs = require('fs/promises')
4 |
5 | module.exports = {
6 | a: 1 + 2,
7 | b: prompt('To be or not to be?', '!2b', (s) => s.toUpperCase() + '...'),
8 | c: {
9 | x: prompt((x) => x * 100),
10 | y: tmpdir + '/y/file.txt',
11 | },
12 | a_function: () => fs.readFile(__filename, 'utf8'),
13 | asyncPrompt: async () => {
14 | await new Promise(r => setTimeout(r, 100))
15 | return prompt('a prompt at any other time would smell as sweet')
16 | },
17 | cbPrompt: (cb) => {
18 | setTimeout(() => {
19 | cb(null, prompt('still works with callbacks'))
20 | }, 100)
21 | },
22 | }
23 |
--------------------------------------------------------------------------------
/test/fixtures/prompts.fixture.js:
--------------------------------------------------------------------------------
1 | /* globals prompt */
2 |
3 | const transform = (a) => a
4 |
5 | module.exports = {
6 | a: prompt({
7 | prompt: 'a',
8 | default: 'a',
9 | transform,
10 | }),
11 | b: prompt('b', 'b', () => 'b', {
12 | prompt: 'a',
13 | default: 'a',
14 | transform,
15 | }),
16 | c: prompt('c', 'c', transform, {}),
17 | }
18 |
--------------------------------------------------------------------------------
/test/fixtures/setup.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('child_process')
2 | const { readFile } = require('fs/promises')
3 | const path = require('path')
4 | const promzard = require('../../')
5 |
6 | const CHILD = 'child'
7 |
8 | const isChild = () => process.argv[2] === CHILD
9 |
10 | const setup = async (file, writes = []) => {
11 | const proc = spawn(process.execPath, [file, CHILD])
12 | const entries = Array.isArray(writes) ? writes : Object.entries(writes)
13 |
14 | let i = 0
15 | let output = ''
16 |
17 | proc.stderr.on('data', (c) => output += c)
18 | proc.stdout.on('data', (c) => {
19 | let write = entries[i]
20 | if (Array.isArray(write)) {
21 | if (write[0].test(c.toString())) {
22 | write = write[1]
23 | } else {
24 | return
25 | }
26 | }
27 | i++
28 | process.nextTick(() => proc.stdin[writes.length === i ? 'end' : 'write'](`${write}\n`))
29 | })
30 |
31 | await new Promise(res => proc.on('close', res))
32 |
33 | return output
34 | }
35 |
36 | const getFixture = (f) => path.join(__dirname, path.basename(f, '.js') + '.fixture.js')
37 |
38 | async function child (f, ctx, options) {
39 | const output = await promzard(getFixture(f), ctx, options)
40 | console.error(JSON.stringify(output))
41 | }
42 |
43 | async function childBuffer (f, ctx, options) {
44 | const buf = await readFile(getFixture(f))
45 | const output = await promzard.fromBuffer(buf, ctx, options)
46 | console.error(JSON.stringify(output))
47 | }
48 |
49 | module.exports = { setup, child, childBuffer, isChild, getFixture }
50 |
--------------------------------------------------------------------------------
/test/fixtures/simple.fixture.js:
--------------------------------------------------------------------------------
1 | /* globals prompt, tmpdir */
2 |
3 | module.exports = {
4 | a: 1 + 2,
5 | b: prompt('To be or not to be?', '!2b'),
6 | c: {
7 | x: prompt(),
8 | y: tmpdir + '/y/file.txt',
9 | },
10 | error: prompt('error', (v) => {
11 | if (v === 'throw') {
12 | throw new Error('this is unexpected')
13 | }
14 | return v
15 | }),
16 | }
17 |
--------------------------------------------------------------------------------
/test/fixtures/validate.fixture.js:
--------------------------------------------------------------------------------
1 | /* globals prompt */
2 |
3 | module.exports = {
4 | name: prompt('name', (data) => {
5 | if (data === 'cool') {
6 | return data
7 | }
8 | return Object.assign(new Error('name must be cool'), {
9 | notValid: true,
10 | })
11 | }),
12 | name2: prompt('name2', (data) => {
13 | if (data === 'cool') {
14 | return data
15 | }
16 | throw Object.assign(new Error('name must be cool'), {
17 | notValid: true,
18 | })
19 | }),
20 | }
21 |
--------------------------------------------------------------------------------
/test/fn.js:
--------------------------------------------------------------------------------
1 |
2 | const t = require('tap')
3 | const fs = require('fs')
4 | const path = require('path')
5 | const { setup, child, isChild } = require('./fixtures/setup')
6 |
7 | if (isChild()) {
8 | return child(__filename, { tmpdir: '/tmp' })
9 | }
10 |
11 | t.test('prompt callback param', async (t) => {
12 | const output = await setup(__filename, ['', '55', 'async prompt', 'cb prompt'])
13 |
14 | t.same(JSON.parse(output), {
15 | a: 3,
16 | b: '!2B...',
17 | c: {
18 | x: 5500,
19 | y: '/tmp/y/file.txt',
20 | },
21 | a_function: fs.readFileSync(path.resolve(__dirname, 'fixtures/fn.fixture.js'), 'utf8'),
22 | asyncPrompt: 'async prompt',
23 | cbPrompt: 'cb prompt',
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/test/prompts.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 | const { setup, child, isChild } = require('./fixtures/setup')
3 |
4 | if (isChild()) {
5 | return child(__filename)
6 | }
7 |
8 | t.test('prompts', async (t) => {
9 | const output = await setup(__filename, ['', '', ''])
10 |
11 | t.same(JSON.parse(output), {
12 | a: 'a',
13 | b: 'a',
14 | c: 'c',
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/test/simple.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 | const { setup: _setup, child, isChild } = require('./fixtures/setup')
3 |
4 | if (isChild()) {
5 | return child(__filename, { tmpdir: '/tmp' })
6 | }
7 |
8 | const setup = (...args) => _setup(__filename, args)
9 |
10 | t.test('simple', async (t) => {
11 | t.same(await setup('', '55', 'no error').then(JSON.parse), {
12 | a: 3,
13 | b: '!2b',
14 | c: {
15 | x: 55,
16 | y: '/tmp/y/file.txt',
17 | },
18 | error: 'no error',
19 | })
20 |
21 | t.match(await setup('', '55', 'throw'), /Error: this is unexpected/)
22 | })
23 |
--------------------------------------------------------------------------------
/test/validate.js:
--------------------------------------------------------------------------------
1 |
2 | const t = require('tap')
3 | const { setup, child, isChild } = require('./fixtures/setup')
4 |
5 | if (isChild()) {
6 | return child(__filename)
7 | }
8 |
9 | t.test('validate', async (t) => {
10 | const output = await setup(__filename, [
11 | [/name: $/, 'not cool'],
12 | [/name: $/, 'cool'],
13 | [/name2: $/, 'not cool'],
14 | [/name2: $/, 'cool'],
15 | ])
16 |
17 | t.same(JSON.parse(output), { name: 'cool', name2: 'cool' })
18 | })
19 |
--------------------------------------------------------------------------------