├── .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 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | All interactions in this repo are covered by the [npm Code of 4 | Conduct](https://docs.npmjs.com/policies/conduct) 5 | 6 | The npm cli team may, at its own discretion, moderate, remove, or edit 7 | any interactions such as pull requests, issues, and comments. 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | # Contributing 4 | 5 | ## Code of Conduct 6 | 7 | All interactions in the **npm** organization on GitHub are considered to be covered by our standard [Code of Conduct](https://docs.npmjs.com/policies/conduct). 8 | 9 | ## Reporting Bugs 10 | 11 | Before submitting a new bug report please search for an existing or similar report. 12 | 13 | Use one of our existing issue templates if you believe you've come across a unique problem. 14 | 15 | Duplicate issues, or issues that don't use one of our templates may get closed without a response. 16 | 17 | ## Pull Request Conventions 18 | 19 | ### Commits 20 | 21 | We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 22 | 23 | When opening a pull request please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: 24 | 25 | - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. 26 | - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. 27 | - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. 28 | - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be *no* change to the version of the package when it is next published (as the commit does not affect the published version). 29 | 30 | ### Test Coverage 31 | 32 | Pull requests made against this repo will run `npm test` automatically. Please make sure tests pass locally before submitting a PR. 33 | 34 | Every new feature or bug fix should come with a corresponding test or tests that validate the solutions. Testing also reports on code coverage and will fail if code coverage drops. 35 | 36 | ### Linting 37 | 38 | Linting is also done automatically once tests pass. `npm run lintfix` will fix most linting errors automatically. 39 | 40 | Please make sure linting passes before submitting a PR. 41 | 42 | ## What _not_ to contribute? 43 | 44 | ### Dependencies 45 | 46 | It should be noted that our team does not accept third-party dependency updates/PRs. If you submit a PR trying to update our dependencies we will close it with or without a reference to these contribution guidelines. 47 | 48 | ### Tools/Automation 49 | 50 | Our core team is responsible for the maintenance of the tooling/automation in this project and we ask contributors to not make changes to these when contributing (e.g. `.github/*`, `.eslintrc.json`, `.licensee.json`). Most of those files also have a header at the top to remind folks they are automatically generated. Pull requests that alter these will not be accepted. 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) 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 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 4 | 5 | If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. 6 | 7 | If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [opensource-security@github.com](mailto:opensource-security@github.com). 8 | 9 | If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award. 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 12 | 13 | Thanks for helping make GitHub safe for everyone. 14 | -------------------------------------------------------------------------------- /lib/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 | --------------------------------------------------------------------------------