Repository: google/zx Branch: main Commit: 98531fcf3455 Files: 168 Total size: 1.3 MB Directory structure: gitextract_tzwf6ncx/ ├── .commitlintrc ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ └── idea.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── SECURITY.md │ ├── codeql/ │ │ └── codeql-config.yml │ ├── pages/ │ │ └── index.html │ └── workflows/ │ ├── codeql.yml │ ├── dev-publish.yml │ ├── docs.yml │ ├── jsr-publish.yml │ ├── osv.yml │ ├── publish.yml │ ├── test.yml │ └── zizmor.yml ├── .gitignore ├── .node_version ├── .nycrc ├── .prettierignore ├── .size-limit.json ├── LICENSE ├── README.md ├── build/ │ ├── 3rd-party-licenses │ ├── cli.cjs │ ├── cli.d.ts │ ├── cli.js │ ├── core.cjs │ ├── core.d.ts │ ├── core.js │ ├── deno.js │ ├── deps.cjs │ ├── deps.d.ts │ ├── error.d.ts │ ├── esblib.cjs │ ├── globals.cjs │ ├── globals.d.ts │ ├── globals.js │ ├── goods.d.ts │ ├── index.cjs │ ├── index.d.ts │ ├── index.js │ ├── internals.cjs │ ├── internals.d.ts │ ├── log.d.ts │ ├── md.d.ts │ ├── util.cjs │ ├── util.d.ts │ ├── vendor-core.cjs │ ├── vendor-core.d.ts │ ├── vendor-extra.cjs │ ├── vendor-extra.d.ts │ ├── vendor.cjs │ ├── vendor.d.ts │ └── versions.d.ts ├── dcr/ │ └── Dockerfile ├── docs/ │ ├── .gitignore │ ├── .vitepress/ │ │ ├── config.mts │ │ └── theme/ │ │ ├── MyLayout.vue │ │ ├── MyOxygen.vue │ │ ├── custom.css │ │ └── index.js │ ├── api.md │ ├── architecture.md │ ├── cli.md │ ├── configuration.md │ ├── contribution.md │ ├── faq.md │ ├── getting-started.md │ ├── index.md │ ├── known-issues.md │ ├── lite.md │ ├── markdown.md │ ├── migration-from-v7.md │ ├── process-output.md │ ├── process-promise.md │ ├── quotes.md │ ├── setup.md │ ├── shell.md │ ├── typescript.md │ └── versions.md ├── examples/ │ ├── background-process.mjs │ ├── backup-github.mjs │ ├── fetch-weather.mjs │ ├── hello.mjs │ ├── interactive.mjs │ └── parallel.mjs ├── lefthook.yml ├── man/ │ └── zx.1 ├── package.json ├── scripts/ │ ├── build-clean.mjs │ ├── build-dts.mjs │ ├── build-js.mjs │ ├── build-jsr.mjs │ ├── build-pkgjson-lite.mjs │ ├── build-pkgjson-main.mjs │ ├── build-tests.mjs │ ├── build-versions.mjs │ ├── deno.polyfill.js │ └── import-meta-url.polyfill.js ├── src/ │ ├── cli.ts │ ├── core.ts │ ├── deps.ts │ ├── error.ts │ ├── globals-jsr.ts │ ├── globals.ts │ ├── goods.ts │ ├── index.ts │ ├── internals.ts │ ├── log.ts │ ├── md.ts │ ├── repl.ts │ ├── util.ts │ ├── vendor-core.ts │ ├── vendor-extra.ts │ ├── vendor.ts │ └── versions.ts ├── test/ │ ├── all.test.js │ ├── bench/ │ │ └── buf-join.mjs │ ├── cli.test.js │ ├── core.test.js │ ├── deps.test.js │ ├── error.test.ts │ ├── export.test.js │ ├── extra.test.js │ ├── fixtures/ │ │ ├── argv.mjs │ │ ├── copyright.txt │ │ ├── echo.http │ │ ├── exit-code.mjs │ │ ├── filename-dirname.mjs │ │ ├── interactive.mjs │ │ ├── js-project/ │ │ │ ├── package.json │ │ │ └── script.js │ │ ├── markdown-crlf.md │ │ ├── markdown.md │ │ ├── md.http │ │ ├── no-extension │ │ ├── no-extension.mjs │ │ ├── non-std-ext.zx │ │ ├── require.mjs │ │ ├── server.mjs │ │ └── ts-project/ │ │ ├── package.json │ │ ├── script.ts │ │ └── tsconfig.json │ ├── global.test.js │ ├── goods.test.ts │ ├── index.test.js │ ├── it/ │ │ ├── build-dcr.test.js │ │ ├── build-jsr.test.js │ │ └── build-npm.test.js │ ├── log.test.ts │ ├── md.test.ts │ ├── smoke/ │ │ ├── bun.test.js │ │ ├── deno.test.js │ │ ├── node.test.cjs │ │ ├── node.test.mjs │ │ ├── ts.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── win32.test.js │ ├── util.test.js │ └── vendor.test.js ├── test-d/ │ ├── core.test-d.ts │ ├── globals.test-d.ts │ └── goods.test-d.ts ├── tsconfig.json └── zizmor.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .commitlintrc ================================================ { "extends": [ "@commitlint/config-conventional" ], "rules": { "type-enum": [ 2, "always", [ "build", "ci", "chore", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test" ] ], } } ================================================ FILE: .gitattributes ================================================ test/fixtures/markdown-crlf.md eol=crlf build/** linguist-language=txt ================================================ FILE: .github/FUNDING.yml ================================================ github: antonmedv ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: Bug Report description: File a bug report. title: '[Bug]: ' labels: ['bug'] assignees: - antongolub body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: textarea id: what-happened attributes: label: What happened? placeholder: Tell us what you see. value: 'A bug happened!' validations: required: true - type: textarea id: what-expected attributes: label: How it should work? description: Also tell us, what did you expect to happen? value: "Here's how it should work..." validations: required: true - type: textarea id: steps-to-reproduce attributes: label: How to reproduce the bug? description: Show an example. value: 'Step-by-step instructions to reproduce the behavior. Code snippet, gist or issue-demo repository are helpful' validations: required: true - type: input id: version attributes: label: Version description: What zx version are you running? placeholder: e.g. 0.0.0 validations: required: true - type: dropdown id: os attributes: label: What's OS kind? multiple: true options: - Linux - Mac - Windows - type: dropdown id: runtime attributes: label: What JS runtime is used? multiple: true options: - Node.js - Deno - Bun - GraalVM - type: input id: runtime-version attributes: label: Runtime Version description: What JS runtime version are you running? placeholder: e.g. 0.0.0 - type: textarea id: logs attributes: label: Error stack / relevant log output description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: shell - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/google/zx?tab=coc-ov-file). options: - label: I agree to follow this project's Code of Conduct required: true ================================================ FILE: .github/ISSUE_TEMPLATE/idea.yml ================================================ name: Feature Request description: Idea, feature request or proposal. title: '[Idea]: ' labels: ['feature'] assignees: - antonmedv body: - type: markdown attributes: value: | Thanks for sharing your vision! - type: textarea id: idea attributes: label: What's your idea? placeholder: Tell us what you'd like to add or improve. value: 'A new shiny feature!' validations: required: true - type: textarea id: why attributes: label: Why is that needed? How it may be useful? placeholder: What problem does it solve?. value: 'It will make something easier because...' validations: required: true - type: textarea id: demo attributes: label: Maybe you have a demo or example? value: 'API sketch, code snippet' render: ts validations: required: false ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Fixes #issue / suggests an improvement ```ts import {$} from 'zx' ``` - [ ] **Setup** Set the latest Node.js LTS version. - [ ] **Build**: I’ve run `npm build` before committing and verified the bundle updates correctly. - [ ] **Tests**: I’ve `run test` and confirmed all tests succeed. Added tests to cover my changes if needed. - [ ] **Docs**: I’ve added or updated relevant documentation as needed. - [ ] **Sign** Commits have [verified signatures](https://docs.github.com/en/enterprise-cloud@latest/authentication/managing-commit-signature-verification/about-commit-signature-verification) and follow [conventinal commits spec](https://www.conventionalcommits.org/en/v1.0.0/) - [ ] **CoC**: My changes follow [the project’s coding guidelines and Code of Conduct](https://github.com/google/zx?tab=coc-ov-file). - [ ] **Review**: This PR represents original work and is not solely generated by AI tools. ================================================ FILE: .github/SECURITY.md ================================================ # Security Policy ## Supported Versions | Version | Status | Comment | |---------|--------------------|-----------------------------------------------------------------------| | 8.x | :white_check_mark: | | | 7.x | :warning: | Bugs, vulnerabilities, compatibility enhancements, performance issues | | 6.x | :warning: | Critical bugs and vulnerability fixes | | < 6.0 | :x: | **No longer supported**, please consider upgrade options | ## Reporting a Vulnerability Please use https://g.co/vulnz to report security vulnerabilities. We use https://g.co/vulnz for our intake and triage. For valid issues we will do coordination and disclosure here on GitHub (including using a GitHub Security Advisory when necessary). The Google Security Team will process your report within a day, and respond within a week (although it will depend on the severity of your report). ================================================ FILE: .github/codeql/codeql-config.yml ================================================ paths: - .github - docs - examples - man - src - scripts - test - test-d paths-ignore: - build ================================================ FILE: .github/pages/index.html ================================================ Here be dragons ================================================ FILE: .github/workflows/codeql.yml ================================================ name: 'CodeQL Advanced' on: push: branches: ['main'] pull_request: branches: ['main'] schedule: - cron: '28 6 * * 3' permissions: {} jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} timeout-minutes: 60 permissions: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs packages: read # only required for workflows in private repositories actions: read contents: read strategy: fail-fast: false matrix: include: - language: javascript-typescript build-mode: none steps: - name: Checkout repository uses: actions/checkout@v6 with: persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: config-file: ./.github/codeql/codeql-config.yml languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - if: matrix.build-mode == 'manual' shell: bash run: | echo 'If you are using a "manual" build mode for one or more of the' \ 'languages you are analyzing, replace this with the commands to build' \ 'your code, for example:' echo ' make bootstrap' echo ' make release' exit 1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 with: category: '/language:${{matrix.language}}' ================================================ FILE: .github/workflows/dev-publish.yml ================================================ name: Dev Publish on: workflow_dispatch: permissions: {} env: npm_config_audit: false npm_config_fund: false jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - run: npm ci - run: npm test env: FORCE_COLOR: 3 - uses: actions/upload-artifact@v6 with: name: build-${{ github.run_id }} path: | build jsr.json package.json package-lite.json package-main.json retention-days: 1 version: runs-on: ubuntu-latest outputs: v: ${{ steps.ref.outputs.ZX_VERSION }} lite: ${{ steps.ref.outputs.ZX_VERSION }}-lite dev: ${{ steps.ref.outputs.ZX_VERSION }}-dev.${{ steps.ref.outputs.SHA_SHORT }} lite-dev: ${{ steps.ref.outputs.ZX_VERSION }}-lite-dev.${{ steps.ref.outputs.SHA_SHORT }} steps: - uses: actions/checkout@v6 with: persist-credentials: false - id: ref run: | echo SHA_SHORT=$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT echo ZX_VERSION=$(jq -r '.version' package.json) >> $GITHUB_OUTPUT npm-publish: needs: [build, version] runs-on: ubuntu-latest permissions: checks: read statuses: write contents: write packages: write id-token: write env: GOOGLE_NPM_REGISTRY: wombat-dressing-room.appspot.com GOOGLE_NPM_TOKEN: ${{ secrets.AUTH_TOKEN }} GH_NPM_REGISTRY: npm.pkg.github.com GH_NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} ZX_VERSION: ${{ needs.version.outputs.v }} ZX_DEV_VERSION: ${{ needs.version.outputs.dev }} ZX_LITE_DEV_VERSION: ${{ needs.version.outputs.lite-dev }} steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - name: Configure npmrc run: | echo "//${{ env.GOOGLE_NPM_REGISTRY }}/:_authToken=$GOOGLE_NPM_TOKEN" >> .npmrc echo "//${{ env.GH_NPM_REGISTRY }}/:_authToken=$GH_NPM_TOKEN" >> .npmrc - uses: actions/download-artifact@v7 with: name: build-${{ github.run_id }} - name: pushing lite snapshot to ${{ env.GOOGLE_NPM_REGISTRY }} run: | mv -f package-lite.json package.json cat <<< $(jq '.version="${ZX_LITE_DEV_VERSION}"' package.json) > package.json npm publish --provenance --access=public --no-git-tag-version --tag dev --registry https://${{ env.GOOGLE_NPM_REGISTRY }} - name: pushing to ${{ env.GOOGLE_NPM_REGISTRY }} run: | mv -f package-main.json package.json cat <<< $(jq '.version="${ZX_DEV_VERSION}"' package.json) > package.json npm publish --provenance --access=public --no-git-tag-version --tag dev --registry https://${{ env.GOOGLE_NPM_REGISTRY }} - name: pushing to ${{ env.GH_NPM_REGISTRY }} run: | cat <<< $(jq '.name="@${{ github.repository }}"' package.json) > package.json npm publish --no-git-tag-version --access=public --tag dev --registry https://${{ env.GH_NPM_REGISTRY }} jsr-publish: needs: [build, version] runs-on: ubuntu-latest permissions: contents: read id-token: write env: ZX_DEV_VERSION: ${{ needs.version.outputs.dev }} steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - uses: actions/download-artifact@v7 with: name: build-${{ github.run_id }} - name: pushing to jsr.io run: | cat <<< $(jq '.version="${ZX_DEV_VERSION}"' jsr.json) > jsr.json npx jsr publish --allow-dirty # https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images docker-publish: needs: [build, version] runs-on: ubuntu-latest # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. permissions: contents: read packages: write attestations: write id-token: write # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} ZX_DEV_VERSION: ${{ needs.version.outputs.dev }} steps: - name: Checkout repository uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/download-artifact@v7 with: name: build-${{ github.run_id }} # Uses the `docker/login-action` action to log in to the Container registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. - name: Log in to the Container registry uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=sha type=semver,pattern={{version}},value=v${{ env.ZX_DEV_VERSION }} # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) in the README of the `docker/build-push-action` repository. # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. - name: Build and push Docker image id: push uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0 with: context: ./ file: ./dcr/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds). - name: Generate artifact attestation uses: actions/attest-build-provenance@v3 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true ================================================ FILE: .github/workflows/docs.yml ================================================ name: Deploy docs on: workflow_dispatch: release: types: [created] concurrency: group: 'pages' cancel-in-progress: false permissions: {} env: npm_config_audit: false npm_config_fund: false npm_config_save: false npm_config_package_lock: false jobs: deploy: permissions: contents: read pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false ref: main - name: Setup Pages uses: actions/configure-pages@v5 - name: Install deps run: npm ci - name: Add additional deps run: npm i @rollup/rollup-linux-x64-gnu@4.46.4 - name: Build docs run: npm run docs:build - name: Upload artifact uses: actions/upload-pages-artifact@v4.0.0 with: path: 'docs/build' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/jsr-publish.yml ================================================ name: JSR Manual Publish on: workflow_dispatch: permissions: {} jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - run: npm ci - run: npm test env: FORCE_COLOR: 3 - uses: actions/upload-artifact@v6 with: name: build-${{ github.run_id }} path: | build jsr.json retention-days: 1 jsr-publish: needs: build runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - uses: actions/download-artifact@v7 with: name: build-${{ github.run_id }} - name: Get zx version info run: | echo SHA_SHORT=$(git rev-parse --short HEAD) >> $GITHUB_ENV echo ZX_VERSION=$(jq -r '.version' jsr.json) >> $GITHUB_ENV - name: pushing to jsr.io run: | cat <<< $(jq '.version="${ZX_VERSION}-dev.${SHA_SHORT}"' jsr.json) > jsr.json npx jsr publish --allow-dirty ================================================ FILE: .github/workflows/osv.yml ================================================ # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. # A sample workflow which sets up periodic OSV-Scanner scanning for vulnerabilities, # in addition to a PR check which fails if new vulnerabilities are introduced. # # For more examples and options, including how to ignore specific vulnerabilities, # see https://google.github.io/osv-scanner/github-action/ name: OSV-Scanner permissions: {} on: pull_request: branches: ['main'] merge_group: branches: ['main'] schedule: - cron: '45 6 * * 5' push: branches: ['main'] jobs: scan-scheduled: if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }} permissions: security-events: write contents: read actions: read uses: 'google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@e92b5d07338d4f0ba0981dffed17c48976ca4730' # v2.2.3 with: # Example of specifying custom arguments scan-args: |- -r ./ scan-pr: if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} permissions: security-events: write contents: read actions: read uses: 'google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@e92b5d07338d4f0ba0981dffed17c48976ca4730' # v2.2.3 with: # Example of specifying custom arguments scan-args: |- -r ./ ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish on: workflow_dispatch: release: types: [created] permissions: {} env: npm_config_audit: false npm_config_fund: false jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - name: Compare release tag with package.json if: github.event_name == 'release' run: | RELEASE_VERSION=${GITHUB_REF#refs/tags/} PKG_VERSION=$(node -p "require('./package.json').version") echo "Release tag: $RELEASE_VERSION" echo "package.json: $PKG_VERSION" [ "$RELEASE_VERSION" = "$PKG_VERSION" ] || { echo "❌ Mismatch"; exit 1; } - run: npm ci - run: npm test env: FORCE_COLOR: 3 - uses: actions/upload-artifact@v6 with: name: build-${{ github.run_id }} path: | build jsr.json package.json package-lite.json package-main.json retention-days: 1 version: runs-on: ubuntu-latest outputs: v: ${{ steps.ref.outputs.ZX_VERSION }} lite: ${{ steps.ref.outputs.ZX_VERSION }}-lite steps: - uses: actions/checkout@v6 with: persist-credentials: false - id: ref run: | echo SHA_SHORT=$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT echo ZX_VERSION=$(jq -r '.version' package.json) >> $GITHUB_OUTPUT npm-publish: needs: [build, version] runs-on: ubuntu-latest permissions: checks: read statuses: write contents: write packages: write id-token: write env: GOOGLE_NPM_REGISTRY: wombat-dressing-room.appspot.com GOOGLE_NPM_TOKEN: ${{ secrets.AUTH_TOKEN }} GH_NPM_REGISTRY: npm.pkg.github.com GH_NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} ZX_VERSION: ${{ needs.version.outputs.v }} steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - name: Configure npmrc run: | echo "//${{ env.GOOGLE_NPM_REGISTRY }}/:_authToken=$GOOGLE_NPM_TOKEN" >> .npmrc echo "//${{ env.GH_NPM_REGISTRY }}/:_authToken=$GH_NPM_TOKEN" >> .npmrc - uses: actions/download-artifact@v7 with: name: build-${{ github.run_id }} - name: pushing to ${{ env.GOOGLE_NPM_REGISTRY }} run: | mv -f package-main.json package.json npm publish --provenance --access=public --registry https://${{ env.GOOGLE_NPM_REGISTRY }} - name: pushing to ${{ env.GH_NPM_REGISTRY }} run: | cat <<< $(jq '.name="@${{ github.repository }}"' package.json) > package.json npm publish --no-git-tag-version --access=public --registry https://${{ env.GH_NPM_REGISTRY }} - name: pushing lite snapshot to ${{ env.GOOGLE_NPM_REGISTRY }} run: | mv -f package-lite.json package.json npm publish --provenance --access=public --no-git-tag-version --tag lite --registry https://${{ env.GOOGLE_NPM_REGISTRY }} jsr-publish: needs: build runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - uses: actions/download-artifact@v7 with: name: build-${{ github.run_id }} - name: pushing to jsr.io run: npx jsr publish --allow-dirty docker-publish: needs: [build, version] runs-on: ubuntu-latest # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} ZX_VERSION: ${{ needs.version.outputs.v }} permissions: contents: read packages: write attestations: write id-token: write steps: - name: Checkout repository uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/download-artifact@v7 with: name: build-${{ github.run_id }} - name: Log in to the Container registry uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=sha type=semver,pattern={{version}},value=v${{ env.ZX_VERSION }} - name: Build and push Docker image id: push uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0 with: context: ./ file: ./dcr/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - name: Generate artifact attestation uses: actions/attest-build-provenance@v3 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: pull_request: schedule: - cron: '0 12 */4 * *' permissions: contents: read env: npm_config_audit: false npm_config_fund: false npm_config_save: false npm_config_package_lock: false jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: persist-credentials: false - name: Use Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - run: npm ci - run: | npm run build cd build && ls -l - uses: actions/upload-artifact@v6 with: name: build path: | build jsr.json package.json package-lite.json package-main.json retention-days: 1 checks: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: persist-credentials: false fetch-depth: ${{ github.event_name == 'pull_request' && '15' || '1' }} # to ensure we have enough history for commitlint - name: Use Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - uses: actions/download-artifact@v7 with: name: build - run: npm ci - name: Format run: npm run fmt:check - name: License run: npm run test:license - name: Size run: npm run test:size - name: Dep audit run: npm run test:audit - name: Circular run: npm run test:circular - name: Bundles run: npm run test:npm timeout-minutes: 1 - name: JSR dry-run run: npm run test:jsr - name: Conventional Commits if: github.event_name == 'pull_request' env: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: npx commitlint --from "$BASE_SHA" --to "$HEAD_SHA" --verbose test: needs: build runs-on: ubuntu-latest env: FORCE_COLOR: 3 steps: - uses: actions/checkout@v6 with: persist-credentials: false - name: Use Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - uses: actions/download-artifact@v7 with: name: build - run: npm ci - name: Unit tests run: npm run test:coverage timeout-minutes: 1 - name: Type tests run: npm run test:types docker-test: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: actions/download-artifact@v7 with: name: build - run: | npm run build:dcr npm run test:dcr smoke-win32-node16: strategy: matrix: os: [windows-2022, windows-2025] name: smoke-${{ matrix.os }}-node16 runs-on: ${{ matrix.os }} needs: build steps: - uses: actions/checkout@v6 with: persist-credentials: false - name: Use Node.js 16 uses: actions/setup-node@v6 with: node-version: 16 cache: 'npm' - uses: actions/download-artifact@v7 with: name: build - run: npm run test:smoke:win32 timeout-minutes: 2 env: FORCE_COLOR: 3 smoke-bun: runs-on: ubuntu-latest needs: build steps: - uses: actions/checkout@v6 with: persist-credentials: false - name: Setup Bun uses: antongolub/action-setup-bun@f0b9f339a7ce9ba1174a58484e4dc9bbd6f7b133 # v1.13.2 - uses: actions/download-artifact@v7 with: name: build - run: | bun test ./test/smoke/bun.test.js bun ./test/smoke/ts.test.ts timeout-minutes: 1 env: FORCE_COLOR: 3 smoke-deno: runs-on: ubuntu-latest needs: build name: smoke-deno${{ matrix.deno-version }} strategy: matrix: deno-version: [1, 2] steps: - uses: actions/checkout@v6 with: persist-credentials: false - name: Setup Deno uses: denoland/setup-deno@909cc5acb0fdd60627fb858598759246509fa755 # v2.0.2 with: deno-version: ${{ matrix.deno-version }} - run: deno install npm:types/node npm:types/fs-extra - uses: actions/download-artifact@v7 with: name: build - run: deno test --allow-read --allow-sys --allow-env --allow-run ./test/smoke/deno.test.js timeout-minutes: 1 env: FORCE_COLOR: 3 smoke-node: runs-on: ubuntu-latest needs: build name: smoke-node${{ matrix.node-version }} strategy: matrix: node-version: [12, 14, 16, 18, 20, 22, 24, 25-nightly] steps: - uses: actions/checkout@v6 with: persist-credentials: false - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'npm' - uses: actions/download-artifact@v7 with: name: build - name: cjs smoke test run: npm run test:smoke:cjs - name: mjs smoke test run: npm run test:smoke:mjs - name: strip-types if: matrix.node-version >= 22 run: npm run test:smoke:strip-types smoke-graal: needs: build runs-on: ubuntu-latest name: smoke-graal${{ matrix.version }} strategy: matrix: version: [17, 20] steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: graalvm/setup-graalvm@54b4f5a65c1a84b2fdfdc2078fe43df32819e4b1 # v1.4.4 with: java-version: ${{ matrix.version }} distribution: 'graalvm-community' components: 'nodejs' github-token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/download-artifact@v7 with: name: build - name: smoke tests run: | which node node -v npm run test:smoke:cjs smoke-ts: runs-on: ubuntu-latest needs: build name: smoke-ts${{ matrix.ts }} strategy: matrix: ts: [4, 5, rc, next] steps: - uses: actions/checkout@v6 with: persist-credentials: false - name: Use Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' - name: Install deps run: npm ci - name: Install TypeScript ${{ matrix.ts }} run: npm i --force typescript@${{ matrix.ts }} - name: Override @types/node if: matrix.ts == 4 run: npm i --force @types/node@24.2.0 - uses: actions/download-artifact@v7 with: name: build - name: tsc run: npm run test:smoke:tsc - name: tsx run: npm run test:smoke:tsx - name: ts-node run: npm run test:smoke:ts-node ================================================ FILE: .github/workflows/zizmor.yml ================================================ name: Zizmor on: push: branches: ['main'] pull_request: branches: ['**'] permissions: {} jobs: zizmor: name: zizmor runs-on: ubuntu-latest permissions: contents: read actions: read steps: - name: Checkout repository uses: actions/checkout@v6 with: persist-credentials: false - name: Install the latest version of uv uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b #v7.2.0 with: enable-cache: false - name: Run zizmor run: uvx zizmor@1.22.0 .github/workflows -v -p --min-severity=medium ================================================ FILE: .gitignore ================================================ node_modules/ coverage/ package/ reports/ docs/.vitepress/cache/ yarn.lock pnpm-lock.yaml temp test/fixtures/ts-project/build/ jsr.json .npmrc package-lite.json package-main.json ================================================ FILE: .node_version ================================================ 24 ================================================ FILE: .nycrc ================================================ { "reporter": ["html", "text"], "lines": 98, "branches": "90", "statements": "98", "exclude": [ "build/deno.js", "build/vendor-extra.cjs", "build/vendor-core.cjs", "build/esblib.cjs", "test/**", "scripts", "src/util.ts", "src/core.ts", "src/index.ts", "src/vendor-extra.ts" ] } ================================================ FILE: .prettierignore ================================================ node_modules/ build/ coverage/ package/ reports/ package-lock.json yarn.lock *.md ================================================ FILE: .size-limit.json ================================================ [ { "name": "zx-lite", "path": [ "build/3rd-party-licenses", "build/core.cjs", "build/core.d.ts", "build/core.js", "build/deno.js", "build/error.d.ts", "build/esblib.cjs", "build/internals.cjs", "build/internals.d.ts", "build/log.d.ts", "build/util.cjs", "build/util.d.ts", "build/vendor-core.cjs", "build/vendor-core.d.ts", "README.md", "LICENSE" ], "limit": "128.85 kB", "brotli": false, "gzip": false }, { "name": "js parts", "path": [ "build/*.cjs", "build/cli.js", "build/core.js", "build/index.js", "build/globals.js", "build/deno.js" ], "limit": "850.20 kB", "brotli": false, "gzip": false }, { "name": "libdefs", "path": "build/*.d.ts", "limit": "44.55 kB", "brotli": false, "gzip": false }, { "name": "vendor", "path": "build/vendor-*.{cjs,d.ts}", "limit": "803.10 kB", "brotli": false, "gzip": false }, { "name": "all", "path": [ "build/3rd-party-licenses", "build/*.cjs", "build/*.d.ts", "build/cli.js", "build/core.js", "build/index.js", "build/globals.js", "build/deno.js", "man/*", "README.md", "LICENSE" ], "limit": "911.85 kB", "brotli": false, "gzip": false } ] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================

Zx logo zx

```js #!/usr/bin/env zx await $`cat package.json | grep name` const branch = await $`git branch --show-current` await $`dep deploy --branch=${branch}` await Promise.all([ $`sleep 1; echo 1`, $`sleep 2; echo 2`, $`sleep 3; echo 3`, ]) const name = 'foo bar' await $`mkdir /tmp/${name}` ``` Bash is great, but when it comes to writing more complex scripts, many people prefer a more convenient programming language. JavaScript is a perfect choice, but the Node.js standard library requires additional hassle before using. No compromise, take the best of both. The `zx` package provides useful cross-platform wrappers around `child_process`, escapes arguments and gives sensible defaults. ## Install ```bash npm install zx ``` All setup options: [zx/setup](https://google.github.io/zx/setup). See also [**zx@lite**](https://google.github.io/zx/lite). ## Usage * [Documentation at google.github.io/zx/](https://google.github.io/zx/) * [Code examples](https://github.com/google/zx/tree/main/examples) ## Compatibility * Linux, macOS, or Windows * JavaScript Runtime: * Node.js >= 12.17.0 * Bun >= 1.0.0 * Deno 1.x, 2.x * GraalVM Node.js * Some kind of [bash or PowerShell](https://google.github.io/zx/shell) * [Both CJS or ESM](https://google.github.io/zx/setup#hybrid) modules in [JS or TS](https://google.github.io/zx/typescript) ## See also - 🔥 [crow.watch](https://crow.watch) — a computing-focused community, link aggregation and discussion, [join](http://crow.watch/join/zx). ## License [Apache-2.0](LICENSE) Disclaimer: _This is not an officially supported Google product._ ================================================ FILE: build/3rd-party-licenses ================================================ THIRD PARTY LICENSES @nodelib/fs.scandir@2.1.5 https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.scandir MIT @nodelib/fs.stat@2.0.5 https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.stat MIT @nodelib/fs.walk@1.2.8 https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.walk MIT @sindresorhus/merge-streams@4.0.0 Sindre Sorhus sindresorhus/merge-streams MIT @webpod/ps@1.0.0 git://github.com/webpod/ps.git MIT braces@3.0.3 Jon Schlinkert (https://github.com/jonschlinkert) micromatch/braces MIT chalk@5.6.2 chalk/chalk MIT create-require@1.1.1 Maël Nison , Paul Soporan , Pooya Parsa nuxt-contrib/create-require MIT envapi@0.2.3 Anton Golub git+https://github.com/webpod/envapi.git MIT fast-glob@3.3.3 Denis Malinochkin mrmlnc/fast-glob MIT fastq@1.20.1 Matteo Collina git+https://github.com/mcollina/fastq.git ISC fill-range@7.1.1 Jon Schlinkert (https://github.com/jonschlinkert) jonschlinkert/fill-range MIT fs-extra@11.3.3 JP Richardson https://github.com/jprichardson/node-fs-extra MIT glob-parent@5.1.2 Gulp Team (https://gulpjs.com/) gulpjs/glob-parent ISC globby@16.1.1 Sindre Sorhus sindresorhus/globby MIT graceful-fs@4.2.11 https://github.com/isaacs/node-graceful-fs ISC is-glob@4.0.3 Jon Schlinkert (https://github.com/jonschlinkert) micromatch/is-glob MIT is-path-inside@4.0.0 Sindre Sorhus sindresorhus/is-path-inside MIT isexe@4.0.0 Isaac Z. Schlueter (http://blog.izs.me/) https://github.com/isaacs/isexe BlueOak-1.0.0 jsonfile@6.2.0 JP Richardson git@github.com:jprichardson/node-jsonfile.git MIT maml.js@0.0.3 Anton Medvedev git+https://github.com/maml-dev/maml.js.git MIT merge2@1.4.1 git@github.com:teambition/merge2.git MIT micromatch@4.0.8 Jon Schlinkert (https://github.com/jonschlinkert) micromatch/micromatch MIT node-fetch-native@1.6.7 unjs/node-fetch-native MIT picomatch@2.3.1 Jon Schlinkert (https://github.com/jonschlinkert) micromatch/picomatch MIT run-parallel@1.2.0 Feross Aboukhadijeh git://github.com/feross/run-parallel.git MIT to-regex-range@5.0.1 Jon Schlinkert (https://github.com/jonschlinkert) micromatch/to-regex-range MIT unicorn-magic@0.4.0 Sindre Sorhus sindresorhus/unicorn-magic MIT which@6.0.1 GitHub Inc. git+https://github.com/npm/node-which.git ISC yaml@2.8.2 Eemeli Aro github:eemeli/yaml ISC zurk@0.11.10 Anton Golub git+https://github.com/webpod/zurk.git MIT ================================================ FILE: build/cli.cjs ================================================ #!/usr/bin/env node "use strict"; const { __export, __toESM, __toCommonJS, __async } = require('./esblib.cjs'); const import_meta_url = typeof document === 'undefined' ? new (require('url').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src) || new URL('main.js', document.baseURI).href // src/cli.ts var cli_exports = {}; __export(cli_exports, { argv: () => argv, autorun: () => autorun, injectGlobalRequire: () => injectGlobalRequire, isMain: () => isMain, main: () => main, normalizeExt: () => normalizeExt, printUsage: () => printUsage, transformMarkdown: () => transformMarkdown }); module.exports = __toCommonJS(cli_exports); var import_node_url = __toESM(require("url"), 1); var import_node_process2 = __toESM(require("process"), 1); var import_index = require("./index.cjs"); var import_deps = require("./deps.cjs"); // src/repl.ts var import_node_process = __toESM(require("process"), 1); var import_node_repl = __toESM(require("repl"), 1); var import_node_util = require("util"); var import_core = require("./core.cjs"); var _a; var HISTORY = (_a = import_node_process.default.env.ZX_REPL_HISTORY) != null ? _a : import_core.path.join(import_core.os.homedir(), ".zx_repl_history"); function startRepl() { return __async(this, arguments, function* (history = HISTORY) { import_core.defaults.verbose = false; const r = import_node_repl.default.start({ prompt: import_core.chalk.greenBright.bold("\u276F "), useGlobal: true, preview: false, writer(output) { return output instanceof import_core.ProcessOutput ? output.toString().trimEnd() : (0, import_node_util.inspect)(output, { colors: true }); } }); r.setupHistory(history, () => { }); }); } // src/cli.ts var import_util2 = require("./util.cjs"); // src/md.ts var import_util = require("./util.cjs"); function transformMarkdown(buf) { var _a2; const out = []; const tabRe = /^( +|\t)/; const fenceRe = new RegExp("^(? {0,3})(?(`{3,20}|~{3,20}))(?:(?js|javascript|ts|typescript)|(?sh|shell|bash)|.*)$"); let state = "root"; let prevEmpty = true; let fenceChar = ""; let stripRe = null; let endRe = /^$/; let linePrefix = ""; let closeOut = ""; const isEnd = (s) => fenceChar !== "" && endRe.test(s); for (const line of (0, import_util.bufToString)(buf).split(/\r\n|[\n\r\u2028\u2029]/)) { switch (state) { case "root": { const g = (_a2 = line.match(fenceRe)) == null ? void 0 : _a2.groups; if (g == null ? void 0 : g.fence) { fenceChar = g.fence[0]; stripRe = g.indent ? new RegExp(`^ {0,${g.indent.length}}`) : null; endRe = new RegExp(`^ {0,3}${fenceChar}{${g.fence.length},}[ \\t]*$`); if (g.js) { out.push(""); linePrefix = ""; closeOut = ""; } else if (g.bash) { out.push("await $`"); linePrefix = ""; closeOut = "`"; } else { out.push(""); linePrefix = "// "; closeOut = ""; } state = "fence"; prevEmpty = false; break; } if (prevEmpty && tabRe.test(line)) { out.push(line); state = "tab"; continue; } prevEmpty = line === ""; out.push("// " + line); continue; } case "tab": if (line === "") out.push(""); else if (tabRe.test(line)) out.push(line); else { out.push("// " + line); state = "root"; } prevEmpty = line === ""; break; case "fence": if (isEnd(line)) { out.push(closeOut); state = "root"; prevEmpty = true; fenceChar = ""; } else { const s = stripRe ? line.replace(stripRe, "") : line; out.push(linePrefix + s); prevEmpty = false; } break; } } return out.join("\n"); } // src/cli.ts var import_vendor = require("./vendor.cjs"); var import_meta = {}; var EXT = ".mjs"; var EXT_RE = /^\.[mc]?[jt]sx?$/; var argv = (0, import_index.parseArgv)(import_node_process2.default.argv.slice(2), { default: (0, import_index.resolveDefaults)({ ["prefer-local"]: false }, "ZX_", import_node_process2.default.env, /* @__PURE__ */ new Set(["env", "install", "registry"])), // exclude 'prefer-local' to let minimist infer the type string: ["shell", "prefix", "postfix", "eval", "cwd", "ext", "registry", "env"], boolean: ["version", "help", "quiet", "verbose", "install", "repl", "experimental"], alias: { e: "eval", i: "install", v: "version", h: "help", l: "prefer-local", "env-file": "env" }, stopEarly: true, parseBoolean: true, camelCase: true }); autorun(import_meta); function autorun(meta) { if (meta && isMain(meta)) main().catch((err) => { if (err instanceof import_index.ProcessOutput) { console.error("Error:", err.message); } else { console.error(err); } import_node_process2.default.exitCode = 1; }); } function printUsage() { console.log(` ${import_index.chalk.bold("zx " + import_index.VERSION)} A tool for writing better scripts ${import_index.chalk.bold("Usage")} zx [options] ================================================ FILE: docs/.vitepress/theme/MyOxygen.vue ================================================ ================================================ FILE: docs/.vitepress/theme/custom.css ================================================ :root { --vp-home-hero-name-color: transparent; --vp-home-hero-name-background: -webkit-linear-gradient( 120deg, #f11a7b 10%, #feffac ); --vp-home-hero-image-background-image: linear-gradient( -45deg, rgba(241, 26, 123, 0.33) 50%, rgba(254, 255, 172, 0.33) 50% ); --vp-home-hero-image-filter: blur(40px); } @media (min-width: 640px) { :root { --vp-home-hero-image-filter: blur(56px); } } @media (min-width: 960px) { :root { --vp-home-hero-image-filter: blur(72px); } } ================================================ FILE: docs/.vitepress/theme/index.js ================================================ import DefaultTheme from 'vitepress/theme' import MyLayout from './MyLayout.vue' import './custom.css' export default { ...DefaultTheme, // override the Layout with a wrapper component that // injects the slots Layout: MyLayout, } ================================================ FILE: docs/api.md ================================================ # API Reference ## `$.sync` Zx provides both synchronous and asynchronous command executions, returns [`ProcessOutput`](./process-output) or [`ProcessPromise`](./process-promise) respectively. ```js const list = await $`ls -la` const dir = $.sync`pwd` ``` ## `$({...})` `$` object holds the default zx [configuration](./configuration), which is used for all execution. To specify a custom preset use `$` as factory: ```js const $$ = $({ verbose: false, env: {NODE_ENV: 'production'}, }) const env = await $$`node -e 'console.log(process.env.NODE_ENV)'` const pwd = $$.sync`pwd` const hello = $({quiet: true})`echo "Hello!"` ``` Moreover, presets are chainable: ```js const $1 = $({ nothrow: true }) assert.equal((await $1`exit 1`).exitCode, 1) const $2 = $1({ sync: true }) // Both {nothrow: true, sync: true} are applied assert.equal($2`exit 2`.exitCode, 2) const $3 = $({ sync: true })({ nothrow: true }) assert.equal($3`exit 3`.exitCode, 3) ``` ### `$({input})` The input option passes the specified `stdin` to the command. ```js const p1 = $({ input: 'foo' })`cat` const p2 = $({ input: Readable.from('bar') })`cat` const p3 = $({ input: Buffer.from('baz') })`cat` const p4 = $({ input: p3 })`cat` const p5 = $({ input: await p3 })`cat` ``` ### `$({signal})` The signal option makes the process abortable. ```js const {signal} = new AbortController() const p = $({ signal })`sleep 9999` setTimeout(() => signal.abort('reason'), 1000) ``` ### `$({timeout})` The timeout option makes the process autokillable after the specified delay. ```js const p = $({timeout: '1s'})`sleep 999` ``` ### `$({nothrow})` The `nothrow` option suppresses errors and returns a `ProcessOutput` with details. ```js const o1 = await $({nothrow: true})`exit 1` o1.ok // false o1.exitCode // 1 o1.message // exit code: 1 ... const o2 = await $({nothrow: true, spawn() { throw new Error('BrokenSpawn') }})`echo foo` o2.ok // false o2.exitCode // null o2.message // BrokenSpawn ... ``` The full options list: ```ts interface Options { cwd: string ac: AbortController signal: AbortSignal input: string | Buffer | Readable | ProcessOutput | ProcessPromise timeout: Duration timeoutSignal: NodeJS.Signals stdio: StdioOptions verbose: boolean sync: boolean env: NodeJS.ProcessEnv shell: string | true nothrow: boolean prefix: string postfix: string quote: typeof quote quiet: boolean detached: boolean preferLocal: boolean | string | string[] spawn: typeof spawn spawnSync: typeof spawnSync store: TSpawnStore log: typeof log kill: typeof kill killSignal: NodeJS.Signals halt: boolean delimiter: string | RegExp } ``` See also [Configuration](./configuration). ## `cd()` Changes the current working directory. ```js cd('/tmp') await $`pwd` // => /tmp ``` Like `echo`, in addition to `string` arguments, `cd` accepts and trims trailing newlines from `ProcessOutput` enabling common idioms like: ```js cd(await $`mktemp -d`) ``` > ⚠️ `cd` invokes `process.chdir()` internally, so it does affect the global context. To keep `process.cwd()` in sync with separate `$` calls enable [syncProcessCwd()](#syncprocesscwd) hook. ## `fetch()` A wrapper around the [node-fetch-native](https://www.npmjs.com/package/node-fetch-native) package. ```js const r1 = await fetch('https://example.com') const json = await r1.json() const r2 = await fetch('https://example.com', { signal: AbortSignal.timeout(5000), }) ``` For some cases, `text()` or `json()` can produce extremely large output that exceeds the string size limit. Streams are just for that, so we've attached a minor adjustment to the `fetch` API to make it more pipe friendly. ```js const p1 = fetch('https://example.com').pipe($`cat`) const p2 = fetch('https://example.com').pipe`cat` ``` ## `question()` A wrapper around the [readline](https://nodejs.org/api/readline.html) API. ```js const bear = await question('What kind of bear is best? ') const selected = await question('Select an option:', { choices: ['A', 'B', 'C'], }) ``` ## `sleep()` A wrapper around the `setTimeout` function. ```js await sleep(1000) ``` ## `echo()` A `console.log()` alternative which can take [ProcessOutput](#processoutput). ```js const branch = await $`git branch --show-current` echo`Current branch is ${branch}.` // or echo('Current branch is', branch) ``` ## `stdin()` Returns the stdin as a string. ```js const content = JSON.parse(await stdin()) ``` ## `within()` Creates a new async context. ```js await $`pwd` // => /home/path $.foo = 'bar' within(async () => { $.cwd = '/tmp' $.foo = 'baz' setTimeout(async () => { await $`pwd` // => /tmp $.foo // baz }, 1000) }) await $`pwd` // => /home/path $.foo // still 'bar' ``` ```js await $`node --version` // => v20.2.0 const version = await within(async () => { $.prefix += 'export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; nvm use 16;' return $`node --version` }) echo(version) // => v16.20.0 ``` ## `syncProcessCwd()` Keeps the `process.cwd()` in sync with the internal `$` current working directory if it is changed via [cd()](#cd). ```ts import {syncProcessCwd} from 'zx' syncProcessCwd() syncProcessCwd(false) // pass false to disable the hook ``` > This feature is disabled by default because of performance overhead. ## `retry()` Retries a callback for a few times. Will return the first successful result, or will throw after the specified attempts count. ```js const p = await retry(10, () => $`curl https://medv.io`) // With a specified delay between attempts. const p = await retry(20, '1s', () => $`curl https://medv.io`) // With an exponential backoff. const p = await retry(30, expBackoff(), () => $`curl https://medv.io`) ``` ## `spinner()` Starts a simple CLI spinner. ```js await spinner(() => $`long-running command`) // With a message. await spinner('working...', () => $`sleep 99`) ``` And it's disabled for `CI` by default. ## `glob()` The [globby](https://github.com/sindresorhus/globby) package. ```js const packages = await glob(['package.json', 'packages/*/package.json']) const markdowns = glob.sync('*.md') // sync API shortcut ``` ## `which()` The [which](https://github.com/npm/node-which) package. ```js const node = await which('node') ``` If nothrow option is used, returns null if not found. ```js const pathOrNull = await which('node', { nothrow: true }) ``` ## `ps` The [@webpod/ps](https://github.com/webpod/ps) package to provide a cross-platform way to list processes. ```js const all = await ps.lookup() const nodejs = await ps.lookup({ command: 'node' }) const children = await ps.tree({ pid: 123 }) const fulltree = await ps.tree({ pid: 123, recursive: true }) ``` ## `kill()` A process killer. ```js await kill(123) await kill(123, 'SIGKILL') ``` ## `tmpdir()` Creates a temporary directory. ```js t1 = tmpdir() // /os/based/tmp/zx-1ra1iofojgg/ t2 = tmpdir('foo') // /os/based/tmp/zx-1ra1iofojgg/foo/ ``` ## `tmpfile()` Temp file factory. ```js f1 = tmpfile() // /os/based/tmp/zx-1ra1iofojgg f2 = tmpfile('f2.txt') // /os/based/tmp/zx-1ra1iofojgg/foo.txt f3 = tmpfile('f3.txt', 'string or buffer') f4 = tmpfile('f4.sh', 'echo "foo"', 0o744) // executable ``` ## `minimist` The [minimist](https://www.npmjs.com/package/minimist) package. ```js const argv = minimist(process.argv.slice(2), {}) ``` ## `argv` A minimist-parsed version of the `process.argv` as `argv`. ```js if (argv.someFlag) { echo('yes') } ``` Use minimist options to customize the parsing: ```js const myCustomArgv = minimist(process.argv.slice(2), { boolean: [ 'force', 'help', ], alias: { h: 'help', }, }) ``` ## `chalk` The [chalk](https://www.npmjs.com/package/chalk) package. ```js console.log(chalk.blue('Hello world!')) ``` ## `fs` The [fs-extra](https://www.npmjs.com/package/fs-extra) package. ```js const {version} = await fs.readJson('./package.json') ``` ## `os` The [os](https://nodejs.org/api/os.html) package. ```js await $`cd ${os.homedir()} && mkdir example` ``` ## `path` The [path](https://nodejs.org/api/path.html) package. ```js await $`mkdir ${path.join(basedir, 'output')}` ``` ## `YAML` The [yaml](https://www.npmjs.com/package/yaml) package. ```js console.log(YAML.parse('foo: bar').foo) ``` ## `MAML` The [maml.js](https://www.npmjs.com/package/maml.js) package. ```js const maml = `{ example: "MAML" # Comments are supported notes: """ This is a multiline string. Keeps formatting as‑is. """ }` console.log(MAML.parse(maml).example) // MAML ``` ## `dotenv` The [envapi](https://www.npmjs.com/package/envapi) package. An API to interact with environment vars in [dotenv](https://www.npmjs.com/package/dotenv) format. ```js // parse const raw = 'FOO=BAR\nBAZ=QUX' const data = dotenv.parse(raw) // {FOO: 'BAR', BAZ: 'QUX'} await fs.writeFile('.env', raw) // load const env = dotenv.load('.env') await $({ env })`echo $FOO`.stdout // BAR // config dotenv.config('.env') process.env.FOO // BAR ``` ## `versions` Exports versions of the zx dependencies. ```ts import { versions } from 'zx' versions.zx // 8.7.2 versions.chalk // 5.4.1 ``` ## `quote()` Default bash quoting function. ```js quote("$FOO") // "$'$FOO'" ``` ## `quotePowerShell()` PowerShell specific quoting. ```js quotePowerShell("$FOO") // "'$FOO'" ``` ## `useBash()` Enables bash preset: sets `$.shell` to `bash` and `$.quote` to `quote`. ```js useBash() ``` ## `usePowerShell()` Switches to PowerShell. Applies the `quotePowerShell` for quoting. ```js usePowerShell() ``` ## `usePwsh()` Sets pwsh (PowerShell v7+) as `$.shell` default. ```js usePwsh() ``` ================================================ FILE: docs/architecture.md ================================================ # The zx architecture This section helps to better understand the `zx` concepts and logic, and will be useful for those who want to become a project contributor, make tools based on it, or create something similar from scratch. ## High-level modules | Module | Description | |-------------------------------------------------------------------------|---------------------------------------------------------------------| | [zurk](https://github.com/webpod/zurk) | Execution engine for spawning and managing child processes. | | [./src/core.ts](https://github.com/google/zx/blob/main/src/core.ts) | `$` factory, presets, utilities, high-level APIs. | | [./src/goods.ts](https://github.com/google/zx/blob/main/src/goods.ts) | Utilities for common tasks like fs ops, glob search, fetching, etc. | | [./src/cli.ts](https://github.com/google/zx/blob/main/src/cli.ts) | CLI interface and scripts pre-processors. | | [./src/deps.ts](https://github.com/google/zx/blob/main/src/deps.ts) | Dependency analyzing and installation. | | [./src/vendor.ts](https://github.com/google/zx/blob/main/src/vendor.ts) | Third-party libraries. | | [./src/utils.ts](https://github.com/google/zx/blob/main/src/utils.ts) | Generic helpers. | | [./src/md.ts](https://github.com/google/zx/blob/main/src/md.ts) | Markdown scripts extractor. | | [./src/error.ts](https://github.com/google/zx/blob/main/src/error.ts) | Error handling and formatting. | | [./src/global.ts](https://github.com/google/zx/blob/main/src/global.ts) | Global injectors. | ## Core design ### `Options` A set of options for `$` and `ProcessPromise` configuration. `defaults` holds the initial library preset. `Snapshot` captures the current `Options `context and attaches isolated subparts. ### `$` A piece of template literal magic. ```ts interface Shell< S = false, R = S extends true ? ProcessOutput : ProcessPromise, > { (pieces: TemplateStringsArray, ...args: any[]): R = Partial, R = O extends { sync: true } ? Shell : Shell>(opts: O): R sync: { (pieces: TemplateStringsArray, ...args: any[]): ProcessOutput (opts: Partial>): Shell } } $`cmd ${arg}` // ProcessPromise $(opts)`cmd ${arg}` // ProcessPromise $.sync`cmd ${arg}` // ProcessOutput $.sync(opts)`cmd ${arg}` // ProcessOutput ``` The `$` factory creates `ProcessPromise` instances and bounds with snapshot-context via `Proxy` and `AsyncLocalStorage`. The trick: ```ts const storage = new AsyncLocalStorage() const getStore = () => storage.getStore() || defaults function within(callback: () => R): R { return storage.run({ ...getStore() }, callback) } // Inside $ factory ... const opts = getStore() if (!Array.isArray(pieces)) { return function (this: any, ...args: any) { return within(() => Object.assign($, opts, pieces).apply(this, args)) } } ``` ### `ProcessPromise` A promise-inherited class represents and operates a child process, provides methods for piping, killing, response formatting. #### Lifecycle | Stage | Description | |--------------|------------------------| | `initial` | Blank instance | | `halted` | Awaits running | | `running` | Process in action | | `fulfilled` | Successfully completed | | `rejected` | Failed | | Gear | Description | |--------------|---------------------------------------------------------------------------------------------| | `build()` | Produces `cmd` from template and context, applies `quote` to arguments | | `run()` | Spawns the process and captures its data via `zurk` events listeners | | `finalize()` | Assigns the result to the instance: analyzes status code, invokes `_resolve()`, `_reject()` | #### Piping The remarkable part is `pipe()` and `_pipe()` interactions: the first provides a facade, the second binds different streams with acceptors. We use initialization inside static scope to comply with TS method visibility restrictions and to avoid extra `Proxy` usage: ```ts const p = $`cmd` const crits = await p.pipe.stderr`grep critical` const names = await p.pipe.stdout`grep name` ``` Another `pipe()` superpower is an internal recorder. It allows binding processes at any stage w/o data loss, even if settled. ```ts const onData = (chunk: string | Buffer) => from.write(chunk) const fill = () => { for (const chunk of source) from.write(chunk) } ee.once(source, () => { fill() // 1. Pulling previous records ee.on(source, onData) // 2. Listening for new data }).once('end', () => { ee.removeListener(source, onData) from.end() }) ``` Wayback machine in action: ```ts const p = $`cmd` await p await p.pipe`grep name` // Still works, but `p` is settled ``` ### `ProcessOutput` A class that represents the output of a `ProcessPromise`. It provides methods to access the process's stdout, stderr, exit code and extra methods for formatting the output and checking the process's success. ### `Fail` Consolidates error handling functionality across the zx library: errors codes mapping, formatting, stack parsing. ## CLI zx provides CLI with embedded script preprocessor to construct an execution context (apply presets, inject global vars) and to install the required deps. Then runs the specified script. | Helper | Description | |----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `main()` | Initializes a preset from flags, env vars and pushes the reader. | | `readScript()` | Fetches, parses and transforms the specified source into a runnable form. `stdin` reader, `https` loader and `md` transformer act right here. Deps analyzer internally relies on [depseek](https://www.npmjs.com/package/depseek) and inherits its limitations | | `runScript()` | Executes the script in the target context via async `import()`, handles temp assets after. | ## Building In the early stages of the project, we [had some difficulties](https://dev.to/antongolub/how-and-why-do-we-bundle-zx-1ca6) with zx packaging. We couldn't find a suitable tool for assembly, so we made our own toolkit based on [esbuild](https://github.com/evanw/esbuild) and [dts-bundle-generator](https://github.com/timocov/dts-bundle-generator). This process is divided into several scripts. | Script | Description | |----------------------------------------------------------------------------------------------|------------------------------------------------------------------------| | [`./scripts/build-dts.mjs`](https://github.com/google/zx/blob/main/scripts/build-dts.mjs) | Extracts and merges 3rd-party types, generates `dts` files. | | [`./scripts/build-js.mjs`](https://github.com/google/zx/blob/main/scripts/build-js.mjs) | Produces [hybrid bundles](./setup#hybrid) for each package entry point | | [`./scripts/build-jsr.mjs`](https://github.com/google/zx/blob/main/scripts/build-jsr.mjs) | Builds extra assets for [JSR](https://jsr.io/@webpod/zx) publishing | | [`./scripts/build-tests.mjs`](https://github.com/google/zx/blob/main/scripts/build-test.mjs) | Generates autotests to verify exports consistency | Corresponding tasks are defined in the `package.json.scripts`: ```json { "prebuild": "rm -rf build", "build": "npm run build:js && npm run build:dts && npm run build:tests", "build:js": "node scripts/build-js.mjs --format=cjs --hybrid --entry=src/*.ts:!src/error.ts:!src/repl.ts:!src/md.ts:!src/log.ts:!src/globals-jsr.ts:!src/goods.ts && npm run build:vendor", "build:vendor": "node scripts/build-js.mjs --format=cjs --entry=src/vendor-*.ts --bundle=all --external='./internals.ts'", "build:tests": "node scripts/build-tests.mjs", "build:dts": "tsc --project tsconfig.json && rm build/repl.d.ts build/globals-jsr.d.ts && node scripts/build-dts.mjs", "build:dcr": "docker build -f ./dcr/Dockerfile . -t zx", "build:jsr": "node scripts/build-jsr.mjs" } ``` ## Testing We understand the importance, impact and risks of the tool and invest significant efforts in comprehensive research of its quality, reliability and safety. Therefore, we use an extensive set of tools and testing scenarios. First, we inspect not how the code was written, but how it actually works. Tests mainly focus on prod bundles, `pretest` ensures they are actual. ```json { "pretest": "npm run build" } ``` A basic set of implementation correctness checks is provided by unit tests. We also evaluate coverage to ensure that areas of code are not missed. ```json { "test:unit": "node --experimental-transform-types ./test/all.test.js", "test:coverage": "c8 -c .nycrc --check-coverage npm run test:unit" } ``` Next, we control the contents of all the artifacts and examine their functionality. ```json { "test:npm": "node ./test/it/build-npm.test.js", "test:jsr": "node ./test/it/build-jsr.test.js", "test:dcr": "node ./test/it/build-dcr.test.js" } ``` Bundle code duplication issues are highlighted through size check. ```json { "test:size": "size-limit" } ``` Static analyzers are responsible for code quality. ```json { "fmt:check": "prettier --check .", "test:circular": "madge --circular src/*" } ``` Type checking examines declarations and compatibility with different loaders and transpilers. ```json { "test:types": "tsd", "test:smoke:strip-types": "node --experimental-strip-types test/smoke/ts.test.ts", "test:smoke:tsx": "tsx test/smoke/ts.test.ts", "test:smoke:tsc": "cd test/smoke && mkdir -p node_modules && ln -s ../../../ ./node_modules/zx; ../../node_modules/typescript/bin/tsc -v && ../../node_modules/typescript/bin/tsc --esModuleInterop --module node16 --rootDir . --outdir ./temp ts.test.ts && node ./temp/ts.test.js", "test:smoke:ts-node": "cd test/smoke && node --loader ts-node/esm ts.test.ts" } ``` We also check compatibility with all the target [runtimes x OS variants](https://github.com/google/zx/blob/main/.github/workflows/test.yml). ```json { "test:smoke:bun": "bun test ./test/smoke/bun.test.js && bun ./test/smoke/node.test.mjs", "test:smoke:win32": "node ./test/smoke/win32.test.js", "test:smoke:deno": "deno test ./test/smoke/deno.test.js --allow-read --allow-sys --allow-env --allow-run", } ``` CJS and EMS exports are verified separately. ```json { "test:smoke:cjs": "node ./test/smoke/node.test.cjs", "test:smoke:mjs": "node ./test/smoke/node.test.mjs" } ``` Finally, we check the license and supply chain security issues. ```json { "test:license": "node ./test/extra.test.js", "test:audit": "npm audit fix", "test:workflow": "zizmor .github/workflows -v -p --min-severity=medium" } ``` ================================================ FILE: docs/cli.md ================================================ # CLI Usage Zx provides a CLI for running scripts. It comes with the package and can be used as `zx` executable (if referenced in package.json `"scripts"`, installed [globally](/setup#install) or added to the `$PATH` somehow). ```sh zx script.mjs ``` `npx` or `node` inits are valid too. ```sh npx zx script.mjs node -r zx/globals script.mjs node --import zx/globals script.mjs ``` ## No extensions If the script does not have a file extension (like `.git/hooks/pre-commit`), zx assumes that it is an [ESM](https://nodejs.org/api/modules.html#modules_module_createrequire_filename) module unless the `--ext` option is specified. ## Non-standard extension `zx` internally loads scripts via `import` API, so you can use any extension supported by the runtime (nodejs, deno, bun) or apply a [custom loader](https://nodejs.org/api/cli.html#--experimental-loadermodule). However, if the script has a non-js-like extension (`/^\.[mc]?[jt]sx?$/`) and the `--ext` is specified, it will be used. ```bash zx script.zx # Unknown file extension "\.zx" zx --ext=mjs script.zx # OK ``` ## Markdown The CLI supports [markdown](/markdown) files and interprets `ts`, `js` and `bash` code blocks as scripts. ```bash zx docs/markdown.md ``` ## Remote scripts If the argument to the `zx` executable starts with `https://`, the file will be downloaded and executed. ```bash zx https://raw.githubusercontent.com/google/zx/refs/heads/main/examples/hello.mjs ``` > [!WARNING] Make sure you trust the remote source and understand the code before running it. ## Scripts from stdin The `zx` supports executing scripts from stdin. ```js zx << 'EOF' await $`pwd` EOF ``` ## `--eval` Evaluate the following argument as a script. ```bash cat package.json | zx --eval 'const v = JSON.parse(await stdin()).version; echo(v)' ``` ## `--repl` Starts zx in [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) mode. ## `--install` ```js // script.mjs: import sh from 'tinysh' sh.say('Hello, world!') ``` Add `--install` flag to the `zx` command to install missing dependencies automatically. ```bash zx --install script.mjs ``` You can also specify needed version by adding comment with `@` after the import. ```js import sh from 'tinysh' // @^1 ``` ## `--registry` By default, `zx` uses `https://registry.npmjs.org` as a registry. Customize if needed. ```bash zx --registry=https://registry.yarnpkg.com script.mjs ``` ## `--quiet` Suppress any outputs. ## `--verbose` Enable verbose mode. ## `--shell` Specify a custom shell binary path. By default, zx refers to `bash`. ```bash zx --shell=/bin/another/sh script.mjs ``` ## `--prefer-local, -l` Prefer locally installed packages and binaries. ```bash zx --prefer-local=/external/node_modules/or/nm-root script.mjs ``` ## `--prefix & --postfix` Attach a command to the beginning or the end of every command. ```bash zx --prefix='echo foo;' --postfix='; echo bar' script.mjs ``` ## `--cwd` Set the current working directory. ```bash zx --cwd=/foo/bar script.mjs ``` ## `--env` Specify an env file. ```bash zx --env=/path/to/some.env script.mjs ``` When `cwd` option is specified, it will be used as base path: `--cwd='/foo/bar' --env='../.env'` → `/foo/.env` ## `--ext` Overrides the default script extension (`.mjs`). ## `--version, -v` Print the current `zx` version. ## `--help, -h` Print help notes. ## Environment variables All the previously mentioned options can be set via the corresponding `ZX_`-prefixed environment variables. ```bash ZX_VERBOSE=true ZX_SHELL='/bin/bash' zx script.mjs ``` ```yaml steps: - name: Run script run: zx script.mjs env: ZX_VERBOSE: true ZX_SHELL: '/bin/bash' ``` ## `__filename & __dirname` In [ESM](https://nodejs.org/api/esm.html) modules, Node.js does not provide `__filename` and `__dirname` globals. As such globals are really handy in scripts, zx provides these for use in `.mjs` files (when using the `zx` executable). ## `require()` In [ESM](https://nodejs.org/api/modules.html#modules_module_createrequire_filename) modules, the `require()` function is not defined. The `zx` provides `require()` function, so it can be used with imports in `.mjs` files (when using `zx` executable). ```js const {version} = require('./package.json') ``` ================================================ FILE: docs/configuration.md ================================================ # Configuration ## `$.shell` Specifies what shell is used. Default is `which bash`. ```js $.shell = '/usr/bin/bash' ``` Or use a CLI argument: `--shell=/bin/bash` ## `$.spawn` Specifies a `spawn` api. Defaults to native `child_process.spawn`. To override a sync API implementation, set `$.spawnSync` correspondingly. ## `$.kill` Specifies a `kill` function. The default implements _half-graceful shutdown_ via `ps.tree()`. You can override with more sophisticated logic. ```js import treekill from 'tree-kill' $.kill = (pid, signal = 'SIGTERM') => { return new Promise((resolve, reject) => { treekill(pid, signal, (err) => { if (err) reject(err) else resolve() }) }) } ``` ## `$.prefix` Specifies the command that will be prefixed to all commands run. Default is `set -euo pipefail;`. Or use a CLI argument: `--prefix='set -e;'` ## `$.postfix` Like a `$.prefix`, but for the end of the command. ```js $.postfix = '; exit $LastExitCode' // for PowerShell compatibility ``` ## `$.preferLocal` Specifies whether to prefer `node_modules/.bin` located binaries over globally system installed ones. ```js $.preferLocal = true await $`c8 npm test` ``` You can also specify a directory to search for local binaries: ```js $.preferLocal = '/some/to/bin' $.preferLocal = ['/path/to/bin', '/another/path/bin'] ``` ## `$.quote` Specifies a function for escaping special characters during command substitution. ## `$.verbose` Specifies verbosity. Default is `false`. In verbose mode, `zx` prints all executed commands alongside with their outputs. Or use the CLI argument: `--verbose` to set `true`. ## `$.quiet` Suppresses all output. Default is `false`. Via CLI argument: `--quiet` sets `$.quiet = true`. ## `$.env` Specifies an environment variables map. Defaults to `process.env`. ## `$.cwd` Specifies a current working directory of all processes created with the `$`. The [cd()](#cd) func changes only `process.cwd()` and if no `$.cwd` specified, all `$` processes use `process.cwd()` by default (same as `spawn` behavior). ## `$.log` Specifies a [logging function](src/log.ts). ```ts import {LogEntry, log} from 'zx/core' $.log = (entry: LogEntry) => { switch (entry.kind) { case 'cmd': // for example, apply custom data masker for cmd printing process.stderr.write(masker(entry.cmd)) break default: log(entry) } } ``` The log mostly acts like a debugger, so by default it uses `process.error` for output. Override the `$.log.output` to change the stream. ```ts $.log.output = process.stdout ``` Define `$.log.formatters` to customize each log entry kind printing: ```ts $.log.formatters = { cmd: (entry: LogEntry) => `CMD: ${entry.cmd}`, fetch: (entry: LogEntry) => `FETCH: ${entry.url}` } ``` ## `$.timeout` Specifies a timeout for the command execution. ```js $.timeout = '1s' $.timeoutSignal= 'SIGKILL' await $`sleep 999` ``` ## `$.delimiter` Specifies a delimiter for splitting command output into lines. Defaults to `\r?\n` (newline or carriage return + newline). ```js $.delimiter = /\0/ // null character await $`find ./ -type f -print0 -maxdepth 1` ``` ## `$.defaults` Holds default configuration values. They will be used if the corresponding `$` options are not specified. ```ts $.defaults = { cwd: process.cwd(), env: process.env, verbose: false, quiet: false, sync: false, shell: true, prefix: 'set -euo pipefail;', // for bash postfix: '; exit $LastExitCode', // for powershell nothrow: false, stdio: 'pipe', // equivalent to ['pipe', 'pipe', 'pipe'] detached: false, preferLocal: false, spawn: childProcess.spawn, spawnSync: childProcess.spawnSync, log: $.log, kill: $.kill, killSignal: 'SIGTERM', timeoutSignal: 'SIGTERM', delimiter: /\r?\n/, } ``` ================================================ FILE: docs/contribution.md ================================================ # Contribution Guide zx is a fully [open-source project](https://github.com/google/zx), which is developing by the community for the community. We welcome contributions of any kind, including but not limited to: * Bug reports * Feature requests * Code contributions * Documentation improvements * Discussions https://google.github.io/zx/contribution ## Community Guidelines This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/). In short: all contributors are treated with respect and fairness. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## How to Contribute Before proposing changes, look for similar ones in the project's [issues](https://github.com/google/zx/issues) and [pull requests](https://github.com/google/zx/pulls). If you can't decide, create a new [discussion](https://github.com/google/zx/discussions) topic, and we will help you figure it out. Dive also into [architecture notes](/architecture) to observe design concepts. When ready to move on: * Prepare your development environment. * Switch to the recommended version of Node.js * Install manually `Node.js >= 22`. * Delegate the routine to any version manager, that [supports .node_version config](https://stackoverflow.com/questions/27425852/what-uses-respects-the-node-version-file) * Use [Volta](https://volta.sh/), the target version will be set automatically from the `package.json` * Bash is essential for running zx scripts. Linux and macOS users usually have it installed by default. Consider using [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install) or [Git Bash](https://git-scm.com/downloads) if you are on Windows. * Fork [the repository](https://github.com/google/zx). * Create a new branch. * Make your changes. * If you are adding a new feature, please include additional tests. The coverage threshold is 98%. * Create a [conventional-commits](https://www.conventionalcommits.org/en/v1.0.0/) compliant messages. * Ensure that everything is working: * `npm run fmt` to format your code. * `npm run test:coverage` to run the tests. * Push the changes to your fork. * Create a pull request. * Describe your changes in detail. * Reference any related issues if applicable. ## Code Reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ## License The project is licensed under the [Apache-2.0](https://github.com/google/zx?tab=Apache-2.0-1-ov-file#readme) ================================================ FILE: docs/faq.md ================================================ # FAQ ## Passing env variables ```js process.env.FOO = 'bar' await $`echo $FOO` ``` ## Passing array of values When passing an array of values as an argument to `$`, items of the array will be escaped individually and concatenated via space. Example: ```js const files = [...] await $`tar cz ${files}` ``` ## Importing into other scripts It is possible to make use of `$` and other functions via explicit imports: ```js #!/usr/bin/env node import {$} from 'zx' await $`date` ``` ## Attaching a profile By default `child_process` does not include aliases and bash functions. But you are still able to do it by hand. Just attach necessary directives to the `$.prefix`. ```js $.prefix += 'export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; ' await $`nvm -v` ``` ## Using GitHub Actions The default GitHub Action runner comes with `npx` installed. ```yaml jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # - uses: actions/setup-node@v4 # with: # node-version: 22 - name: Build with zx env: FORCE_COLOR: 3 run: | npx zx <<'EOF' await $`...` EOF ``` ## Verbose and Quiet zx has internal logger, which captures events if a condition is met: | Event | Verbose | Quiet | Description | |--------|---------|---------|------------------------------| | stdout | `true` | `false` | Spawned process stdout | | stderr | `any` | `false` | Process stderr data | | cmd | `true` | `false` | Command execution | | fetch | `true` | `false` | Fetch resources by http(s) | | cd | `true` | `false` | Change directory | | retry | `true` | `false` | Capture exec error | | custom | `true` | `false` | User-defined event | By default, both `$.verbose` and `$.quiet` options are `false`, so only `stderr` events are written. Any output goes to the `process.stderr` stream. You may control this flow globally or in-place ```js // Global debug mode on $.verbose = true await $`echo hello` // Suppress the particular command await $`echo fobar`.quiet() // Suppress everything $.quiet = true await $`echo world` // Turn on in-place debug await $`echo foo`.verbose() ``` You can also override the default logger with your own: ```js // globally $.log = (entry) => { switch (entry.kind) { case 'cmd': console.log('Command:', entry.cmd) break default: console.warn(entry) } } // or in-place $({log: () => {}})`echo hello` ``` ## Canary / Beta / RC builds Impatient early adopters can try the experimental zx versions. But keep in mind: these builds are ⚠️️__beta__ in every sense. ```bash npm i zx@dev npx zx@dev --install --quiet <<< 'import _ from "lodash" /* 4.17.15 */; console.log(_.VERSION)' ``` ================================================ FILE: docs/getting-started.md ================================================ # Getting Started ## Overview ```js #!/usr/bin/env zx await $`cat package.json | grep name` const branch = await $`git branch --show-current` await $`dep deploy --branch=${branch}` await Promise.all([ $`sleep 1; echo 1`, $`sleep 2; echo 2`, $`sleep 3; echo 3`, ]) const name = 'foo bar' await $`mkdir /tmp/${name}` ``` Bash is great, but when it comes to writing more complex scripts, many people prefer a more convenient programming language. JavaScript is a perfect choice, but the Node.js standard library requires additional hassle before using. The `zx` package provides useful wrappers around `child_process`, escapes arguments and gives sensible defaults. ## Install ```bash npm install zx ``` or many [other ways](/setup) ## Usage Write your scripts in a file with an `.mjs` extension in order to use `await` at the top level. If you prefer the `.js` extension, wrap your scripts in something like `void async function () {...}()`. [TypeScript](./typescript.md) is also supported. Add the following shebang to the beginning of your `zx` scripts: ```bash #!/usr/bin/env zx ``` Now you will be able to run your script like so: ```bash chmod +x ./script.mjs ./script.mjs ``` Or via the [CLI](cli.md): ```bash zx ./script.mjs ``` All functions (`$`, `cd`, `fetch`, etc) are available straight away without any imports. Or import globals explicitly (for better autocomplete in VS Code). ```js import 'zx/globals' ``` ### ``$`command` `` Executes a given command using the `spawn` func and returns [`ProcessPromise`](process-promise.md). It supports both sync and async modes. ```js const list = await $`ls -la` const dir = $.sync`pwd` ``` Everything passed through `${...}` will be automatically escaped and quoted. ```js const name = 'foo & bar' await $`mkdir ${name}` ``` **There is no need to add extra quotes.** Read more about it in [quotes](quotes.md). You can pass an array of arguments if needed: ```js const flags = [ '--oneline', '--decorate', '--color', ] await $`git log ${flags}` ``` In async mode, zx awaits any `thenable` in literal before executing the command. ```js const a1 = $`echo foo` const a2 = new Promise((resolve) => setTimeout(resolve, 20, ['bar', 'baz'])) await $`echo ${a1} ${a2}` // foo bar baz ``` If the executed program returns a non-zero exit code, [`ProcessOutput`](#processoutput) will be thrown. ```js try { await $`exit 1` } catch (p) { console.log(`Exit code: ${p.exitCode}`) console.log(`Error: ${p.stderr}`) } ``` ### `ProcessOutput` ```ts class ProcessOutput { readonly stdout: string readonly stderr: string readonly signal: string readonly exitCode: number // ... toString(): string // Combined stdout & stderr. valueOf(): string // Returns .toString().trim() } ``` The output of the process is captured as-is. Usually, programs print a new line `\n` at the end. If `ProcessOutput` is used as an argument to some other `$` process, **zx** will use stdout and trim the new line. ```js const date = await $`date` await $`echo Current date is ${date}.` ``` ## License [Apache-2.0](https://github.com/google/zx/blob/main/LICENSE) Disclaimer: _This is not an officially supported Google product._ ================================================ FILE: docs/index.md ================================================ --- # https://vitepress.dev/reference/default-theme-home-page layout: home titleTemplate: google/zx hero: name: "zx" text: "A tool for writing better scripts" image: src: /img/logo.svg alt: Zx Logo actions: - theme: brand text: Documentation link: /getting-started features: - title: Simple details: Write your scripts in a familiar language. - title: Powerful details: Interact with the full ecosystem of JS libraries. - title: Batteries included details: Everything you need, right out of the box. --- ================================================ FILE: docs/known-issues.md ================================================ # Known Issues ## Output gets truncated This is a known issue with `console.log()` (see [nodejs/node#6379](https://github.com/nodejs/node/issues/6379)). It's caused by different behaviour of `console.log()` writing to the terminal vs to a file. If a process calls `process.exit()`, buffered output will be truncated. To prevent this, the process should use `process.exitCode = 1` and wait for the process to exit itself. Or use something like [exit](https://www.npmjs.com/package/exit) package. Workaround is to write to a temp file: ```js const tmp = await $`mktemp` // Creates a temp file. const {stdout} = await $`cmd > ${tmp}; cat ${tmp}` ``` ## Colors in subprocess You may see what tools invoked with `await $` don't show colors, compared to what you see in a terminal. This is because, the subprocess does not think it's a TTY and the subprocess turns off colors. Usually there is a way force the subprocess to add colors. ```js process.env.FORCE_COLOR='1' await $`cmd` ``` ================================================ FILE: docs/lite.md ================================================ # zx@lite Just core functions without extras: * ~7x smaller than the full version * No CLI, no docs, no manpage assets embedded * Same package name, but different publish channel — `@lite` * Less code — ~~less risks~~ faster and more reliable ISEC audit * Recommended for custom toolkits based on zx ```sh npm i zx@lite npm i zx@8.5.5-lite ``` Detailed comparison: [versions](./versions) ```js import { $ } from 'zx' await $`echo foo` ``` ### Range of choice **tool size ← [`child_process`](https://nodejs.org/api/child_process.html) [`zurk`](https://github.com/webpod/zurk) `zx@lite` `zx` → built-in functionality** ================================================ FILE: docs/markdown.md ================================================ # Markdown Scripts Imagine a script with code blocks, formatted comments, schemas, illustrations, etc. [Markdown](https://en.wikipedia.org/wiki/Markdown) is right for this purpose. Combine `ts`, `js`, `bash` sections to produce a single zx scenario. For example: ````text # Some script `ls` — is an unix command to get directory contents. Let's see how to use it in `zx`: ```js // ts, js, cjs, mjs, etc const {stdout} = await $`ls -l` console.log('directory contents:', stdout) ``` This part invokes the same command in a different way: ```bash # bash syntax ls -l ``` ```` And how it looks like: > # Some script > `ls` — is an unix command to get directory contents. Let's see how to use it in `zx`: > ```js > // ts, js, cjs, mjs, etc > const {stdout} = await $`ls -l` > console.log('directory contents:', stdout) > ``` > > This part invokes the same command in a different way: > ```bash > # bash syntax > ls -l > ``` The rest is simple: just run via `zx` command: ```bash zx script.md ``` ## Hints You can use imports here as well: ```js await import('chalk') ``` `js`, `javascript`, `ts`, `typescript`, `sh`, `shell`, `bash` code blocks will be executed by zx. ```bash VAR=$(date) echo "$VAR" | wc -c ``` Other kinds are ignored: ```css body .hero { margin: 42px; } ``` The `__filename` will be pointed to **markdown.md**: ```js console.log(chalk.yellowBright(__filename)) ``` ================================================ FILE: docs/migration-from-v7.md ================================================ # Migration from v7 to v8 [v8.0.0 release](https://github.com/google/zx/releases/tag/8.0.0) brought many features, improvements, optimizations and fixes, but also has introduced a few breaking changes. Fortunately, everything can be restored and legacy v7 scripts can still run with minor configurations. 1. `$.verbose` is set to `false` by default, but errors are still printed to `stderr`. Set `$.quiet = true` to suppress any output. ```js $.verbose = true // everything works like in v7 $.quiet = true // to completely turn off logging ``` 2. `ssh` API was dropped. Install [webpod](https://github.com/webpod/webpod) package instead. ```js // import {ssh} from 'zx' ↓ import {ssh} from 'webpod' const remote = ssh('user@host') await remote`echo foo` ``` 3. zx is not looking for `PowerShell` anymore, on Windows by default. If you still need it, use the `usePowerShell` helper to enable: ```js import { usePowerShell, useBash } from 'zx' usePowerShell() // to enable powershell useBash() // switch to bash, the default ``` To look for modern [PowerShell v7+](https://github.com/google/zx/pull/790), invoke `usePwsh()` helper instead: ```js import { usePwsh } from 'zx' usePwsh() ``` 4. Process cwd synchronization between `$` invocations is now disabled by default. This functionality is provided via an async hook and can now be controlled directly. ```js import { syncProcessCwd } from 'zx' syncProcessCwd() // restores legacy v7 behavior ``` # 🚀 Keep in mind, v7 is in maintenance mode, so it will not receive any new enhancements. We encourage you to upgrade to the latest: it's [16x smaller](https://dev.to/antongolub/how-and-why-do-we-bundle-zx-1ca6), faster, safer, more reliable and useful in a [wider range of practical scenarios](https://github.com/google/zx/releases). ================================================ FILE: docs/process-output.md ================================================ # Process Output Represents a cmd execution result. ```ts const p = $`command` // ProcessPromise const o = await p // ProcessOutput ``` ```ts interface ProcessOutput extends Error { // Exit code of the process: 0 for success, non-zero for failure exitCode: number // Signal that caused the process to exit: SIGTERM, SIGKILL, etc. signal: NodeJS.Signals | null // Holds the stdout of the process stdout: string // Process errors are written to stderr stderr: string buffer(): Buffer json(): T blob(type = 'text/plain'): Blob text(encoding: Encoding = 'utf8'): string // Output lines splitted by newline lines(delimiter?: string | RegExp): string[] // combined stdout and stderr toString(): string // Same as toString() but trimmed valueOf(): string } ``` ================================================ FILE: docs/process-promise.md ================================================ # Process Promise The `$` returns a `ProcessPromise` instance, which inherits native `Promise`. When resolved, it becomes a [`ProcessOutput`](./process-output.md). ```js const p = $`command` // ProcessPromise const o = await p // ProcessOutput ``` By default, `$` spawns a new process immediately, but you can delay the start to trigger in manually. ```ts const p = $({halt: true})`command` const o = await p.run() ``` ## `stage` Shows the current process stage: `initial` | `halted` | `running` | `fulfilled` | `rejected` ```ts const p = $`echo foo` p.stage // 'running' await p p.stage // 'fulfilled' ``` ## `stdin` Returns a writable stream of the stdin process. Accessing this getter will trigger execution of a subprocess with [`stdio('pipe')`](#stdio). Do not forget to end the stream. ```js const p = $`while read; do echo $REPLY; done` p.stdin.write('Hello, World!\n') p.stdin.end() ``` By default, each process is created with stdin in _inherit_ mode. ## `stdout`/`stderr` Returns a readable streams of stdout/stderr process. ```js const p = $`npm init` for await (const chunk of p.stdout) { echo(chunk) } ``` ## `exitCode` Returns a promise which resolves to the exit code of the process. ```js if (await $`[[ -d path ]]`.exitCode == 0) { // ... } ``` ## `json(), text(), lines(), buffer(), blob()` Output formatters collection. ```js const p = $`echo 'foo\nbar'` await p.text() // foo\n\bar\n await p.text('hex') // 666f6f0a0861720a await p.buffer() // Buffer.from('foo\n\bar\n') await p.lines() // ['foo', 'bar'] // You can specify a custom lines delimiter if necessary: await $`touch foo bar baz; find ./ -type f -print0` .lines('\0') // ['./bar', './baz', './foo'] // If the output is a valid JSON, parse it in place: await $`echo '{"foo": "bar"}'` .json() // {foo: 'bar'} ``` ## `pid, cwd, cmd, fullCmd` Process metadata getters. ```js const p = $`sleep 1` p.pid // process id p.cwd // process working directory p.cmd // command: "sleep 1" p.fullCmd // full command with prefix and postfix: "set -euo pipefail;sleep 1" ``` ## `[Symbol.asyncIterator]` Returns an async iterator for the process stdout. ```js const p = $`echo "Line1\nLine2\nLine3"` for await (const line of p) { console.log(line) } // Custom delimiter can be specified: for await (const line of $({ delimiter: '\0' })`touch foo bar baz; find ./ -type f -print0`) { console.log(line) } ``` ## `pipe()` Redirects the output of the process. Almost same as `|` in bash but with enhancements. ```js const greeting = await $`printf "hello"` .pipe($`awk '{printf $1", world!"}'`) .pipe($`tr '[a-z]' '[A-Z]'`) ``` `pipe()` accepts any kind `Writable`, `ProcessPromise` or a file path. ```js await $`echo "Hello, stdout!"` .pipe(fs.createWriteStream('/tmp/output.txt')) ``` You can pass a string to `pipe()` to implicitly create a receiving file. The previous example is equivalent to: ```js await $`echo "Hello, stdout!"` .pipe('/tmp/output.txt') ``` Chained streams become _thenables_, so you can `await` them: ```js const p = $`echo "hello"` .pipe(getUpperCaseTransform()) .pipe(fs.createWriteStream(tempfile())) // <- stream const o = await p ``` And the `ProcessPromise` itself is compatible with the standard `Stream.pipe` API: ```js const { stdout } = await fs .createReadStream(await fs.writeFile(file, 'test')) .pipe(getUpperCaseTransform()) .pipe($`cat`) ``` Pipes can be used to show a real-time output of the process: ```js await $`echo 1; sleep 1; echo 2; sleep 1; echo 3;` .pipe(process.stdout) ``` And the time machine is in stock! You can pipe the process at any phase: on start, in the middle, or even after the end. All chunks will be buffered and processed in the right order. ```js const result = $`echo 1; sleep 1; echo 2; sleep 1; echo 3` const piped1 = result.pipe`cat` let piped2 setTimeout(() => { piped2 = result.pipe`cat` }, 1500) (await piped1).toString() // '1\n2\n3\n' (await piped2).toString() // '1\n2\n3\n' ``` This mechanism allows you to easily split streams to multiple consumers: ```js const p = $`some-command` const [o1, o2] = await Process.all([ p.pipe`log`, p.pipe`extract` ]) ``` Use combinations of `pipe()` and [`nothrow()`](#nothrow): ```js await $`find ./examples -type f -print0` .pipe($`xargs -0 grep ${'missing' + 'part'}`.nothrow()) .pipe($`wc -l`) ``` And literals! The `pipe()` does support them too: ```js await $`printf "hello"` .pipe`awk '{printf $1", world!"}'` .pipe`tr '[a-z]' '[A-Z]'` ``` The `pipe()` allows not only chain or split stream, but also to merge them. ```js const $h = $({ halt: true }) const p1 = $`echo foo` const p2 = $h`echo a && sleep 0.1 && echo c && sleep 0.2 && echo e` const p3 = $h`sleep 0.05 && echo b && sleep 0.1 && echo d` const p4 = $`sleep 0.4 && echo bar` const p5 = $h`cat` await p1 p1.pipe(p5) p2.pipe(p5) p3.pipe(p5) p4.pipe(p5) const { stdout } = await p5.run() // 'foo\na\nb\nc\nd\ne\nbar\n' ``` By default, `pipe()` operates with `stdout` stream, but you can specify `stderr` as well: ```js const p = $`echo foo >&2; echo bar` const o1 = (await p.pipe.stderr`cat`).toString() // 'foo\n' const o2 = (await p.pipe.stdout`cat`).toString() // 'bar\n' ``` The [signal](/api#signal) option, if specified, will be transmitted through the pipeline. ```js const ac = new AbortController() const { signal } = ac const p = $({ signal, nothrow: true })`echo test`.pipe`sleep 999` setTimeout(() => ac.abort(), 50) try { await p } catch ({ message }) { message // The operation was aborted } ``` In short, combine anything you want: ```js const getUpperCaseTransform = () => new Transform({ transform(chunk, encoding, callback) { callback(null, String(chunk).toUpperCase()) }, }) // $ > stream (promisified) > $ const o1 = await $`echo "hello"` .pipe(getUpperCaseTransform()) .pipe($`cat`) o1.stdout // 'HELLO\n' // stream > $ const file = tempfile() await fs.writeFile(file, 'test') const o2 = await fs .createReadStream(file) .pipe(getUpperCaseTransform()) .pipe($`cat`) o2.stdout // 'TEST' ``` ## `unpipe()` Opposite of `pipe()`, it removes the process from the pipeline. ```js const p1 = $`echo foo && sleep 0.05 && echo bar && sleep 0.05 && echo baz && sleep 0.05 && echo qux` const p2 = $`echo 1 && sleep 0.05 && echo 2 && sleep 0.05 && echo 3` const p3 = $`cat` p1.pipe(p3) p2.pipe(p3) setTimeout(() => p1.unpipe(p3), 105) assert.equal((await p1).stdout, 'foo\nbar\nbaz\nqux') assert.equal((await p2).stdout, '1\n2\n3') assert.equal((await p3).stdout, 'foo\n1\nbar\n2\n3') ``` ## `kill()` Kills the process and all children. By default, signal `SIGTERM` is sent. You can specify a signal via an argument. ```js const p = $`sleep 999` setTimeout(() => p.kill('SIGINT'), 100) await p ``` Killing the expired process raises an error: ```js const p = await $`sleep 999` p.kill() // Error: Too late to kill the process. ``` ## `abort()` Terminates the process via an `AbortController` signal. ```js const ac = new AbortController() const {signal} = ac const p = $({signal})`sleep 999` setTimeout(() => ac.abort('reason'), 100) await p ``` If `ac` or `signal` is not provided, it will be autocreated and could be used to control external processes. ```js const p = $`sleep 999` const {signal} = p const res = fetch('https://example.com', {signal}) p.abort('reason') ``` The process may be aborted while executing, the method raises an error otherwise: ```js const p = $({nothrow: true})`sleep 999` p.abort() // ok await p p.abort() // Error: Too late to abort the process. ``` ## `stdio()` Specifies a standard input-output for the process. ```js const h$ = $({halt: true}) const p1 = h$`read`.stdio('inherit', 'pipe', null).run() const p2 = h$`read`.stdio('pipe').run() // sets ['pipe', 'pipe', 'pipe'] ``` Keep in mind, `stdio` should be set before the process is started, so the preset syntax might be preferable: ```js await $({stdio: ['pipe', 'pipe', 'pipe']})`read` ``` ## `nothrow()` Changes behavior of `$` to not throw an exception on non-zero exit codes. Equivalent to [`$({nothrow: true})` option](./api#nothrow). ```js await $`grep something from-file`.nothrow() // Inside a pipe(): await $`find ./examples -type f -print0` .pipe($`xargs -0 grep something`.nothrow()) .pipe($`wc -l`) // Accepts a flag to switch nothrow mode for the specific command $.nothrow = true await $`echo foo`.nothrow(false) ``` If only the `exitCode` is needed, you can use [`exitCode`](#exitcode) directly: ```js if (await $`[[ -d path ]]`.exitCode == 0) { //... } // Equivalent of: if ((await $`[[ -d path ]]`.nothrow()).exitCode == 0) { //... } ``` ## `quiet()` Changes behavior of `$` to enable suppress mode. ```js // Command output will not be displayed. await $`grep something from-file`.quiet() $.quiet = true await $`echo foo`.quiet(false) // Disable for the specific command ``` ## `verbose()` Enables verbose output. Pass `false` to disable. ```js await $`grep something from-file`.verbose() $.verbose = true await $`echo foo`.verbose(false) // Turn off verbose mode once ``` ## `timeout()` Kills the process after a specified period. ```js await $`sleep 999`.timeout('5s') // Or with a specific signal. await $`sleep 999`.timeout('5s', 'SIGKILL') ``` If the process is already settled, the method does nothing. Passing nullish value will disable the timeout. ================================================ FILE: docs/quotes.md ================================================ # Quotes Bash supports various ways to quote arguments: single quotes, double quotes, and a bash-specific method using C-style quotes `$'...'`. Zx prefers the latter approach. ```js const name = 'foo & bar' await $`mkdir ${name}` ``` > [!WARNING] > Zx automatically escapes and quotes anything within `${...}`, so there's no need for additional quotes. Moreover, this may result in an **unsafe injection**. > ```ts > const args = ['param && echo bar'] > const p = $`echo --foo=$'${args}'` > (await p).stdout // '--foo=$param\nbar\n' > ``` The following examples produce the same, correct result: ```js await $`mkdir ${'path/to-dir/' + name}` ``` ```js await $`mkdir path/to-dir/${name}` ``` Keep in mind, that `PowerShell` or `pwsh` requires a corresponding quote implementation. Define it [via helpers](./setup#bash) or manually: ```js import { quotePowerShell } from 'zx' $.quote = quotePowerShell ``` ## Array of arguments Zx can also accept an array of arguments within `${...}`. Each array item will be quoted separately and then joined by a space. ```js const flags = [ '--oneline', '--decorate', '--color', ] await $`git log ${flags}` ``` ## Glob patterns Because Zx escapes everything inside `${...}`, you can't use glob syntax directly. Instead, Zx provides a [`glob`](api.md#glob) function. The following example won't work: ```js const files = './**/*.md' // [!code error] // Incorrect await $`ls ${files}` ``` The correct approach: ```js const files = await glob('./**/*.md') await $`ls ${files}` ``` ## Home dir `~` Zx won't expand the home directory symbol `~` if it's within `${...}`. Use `os.homedir()` for that purpose. ```js const dir = `~/Downloads` // [!code error] // Incorrect await $`ls ${dir}` ``` ```js await $`ls ${os.homedir()}/Downloads` // Correct ``` ```js await $`ls ~/Downloads` // Correct, ~ is outside of ${...} ``` ## Assembling commands If you're trying to dynamically assemble commands in Zx, you might run into limitations. For instance, the following approach won't work: ```js const cmd = 'rm' if (force) cmd += ' -f' if (recursive) cmd += ' -r' cmd += ' ' + file await $`${cmd}` // [!code error] // Incorrect ``` Zx will escape the entire string, making the command invalid. Instead, assemble an array of arguments and pass it to Zx like this: ```js const args = [] if (force) args.push('-f') if (recursive) args.push('-r') args.push(file) await $`rm ${args}` // [!code hl] ``` ================================================ FILE: docs/setup.md ================================================ # Setup ## Requirements * Linux, macOS, or Windows * JavaScript Runtime: * Node.js >= 12.17.0 * Bun >= 1.0.0 * Deno 1.x, 2.x * GraalVM Node.js * Some kind of bash or PowerShell ## Install ::: code-group ```bash [npm] npm install zx # add -g to install globally ``` ```bash [npx] npx zx script.js # run script without installing the zx package npx zx@8.6.0 script.js # pin to a specific zx version ``` ```bash [yarn] yarn add zx ``` ```bash [pnpm] pnpm add zx ``` ```bash [bun] bun install zx ``` ```bash [deno] deno install -A npm:zx # zx requires additional permissions: --allow-read --allow-sys --allow-env --allow-run ``` ```bash [jsr] npx jsr add @webpod/zx deno add jsr:@webpod/zx # https://jsr.io/docs/using-packages ``` ```bash [docker] docker pull ghcr.io/google/zx:8.5.0 docker run -t ghcr.io/google/zx:8.5.0 -e="await \$({verbose: true})\`echo foo\`" docker run -t -i -v ./:/script ghcr.io/google/zx:8.5.0 script/t.js ``` ```bash [brew] brew install zx ``` ::: ### Channels zx is distributed in several versions, each with its own set of features. | Channel | Description | Install | |----------|----------------------------------------------------------------------------------------------|----------------------| | `latest` | Mainline releases with the latest features and improvements. | `npm i zx` | | `lite` | [A minimalistic version of zx](./lite), suitable for lightweight scripts. | `npm i zx@lite` | | `dev` | Development snapshots with the latest changes, may be unstable. | `npm i zx@dev` | | `legacy` | Legacy supporting versions for compatibility with older scripts, no new features, only bugfixes | `npm i zx@` | Detailed comparison: [versions](./versions). Please check the download sources carefully. Official links: * [npmjs](https://www.npmjs.com/package/zx) * [GH npm](https://github.com/google/zx/pkgs/npm/zx) * [GH repo](https://github.com/google/zx) * [GH docker](https://github.com/google/zx/pkgs/container/zx) * [JSR](https://jsr.io/@webpod/zx) * [Homebrew](https://github.com/Homebrew/homebrew-core/blob/master/Formula/z/zx.rb) ### Github To fetch zx directly from the GitHub: ```bash # Install via git npm i google/zx npm i git@github.com:google/zx.git # Fetch from the GH pkg registry npm i --registry=https://npm.pkg.github.com @google/zx ``` ### Docker If you'd prefer to run scripts in a container, you can pull the zx image from the [ghcr.io](https://ghcr.io). [node:24-alpine](https://hub.docker.com/_/node) is used as [a base](https://github.com/google/zx/blob/main/dcr/Dockerfile). ```shell docker pull ghcr.io/google/zx:8.5.0 docker run -t ghcr.io/google/zx:8.5.0 -e="await \$({verbose: true})\`echo foo\`" docker run -t -i -v ./:/script ghcr.io/google/zx:8.5.0 script/t.js ``` ## Bash zx mostly relies on bash, so make sure it's available in your environment. If you're on Windows, consider using [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install) or [Git Bash](https://git-scm.com/downloads). By default, zx looks for bash binary, but you can switch to PowerShell by invoking `usePowerShell()` or `usePwsh()`. ```js import { useBash, usePowerShell, usePwsh } from 'zx' usePowerShell() // Use PowerShell.exe usePwsh() // Rely on pwsh binary (PowerShell v7+) useBash() // Switch back to bash ``` ## Package ### Hybrid zx is distributed as a [hybrid package](https://2ality.com/2019/10/hybrid-npm-packages.html): it provides both CJS and ESM entry points. ```js import { $ } from 'zx' const { $ } = require('zx') ``` It also contains built-in TypeScript libdefs. But `@types/fs-extra` and `@types/node` are required to be installed on user's side. ```bash npm i -D @types/fs-extra @types/node ``` ```ts import { type Options } from 'zx' const opts: Options = { quiet: true, timeout: '5s' } ``` ### Bundled We use [esbuild](https://dev.to/antongolub/how-and-why-do-we-bundle-zx-1ca6) to produce a static build that allows us to solve several issues at once: * Reduce the pkg size and install time. * Make npx (yarn dlx / bunx) invocations reproducible. * Provide support for a wide range of Node.js versions: from [12 to 25](https://github.com/google/zx/blob/61d03329349770d90fda3c9e26f7ef09f869a096/.github/workflows/test.yml#L195). * Make auditing easier: complete code is in one place. ### Composite zx exports several entry points adapted for different use cases: * `zx` – the main entry point, provides all the features. * `zx/global` – to populate the global scope with zx functions. * `zx/cli` – to run zx scripts from the command line. * `zx/core` – to use zx template spawner as part of 3rd party libraries with alternating set of utilities. ### Typed The library is written in TypeScript 5 and provides comprehensive type definitions for TS users. * Libdefs are bundled via [dts-bundle-generator](https://github.com/timocov/dts-bundle-generator). * Compatible with TS 4.0 and later. * Requires `@types/node` and `@types/fs-extra` to be installed. ================================================ FILE: docs/shell.md ================================================ # Shell [Bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) is a fundamental part of the Unix ecosystem, and it is widely used for scripting and automation tasks. It provides a powerful set of built-in utils, operators, process controllers. Bash gives an efficient way to fine-tune the behavior: cmd aliases, context presets, custom functions, env injections, and more. zx is not trying to replace bash, but to enhance it with JavaScript's capabilities: * Parallel execution * Data transformations * Exception handling * Conditional logic and loops ```js #!/usr/bin/env zx import { $ } from 'zx' $.nothrow = true const repos = ['zx', 'webpod'] const clones = repos .map(n => $`git clone https://github.com/google/${n} ${n}-clone`) const results = await Promise.all(clones) const errors = results.filter(o => !o.ok).map(o => o.stderr.trim()) console.log('errors', errors.join('\n')) for (p of clones) { await p.pipe`cat > ${p.pid}.txt` } ``` ## Bash and Pwsh There're many shell implementations. zx brings a few setup helpers: * [`useBash`](./api#usebash) switches to bash * [`usePowerShell`](./api#usepowershell) — PowerShell * [`usePwsh`](./api#usepwsh) — pwsh (PowerShell v7+) You can also set the shell directly via [JS API](./setup#bash), [CLI flags](./cli#shell) or [envars](./cli#environment-variables): ```js $.shell = '/bin/zsh' ``` ```bash zx --shell /bin/zsh script.js ``` ```bash ZX_SHELL=/bin/zsh zx script.js ``` ## zx = bash + js No compromise, take the best of both. ================================================ FILE: docs/typescript.md ================================================ # TypeScript zx is written in TypeScript and provides the corresponding libdefs out of the box. Types are TS 4+ compatible. Write code in any suitable format `.ts`, `.mts`, `.cts` or add [a custom loader](./cli#non-standard-extension). ```ts // script.ts import { $ } from 'zx' const list = await $`ls -la` ``` Some runtimes like [Bun](https://bun.sh/) or [Deno](https://deno.com/) have built-in TS support. Node.js requires additional setup. Configure your project according to the [ES modules contract](https://nodejs.org/api/packages.html#packages_type): - Set [`"type": "module"`](https://nodejs.org/api/packages.html#packages_type) in **package.json** - Set [`"module": "ESNext"`](https://www.typescriptlang.org/tsconfig/#module) in **tsconfig.json**. Using TypeScript compiler is the most straightforward way, but native TS support from runtimes is gradually increasing. ::: code-group ```bash [node] # Since Node.js v22.6.0 node --experimental-strip-types script.js ``` ```bash [npx] # Since Node.js v22.6.0 NODE_OPTIONS="--experimental-strip-types" zx script.js ``` ```bash [tsc] npm install typescript tsc script.ts node script.js ``` ```bash [ts-node] npm install ts-node ts-node script.ts # or via node loader node --loader ts-node/esm script.ts ``` ```bash [swc-node] npm install swc-node swc-node script.ts ``` ```bash [tsx] npm install tsx tsx script.ts # or node --import=tsx script.ts ``` ```bash [bun] bun script.ts ``` ```bash [deno] deno run --allow-read --allow-sys --allow-env --allow-run script.ts ``` ::: ================================================ FILE: docs/versions.md ================================================ # Versions zx is distributed in several versions, each with its own set of features. * `@latest` represents the stable full-featured version. * `@lite` separates the zx core from the extensions. * `@dev` brings experimental snapshots and RCs. | Feature | latest | lite | |-------------------|--------|------| | **zx/globals** | ✔️ | ️ | | **zx/cli** | ✔️ | | | `$` | ✔️ | ✔️ | | `ProcessPromise` | ✔️ | ✔️ | | `ProcessOutput` | ✔️ | ✔️ | | `argv` | ✔️ | ️ | | `cd` | ✔️ | ✔️ | | `chalk` | ✔️ | ✔️ | | `defaults` | ✔️ | ✔️ | | `dotenv` | ✔️ | ️ | | `echo` | ✔️ | ️ | | `expBackoff` | ✔️ | ️ | | `fetch` | ✔️ | ️ | | `fs` | ✔️ | ️ | | `glob` | ✔️ | ️ | | `kill` | ✔️ | ✔️ | | `log` | ✔️ | ✔️ | | `minimist` | ✔️ | ️ | | `nothrow` | ✔️ | ️ | | `os` | ✔️ | ✔️ | | `parseArgv` | ✔️ | ️ | | `path` | ✔️ | ✔️ | | `ps` | ✔️ | ✔️ | | `question` | ✔️ | ️ | | `quiet` | ✔️ | ️ | | `quote` | ✔️ | ✔️ | | `quotePowerShell` | ✔️ | ✔️ | | `resolveDefaults` | ✔️ | ✔️ | | `retry` | ✔️ | ️ | | `sleep` | ✔️ | ️ | | `spinner` | ✔️ | ️ | | `syncProcessCwd` | ✔️ | ✔️ | | `tempdir` | ✔️ | | | `tempfile` | ✔️ | | | `updateArgv` | ✔️ | | | `useBash` | ✔️ | ✔️ | | `usePowerShell` | ✔️ | ✔️ | | `usePwsh` | ✔️ | ✔️ | | `version` | ✔️ | ️ | | `which` | ✔️ | ✔️ | | `within` | ✔️ | ✔️ | | `YAML` | ✔️ | ️ | | `MAML` | ✔️ | ️ | ================================================ FILE: examples/background-process.mjs ================================================ #!/usr/bin/env zx // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const serve = $`npx serve` for await (const chunk of serve.stdout) { if (chunk.includes('Accepting connections')) break } await $`curl http://localhost:3000` serve.kill('SIGINT') ================================================ FILE: examples/backup-github.mjs ================================================ #!/usr/bin/env zx // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const username = await question('What is your GitHub username? ') const token = await question('Do you have GitHub token in env? ', { choices: Object.keys(process.env), }) let headers = {} if (process.env[token]) { headers = { Authorization: `token ${process.env[token]}`, } } let res = await fetch( `https://api.github.com/users/${username}/repos?per_page=1000`, { headers } ) const data = await res.json() const urls = data.map((x) => x.git_url.replace('git://github.com/', 'git@github.com:') ) await $`mkdir -p backups` cd('./backups') for (const url of urls) { await $`git clone ${url}` } ================================================ FILE: examples/fetch-weather.mjs ================================================ #!/usr/bin/env zx // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. async function main() { const argv = minimist(process.argv.slice(2), { boolean: ['help'], alias: { h: 'help' }, }) if (argv.help) { echo(` ${chalk.bold('Usage:')} zx fetch-weather.mjs [city name] Fetches weather data using wttr.in with a neat two-column colored table format. ${chalk.bold('Examples:')} zx fetch-weather.mjs London ./fetch-weather.mjs "New York" `) process.exit(0) } const args = argv._.slice(__filename === process.argv[1] ? 0 : 1) const city = args.join(' ') if (!city) throw 'No city provided. Use -h for help.' const svc_url = 'https://wttr.in' const data = await spinner( `📡 Fetching weather for "${city}" from ${svc_url}...`, async () => { try { const res = await fetch( `${svc_url}/${encodeURIComponent(city)}?format=j1`, { signal: AbortSignal.timeout(5000), } ) if (!res.ok) throw `API error: ${res.status} ${res.statusText}` return res.json() } catch (err) { if (err.name === 'AbortError') { throw 'Request timed out after 5 seconds.' } throw err } } ) const area = data.nearest_area[0] const current = data.current_condition[0] if (!area || !current) { throw '❌ Missing weather data in API response.' } const location = area.areaName[0].value const condition = current.weatherDesc[0].value const temperature = current.temp_C const humidity = current.humidity echo(chalk.yellow(`🌤️ Weather in ${location}: ${condition}`)) echo(chalk.red(`🌡️ Temperature: ${temperature}°C`)) echo(chalk.blue(`💧 Humidity: ${humidity}%`)) } await main().then( () => process.exit(0), (err) => { const msg = typeof err === 'string' ? err : err.message echo(chalk.red(`❌ ${msg}`)) process.exit(1) } ) // Here's how to add this script to your shell as a bash alias. This assumes you have zx installed globally. // 1. Save this script as `fetch-weather.mjs`. // 2. Add the following line to your .bashrc file, replacing the path with your own: // alias weather='zx /full/path/to/fetch-weather.mjs' // 3. Then reload your shell using the following command: // source ~/.bashrc // Now you can use the `weather` command to fetch weather data for any city. // Example usage: `weather London` ================================================ FILE: examples/hello.mjs ================================================ #!/usr/bin/env zx // Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. await $({ verbose: true })`echo "Hello!"` ================================================ FILE: examples/interactive.mjs ================================================ #!/usr/bin/env zx // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const p = $`npm init`.stdio('pipe') for await (const chunk of p.stdout) { if (chunk.includes('package name:')) p.stdin.write('test\n') if (chunk.includes('version:')) p.stdin.write('1.0.0\n') if (chunk.includes('description:')) p.stdin.write('My test package\n') if (chunk.includes('entry point:')) p.stdin.write('index.mjs\n') if (chunk.includes('test command:')) p.stdin.write('test.mjs\n') if (chunk.includes('git repository:')) p.stdin.write('my-org/repo\n') if (chunk.includes('keywords:')) p.stdin.write('foo, bar\n') if (chunk.includes('author:')) p.stdin.write('Anton Medvedev\n') if (chunk.includes('license:')) p.stdin.write('MIT\n') if (chunk.includes('Is this OK?')) p.stdin.write('yes\n') } ================================================ FILE: examples/parallel.mjs ================================================ #!/usr/bin/env zx // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { spinner } from 'zx' const tests = await glob('test/*.test.js') await spinner('running tests', async () => { try { const res = await Promise.all(tests.map((file) => $`npx uvu . ${file}`)) res.forEach((r) => console.log(r.toString())) console.log(chalk.bgGreen.black(' SUCCESS ')) } catch (e) { console.log(e.toString()) process.exitCode = 1 } }) ================================================ FILE: lefthook.yml ================================================ pre-commit: parallel: true commands: format: glob: '*.{js,ts,md,yml,yaml}' run: npm run fmt && git add {staged_files} commit-msg: commands: lint-commit-msg: run: npx commitlint --edit pre-push: parallel: true commands: license: run: npm run test:license size: run: npm run test:size circular: run: npm run test:circular ================================================ FILE: man/zx.1 ================================================ .\" Manpage for zx. .TH man 8 "06 Jul 2024" "8.x" "zx man page" .SH NAME zx \- the zx CLI .SH DESCRIPTION A tool for writing better scripts. .SH SYNOPSIS .SS zx\fR [\fIOPTIONS\fR] \fIURI\fR .SH OPTIONS .SS --cwd set current directory .SS --quiet suppress any outputs .SS --verbose enables verbose mode .SS --shell= set the shell to use .SS --prefix= prefix all commands .SS --postfix= postfix all commands .SS --prefer-local, -l prefer locally installed packages and binaries .SS --eval=, -e evaluate script .SS --ext=<.mjs> script extension .SS --install, -i install dependencies .SS --registry= npm registry, defaults to https://registry.npmjs.org/ .SS --repl start repl .SS --env= path to env file .SS --version, -v print current zx version .SS --help, -h print command help and options .SH EXAMPLES .TP .I zx --verbose script.js .TP .I zx https://example.com/script.js .TP .I zx -e '$`ls -l`' .SH BUGS https://github.com/google/zx/issues. .SH AUTHOR Anton Medvedev (https://medv.io/) ================================================ FILE: package.json ================================================ { "name": "zx", "version": "8.9.0", "description": "A tool for writing better scripts", "type": "module", "main": "./build/index.cjs", "types": "./build/index.d.ts", "typesVersions": { "*": { ".": [ "./build/index.d.ts" ], "globals": [ "./build/globals.d.ts" ], "cli": [ "./build/cli.d.ts" ], "core": [ "./build/core.d.ts" ] } }, "exports": { ".": { "types": "./build/index.d.ts", "import": "./build/index.js", "require": "./build/index.cjs", "default": "./build/index.js" }, "./globals": { "types": "./build/globals.d.ts", "import": "./build/globals.js", "require": "./build/globals.cjs", "default": "./build/globals.js" }, "./cli": { "types": "./build/cli.d.ts", "import": "./build/cli.js", "require": "./build/cli.cjs", "default": "./build/cli.js" }, "./core": { "types": "./build/core.d.ts", "import": "./build/core.js", "require": "./build/core.cjs", "default": "./build/core.js" }, "./package.json": "./package.json" }, "bin": { "zx": "build/cli.js" }, "man": "./man/zx.1", "files": [ "build/3rd-party-licenses", "build/cli.js", "build/core.js", "build/deno.js", "build/globals.js", "build/index.js", "build/*.cjs", "build/*.d.ts", "man" ], "engines": { "node": ">= 12.17.0" }, "scripts": { "fmt": "prettier --write .", "fmt:check": "prettier --check .", "prebuild": "rm -rf build", "build": "npm run build:versions && npm run build:js && npm run build:dts && npm run build:tests", "build:js": "node scripts/build-js.mjs --format=cjs --hybrid --entry='src/{cli,core,deps,globals,index,internals,util,vendor*}.ts' && npm run build:vendor", "build:vendor": "node scripts/build-js.mjs --format=cjs --entry=src/vendor-*.ts --bundle=all --external='./internals.ts'", "build:versions": "node scripts/build-versions.mjs", "build:tests": "node scripts/build-tests.mjs", "build:dts": "tsc --project tsconfig.json && node scripts/build-dts.mjs", "build:dcr": "docker build -f ./dcr/Dockerfile . -t zx", "build:jsr": "node scripts/build-jsr.mjs", "build:lite": "node scripts/build-pkgjson-lite.mjs", "build:pkgjson": "node scripts/build-pkgjson-main.mjs", "build:manifest": "npm run build:pkgjson && npm run build:lite && npm run build:jsr", "postbuild": "node scripts/build-clean.mjs && npm run build:manifest", "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", "pretest": "npm run build", "test": "npm run test:size && npm run fmt:check && npm run test:unit && npm run test:types && npm run test:license", "test:npm": "node ./test/it/build-npm.test.js", "test:jsr": "node ./test/it/build-jsr.test.js", "test:dcr": "node ./test/it/build-dcr.test.js", "test:unit": "node --experimental-transform-types ./test/all.test.js", "test:coverage": "c8 -c .nycrc --check-coverage npm run test:unit", "test:circular": "madge --circular src/*", "test:types": "tsd", "test:license": "node ./test/extra.test.js", "test:audit": "npm audit --package-lock", "test:size": "size-limit", "test:smoke:strip-types": "node --experimental-strip-types test/smoke/ts.test.ts", "test:smoke:tsx": "tsx test/smoke/ts.test.ts", "test:smoke:tsc": "cd test/smoke && mkdir -p node_modules && ln -s ../../../ ./node_modules/zx; ../../node_modules/typescript/bin/tsc -v && ../../node_modules/typescript/bin/tsc --project tsconfig.test.json && node ./temp/ts.test.js", "test:smoke:ts-node": "cd test/smoke && node --loader ts-node/esm ts.test.ts", "test:smoke:bun": "bun test ./test/smoke/bun.test.js && bun ./test/smoke/node.test.mjs", "test:smoke:win32": "node ./test/smoke/win32.test.js", "test:smoke:cjs": "node ./test/smoke/node.test.cjs", "test:smoke:mjs": "node ./test/smoke/node.test.mjs", "test:smoke:deno": "deno test ./test/smoke/deno.test.js --allow-read --allow-sys --allow-env --allow-run", "test:workflow": "zizmor .github/workflows -v -p --min-severity=medium" }, "devDependencies": { "@commitlint/cli": "^20.4.2", "@commitlint/config-conventional": "^20.4.2", "@size-limit/file": "12.0.0", "@types/fs-extra": "11.0.4", "@types/minimist": "1.2.5", "@types/node": "25.3.2", "@types/which": "3.0.4", "@webpod/ingrid": "1.1.1", "@webpod/ps": "1.0.0", "c8": "11.0.0", "chalk": "5.6.2", "create-require": "1.1.1", "cronometro": "6.0.3", "depseek": "0.4.3", "dts-bundle-generator": "9.5.1", "envapi": "0.2.3", "esbuild": "0.27.3", "esbuild-node-externals": "1.20.1", "esbuild-plugin-entry-chunks": "0.1.17", "esbuild-plugin-extract-helpers": "0.0.6", "esbuild-plugin-hybrid-export": "0.3.1", "esbuild-plugin-resolve": "2.0.0", "esbuild-plugin-transform-hook": "0.2.0", "esbuild-plugin-utils": "0.1.0", "fs-extra": "11.3.3", "get-port": "7.1.0", "globby": "16.1.1", "jsr": "0.14.3", "lefthook": "2.1.1", "madge": "8.0.0", "maml.js": "^0.0.3", "minimist": "1.2.8", "node-fetch-native": "1.6.7", "prettier": "3.8.1", "size-limit": "12.0.0", "ts-node": "10.9.2", "tsd": "0.33.0", "tsx": "4.21.0", "typescript": "5.9.3", "vitepress": "1.6.4", "which": "6.0.1", "yaml": "2.8.2", "zurk": "0.11.10" }, "overrides": { "globby": { "fast-glob": "3.3.3" }, "tsx": { "esbuild": "$esbuild" }, "vite": { "esbuild": "$esbuild" }, "@webpod/ps": { "zurk": "$zurk" } }, "publishConfig": { "registry": "https://wombat-dressing-room.appspot.com" }, "keywords": [ "bash", "bin", "binary", "call", "child", "child_process", "exec", "execute", "invoke", "pipe", "process", "script", "shell", "spawn", "zx" ], "prettier": { "semi": false, "singleQuote": true, "endOfLine": "lf", "trailingComma": "es5" }, "repository": { "type": "git", "url": "git+https://github.com/google/zx.git" }, "homepage": "https://google.github.io/zx/", "author": "Anton Medvedev ", "license": "Apache-2.0", "volta": { "node": "24.13.1" }, "tsd": { "compilerOptions": { "rootDir": "." } } } ================================================ FILE: scripts/build-clean.mjs ================================================ #!/usr/bin/env node // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import fs from 'node:fs' import glob from 'fast-glob' const redundants = await glob( ['build/{repl,globals-jsr}.d.ts', 'build/{deps,internals,util,vendor*}.js'], { onlyFiles: true, absolute: true, } ) for (const file of redundants) { fs.unlinkSync(file) } console.log('postbuild removed', redundants) ================================================ FILE: scripts/build-dts.mjs ================================================ #!/usr/bin/env node // Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import fs from 'node:fs/promises' import { generateDtsBundle } from 'dts-bundle-generator' import glob from 'fast-glob' const output = { inlineDeclareExternals: true, inlineDeclareGlobals: true, sortNodes: false, exportReferencedTypes: false, //args['export-referenced-types'], } const entries = [ { filePath: './src/vendor-extra.ts', outFile: './build/vendor-extra.d.ts', libraries: { allowedTypesLibraries: ['node'], // args['external-types'], inlinedLibraries: [ '@nodelib/fs.stat', '@nodelib/fs.scandir', '@nodelib/fs.walk', 'fast-glob', '@types/jsonfile', 'node-fetch-native', // 'chalk', 'globby', '@types/minimist', // '@types/which', // 'zurk', // '@webpod/ps', '@webpod/ingrid', 'depseek', 'envapi', 'maml.js', ], // args['external-inlines'], }, output, }, { filePath: './src/vendor-core.ts', outFile: './build/vendor-core.d.ts', libraries: { allowedTypesLibraries: ['node'], // args['external-types'], inlinedLibraries: [ '@types/which', '@webpod/ps', '@webpod/ingrid', 'chalk', 'zurk', ], // args['external-inlines'], }, output, }, ] const compilationOptions = { preferredConfigPath: './tsconfig.json', // args.project, followSymlinks: true, } const results = generateDtsBundle(entries, compilationOptions) // generateDtsBundle cannot handle the circular refs on types inlining, so we need to help it manually: /* build/vendor.d.ts(163,7): error TS2456: Type alias 'Options' circularly references itself. build/vendor.d.ts(164,7): error TS2456: Type alias 'Entry' circularly references itself. build/vendor.d.ts(165,7): error TS2456: Type alias 'Task' circularly references itself. build/vendor.d.ts(166,7): error TS2456: Type alias 'Pattern' circularly references itself. build/vendor.d.ts(167,7): error TS2456: Type alias 'FileSystemAdapter' circularly references itself. build/vendor.d.ts(197,48): error TS2694: Namespace 'FastGlob' has no exported member 'FastGlobOptions */ .map((r) => r .replace('type Options = Options;', 'export {Options};') .replace('type Task = Task;', 'export {Task};') .replace('type Pattern = Pattern;', 'export {Pattern};') .replace('FastGlob.FastGlobOptions', 'FastGlob.Options') .replace('type Entry =', 'export type Entry =') ) for (const i in results) { const entry = entries[i] const result = results[i] await fs.writeFile(entry.outFile, result, 'utf8') } // Properly formats triple-slash directives const pkgEntries = ['core', 'index', 'vendor'] const prefix = `/// /// ` for (const dts of await glob(['build/**/*.d.ts', '!build/vendor-*.d.ts'])) { const contents = (pkgEntries.some((e) => dts.includes(e)) ? prefix : '') + (await fs.readFile(dts, 'utf8')) .replaceAll(".ts';", ".js';") .split('\n') .filter((line) => !line.startsWith('/// path.relative(cwd, path.resolve(cwd, p))) const _bundle = bundle && bundle !== 'none' const _external = ['zx/globals', ...(_bundle ? external.split(',') : [])] // https://github.com/evanw/esbuild/issues/1466 const plugins = [ esbuildResolvePlugin({ yaml: path.resolve(__dirname, '../node_modules/yaml/browser'), }), ] const thirdPartyModules = new Set() if (_bundle && entryPoints.length > 1) { plugins.push(entryChunksPlugin()) } if (bundle === 'src') { // https://github.com/evanw/esbuild/issues/619 // https://github.com/pradel/esbuild-node-externals/pull/52 plugins.push(nodeExternalsPlugin()) } if (hybrid) { plugins.push( hybridExportPlugin({ loader: 'reexport', to: 'build', toExt: '.js', }) ) } plugins.push( { name: 'get-3rd-party-modules', setup: (build) => { build.onResolve({ filter: /./, namespace: 'file' }, async (args) => { thirdPartyModules.add(args.resolveDir) }) }, }, transformHookPlugin({ hooks: [ { on: 'end', if: !hybrid, pattern: /\.js$/, transform(contents, file) { const { name } = path.parse(file) const _contents = contents .toString() .replace( '} = __module__', `} = globalThis.Deno ? globalThis.require("./${name}.cjs") : __module__` ) return injectCode(_contents, `import "./deno.js"`) }, }, { on: 'end', if: !hybrid, pattern: /cli\.js$/, transform(contents) { return `${contents}autorun(import.meta) ` }, }, { on: 'end', pattern: entryPointsToRegexp(entryPoints), transform(contents) { const extras = [ // https://github.com/evanw/esbuild/issues/1633 contents.includes('import_meta') ? './scripts/import-meta-url.polyfill.js' : '', //https://github.com/evanw/esbuild/issues/1921 // p.includes('vendor') ? './scripts/require.polyfill.js' : '', ].filter(Boolean) return injectFile(contents, ...extras) }, }, { on: 'end', pattern: entryPointsToRegexp(entryPoints), transform(contents) { return contents .toString() .replaceAll('import.meta.url', 'import_meta_url') .replaceAll('import_meta.url', 'import_meta_url') .replaceAll('"node:', '"') .replaceAll( 'require("stream/promises")', 'require("stream").promises' ) .replaceAll('require("fs/promises")', 'require("fs").promises') .replaceAll('}).prototype', '}).prototype || {}') .replace(/DISABLE_NODE_FETCH_NATIVE_WARN/, ($0) => `${$0} || true`) .replace( /\/\/ Annotate the CommonJS export names for ESM import in node:/, ($0) => `/* c8 ignore next 100 */\n${$0}` ) .replace( 'yield import("zx/globals")', 'yield require("./globals.cjs")' ) .replace('require("./internals.ts")', 'require("./internals.cjs")') }, }, ], }), extractHelpersPlugin({ cwd: 'build', include: /\.cjs/, }), { name: 'deno', setup(build) { build.onEnd(() => { fs.copyFileSync('./scripts/deno.polyfill.js', './build/deno.js') fs.writeFileSync( './build/3rd-party-licenses', digestLicenses(thirdPartyModules) ) }) }, } ) // prettier-ignore function digestLicenses(dirs) { const digest = [...[...dirs] .reduce((m, d) => { const chunks = d.split('/') const i = chunks.lastIndexOf('node_modules') const name = chunks[i + 1] const shift = i + 1 + (name.startsWith('@') ? 2 : 1) const root = chunks.slice(0, shift).join('/') m.add(root) return m }, new Set())] .map(d => { const extractName = (entry) => entry?.name ? `${entry.name} <${entry.email}>` : entry const pkg = path.join(d, 'package.json') const pkgJson = JSON.parse(fs.readFileSync(pkg, 'utf-8')) const author = extractName(pkgJson.author) const contributors = (pkgJson.contributors || pkgJson.maintainers || []).map(extractName).join(', ') const by = author || contributors || '' const repository = pkgJson.repository?.url || pkgJson.repository || '' const license = pkgJson.license || '' if (pkgJson.name === 'zx') return return `${pkgJson.name}@${pkgJson.version} ${by} ${repository} ${license}` }) .filter(Boolean) .sort() .join('\n\n') return `THIRD PARTY LICENSES ${digest} ` } function entryPointsToRegexp(entryPoints) { return new RegExp( '(' + entryPoints.map((e) => escapeRegExp(path.parse(e).name)).join('|') + ')\\.cjs$' ) } function escapeRegExp(str) { return str.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') } const esmConfig = { absWorkingDir: cwd, entryPoints, outdir: './build', bundle: _bundle, external: _external, minify, sourcemap, sourcesContent: false, platform: 'node', target: 'esnext', format: 'esm', outExtension: { '.js': '.mjs', }, plugins, legalComments: license, tsconfig: './tsconfig.json', } const cjsConfig = { ...esmConfig, outdir: './build', target: 'es6', format: 'cjs', outExtension: { '.js': '.cjs', }, } for (const format of formats) { const config = format === 'cjs' ? cjsConfig : esmConfig console.log('esbuild config:', config) await esbuild.build(config).catch(() => process.exit(1)) } process.exit(0) ================================================ FILE: scripts/build-jsr.mjs ================================================ // Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import fs from 'node:fs' import path from 'node:path' const __dirname = path.dirname(new URL(import.meta.url).pathname) const root = path.resolve(__dirname, '..') const pkgJson = JSON.parse( fs.readFileSync(path.resolve(root, 'package.json'), 'utf-8') ) const deps = pkgJson.devDependencies const jsrDeps = { yaml: 'jsr:@eemeli/yaml', zurk: 'jsr:@webpod/zurk', } const prodDeps = new Set([ '@types/fs-extra', '@types/minimist', '@types/node', '@types/which', '@webpod/ingrid', '@webpod/ps', 'chalk', 'create-require', 'depseek', 'envapi', 'fs-extra', 'globby', 'minimist', 'node-fetch-native', 'which', 'yaml', 'zurk', ]) fs.writeFileSync( path.resolve(root, 'jsr.json'), JSON.stringify( { name: '@webpod/zx', version: pkgJson.version, license: pkgJson.license, exports: { '.': './src/index.ts', './core': './src/core.ts', './cli': './src/cli.ts', './globals': './src/globals-jsr.ts', }, publish: { include: ['src', 'README.md', 'LICENSE'], exclude: ['src/globals.ts'], }, nodeModulesDir: 'auto', imports: Object.entries(deps).reduce( (m, [k, v]) => { if (prodDeps.has(k)) { const name = jsrDeps[k] || `npm:${k}` m[k] = `${name}@${v}` } return m }, { 'zurk/spawn': `jsr:@webpod/zurk@${deps.zurk}`, 'zx/globals': './src/globals-jsr.ts', } ), }, null, 2 ) ) console.log('jsr.json prepared for JSR') ================================================ FILE: scripts/build-pkgjson-lite.mjs ================================================ // Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Prepares a lite (core) version of zx to publish import fs from 'node:fs' import path from 'node:path' import { depseekSync } from 'depseek' const __dirname = path.dirname(new URL(import.meta.url).pathname) const root = path.resolve(__dirname, '..') const source = 'package.json' const dest = 'package-lite.json' const _pkgJson = JSON.parse(fs.readFileSync(path.join(root, source), 'utf-8')) const files = new Set() const entries = new Set(['./core.js', './3rd-party-licenses']) for (const entry of entries) { if (!fs.existsSync(path.join(root, 'build', entry))) continue files.add(entry) const contents = fs.readFileSync(path.join(root, 'build', entry), 'utf-8') const deps = depseekSync(contents) for (const { value: file } of deps) { if (file.startsWith('.')) { entries.add(file) entries.add(file.replace(/\.c?js$/, '.d.ts')) } } } const whitelist = new Set([ 'name', 'version', 'description', 'type', 'main', 'types', 'typesVersions', 'exports', 'files', 'engines', 'optionalDependencies', 'publishConfig', 'keywords', 'repository', 'homepage', 'author', 'license', ]) const __pkgJson = Object.fromEntries( Object.entries(_pkgJson).filter(([k]) => whitelist.has(k)) ) const pkgJson = { ...__pkgJson, version: _pkgJson.version + '-lite', exports: { '.': { types: './build/core.d.ts', import: './build/core.js', require: './build/core.cjs', default: './build/core.js', }, './package.json': './package.json', }, main: './build/core.cjs', types: './build/core.d.ts', typesVersions: { '*': { '.': ['./build/core.d.ts'], }, }, files: [...files].map((f) => path.join('build', f)).sort(), } fs.writeFileSync(path.resolve(root, dest), JSON.stringify(pkgJson, null, 2)) console.log(`${dest} prepared for npm`) ================================================ FILE: scripts/build-pkgjson-main.mjs ================================================ // Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Optimizes package.json for npm publishing import fs from 'node:fs' import path from 'node:path' const __dirname = path.dirname(new URL(import.meta.url).pathname) const root = path.resolve(__dirname, '..') const source = 'package.json' const dest = 'package-main.json' const _pkgJson = JSON.parse(fs.readFileSync(path.join(root, source), 'utf-8')) const whitelist = new Set([ 'name', 'version', 'description', 'type', 'main', 'types', 'typesVersions', 'exports', 'bin', 'man', 'files', 'engines', 'optionalDependencies', 'publishConfig', 'keywords', 'repository', 'homepage', 'author', 'license', ]) const pkgJson = Object.fromEntries( Object.entries(_pkgJson).filter(([k]) => whitelist.has(k)) ) fs.writeFileSync(path.resolve(root, dest), JSON.stringify(pkgJson, null, 2)) console.log(`${dest} prepared for npm`) ================================================ FILE: scripts/build-tests.mjs ================================================ #!/usr/bin/env node // Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import fs from 'node:fs' import path from 'node:path' import * as core from '../build/core.js' import * as cli from '../build/cli.js' import * as index from '../build/index.js' // prettier-ignore const modules = [ ['core', core], ['cli', cli], ['index', index], ] const root = path.resolve(new URL(import.meta.url).pathname, '../..') const filePath = path.resolve(root, `test/export.test.js`) const copyright = fs.readFileSync( path.resolve(root, 'test/fixtures/copyright.txt'), 'utf8' ) let head = `${copyright.replace('YEAR', new Date().getFullYear())} import assert from 'node:assert' import { test, describe } from 'node:test'` let body = '\n' for (const [name, ref, apis = Object.keys(ref).sort()] of modules) { head += `\nimport * as ${name} from '../build/${name}.cjs'` body += `\n//prettier-ignore\ndescribe('${name}', () => {\n` body += ` test('exports', () => {\n` for (const r of apis) { const api = ref[r] body += ` assert.equal(typeof ${name}.${r}, '${typeof api}', '${name}.${r}')\n` if (typeof api !== 'function' && typeof api !== 'object') continue for (const k of Object.keys(api).sort()) { const v = api[k] body += ` assert.equal(typeof ${name}.${r}.${k}, '${typeof v}', '${name}.${r}.${k}')\n` } } body += ' })\n' body += '})\n' } const contents = head + body fs.writeFileSync(filePath, contents) ================================================ FILE: scripts/build-versions.mjs ================================================ #!/usr/bin/env node // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import fs from 'fs-extra' import path from 'node:path' import minimist from 'minimist' const root = path.resolve(new URL(import.meta.url).pathname, '../..') const copyright = fs.readFileSync( path.resolve(root, 'test/fixtures/copyright.txt'), 'utf8' ) const license = copyright.replace('YEAR', new Date().getFullYear()) const deps = [ 'chalk', 'depseek', 'dotenv', 'fetch', 'fs', 'glob', 'minimist', 'ps', 'which', 'yaml', ] const namemap = { dotenv: 'envapi', fs: 'fs-extra', fetch: 'node-fetch-native', glob: 'globby', ps: '@webpod/ps', } const versions = deps.reduce( (m, name) => { m[name] = fs.readJsonSync( path.resolve(root, 'node_modules', namemap[name] || name, 'package.json') ).version return m }, { zx: fs.readJsonSync(path.join(root, 'package.json')).version, } ) const argv = minimist(process.argv.slice(2), { default: versions, string: ['zx', ...deps], }) delete argv._ const list = JSON.stringify(argv, null, 2) .replaceAll(' "', ' ') .replaceAll('": ', ': ') .replaceAll('"', "'") .replace(/\n}$/, ',\n}') const versionsTs = `${license} export const versions: Record = ${list} ` const versionsCjs = `${license} module.exports = { versions: ${list} ` fs.writeFileSync(path.join(root, 'src/versions.ts'), versionsTs, 'utf8') // fs.writeFileSync(path.join(root, 'build/versions.cjs'), versionsCjs, 'utf8') ================================================ FILE: scripts/deno.polyfill.js ================================================ import { createRequire } from 'node:module' import * as process from 'node:process' // prettier-ignore if (globalThis.Deno) { globalThis.require = createRequire(import.meta.url) globalThis.__filename = new URL(import.meta.url).pathname globalThis.__dirname = new URL('.', import.meta.url).pathname globalThis.module = new Proxy({}, { set() { return true } }) const p = globalThis.process = globalThis.process || process p.version || (p.version = 'v18.0.0') p.version || (p.version = { node: '18.0.0' }) p.env || (p.env = globalThis.Deno.env.toObject()) p.argv || (p.argv = [globalThis.Deno.execPath(), globalThis.Deno.mainModule.replace('file://', ''), ...globalThis.Deno.args]) } ================================================ FILE: scripts/import-meta-url.polyfill.js ================================================ const import_meta_url = typeof document === 'undefined' ? new (require('url').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src) || new URL('main.js', document.baseURI).href ================================================ FILE: src/cli.ts ================================================ #!/usr/bin/env node // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import url from 'node:url' import process from 'node:process' import { $, ProcessOutput, parseArgv, updateArgv, resolveDefaults, chalk, dotenv, fetch, fs, path, stdin, VERSION, Fail, } from './index.ts' import { installDeps, parseDeps } from './deps.ts' import { startRepl } from './repl.ts' import { randomId } from './util.ts' import { transformMarkdown } from './md.ts' import { createRequire, type minimist } from './vendor.ts' export { transformMarkdown } from './md.ts' const EXT = '.mjs' const EXT_RE = /^\.[mc]?[jt]sx?$/ // prettier-ignore export const argv: minimist.ParsedArgs = parseArgv(process.argv.slice(2), { default: resolveDefaults({ ['prefer-local']: false } as any, 'ZX_', process.env, new Set(['env', 'install', 'registry'])), // exclude 'prefer-local' to let minimist infer the type string: ['shell', 'prefix', 'postfix', 'eval', 'cwd', 'ext', 'registry', 'env'], boolean: ['version', 'help', 'quiet', 'verbose', 'install', 'repl', 'experimental'], alias: { e: 'eval', i: 'install', v: 'version', h: 'help', l: 'prefer-local', 'env-file': 'env' }, stopEarly: true, parseBoolean: true, camelCase: true, }) autorun(import.meta) export function autorun(meta: ImportMeta): void { if (meta && isMain(meta)) main().catch((err) => { if (err instanceof ProcessOutput) { console.error('Error:', err.message) } else { console.error(err) } process.exitCode = 1 }) } export function printUsage() { // language=txt console.log(` ${chalk.bold('zx ' + VERSION)} A tool for writing better scripts ${chalk.bold('Usage')} zx [options]