[
  {
    "path": ".envrc",
    "content": "#!/usr/bin/env bash\n\n# Automatically sets up your devbox environment whenever you cd into this\n# directory via our direnv integration:\n\neval \"$(devbox generate direnv --print-envrc)\"\n\n# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/\n# for more details\n"
  },
  {
    "path": ".gitattributes",
    "content": "*.go diff=golang\n*.sh diff=bash\n*.md diff=markdown\n*.py diff=python\n*.sql diff=sql\n\ndevbox.json linguist-language=json5\n\ngo.sum -diff linguist-generated\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/01-bug.yaml",
    "content": "name: Bug Report\ndescription: File a bug report\nlabels:\n  - bug\n  - triage\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: What happened?\n      description: >-\n        Also include what you expected to happen and any other relevant details.\n    validations:\n      required: true\n  - type: textarea\n    id: repro\n    attributes:\n      label: Steps to reproduce\n      description: >-\n        What specific steps can we take to reproduce this issue?\n        Including a script would be much appreciated!\n      value: |\n        1.\n        2.\n        3.\n  - type: dropdown\n    id: commands\n    attributes:\n      label: Command\n      description: What Devbox command were you running when the bug occurred?\n      multiple: true\n      options:\n        - add\n        - auth\n        - create\n        - generate\n        - global\n        - info\n        - init\n        - install\n        - rm\n        - run\n        - search\n        - services\n        - shell\n        - shellenv\n        - update\n        - version\n  - type: textarea\n    id: devbox-json\n    attributes:\n      label: devbox.json\n      description: Please include a copy of your devbox.json file.\n      render: \"jsonc\"\n  - type: input\n    id: devbox-version\n    attributes:\n      label: Devbox version\n      description: \"Paste the output of `devbox version`.\"\n    validations:\n      required: true\n  - type: input\n    id: nix-version\n    attributes:\n      label: Nix version\n      description: \"Paste the output of `nix --version`.\"\n  - type: dropdown\n    id: system\n    attributes:\n      label: What system does this bug occur on?\n      options:\n        - macOS (Intel)\n        - macOS (Apple Silicon)\n        - Linux (x86-64)\n        - Linux (ARM64)\n        - Other (please include in the description above)\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Debug logs\n      description: >-\n        If possible, reproduce the bug with the `DEVBOX_DEBUG=1` environment\n        variable set and paste any output here.\n        For example: `DEVBOX_DEBUG=1 devbox run -- mycrash.sh`.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/02-feature.yaml",
    "content": "name: Feature Request\ndescription: Suggest an idea or new feature\nlabels:\n  - feature\n  - triage\nbody:\n  - type: textarea\n    id: problem\n    attributes:\n      label: What problem are you trying to solve?\n      description: >-\n        Describe the problem you're trying to solve with this feature.\n      placeholder: I'm always frustrated when...\n    validations:\n      required: true\n  - type: textarea\n    id: solution\n    attributes:\n      label: What solution would you like?\n      description: >-\n        Describe the feature you would like to see implemented and explain how\n        it would address the problem you described above.\n    validations:\n      required: true\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Alternatives you've considered\n      description: >-\n        Describe any alternative solutions or features you've considered. If you\n        know of any similar features requested before, please include links to\n        them.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/03-package-bug.yaml",
    "content": "name: Package Issue\ndescription: Report a problem with an existing package on either Devbox or Nixhub\nlabels:\n  - package\n  - bug\n  - triage\nbody:\n  - type: input\n    id: name\n    attributes:\n      label: Package name\n      placeholder: go@1.21.6, python@3.10.13, etc.\n    validations:\n      required: true\n  - type: textarea\n    id: solution\n    attributes:\n      label: What changes are you requesting?\n      description: >-\n        Describe what's going wrong or what changes you'd like to see to the\n        package.\n    validations:\n      required: true\n  - type: input\n    id: link\n    attributes:\n      label: Nixhub link\n      placeholder: https://www.nixhub.io/packages/go\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/04-package-request.yaml",
    "content": "name: Package Request\ndescription: Request a new package to be added to Devbox and Nixhub\nlabels:\n  - package\n  - triage\nbody:\n  - type: input\n    id: name\n    attributes:\n      label: Package name\n      description: What name are you requesting for the new package?\n    validations:\n      required: true\n  - type: input\n    id: nixpkgs\n    attributes:\n      label: Nix package link\n      description: >-\n        Are you able to find the package on https://search.nixos.org/packages?\n        If so, please include a link to the search results. Otherwise, leave\n        blank.\n  - type: textarea\n    id: software\n    attributes:\n      label: Software\n      description: >-\n        Provide a description of the software that should be added to the new\n        package. Include any relevant links such as websites,\n        GitHub repositories, etc.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Summary\n\n## How was it tested?\n\n## Community Contribution License\n\nAll community contributions in this pull request are licensed to the project\nmaintainers under the terms of the\n[Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0).\n\nBy creating this pull request, I represent that I have the right to license the\ncontributions to the project maintainers under the Apache 2 License as stated in\nthe\n[Community Contribution License](https://github.com/jetify-com/opensource/blob/main/CONTRIBUTING.md#community-contribution-license).\n"
  },
  {
    "path": ".github/workflows/cache-upload.yml",
    "content": "name: cache-upload\n# Uploads devbox nix dependencies to cache\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n  schedule:\n    - cron: '30 8 * * *' # Run nightly at 8:30 UTC\n\npermissions:\n  contents: read\n  pull-requests: read\n\ndefaults:\n  run:\n    shell: bash\n\nenv:\n  DEVBOX_API_TOKEN: ${{ secrets.DEVBOX_API_TOKEN }}\n  DEVBOX_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  DEVBOX_DEBUG: 1\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  # I think this should be added to individual nix commands within devbox, but this is quick fix for now\n  NIX_CONFIG: |\n    access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}\n\njobs:\n  upload-cache:\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 10\n    steps:\n      - uses: actions/checkout@v4\n\n      # Build devbox from scratch because released devbox has a bug that prevents \n      # DEVBOX_API_TOKEN use\n      # we can remove this after 0.10.6 is out.\n      - uses: actions/setup-go@v5\n        with:\n          go-version-file: ./go.mod\n      - name: Build devbox\n        run: | \n          go build -o dist/devbox ./cmd/devbox\n          sudo mv ./dist/devbox /usr/local/bin/\n\n      # - name: Install devbox\n      #   uses: jetify-com/devbox-install-action@v0.14.0\n      #   with:\n      #     enable-cache: true\n\n      # We upload twice, once before updating and once after. This shows a simple\n      # method to cache the latest current and latest dependencies.\n      # If we want read access to cache on multi-user nix installs (e.g. macos), \n      # we need to call devbox cache configure. This is currently not working\n      # as expected on CICD.\n      - name: Upload cache\n        run: |\n          devbox cache upload\n          devbox update\n          devbox cache upload\n"
  },
  {
    "path": ".github/workflows/cli-post-release.yml",
    "content": "name: cli-post-release\n# Finalize and announce the release once its been published on Github.\n\non:\n  release:\n    types: [released]\n\npermissions:\n  contents: write\n  pull-requests: read\n  id-token: write # Needed for aws-actions/configure-aws-credentials@v1\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    environment: release\n    steps:\n      - name: Configure AWS Credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          role-to-assume: ${{ secrets.AWS_ROLE }}\n          aws-region: us-west-2\n      - name: Update latest version in s3\n        run: |\n          tmp_file=$(mktemp)\n          echo \"${{ github.ref_name }}\" > $tmp_file\n          aws s3 cp $tmp_file s3://releases.jetpack.io/devbox/stable/version\n"
  },
  {
    "path": ".github/workflows/cli-release.yml",
    "content": "name: cli-release\n# Releases the Devbox CLI\n\nconcurrency: cli-release\n\non:\n  # Build/Release on demand\n  workflow_dispatch:\n    inputs:\n      create_edge_release:\n        description: \"Create edge release?\"\n        required: false\n        default: false\n        type: boolean\n  schedule:\n    - cron: \"45 8 * * 4\" # Create edge weekly on Thursdays.\n  push:\n    tags:\n      - \"*\" # Tags that trigger a new release version\n\npermissions:\n  contents: write\n  pull-requests: read\n  id-token: write # Needed for aws-actions/configure-aws-credentials@v1\n\njobs:\n  tests:\n    uses: ./.github/workflows/cli-tests.yaml\n\n  report-test-failures:\n    runs-on: ubuntu-latest\n    needs: tests\n    if: failure() || cancelled()\n    steps:\n      - name: Notify jetpack.io slack of release status (only if tests fail)\n        id: slack\n        uses: slackapi/slack-github-action@v1.25.0\n        with:\n          payload: |\n            {\n              \"status\": \"test ${{ needs.tests.result }}\"\n            }\n        env:\n          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLI_RELEASE_WEBHOOK_URL }}\n\n  edge:\n    runs-on: ubuntu-latest\n    environment: release\n    needs: tests\n    if: ${{ inputs.create_edge_release || github.event.schedule }}\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0 # Needed by goreleaser to browse history.\n      - name: Determine edge tag\n        # This tag is semver and works with semver.Compare\n        run: echo \"EDGE_TAG=0.0.0-edge.$(date +%Y-%m-%d)\" >> $GITHUB_ENV\n      - name: Set edge tag\n        id: tag_version\n        uses: mathieudutour/github-tag-action@v6.1\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          custom_tag: ${{ env.EDGE_TAG }}\n          tag_prefix: \"\"\n      - name: Set up go\n        uses: actions/setup-go@v5\n        with:\n          go-version-file: ./go.mod\n      - name: Build snapshot with goreleaser\n        uses: goreleaser/goreleaser-action@v6\n        with:\n          distribution: goreleaser\n          version: latest\n          args: release --clean --skip=announce,publish --snapshot\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TELEMETRY_KEY: ${{ secrets.TELEMETRY_KEY }}\n          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}\n      - name: Create Sentry release\n        uses: getsentry/action-release@v1\n        env:\n          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n          SENTRY_ORG: ${{ vars.SENTRY_ORG }}\n          SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}\n        with:\n          environment: development\n          version:  ${{ env.EDGE_TAG }}\n          version_prefix: \"devbox@\"\n      - name: Publish snapshot release to GitHub\n        uses: softprops/action-gh-release@v1\n        with:\n          prerelease: true\n          body: \"${{ env.EDGE_TAG }} edge release\"\n          fail_on_unmatched_files: true\n          tag_name: ${{ env.EDGE_TAG }}\n          files: |\n            dist/checksums.txt\n            dist/*.tar.gz\n      - name: Configure AWS Credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          role-to-assume: ${{ secrets.AWS_ROLE }}\n          aws-region: us-west-2\n      - name: Update edge version in s3\n        run: |\n          tmp_file=$(mktemp)\n          echo \"${{ env.EDGE_TAG }}\" > $tmp_file\n          aws s3 cp $tmp_file s3://releases.jetpack.io/devbox/edge/version\n\n  release:\n    runs-on: ubuntu-latest\n    environment: release\n    needs: tests\n    # Only release when there's a tag for the release.\n    if: startsWith(github.ref, 'refs/tags/')\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0 # Needed by goreleaser to browse history.\n      - name: Set up go\n        uses: actions/setup-go@v5\n        with:\n          go-version-file: ./go.mod\n      - name: Create Sentry release\n        uses: getsentry/action-release@v1\n        env:\n          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n          SENTRY_ORG: ${{ vars.SENTRY_ORG }}\n          SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}\n        with:\n          environment: production\n          version: ${{ github.ref }}\n          version_prefix: \"devbox@\"\n      - name: Release with goreleaser\n        uses: goreleaser/goreleaser-action@v3\n        with:\n          distribution: goreleaser\n          version: latest\n          args: release --clean\n        env:\n          DISCORD_WEBHOOK_ID: ${{ secrets.DISCORD_WEBHOOK_ID }}\n          DISCORD_WEBHOOK_TOKEN: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TELEMETRY_KEY: ${{ secrets.TELEMETRY_KEY }}\n          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}\n      - name: Notify jetpack.io slack of release status\n        id: slack\n        if: always()\n        uses: slackapi/slack-github-action@v1.25.0\n        with:\n          payload: |\n            {\n              \"status\": \"release ${{ job.status }}\"\n            }\n        env:\n          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLI_RELEASE_WEBHOOK_URL }}\n"
  },
  {
    "path": ".github/workflows/cli-tests.yaml",
    "content": "name: cli-tests\n# Runs the Devbox CLI tests\n\nconcurrency:\n  group: ${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  pull_request:\n  push:\n    branches:\n      - main\n  merge_group:\n    branches:\n      - main\n  workflow_call:\n    inputs:\n      run-mac-tests:\n        type: boolean\n  workflow_dispatch:\n    inputs:\n      run-mac-tests:\n        type: boolean\n        description: Run tests on macOS\n      example-debug:\n        type: boolean\n        description: Run example tests with DEVBOX_DEBUG=1 to increase verbosity\n  schedule:\n    - cron: '30 8 * * *' # Run nightly at 8:30 UTC\n\npermissions:\n  contents: read\n  pull-requests: read\n\ndefaults:\n  run:\n    # Explicitly setting the shell to bash runs commands with\n    # `bash --noprofile --norc -eo pipefail` instead of `bash -e`.\n    shell: bash\n\nenv:\n  DEVBOX_DEBUG: 1\n  DEVBOX_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  HOMEBREW_NO_ANALYTICS: 1\n  HOMEBREW_NO_AUTO_UPDATE: 1\n  HOMEBREW_NO_EMOJI: 1\n  HOMEBREW_NO_ENV_HINTS: 1\n  HOMEBREW_NO_INSTALL_CLEANUP: 1\n  NIX_CONFIG: |\n    access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}\n\njobs:\n  build-devbox:\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-go@v5\n        with:\n          go-version-file: ./go.mod\n      - name: Build devbox\n        run: go build -o dist/devbox ./cmd/devbox\n      - name: Upload devbox artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: devbox-${{ runner.os }}-${{ runner.arch }}\n          path: ./dist/devbox\n          retention-days: 7\n\n  typos:\n    name: Spell Check with Typos\n    if: github.ref != 'refs/heads/main'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: crate-ci/typos@v1.16.26\n\n  flake-test:\n    name: Test Flake Build\n    if: github.ref != 'refs/heads/main'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install devbox\n        uses: jetify-com/devbox-install-action@jl/migrate-installer\n        with:\n          enable-cache: true\n      - name: Build flake\n        run: |\n          if ! devbox run build-flake; then\n            echo \"::warning::If this fails, you probably have to run 'devbox run update-hash'\"\n            exit 1\n          fi\n      - run: ./result/bin/devbox version\n\n  golangci-lint:\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 10\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install devbox\n        uses: jetify-com/devbox-install-action@jl/migrate-installer\n        with:\n          enable-cache: true\n\n      - name: Mount golang cache\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.cache/golangci-lint\n            ~/.cache/go-build\n            ~/go/pkg\n          key: go-${{ runner.os }}-${{ hashFiles('go.sum') }}\n\n      - run:  devbox run lint\n\n  test:\n    needs: build-devbox\n    strategy:\n      matrix:\n        is-main:\n          - ${{ github.ref == 'refs/heads/main' && 'is-main' || 'not-main' }}\n        os: [ubuntu-latest, macos-latest]\n        # This is an optimization that runs tests twice, with and without\n        # the devbox.json tests. We can require the other tests to complete before\n        # merging, while keeping the others as an additional non-required signal\n        run-project-tests: [\"project-tests-only\", \"project-tests-off\"]\n        # Run tests on:\n        # 1. the oldest supported nix version (Nixpkgs requires >= 2.18 as of 2026)\n        # 2. nix 2.19.2: version before nix profile changes\n        # 3. latest nix version (note, 2.20.1 introduced a new profile change)\n        nix-version: [\"2.18.0\", \"2.19.2\", \"2.30.2\"]\n        exclude:\n          # Only runs tests on macos if explicitly requested, or on a schedule\n          - os: \"${{ (inputs.run-mac-tests || github.event.schedule != '') && 'dummy' || 'macos-latest' }}\"\n\n\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 60\n    env:\n      # For devbox.json tests, we default to non-debug mode since the debug output is less useful than for unit testscripts.\n      # But we allow overriding via inputs.example-debug\n      DEVBOX_DEBUG: ${{ (matrix.run-project-tests == 'project-tests-off' || inputs.example-debug) && '1' || '0' }}\n      DEVBOX_GOLANG_TEST_TIMEOUT: \"${{ (github.ref == 'refs/heads/main' || inputs.run-mac-tests) && '1h' || '30m' }}\"\n    steps:\n      - name: clear directories to reduce disk usage\n        # https://github.com/actions/runner-images/issues/2840#issuecomment-1284059930\n        run: |\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n\n      - uses: actions/checkout@v4\n      - name: Mount golang cache\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.cache/go-build\n            ~/go/pkg\n          key: go-devbox-tests-${{ runner.os }}-${{ hashFiles('go.sum') }}\n      - name: Install additional shells (dash, zsh)\n        run: |\n          if [ \"$RUNNER_OS\" == \"Linux\" ]; then\n            sudo apt-get update\n            sudo apt-get install dash zsh\n          elif [ \"$RUNNER_OS\" == \"macOS\" ]; then\n            brew update\n            brew install dash zsh\n          fi\n      - name: Install devbox\n        uses: jetify-com/devbox-install-action@jl/migrate-installer\n        with:\n          enable-cache: true\n      - name: Setup Nix GitHub authentication\n        run: |\n          # Setup github authentication to ensure Github's rate limits are not hit\n          # For macOS, we need to configure the system-wide nix.conf because the Nix daemon\n          # runs as a different user and doesn't read the user's ~/.config/nix/nix.conf\n          if [ \"$RUNNER_OS\" == \"macOS\" ]; then\n            echo \"Configuring system-wide Nix config for macOS daemon\"\n            # Ensure /etc/nix directory exists\n            if [ ! -d /etc/nix ]; then\n              sudo mkdir -p /etc/nix\n            fi\n            # Check if file exists, create it if not\n            if [ ! -f /etc/nix/nix.conf ]; then\n              echo \"# Nix configuration\" | sudo tee /etc/nix/nix.conf > /dev/null\n            fi\n            echo \"access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}\" | sudo tee -a /etc/nix/nix.conf\n            # Restart nix daemon to pick up the new configuration\n            sudo launchctl stop org.nixos.nix-daemon || true\n            sudo launchctl start org.nixos.nix-daemon || true\n            sleep 2  # Give daemon time to restart\n          fi\n          # For Linux and as a backup for macOS, also configure user config\n          mkdir -p ~/.config/nix\n          echo \"access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}\" > ~/.config/nix/nix.conf\n      - name: Run fast tests\n        if: matrix.run-project-tests == 'project-tests-off'\n        run: |\n          echo \"::group::Nix version\"\n          nix --version\n          echo \"::endgroup::\"\n          echo \"::group::Contents of /etc/nix/nix.conf\"\n          cat /etc/nix/nix.conf || true\n          echo \"::endgroup::\"\n          echo \"::group::Resolved Nix config\"\n          nix show-config --extra-experimental-features nix-command\n          echo \"::endgroup::\"\n          devbox run go test -v -timeout $DEVBOX_GOLANG_TEST_TIMEOUT ./...\n      - name: Run project (slow) tests\n        if:  matrix.run-project-tests == 'project-tests-only'\n        run: devbox run test-projects-only\n\n  auto-nix-install: # ensure Devbox installs nix and works properly after installation.\n    needs: build-devbox\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n        use-detsys: [true, false]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n      - name: Download devbox\n        uses: actions/download-artifact@v4\n        with:\n          name: devbox-${{ runner.os }}-${{ runner.arch }}\n      - name: Add devbox to path\n        run: |\n          chmod +x ./devbox\n          sudo mv ./devbox /usr/local/bin/\n      - name: Install nix and devbox packages\n        run: |\n          export NIX_INSTALLER_NO_CHANNEL_ADD=1\n          export DEVBOX_FEATURE_DETSYS_INSTALLER=${{ matrix.use-detsys }}\n\n          # Setup github authentication BEFORE running devbox to ensure Github's rate limits are not hit.\n          # Configure user config first (Nix installer will respect this)\n          mkdir -p ~/.config/nix\n          echo \"access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}\" > ~/.config/nix/nix.conf\n\n          # Run devbox which will auto-install Nix if needed\n          devbox run echo \"Installing packages...\"\n\n          # After Nix is installed, configure system-wide config for the daemon on macOS\n          if [ \"$RUNNER_OS\" == \"macOS\" ]; then\n            echo \"Configuring system-wide Nix config for macOS daemon\"\n            # Check if file exists, create directory if needed\n            if [ ! -f /etc/nix/nix.conf ]; then\n              sudo mkdir -p /etc/nix\n              echo \"# Nix configuration\" | sudo tee /etc/nix/nix.conf > /dev/null\n            fi\n            echo \"access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}\" | sudo tee -a /etc/nix/nix.conf\n            # Restart nix daemon to pick up the new configuration\n            sudo launchctl stop org.nixos.nix-daemon || true\n            sudo launchctl start org.nixos.nix-daemon || true\n            sleep 2  # Give daemon time to restart\n          fi\n      - name: Test removing package\n        run: devbox rm go\n\n  # Run a sanity test to make sure Devbox can install and remove packages with\n  # the last few Nix releases.\n  test-nix-versions:\n    needs: build-devbox\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n        nix-version: [2.18.0, 2.19.2, 2.24.7, 2.30.2]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n      - name: Download devbox\n        uses: actions/download-artifact@v4\n        with:\n          name: devbox-${{ runner.os }}-${{ runner.arch }}\n      - name: Add devbox to path\n        run: |\n          chmod +x ./devbox\n          sudo mv ./devbox /usr/local/bin/\n      - name: Install Nix\n        uses: DeterminateSystems/nix-installer-action@v4\n        with:\n          logger: pretty\n          extra-conf: experimental-features = ca-derivations fetch-closure\n          nix-package-url: https://releases.nixos.org/nix/nix-${{ matrix.nix-version }}/nix-${{ matrix.nix-version }}-${{ runner.arch == 'X64' && 'x86_64' || 'aarch64' }}-${{ runner.os == 'macOS' && 'darwin' || 'linux' }}.tar.xz\n      - name: Run devbox install, devbox run, devbox rm\n        run: |\n          echo \"::group::Nix version\"\n          nix --version\n          echo \"::endgroup::\"\n          echo \"::group::Contents of /etc/nix/nix.conf\"\n          cat /etc/nix/nix.conf || true\n          echo \"::endgroup::\"\n          echo \"::group::Resolved Nix config\"\n          nix show-config --extra-experimental-features nix-command\n          echo \"::endgroup::\"\n          devbox install\n          devbox run -- echo \"Hello from devbox!\"\n          devbox rm go\n"
  },
  {
    "path": ".github/workflows/debug.yaml",
    "content": "name: debug\n\non:\n  workflow_dispatch:\n    inputs:\n      runner:\n        description: \"Runner type to debug on\"\n        required: true\n        default: \"ubuntu-latest\"\n        type: choice\n        options:\n          - macos-latest\n          - ubuntu-latest\n\npermissions:\n  contents: read\n\nenv:\n  HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}\"\n  HOMEBREW_NO_ANALYTICS: 1\n  HOMEBREW_NO_AUTO_UPDATE: 1\n  HOMEBREW_NO_EMOJI: 1\n  HOMEBREW_NO_ENV_HINTS: 1\n  HOMEBREW_NO_INSTALL_CLEANUP: 1\n\njobs:\n  debug:\n    runs-on: ${{ inputs.runner }}\n    timeout-minutes: 10\n    steps:\n      - name: Get rate limits\n        run: |\n          curl https://api.github.com/rate_limit \\\n              -H \"Accept: application/vnd.github+json\" \\\n              -H \"Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}\" \\\n              --show-error \\\n              --silent \\\n            | jq .\n      - uses: actions/checkout@v4\n      - uses: actions/setup-go@v5\n        with:\n          go-version-file: ./go.mod\n      - run: |\n          echo \"Starting a tmate session for 10 minutes.\"\n          echo\n          echo \"You can connect using the SSH command printed below to get an interactive shell\"\n          echo \"on this GitHub Actions runner. Access is limited to the public SSH keys\"\n          echo \"associated with your GitHub account.\"\n\n          curl https://api.github.com/users/${{ github.actor }}/keys \\\n              -H \"Accept: application/vnd.github+json\" \\\n              --show-error \\\n              --silent \\\n            | jq .\n      - uses: mxschmitt/action-tmate@v3\n        with:\n          limit-access-to-actor: true\n"
  },
  {
    "path": ".github/workflows/docker-image-release.yml",
    "content": "name: docker-image-release\n\non:\n  release:\n    types:\n      - published\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: 'tag name'\n        required: true\n        default: ''\n        type: string\n\njobs:\n  docker-image-build-push:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v4\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            jetpackio/devbox\n          tags: |\n            type=raw,value=${{ inputs.tag || github.ref_name }}\n          flavor: |\n            latest=false\n      - name: Docker meta root\n        id: metaroot\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            jetpackio/devbox-root-user\n          tags: |\n            type=raw,value=${{ inputs.tag || github.ref_name }}\n          flavor: |\n            latest=false\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n      - name: Login to Docker Hub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\n      - name: Build and push default\n        uses: docker/build-push-action@v5\n        with:\n          context: ./internal/devbox/generate/tmpl/\n          file: ./internal/devbox/generate/tmpl/DevboxImageDockerfile\n          build-args: |\n            DEVBOX_USE_VERSION=${{ inputs.tag || github.ref_name }}\n          push: true\n          platforms: linux/amd64,linux/arm64\n          tags: ${{ steps.meta.outputs.tags }}\n      - name: Build and push root user\n        uses: docker/build-push-action@v5\n        with:\n          context: ./internal/devbox/generate/tmpl/\n          file: ./internal/devbox/generate/tmpl/DevboxImageDockerfileRootUser\n          build-args: |\n            DEVBOX_USE_VERSION=${{ inputs.tag || github.ref_name }}\n          push: true\n          platforms: linux/amd64,linux/arm64\n          tags: ${{ steps.metaroot.outputs.tags }}\n      - name: Docker meta latest\n        id: metalatest\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            jetpackio/devbox\n          tags: |\n            type=raw,value=latest\n          flavor: |\n            latest=true\n      - name: Build and push latest\n        uses: docker/build-push-action@v5\n        with:\n          context: ./internal/devbox/generate/tmpl/\n          file: ./internal/devbox/generate/tmpl/DevboxImageDockerfile\n          push: true\n          platforms: linux/amd64,linux/arm64\n          tags: ${{ steps.metalatest.outputs.tags }}\n      - name: Docker meta root latest\n        id: metarootlatest\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            jetpackio/devbox-root-user\n          tags: |\n            type=raw,value=latest\n          flavor: |\n            latest=true\n      - name: Build and push root user latest\n        uses: docker/build-push-action@v5\n        with:\n          context: ./internal/devbox/generate/tmpl/\n          file: ./internal/devbox/generate/tmpl/DevboxImageDockerfileRootUser\n          push: true\n          platforms: linux/amd64,linux/arm64\n          tags: ${{ steps.metarootlatest.outputs.tags }}\n"
  },
  {
    "path": ".github/workflows/random-reviewer-assignment.yml",
    "content": "name: Random Reviewer Assignment\non:\n  pull_request:\n    types: [opened]\n\npermissions:\n  contents: read\n  pull-requests: write\n\nenv:\n  GITHUB_TOKEN: ${{ secrets.GH_TOKEN_FOR_PR_ASSIGNMENT }}\n\njobs:\n  assign-reviewer:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Randomly assign reviewer from team\n        uses: actions/github-script@v6\n        with:\n          script: |\n            const TRIAGE_USERNAME = 'Lagoja';\n            const EXCLUDE_USERNAMES = ['jetpack-io-bot'];\n\n            try {\n              const authenticatedUser = await github.rest.users.getAuthenticated();\n              \n              const teamMembers = await github.rest.teams.listMembersInOrg({\n                org: 'jetify-com',\n                team_slug: 'eng'\n              });\n              \n              const prAuthor = context.payload.pull_request.user.login.toLowerCase();\n              const prAuthorId = context.payload.pull_request.user.id;\n              const authenticatedUserLower = authenticatedUser.data.login.toLowerCase();\n\n              // If the PR author is already a member of the team, we can skip random assignment\n              const isPrAuthorInTeam = teamMembers.data.some(member => \n                member.login.toLowerCase() === prAuthor && member.id === prAuthorId\n              );\n\n              if (isPrAuthorInTeam) {\n                console.log(`PR author ${prAuthor} is already a team member, skipping random assignment.`);\n                return;\n              }\n              \n              // Get eligible reviewers (excluding PR author, authenticated user, and lagoja)\n              const eligibleReviewers = teamMembers.data\n                .filter(member => {\n                  const loginLower = member.login.toLowerCase();\n                  \n                  // Exclude authenticated user\n                  const isNotAuthenticatedUser = member.id !== authenticatedUser.data.id;\n                  const isNotTriage = loginLower !== TRIAGE_USERNAME.toLowerCase();\n                  const isNotExcludedUsername = !EXCLUDE_USERNAMES.includes(loginLower);\n\n                  return isNotAuthenticatedUser && isNotTriage && isNotExcludedUsername;\n                })\n                .map(member => member.login);\n              \n              console.log(`Eligible reviewers: ${eligibleReviewers.join(', ')}`);\n              \n              if (eligibleReviewers.length === 0) {\n                console.log('No eligible reviewers found');\n                return;\n              }\n              \n              const randomReviewer = eligibleReviewers[Math.floor(Math.random() * eligibleReviewers.length)];\n              const reviewers = [randomReviewer];\n\n              // Only add TRIAGE_USERNAME if they're not the PR author and not the authenticated user\n              if (prAuthor !== TRIAGE_USERNAME.toLowerCase() && \n                  authenticatedUserLower !== TRIAGE_USERNAME.toLowerCase()) {\n                reviewers.push(TRIAGE_USERNAME);\n              }\n              \n              console.log(`Final reviewers: ${reviewers.join(', ')}`);\n              \n              console.log(`Assigning reviewers: ${reviewers.join(', ')}`);\n              \n              await github.rest.pulls.requestReviewers({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                pull_number: context.payload.pull_request.number,\n                reviewers\n              });\n              \n            } catch (error) {\n              console.error('Error assigning reviewer:', error);\n            }\n"
  },
  {
    "path": ".github/workflows/stale-issue-cleanup.yml",
    "content": "name: close-stale-issues\n# Marks issues and PRs as stale after 30 days, then closes them if marked stale for 5 days\non:\n  schedule:\n    - cron: '30 1 * * *'\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/stale@v7\n        with:\n          stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove the `stale` label or add a comment, otherwise this issue will be closed in 5 days.'\n          stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove the `stale` label or add a comment, otherwise this PR will be closed in 5 days.'\n          exempt-issue-labels: 'future'\n          exempt-pr-labels: 'awaiting-approval, work-in-progress'\n          days-before-stale: 45\n          days-before-close: 5\n          operations: 100"
  },
  {
    "path": ".github/workflows/vscode-ext-release.yaml",
    "content": "name: vscode-ext-release\n# Releases the Devbox VSCode extension to the marketplace\n\nconcurrency: vscode-ext-release\n\non: workflow_dispatch\n\njobs:\n  build-publish:\n    runs-on: ubuntu-latest\n    environment: release\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n      - name: Setup NodeJS 24\n        uses: actions/setup-node@v5\n        with:\n          node-version: 24\n      - name: Install dependencies\n        run: |\n          npm install -g yarn\n          npm install -g vsce\n          npm install -g ovsx\n          yarn install\n        working-directory: vscode-extension\n      - name: publish-vs\n        run: |\n          vsce publish -p ${{ secrets.VS_MARKETPLACE_TOKEN }} --yarn --skip-duplicate\n        working-directory: vscode-extension\n      - name: publish-ovsx\n        run: |\n          sed -i 's/\"publisher\": \"jetpack-io\"/\"publisher\": \"Jetify\"/g' package.json\n          ovsx publish --pat ${{ secrets.OVSX_MARKETPLACE_TOKEN }} --yarn --skip-duplicate\n        working-directory: vscode-extension\n"
  },
  {
    "path": ".gitignore",
    "content": "# Global gitignore for the entire monorepo. Only add things here that truly\n# need to always be ignored regardless of project.\n#\n# If something is more specific to a particular project, add a gitignore in the\n# corresponding subdirectory.\n\n# MacOS filesystem\n.DS_Store\n\n# Editors\n.idea\n.vscode\n.zed\n\n# NodeJS\nnode_modules\n.yalc\ndist\n\n\n# Python\n*.pyc\n__pycache__/\n*.py[cod]\n*$py.class\n\n\n# Java\n*.class\n*.jar\n\n# devcontainer\n*.devcontainer\n\n# test specific files\n.test_tmp_*\n\n# deployment\n.vercel\n.yarn\n\n# Nix\nvendor/\nresult\n"
  },
  {
    "path": ".golangci.yml",
    "content": "linters:\n  disable-all: true\n  enable:\n    - dupl\n    - errcheck\n    - errorlint\n    - gofmt\n    - goimports\n    - gosimple\n    - govet\n    - importas\n    - ineffassign\n    - misspell\n    - nilerr\n    - reassign\n    - revive\n    - staticcheck\n    - stylecheck\n    - typecheck\n    - unconvert\n    - unparam\n    - unused\n    - usestdlibvars\n    - usetesting\n    - varnamelen\n    # - wrapcheck If we're going to use github.com/pkg/errors we should probably turn this on?\n    # We'd like to have the following linter enabled, but it's broken for Go\n    # 1.19 as of golangci-lint v1.48.0. Re-enable it when this issue is\n    # fixed: https://github.com/golangci/golangci-lint/issues/2649\n    # - structcheck\n\nlinters-settings:\n  errorlint:\n    errorf: false\n  revive:\n    rules: # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md\n      - name: atomic\n      - name: bare-return\n      - name: bool-literal-in-expr\n      - name: cognitive-complexity\n        exclude:\n          - \"**_test.go\"\n        arguments:\n          - 32 # TODO: gradually reduce it\n      - name: datarace\n      - name: duplicated-imports\n      - name: early-return\n      - name: error-return\n      - name: error-strings\n      - name: if-return\n      - name: indent-error-flow\n      - name: range-val-address\n      - name: receiver-naming\n      - name: time-naming\n      - name: var-naming\n      - name: unreachable-code\n  varnamelen:\n    max-distance: 10\n    ignore-decls:\n      - a []any\n      - c echo.Context\n      - const C\n      - e error\n      - e watch.Event\n      - f *foo.Bar\n      - f fmt.State\n      - i int\n      - id string\n      - m map[string]any\n      - m map[string]int\n      - n int\n      - ns string\n      - ok bool\n      - r *http.Request\n      - r io.Reader\n      - r *os.File\n      - re *regexp.Regexp\n      - sh *Shell\n      - sh *shell\n      - sh *shell.Shell\n      - sh shell\n      - T any\n      - t testing.T\n      - w http.ResponseWriter\n      - w io.Writer\n      - w *os.File\n  wrapcheck:\n    ignorePackageGlobs:\n      - go.jetify.com/devbox/*\n  misspell:\n    ignore-words:\n      - substituters\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "project_name: devbox\nbefore:\n  hooks:\n    - go mod tidy\nbuilds:\n  - main: ./cmd/devbox/main.go\n    binary: devbox\n    flags:\n      - -trimpath\n    mod_timestamp: \"{{ .CommitTimestamp }}\" # For reproducible builds\n    ldflags:\n      - -s -w\n      - -X go.jetify.com/devbox/internal/build.Version={{.Version}}\n      - -X go.jetify.com/devbox/internal/build.Commit={{.Commit}}\n      - -X go.jetify.com/devbox/internal/build.CommitDate={{.CommitDate}}\n      - -X go.jetify.com/devbox/internal/build.SentryDSN={{ .Env.SENTRY_DSN }}\n      - -X go.jetify.com/devbox/internal/build.TelemetryKey={{ .Env.TELEMETRY_KEY }}\n    env:\n      - CGO_ENABLED=0\n      - GO111MODULE=on\n    goos:\n      - linux\n      - darwin\n    goarch:\n      - 386\n      - amd64\n      - arm64\n      - arm\n    goarm:\n      - 7\narchives:\n  - files:\n      - no-files-will-match-* # Glob that does not match to create archive with only binaries.\n    name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if eq .Arch \"arm\" }}v{{ .Arm }}l{{ end }}'\nsnapshot:\n  name_template: \"{{ .Env.EDGE_TAG }}\"\nchecksum:\n  name_template: \"checksums.txt\"\n  algorithm: sha256\nrelease:\n  prerelease: auto\n  draft: true\n  github:\n    owner: jetify-com\n    name: devbox\nannounce:\n  discord:\n    # Whether its enabled or not.\n    # Defaults to false.\n    enabled: false\n\n    # Message template to use while publishing.\n    # Defaults to `{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}`\n    message_template: |\n      **New Release: Devbox {{.Tag}}**\n      We just released a version {{.Tag}} of `devbox`.\n\n      Description:\n      {{.TagBody}}\n\n      Release: {{.ReleaseURL}}\n\n      Updating:\n      If you installed devbox via our recommended installer\n      (`curl -fsSL https://get.jetpack.io/devbox | bash`) you will get the new version\n      _automatically_, the next time you run `devbox`\n\n      Thanks,\n      jetpack.io\n\n    # Set author of the embed.\n    # Defaults to `GoReleaser`\n    author: \"jetpack.io\"\n\n    # Color code of the embed. You have to use decimal numeral system, not hexadecimal.\n    # Defaults to `3888754` - the grey-ish from goreleaser\n    color: \"2622553\" #This is the Jetpack Space color\n\n    # URL to an image to use as the icon for the embed.\n    # Defaults to `https://goreleaser.com/static/avatar.png`\n    icon_url: \"\"\n"
  },
  {
    "path": ".schema/devbox-plugin.schema.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft-04/schema#\",\n  \"$id\": \"https://github.com/jetify-com/devbox/plugins\",\n  \"title\": \"Devbox Plugin Schema\",\n  \"description\": \"Defines fields and values for public devbox plugins\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"$schema\": {\n      \"description\": \"The schema version of this plugin file.\",\n      \"type\": \"string\"\n    },\n    \"name\": {\n      \"description\": \"The name of the plugin.\",\n      \"type\": \"string\"\n    },\n    \"version\": {\n      \"description\": \"The version of the plugin.\",\n      \"type\": \"string\"\n    },\n    \"description\": {\n      \"description\": \"A short description of the plugin and how it works. This will automatically display when the user first installs the plugin, or runs `devbox info`\",\n      \"type\": \"string\"\n    },\n    \"packages\": {\n      \"description\": \"Collection of packages to install\",\n      \"oneOf\": [\n        {\n          \"type\": \"array\",\n          \"items\": {\n            \"description\": \"Name and version of each package in name@version format.\",\n            \"type\": \"string\"\n          }\n        },\n        {\n          \"type\": \"object\",\n          \"description\": \"Name of each package in {\\\"name\\\": {\\\"version\\\": \\\"1.2.3\\\"}} format.\",\n          \"patternProperties\": {\n            \".*\": {\n              \"oneOf\": [\n                {\n                  \"type\": \"object\",\n                  \"description\": \"Version number of the specified package in {\\\"version\\\": \\\"1.2.3\\\"} format.\",\n                  \"properties\": {\n                    \"version\": {\n                      \"type\": \"string\",\n                      \"description\": \"Version of the package\"\n                    },\n                    \"platforms\": {\n                      \"type\": \"array\",\n                      \"description\": \"Names of platforms to install the package on. This package will be skipped for any platforms not on this list\",\n                      \"items\": {\n                        \"enum\": [\n                          \"i686-linux\",\n                          \"aarch64-linux\",\n                          \"aarch64-darwin\",\n                          \"x86_64-darwin\",\n                          \"x86_64-linux\",\n                          \"armv7l-linux\"\n                        ]\n                      }\n                    },\n                    \"excluded_platforms\": {\n                      \"type\": \"array\",\n                      \"description\": \"Names of platforms to exclude the package on\",\n                      \"items\": {\n                        \"enum\": [\n                          \"i686-linux\",\n                          \"aarch64-linux\",\n                          \"aarch64-darwin\",\n                          \"x86_64-darwin\",\n                          \"x86_64-linux\",\n                          \"armv7l-linux\"\n                        ]\n                      }\n                    },\n                    \"glibc_patch\": {\n                      \"type\": \"boolean\",\n                      \"description\": \"Whether to patch glibc to the latest available version for this package\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"string\",\n                  \"description\": \"Version of the package to install.\"\n                }\n              ]\n            }\n          }\n        }\n      ]\n    },\n    \"env\": {\n      \"type\": \"object\",\n      \"description\": \"List of additional environment variables to be set in the Devbox environment. These can be overridden by environment variables set in the user's devbox.json\",\n      \"patternProperties\": {\n        \".*\": {\n          \"type\": \"string\",\n          \"description\": \"Value of the environment variable.\"\n        }\n      }\n    },\n    \"create_files\": {\n      \"type\": \"object\",\n      \"description\": \"List of files to create in the user's project directory when the plugin is activated. The key points to the file path where the file will be created. The value points to the default file that should be copied to that location\",\n      \"patternProperties\": {\n        \".*\": {\n          \"type\": \"string\",\n          \"description\": \"Contents of the file.\"\n        }\n      }\n    },\n    \"shell\": {\n      \"type\": \"object\",\n      \"description\": \"Shell specific options and hooks for the plugin.\",\n      \"items\": {\n        \"init_hook\": {\n          \"type\": [\"array\", \"string\"],\n          \"description\": \"Shell command to run right before initializing the user's shell, running a script, or starting a service\"\n        },\n        \"scripts\": {\n          \"description\": \"List of command/script definitions to run with `devbox run <script_name>`.\",\n          \"type\": \"object\",\n          \"patternProperties\": {\n            \".*\": {\n              \"description\": \"Alias name for the script.\",\n              \"type\": [\"array\", \"string\"],\n              \"items\": {\n                \"type\": \"string\",\n                \"description\": \"The script's shell commands.\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"include\": {\n      \"description\": \"List of additional plugins to activate within your devbox shell\",\n      \"type\": \"array\",\n      \"items\": {\n        \"description\": \"Name of the plugin to activate.\",\n        \"type\": \"string\"\n      }\n    }\n  },\n  \"required\": [\"name\", \"version\", \"description\"]\n}\n"
  },
  {
    "path": ".schema/devbox.schema.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n    \"$id\": \"https://github.com/jetify-com/devbox\",\n    \"title\": \"Devbox json definition\",\n    \"description\": \"Defines fields and acceptable values of devbox.json\",\n    \"type\": \"object\",\n    \"properties\": {\n        \"$schema\": {\n            \"description\": \"The schema version of this devbox.json file.\",\n            \"type\": \"string\"\n        },\n        \"name\": {\n            \"description\": \"The name of the Devbox development environment.\",\n            \"type\": \"string\"\n        },\n        \"description\": {\n            \"description\": \"A description of the Devbox development environment.\",\n            \"type\": \"string\"\n        },\n        \"packages\": {\n            \"description\": \"Collection of packages to install\",\n            \"oneOf\": [\n                {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"description\": \"Name and version of each package in name@version format.\",\n                        \"type\": \"string\"\n                    }\n                },\n                {\n                    \"type\": \"object\",\n                    \"description\": \"Name of each package in {\\\"name\\\": {\\\"version\\\": \\\"1.2.3\\\"}} format.\",\n                    \"patternProperties\": {\n                        \".*\": {\n                            \"oneOf\": [\n                                {\n                                    \"type\": \"object\",\n                                    \"description\": \"Version number of the specified package in {\\\"version\\\": \\\"1.2.3\\\"} format.\",\n                                    \"properties\": {\n                                        \"version\": {\n                                            \"type\": \"string\",\n                                            \"description\": \"Version of the package\"\n                                        },\n                                        \"platforms\": {\n                                            \"type\": \"array\",\n                                            \"description\": \"Names of platforms to install the package on. This package will be skipped for any platforms not on this list\",\n                                            \"items\": {\n                                                \"enum\": [\n                                                    \"i686-linux\",\n                                                    \"aarch64-linux\",\n                                                    \"aarch64-darwin\",\n                                                    \"x86_64-darwin\",\n                                                    \"x86_64-linux\",\n                                                    \"armv7l-linux\"\n                                                ]\n                                            }\n                                        },\n                                        \"excluded_platforms\": {\n                                            \"type\": \"array\",\n                                            \"description\": \"Names of platforms to exclude the package on\",\n                                            \"items\": {\n                                                \"enum\": [\n                                                    \"i686-linux\",\n                                                    \"aarch64-linux\",\n                                                    \"aarch64-darwin\",\n                                                    \"x86_64-darwin\",\n                                                    \"x86_64-linux\",\n                                                    \"armv7l-linux\"\n                                                ]\n                                            }\n                                        },\n                                        \"glibc_patch\": {\n                                            \"type\": \"boolean\",\n                                            \"description\": \"Whether to patch glibc to the latest available version for this package\"\n                                        }\n                                    }\n                                },\n                                {\n                                    \"type\": \"string\",\n                                    \"description\": \"Version of the package to install.\"\n                                }\n                            ]\n                        }\n                    }\n                }\n            ]\n        },\n        \"env\": {\n            \"description\": \"List of additional environment variables to be set in the Devbox environment. Values containing $PATH or $PWD will be expanded. No other variable expansion or command substitution will occur.\",\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \".*\": {\n                    \"type\": \"string\",\n                    \"description\": \"Value of the environment variable.\"\n                }\n            }\n        },\n        \"shell\": {\n            \"description\": \"Definitions of scripts and actions to take when in devbox shell.\",\n            \"type\": \"object\",\n            \"properties\": {\n                \"init_hook\": {\n                    \"type\": [\n                        \"array\",\n                        \"string\"\n                    ],\n                    \"items\": {\n                        \"description\": \"List of shell commands/scripts to run right after devbox shell starts.\",\n                        \"type\": \"string\"\n                    }\n                },\n                \"scripts\": {\n                    \"description\": \"List of command/script definitions to run with `devbox run <script_name>`.\",\n                    \"type\": \"object\",\n                    \"patternProperties\": {\n                        \".*\": {\n                            \"description\": \"Alias name for the script.\",\n                            \"type\": [\n                                \"array\",\n                                \"string\"\n                            ],\n                            \"items\": {\n                                \"type\": \"string\",\n                                \"description\": \"The script's shell commands.\"\n                            }\n                        }\n                    }\n                }\n            },\n            \"additionalProperties\": false\n        },\n        \"include\": {\n            \"description\": \"List of additional plugins to activate within your devbox shell\",\n            \"type\": \"array\",\n            \"items\": {\n                \"description\": \"Name of the plugin to activate.\",\n                \"type\": \"string\"\n            }\n        },\n        \"env_from\": {\n            \"type\": \"string\"\n        },\n        \"nixpkgs\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"commit\": {\n                    \"type\": \"string\",\n                    \"description\": \"The commit hash of the nixpkgs repository to use\"\n                }\n            }\n        }\n    },\n    \"additionalProperties\": false\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, caste, color, religion, or sexual\nidentity and orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the overall\n  community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or advances of\n  any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email address,\n  without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement. Use the \"Report\nto repository admins\" functionality on GitHub to report.\n\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series of\nactions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or permanent\nban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within the\ncommunity.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.1, available at\n[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].\n\nCommunity Impact Guidelines were inspired by\n[Mozilla's code of conduct enforcement ladder][mozilla coc].\n\nFor answers to common questions about this code of conduct, see the FAQ at\n[https://www.contributor-covenant.org/faq][faq]. Translations are available at\n[https://www.contributor-covenant.org/translations][translations].\n\n[homepage]: https://www.contributor-covenant.org\n[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html\n[mozilla coc]: https://github.com/mozilla/diversity\n[faq]: https://www.contributor-covenant.org/faq\n[translations]: https://www.contributor-covenant.org/translations\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nWhen contributing to this repository, please describe the change you wish to\nmake via a related issue, or a pull request.\n\nPlease note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in\nall your interactions with the project.\n\n## Setting Up Development Environment\n\nBefore making any changes to the source code (documentation excluded) make sure\nyou have installed all the required tools.\n\n### With Devbox\n\nThe easiest way to develop Devbox is with Devbox!\n\n1. Install Devbox:\n\n       curl -fsSL https://get.jetify.com/devbox | bash\n\n2. Clone this repository:\n\n       git clone https://github.com/jetify-com/devbox.git go.jetify.com/devbox\n       cd go.jetify.com/devbox\n\n3. Build the Devbox CLI. If you don't have Nix installed, Devbox will\n   automatically install it for you before building:\n\n       devbox run build\n\n4. Start a development shell using your build of Devbox:\n\n       dist/devbox shell\n\nTip: you can also start VSCode from inside your Devbox shell with\n`devbox run code`.\n\n- If you encounter an error similar to: `line 3: command 'code' not found`, it\n  means you do not have the Visual Studio Code \"Shell Command\" installed. Follow\n  the official guide at https://code.visualstudio.com/docs/setup/mac. Please\n  refer to the section under: \"Launching from the command line\".\n\n### Setting up the Environment Without Devbox\n\nIf you are unable to install or use Devbox, you can manually replicate the\nenvironment by following the steps below.\n\n1. Install Nix Package Manager. We recommend using the\n   [Determinate Systems installer](https://github.com/DeterminateSystems/nix-installer):\n\n       curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install\n\n   Alternatively, you can also use\n   [the official installer](https://nixos.org/download.html).\n\n2. Install [Go](https://go.dev/doc/install) (current version: 1.20)\n\n3. Clone this repository and build Devbox:\n\n       git clone https://github.com/jetify-com/devbox.git go.jetify.com/devbox\n       cd go.jetify.com/devbox\n       go build ./cmd/devbox\n       ./devbox run -- echo hello, world\n\n## Pull Request Process\n\n1. For new features or non-trivial changes, consider first filing an issue to\n   discuss what changes you intend to make. This will let us help you with\n   implementation details and to make sure we don't duplicate any work.\n2. Ensure any new feature or functionality includes tests to verify its\n   correctness.\n3. Run `devbox run lint` and `devbox run test`.\n4. Run `go mod tidy` if you added any new dependencies.\n5. Submit your pull request and someone will take a look!\n\n### Style Guide\n\nWe don't expect you to read through a long style guide or be an expert in Go\nbefore contributing. When necessary, a reviewer will be happy to help out with\nany suggestions around code style when you submit your PR. Otherwise, the Devbox\ncodebase generally follows common Go idioms and patterns:\n\n- If you're unfamiliar with idiomatic Go,\n  [Effective Go](https://go.dev/doc/effective_go) and the\n  [Google Go Style Guide](https://google.github.io/styleguide/go) are good\n  resources.\n- There's no strict commit message format, but a good practice is to start the\n  subject with the name of the Go packages you add/modified. For example,\n  `boxcli: update help for add command`.\n\n## Community Contribution License\n\nContributions made to this project must be made under the terms of the\n[Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0).\n\n```\nBy making a contribution to this project, you certify that:\n\n  a. The contribution was created in whole or in part by you and you have the right\n  to submit it under the Apache 2 License; or\n\n  b. The contribution is based upon previous work that, to the best of your\n  knowledge, is covered under an appropriate open source license and you have the\n  right under that license to submit that work with modifications, whether\n  created in whole or in part by you, under the Apache 2 License; or\n\n  c. The contribution was provided directly to you by some other person who\n  certified (a), (b) or (c) and you have not modified it.\n\n  d. You understand and agree that this project and the contribution are public\n  and that a record of the contribution (including all personal information you\n  submit with it, including your sign-off) is maintained indefinitely and may be\n  redistributed consistent with this project or the open source license(s)\n  involved.\n```\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "NUSHELL.md",
    "content": "# Using Devbox with Nushell\n\nDevbox now supports [nushell](https://github.com/nushell/nushell) through the `--format` flag on the `shellenv` command.\n\n## Quick Start\n\n**Add this to `~/.config/nushell/env.nu`:**\n\n```nushell\ndevbox global shellenv --format nushell --preserve-path-stack -r\n  | lines \n  | parse \"$env.{name} = \\\"{value}\\\"\"\n  | where name != null \n  | transpose -r \n  | into record \n  | load-env\n```\n\nThis is equivalent to bash's `eval \"$(devbox global shellenv)\"` and runs on every fresh shell start.\n\n---\n\n## Global Configuration\n\nTo use devbox global packages with nushell, you need to load the environment similar to how bash/zsh use `eval \"$(devbox global shellenv)\"`.\n\n### Dynamic loading with `load-env` - eval equivalent\n\nAdd this to `~/.config/nushell/env.nu` to regenerate and load devbox environment fresh every time, just like bash's `eval`:\n\n```nushell\n# Load devbox global environment dynamically (equivalent to bash eval)\ndevbox global shellenv --format nushell --preserve-path-stack -r\n  | lines \n  | parse \"$env.{name} = \\\"{value}\\\"\"\n  | where name != null \n  | transpose -r \n  | into record \n  | load-env\n```\n\n- `--format nushell` - Output in nushell syntax\n- `--preserve-path-stack` - Maintain existing PATH order if devbox is already active\n- `-r` (recompute) - Always recompute the environment, prevents \"out of date\" warnings\n"
  },
  {
    "path": "README.md",
    "content": "<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"docs/app/static/img/devbox_logo_dark.svg\">\n <source media=\"(prefers-color-scheme: light)\" srcset=\"docs/app/static/img/devbox_logo_light.svg\">\n <img alt=\"Devbox logo.\" src=\"docs/app/static/img/devbox_logo_light.svg\">\n</picture>\n\n# Devbox\n\n### Instant, easy, and predictable development environments\n\n[![Join Discord](https://img.shields.io/discord/903306922852245526?color=7389D8&label=discord&logo=discord&logoColor=ffffff&cacheSeconds=1800)](https://discord.gg/jetify)\n![License: Apache 2.0](https://img.shields.io/github/license/jetify-com/devbox)\n[![version](https://img.shields.io/github/v/release/jetify-com/devbox?color=green&label=version&sort=semver)](https://github.com/jetify-com/devbox/releases)\n[![tests](https://github.com/jetify-com/devbox/actions/workflows/cli-post-release.yml/badge.svg)](https://github.com/jetify-com/devbox/actions/workflows/cli-release.yml?branch=main)\n[![Built with Devbox](https://www.jetify.com/img/devbox/shield_galaxy.svg)](https://www.jetify.com/devbox/docs/contributor-quickstart/)\n\n## What is it?\n\n[Devbox](https://www.jetify.com/devbox/) is a command-line tool that lets you\neasily create isolated shells for development. You start by defining the list of\npackages required by your development environment, and devbox uses that\ndefinition to create an isolated environment just for your application.\n\nIn practice, Devbox works similar to a package manager like `yarn` – except the\npackages it manages are at the operating-system level (the sort of thing you\nwould normally install with `brew` or `apt-get`). With Devbox, you can install\nover [400,000 package versions](https://www.nixhub.io) from the Nix Package\nRegistry\n\nDevbox was originally developed by [Jetify](https://www.jetify.com) and is\ninternally powered by `nix`.\n\n## Demo\n\nThe example below creates a development environment with `python 2.7` and\n`go 1.18`, even though those packages are not installed in the underlying\nmachine:\n\n![screen cast](https://user-images.githubusercontent.com/279789/186491771-6b910175-18ec-4c65-92b0-ed1a91bb15ed.svg)\n\n## Installing Devbox\n\nUse the following install script to get the latest version of Devbox:\n\n```sh\ncurl -fsSL https://get.jetify.com/devbox | bash\n```\n\nRead more on the\n[Devbox docs](https://www.jetify.com/devbox/docs/installing-devbox/).\n\n## Benefits\n\n### A consistent shell for everyone on the team\n\nDeclare the list of tools needed by your project via a `devbox.json` file and\nrun `devbox shell`. Everyone working on the project gets a shell environment\nwith the exact same version of those tools.\n\n### Try new tools without polluting your laptop\n\nDevelopment environments created by Devbox are isolated from everything else in\nyour laptop. Is there a tool you want to try without making a mess? Add it to a\nDevbox shell, and remove it when you don't want it anymore – all while keeping\nyour laptop pristine.\n\n### Don't sacrifice speed\n\nDevbox can create isolated environments right on your laptop, without an\nextra-layer of virtualization slowing your file system or every command. When\nyou're ready to ship, it'll turn it into an equivalent container – but not\nbefore.\n\n### Goodbye conflicting versions\n\nAre you working on multiple projects, all of which need different versions of\nthe same binary? Instead of attempting to install conflicting versions of the\nsame binary on your laptop, create an isolated environment for each project, and\nuse whatever version you want for each.\n\n### Take your environment with you\n\nDevbox's dev environments are _portable_. We make it possible to declare your\nenvironment exactly once, and use that single definition in several different\nways, including:\n\n- A local shell created through `devbox shell`\n- A devcontainer you can use with VSCode\n- A Dockerfile so you can build a production image with the exact same tools you\n  used for development.\n- A remote development environment in the cloud that mirrors your local\n  environment.\n\n## Quickstart: Fast, Deterministic Shell\n\nIn this quickstart we'll create a development shell with specific tools\ninstalled. These tools will only be available when using this Devbox shell,\nensuring we don't pollute your machine.\n\n1. Open a terminal in a new empty folder.\n\n2. Initialize Devbox:\n\n   ```bash\n   devbox init\n   ```\n\n   This creates a `devbox.json` file in the current directory. You should commit\n   it to source control.\n\n3. Add command-line tools from Nix. For example, to add Python 3.10:\n\n   ```bash\n   devbox add python@3.10\n   ```\n\n   Search for more packages on [Nixhub.io](https://www.nixhub.io)\n\n4. Your `devbox.json` file keeps track of the packages you've added, it should\n   now look like this:\n\n   ```json\n   {\n     \"packages\": [\n       \"python@3.10\"\n     ]\n   }\n   ```\n\n5. Start a new shell that has these tools installed:\n\n   ```bash\n   devbox shell\n   ```\n\n   You can tell you're in a Devbox shell (and not your regular terminal) because\n   the shell prompt changed.\n\n6. Use your favorite tools.\n\n   In this example we installed Python 3.10, so let's use it.\n\n   ```bash\n   python --version\n   ```\n\n7. Your regular tools are also available including environment variables and\n   config settings.\n\n   ```bash\n   git config --get user.name\n   ```\n\n8. To exit the Devbox shell and return to your regular shell:\n\n   ```bash\n   exit\n   ```\n\nRead more on the\n[Devbox docs Quickstart](https://www.jetify.com/devbox/docs/quickstart/).\n\n## Additional commands\n\n`devbox help` - see all commands\n\nSee the\n[CLI Reference](https://www.jetify.com/devbox/docs/cli_reference/devbox/) for\nthe full list of commands.\n\n## Join our Developer Community\n\n- Chat with us by joining the [Jetify Discord Server](https://discord.gg/jetify)\n  – we have a #devbox channel dedicated to this project.\n- File bug reports and feature requests using\n  [Github Issues](https://github.com/jetify-com/devbox/issues)\n- Follow us on [Jetify's Twitter](https://twitter.com/jetify_com) for product\n  updates\n\n## Contributing\n\nDevbox is an opensource project, so contributions are always welcome. Please read\n[our contributing guide](CONTRIBUTING.md) before submitting pull requests.\n\n[Devbox development readme](devbox.md)\n\n## Related Work\n\nThanks to [Nix](https://nixos.org/) for providing isolated shells.\n\n## License\n\nThis project is proudly open-source under the\n[Apache 2.0 License](https://github.com/jetify-com/devbox/blob/main/LICENSE)\n"
  },
  {
    "path": "cmd/devbox/main.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage main\n\nimport (\n\t\"go.jetify.com/devbox/internal/boxcli\"\n)\n\nfunc main() {\n\tboxcli.Main()\n}\n"
  },
  {
    "path": "devbox.go",
    "content": "// Package devbox creates and configures Devbox development environments.\npackage devbox\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n)\n\n// Devbox is a Devbox development environment.\ntype Devbox struct {\n\tdx *devbox.Devbox\n}\n\n// Open loads a Devbox environment from a config file or directory.\nfunc Open(path string) (*Devbox, error) {\n\tdx, err := devbox.Open(&devopt.Opts{\n\t\tDir:    path,\n\t\tStderr: io.Discard,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Devbox{dx: dx}, nil\n}\n\n// Install downloads and installs missing packages.\nfunc (d *Devbox) Install(ctx context.Context) error {\n\treturn d.dx.Install(ctx)\n}\n"
  },
  {
    "path": "devbox.json",
    "content": "{\n  \"name\":        \"devbox\",\n  \"description\": \"Instant, easy, and predictable development environments\",\n  \"packages\": {\n    \"fd\":  \"latest\",\n    \"git\": \"latest\",\n    \"go\":  \"latest\"\n  },\n  \"env\": {\n    \"GOENV\": \"off\",\n    \"PATH\":  \"$PWD/dist/tools:$PATH:$PWD/dist\",\n     // Disabling CGO is a workaround for a clang linker error in macos\n     // This should be okay, because Devbox doesn't require CGO\n     // https://github.com/NixOS/nixpkgs/issues/433688#issuecomment-3231557942\n    \"CGO_ENABLED\": \"0\",\n  },\n  \"shell\": {\n    \"init_hook\": [\n      // Remove Go environment variables that might've been inherited from the\n      // user's environment and could affect the build.\n      \"test -z $FISH_VERSION && \\\\\",\n      \"unset       GO111MODULE GOARCH GOFLAGS GOMOD GOOS GOROOT GOTOOLCHAIN GOWORK || \\\\\",\n      \"set --erase GO111MODULE GOARCH GOFLAGS GOMOD GOOS GOROOT GOTOOLCHAIN GOWORK\",\n      \"GOBIN=$PWD/dist/tools go install tool\"\n    ],\n    \"scripts\": {\n      // Build devbox for the current platform\n      \"build\":              \"go build -o dist/devbox ./cmd/devbox\",\n      \"build-darwin-amd64\": \"GOOS=darwin GOARCH=amd64 go build -o dist/devbox-darwin-amd64 ./cmd/devbox\",\n      \"build-darwin-arm64\": \"GOOS=darwin GOARCH=arm64 go build -o dist/devbox-darwin-arm64 ./cmd/devbox\",\n      \"build-linux-amd64\":  \"GOOS=linux GOARCH=amd64 go build -o dist/devbox-linux-amd64 ./cmd/devbox\",\n      \"build-linux-arm64\":  \"GOOS=linux GOARCH=arm64 go build -o dist/devbox-linux-arm64 ./cmd/devbox\",\n      \"build-all\": [\n        \"devbox run build-darwin-amd64\",\n        \"devbox run build-darwin-arm64\",\n        \"devbox run build-linux-amd64\",\n        \"devbox run build-linux-arm64\"\n      ],\n      // Open VSCode\n      \"code\":               \"code .\",\n      \"lint\":               \"go tool golangci-lint run --timeout 5m && scripts/gofumpt.sh\",\n      \"fmt\":                \"scripts/gofumpt.sh\",\n      \"test\":               \"go test -race -cover ./...\",\n      \"test-projects-only\": \"DEVBOX_RUN_PROJECT_TESTS=1 go test -v -timeout ${DEVBOX_GOLANG_TEST_TIMEOUT:-30m} ./... -run \\\"TestExamples|TestScriptsWithProjects\\\"\",\n      \"update-examples\":    \"devbox run build && go run testscripts/testrunner/updater/main.go\",\n      // Updates the Flake's vendorHash: First run `go mod vendor` to vendor\n      // the dependencies, then hash the vendor directory with Nix.\n      // The hash is saved to the `vendor-hash` file, which is then\n      // read into the Nix Flake.\n      \"update-hash\": [\n        // realpath to work-around nix hash not liking symlinks\n        \"vendor=$(realpath $(mktemp -d))\",\n        \"trap \\\"rm -rf $vendor\\\" EXIT\",\n        \"go mod vendor -o $vendor\",\n        \"nix hash path $vendor >vendor-hash\"\n      ],\n      \"build-flake\": \"nix build .\",\n      \"tidy\": [\"go mod tidy\", \"devbox run update-hash\"],\n      // docker-testscripts runs the testscripts with Docker to exercise\n      // Linux-specific tests. It invokes the test binary directly, so any extra\n      // test runner flags must have their \"-test.\" prefix.\n      //\n      // For example, to only run Python tests:\n      //\n      //   devbox run docker-testscripts -test.run ^TestScripts$/python\n      \"docker-testscripts\": [\n        \"cd testscripts\",\n\n        // The Dockerfile looks for a testscripts-$TARGETOS-$TARGETARCH binary\n        // to run the tests. Pre-compiling a static test binary lets us avoid\n        // polluting the container with a Go toolchain or shared libraries that\n        // might interfere with linker tests.\n        \"trap 'rm -f testscripts-linux-amd64 testscripts-linux-arm64' EXIT\",\n        \"GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go test -c -o testscripts-linux-amd64\",\n        \"GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go test -c -o testscripts-linux-arm64\",\n        \"image=$(docker build --quiet --tag devbox-testscripts-ubuntu:noble --platform linux/amd64 .)\",\n        \"docker run --rm --mount type=volume,src=devbox-testscripts-amd64,dst=/nix --platform linux/amd64 -e DEVBOX_RUN_FAILING_TESTS -e DEVBOX_RUN_PROJECT_TESTS -e DEVBOX_DEBUG $image \\\"$@\\\"\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "devbox.md",
    "content": "<!-- gen-readme start - generated by https://github.com/jetify-com/devbox/ -->\n\n# devbox\n\nInstant, easy, and predictable development environments\n\n## Getting Started\n\nThis project uses [devbox](https://github.com/jetify-com/devbox) to manage its\ndevelopment environment.\n\nInstall devbox:\n\n```sh\ncurl -fsSL https://get.jetify.com/devbox | bash\n```\n\nStart the devbox shell:\n\n```sh\ndevbox shell\n```\n\nRun a script in the devbox environment:\n\n```sh\ndevbox run <script>\n```\n\n## Scripts\n\nScripts are custom commands that can be run using this project's environment.\nThis project has the following scripts:\n\n- [devbox](#devbox)\n  - [Getting Started](#getting-started)\n  - [Scripts](#scripts)\n  - [Environment](#environment)\n  - [Shell Init Hook](#shell-init-hook)\n  - [Packages](#packages)\n  - [Script Details](#script-details)\n    - [devbox run build](#devbox-run-build)\n    - [devbox run build-all](#devbox-run-build-all)\n    - [devbox run build-darwin-amd64](#devbox-run-build-darwin-amd64)\n    - [devbox run build-darwin-arm64](#devbox-run-build-darwin-arm64)\n    - [devbox run build-linux-amd64](#devbox-run-build-linux-amd64)\n    - [devbox run build-linux-arm64](#devbox-run-build-linux-arm64)\n    - [devbox run code](#devbox-run-code)\n    - [devbox run fmt](#devbox-run-fmt)\n    - [devbox run lint](#devbox-run-lint)\n    - [devbox run test](#devbox-run-test)\n    - [devbox run tidy](#devbox-run-tidy)\n    - [devbox run update-examples](#devbox-run-update-examples)\n\n## Environment\n\n```sh\nGOENV=\"off\"\nPATH=\"$PATH:$PWD/dist\"\n```\n\n## Shell Init Hook\n\nThe Shell Init Hook is a script that runs whenever the devbox environment is\ninstantiated. It runs on `devbox shell` and on `devbox run`.\n\n```sh\ntest -z $FISH_VERSION && unset CGO_ENABLED GO111MODULE GOARCH GOFLAGS GOMOD GOOS GOROOT GOTOOLCHAIN GOWORK\n```\n\n## Packages\n\n- [go@latest](https://www.nixhub.io/packages/go)\n- [runx:golangci/golangci-lint@latest](https://www.github.com/golangci/golangci-lint)\n- [runx:mvdan/gofumpt@latest](https://www.github.com/mvdan/gofumpt)\n\n## Script Details\n\n### devbox run build\n\nBuild devbox for the current platform\n\n```sh\ngo build -o dist/devbox ./cmd/devbox\n```\n\n&ensp;\n\n### devbox run build-all\n\n```sh\ndevbox run build-darwin-amd64\ndevbox run build-darwin-arm64\ndevbox run build-linux-amd64\ndevbox run build-linux-arm64\n```\n\n&ensp;\n\n### devbox run build-darwin-amd64\n\n```sh\nGOOS=darwin GOARCH=amd64 go build -o dist/devbox-darwin-amd64 ./cmd/devbox\n```\n\n&ensp;\n\n### devbox run build-darwin-arm64\n\n```sh\nGOOS=darwin GOARCH=arm64 go build -o dist/devbox-darwin-arm64 ./cmd/devbox\n```\n\n&ensp;\n\n### devbox run build-linux-amd64\n\n```sh\nGOOS=linux GOARCH=amd64 go build -o dist/devbox-linux-amd64 ./cmd/devbox\n```\n\n&ensp;\n\n### devbox run build-linux-arm64\n\n```sh\nGOOS=linux GOARCH=arm64 go build -o dist/devbox-linux-arm64 ./cmd/devbox\n```\n\n&ensp;\n\n### devbox run code\n\nOpen VSCode\n\n```sh\ncode .\n```\n\n&ensp;\n\n### devbox run fmt\n\n```sh\nscripts/gofumpt.sh\n```\n\n&ensp;\n\n### devbox run lint\n\n```sh\ngolangci-lint run --timeout 5m && scripts/gofumpt.sh\n```\n\n&ensp;\n\n### devbox run test\n\n```sh\ngo test -race -cover ./...\n```\n\n&ensp;\n\n### devbox run tidy\n\n```sh\ngo mod tidy\n```\n\n&ensp;\n\n### devbox run update-examples\n\n```sh\ndevbox run build && go run testscripts/testrunner/updater/main.go\n```\n\n&ensp;\n\n<!-- gen-readme end -->\n"
  },
  {
    "path": "examples/.gitignore",
    "content": ".devbox/\n.DS_STORE\n.vscode/settings.json\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Devbox Examples\n\n[![Built with Devbox](https://www.jetify.com/img/devbox/shield_moon.svg)](https://www.jetify.com/devbox/docs/contributor-quickstart/)\n\nExample dev environments built with Devbox:\n\n1. `databases` - Examples of popular DBs (eg.,MariaDB, Postgres, Redis)\n1. `development` - Shells for developing in different programming languages\n1. `flakes` - Examples of using Nix Flakes with Devbox\n1. `servers` - Examples of servers, like Apache + Nginx\n1. `stacks` - Full projects and web stacks, like LAMP and Drupal\n"
  },
  {
    "path": "examples/cloud_development/argo-workflows/README.md",
    "content": "# Minikube + Argo Example\n\nRun and test Argo Workflows on a local Minkube instance.\n\nThe init_hook in this example configures minikube to store it's data in a local `home/` directory, so your host kubeconfig is not affected by this shell. The scripts in this example do the following:\n\n`minikube` starts minikube and tails it's logs\n`install-argo` Installs Argo Workflows based on the Argo Quickstart documentation\n`argo-port-forward` Forwards the port of the Argo deployment, so you can access the Argo UI at `https://localhost:2746` (note the `https`).\n\n## Usage Instructions\n\nNote: macOS users need to have Docker Desktop installed. This is because the Docker Daemon cannot run natively on macOS\n\n1. Start `minikube` by running `devbox run minikube`. This will install and spin up minikube in a local shell, and then tail the logs\n2. Install argo on minkube using `devbox run argo-install`\n3. Forward the ports from your argo deployment using `devbox run argo-port-forward`\n4. You can now run `devbox shell`, and use the Argo CLI to interact with Argo in minikube. You can also access the Argo UI at [https://localhost:2746](https://localhost:2746)\n"
  },
  {
    "path": "examples/cloud_development/argo-workflows/argo-patch.sh",
    "content": "# Patch so we don't need to login through the UI, for development purposes. See https://argoproj.github.io/argo-workflows/quick-start/ for details.\n\nkubectl patch deployment \\\n  argo-server \\\n  --namespace=argo \\\n  --type='json' \\\n  -p='[{\"op\": \"replace\", \"path\": \"/spec/template/spec/containers/0/args\", \"value\": [\n  \"server\",\n  \"--auth-mode=server\"\n]}]'"
  },
  {
    "path": "examples/cloud_development/argo-workflows/devbox.json",
    "content": "{\n  \"packages\": [\n    \"kubectl\",\n    \"argo\",\n    \"docker\",\n    \"minikube@latest\"\n  ],\n  \"env\": {\n    \"MINIKUBE_HOME\": \"$PWD/home/.minikube\",\n    \"KUBECONFIG\":    \"$PWD/home/.kube/config\"\n  },\n  \"shell\": {\n    \"init_hook\": [],\n    \"scripts\": {\n      \"argo-install\": [\n        \"kubectl create namespace argo\",\n        \"kubectl apply -n argo -f https://github.com/argoproj/argo-workflows/releases/download/v3.4.5/install.yaml\",\n        \"bash argo-patch.sh\"\n      ],\n      \"argo-port-forward\": [\n        \"kubectl -n argo port-forward deployment/argo-server 2746:2746\"\n      ],\n      \"minikube\": [\n        \"finish() { minikube -p argo-test stop; }\",\n        \"trap finish SIGTERM SIGINT EXIT\",\n        \"minikube start -p argo-test\",\n        \"minikube -p argo-test logs -f\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/cloud_development/maelstrom/README.md",
    "content": "# Maelstrom\n\n\nA Devbox for running [Maelstrom](https://github.com/jepsen-io/maelstrom) Tests. Maelstrom is a testing library for toy distributed systems built by @aphyr, useful for learning the basics and principals of building distributed systems\n\nYou should also check out the [Fly.io Distributed Systems Challenge](https://fly.io/dist-sys/)\n\n## Prerequisites\n\nIf you don't already have [Devbox](https://www.jetify.com/docs/devbox/installing-devbox/index), you can install it by running the following command:\n\n```bash\ncurl -s https://get.jetify.com/install.sh | bash\n```\n\nYou can skip this step if you're running on devbox.sh\n\n## Usage\n\n1. Install Maelstrom by running `devbox run install`. This should install Maelstrom 0.2.2 in a `maelstrom` subdirectory\n\n1. cd into the `maelstrom` directory and run `./maelstrom` to verify everything is working\n\n1. You can now follow the docs and run the tests in the Maelstrom Docs + Readme. You can use `glow` from the command line to browse the docs.\n\nThis shell includes Ruby 3.10 for running the Ruby Demos. To run demos in other languages, install the appropriate runtimes using `devbox add`. For example, to run the Python demos, use `devbox add python310`.\n"
  },
  {
    "path": "examples/cloud_development/maelstrom/devbox.json",
    "content": "{\n  \"packages\": [\n    \"graphviz\",\n    \"gnuplot\",\n    \"ruby_3_1\",\n    \"curl\",\n    \"glow\",\n    \"openjdk17@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \"clear\",\n      \"echo 'Welcome to the Maelstrom in Devbox Shell! \\n * Type `devbox run help` to get started.\\n * Type `devbox run install` to install Maelstrom.\\n * After installing Maelstrom, type `devbox run docs` to browse the Maelstrom docs.'\"\n    ],\n    \"scripts\": {\n      \"install\": [\n        \"tar xjf <(curl -L -k https://github.com/jepsen-io/maelstrom/releases/download/v0.2.2/maelstrom.tar.bz2)\"\n      ],\n      \"help\": [\n        \"glow README.md\"\n      ],\n      \"docs\": [\n        \"glow maelstrom/doc\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/cloud_development/minikube/README.md",
    "content": "# Minikube + Helm + Kubectl Example\n\nRun Helm + Kubernetes locally using Minikube in a Devbox shell.\n\nThe init_hook in this example configures minikube + helm to store their data in a local `home/` directory, so your host kubeconfig and helm repos are not affected by this shell.\n\n## Usage Instructions\n\nNote: macOS users need to have Docker Desktop installed. This is because the Docker Daemon cannot run natively on macOS\n\n1. Start `minikube` by running `devbox run minikube`. This will install and spin up minikube in a local shell, and then tail the logs\n2. In a different terminal, create a new shell with `devbox shell`.\n3. You can now deploy to minikube using `kubectl` or `helm`.\n4. To shutdown minikube, use CTRL-C to terminate the shell where you started minikube.\n"
  },
  {
    "path": "examples/cloud_development/minikube/devbox.json",
    "content": "{\n  \"packages\": [\n    \"kubectl\",\n    \"kubernetes-helm-wrapped\",\n    \"docker\",\n    \"minikube@latest\"\n  ],\n  \"env\": {\n    \"MINIKUBE_HOME\": \"$PWD/home/.minkube\",\n    \"KUBECONFIG\":    \"$PWD/home/.kube/config\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \"helm repo add my-repo https://charts.bitnami.com/bitnami\"\n    ],\n    \"scripts\": {\n      \"minikube\": [\n        \"finish() { minikube stop; }\",\n        \"trap finish SIGTERM SIGINT EXIT\",\n        \"minikube start\",\n        \"minikube logs -f\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/cloud_development/temporal/.envrc",
    "content": "# Automatically sets up your devbox environment whenever you cd into this\n# directory via our direnv integration:\n\nuse_devbox() {\n    watch_file devbox.json\n    if [ -f .devbox/gen/flake/flake.nix ]; then\n        DEVBOX_SHELL_ENABLED_BACKUP=$DEVBOX_SHELL_ENABLED\n        eval \"$(devbox shellenv --init-hook)\"\n        export DEVBOX_SHELL_ENABLED=$DEVBOX_SHELL_ENABLED_BACKUP\n    fi\n}\nuse devbox\n\n# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/\n# for more details\n"
  },
  {
    "path": "examples/cloud_development/temporal/.gitignore",
    "content": ".venv/"
  },
  {
    "path": "examples/cloud_development/temporal/README.md",
    "content": "# Temporal\n\n[![Built with Devbox](https://www.jetify.com/devbox/img/shield_galaxy.svg)](https://www.jetify.com/devbox/docs/contributor-quickstart/)\n\nExample devbox for testing and developing Temporal workflows using Temporalite and the Python Temporal SDK.\n\nFor more details, check out:\n\n* [Temporal.io](https://temporal.io/)\n* [Temporalite](https://github.com/temporalio/temporalite)\n* [Temporal Python SDK](https://github.com/temporalio/sdk-python)\n* [Temporal Python Samples](https://github.com/temporalio/samples-python)\n\n## Starting Temporal\n\n```bash\ndevbox run start-temporal\n```\n\nThis will start the temporalite server for testing.\n\n* You can view the WebUI at `localhost:8233`\n* By default, Temporal will listen for activities/requests on port `7233`\n\n## Starting a Devbox Shell\n\n```bash\ndevbox shell\n```\n\nThis will activate a virtual environment and install the Temporal Python SDK.\n\n## Testing the Temporal Workflows\n\nFrom inside your `devbox shell`\n\n```bash\ncd temporal_example/hello\npython run hello_activity.py\n```\n\nThis should start the workflow using temporalite.\n"
  },
  {
    "path": "examples/cloud_development/temporal/devbox.json",
    "content": "{\n  \"packages\": [\n    \"python310Packages.pip\",\n    \"python310Packages.pylint\",\n    \"python310Packages.black\",\n    \"python310Packages.isort\",\n    \"python310Packages.mypy\",\n    \"temporalite\",\n    \"temporal-cli\",\n    \"python310@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Setting flags to allow Python C extension compilation'\",\n      \"export NIX_CFLAGS_COMPILE=\\\"$NIX_CFLAGS_COMPILE $(cat $(dirname $(command -v clang))/../nix-support/libcxx-cxxflags)\\\"\",\n      \"echo 'Setting up virtual environment'\",\n      \". $VENV_DIR/bin/activate\"\n    ],\n    \"scripts\": {\n      \"start-temporal\": \"temporalite start --namespace default --log-level warn --log-format pretty --ephemeral\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/cloud_development/temporal/hello/.gitignore",
    "content": "__pycache__\n.poetry-cache/"
  },
  {
    "path": "examples/cloud_development/temporal/hello/__init__.py",
    "content": ""
  },
  {
    "path": "examples/cloud_development/temporal/hello/hello_activity.py",
    "content": "import asyncio\nimport logging\nfrom dataclasses import dataclass\nfrom datetime import timedelta\n\nfrom temporalio import activity, workflow\nfrom temporalio.client import Client\nfrom temporalio.worker import Worker\n\n\n# While we could use multiple parameters in the activity, Temporal strongly\n# encourages using a single dataclass instead which can have fields added to it\n# in a backwards-compatible way.\n@dataclass\nclass ComposeGreetingInput:\n    greeting: str\n    name: str\n\n\n# Basic activity that logs and does string concatenation\n@activity.defn\nasync def compose_greeting(input: ComposeGreetingInput) -> str:\n    activity.logger.info(\"Running activity with parameter %s\" % input)\n    return f\"{input.greeting}, {input.name}!\"\n\n\n# Basic workflow that logs and invokes an activity\n@workflow.defn\nclass GreetingWorkflow:\n    @workflow.run\n    async def run(self, name: str) -> str:\n        workflow.logger.info(\"Running workflow with parameter %s\" % name)\n        return await workflow.execute_activity(\n            compose_greeting,\n            ComposeGreetingInput(\"Hello\", name),\n            start_to_close_timeout=timedelta(seconds=10),\n        )\n\n\nasync def main():\n    # Uncomment the line below to see logging\n    # logging.basicConfig(level=logging.INFO)\n\n    # Start client\n    client = await Client.connect(\"localhost:7233\")\n\n    # Run a worker for the workflow\n    async with Worker(\n        client,\n        task_queue=\"hello-activity-task-queue\",\n        workflows=[GreetingWorkflow],\n        activities=[compose_greeting],\n    ):\n\n        # While the worker is running, use the client to run the workflow and\n        # print out its result. Note, in many production setups, the client\n        # would be in a completely separate process from the worker.\n        result = await client.execute_workflow(\n            GreetingWorkflow.run,\n            \"World\",\n            id=\"hello-activity-workflow-id\",\n            task_queue=\"hello-activity-task-queue\",\n        )\n        print(f\"Result: {result}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/cloud_development/temporal/hello/hello_cron.py",
    "content": "import asyncio\nfrom dataclasses import dataclass\nfrom datetime import timedelta\n\nfrom temporalio import activity, workflow\nfrom temporalio.client import Client\nfrom temporalio.worker import Worker\n\n\n@dataclass\nclass ComposeGreetingInput:\n    greeting: str\n    name: str\n\n\n@activity.defn\nasync def compose_greeting(input: ComposeGreetingInput) -> str:\n    return f\"{input.greeting}, {input.name}!\"\n\n\n@workflow.defn\nclass GreetingWorkflow:\n    @workflow.run\n    async def run(self, name: str) -> None:\n        result = await workflow.execute_activity(\n            compose_greeting,\n            ComposeGreetingInput(\"Hello\", name),\n            start_to_close_timeout=timedelta(seconds=10),\n        )\n        workflow.logger.info(\"Result: %s\", result)\n\n\nasync def main():\n    # Start client\n    client = await Client.connect(\"localhost:7233\")\n\n    # Run a worker for the workflow\n    async with Worker(\n        client,\n        task_queue=\"hello-cron-task-queue\",\n        workflows=[GreetingWorkflow],\n        activities=[compose_greeting],\n    ):\n\n        print(\"Running workflow once a minute\")\n\n        # While the worker is running, use the client to start the workflow.\n        # Note, in many production setups, the client would be in a completely\n        # separate process from the worker.\n        await client.start_workflow(\n            GreetingWorkflow.run,\n            \"World\",\n            id=\"hello-cron-workflow-id\",\n            task_queue=\"hello-cron-task-queue\",\n            cron_schedule=\"* * * * *\",\n        )\n\n        # Wait forever\n        await asyncio.Future()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/cloud_development/temporal/requirements.txt",
    "content": "temporalio==0.1b4\ncython"
  },
  {
    "path": "examples/cloud_development/temporal/tests/__init__.py",
    "content": ""
  },
  {
    "path": "examples/cloud_development/temporal/venvShellHook.sh",
    "content": "SOURCE_DATE_EPOCH=$(date +%s)\n\nif [ -d \"$VENV_DIR\" ]; then\n    echo \"Skipping venv creation, '${VENV_DIR}' already exists\"\nelse\n    echo \"Creating new venv environment in path: '${VENV_DIR}'\"\n    # Note that the module venv was only introduced in python 3, so for 2.7\n    # this needs to be replaced with a call to virtualenv\n    which python3\n    python3 -m venv \"$VENV_DIR\"\nfi\n"
  },
  {
    "path": "examples/data_science/R/.gitignore",
    "content": ".RData\n.Rhistory\n"
  },
  {
    "path": "examples/data_science/R/README.md",
    "content": "# R\n\n[R is a free software environment for statistical computing and graphics](https://www.r-project.org/).\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/data_science/R)\n\n## Adding R to your Project\n\n`devbox add R@4.4.1`, or in your `devbox.json` add:\n\n```json\n  \"packages\": [\n    \"R@4.4.1\"\n  ],\n```\n\nThis will install R in your shell. You can find other versions of R by running `devbox search R`.\nYou can also view the available versions on [Nixhub](https://www.nixhub.io/packages/R).\n\n## Installing Packages\n\n[CRAN](https://cran.r-project.org/) is the main repository of R packages.\nAll of the CRAN packages are also available on [Nixhub](https://www.nixhub.io/).\n\nYou can install packages by running `devbox add rPackages.package_name`, where `package_name` is the name of the package you would normally install with `install.packages()`.\nNote that for packages with a dot in the name you will need to replace the dot with an underscore, i.e. `data.table` -> `data_table` (see example below).\n\n```json\n{\n    \"packages\": [\n      \"R@4.4.1\",\n      \"rPackages.data_table@latest\",\n      \"rPackages.ggplot2@latest\",\n      \"rPackages.tidyverse@latest\"\n    ],\n}\n```\n\nYou can access these packages in your R scripts as usual with `library(data.table)``.\n\n## Example script\n\nIn this [example repo](https://github.com/jetify-com/devbox/tree/main/examples/data_science/R), after running `devbox shell`, you can start an R repl with `R` then create an example plot with `source(\"src/examplePlot.R\")`. \nAlternatively run `Rscript src/examplePlot.R`.\nThis will create an `Rplots.pdf` file.\n\n## Troubleshooting\n\nIf you get warnings like:\n\n> During startup - Warning messages:\n> 1: setting LC_CTYPE failed, using \"C\"\n> 2: Setting LC_COLLATE failed, using \"C\"\n> ...\n\nthen you need to set your locale.\nFind your locale (outside of a devbox shell) using `locale` in your terminal. You will see something like:\n\n> LANG=en_NZ.UTF-8\n> LANGUAGE=en_NZ:en\n> LC_CTYPE=\"en_NZ.UTF-8\"\n> ...\n\nTo set your locale, edit the `init_hook` array in the shell object in `devbox.json` to export two environment variables like below (using your specific locale):\n\n```json\n{\n  \"shell\": {\n    \"init_hook\": [\n      \"export LANG=en_NZ.UTF-8\",\n      \"export LC_ALL=en_NZ.UTF-8\"\n    ]\n  }\n}\n```\n"
  },
  {
    "path": "examples/data_science/R/devbox.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/jetify-com/devbox/0.10.6/.schema/devbox.schema.json\",\n  \"packages\": [\n    \"R@4.4.1\",\n    \"rPackages.data_table@latest\",\n    \"rPackages.ggplot2@latest\",\n    \"rPackages.tidyverse@latest\",\n    \"rPackages.dplyr@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ],\n    \"scripts\": {\n      \"test\": [\n        \"echo \\\"Error: no test specified\\\" && exit 1\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/data_science/R/src/examplePlot.R",
    "content": "# Load the required libraries\nlibrary(data.table)\nlibrary(ggplot2)\n\n# Create example data\ndata <- data.table(\n  x = rnorm(100),  # 100 random numbers from a normal distribution\n  y = rnorm(100)   # 100 random numbers from a normal distribution\n)\n\n# Plot the data using ggplot2\ndemoPlot <- ggplot(data, aes(x = x, y = y)) +\n  geom_point() +  # Create a scatter plot\n  theme_minimal() +  # Use a minimal theme\n  labs(title = \"Scatter Plot of Random Data\",\n       x = \"X Axis\",\n       y = \"Y Axis\")\n\nprint(demoPlot)\n"
  },
  {
    "path": "examples/data_science/README.md",
    "content": "# Data Science and ML Examples\n\nBasic data science and ML examples. These examples are WIP\n\nNote: Examples that require a GPU are currently configured for WSL2.0 only. Modifications are needed for them to use GPUs on Linux"
  },
  {
    "path": "examples/data_science/jupyter/.envrc",
    "content": "# Automatically sets up your devbox environment whenever you cd into this\n# directory via our direnv integration:\n\nuse_devbox() {\n    watch_file devbox.json\n    if [ -f .devbox/gen/flake/flake.nix ]; then\n        DEVBOX_SHELL_ENABLED_BACKUP=$DEVBOX_SHELL_ENABLED\n        eval \"$(devbox shellenv --init-hook)\"\n        export DEVBOX_SHELL_ENABLED=$DEVBOX_SHELL_ENABLED_BACKUP\n    fi\n}\nuse devbox\n\n# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/\n# for more details\n"
  },
  {
    "path": "examples/data_science/jupyter/.pdm-python",
    "content": "/Users/daniel/Code/jetify/devbox/examples/data_science/jupyter/.venv/bin/python"
  },
  {
    "path": "examples/data_science/jupyter/.pdm.toml",
    "content": "[python]\npath = \"/nix/store/7rjqb838snvvxcmpvck1smfxhkwzqal5-python3-3.10.10/bin/python3.10\"\n"
  },
  {
    "path": "examples/data_science/jupyter/__pypackages__/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "examples/data_science/jupyter/devbox.json",
    "content": "{\n  \"packages\": [\n    \"pdm\",\n    \"nodePackages.pyright\",\n    \"python311Packages.jupyter\",\n    \"python311Packages.virtualenv\",\n    \"python311@latest\"\n  ],\n  \"env\": {\n    \"PATH\":             \"$PATH:$PWD/__pypackages__/3.11/bin\",\n    \"PDM_CHECK_UPDATE\": \"0\",\n    \"PYTHONPATH\":       \"$PYTHONPATH:$PWD/__pypackages__/3.11/lib\"\n  },\n  \"shell\": {\n    \"init_hook\": null\n  }\n}\n"
  },
  {
    "path": "examples/data_science/jupyter/main.py",
    "content": ""
  },
  {
    "path": "examples/data_science/jupyter/pyproject.toml",
    "content": "[project]\nname = \"\"\nversion = \"\"\ndescription = \"\"\nauthors = [\n    {name = \"John Lago\", email = \"750845+Lagoja@users.noreply.github.com\"},\n]\ndependencies = [\n    \"jupyter>=1.0.0\",\n]\nrequires-python = \">=3.10\"\nlicense = {text = \"MIT\"}\n\n\n[tool.pdm]\n"
  },
  {
    "path": "examples/data_science/llama/README.md",
    "content": "# Llama build and run\n\nSimple Llama (generative AI) build and run with Devbox.\n\n## Setup\n\n- Make sure to have [devbox installed](https://www.jetify.com/devbox/docs/quickstart/#install-devbox)\n- Clone this repo: `git clone https://github.com/jetify-com/devbox.git`\n- `cd devbox/examples/data_science/llama/`\n- `devbox shell`\n- Once in devbox shell, there will be an available binary `llama` that you can use to run the built llama.cpp.\n- `devbox run get_model`\n- `devbox run llama`\n\n## Updating the model\n\nThis example downloads [vicuna-7b model](https://huggingface.co/eachadea/ggml-vicuna-7b-1.1). You can change it to download another Llama model by editing the devbox.json\n\n## Using Llama\n\n`devbox run llama` runs the llama binary with a \"hello world\" prompt. To change that you can edit the prompt in devbox.json or once in devbox shell, run\n\n```bash\nllama -m ./models/vic7B/ggml-vic7b-q5_0.bin -n 512 -p \"your custom prompt\"\n```\n\nFor more details on llama inference parameters refer to [llama.cpp docs](https://github.com/ggerganov/llama.cpp). Note that, instead of running `./main` you can run `llama` inside devbox shell.\n"
  },
  {
    "path": "examples/data_science/llama/devbox.json",
    "content": "{\n  \"packages\": [\n    \"github:ggerganov/llama.cpp\",\n    \"wget@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": null,\n    \"scripts\": {\n      \"get_model\": [\n        \"mkdir -p models/vic7B/\",\n        \"cd models/vic7B\",\n        \"wget https://huggingface.co/eachadea/ggml-vicuna-7b-1.1/resolve/main/ggml-vic7b-q5_0.bin\"\n      ],\n      \"llama\": \"llama -m ./models/vic7B/ggml-vic7b-q5_0.bin -n 512 -p \\\"hello world\\\"\"\n    }\n  },\n  \"nixpkgs\": {\n    \"commit\": \"f80ac848e3d6f0c12c52758c0f25c10c97ca3b62\"\n  }\n}\n"
  },
  {
    "path": "examples/data_science/pytorch/basic-example/README.md",
    "content": "# Basic Pytorch Example\n\nThis is a Devbox for running PyTorch. \n\nNote: This demo currently only works on WSL3 with Nvidia GPUs. For Linux, additional packages and configuration may be needed."
  },
  {
    "path": "examples/data_science/pytorch/basic-example/devbox.json",
    "content": "{\n  \"packages\": {\n    \"poetry\":           \"\",\n    \"stdenv.cc.cc.lib\": \"\",\n    \"cudatoolkit\": {\n      \"version\":            \"latest\",\n      \"excluded_platforms\": [\"aarch64-darwin\", \"x86_64-darwin\"]\n    }\n  },\n  \"env\": {},\n  \"shell\": {\n    \"init_hook\": [],\n    \"scripts\": {\n      \"install\": \"poetry install\",\n      \"main\":    \"poetry run python main.py\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/data_science/pytorch/basic-example/main.py",
    "content": "import torch\n\ndef create_arrays(n):\n    x = torch.ones(n, n)\n    y = torch.randn(n, n * 2)\n    return x , y\n\n\ndef main():\n    x, y = create_arrays(1000)\n    x = x.to(\"cuda\")\n    y = y.to(\"cuda\")\n    z = x @ y\n    print(z)\n\nif __name__ == \"__main__\":\n    main()"
  },
  {
    "path": "examples/data_science/pytorch/basic-example/pyproject.toml",
    "content": "[tool.poetry]\nname = \"devbox-cuda-dev\"\nversion = \"0.1.0\"\ndescription = \"\"\nauthors = [\"jetify.com\"]\nreadme = \"README.md\"\npackages = [{include = \"devbox_cuda_dev\"}]\n\n[tool.poetry.dependencies]\npython = \"^3.10\"\ntorch = \"^2.7.0\"\nnumpy = \"^1.24.2\"\n\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "examples/data_science/pytorch/gradio/.gitignore",
    "content": "flagged/"
  },
  {
    "path": "examples/data_science/pytorch/gradio/README.md",
    "content": "# Gradio + Pytorch\n\nThis is a Devbox for running the [Pictionary](https://huggingface.co/spaces/gradio/pictionary/tree/main) demo by [@aliabd](https://github.com/aliabd) that can be found on Hugging Face Spaces. \n\nNote: This demo currently only works on WSL3 with Nvidia GPUs. For Linux, additional packages and configuration may be needed."
  },
  {
    "path": "examples/data_science/pytorch/gradio/class_names.txt",
    "content": "airplane\nalarm_clock\nanvil\napple\naxe\nbaseball\nbaseball_bat\nbasketball\nbeard\nbed\nbench\nbicycle\nbird\nbook\nbread\nbridge\nbroom\nbutterfly\ncamera\ncandle\ncar\ncat\nceiling_fan\ncell_phone\nchair\ncircle\nclock\ncloud\ncoffee_cup\ncookie\ncup\ndiving_board\ndonut\ndoor\ndrums\ndumbbell\nenvelope\neye\neyeglasses\nface\nfan\nflower\nfrying_pan\ngrapes\nhammer\nhat\nheadphones\nhelmet\nhot_dog\nice_cream\nkey\nknife\nladder\nlaptop\nlight_bulb\nlightning\nline\nlollipop\nmicrophone\nmoon\nmountain\nmoustache\nmushroom\npants\npaper_clip\npencil\npillow\npizza\npower_outlet\nradio\nrainbow\nrifle\nsaw\nscissors\nscrewdriver\nshorts\nshovel\nsmiley_face\nsnake\nsock\nspider\nspoon\nsquare\nstar\nstop_sign\nsuitcase\nsun\nsword\nsyringe\nt-shirt\ntable\ntennis_racquet\ntent\ntooth\ntraffic_light\ntree\ntriangle\numbrella\nwheel\nwristwatch"
  },
  {
    "path": "examples/data_science/pytorch/gradio/devbox.json",
    "content": "{\n  \"packages\": [\n    \"python310Packages.pip\",\n    \"stdenv.cc.cc.lib\",\n    \"python310@latest\"\n  ],\n  \"env\": {},\n  \"shell\": {\n    \"init_hook\": [\n      \". $VENV_DIR/bin/activate\",\n      \"pip install -r requirements.txt\"\n    ]\n  },\n  \"nixpkgs\": {\n    \"commit\": \"f80ac848e3d6f0c12c52758c0f25c10c97ca3b62\"\n  }\n}\n"
  },
  {
    "path": "examples/data_science/pytorch/gradio/requirements.txt",
    "content": "torch\ngradio\npathlib\ngdown"
  },
  {
    "path": "examples/data_science/pytorch/gradio/run.py",
    "content": "from pathlib import Path\n\nimport torch\nimport gradio as gr\nfrom torch import nn\nimport gdown \n\nurl = 'https://drive.google.com/uc?id=1dsk2JNZLRDjC-0J4wIQX_FcVurPaXaAZ'\noutput = 'pytorch_model.bin'\ngdown.download(url, output, quiet=False)\n\nLABELS = Path('class_names.txt').read_text().splitlines()\n\nmodel = nn.Sequential(\n    nn.Conv2d(1, 32, 3, padding='same'),\n    nn.ReLU(),\n    nn.MaxPool2d(2),\n    nn.Conv2d(32, 64, 3, padding='same'),\n    nn.ReLU(),\n    nn.MaxPool2d(2),\n    nn.Conv2d(64, 128, 3, padding='same'),\n    nn.ReLU(),\n    nn.MaxPool2d(2),\n    nn.Flatten(),\n    nn.Linear(1152, 256),\n    nn.ReLU(),\n    nn.Linear(256, len(LABELS)),\n)\nstate_dict = torch.load('pytorch_model.bin', map_location='cpu')\nmodel.load_state_dict(state_dict, strict=False)\nmodel.eval()\n\ndef predict(input):\n    im = input\n    if im is None:\n        return None\n        \n    x = torch.tensor(im, dtype=torch.float32).unsqueeze(0).unsqueeze(0) / 255.\n\n    with torch.no_grad():\n        out = model(x)\n\n    probabilities = torch.nn.functional.softmax(out[0], dim=0)\n\n    values, indices = torch.topk(probabilities, 5)\n\n    return {LABELS[i]: v.item() for i, v in zip(indices, values)}\n\n\ninterface = gr.Interface(predict, inputs=gr.templates.Sketchpad(label=\"Draw Here\"), outputs=gr.Label(label=\"Guess\"), theme=\"default\", css=\".footer{display:none !important}\", live=True)\ninterface.launch(enable_queue=False)"
  },
  {
    "path": "examples/data_science/tensorflow/README.md",
    "content": "# Tensorflow application\n\nSimple Tensorflow sample project with Devbox.\n\n## Setup\n\n- Make sure to have [devbox installed](https://www.jetify.com/devbox/docs/quickstart/#install-devbox)\n- Clone this repo: `git clone https://github.com/jetify-com/devbox.git`\n- `cd devbox/examples/data_science/tensorflow/`\n- `devbox shell`\n- `python3 main.py`\n"
  },
  {
    "path": "examples/data_science/tensorflow/devbox.json",
    "content": "{\n  \"packages\": [\n    \"python310Packages.pip\",\n    \"stdenv.cc.cc.lib\",\n    \"python310@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \". $VENV_DIR/bin/activate\",\n      \"pip install -r requirements.txt\"\n    ]\n  },\n  \"nixpkgs\": {\n    \"commit\": \"f80ac848e3d6f0c12c52758c0f25c10c97ca3b62\"\n  }\n}\n"
  },
  {
    "path": "examples/data_science/tensorflow/main.py",
    "content": "import tensorflow as tf\n\n# Simple python script using Tensorflow\nprint(tf.reduce_sum(tf.random.normal([1000, 1000])))\n"
  },
  {
    "path": "examples/data_science/tensorflow/requirements.txt",
    "content": "tf-nightly"
  },
  {
    "path": "examples/databases/mariadb/.gitignore",
    "content": "conf/mysql/data\n*.log\n*.pid\n*.sock"
  },
  {
    "path": "examples/databases/mariadb/README.md",
    "content": "# mariadb\n\n## mariadb Notes\n\n1. Start the mariadb server using `devbox services up`\n1. Create a database using `\"mysql --socket-path=$MYSQL_UNIX_PORT --password='' < setup_db.sql\"`\n1. You can now connect to the database from the command line by running `devbox run connect_db`\n\n## Services\n\n* mariadb\n\nUse `devbox services start|stop [service]` to interact with services\n\n## This plugin sets the following environment variables\n\n* MYSQL_BASEDIR=/<projectDir>/.devbox/nix/profile/default\n* MYSQL_HOME=/<projectDir>/.devbox/virtenv/mariadb/run\n* MYSQL_DATADIR=/<projectDir>/.devbox/virtenv/mariadb/data\n* MYSQL_UNIX_PORT=/<projectDir>/.devbox/virtenv/mariadb/run/mysql.sock\n* MYSQL_PID_FILE=/<projectDir>/.devbox/virtenv/mariadb/run/mysql.pid\n\nTo show this information, run `devbox info mariadb`\n\nNote that the `.sock` filepath can only be maximum 100 characters long. You can point to a different path by setting the `MYSQL_UNIX_PORT` env variable in your `devbox.json`.\n"
  },
  {
    "path": "examples/databases/mariadb/devbox.json",
    "content": "{\n  \"packages\": [\n    \"mariadb@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": [],\n    \"scripts\": {\n      \"connect_db\": [\n        \"mysql -u devbox_user -p -D devbox_lamp\"\n      ],\n      \"test_db_setup\": [\n        \"mkdir -p /tmp/devbox/mariadb/run\",\n        \"devbox services up -b\",\n        \"sleep 5\",\n        \"mysql -u root --password='' < setup_db.sql\",\n        \"devbox services stop\"\n      ]\n    }\n  },\n  \"nixpkgs\": {\n    \"commit\": \"f80ac848e3d6f0c12c52758c0f25c10c97ca3b62\"\n  },\n  \"env\": {\n   }\n}\n"
  },
  {
    "path": "examples/databases/mariadb/setup_db.sql",
    "content": "--- You should run this query using `mysql -u root < setup_db.sql`\n\nDROP DATABASE IF EXISTS devbox_lamp;\nCREATE DATABASE devbox_lamp;\n\nUSE devbox_lamp\n\nCREATE USER 'devbox_user'@'localhost' IDENTIFIED BY 'password';\nGRANT ALL PRIVILEGES ON devbox_lamp.* TO 'devbox_user'@'localhost' IDENTIFIED BY 'password';\n\nDROP TABLE IF EXISTS colors;\nCREATE TABLE colors (\n\tid INT NOT NULL AUTO_INCREMENT,\n\tname VARCHAR(100) NOT NULL,\n\thex VARCHAR(7) NOT NULL,\n\tPRIMARY KEY (id));\n\nINSERT INTO colors (name, hex) VALUES ('red', '#FF0000'), ('blue', '#0000FF'), ('green', '#00FF00');\n\n\n"
  },
  {
    "path": "examples/databases/mysql/README.md",
    "content": "# mysql\n\n## mysql Notes\n\n1. Start the mysql server using `devbox services up`\n1. Create a database using `\"mysql -u root --password='' < setup_db.sql\"`\n1. You can now connect to the database from the command line by running `devbox run connect_db`\n\n## Services\n\n* mysql\n\nUse `devbox services start|stop [service]` to interact with services\n\n## This plugin sets the following environment variables\n\n* MYSQL_BASEDIR=&lt;projectDir>/.devbox/nix/profile/default\n* MYSQL_HOME=&lt;projectDir>/.devbox/virtenv/mysql/run\n* MYSQL_DATADIR=&lt;projectDir>/.devbox/virtenv/mysql/data\n* MYSQL_UNIX_PORT=&lt;projectDir>/.devbox/virtenv/mysql/run/mysql.sock\n* MYSQL_PID_FILE=&lt;projectDir>/.devbox/virtenv/mysql/run/mysql.pid\n\nTo show this information, run `devbox info mysql`\n\nNote that the `.sock` filepath can only be maximum 100 characters long. You can point to a different path by setting the `MYSQL_UNIX_PORT` env variable in your `devbox.json`.\n"
  },
  {
    "path": "examples/databases/mysql/devbox.d/mysql/my.cnf",
    "content": "# MySQL configuration file\n\n[mariadbd]\n# Change this port if 3306 is already used\n#port = 3306\nlog_error=mysql.log\n"
  },
  {
    "path": "examples/databases/mysql/devbox.d/mysql80/my.cnf",
    "content": "# MySQL configuration file\n\n# [mysqld]\n# skip-log-bin\n# Change this port if 3306 is already used\n#port = 3306\n"
  },
  {
    "path": "examples/databases/mysql/devbox.d/mysql84/my.cnf",
    "content": "# MySQL configuration file\n\n# [mysqld]\n# skip-log-bin\n# Change this port if 3306 is already used\n#port = 3306\n"
  },
  {
    "path": "examples/databases/mysql/devbox.json",
    "content": "{\n  \"packages\": [\"mysql84@latest\"],\n  \"shell\": {\n    \"init_hook\": [],\n    \"scripts\": {\n      \"connect_db\": [\n        \"mysql -u devbox_user -p -D devbox_lamp\",\n      ],\n      \"test_db_setup\": [\n        \"mkdir -p /tmp/devbox/mariadb/run\",\n        \"devbox services up -b\",\n        \"sleep 5\",\n        \"mysql -u root --password='' < setup_db.sql\",\n        \"devbox services stop\",\n      ],\n    },\n  },\n}\n"
  },
  {
    "path": "examples/databases/mysql/my.cnf",
    "content": "[mysqld]\n# Use a different port to avoid conflicts\nport = 3307\n\n# Basic settings\ndatadir = .devbox/virtenv/mysql80/data\npid-file = .devbox/virtenv/mysql80/run/mysql.pid\nlog-error = .devbox/virtenv/mysql80/run/mysql-error.log\nsocket = .devbox/virtenv/mysql80/run/mysql.sock\nlc_messages_dir = .devbox/virtenv/mysql80/share/mysql/\nlc_messages = en_US\n\n# Disable X Plugin which also causes conflicts\nmysqlx = 0\n\n# Performance settings\nmax_connections = 100\nkey_buffer_size = 16M\nmax_allowed_packet = 16M\n\n[client]\nsocket = .devbox/virtenv/mysql80/run/mysql.sock\n"
  },
  {
    "path": "examples/databases/mysql/setup_db.sql",
    "content": "-- You should run this query using `mysql -u root < setup_db.sql`\n\nDROP DATABASE IF EXISTS devbox_lamp;\nCREATE DATABASE devbox_lamp;\n\nUSE devbox_lamp;\n\nCREATE USER 'devbox_user'@'localhost' IDENTIFIED BY 'password';\nGRANT ALL ON devbox_lamp.* TO 'devbox_user'@'localhost';\n\nDROP TABLE IF EXISTS colors;\nCREATE TABLE colors (\n\tid INT NOT NULL AUTO_INCREMENT,\n\tname VARCHAR(100) NOT NULL,\n\thex VARCHAR(7) NOT NULL,\n\tPRIMARY KEY (id));\n\nINSERT INTO colors (name, hex) VALUES ('red', '#FF0000'), ('blue', '#0000FF'), ('green', '#00FF00');\n\n\n"
  },
  {
    "path": "examples/databases/postgres/.gitignore",
    "content": "*.log\n*.pid\n*.sock\ndata/"
  },
  {
    "path": "examples/databases/postgres/README.md",
    "content": "# postgresql-14.6\n\n## postgresql Notes\n\nYou need to initialize and create a database as part of your setup.\n\n1. Initialize a DB by running `initdb`\n1. Start the Postgres server using `devbox services up`\n1. Create a database using `createdb <name_of_db>`\n1. You can now connect to the database from the command line by running `psql <name_of_db>`\n\nTo start the database manually run `pg_ctl -l .devbox/conf/postgresql/logfile start`.\nTo stop use `pg_ctl stop`.\n\n## Services\n\n* postgresql\n\nUse `devbox services start|stop [service]` to interact with services\n\n## This plugin sets the following environment variables\n\n* PGDATA=/<projectDir>/.devbox/conf/postgresql/data\n* PGHOST=/<projectDir>/.devbox/virtenv/postgresql\n\nTo show this information, run `devbox info postgresql`\n"
  },
  {
    "path": "examples/databases/postgres/devbox.json",
    "content": "{\n  \"packages\": {\n    \"postgresql\": \"latest\"\n  },\n  \"shell\": {\n    \"init_hook\": null\n  }\n}\n"
  },
  {
    "path": "examples/databases/postgres/setup_postgres_db.sql",
    "content": "--- You should run this query using psql < setup_db.sql`\n\nDROP DATABASE IF EXISTS devbox_lamp;\nCREATE DATABASE devbox_lamp;\n\nCREATE USER devbox_user WITH PASSWORD 'password';\n\nDROP TABLE IF EXISTS colors;\nCREATE TABLE colors (\n\tid SERIAL NOT NULL PRIMARY KEY,\n\tname VARCHAR(100) NOT NULL,\n\thex VARCHAR(7) NOT NULL);\n\nINSERT INTO colors (name, hex) VALUES ('red', '#FF0000'), ('blue', '#0000FF'), ('green', '#00FF00');\n\nGRANT ALL PRIVILEGES ON colors TO devbox_user;\n"
  },
  {
    "path": "examples/databases/redis/README.md",
    "content": "# redis-7.0.5\n\n## redis Notes\n\nRunning `devbox services start redis` will start redis as a daemon in the background.\n\nYou can manually start Redis in the foreground by running `redis-server $REDIS_CONF --port $REDIS_PORT`.\n\nLogs, pidfile, and data dumps are stored in `.devbox/virtenv/redis`. You can change this by modifying the `dir` directive in `devbox.d/redis/redis.conf`\n\n## Services\n\n* redis\n\nUse `devbox services start|stop [service]` to interact with services\n\n## This plugin creates the following helper files\n\n* ./devbox.d/redis/redis.conf\n\n## This plugin sets the following environment variables\n\n* REDIS_PORT=6379\n* REDIS_CONF=./devbox.d/redis/redis.conf\n\nTo show this information, run `devbox info redis`\n"
  },
  {
    "path": "examples/databases/redis/devbox.d/redis/redis.conf",
    "content": "# Redis configuration file example.\n#\n# Note that in order to read the configuration file, Redis must be\n# started with the file path as first argument:\n#\n# ./redis-server /path/to/redis.conf\n\n# Note on units: when memory size is needed, it is possible to specify\n# it in the usual form of 1k 5GB 4M and so forth:\n#\n# 1k => 1000 bytes\n# 1kb => 1024 bytes\n# 1m => 1000000 bytes\n# 1mb => 1024*1024 bytes\n# 1g => 1000000000 bytes\n# 1gb => 1024*1024*1024 bytes\n#\n# units are case insensitive so 1GB 1Gb 1gB are all the same.\n\n################################## INCLUDES ###################################\n\n# Include one or more other config files here.  This is useful if you\n# have a standard template that goes to all Redis servers but also need\n# to customize a few per-server settings.  Include files can include\n# other files, so use this wisely.\n#\n# Notice option \"include\" won't be rewritten by command \"CONFIG REWRITE\"\n# from admin or Redis Sentinel. Since Redis always uses the last processed\n# line as value of a configuration directive, you'd better put includes\n# at the beginning of this file to avoid overwriting config change at runtime.\n#\n# If instead you are interested in using includes to override configuration\n# options, it is better to use include as the last line.\n#\n# include /path/to/local.conf\n# include /path/to/other.conf\n\n################################## MODULES #####################################\n\n# Load modules at startup. If the server is not able to load modules\n# it will abort. It is possible to use multiple loadmodule directives.\n#\n# loadmodule /path/to/my_module.so\n# loadmodule /path/to/other_module.so\n\n################################## NETWORK #####################################\n\n# By default, if no \"bind\" configuration directive is specified, Redis listens\n# for connections from all the network interfaces available on the server.\n# It is possible to listen to just one or multiple selected interfaces using\n# the \"bind\" configuration directive, followed by one or more IP addresses.\n#\n# Examples:\n#\n# bind 192.168.1.100 10.0.0.1\n# bind 127.0.0.1 ::1\n#\n# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the\n# internet, binding to all the interfaces is dangerous and will expose the\n# instance to everybody on the internet. So by default we uncomment the\n# following bind directive, that will force Redis to listen only into\n# the IPv4 lookback interface address (this means Redis will be able to\n# accept connections only from clients running into the same computer it\n# is running).\n#\n# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES\n# JUST COMMENT THE FOLLOWING LINE.\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nbind 127.0.0.1\n\n# Protected mode is a layer of security protection, in order to avoid that\n# Redis instances left open on the internet are accessed and exploited.\n#\n# When protected mode is on and if:\n#\n# 1) The server is not binding explicitly to a set of addresses using the\n#    \"bind\" directive.\n# 2) No password is configured.\n#\n# The server only accepts connections from clients connecting from the\n# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain\n# sockets.\n#\n# By default protected mode is enabled. You should disable it only if\n# you are sure you want clients from other hosts to connect to Redis\n# even if no authentication is configured, nor a specific set of interfaces\n# are explicitly listed using the \"bind\" directive.\nprotected-mode yes\n\n# Accept connections on the specified port, default is 6379 (IANA #815344).\n# If port 0 is specified Redis will not listen on a TCP socket.\nport 6379\n\n# TCP listen() backlog.\n#\n# In high requests-per-second environments you need an high backlog in order\n# to avoid slow clients connections issues. Note that the Linux kernel\n# will silently truncate it to the value of /proc/sys/net/core/somaxconn so\n# make sure to raise both the value of somaxconn and tcp_max_syn_backlog\n# in order to get the desired effect.\ntcp-backlog 511\n\n# Unix socket.\n#\n# Specify the path for the Unix socket that will be used to listen for\n# incoming connections. There is no default, so Redis will not listen\n# on a unix socket when not specified.\n#\n# unixsocket /tmp/redis.sock\n# unixsocketperm 700\n\n# Close the connection after a client is idle for N seconds (0 to disable)\ntimeout 0\n\n# TCP keepalive.\n#\n# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence\n# of communication. This is useful for two reasons:\n#\n# 1) Detect dead peers.\n# 2) Take the connection alive from the point of view of network\n#    equipment in the middle.\n#\n# On Linux, the specified value (in seconds) is the period used to send ACKs.\n# Note that to close the connection the double of the time is needed.\n# On other kernels the period depends on the kernel configuration.\n#\n# A reasonable value for this option is 300 seconds, which is the new\n# Redis default starting with Redis 3.2.1.\ntcp-keepalive 300\n\n################################# GENERAL #####################################\n\n# By default Redis does not run as a daemon. Use 'yes' if you need it.\n# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.\ndaemonize no\n\n# If you run Redis from upstart or systemd, Redis can interact with your\n# supervision tree. Options:\n#   supervised no      - no supervision interaction\n#   supervised upstart - signal upstart by putting Redis into SIGSTOP mode\n#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET\n#   supervised auto    - detect upstart or systemd method based on\n#                        UPSTART_JOB or NOTIFY_SOCKET environment variables\n# Note: these supervision methods only signal \"process is ready.\"\n#       They do not enable continuous liveness pings back to your supervisor.\nsupervised no\n\n# If a pid file is specified, Redis writes it where specified at startup\n# and removes it at exit.\n#\n# When the server runs non daemonized, no pid file is created if none is\n# specified in the configuration. When the server is daemonized, the pid file\n# is used even if not specified, defaulting to \"/var/run/redis.pid\".\n#\n# Creating a pid file is best effort: if Redis is not able to create it\n# nothing bad happens, the server will start and run normally.\npidfile redis.pid\n\n# Specify the server verbosity level.\n# This can be one of:\n# debug (a lot of information, useful for development/testing)\n# verbose (many rarely useful info, but not a mess like the debug level)\n# notice (moderately verbose, what you want in production probably)\n# warning (only very important / critical messages are logged)\nloglevel notice\n\n# Specify the log file name. Also the empty string can be used to force\n# Redis to log on the standard output. Note that if you use standard\n# output for logging but daemonize, logs will be sent to /dev/null\nlogfile redis.log\n\n# To enable logging to the system logger, just set 'syslog-enabled' to yes,\n# and optionally update the other syslog parameters to suit your needs.\n# syslog-enabled no\n\n# Specify the syslog identity.\n# syslog-ident redis\n\n# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.\n# syslog-facility local0\n\n# Set the number of databases. The default database is DB 0, you can select\n# a different one on a per-connection basis using SELECT <dbid> where\n# dbid is a number between 0 and 'databases'-1\ndatabases 16\n\n# By default Redis shows an ASCII art logo only when started to log to the\n# standard output and if the standard output is a TTY. Basically this means\n# that normally a logo is displayed only in interactive sessions.\n#\n# However it is possible to force the pre-4.0 behavior and always show a\n# ASCII art logo in startup logs by setting the following option to yes.\nalways-show-logo yes\n\n################################ SNAPSHOTTING  ################################\n#\n# Save the DB on disk:\n#\n#   save <seconds> <changes>\n#\n#   Will save the DB if both the given number of seconds and the given\n#   number of write operations against the DB occurred.\n#\n#   In the example below the behaviour will be to save:\n#   after 900 sec (15 min) if at least 1 key changed\n#   after 300 sec (5 min) if at least 10 keys changed\n#   after 60 sec if at least 10000 keys changed\n#\n#   Note: you can disable saving completely by commenting out all \"save\" lines.\n#\n#   It is also possible to remove all the previously configured save\n#   points by adding a save directive with a single empty string argument\n#   like in the following example:\n#\n#   save \"\"\n\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# By default Redis will stop accepting writes if RDB snapshots are enabled\n# (at least one save point) and the latest background save failed.\n# This will make the user aware (in a hard way) that data is not persisting\n# on disk properly, otherwise chances are that no one will notice and some\n# disaster will happen.\n#\n# If the background saving process will start working again Redis will\n# automatically allow writes again.\n#\n# However if you have setup your proper monitoring of the Redis server\n# and persistence, you may want to disable this feature so that Redis will\n# continue to work as usual even if there are problems with disk,\n# permissions, and so forth.\nstop-writes-on-bgsave-error yes\n\n# Compress string objects using LZF when dump .rdb databases?\n# For default that's set to 'yes' as it's almost always a win.\n# If you want to save some CPU in the saving child set it to 'no' but\n# the dataset will likely be bigger if you have compressible values or keys.\nrdbcompression yes\n\n# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.\n# This makes the format more resistant to corruption but there is a performance\n# hit to pay (around 10%) when saving and loading RDB files, so you can disable it\n# for maximum performances.\n#\n# RDB files created with checksum disabled have a checksum of zero that will\n# tell the loading code to skip the check.\nrdbchecksum yes\n\n# The filename where to dump the DB\ndbfilename dump.rdb\n\n# The working directory.\n#\n# The DB will be written inside this directory, with the filename specified\n# above using the 'dbfilename' configuration directive.\n#\n# The Append Only File will also be created inside this directory.\n#\n# Note that you must specify a directory here, not a file name.\ndir .devbox/virtenv/redis/\n\n################################# REPLICATION #################################\n\n# Master-Slave replication. Use slaveof to make a Redis instance a copy of\n# another Redis server. A few things to understand ASAP about Redis replication.\n#\n# 1) Redis replication is asynchronous, but you can configure a master to\n#    stop accepting writes if it appears to be not connected with at least\n#    a given number of slaves.\n# 2) Redis slaves are able to perform a partial resynchronization with the\n#    master if the replication link is lost for a relatively small amount of\n#    time. You may want to configure the replication backlog size (see the next\n#    sections of this file) with a sensible value depending on your needs.\n# 3) Replication is automatic and does not need user intervention. After a\n#    network partition slaves automatically try to reconnect to masters\n#    and resynchronize with them.\n#\n# slaveof <masterip> <masterport>\n\n# If the master is password protected (using the \"requirepass\" configuration\n# directive below) it is possible to tell the slave to authenticate before\n# starting the replication synchronization process, otherwise the master will\n# refuse the slave request.\n#\n# masterauth <master-password>\n\n# When a slave loses its connection with the master, or when the replication\n# is still in progress, the slave can act in two different ways:\n#\n# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will\n#    still reply to client requests, possibly with out of date data, or the\n#    data set may just be empty if this is the first synchronization.\n#\n# 2) if slave-serve-stale-data is set to 'no' the slave will reply with\n#    an error \"SYNC with master in progress\" to all the kind of commands\n#    but to INFO and SLAVEOF.\n#\nslave-serve-stale-data yes\n\n# You can configure a slave instance to accept writes or not. Writing against\n# a slave instance may be useful to store some ephemeral data (because data\n# written on a slave will be easily deleted after resync with the master) but\n# may also cause problems if clients are writing to it because of a\n# misconfiguration.\n#\n# Since Redis 2.6 by default slaves are read-only.\n#\n# Note: read only slaves are not designed to be exposed to untrusted clients\n# on the internet. It's just a protection layer against misuse of the instance.\n# Still a read only slave exports by default all the administrative commands\n# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve\n# security of read only slaves using 'rename-command' to shadow all the\n# administrative / dangerous commands.\nslave-read-only yes\n\n# Replication SYNC strategy: disk or socket.\n#\n# -------------------------------------------------------\n# WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY\n# -------------------------------------------------------\n#\n# New slaves and reconnecting slaves that are not able to continue the replication\n# process just receiving differences, need to do what is called a \"full\n# synchronization\". An RDB file is transmitted from the master to the slaves.\n# The transmission can happen in two different ways:\n#\n# 1) Disk-backed: The Redis master creates a new process that writes the RDB\n#                 file on disk. Later the file is transferred by the parent\n#                 process to the slaves incrementally.\n# 2) Diskless: The Redis master creates a new process that directly writes the\n#              RDB file to slave sockets, without touching the disk at all.\n#\n# With disk-backed replication, while the RDB file is generated, more slaves\n# can be queued and served with the RDB file as soon as the current child producing\n# the RDB file finishes its work. With diskless replication instead once\n# the transfer starts, new slaves arriving will be queued and a new transfer\n# will start when the current one terminates.\n#\n# When diskless replication is used, the master waits a configurable amount of\n# time (in seconds) before starting the transfer in the hope that multiple slaves\n# will arrive and the transfer can be parallelized.\n#\n# With slow disks and fast (large bandwidth) networks, diskless replication\n# works better.\nrepl-diskless-sync no\n\n# When diskless replication is enabled, it is possible to configure the delay\n# the server waits in order to spawn the child that transfers the RDB via socket\n# to the slaves.\n#\n# This is important since once the transfer starts, it is not possible to serve\n# new slaves arriving, that will be queued for the next RDB transfer, so the server\n# waits a delay in order to let more slaves arrive.\n#\n# The delay is specified in seconds, and by default is 5 seconds. To disable\n# it entirely just set it to 0 seconds and the transfer will start ASAP.\nrepl-diskless-sync-delay 5\n\n# Slaves send PINGs to server in a predefined interval. It's possible to change\n# this interval with the repl_ping_slave_period option. The default value is 10\n# seconds.\n#\n# repl-ping-slave-period 10\n\n# The following option sets the replication timeout for:\n#\n# 1) Bulk transfer I/O during SYNC, from the point of view of slave.\n# 2) Master timeout from the point of view of slaves (data, pings).\n# 3) Slave timeout from the point of view of masters (REPLCONF ACK pings).\n#\n# It is important to make sure that this value is greater than the value\n# specified for repl-ping-slave-period otherwise a timeout will be detected\n# every time there is low traffic between the master and the slave.\n#\n# repl-timeout 60\n\n# Disable TCP_NODELAY on the slave socket after SYNC?\n#\n# If you select \"yes\" Redis will use a smaller number of TCP packets and\n# less bandwidth to send data to slaves. But this can add a delay for\n# the data to appear on the slave side, up to 40 milliseconds with\n# Linux kernels using a default configuration.\n#\n# If you select \"no\" the delay for data to appear on the slave side will\n# be reduced but more bandwidth will be used for replication.\n#\n# By default we optimize for low latency, but in very high traffic conditions\n# or when the master and slaves are many hops away, turning this to \"yes\" may\n# be a good idea.\nrepl-disable-tcp-nodelay no\n\n# Set the replication backlog size. The backlog is a buffer that accumulates\n# slave data when slaves are disconnected for some time, so that when a slave\n# wants to reconnect again, often a full resync is not needed, but a partial\n# resync is enough, just passing the portion of data the slave missed while\n# disconnected.\n#\n# The bigger the replication backlog, the longer the time the slave can be\n# disconnected and later be able to perform a partial resynchronization.\n#\n# The backlog is only allocated once there is at least a slave connected.\n#\n# repl-backlog-size 1mb\n\n# After a master has no longer connected slaves for some time, the backlog\n# will be freed. The following option configures the amount of seconds that\n# need to elapse, starting from the time the last slave disconnected, for\n# the backlog buffer to be freed.\n#\n# Note that slaves never free the backlog for timeout, since they may be\n# promoted to masters later, and should be able to correctly \"partially\n# resynchronize\" with the slaves: hence they should always accumulate backlog.\n#\n# A value of 0 means to never release the backlog.\n#\n# repl-backlog-ttl 3600\n\n# The slave priority is an integer number published by Redis in the INFO output.\n# It is used by Redis Sentinel in order to select a slave to promote into a\n# master if the master is no longer working correctly.\n#\n# A slave with a low priority number is considered better for promotion, so\n# for instance if there are three slaves with priority 10, 100, 25 Sentinel will\n# pick the one with priority 10, that is the lowest.\n#\n# However a special priority of 0 marks the slave as not able to perform the\n# role of master, so a slave with priority of 0 will never be selected by\n# Redis Sentinel for promotion.\n#\n# By default the priority is 100.\nslave-priority 100\n\n# It is possible for a master to stop accepting writes if there are less than\n# N slaves connected, having a lag less or equal than M seconds.\n#\n# The N slaves need to be in \"online\" state.\n#\n# The lag in seconds, that must be <= the specified value, is calculated from\n# the last ping received from the slave, that is usually sent every second.\n#\n# This option does not GUARANTEE that N replicas will accept the write, but\n# will limit the window of exposure for lost writes in case not enough slaves\n# are available, to the specified number of seconds.\n#\n# For example to require at least 3 slaves with a lag <= 10 seconds use:\n#\n# min-slaves-to-write 3\n# min-slaves-max-lag 10\n#\n# Setting one or the other to 0 disables the feature.\n#\n# By default min-slaves-to-write is set to 0 (feature disabled) and\n# min-slaves-max-lag is set to 10.\n\n# A Redis master is able to list the address and port of the attached\n# slaves in different ways. For example the \"INFO replication\" section\n# offers this information, which is used, among other tools, by\n# Redis Sentinel in order to discover slave instances.\n# Another place where this info is available is in the output of the\n# \"ROLE\" command of a master.\n#\n# The listed IP and address normally reported by a slave is obtained\n# in the following way:\n#\n#   IP: The address is auto detected by checking the peer address\n#   of the socket used by the slave to connect with the master.\n#\n#   Port: The port is communicated by the slave during the replication\n#   handshake, and is normally the port that the slave is using to\n#   list for connections.\n#\n# However when port forwarding or Network Address Translation (NAT) is\n# used, the slave may be actually reachable via different IP and port\n# pairs. The following two options can be used by a slave in order to\n# report to its master a specific set of IP and port, so that both INFO\n# and ROLE will report those values.\n#\n# There is no need to use both the options if you need to override just\n# the port or the IP address.\n#\n# slave-announce-ip 5.5.5.5\n# slave-announce-port 1234\n\n################################## SECURITY ###################################\n\n# Require clients to issue AUTH <PASSWORD> before processing any other\n# commands.  This might be useful in environments in which you do not trust\n# others with access to the host running redis-server.\n#\n# This should stay commented out for backward compatibility and because most\n# people do not need auth (e.g. they run their own servers).\n#\n# Warning: since Redis is pretty fast an outside user can try up to\n# 150k passwords per second against a good box. This means that you should\n# use a very strong password otherwise it will be very easy to break.\n#\n# requirepass foobared\n\n# Command renaming.\n#\n# It is possible to change the name of dangerous commands in a shared\n# environment. For instance the CONFIG command may be renamed into something\n# hard to guess so that it will still be available for internal-use tools\n# but not available for general clients.\n#\n# Example:\n#\n# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52\n#\n# It is also possible to completely kill a command by renaming it into\n# an empty string:\n#\n# rename-command CONFIG \"\"\n#\n# Please note that changing the name of commands that are logged into the\n# AOF file or transmitted to slaves may cause problems.\n\n################################### CLIENTS ####################################\n\n# Set the max number of connected clients at the same time. By default\n# this limit is set to 10000 clients, however if the Redis server is not\n# able to configure the process file limit to allow for the specified limit\n# the max number of allowed clients is set to the current file limit\n# minus 32 (as Redis reserves a few file descriptors for internal uses).\n#\n# Once the limit is reached Redis will close all the new connections sending\n# an error 'max number of clients reached'.\n#\n# maxclients 10000\n\n############################## MEMORY MANAGEMENT ################################\n\n# Set a memory usage limit to the specified amount of bytes.\n# When the memory limit is reached Redis will try to remove keys\n# according to the eviction policy selected (see maxmemory-policy).\n#\n# If Redis can't remove keys according to the policy, or if the policy is\n# set to 'noeviction', Redis will start to reply with errors to commands\n# that would use more memory, like SET, LPUSH, and so on, and will continue\n# to reply to read-only commands like GET.\n#\n# This option is usually useful when using Redis as an LRU or LFU cache, or to\n# set a hard memory limit for an instance (using the 'noeviction' policy).\n#\n# WARNING: If you have slaves attached to an instance with maxmemory on,\n# the size of the output buffers needed to feed the slaves are subtracted\n# from the used memory count, so that network problems / resyncs will\n# not trigger a loop where keys are evicted, and in turn the output\n# buffer of slaves is full with DELs of keys evicted triggering the deletion\n# of more keys, and so forth until the database is completely emptied.\n#\n# In short... if you have slaves attached it is suggested that you set a lower\n# limit for maxmemory so that there is some free RAM on the system for slave\n# output buffers (but this is not needed if the policy is 'noeviction').\n#\n# maxmemory <bytes>\n\n# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory\n# is reached. You can select among five behaviors:\n#\n# volatile-lru -> Evict using approximated LRU among the keys with an expire set.\n# allkeys-lru -> Evict any key using approximated LRU.\n# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.\n# allkeys-lfu -> Evict any key using approximated LFU.\n# volatile-random -> Remove a random key among the ones with an expire set.\n# allkeys-random -> Remove a random key, any key.\n# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)\n# noeviction -> Don't evict anything, just return an error on write operations.\n#\n# LRU means Least Recently Used\n# LFU means Least Frequently Used\n#\n# Both LRU, LFU and volatile-ttl are implemented using approximated\n# randomized algorithms.\n#\n# Note: with any of the above policies, Redis will return an error on write\n#       operations, when there are no suitable keys for eviction.\n#\n#       At the date of writing these commands are: set setnx setex append\n#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd\n#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby\n#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby\n#       getset mset msetnx exec sort\n#\n# The default is:\n#\n# maxmemory-policy noeviction\n\n# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated\n# algorithms (in order to save memory), so you can tune it for speed or\n# accuracy. For default Redis will check five keys and pick the one that was\n# used less recently, you can change the sample size using the following\n# configuration directive.\n#\n# The default of 5 produces good enough results. 10 Approximates very closely\n# true LRU but costs more CPU. 3 is faster but not very accurate.\n#\n# maxmemory-samples 5\n\n############################# LAZY FREEING ####################################\n\n# Redis has two primitives to delete keys. One is called DEL and is a blocking\n# deletion of the object. It means that the server stops processing new commands\n# in order to reclaim all the memory associated with an object in a synchronous\n# way. If the key deleted is associated with a small object, the time needed\n# in order to execute the DEL command is very small and comparable to most other\n# O(1) or O(log_N) commands in Redis. However if the key is associated with an\n# aggregated value containing millions of elements, the server can block for\n# a long time (even seconds) in order to complete the operation.\n#\n# For the above reasons Redis also offers non blocking deletion primitives\n# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and\n# FLUSHDB commands, in order to reclaim memory in background. Those commands\n# are executed in constant time. Another thread will incrementally free the\n# object in the background as fast as possible.\n#\n# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.\n# It's up to the design of the application to understand when it is a good\n# idea to use one or the other. However the Redis server sometimes has to\n# delete keys or flush the whole database as a side effect of other operations.\n# Specifically Redis deletes objects independently of a user call in the\n# following scenarios:\n#\n# 1) On eviction, because of the maxmemory and maxmemory policy configurations,\n#    in order to make room for new data, without going over the specified\n#    memory limit.\n# 2) Because of expire: when a key with an associated time to live (see the\n#    EXPIRE command) must be deleted from memory.\n# 3) Because of a side effect of a command that stores data on a key that may\n#    already exist. For example the RENAME command may delete the old key\n#    content when it is replaced with another one. Similarly SUNIONSTORE\n#    or SORT with STORE option may delete existing keys. The SET command\n#    itself removes any old content of the specified key in order to replace\n#    it with the specified string.\n# 4) During replication, when a slave performs a full resynchronization with\n#    its master, the content of the whole database is removed in order to\n#    load the RDB file just transferred.\n#\n# In all the above cases the default is to delete objects in a blocking way,\n# like if DEL was called. However you can configure each case specifically\n# in order to instead release memory in a non-blocking way like if UNLINK\n# was called, using the following configuration directives:\n\nlazyfree-lazy-eviction no\nlazyfree-lazy-expire no\nlazyfree-lazy-server-del no\nslave-lazy-flush no\n\n############################## APPEND ONLY MODE ###############################\n\n# By default Redis asynchronously dumps the dataset on disk. This mode is\n# good enough in many applications, but an issue with the Redis process or\n# a power outage may result into a few minutes of writes lost (depending on\n# the configured save points).\n#\n# The Append Only File is an alternative persistence mode that provides\n# much better durability. For instance using the default data fsync policy\n# (see later in the config file) Redis can lose just one second of writes in a\n# dramatic event like a server power outage, or a single write if something\n# wrong with the Redis process itself happens, but the operating system is\n# still running correctly.\n#\n# AOF and RDB persistence can be enabled at the same time without problems.\n# If the AOF is enabled on startup Redis will load the AOF, that is the file\n# with the better durability guarantees.\n#\n# Please check http://redis.io/topics/persistence for more information.\n\nappendonly no\n\n# The name of the append only file (default: \"appendonly.aof\")\n\nappendfilename \"appendonly.aof\"\n\n# The fsync() call tells the Operating System to actually write data on disk\n# instead of waiting for more data in the output buffer. Some OS will really flush\n# data on disk, some other OS will just try to do it ASAP.\n#\n# Redis supports three different modes:\n#\n# no: don't fsync, just let the OS flush the data when it wants. Faster.\n# always: fsync after every write to the append only log. Slow, Safest.\n# everysec: fsync only one time every second. Compromise.\n#\n# The default is \"everysec\", as that's usually the right compromise between\n# speed and data safety. It's up to you to understand if you can relax this to\n# \"no\" that will let the operating system flush the output buffer when\n# it wants, for better performances (but if you can live with the idea of\n# some data loss consider the default persistence mode that's snapshotting),\n# or on the contrary, use \"always\" that's very slow but a bit safer than\n# everysec.\n#\n# More details please check the following article:\n# http://antirez.com/post/redis-persistence-demystified.html\n#\n# If unsure, use \"everysec\".\n\n# appendfsync always\nappendfsync everysec\n# appendfsync no\n\n# When the AOF fsync policy is set to always or everysec, and a background\n# saving process (a background save or AOF log background rewriting) is\n# performing a lot of I/O against the disk, in some Linux configurations\n# Redis may block too long on the fsync() call. Note that there is no fix for\n# this currently, as even performing fsync in a different thread will block\n# our synchronous write(2) call.\n#\n# In order to mitigate this problem it's possible to use the following option\n# that will prevent fsync() from being called in the main process while a\n# BGSAVE or BGREWRITEAOF is in progress.\n#\n# This means that while another child is saving, the durability of Redis is\n# the same as \"appendfsync none\". In practical terms, this means that it is\n# possible to lose up to 30 seconds of log in the worst scenario (with the\n# default Linux settings).\n#\n# If you have latency problems turn this to \"yes\". Otherwise leave it as\n# \"no\" that is the safest pick from the point of view of durability.\n\nno-appendfsync-on-rewrite no\n\n# Automatic rewrite of the append only file.\n# Redis is able to automatically rewrite the log file implicitly calling\n# BGREWRITEAOF when the AOF log size grows by the specified percentage.\n#\n# This is how it works: Redis remembers the size of the AOF file after the\n# latest rewrite (if no rewrite has happened since the restart, the size of\n# the AOF at startup is used).\n#\n# This base size is compared to the current size. If the current size is\n# bigger than the specified percentage, the rewrite is triggered. Also\n# you need to specify a minimal size for the AOF file to be rewritten, this\n# is useful to avoid rewriting the AOF file even if the percentage increase\n# is reached but it is still pretty small.\n#\n# Specify a percentage of zero in order to disable the automatic AOF\n# rewrite feature.\n\nauto-aof-rewrite-percentage 100\nauto-aof-rewrite-min-size 64mb\n\n# An AOF file may be found to be truncated at the end during the Redis\n# startup process, when the AOF data gets loaded back into memory.\n# This may happen when the system where Redis is running\n# crashes, especially when an ext4 filesystem is mounted without the\n# data=ordered option (however this can't happen when Redis itself\n# crashes or aborts but the operating system still works correctly).\n#\n# Redis can either exit with an error when this happens, or load as much\n# data as possible (the default now) and start if the AOF file is found\n# to be truncated at the end. The following option controls this behavior.\n#\n# If aof-load-truncated is set to yes, a truncated AOF file is loaded and\n# the Redis server starts emitting a log to inform the user of the event.\n# Otherwise if the option is set to no, the server aborts with an error\n# and refuses to start. When the option is set to no, the user requires\n# to fix the AOF file using the \"redis-check-aof\" utility before to restart\n# the server.\n#\n# Note that if the AOF file will be found to be corrupted in the middle\n# the server will still exit with an error. This option only applies when\n# Redis will try to read more data from the AOF file but not enough bytes\n# will be found.\naof-load-truncated yes\n\n# When rewriting the AOF file, Redis is able to use an RDB preamble in the\n# AOF file for faster rewrites and recoveries. When this option is turned\n# on the rewritten AOF file is composed of two different stanzas:\n#\n#   [RDB file][AOF tail]\n#\n# When loading Redis recognizes that the AOF file starts with the \"REDIS\"\n# string and loads the prefixed RDB file, and continues loading the AOF\n# tail.\n#\n# This is currently turned off by default in order to avoid the surprise\n# of a format change, but will at some point be used as the default.\naof-use-rdb-preamble no\n\n################################ LUA SCRIPTING  ###############################\n\n# Max execution time of a Lua script in milliseconds.\n#\n# If the maximum execution time is reached Redis will log that a script is\n# still in execution after the maximum allowed time and will start to\n# reply to queries with an error.\n#\n# When a long running script exceeds the maximum execution time only the\n# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be\n# used to stop a script that did not yet called write commands. The second\n# is the only way to shut down the server in the case a write command was\n# already issued by the script but the user doesn't want to wait for the natural\n# termination of the script.\n#\n# Set it to 0 or a negative value for unlimited execution without warnings.\nlua-time-limit 5000\n\n################################ REDIS CLUSTER  ###############################\n#\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n# WARNING EXPERIMENTAL: Redis Cluster is considered to be stable code, however\n# in order to mark it as \"mature\" we need to wait for a non trivial percentage\n# of users to deploy it in production.\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n#\n# Normal Redis instances can't be part of a Redis Cluster; only nodes that are\n# started as cluster nodes can. In order to start a Redis instance as a\n# cluster node enable the cluster support uncommenting the following:\n#\n# cluster-enabled yes\n\n# Every cluster node has a cluster configuration file. This file is not\n# intended to be edited by hand. It is created and updated by Redis nodes.\n# Every Redis Cluster node requires a different cluster configuration file.\n# Make sure that instances running in the same system do not have\n# overlapping cluster configuration file names.\n#\n# cluster-config-file nodes-6379.conf\n\n# Cluster node timeout is the amount of milliseconds a node must be unreachable\n# for it to be considered in failure state.\n# Most other internal time limits are multiple of the node timeout.\n#\n# cluster-node-timeout 15000\n\n# A slave of a failing master will avoid to start a failover if its data\n# looks too old.\n#\n# There is no simple way for a slave to actually have an exact measure of\n# its \"data age\", so the following two checks are performed:\n#\n# 1) If there are multiple slaves able to failover, they exchange messages\n#    in order to try to give an advantage to the slave with the best\n#    replication offset (more data from the master processed).\n#    Slaves will try to get their rank by offset, and apply to the start\n#    of the failover a delay proportional to their rank.\n#\n# 2) Every single slave computes the time of the last interaction with\n#    its master. This can be the last ping or command received (if the master\n#    is still in the \"connected\" state), or the time that elapsed since the\n#    disconnection with the master (if the replication link is currently down).\n#    If the last interaction is too old, the slave will not try to failover\n#    at all.\n#\n# The point \"2\" can be tuned by user. Specifically a slave will not perform\n# the failover if, since the last interaction with the master, the time\n# elapsed is greater than:\n#\n#   (node-timeout * slave-validity-factor) + repl-ping-slave-period\n#\n# So for example if node-timeout is 30 seconds, and the slave-validity-factor\n# is 10, and assuming a default repl-ping-slave-period of 10 seconds, the\n# slave will not try to failover if it was not able to talk with the master\n# for longer than 310 seconds.\n#\n# A large slave-validity-factor may allow slaves with too old data to failover\n# a master, while a too small value may prevent the cluster from being able to\n# elect a slave at all.\n#\n# For maximum availability, it is possible to set the slave-validity-factor\n# to a value of 0, which means, that slaves will always try to failover the\n# master regardless of the last time they interacted with the master.\n# (However they'll always try to apply a delay proportional to their\n# offset rank).\n#\n# Zero is the only value able to guarantee that when all the partitions heal\n# the cluster will always be able to continue.\n#\n# cluster-slave-validity-factor 10\n\n# Cluster slaves are able to migrate to orphaned masters, that are masters\n# that are left without working slaves. This improves the cluster ability\n# to resist to failures as otherwise an orphaned master can't be failed over\n# in case of failure if it has no working slaves.\n#\n# Slaves migrate to orphaned masters only if there are still at least a\n# given number of other working slaves for their old master. This number\n# is the \"migration barrier\". A migration barrier of 1 means that a slave\n# will migrate only if there is at least 1 other working slave for its master\n# and so forth. It usually reflects the number of slaves you want for every\n# master in your cluster.\n#\n# Default is 1 (slaves migrate only if their masters remain with at least\n# one slave). To disable migration just set it to a very large value.\n# A value of 0 can be set but is useful only for debugging and dangerous\n# in production.\n#\n# cluster-migration-barrier 1\n\n# By default Redis Cluster nodes stop accepting queries if they detect there\n# is at least an hash slot uncovered (no available node is serving it).\n# This way if the cluster is partially down (for example a range of hash slots\n# are no longer covered) all the cluster becomes, eventually, unavailable.\n# It automatically returns available as soon as all the slots are covered again.\n#\n# However sometimes you want the subset of the cluster which is working,\n# to continue to accept queries for the part of the key space that is still\n# covered. In order to do so, just set the cluster-require-full-coverage\n# option to no.\n#\n# cluster-require-full-coverage yes\n\n# This option, when set to yes, prevents slaves from trying to failover its\n# master during master failures. However the master can still perform a\n# manual failover, if forced to do so.\n#\n# This is useful in different scenarios, especially in the case of multiple\n# data center operations, where we want one side to never be promoted if not\n# in the case of a total DC failure.\n#\n# cluster-slave-no-failover no\n\n# In order to setup your cluster make sure to read the documentation\n# available at http://redis.io web site.\n\n########################## CLUSTER DOCKER/NAT support  ########################\n\n# In certain deployments, Redis Cluster nodes address discovery fails, because\n# addresses are NAT-ted or because ports are forwarded (the typical case is\n# Docker and other containers).\n#\n# In order to make Redis Cluster working in such environments, a static\n# configuration where each node knows its public address is needed. The\n# following two options are used for this scope, and are:\n#\n# * cluster-announce-ip\n# * cluster-announce-port\n# * cluster-announce-bus-port\n#\n# Each instruct the node about its address, client port, and cluster message\n# bus port. The information is then published in the header of the bus packets\n# so that other nodes will be able to correctly map the address of the node\n# publishing the information.\n#\n# If the above options are not used, the normal Redis Cluster auto-detection\n# will be used instead.\n#\n# Note that when remapped, the bus port may not be at the fixed offset of\n# clients port + 10000, so you can specify any port and bus-port depending\n# on how they get remapped. If the bus-port is not set, a fixed offset of\n# 10000 will be used as usually.\n#\n# Example:\n#\n# cluster-announce-ip 10.1.1.5\n# cluster-announce-port 6379\n# cluster-announce-bus-port 6380\n\n################################## SLOW LOG ###################################\n\n# The Redis Slow Log is a system to log queries that exceeded a specified\n# execution time. The execution time does not include the I/O operations\n# like talking with the client, sending the reply and so forth,\n# but just the time needed to actually execute the command (this is the only\n# stage of command execution where the thread is blocked and can not serve\n# other requests in the meantime).\n#\n# You can configure the slow log with two parameters: one tells Redis\n# what is the execution time, in microseconds, to exceed in order for the\n# command to get logged, and the other parameter is the length of the\n# slow log. When a new command is logged the oldest one is removed from the\n# queue of logged commands.\n\n# The following time is expressed in microseconds, so 1000000 is equivalent\n# to one second. Note that a negative number disables the slow log, while\n# a value of zero forces the logging of every command.\nslowlog-log-slower-than 10000\n\n# There is no limit to this length. Just be aware that it will consume memory.\n# You can reclaim memory used by the slow log with SLOWLOG RESET.\nslowlog-max-len 128\n\n################################ LATENCY MONITOR ##############################\n\n# The Redis latency monitoring subsystem samples different operations\n# at runtime in order to collect data related to possible sources of\n# latency of a Redis instance.\n#\n# Via the LATENCY command this information is available to the user that can\n# print graphs and obtain reports.\n#\n# The system only logs operations that were performed in a time equal or\n# greater than the amount of milliseconds specified via the\n# latency-monitor-threshold configuration directive. When its value is set\n# to zero, the latency monitor is turned off.\n#\n# By default latency monitoring is disabled since it is mostly not needed\n# if you don't have latency issues, and collecting data has a performance\n# impact, that while very small, can be measured under big load. Latency\n# monitoring can easily be enabled at runtime using the command\n# \"CONFIG SET latency-monitor-threshold <milliseconds>\" if needed.\nlatency-monitor-threshold 0\n\n############################# EVENT NOTIFICATION ##############################\n\n# Redis can notify Pub/Sub clients about events happening in the key space.\n# This feature is documented at http://redis.io/topics/notifications\n#\n# For instance if keyspace events notification is enabled, and a client\n# performs a DEL operation on key \"foo\" stored in the Database 0, two\n# messages will be published via Pub/Sub:\n#\n# PUBLISH __keyspace@0__:foo del\n# PUBLISH __keyevent@0__:del foo\n#\n# It is possible to select the events that Redis will notify among a set\n# of classes. Every class is identified by a single character:\n#\n#  K     Keyspace events, published with __keyspace@<db>__ prefix.\n#  E     Keyevent events, published with __keyevent@<db>__ prefix.\n#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...\n#  $     String commands\n#  l     List commands\n#  s     Set commands\n#  h     Hash commands\n#  z     Sorted set commands\n#  x     Expired events (events generated every time a key expires)\n#  e     Evicted events (events generated when a key is evicted for maxmemory)\n#  A     Alias for g$lshzxe, so that the \"AKE\" string means all the events.\n#\n#  The \"notify-keyspace-events\" takes as argument a string that is composed\n#  of zero or multiple characters. The empty string means that notifications\n#  are disabled.\n#\n#  Example: to enable list and generic events, from the point of view of the\n#           event name, use:\n#\n#  notify-keyspace-events Elg\n#\n#  Example 2: to get the stream of the expired keys subscribing to channel\n#             name __keyevent@0__:expired use:\n#\n#  notify-keyspace-events Ex\n#\n#  By default all notifications are disabled because most users don't need\n#  this feature and the feature has some overhead. Note that if you don't\n#  specify at least one of K or E, no events will be delivered.\nnotify-keyspace-events \"\"\n\n############################### ADVANCED CONFIG ###############################\n\n# Hashes are encoded using a memory efficient data structure when they have a\n# small number of entries, and the biggest entry does not exceed a given\n# threshold. These thresholds can be configured using the following directives.\nhash-max-ziplist-entries 512\nhash-max-ziplist-value 64\n\n# Lists are also encoded in a special way to save a lot of space.\n# The number of entries allowed per internal list node can be specified\n# as a fixed maximum size or a maximum number of elements.\n# For a fixed maximum size, use -5 through -1, meaning:\n# -5: max size: 64 Kb  <-- not recommended for normal workloads\n# -4: max size: 32 Kb  <-- not recommended\n# -3: max size: 16 Kb  <-- probably not recommended\n# -2: max size: 8 Kb   <-- good\n# -1: max size: 4 Kb   <-- good\n# Positive numbers mean store up to _exactly_ that number of elements\n# per list node.\n# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),\n# but if your use case is unique, adjust the settings as necessary.\nlist-max-ziplist-size -2\n\n# Lists may also be compressed.\n# Compress depth is the number of quicklist ziplist nodes from *each* side of\n# the list to *exclude* from compression.  The head and tail of the list\n# are always uncompressed for fast push/pop operations.  Settings are:\n# 0: disable all list compression\n# 1: depth 1 means \"don't start compressing until after 1 node into the list,\n#    going from either the head or tail\"\n#    So: [head]->node->node->...->node->[tail]\n#    [head], [tail] will always be uncompressed; inner nodes will compress.\n# 2: [head]->[next]->node->node->...->node->[prev]->[tail]\n#    2 here means: don't compress head or head->next or tail->prev or tail,\n#    but compress all nodes between them.\n# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]\n# etc.\nlist-compress-depth 0\n\n# Sets have a special encoding in just one case: when a set is composed\n# of just strings that happen to be integers in radix 10 in the range\n# of 64 bit signed integers.\n# The following configuration setting sets the limit in the size of the\n# set in order to use this special memory saving encoding.\nset-max-intset-entries 512\n\n# Similarly to hashes and lists, sorted sets are also specially encoded in\n# order to save a lot of space. This encoding is only used when the length and\n# elements of a sorted set are below the following limits:\nzset-max-ziplist-entries 128\nzset-max-ziplist-value 64\n\n# HyperLogLog sparse representation bytes limit. The limit includes the\n# 16 bytes header. When an HyperLogLog using the sparse representation crosses\n# this limit, it is converted into the dense representation.\n#\n# A value greater than 16000 is totally useless, since at that point the\n# dense representation is more memory efficient.\n#\n# The suggested value is ~ 3000 in order to have the benefits of\n# the space efficient encoding without slowing down too much PFADD,\n# which is O(N) with the sparse encoding. The value can be raised to\n# ~ 10000 when CPU is not a concern, but space is, and the data set is\n# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.\nhll-sparse-max-bytes 3000\n\n# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in\n# order to help rehashing the main Redis hash table (the one mapping top-level\n# keys to values). The hash table implementation Redis uses (see dict.c)\n# performs a lazy rehashing: the more operation you run into a hash table\n# that is rehashing, the more rehashing \"steps\" are performed, so if the\n# server is idle the rehashing is never complete and some more memory is used\n# by the hash table.\n#\n# The default is to use this millisecond 10 times every second in order to\n# actively rehash the main dictionaries, freeing memory when possible.\n#\n# If unsure:\n# use \"activerehashing no\" if you have hard latency requirements and it is\n# not a good thing in your environment that Redis can reply from time to time\n# to queries with 2 milliseconds delay.\n#\n# use \"activerehashing yes\" if you don't have such hard requirements but\n# want to free memory asap when possible.\nactiverehashing yes\n\n# The client output buffer limits can be used to force disconnection of clients\n# that are not reading data from the server fast enough for some reason (a\n# common reason is that a Pub/Sub client can't consume messages as fast as the\n# publisher can produce them).\n#\n# The limit can be set differently for the three different classes of clients:\n#\n# normal -> normal clients including MONITOR clients\n# slave  -> slave clients\n# pubsub -> clients subscribed to at least one pubsub channel or pattern\n#\n# The syntax of every client-output-buffer-limit directive is the following:\n#\n# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>\n#\n# A client is immediately disconnected once the hard limit is reached, or if\n# the soft limit is reached and remains reached for the specified number of\n# seconds (continuously).\n# So for instance if the hard limit is 32 megabytes and the soft limit is\n# 16 megabytes / 10 seconds, the client will get disconnected immediately\n# if the size of the output buffers reach 32 megabytes, but will also get\n# disconnected if the client reaches 16 megabytes and continuously overcomes\n# the limit for 10 seconds.\n#\n# By default normal clients are not limited because they don't receive data\n# without asking (in a push way), but just after a request, so only\n# asynchronous clients may create a scenario where data is requested faster\n# than it can read.\n#\n# Instead there is a default limit for pubsub and slave clients, since\n# subscribers and slaves receive data in a push fashion.\n#\n# Both the hard or the soft limit can be disabled by setting them to zero.\nclient-output-buffer-limit normal 0 0 0\nclient-output-buffer-limit slave 256mb 64mb 60\nclient-output-buffer-limit pubsub 32mb 8mb 60\n\n# Client query buffers accumulate new commands. They are limited to a fixed\n# amount by default in order to avoid that a protocol desynchronization (for\n# instance due to a bug in the client) will lead to unbound memory usage in\n# the query buffer. However you can configure it here if you have very special\n# needs, such us huge multi/exec requests or alike.\n#\n# client-query-buffer-limit 1gb\n\n# In the Redis protocol, bulk requests, that are, elements representing single\n# strings, are normally limited to 512 mb. However you can change this limit\n# here.\n#\n# proto-max-bulk-len 512mb\n\n# Redis calls an internal function to perform many background tasks, like\n# closing connections of clients in timeout, purging expired keys that are\n# never requested, and so forth.\n#\n# Not all tasks are performed with the same frequency, but Redis checks for\n# tasks to perform according to the specified \"hz\" value.\n#\n# By default \"hz\" is set to 10. Raising the value will use more CPU when\n# Redis is idle, but at the same time will make Redis more responsive when\n# there are many keys expiring at the same time, and timeouts may be\n# handled with more precision.\n#\n# The range is between 1 and 500, however a value over 100 is usually not\n# a good idea. Most users should use the default of 10 and raise this up to\n# 100 only in environments where very low latency is required.\nhz 10\n\n# When a child rewrites the AOF file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\naof-rewrite-incremental-fsync yes\n\n# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good\n# idea to start with the default settings and only change them after investigating\n# how to improve the performances and how the keys LFU change over time, which\n# is possible to inspect via the OBJECT FREQ command.\n#\n# There are two tunable parameters in the Redis LFU implementation: the\n# counter logarithm factor and the counter decay time. It is important to\n# understand what the two parameters mean before changing them.\n#\n# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis\n# uses a probabilistic increment with logarithmic behavior. Given the value\n# of the old counter, when a key is accessed, the counter is incremented in\n# this way:\n#\n# 1. A random number R between 0 and 1 is extracted.\n# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).\n# 3. The counter is incremented only if R < P.\n#\n# The default lfu-log-factor is 10. This is a table of how the frequency\n# counter changes with a different number of accesses with different\n# logarithmic factors:\n#\n# +--------+------------+------------+------------+------------+------------+\n# | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |\n# +--------+------------+------------+------------+------------+------------+\n# | 0      | 104        | 255        | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 1      | 18         | 49         | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 10     | 10         | 18         | 142        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 100    | 8          | 11         | 49         | 143        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n#\n# NOTE: The above table was obtained by running the following commands:\n#\n#   redis-benchmark -n 1000000 incr foo\n#   redis-cli object freq foo\n#\n# NOTE 2: The counter initial value is 5 in order to give new objects a chance\n# to accumulate hits.\n#\n# The counter decay time is the time, in minutes, that must elapse in order\n# for the key counter to be divided by two (or decremented if it has a value\n# less <= 10).\n#\n# The default value for the lfu-decay-time is 1. A Special value of 0 means to\n# decay the counter every time it happens to be scanned.\n#\n# lfu-log-factor 10\n# lfu-decay-time 1\n\n########################### ACTIVE DEFRAGMENTATION #######################\n#\n# WARNING THIS FEATURE IS EXPERIMENTAL. However it was stress tested\n# even in production and manually tested by multiple engineers for some\n# time.\n#\n# What is active defragmentation?\n# -------------------------------\n#\n# Active (online) defragmentation allows a Redis server to compact the\n# spaces left between small allocations and deallocations of data in memory,\n# thus allowing to reclaim back memory.\n#\n# Fragmentation is a natural process that happens with every allocator (but\n# less so with Jemalloc, fortunately) and certain workloads. Normally a server\n# restart is needed in order to lower the fragmentation, or at least to flush\n# away all the data and create it again. However thanks to this feature\n# implemented by Oran Agra for Redis 4.0 this process can happen at runtime\n# in an \"hot\" way, while the server is running.\n#\n# Basically when the fragmentation is over a certain level (see the\n# configuration options below) Redis will start to create new copies of the\n# values in contiguous memory regions by exploiting certain specific Jemalloc\n# features (in order to understand if an allocation is causing fragmentation\n# and to allocate it in a better place), and at the same time, will release the\n# old copies of the data. This process, repeated incrementally for all the keys\n# will cause the fragmentation to drop back to normal values.\n#\n# Important things to understand:\n#\n# 1. This feature is disabled by default, and only works if you compiled Redis\n#    to use the copy of Jemalloc we ship with the source code of Redis.\n#    This is the default with Linux builds.\n#\n# 2. You never need to enable this feature if you don't have fragmentation\n#    issues.\n#\n# 3. Once you experience fragmentation, you can enable this feature when\n#    needed with the command \"CONFIG SET activedefrag yes\".\n#\n# The configuration parameters are able to fine tune the behavior of the\n# defragmentation process. If you are not sure about what they mean it is\n# a good idea to leave the defaults untouched.\n\n# Enabled active defragmentation\n# activedefrag yes\n\n# Minimum amount of fragmentation waste to start active defrag\n# active-defrag-ignore-bytes 100mb\n\n# Minimum percentage of fragmentation to start active defrag\n# active-defrag-threshold-lower 10\n\n# Maximum percentage of fragmentation at which we use maximum effort\n# active-defrag-threshold-upper 100\n\n# Minimal effort for defrag in CPU percentage\n# active-defrag-cycle-min 25\n\n# Maximal effort for defrag in CPU percentage\n# active-defrag-cycle-max 75\n"
  },
  {
    "path": "examples/databases/redis/devbox.json",
    "content": "{\n  \"packages\": [\"redis@latest\"],\n  \"shell\": {\n    \"init_hook\": \"\",\n    \"scripts\": {\n      \"redis-cli\": \"redis-cli\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/databases/valkey/README.md",
    "content": "# valkey-7.2.5\n\n## valkey Notes\n\nRunning `devbox services start valkey` will start valkey as a daemon in the background.\n\nYou can manually start Valkey in the foreground by running `valkey-server $VALKEY_CONF --port $VALKEY_PORT`.\n\nLogs, pidfile, and data dumps are stored in `.devbox/virtenv/valkey`. You can change this by modifying the `dir` directive in `devbox.d/valkey/valkey.conf`\n\n## Services\n\n* valkey\n\nUse `devbox services start|stop [service]` to interact with services\n\n## This plugin creates the following helper files\n\n* ./devbox.d/valkey/valkey.conf\n\n## This plugin sets the following environment variables\n\n* VALKEY_PORT=6379\n* VALKEY_CONF=./devbox.d/valkey/valkey.conf\n\nTo show this information, run `devbox info valkey`\n"
  },
  {
    "path": "examples/databases/valkey/devbox.d/valkey/valkey.conf",
    "content": "# Valkey configuration file example.\n#\n# Note that in order to read the configuration file, Valkey must be\n# started with the file path as first argument:\n#\n# ./valkey-server /path/to/valkey.conf\n\n# Note on units: when memory size is needed, it is possible to specify\n# it in the usual form of 1k 5GB 4M and so forth:\n#\n# 1k => 1000 bytes\n# 1kb => 1024 bytes\n# 1m => 1000000 bytes\n# 1mb => 1024*1024 bytes\n# 1g => 1000000000 bytes\n# 1gb => 1024*1024*1024 bytes\n#\n# units are case insensitive so 1GB 1Gb 1gB are all the same.\n\n################################## INCLUDES ###################################\n\n# Include one or more other config files here.  This is useful if you\n# have a standard template that goes to all Valkey servers but also need\n# to customize a few per-server settings.  Include files can include\n# other files, so use this wisely.\n#\n# Notice option \"include\" won't be rewritten by command \"CONFIG REWRITE\"\n# from admin or Valkey Sentinel. Since Valkey always uses the last processed\n# line as value of a configuration directive, you'd better put includes\n# at the beginning of this file to avoid overwriting config change at runtime.\n#\n# If instead you are interested in using includes to override configuration\n# options, it is better to use include as the last line.\n#\n# include /path/to/local.conf\n# include /path/to/other.conf\n\n################################## MODULES #####################################\n\n# Load modules at startup. If the server is not able to load modules\n# it will abort. It is possible to use multiple loadmodule directives.\n#\n# loadmodule /path/to/my_module.so\n# loadmodule /path/to/other_module.so\n\n################################## NETWORK #####################################\n\n# By default, if no \"bind\" configuration directive is specified, Valkey listens\n# for connections from all the network interfaces available on the server.\n# It is possible to listen to just one or multiple selected interfaces using\n# the \"bind\" configuration directive, followed by one or more IP addresses.\n#\n# Examples:\n#\n# bind 192.168.1.100 10.0.0.1\n# bind 127.0.0.1 ::1\n#\n# ~~~ WARNING ~~~ If the computer running Valkey is directly exposed to the\n# internet, binding to all the interfaces is dangerous and will expose the\n# instance to everybody on the internet. So by default we uncomment the\n# following bind directive, that will force Valkey to listen only into\n# the IPv4 lookback interface address (this means Valkey will be able to\n# accept connections only from clients running into the same computer it\n# is running).\n#\n# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES\n# JUST COMMENT THE FOLLOWING LINE.\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nbind 127.0.0.1\n\n# Protected mode is a layer of security protection, in order to avoid that\n# Valkey instances left open on the internet are accessed and exploited.\n#\n# When protected mode is on and if:\n#\n# 1) The server is not binding explicitly to a set of addresses using the\n#    \"bind\" directive.\n# 2) No password is configured.\n#\n# The server only accepts connections from clients connecting from the\n# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain\n# sockets.\n#\n# By default protected mode is enabled. You should disable it only if\n# you are sure you want clients from other hosts to connect to Valkey\n# even if no authentication is configured, nor a specific set of interfaces\n# are explicitly listed using the \"bind\" directive.\nprotected-mode yes\n\n# Accept connections on the specified port, default is 6379 (IANA #815344).\n# If port 0 is specified Valkey will not listen on a TCP socket.\nport 6379\n\n# TCP listen() backlog.\n#\n# In high requests-per-second environments you need an high backlog in order\n# to avoid slow clients connections issues. Note that the Linux kernel\n# will silently truncate it to the value of /proc/sys/net/core/somaxconn so\n# make sure to raise both the value of somaxconn and tcp_max_syn_backlog\n# in order to get the desired effect.\ntcp-backlog 511\n\n# Unix socket.\n#\n# Specify the path for the Unix socket that will be used to listen for\n# incoming connections. There is no default, so Valkey will not listen\n# on a unix socket when not specified.\n#\n# unixsocket /tmp/valkey.sock\n# unixsocketperm 700\n\n# Close the connection after a client is idle for N seconds (0 to disable)\ntimeout 0\n\n# TCP keepalive.\n#\n# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence\n# of communication. This is useful for two reasons:\n#\n# 1) Detect dead peers.\n# 2) Take the connection alive from the point of view of network\n#    equipment in the middle.\n#\n# On Linux, the specified value (in seconds) is the period used to send ACKs.\n# Note that to close the connection the double of the time is needed.\n# On other kernels the period depends on the kernel configuration.\n#\n# A reasonable value for this option is 300 seconds, which is the new\n# Valkey default starting with Valkey 3.2.1.\ntcp-keepalive 300\n\n################################# GENERAL #####################################\n\n# By default Valkey does not run as a daemon. Use 'yes' if you need it.\n# Note that Valkey will write a pid file in /var/run/valkey.pid when daemonized.\ndaemonize no\n\n# If you run Valkey from upstart or systemd, Valkey can interact with your\n# supervision tree. Options:\n#   supervised no      - no supervision interaction\n#   supervised upstart - signal upstart by putting Valkey into SIGSTOP mode\n#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET\n#   supervised auto    - detect upstart or systemd method based on\n#                        UPSTART_JOB or NOTIFY_SOCKET environment variables\n# Note: these supervision methods only signal \"process is ready.\"\n#       They do not enable continuous liveness pings back to your supervisor.\nsupervised no\n\n# If a pid file is specified, Valkey writes it where specified at startup\n# and removes it at exit.\n#\n# When the server runs non daemonized, no pid file is created if none is\n# specified in the configuration. When the server is daemonized, the pid file\n# is used even if not specified, defaulting to \"/var/run/valkey.pid\".\n#\n# Creating a pid file is best effort: if Valkey is not able to create it\n# nothing bad happens, the server will start and run normally.\npidfile valkey.pid\n\n# Specify the server verbosity level.\n# This can be one of:\n# debug (a lot of information, useful for development/testing)\n# verbose (many rarely useful info, but not a mess like the debug level)\n# notice (moderately verbose, what you want in production probably)\n# warning (only very important / critical messages are logged)\nloglevel notice\n\n# Specify the log file name. Also the empty string can be used to force\n# Valkey to log on the standard output. Note that if you use standard\n# output for logging but daemonize, logs will be sent to /dev/null\n# logfile valkey.log\n\n# To enable logging to the system logger, just set 'syslog-enabled' to yes,\n# and optionally update the other syslog parameters to suit your needs.\n# syslog-enabled no\n\n# Specify the syslog identity.\n# syslog-ident valkey\n\n# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.\n# syslog-facility local0\n\n# Set the number of databases. The default database is DB 0, you can select\n# a different one on a per-connection basis using SELECT <dbid> where\n# dbid is a number between 0 and 'databases'-1\ndatabases 16\n\n# By default Valkey shows an ASCII art logo only when started to log to the\n# standard output and if the standard output is a TTY. Basically this means\n# that normally a logo is displayed only in interactive sessions.\n#\n# However it is possible to force the pre-4.0 behavior and always show a\n# ASCII art logo in startup logs by setting the following option to yes.\nalways-show-logo yes\n\n################################ SNAPSHOTTING  ################################\n#\n# Save the DB on disk:\n#\n#   save <seconds> <changes>\n#\n#   Will save the DB if both the given number of seconds and the given\n#   number of write operations against the DB occurred.\n#\n#   In the example below the behaviour will be to save:\n#   after 900 sec (15 min) if at least 1 key changed\n#   after 300 sec (5 min) if at least 10 keys changed\n#   after 60 sec if at least 10000 keys changed\n#\n#   Note: you can disable saving completely by commenting out all \"save\" lines.\n#\n#   It is also possible to remove all the previously configured save\n#   points by adding a save directive with a single empty string argument\n#   like in the following example:\n#\n#   save \"\"\n\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# By default Valkey will stop accepting writes if RDB snapshots are enabled\n# (at least one save point) and the latest background save failed.\n# This will make the user aware (in a hard way) that data is not persisting\n# on disk properly, otherwise chances are that no one will notice and some\n# disaster will happen.\n#\n# If the background saving process will start working again Valkey will\n# automatically allow writes again.\n#\n# However if you have setup your proper monitoring of the Valkey server\n# and persistence, you may want to disable this feature so that Valkey will\n# continue to work as usual even if there are problems with disk,\n# permissions, and so forth.\nstop-writes-on-bgsave-error yes\n\n# Compress string objects using LZF when dump .rdb databases?\n# For default that's set to 'yes' as it's almost always a win.\n# If you want to save some CPU in the saving child set it to 'no' but\n# the dataset will likely be bigger if you have compressible values or keys.\nrdbcompression yes\n\n# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.\n# This makes the format more resistant to corruption but there is a performance\n# hit to pay (around 10%) when saving and loading RDB files, so you can disable it\n# for maximum performances.\n#\n# RDB files created with checksum disabled have a checksum of zero that will\n# tell the loading code to skip the check.\nrdbchecksum yes\n\n# The filename where to dump the DB\ndbfilename dump.rdb\n\n# The working directory.\n#\n# The DB will be written inside this directory, with the filename specified\n# above using the 'dbfilename' configuration directive.\n#\n# The Append Only File will also be created inside this directory.\n#\n# Note that you must specify a directory here, not a file name.\ndir .devbox/virtenv/valkey/\n\n################################# REPLICATION #################################\n\n# Master-Slave replication. Use slaveof to make a Valkey instance a copy of\n# another Valkey server. A few things to understand ASAP about Valkey replication.\n#\n# 1) Valkey replication is asynchronous, but you can configure a master to\n#    stop accepting writes if it appears to be not connected with at least\n#    a given number of slaves.\n# 2) Valkey slaves are able to perform a partial resynchronization with the\n#    master if the replication link is lost for a relatively small amount of\n#    time. You may want to configure the replication backlog size (see the next\n#    sections of this file) with a sensible value depending on your needs.\n# 3) Replication is automatic and does not need user intervention. After a\n#    network partition slaves automatically try to reconnect to masters\n#    and resynchronize with them.\n#\n# slaveof <masterip> <masterport>\n\n# If the master is password protected (using the \"requirepass\" configuration\n# directive below) it is possible to tell the slave to authenticate before\n# starting the replication synchronization process, otherwise the master will\n# refuse the slave request.\n#\n# masterauth <master-password>\n\n# When a slave loses its connection with the master, or when the replication\n# is still in progress, the slave can act in two different ways:\n#\n# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will\n#    still reply to client requests, possibly with out of date data, or the\n#    data set may just be empty if this is the first synchronization.\n#\n# 2) if slave-serve-stale-data is set to 'no' the slave will reply with\n#    an error \"SYNC with master in progress\" to all the kind of commands\n#    but to INFO and SLAVEOF.\n#\nslave-serve-stale-data yes\n\n# You can configure a slave instance to accept writes or not. Writing against\n# a slave instance may be useful to store some ephemeral data (because data\n# written on a slave will be easily deleted after resync with the master) but\n# may also cause problems if clients are writing to it because of a\n# misconfiguration.\n#\n# Since Valkey 2.6 by default slaves are read-only.\n#\n# Note: read only slaves are not designed to be exposed to untrusted clients\n# on the internet. It's just a protection layer against misuse of the instance.\n# Still a read only slave exports by default all the administrative commands\n# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve\n# security of read only slaves using 'rename-command' to shadow all the\n# administrative / dangerous commands.\nslave-read-only yes\n\n# Replication SYNC strategy: disk or socket.\n#\n# -------------------------------------------------------\n# WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY\n# -------------------------------------------------------\n#\n# New slaves and reconnecting slaves that are not able to continue the replication\n# process just receiving differences, need to do what is called a \"full\n# synchronization\". An RDB file is transmitted from the master to the slaves.\n# The transmission can happen in two different ways:\n#\n# 1) Disk-backed: The Valkey master creates a new process that writes the RDB\n#                 file on disk. Later the file is transferred by the parent\n#                 process to the slaves incrementally.\n# 2) Diskless: The Valkey master creates a new process that directly writes the\n#              RDB file to slave sockets, without touching the disk at all.\n#\n# With disk-backed replication, while the RDB file is generated, more slaves\n# can be queued and served with the RDB file as soon as the current child producing\n# the RDB file finishes its work. With diskless replication instead once\n# the transfer starts, new slaves arriving will be queued and a new transfer\n# will start when the current one terminates.\n#\n# When diskless replication is used, the master waits a configurable amount of\n# time (in seconds) before starting the transfer in the hope that multiple slaves\n# will arrive and the transfer can be parallelized.\n#\n# With slow disks and fast (large bandwidth) networks, diskless replication\n# works better.\nrepl-diskless-sync no\n\n# When diskless replication is enabled, it is possible to configure the delay\n# the server waits in order to spawn the child that transfers the RDB via socket\n# to the slaves.\n#\n# This is important since once the transfer starts, it is not possible to serve\n# new slaves arriving, that will be queued for the next RDB transfer, so the server\n# waits a delay in order to let more slaves arrive.\n#\n# The delay is specified in seconds, and by default is 5 seconds. To disable\n# it entirely just set it to 0 seconds and the transfer will start ASAP.\nrepl-diskless-sync-delay 5\n\n# Slaves send PINGs to server in a predefined interval. It's possible to change\n# this interval with the repl_ping_slave_period option. The default value is 10\n# seconds.\n#\n# repl-ping-slave-period 10\n\n# The following option sets the replication timeout for:\n#\n# 1) Bulk transfer I/O during SYNC, from the point of view of slave.\n# 2) Master timeout from the point of view of slaves (data, pings).\n# 3) Slave timeout from the point of view of masters (REPLCONF ACK pings).\n#\n# It is important to make sure that this value is greater than the value\n# specified for repl-ping-slave-period otherwise a timeout will be detected\n# every time there is low traffic between the master and the slave.\n#\n# repl-timeout 60\n\n# Disable TCP_NODELAY on the slave socket after SYNC?\n#\n# If you select \"yes\" Valkey will use a smaller number of TCP packets and\n# less bandwidth to send data to slaves. But this can add a delay for\n# the data to appear on the slave side, up to 40 milliseconds with\n# Linux kernels using a default configuration.\n#\n# If you select \"no\" the delay for data to appear on the slave side will\n# be reduced but more bandwidth will be used for replication.\n#\n# By default we optimize for low latency, but in very high traffic conditions\n# or when the master and slaves are many hops away, turning this to \"yes\" may\n# be a good idea.\nrepl-disable-tcp-nodelay no\n\n# Set the replication backlog size. The backlog is a buffer that accumulates\n# slave data when slaves are disconnected for some time, so that when a slave\n# wants to reconnect again, often a full resync is not needed, but a partial\n# resync is enough, just passing the portion of data the slave missed while\n# disconnected.\n#\n# The bigger the replication backlog, the longer the time the slave can be\n# disconnected and later be able to perform a partial resynchronization.\n#\n# The backlog is only allocated once there is at least a slave connected.\n#\n# repl-backlog-size 1mb\n\n# After a master has no longer connected slaves for some time, the backlog\n# will be freed. The following option configures the amount of seconds that\n# need to elapse, starting from the time the last slave disconnected, for\n# the backlog buffer to be freed.\n#\n# Note that slaves never free the backlog for timeout, since they may be\n# promoted to masters later, and should be able to correctly \"partially\n# resynchronize\" with the slaves: hence they should always accumulate backlog.\n#\n# A value of 0 means to never release the backlog.\n#\n# repl-backlog-ttl 3600\n\n# The slave priority is an integer number published by Valkey in the INFO output.\n# It is used by Valkey Sentinel in order to select a slave to promote into a\n# master if the master is no longer working correctly.\n#\n# A slave with a low priority number is considered better for promotion, so\n# for instance if there are three slaves with priority 10, 100, 25 Sentinel will\n# pick the one with priority 10, that is the lowest.\n#\n# However a special priority of 0 marks the slave as not able to perform the\n# role of master, so a slave with priority of 0 will never be selected by\n# Valkey Sentinel for promotion.\n#\n# By default the priority is 100.\nslave-priority 100\n\n# It is possible for a master to stop accepting writes if there are less than\n# N slaves connected, having a lag less or equal than M seconds.\n#\n# The N slaves need to be in \"online\" state.\n#\n# The lag in seconds, that must be <= the specified value, is calculated from\n# the last ping received from the slave, that is usually sent every second.\n#\n# This option does not GUARANTEE that N replicas will accept the write, but\n# will limit the window of exposure for lost writes in case not enough slaves\n# are available, to the specified number of seconds.\n#\n# For example to require at least 3 slaves with a lag <= 10 seconds use:\n#\n# min-slaves-to-write 3\n# min-slaves-max-lag 10\n#\n# Setting one or the other to 0 disables the feature.\n#\n# By default min-slaves-to-write is set to 0 (feature disabled) and\n# min-slaves-max-lag is set to 10.\n\n# A Valkey master is able to list the address and port of the attached\n# slaves in different ways. For example the \"INFO replication\" section\n# offers this information, which is used, among other tools, by\n# Valkey Sentinel in order to discover slave instances.\n# Another place where this info is available is in the output of the\n# \"ROLE\" command of a master.\n#\n# The listed IP and address normally reported by a slave is obtained\n# in the following way:\n#\n#   IP: The address is auto detected by checking the peer address\n#   of the socket used by the slave to connect with the master.\n#\n#   Port: The port is communicated by the slave during the replication\n#   handshake, and is normally the port that the slave is using to\n#   list for connections.\n#\n# However when port forwarding or Network Address Translation (NAT) is\n# used, the slave may be actually reachable via different IP and port\n# pairs. The following two options can be used by a slave in order to\n# report to its master a specific set of IP and port, so that both INFO\n# and ROLE will report those values.\n#\n# There is no need to use both the options if you need to override just\n# the port or the IP address.\n#\n# slave-announce-ip 5.5.5.5\n# slave-announce-port 1234\n\n################################## SECURITY ###################################\n\n# Require clients to issue AUTH <PASSWORD> before processing any other\n# commands.  This might be useful in environments in which you do not trust\n# others with access to the host running valkey-server.\n#\n# This should stay commented out for backward compatibility and because most\n# people do not need auth (e.g. they run their own servers).\n#\n# Warning: since Valkey is pretty fast an outside user can try up to\n# 150k passwords per second against a good box. This means that you should\n# use a very strong password otherwise it will be very easy to break.\n#\n# requirepass foobared\n\n# Command renaming.\n#\n# It is possible to change the name of dangerous commands in a shared\n# environment. For instance the CONFIG command may be renamed into something\n# hard to guess so that it will still be available for internal-use tools\n# but not available for general clients.\n#\n# Example:\n#\n# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52\n#\n# It is also possible to completely kill a command by renaming it into\n# an empty string:\n#\n# rename-command CONFIG \"\"\n#\n# Please note that changing the name of commands that are logged into the\n# AOF file or transmitted to slaves may cause problems.\n\n################################### CLIENTS ####################################\n\n# Set the max number of connected clients at the same time. By default\n# this limit is set to 10000 clients, however if the Valkey server is not\n# able to configure the process file limit to allow for the specified limit\n# the max number of allowed clients is set to the current file limit\n# minus 32 (as Valkey reserves a few file descriptors for internal uses).\n#\n# Once the limit is reached Valkey will close all the new connections sending\n# an error 'max number of clients reached'.\n#\n# maxclients 10000\n\n############################## MEMORY MANAGEMENT ################################\n\n# Set a memory usage limit to the specified amount of bytes.\n# When the memory limit is reached Valkey will try to remove keys\n# according to the eviction policy selected (see maxmemory-policy).\n#\n# If Valkey can't remove keys according to the policy, or if the policy is\n# set to 'noeviction', Valkey will start to reply with errors to commands\n# that would use more memory, like SET, LPUSH, and so on, and will continue\n# to reply to read-only commands like GET.\n#\n# This option is usually useful when using Valkey as an LRU or LFU cache, or to\n# set a hard memory limit for an instance (using the 'noeviction' policy).\n#\n# WARNING: If you have slaves attached to an instance with maxmemory on,\n# the size of the output buffers needed to feed the slaves are subtracted\n# from the used memory count, so that network problems / resyncs will\n# not trigger a loop where keys are evicted, and in turn the output\n# buffer of slaves is full with DELs of keys evicted triggering the deletion\n# of more keys, and so forth until the database is completely emptied.\n#\n# In short... if you have slaves attached it is suggested that you set a lower\n# limit for maxmemory so that there is some free RAM on the system for slave\n# output buffers (but this is not needed if the policy is 'noeviction').\n#\n# maxmemory <bytes>\n\n# MAXMEMORY POLICY: how Valkey will select what to remove when maxmemory\n# is reached. You can select among five behaviors:\n#\n# volatile-lru -> Evict using approximated LRU among the keys with an expire set.\n# allkeys-lru -> Evict any key using approximated LRU.\n# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.\n# allkeys-lfu -> Evict any key using approximated LFU.\n# volatile-random -> Remove a random key among the ones with an expire set.\n# allkeys-random -> Remove a random key, any key.\n# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)\n# noeviction -> Don't evict anything, just return an error on write operations.\n#\n# LRU means Least Recently Used\n# LFU means Least Frequently Used\n#\n# Both LRU, LFU and volatile-ttl are implemented using approximated\n# randomized algorithms.\n#\n# Note: with any of the above policies, Valkey will return an error on write\n#       operations, when there are no suitable keys for eviction.\n#\n#       At the date of writing these commands are: set setnx setex append\n#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd\n#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby\n#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby\n#       getset mset msetnx exec sort\n#\n# The default is:\n#\n# maxmemory-policy noeviction\n\n# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated\n# algorithms (in order to save memory), so you can tune it for speed or\n# accuracy. For default Valkey will check five keys and pick the one that was\n# used less recently, you can change the sample size using the following\n# configuration directive.\n#\n# The default of 5 produces good enough results. 10 Approximates very closely\n# true LRU but costs more CPU. 3 is faster but not very accurate.\n#\n# maxmemory-samples 5\n\n############################# LAZY FREEING ####################################\n\n# Valkey has two primitives to delete keys. One is called DEL and is a blocking\n# deletion of the object. It means that the server stops processing new commands\n# in order to reclaim all the memory associated with an object in a synchronous\n# way. If the key deleted is associated with a small object, the time needed\n# in order to execute the DEL command is very small and comparable to most other\n# O(1) or O(log_N) commands in Valkey. However if the key is associated with an\n# aggregated value containing millions of elements, the server can block for\n# a long time (even seconds) in order to complete the operation.\n#\n# For the above reasons Valkey also offers non blocking deletion primitives\n# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and\n# FLUSHDB commands, in order to reclaim memory in background. Those commands\n# are executed in constant time. Another thread will incrementally free the\n# object in the background as fast as possible.\n#\n# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.\n# It's up to the design of the application to understand when it is a good\n# idea to use one or the other. However the Valkey server sometimes has to\n# delete keys or flush the whole database as a side effect of other operations.\n# Specifically Valkey deletes objects independently of a user call in the\n# following scenarios:\n#\n# 1) On eviction, because of the maxmemory and maxmemory policy configurations,\n#    in order to make room for new data, without going over the specified\n#    memory limit.\n# 2) Because of expire: when a key with an associated time to live (see the\n#    EXPIRE command) must be deleted from memory.\n# 3) Because of a side effect of a command that stores data on a key that may\n#    already exist. For example the RENAME command may delete the old key\n#    content when it is replaced with another one. Similarly SUNIONSTORE\n#    or SORT with STORE option may delete existing keys. The SET command\n#    itself removes any old content of the specified key in order to replace\n#    it with the specified string.\n# 4) During replication, when a slave performs a full resynchronization with\n#    its master, the content of the whole database is removed in order to\n#    load the RDB file just transferred.\n#\n# In all the above cases the default is to delete objects in a blocking way,\n# like if DEL was called. However you can configure each case specifically\n# in order to instead release memory in a non-blocking way like if UNLINK\n# was called, using the following configuration directives:\n\nlazyfree-lazy-eviction no\nlazyfree-lazy-expire no\nlazyfree-lazy-server-del no\nslave-lazy-flush no\n\n############################## APPEND ONLY MODE ###############################\n\n# By default Valkey asynchronously dumps the dataset on disk. This mode is\n# good enough in many applications, but an issue with the Valkey process or\n# a power outage may result into a few minutes of writes lost (depending on\n# the configured save points).\n#\n# The Append Only File is an alternative persistence mode that provides\n# much better durability. For instance using the default data fsync policy\n# (see later in the config file) Valkey can lose just one second of writes in a\n# dramatic event like a server power outage, or a single write if something\n# wrong with the Valkey process itself happens, but the operating system is\n# still running correctly.\n#\n# AOF and RDB persistence can be enabled at the same time without problems.\n# If the AOF is enabled on startup Valkey will load the AOF, that is the file\n# with the better durability guarantees.\n#\n# Please check https://valkey.io/docs/topics/persistence/ for more information.\n\nappendonly no\n\n# The name of the append only file (default: \"appendonly.aof\")\n\nappendfilename \"appendonly.aof\"\n\n# The fsync() call tells the Operating System to actually write data on disk\n# instead of waiting for more data in the output buffer. Some OS will really flush\n# data on disk, some other OS will just try to do it ASAP.\n#\n# Valkey supports three different modes:\n#\n# no: don't fsync, just let the OS flush the data when it wants. Faster.\n# always: fsync after every write to the append only log. Slow, Safest.\n# everysec: fsync only one time every second. Compromise.\n#\n# The default is \"everysec\", as that's usually the right compromise between\n# speed and data safety. It's up to you to understand if you can relax this to\n# \"no\" that will let the operating system flush the output buffer when\n# it wants, for better performances (but if you can live with the idea of\n# some data loss consider the default persistence mode that's snapshotting),\n# or on the contrary, use \"always\" that's very slow but a bit safer than\n# everysec.\n#\n# More details please check the following article:\n# http://antirez.com/post/redis-persistence-demystified.html\n#\n# If unsure, use \"everysec\".\n\n# appendfsync always\nappendfsync everysec\n# appendfsync no\n\n# When the AOF fsync policy is set to always or everysec, and a background\n# saving process (a background save or AOF log background rewriting) is\n# performing a lot of I/O against the disk, in some Linux configurations\n# Valkey may block too long on the fsync() call. Note that there is no fix for\n# this currently, as even performing fsync in a different thread will block\n# our synchronous write(2) call.\n#\n# In order to mitigate this problem it's possible to use the following option\n# that will prevent fsync() from being called in the main process while a\n# BGSAVE or BGREWRITEAOF is in progress.\n#\n# This means that while another child is saving, the durability of Valkey is\n# the same as \"appendfsync none\". In practical terms, this means that it is\n# possible to lose up to 30 seconds of log in the worst scenario (with the\n# default Linux settings).\n#\n# If you have latency problems turn this to \"yes\". Otherwise leave it as\n# \"no\" that is the safest pick from the point of view of durability.\n\nno-appendfsync-on-rewrite no\n\n# Automatic rewrite of the append only file.\n# Valkey is able to automatically rewrite the log file implicitly calling\n# BGREWRITEAOF when the AOF log size grows by the specified percentage.\n#\n# This is how it works: Valkey remembers the size of the AOF file after the\n# latest rewrite (if no rewrite has happened since the restart, the size of\n# the AOF at startup is used).\n#\n# This base size is compared to the current size. If the current size is\n# bigger than the specified percentage, the rewrite is triggered. Also\n# you need to specify a minimal size for the AOF file to be rewritten, this\n# is useful to avoid rewriting the AOF file even if the percentage increase\n# is reached but it is still pretty small.\n#\n# Specify a percentage of zero in order to disable the automatic AOF\n# rewrite feature.\n\nauto-aof-rewrite-percentage 100\nauto-aof-rewrite-min-size 64mb\n\n# An AOF file may be found to be truncated at the end during the Valkey\n# startup process, when the AOF data gets loaded back into memory.\n# This may happen when the system where Valkey is running\n# crashes, especially when an ext4 filesystem is mounted without the\n# data=ordered option (however this can't happen when Valkey itself\n# crashes or aborts but the operating system still works correctly).\n#\n# Valkey can either exit with an error when this happens, or load as much\n# data as possible (the default now) and start if the AOF file is found\n# to be truncated at the end. The following option controls this behavior.\n#\n# If aof-load-truncated is set to yes, a truncated AOF file is loaded and\n# the Valkey server starts emitting a log to inform the user of the event.\n# Otherwise if the option is set to no, the server aborts with an error\n# and refuses to start. When the option is set to no, the user requires\n# to fix the AOF file using the \"valkey-check-aof\" utility before to restart\n# the server.\n#\n# Note that if the AOF file will be found to be corrupted in the middle\n# the server will still exit with an error. This option only applies when\n# Valkey will try to read more data from the AOF file but not enough bytes\n# will be found.\naof-load-truncated yes\n\n# When rewriting the AOF file, Valkey is able to use an RDB preamble in the\n# AOF file for faster rewrites and recoveries. When this option is turned\n# on the rewritten AOF file is composed of two different stanzas:\n#\n#   [RDB file][AOF tail]\n#\n# When loading Valkey recognizes that the AOF file starts with the \"VALKEY\"\n# string and loads the prefixed RDB file, and continues loading the AOF\n# tail.\n#\n# This is currently turned off by default in order to avoid the surprise\n# of a format change, but will at some point be used as the default.\naof-use-rdb-preamble no\n\n################################ LUA SCRIPTING  ###############################\n\n# Max execution time of a Lua script in milliseconds.\n#\n# If the maximum execution time is reached Valkey will log that a script is\n# still in execution after the maximum allowed time and will start to\n# reply to queries with an error.\n#\n# When a long running script exceeds the maximum execution time only the\n# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be\n# used to stop a script that did not yet called write commands. The second\n# is the only way to shut down the server in the case a write command was\n# already issued by the script but the user doesn't want to wait for the natural\n# termination of the script.\n#\n# Set it to 0 or a negative value for unlimited execution without warnings.\nlua-time-limit 5000\n\n################################ VALKEY CLUSTER  ###############################\n#\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n# WARNING EXPERIMENTAL: Valkey Cluster is considered to be stable code, however\n# in order to mark it as \"mature\" we need to wait for a non trivial percentage\n# of users to deploy it in production.\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n#\n# Normal Valkey instances can't be part of a Valkey Cluster; only nodes that are\n# started as cluster nodes can. In order to start a Valkey instance as a\n# cluster node enable the cluster support uncommenting the following:\n#\n# cluster-enabled yes\n\n# Every cluster node has a cluster configuration file. This file is not\n# intended to be edited by hand. It is created and updated by Valkey nodes.\n# Every Valkey Cluster node requires a different cluster configuration file.\n# Make sure that instances running in the same system do not have\n# overlapping cluster configuration file names.\n#\n# cluster-config-file nodes-6379.conf\n\n# Cluster node timeout is the amount of milliseconds a node must be unreachable\n# for it to be considered in failure state.\n# Most other internal time limits are multiple of the node timeout.\n#\n# cluster-node-timeout 15000\n\n# A slave of a failing master will avoid to start a failover if its data\n# looks too old.\n#\n# There is no simple way for a slave to actually have an exact measure of\n# its \"data age\", so the following two checks are performed:\n#\n# 1) If there are multiple slaves able to failover, they exchange messages\n#    in order to try to give an advantage to the slave with the best\n#    replication offset (more data from the master processed).\n#    Slaves will try to get their rank by offset, and apply to the start\n#    of the failover a delay proportional to their rank.\n#\n# 2) Every single slave computes the time of the last interaction with\n#    its master. This can be the last ping or command received (if the master\n#    is still in the \"connected\" state), or the time that elapsed since the\n#    disconnection with the master (if the replication link is currently down).\n#    If the last interaction is too old, the slave will not try to failover\n#    at all.\n#\n# The point \"2\" can be tuned by user. Specifically a slave will not perform\n# the failover if, since the last interaction with the master, the time\n# elapsed is greater than:\n#\n#   (node-timeout * slave-validity-factor) + repl-ping-slave-period\n#\n# So for example if node-timeout is 30 seconds, and the slave-validity-factor\n# is 10, and assuming a default repl-ping-slave-period of 10 seconds, the\n# slave will not try to failover if it was not able to talk with the master\n# for longer than 310 seconds.\n#\n# A large slave-validity-factor may allow slaves with too old data to failover\n# a master, while a too small value may prevent the cluster from being able to\n# elect a slave at all.\n#\n# For maximum availability, it is possible to set the slave-validity-factor\n# to a value of 0, which means, that slaves will always try to failover the\n# master regardless of the last time they interacted with the master.\n# (However they'll always try to apply a delay proportional to their\n# offset rank).\n#\n# Zero is the only value able to guarantee that when all the partitions heal\n# the cluster will always be able to continue.\n#\n# cluster-slave-validity-factor 10\n\n# Cluster slaves are able to migrate to orphaned masters, that are masters\n# that are left without working slaves. This improves the cluster ability\n# to resist to failures as otherwise an orphaned master can't be failed over\n# in case of failure if it has no working slaves.\n#\n# Slaves migrate to orphaned masters only if there are still at least a\n# given number of other working slaves for their old master. This number\n# is the \"migration barrier\". A migration barrier of 1 means that a slave\n# will migrate only if there is at least 1 other working slave for its master\n# and so forth. It usually reflects the number of slaves you want for every\n# master in your cluster.\n#\n# Default is 1 (slaves migrate only if their masters remain with at least\n# one slave). To disable migration just set it to a very large value.\n# A value of 0 can be set but is useful only for debugging and dangerous\n# in production.\n#\n# cluster-migration-barrier 1\n\n# By default Valkey Cluster nodes stop accepting queries if they detect there\n# is at least an hash slot uncovered (no available node is serving it).\n# This way if the cluster is partially down (for example a range of hash slots\n# are no longer covered) all the cluster becomes, eventually, unavailable.\n# It automatically returns available as soon as all the slots are covered again.\n#\n# However sometimes you want the subset of the cluster which is working,\n# to continue to accept queries for the part of the key space that is still\n# covered. In order to do so, just set the cluster-require-full-coverage\n# option to no.\n#\n# cluster-require-full-coverage yes\n\n# This option, when set to yes, prevents slaves from trying to failover its\n# master during master failures. However the master can still perform a\n# manual failover, if forced to do so.\n#\n# This is useful in different scenarios, especially in the case of multiple\n# data center operations, where we want one side to never be promoted if not\n# in the case of a total DC failure.\n#\n# cluster-slave-no-failover no\n\n# In order to setup your cluster make sure to read the documentation\n# available at http://valkey.io web site.\n\n########################## CLUSTER DOCKER/NAT support  ########################\n\n# In certain deployments, Valkey Cluster nodes address discovery fails, because\n# addresses are NAT-ted or because ports are forwarded (the typical case is\n# Docker and other containers).\n#\n# In order to make Valkey Cluster working in such environments, a static\n# configuration where each node knows its public address is needed. The\n# following two options are used for this scope, and are:\n#\n# * cluster-announce-ip\n# * cluster-announce-port\n# * cluster-announce-bus-port\n#\n# Each instruct the node about its address, client port, and cluster message\n# bus port. The information is then published in the header of the bus packets\n# so that other nodes will be able to correctly map the address of the node\n# publishing the information.\n#\n# If the above options are not used, the normal Valkey Cluster auto-detection\n# will be used instead.\n#\n# Note that when remapped, the bus port may not be at the fixed offset of\n# clients port + 10000, so you can specify any port and bus-port depending\n# on how they get remapped. If the bus-port is not set, a fixed offset of\n# 10000 will be used as usually.\n#\n# Example:\n#\n# cluster-announce-ip 10.1.1.5\n# cluster-announce-port 6379\n# cluster-announce-bus-port 6380\n\n################################## SLOW LOG ###################################\n\n# The Valkey Slow Log is a system to log queries that exceeded a specified\n# execution time. The execution time does not include the I/O operations\n# like talking with the client, sending the reply and so forth,\n# but just the time needed to actually execute the command (this is the only\n# stage of command execution where the thread is blocked and can not serve\n# other requests in the meantime).\n#\n# You can configure the slow log with two parameters: one tells Valkey\n# what is the execution time, in microseconds, to exceed in order for the\n# command to get logged, and the other parameter is the length of the\n# slow log. When a new command is logged the oldest one is removed from the\n# queue of logged commands.\n\n# The following time is expressed in microseconds, so 1000000 is equivalent\n# to one second. Note that a negative number disables the slow log, while\n# a value of zero forces the logging of every command.\nslowlog-log-slower-than 10000\n\n# There is no limit to this length. Just be aware that it will consume memory.\n# You can reclaim memory used by the slow log with SLOWLOG RESET.\nslowlog-max-len 128\n\n################################ LATENCY MONITOR ##############################\n\n# The Valkey latency monitoring subsystem samples different operations\n# at runtime in order to collect data related to possible sources of\n# latency of a Valkey instance.\n#\n# Via the LATENCY command this information is available to the user that can\n# print graphs and obtain reports.\n#\n# The system only logs operations that were performed in a time equal or\n# greater than the amount of milliseconds specified via the\n# latency-monitor-threshold configuration directive. When its value is set\n# to zero, the latency monitor is turned off.\n#\n# By default latency monitoring is disabled since it is mostly not needed\n# if you don't have latency issues, and collecting data has a performance\n# impact, that while very small, can be measured under big load. Latency\n# monitoring can easily be enabled at runtime using the command\n# \"CONFIG SET latency-monitor-threshold <milliseconds>\" if needed.\nlatency-monitor-threshold 0\n\n############################# EVENT NOTIFICATION ##############################\n\n# Valkey can notify Pub/Sub clients about events happening in the key space.\n# This feature is documented at https://valkey.io/docs/topics/notifications/\n#\n# For instance if keyspace events notification is enabled, and a client\n# performs a DEL operation on key \"foo\" stored in the Database 0, two\n# messages will be published via Pub/Sub:\n#\n# PUBLISH __keyspace@0__:foo del\n# PUBLISH __keyevent@0__:del foo\n#\n# It is possible to select the events that Valkey will notify among a set\n# of classes. Every class is identified by a single character:\n#\n#  K     Keyspace events, published with __keyspace@<db>__ prefix.\n#  E     Keyevent events, published with __keyevent@<db>__ prefix.\n#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...\n#  $     String commands\n#  l     List commands\n#  s     Set commands\n#  h     Hash commands\n#  z     Sorted set commands\n#  x     Expired events (events generated every time a key expires)\n#  e     Evicted events (events generated when a key is evicted for maxmemory)\n#  A     Alias for g$lshzxe, so that the \"AKE\" string means all the events.\n#\n#  The \"notify-keyspace-events\" takes as argument a string that is composed\n#  of zero or multiple characters. The empty string means that notifications\n#  are disabled.\n#\n#  Example: to enable list and generic events, from the point of view of the\n#           event name, use:\n#\n#  notify-keyspace-events Elg\n#\n#  Example 2: to get the stream of the expired keys subscribing to channel\n#             name __keyevent@0__:expired use:\n#\n#  notify-keyspace-events Ex\n#\n#  By default all notifications are disabled because most users don't need\n#  this feature and the feature has some overhead. Note that if you don't\n#  specify at least one of K or E, no events will be delivered.\nnotify-keyspace-events \"\"\n\n############################### ADVANCED CONFIG ###############################\n\n# Hashes are encoded using a memory efficient data structure when they have a\n# small number of entries, and the biggest entry does not exceed a given\n# threshold. These thresholds can be configured using the following directives.\nhash-max-ziplist-entries 512\nhash-max-ziplist-value 64\n\n# Lists are also encoded in a special way to save a lot of space.\n# The number of entries allowed per internal list node can be specified\n# as a fixed maximum size or a maximum number of elements.\n# For a fixed maximum size, use -5 through -1, meaning:\n# -5: max size: 64 Kb  <-- not recommended for normal workloads\n# -4: max size: 32 Kb  <-- not recommended\n# -3: max size: 16 Kb  <-- probably not recommended\n# -2: max size: 8 Kb   <-- good\n# -1: max size: 4 Kb   <-- good\n# Positive numbers mean store up to _exactly_ that number of elements\n# per list node.\n# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),\n# but if your use case is unique, adjust the settings as necessary.\nlist-max-ziplist-size -2\n\n# Lists may also be compressed.\n# Compress depth is the number of quicklist ziplist nodes from *each* side of\n# the list to *exclude* from compression.  The head and tail of the list\n# are always uncompressed for fast push/pop operations.  Settings are:\n# 0: disable all list compression\n# 1: depth 1 means \"don't start compressing until after 1 node into the list,\n#    going from either the head or tail\"\n#    So: [head]->node->node->...->node->[tail]\n#    [head], [tail] will always be uncompressed; inner nodes will compress.\n# 2: [head]->[next]->node->node->...->node->[prev]->[tail]\n#    2 here means: don't compress head or head->next or tail->prev or tail,\n#    but compress all nodes between them.\n# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]\n# etc.\nlist-compress-depth 0\n\n# Sets have a special encoding in just one case: when a set is composed\n# of just strings that happen to be integers in radix 10 in the range\n# of 64 bit signed integers.\n# The following configuration setting sets the limit in the size of the\n# set in order to use this special memory saving encoding.\nset-max-intset-entries 512\n\n# Similarly to hashes and lists, sorted sets are also specially encoded in\n# order to save a lot of space. This encoding is only used when the length and\n# elements of a sorted set are below the following limits:\nzset-max-ziplist-entries 128\nzset-max-ziplist-value 64\n\n# HyperLogLog sparse representation bytes limit. The limit includes the\n# 16 bytes header. When an HyperLogLog using the sparse representation crosses\n# this limit, it is converted into the dense representation.\n#\n# A value greater than 16000 is totally useless, since at that point the\n# dense representation is more memory efficient.\n#\n# The suggested value is ~ 3000 in order to have the benefits of\n# the space efficient encoding without slowing down too much PFADD,\n# which is O(N) with the sparse encoding. The value can be raised to\n# ~ 10000 when CPU is not a concern, but space is, and the data set is\n# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.\nhll-sparse-max-bytes 3000\n\n# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in\n# order to help rehashing the main Valkey hash table (the one mapping top-level\n# keys to values). The hash table implementation Valkey uses (see dict.c)\n# performs a lazy rehashing: the more operation you run into a hash table\n# that is rehashing, the more rehashing \"steps\" are performed, so if the\n# server is idle the rehashing is never complete and some more memory is used\n# by the hash table.\n#\n# The default is to use this millisecond 10 times every second in order to\n# actively rehash the main dictionaries, freeing memory when possible.\n#\n# If unsure:\n# use \"activerehashing no\" if you have hard latency requirements and it is\n# not a good thing in your environment that Valkey can reply from time to time\n# to queries with 2 milliseconds delay.\n#\n# use \"activerehashing yes\" if you don't have such hard requirements but\n# want to free memory asap when possible.\nactiverehashing yes\n\n# The client output buffer limits can be used to force disconnection of clients\n# that are not reading data from the server fast enough for some reason (a\n# common reason is that a Pub/Sub client can't consume messages as fast as the\n# publisher can produce them).\n#\n# The limit can be set differently for the three different classes of clients:\n#\n# normal -> normal clients including MONITOR clients\n# slave  -> slave clients\n# pubsub -> clients subscribed to at least one pubsub channel or pattern\n#\n# The syntax of every client-output-buffer-limit directive is the following:\n#\n# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>\n#\n# A client is immediately disconnected once the hard limit is reached, or if\n# the soft limit is reached and remains reached for the specified number of\n# seconds (continuously).\n# So for instance if the hard limit is 32 megabytes and the soft limit is\n# 16 megabytes / 10 seconds, the client will get disconnected immediately\n# if the size of the output buffers reach 32 megabytes, but will also get\n# disconnected if the client reaches 16 megabytes and continuously overcomes\n# the limit for 10 seconds.\n#\n# By default normal clients are not limited because they don't receive data\n# without asking (in a push way), but just after a request, so only\n# asynchronous clients may create a scenario where data is requested faster\n# than it can read.\n#\n# Instead there is a default limit for pubsub and slave clients, since\n# subscribers and slaves receive data in a push fashion.\n#\n# Both the hard or the soft limit can be disabled by setting them to zero.\nclient-output-buffer-limit normal 0 0 0\nclient-output-buffer-limit slave 256mb 64mb 60\nclient-output-buffer-limit pubsub 32mb 8mb 60\n\n# Client query buffers accumulate new commands. They are limited to a fixed\n# amount by default in order to avoid that a protocol desynchronization (for\n# instance due to a bug in the client) will lead to unbound memory usage in\n# the query buffer. However you can configure it here if you have very special\n# needs, such us huge multi/exec requests or alike.\n#\n# client-query-buffer-limit 1gb\n\n# In the Valkey protocol, bulk requests, that are, elements representing single\n# strings, are normally limited to 512 mb. However you can change this limit\n# here.\n#\n# proto-max-bulk-len 512mb\n\n# Valkey calls an internal function to perform many background tasks, like\n# closing connections of clients in timeout, purging expired keys that are\n# never requested, and so forth.\n#\n# Not all tasks are performed with the same frequency, but Valkey checks for\n# tasks to perform according to the specified \"hz\" value.\n#\n# By default \"hz\" is set to 10. Raising the value will use more CPU when\n# Valkey is idle, but at the same time will make Valkey more responsive when\n# there are many keys expiring at the same time, and timeouts may be\n# handled with more precision.\n#\n# The range is between 1 and 500, however a value over 100 is usually not\n# a good idea. Most users should use the default of 10 and raise this up to\n# 100 only in environments where very low latency is required.\nhz 10\n\n# When a child rewrites the AOF file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\naof-rewrite-incremental-fsync yes\n\n# Valkey LFU eviction (see maxmemory setting) can be tuned. However it is a good\n# idea to start with the default settings and only change them after investigating\n# how to improve the performances and how the keys LFU change over time, which\n# is possible to inspect via the OBJECT FREQ command.\n#\n# There are two tunable parameters in the Valkey LFU implementation: the\n# counter logarithm factor and the counter decay time. It is important to\n# understand what the two parameters mean before changing them.\n#\n# The LFU counter is just 8 bits per key, it's maximum value is 255, so Valkey\n# uses a probabilistic increment with logarithmic behavior. Given the value\n# of the old counter, when a key is accessed, the counter is incremented in\n# this way:\n#\n# 1. A random number R between 0 and 1 is extracted.\n# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).\n# 3. The counter is incremented only if R < P.\n#\n# The default lfu-log-factor is 10. This is a table of how the frequency\n# counter changes with a different number of accesses with different\n# logarithmic factors:\n#\n# +--------+------------+------------+------------+------------+------------+\n# | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |\n# +--------+------------+------------+------------+------------+------------+\n# | 0      | 104        | 255        | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 1      | 18         | 49         | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 10     | 10         | 18         | 142        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 100    | 8          | 11         | 49         | 143        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n#\n# NOTE: The above table was obtained by running the following commands:\n#\n#   valkey-benchmark -n 1000000 incr foo\n#   valkey-cli object freq foo\n#\n# NOTE 2: The counter initial value is 5 in order to give new objects a chance\n# to accumulate hits.\n#\n# The counter decay time is the time, in minutes, that must elapse in order\n# for the key counter to be divided by two (or decremented if it has a value\n# less <= 10).\n#\n# The default value for the lfu-decay-time is 1. A Special value of 0 means to\n# decay the counter every time it happens to be scanned.\n#\n# lfu-log-factor 10\n# lfu-decay-time 1\n\n########################### ACTIVE DEFRAGMENTATION #######################\n#\n# WARNING THIS FEATURE IS EXPERIMENTAL. However it was stress tested\n# even in production and manually tested by multiple engineers for some\n# time.\n#\n# What is active defragmentation?\n# -------------------------------\n#\n# Active (online) defragmentation allows a Valkey server to compact the\n# spaces left between small allocations and deallocations of data in memory,\n# thus allowing to reclaim back memory.\n#\n# Fragmentation is a natural process that happens with every allocator (but\n# less so with Jemalloc, fortunately) and certain workloads. Normally a server\n# restart is needed in order to lower the fragmentation, or at least to flush\n# away all the data and create it again. However thanks to this feature\n# implemented by Oran Agra for Valkey 4.0 this process can happen at runtime\n# in an \"hot\" way, while the server is running.\n#\n# Basically when the fragmentation is over a certain level (see the\n# configuration options below) Valkey will start to create new copies of the\n# values in contiguous memory regions by exploiting certain specific Jemalloc\n# features (in order to understand if an allocation is causing fragmentation\n# and to allocate it in a better place), and at the same time, will release the\n# old copies of the data. This process, repeated incrementally for all the keys\n# will cause the fragmentation to drop back to normal values.\n#\n# Important things to understand:\n#\n# 1. This feature is disabled by default, and only works if you compiled Valkey\n#    to use the copy of Jemalloc we ship with the source code of Valkey.\n#    This is the default with Linux builds.\n#\n# 2. You never need to enable this feature if you don't have fragmentation\n#    issues.\n#\n# 3. Once you experience fragmentation, you can enable this feature when\n#    needed with the command \"CONFIG SET activedefrag yes\".\n#\n# The configuration parameters are able to fine tune the behavior of the\n# defragmentation process. If you are not sure about what they mean it is\n# a good idea to leave the defaults untouched.\n\n# Enabled active defragmentation\n# activedefrag yes\n\n# Minimum amount of fragmentation waste to start active defrag\n# active-defrag-ignore-bytes 100mb\n\n# Minimum percentage of fragmentation to start active defrag\n# active-defrag-threshold-lower 10\n\n# Maximum percentage of fragmentation at which we use maximum effort\n# active-defrag-threshold-upper 100\n\n# Minimal effort for defrag in CPU percentage\n# active-defrag-cycle-min 25\n\n# Maximal effort for defrag in CPU percentage\n# active-defrag-cycle-max 75"
  },
  {
    "path": "examples/databases/valkey/devbox.json",
    "content": "{\n  \"packages\": [\"valkey@latest\"],\n  \"shell\": {\n    \"init_hook\": \"\",\n    \"scripts\": {\n      \"valkey-cli\": \"valkey-cli\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/bun/.gitignore",
    "content": "# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore\n\n# Logs\n\nlogs\n_.log\nnpm-debug.log_\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Caches\n\n.cache\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\n\nreport.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json\n\n# Runtime data\n\npids\n_.pid\n_.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\n\nlib-cov\n\n# Coverage directory used by tools like istanbul\n\ncoverage\n*.lcov\n\n# nyc test coverage\n\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n\n.grunt\n\n# Bower dependency directory (https://bower.io/)\n\nbower_components\n\n# node-waf configuration\n\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\n\nbuild/Release\n\n# Dependency directories\n\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\n\nweb_modules/\n\n# TypeScript cache\n\n*.tsbuildinfo\n\n# Optional npm cache directory\n\n.npm\n\n# Optional eslint cache\n\n.eslintcache\n\n# Optional stylelint cache\n\n.stylelintcache\n\n# Microbundle cache\n\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n\n.node_repl_history\n\n# Output of 'npm pack'\n\n*.tgz\n\n# Yarn Integrity file\n\n.yarn-integrity\n\n# dotenv environment variable files\n\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n\n.parcel-cache\n\n# Next.js build output\n\n.next\nout\n\n# Nuxt.js build / generate output\n\n.nuxt\ndist\n\n# Gatsby files\n\n# Comment in the public line in if your project uses Gatsby and not Next.js\n\n# https://nextjs.org/blog/next-9-1#public-directory-support\n\n# public\n\n# vuepress build output\n\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n\n.temp\n\n# Docusaurus cache and generated files\n\n.docusaurus\n\n# Serverless directories\n\n.serverless/\n\n# FuseBox cache\n\n.fusebox/\n\n# DynamoDB Local files\n\n.dynamodb/\n\n# TernJS port file\n\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n\n.vscode-test\n\n# yarn v2\n\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# IntelliJ based IDEs\n.idea\n\n# Finder (MacOS) folder config\n.DS_Store\n"
  },
  {
    "path": "examples/development/bun/README.md",
    "content": "# Bun\n\nBun projects can be run in Devbox by adding the Bun runtime + package manager to your project.\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/bun)\n\n\n## Add Bun to your Project\n\n```bash\ndevbox add bun@latest\n```\n\nYou can see which versions of `bun` are available using:\n\n```bash\ndevbox search bun\n```\n\nTo update bun to the latest version:\n\n```bash\ndevbox update bun\n```\n\n## Scripts\n\nTo install dependencies:\n\n```bash\ndevbox run bun install\n```\n\nTo start + watch your project:\n\n```bash\ndevbox run dev\n```\n\nThis project was created using `bun init` in bun v1.0.33. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.\n"
  },
  {
    "path": "examples/development/bun/devbox.json",
    "content": "{\n  \"$schema\":  \"https://raw.githubusercontent.com/jetify-com/devbox/0.10.1/.schema/devbox.schema.json\",\n  \"packages\": [\"bun@latest\"],\n  \"shell\": {\n    \"init_hook\": [],\n    \"scripts\": {\n      \"build\": \"bun build ./index.ts\",\n      \"test\": \"bun test\",\n      \"dev\": \"bun --watch run index.ts\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/bun/index.test.ts",
    "content": "import { expect, test } from \"bun:test\";\n\ntest(\"2 + 2\", () => {\n  expect(2 + 2).toBe(4);\n});"
  },
  {
    "path": "examples/development/bun/index.ts",
    "content": "import figlet from 'figlet';\n\nconst server = Bun.serve({\n  port: 3000,\n  fetch(req) {\n    const body = figlet.textSync(\"Bun!\");\n    return new Response(body);\n  },\n});\n\nconsole.log(`Listening on http://localhost:${server.port} ...`);\n"
  },
  {
    "path": "examples/development/bun/package.json",
    "content": "{\n  \"name\": \"bun\",\n  \"module\": \"index.tx\",\n  \"type\": \"module\",\n  \"devDependencies\": {\n    \"@types/bun\": \"latest\"\n  },\n  \"peerDependencies\": {\n    \"typescript\": \"^5.0.0\"\n  },\n  \"dependencies\": {\n    \"figlet\": \"^1.7.0\"\n  }\n}"
  },
  {
    "path": "examples/development/bun/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    // Enable latest features\n    \"lib\": [\"ESNext\"],\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleDetection\": \"force\",\n    \"jsx\": \"react-jsx\",\n    \"allowJs\": true,\n\n    // Bundler mode\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n\n    // Best practices\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"noFallthroughCasesInSwitch\": true,\n\n    // Some stricter flags (disabled by default)\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noPropertyAccessFromIndexSignature\": false\n  }\n}\n"
  },
  {
    "path": "examples/development/csharp/hello-world/.gitignore",
    "content": "bin/\nobj/\n"
  },
  {
    "path": "examples/development/csharp/hello-world/Program.cs",
    "content": "﻿namespace csharp_10_dotnet_6_with_package;\r\n\r\nusing Newtonsoft.Json;\r\n\r\nclass Product \r\n{\r\n  public string Name;\r\n  public DateTime Expiry;\r\n  public string[] Sizes;\r\n\r\n  public Product() \r\n  {\r\n    Name = \"\";\r\n    Sizes = new string[] {};\r\n  } \r\n}\r\n\r\nclass Program\r\n{\r\n    static void Main(string[] args)\r\n    {\r\n        Product product = new Product();\r\n        product.Name = \"Apple\";\r\n        product.Expiry = new DateTime(2008, 12, 28);\r\n        product.Sizes = new string[] { \"Small\" };\r\n\r\n        string json = JsonConvert.SerializeObject(product);\r\n        Console.WriteLine(string.Format(\"serialized json for {0} is {1}\", product, json));\r\n    }\r\n}\r\n"
  },
  {
    "path": "examples/development/csharp/hello-world/README.md",
    "content": "# C# and .NET\n\nC# and .NET projects can be easily generated in Devbox by adding the dotnet SDK to your project. You can then create new projects using `dotnet new`\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/csharp)\n\n\n## Adding .NET to your project\n\n`devbox add dotnet-sdk`, or add the following to your `devbox.json`:\n\n```json\n  \"packages\": [\n    \"dotnet-sdk@latest\"\n  ],\n```\n\nThis will install the latest version of the dotnet SDK. You can find other installable versions of the dotnet SDK by running `devbox search dotnet-sdk`.\n\nIf you need a specific version of the .NET SDK, you can search on [Nixhub](https://www.nixhub.io/search?q=dotnet)\n\n## Creating a new C# Project\n\n`dotnet new console -lang \"C#\" -o <name>`\n"
  },
  {
    "path": "examples/development/csharp/hello-world/devbox.json",
    "content": "{\n    \"packages\": [\n        \"dotnet-sdk@latest\"\n    ],\n    \"shell\": {\n        \"init_hook\": null,\n        \"scripts\": {\n            \"run_test\": \"dotnet run\"\n        }\n    }\n}\n"
  },
  {
    "path": "examples/development/csharp/hello-world/hello-world.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\r\n\r\n  <PropertyGroup>\r\n    <OutputType>Exe</OutputType>\r\n    <TargetFramework>net6.0</TargetFramework>\r\n    <RootNamespace>hello-world</RootNamespace>\r\n    <ImplicitUsings>enable</ImplicitUsings>\r\n    <Nullable>enable</Nullable>\r\n  </PropertyGroup>\r\n\r\n  <ItemGroup>\r\n    <PackageReference Include=\"Newtonsoft.Json\" Version=\"13.0.1\" />\r\n  </ItemGroup>\r\n\r\n</Project>\r\n"
  },
  {
    "path": "examples/development/elixir/elixir_hello/.formatter.exs",
    "content": "# Used by \"mix format\"\n[\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": "examples/development/elixir/elixir_hello/.gitignore",
    "content": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up here.\n/cover/\n\n# The directory Mix downloads your dependencies sources to.\n/deps/\n\n# Where third-party dependencies like ExDoc output generated docs.\n/doc/\n\n# Ignore .fetch files in case you like to edit your project deps locally.\n/.fetch\n\n# If the VM crashes, it generates a dump, let's ignore it too.\nerl_crash.dump\n\n# Also ignore archive artifacts (built via \"mix archive.build\").\n*.ez\n\n# Ignore package tarball (built via \"mix hex.build\").\nelixir_hello-*.tar\n\n# Temporary files, for example, from tests.\n/tmp/\n\n.nix-mix/\n.nix-hex/\n"
  },
  {
    "path": "examples/development/elixir/elixir_hello/README.md",
    "content": "# Elixir\n\nBasic Elixir project using Mix in Devbox.\n\n\n## Configuration\n\nThis project configures Hex and Mix to install packages + dependencies in local project directories. You can modify where these packages are installed by changing the variables in `conf/set-env.sh`\n\n## Installation\n\nTo run the project: `mix run`\n\nTo create a release: `mix release`\n\n## Elixir Readme\n\nIf [available in Hex](https://hex.pm/docs/publish), the package can be installed\nby adding `elixir_hello` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:elixir_hello, \"~> 0.1.0\"}\n  ]\nend\n```\n\nDocumentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)\nand published on [HexDocs](https://hexdocs.pm). Once published, the docs can\nbe found at `https://hexdocs.pm/elixir_hello`.\n"
  },
  {
    "path": "examples/development/elixir/elixir_hello/devbox.json",
    "content": "{\n  \"packages\": [\n    \"elixir@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \"mix deps.get\"\n    ],\n    \"scripts\": {\n      \"run_test\": \"mix run\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/elixir/elixir_hello/lib/elixir_hello.ex",
    "content": "defmodule ElixirHello do\n  use Application\n  @moduledoc \"\"\"\n  Documentation for `ElixirHello`.\n  \"\"\"\n\n  @doc \"\"\"\n  Hello world.\n\n  ## Examples\n\n      iex> ElixirHello.hello()\n      :world\n\n  \"\"\"\n  def start(_type, _args) do\n    IO.puts(\"Hello World!\")\n    Task.start(fn -> :timer.sleep(1000); IO.puts(\"Goodbye World\"); exit(:shutdown) end)\n  end\nend\n"
  },
  {
    "path": "examples/development/elixir/elixir_hello/lib/kv.exs",
    "content": "defmodule KV do\n  def start_link do\n    Task.start_link(fn -> loop(%{}) end)\n  end\n\n  defp loop(map) do\n  receive do\n    {:get, key, caller} ->\n      send caller, Map.get(map, key)\n      loop(map)\n    {:put, key, value} ->\n      loop(Map.put(map,key,value))\n    end\n  end\nend\n"
  },
  {
    "path": "examples/development/elixir/elixir_hello/mix.exs",
    "content": "defmodule ElixirHello.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :elixir_hello,\n      version: \"0.1.0\",\n      elixir: \"~> 1.13\",\n      start_permanent: Mix.env() == :prod,\n      deps: deps(),\n    ]\n  end\n\n  # Run \"mix help compile.app\" to learn about applications.\n  def application do\n    [\n      mod: {ElixirHello, []},\n      extra_applications: [:logger]\n    ]\n  end\n\n  # Run \"mix help deps\" to learn about dependencies.\n  defp deps do\n    [\n      {:cowboy, \"~> 2.9\"}\n      # {:dep_from_hexpm, \"~> 0.3.0\"},\n      # {:dep_from_git, git: \"https://github.com/elixir-lang/my_dep.git\", tag: \"0.1.0\"}\n    ]\n  end\nend\n"
  },
  {
    "path": "examples/development/elixir/elixir_hello/test/elixir_hello_test.exs",
    "content": "defmodule ElixirHelloTest do\n  use ExUnit.Case\n  doctest ElixirHello\n\n  test \"greets the world\" do\n    assert ElixirHello.hello() == \"Hello World!\"\n  end\nend\n"
  },
  {
    "path": "examples/development/elixir/elixir_hello/test/test_helper.exs",
    "content": "ExUnit.start()\n"
  },
  {
    "path": "examples/development/fsharp/hello-world/.gitignore",
    "content": "bin/\nobj/\n"
  },
  {
    "path": "examples/development/fsharp/hello-world/Program.fs",
    "content": "﻿// For more information see https://aka.ms/fsharp-console-apps\r\n\r\n[<EntryPoint>]\r\nlet main args =\r\n    let dotNetVersion = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription\r\n    printfn \"Installed version is %A\" dotNetVersion\r\n\r\n    let expectedVersionPrefix = \".NET 6\"\r\n    if (not (dotNetVersion.StartsWith(expectedVersionPrefix))) then\r\n      raise (System.Exception(sprintf \"Expected version %A but got version %A\" expectedVersionPrefix dotNetVersion))\r\n    else \r\n      0\r\n"
  },
  {
    "path": "examples/development/fsharp/hello-world/README.md",
    "content": "# F# and .NET\n\nF# and .NET projects can be easily generated in Devbox by adding the dotnet SDK to your project. You can then create new projects using `dotnet new`\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/fsharp)\n\n\n## Adding .NET to your project\n\n`devbox add dotnet-sdk`, or add the following to your `devbox.json`:\n\n```json\n  \"packages\": [\n    \"dotnet-sdk@latest\"\n  ],\n```\n\nThis will install the latest version of the dotnet SDK. You can find other installable versions of the dotnet SDK by running `devbox search dotnet-sdk`. You can also view the available versions on [Nixhub](https://www.nixhub.io/search?q=dotnet)\n\n## Creating a new F# Project\n\n`dotnet new console -lang \"F#\" -o <name>`\n"
  },
  {
    "path": "examples/development/fsharp/hello-world/devbox.json",
    "content": "{\n    \"packages\": [\n        \"dotnet-sdk@latest\"\n    ],\n    \"shell\": {\n        \"init_hook\": null,\n        \"scripts\": {\n            \"run_test\": \"dotnet run\"\n        }\n    }\n}\n"
  },
  {
    "path": "examples/development/fsharp/hello-world/hello-world.fsproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\r\n\r\n  <PropertyGroup>\r\n    <OutputType>Exe</OutputType>\r\n    <TargetFramework>net6.0</TargetFramework>\r\n    <RootNamespace>hello_world</RootNamespace>\r\n  </PropertyGroup>\r\n\r\n  <ItemGroup>\r\n    <Compile Include=\"Program.fs\" />\r\n  </ItemGroup>\r\n\r\n</Project>\r\n"
  },
  {
    "path": "examples/development/go/hello-world/.envrc",
    "content": "# Automatically sets up your devbox environment whenever you cd into this\n# directory via our direnv integration:\n\neval \"$(devbox generate direnv --print-envrc)\"\n\n# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/\n# for more details\n"
  },
  {
    "path": "examples/development/go/hello-world/README.md",
    "content": "# Go\n\nGo projects can be run in Devbox by adding the Go SDK to your project. If your project uses cgo or compiles against C libraries, you should also include them in your packages to ensure Go can compile successfully\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/go/hello-world)\n\n\n## Adding Go to your Project\n\n`devbox add go`, or add the following to your `devbox.json`\n\n```json\n  \"packages\": [\n    \"go@latest\"\n  ]\n```\n\nThis will install the latest version of the Go SDK. You can find other installable versions of Go by running `devbox search go`. You can also view the available versions on [Nixhub](https://www.nixhub.io/packages/go)\n\nIf you need additional C libraries, you can add them along with `gcc` to your package list. For example, if libcap is required for your project:\n\n```json\n\"packages\": [\n    \"go\",\n    \"gcc\",\n    \"libcap\"\n]\n```\n"
  },
  {
    "path": "examples/development/go/hello-world/devbox.json",
    "content": "{\n  \"packages\": [\"go@1.24.5\"],\n  \"env\": {\n    \"GOPATH\": \"$HOME/go/\",\n    \"PATH\":   \"$PATH:$HOME/go/bin\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \"export \\\"GOROOT=$(go env GOROOT)\\\"\"\n    ],\n    \"scripts\": {\n      \"run_test\": \"go run main.go\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/go/hello-world/go.mod",
    "content": "module example\n\ngo 1.24.5\n"
  },
  {
    "path": "examples/development/go/hello-world/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n)\n\nfunc main() {\n\texpected := \"go1.24.5\"\n\tgoVersion := runtime.Version()\n\tfmt.Printf(\"Go version: %s\\n\", goVersion)\n\tif goVersion != expected {\n\t\tpanic(fmt.Errorf(\"expected version: %s, got: %s\", expected, goVersion))\n\t}\n}\n"
  },
  {
    "path": "examples/development/haskell/README.md",
    "content": "# Haskell\n\nHaskell projects that use the Stack Framework can be run in Devbox by adding the Stack and the Cabal packages to your project. You may also want to include libraries that Stack requires for compilation (described below)\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/haskell/)\n\n## Adding Haskell and Stack to your Project\n\n`devbox add stack cabal-install zlib hpack`, or add the following to your `devbox.json`\n\n```json\n  \"packages\": [\n    \"stack@latest\",\n    \"cabal-install@latest\",\n    \"zlib@latest\",\n    \"hpack@latest\"\n  ]\n```\n\nThis will install GHC, and the Haskell Tool Stack in your Devbox Shell at their latest version. You can find other installable versions of Stack by running `devbox search <pkg>`.\n"
  },
  {
    "path": "examples/development/haskell/devbox.json",
    "content": "{\n  \"packages\": [\n    \"ghc@latest\",\n    \"gmp@latest\",\n    \"stack@latest\",\n    \"cabal-install@latest\",\n    \"zlib@latest\",\n    \"hpack@latest\"\n  ],\n  \"env\": {\n    \"PATH\": \"$PATH:/usr/bin\"\n  },\n  \"shell\": {\n    \"init_hook\": null,\n    \"scripts\": {\n      \"run_test\": [\n        \"cd my-project\",\n        \"stack build\",\n        \"stack exec my-project-exe\"\n      ]\n    }\n  }\n}"
  },
  {
    "path": "examples/development/haskell/my-project/.gitignore",
    "content": ".stack-work/"
  },
  {
    "path": "examples/development/haskell/my-project/CHANGELOG.md",
    "content": "# Changelog for `my-project`\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to the\n[Haskell Package Versioning Policy](https://pvp.haskell.org/).\n\n## Unreleased\n\n## 0.1.0.0 - YYYY-MM-DD\n"
  },
  {
    "path": "examples/development/haskell/my-project/LICENSE",
    "content": "Copyright Author name here (c) 2022\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n\n    * Neither the name of Author name here nor the names of other\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "examples/development/haskell/my-project/README.md",
    "content": "# Haskell\n\nThis example was generated using `stack`. To generate a similar example, you can copy the devbox json and run `devbox shell`.\n\nOnce your shell has activated, run:\n\n```\nstack new my-project\ncd my-project\nstack build\nstack exec my-project-exe\n```\n\nFor more details, check out the [Haskell + Devbox Docs](https://www.jetify.com/devbox/docs/devbox_examples/languages/haskell/)\n"
  },
  {
    "path": "examples/development/haskell/my-project/Setup.hs",
    "content": "import Distribution.Simple\nmain = defaultMain\n"
  },
  {
    "path": "examples/development/haskell/my-project/app/Main.hs",
    "content": "module Main (main) where\n\nimport Lib\n\nmain :: IO ()\nmain = someFunc\n"
  },
  {
    "path": "examples/development/haskell/my-project/my-project.cabal",
    "content": "cabal-version: 1.12\n\n-- This file has been generated from package.yaml by hpack version 0.35.0.\n--\n-- see: https://github.com/sol/hpack\n\nname:           my-project\nversion:        0.1.0.0\ndescription:    Please see the README on GitHub at <https://github.com/githubuser/my-project#readme>\nhomepage:       https://github.com/githubuser/my-project#readme\nbug-reports:    https://github.com/githubuser/my-project/issues\nauthor:         Author name here\nmaintainer:     example@example.com\ncopyright:      2022 Author name here\nlicense:        BSD3\nlicense-file:   LICENSE\nbuild-type:     Simple\nextra-source-files:\n    README.md\n    CHANGELOG.md\n\nsource-repository head\n  type: git\n  location: https://github.com/githubuser/my-project\n\nlibrary\n  exposed-modules:\n      Lib\n  other-modules:\n      Paths_my_project\n  hs-source-dirs:\n      src\n  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints\n  build-depends:\n      base >=4.7 && <5\n  default-language: Haskell2010\n\nexecutable my-project-exe\n  main-is: Main.hs\n  other-modules:\n      Paths_my_project\n  hs-source-dirs:\n      app\n  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N\n  build-depends:\n      base >=4.7 && <5\n    , my-project\n  default-language: Haskell2010\n\ntest-suite my-project-test\n  type: exitcode-stdio-1.0\n  main-is: Spec.hs\n  other-modules:\n      Paths_my_project\n  hs-source-dirs:\n      test\n  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N\n  build-depends:\n      base >=4.7 && <5\n    , my-project\n  default-language: Haskell2010\n"
  },
  {
    "path": "examples/development/haskell/my-project/package.yaml",
    "content": "name:                my-project\nversion:             0.1.0.0\ngithub:              \"githubuser/my-project\"\nlicense:             BSD3\nauthor:              \"Author name here\"\nmaintainer:          \"example@example.com\"\ncopyright:           \"2022 Author name here\"\n\nextra-source-files:\n- README.md\n- CHANGELOG.md\n\n# Metadata used when publishing your package\n# synopsis:            Short description of your package\n# category:            Web\n\n# To avoid duplicated efforts in documentation and dealing with the\n# complications of embedding Haddock markup inside cabal files, it is\n# common to point users to the README.md file.\ndescription:         Please see the README on GitHub at <https://github.com/githubuser/my-project#readme>\n\ndependencies:\n- base >= 4.7 && < 5\n\nghc-options:\n- -Wall\n- -Wcompat\n- -Widentities\n- -Wincomplete-record-updates\n- -Wincomplete-uni-patterns\n- -Wmissing-export-lists\n- -Wmissing-home-modules\n- -Wpartial-fields\n- -Wredundant-constraints\n\nlibrary:\n  source-dirs: src\n\nexecutables:\n  my-project-exe:\n    main:                Main.hs\n    source-dirs:         app\n    ghc-options:\n    - -threaded\n    - -rtsopts\n    - -with-rtsopts=-N\n    dependencies:\n    - my-project\n\ntests:\n  my-project-test:\n    main:                Spec.hs\n    source-dirs:         test\n    ghc-options:\n    - -threaded\n    - -rtsopts\n    - -with-rtsopts=-N\n    dependencies:\n    - my-project\n"
  },
  {
    "path": "examples/development/haskell/my-project/src/Lib.hs",
    "content": "module Lib\n    ( someFunc\n    ) where\n\nsomeFunc :: IO ()\nsomeFunc = putStrLn \"someFunc\"\n"
  },
  {
    "path": "examples/development/haskell/my-project/stack.yaml",
    "content": "# This file was automatically generated by 'stack init'\n#\n# Some commonly used options have been documented as comments in this file.\n# For advanced use and comprehensive documentation of the format, please see:\n# https://docs.haskellstack.org/en/stable/yaml_configuration/\n\n# Resolver to choose a 'specific' stackage snapshot or a compiler version.\n# A snapshot resolver dictates the compiler version and the set of packages\n# to be used for project dependencies. For example:\n#\n# resolver: lts-3.5\n# resolver: nightly-2015-09-21\n# resolver: ghc-7.10.2\n#\n# The location of a snapshot can be provided as a file or url. Stack assumes\n# a snapshot provided as a file might change, whereas a url resource does not.\n#\n# resolver: ./custom-snapshot.yaml\n# resolver: https://example.com/snapshots/2018-01-01.yaml\nresolver:\n  url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/4.yaml\n\n# User packages to be built.\n# Various formats can be used as shown in the example below.\n#\n# packages:\n# - some-directory\n# - https://example.com/foo/bar/baz-0.0.2.tar.gz\n#   subdirs:\n#   - auto-update\n#   - wai\npackages:\n- .\n# Dependency packages to be pulled from upstream that are not in the resolver.\n# These entries can reference officially published versions as well as\n# forks / in-progress versions pinned to a git hash. For example:\n#\n# extra-deps:\n# - acme-missiles-0.3\n# - git: https://github.com/commercialhaskell/stack.git\n#   commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a\n#\n# extra-deps: []\n\n# Override default flag values for local packages and extra-deps\n# flags: {}\n\n# Extra package databases containing global packages\n# extra-package-dbs: []\n\n# Control whether we use the GHC we find on the path\n# system-ghc: true\n#\n# Require a specific version of stack, using version ranges\n# require-stack-version: -any # Default\n# require-stack-version: \">=2.9\"\n#\n# Override the architecture used by stack, especially useful on Windows\n# arch: i386\n# arch: x86_64\n#\n# Extra directories used by stack for building\n# extra-include-dirs: [/path/to/dir]\n# extra-lib-dirs: [/path/to/dir]\n#\n# Allow a newer minor version of GHC than the snapshot specifies\n# compiler-check: newer-minor\n"
  },
  {
    "path": "examples/development/haskell/my-project/test/Spec.hs",
    "content": "main :: IO ()\nmain = putStrLn \"Test suite not yet implemented\"\n"
  },
  {
    "path": "examples/development/java/README.md",
    "content": "# Setting Up Example Projects For Java\n\n## Java with Maven\nMaven is an all-in-one CI-CD tool for building testing and deploying Java projects. To setup a sample project with Java and Maven in devbox follow the steps below:\n\n1. Create a dummy folder: `dummy/` and call `devbox init` inside it. Then add the nix-pkg: `devbox add jdk` and `devbox add maven`.\n    - Replace `jdk` with the version of JDK you want. Get the exact nix-pkg name from `search.nixos.org`.\n2. Then do `devbox shell` to get a shell with that `jdk` nix pkg.\n3. Then do: `mvn archetype:generate -DgroupId=com.devbox.mavenapp -DartifactId=devbox-maven-app -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false`\n    - In the generated `pom.xml` file, replace java version in `<maven.compiler.source>` with the specific version you are testing for.\n4. `mvn package` should compile the package and create a `target/` directory.\n5. `java -cp target/devbox-maven-app-1.0-SNAPSHOT.jar com.devbox.mavenapp.App` should print \"Hello World!\".\n6. Add `target/` to `.gitignore`.\n\n## Java with Gradle\nTo test a sample Gradle app with devbox, follow the steps below:\n\n1. Create a dummy folder: `dummy/` and call `devbox init` inside it. Then add these packages: `devbox add jdk` and `devbox add gradle`.\n    - Replace `jdk` with the version of JDK you want. Get the exact nix-pkg name from `search.nixos.org`.\n2. Then do `devbox shell` to get a shell with that `jdk` nix pkg.\n3. Then do: `gradle init`\n    - In the generated `gradle.build` file, put the following text block:\n        ```gradle\n        apply plugin: 'java'\n        apply plugin: 'application'\n        sourceCompatibility = 17\n        targetCompatibility = 17\n        mainClassName = 'hello.HelloWorld'\n        jar {\n            manifest {\n                attributes 'Main-Class': 'hello.HelloWorld'\n            }\n        }\n        ```\n4. `gradle build` should compile the package and create a `build/` directory that contains an executable jar file.\n5. `gradle run` should print \"Hello World!\".\n6. Add `build/` to `.gitignore`.\n"
  },
  {
    "path": "examples/development/java/gradle/.gitignore",
    "content": ".gradle/\nbuild/"
  },
  {
    "path": "examples/development/java/gradle/hello-world/README.md",
    "content": "# Java\n\nIn addition to installing the JDK, you'll need to install either the Maven or Gradle build systems in your shell.\n\nIn both cases, you'll want to first activate `devbox shell` before generating your Maven or Gradle projects, so that the tools use the right version of the JDK for creating your project.\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/java)\n\n## Adding the JDK to your project\n\n`devbox add jdk binutils`, or in your `devbox.json`\n\n```json\n  \"packages\": [\n    \"jdk@latest\",\n    \"binutils@latest\"\n  ],\n\n```\n\nThis will install the latest version of the JDK. To find other installable versions of the JDK, run `devbox search jdk`.\n\nOther distributions of the JDK (such as OracleJDK and Eclipse Temurin) are available in Nixpkgs, and can be found using [NixPkg Search](https://search.nixos.org/packages?channel=22.05&from=0&size=50&sort=relevance&type=packages&query=jdk#)\n\n## Gradle\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/java/gradle/hello-world)\n\n\nGradle is a popular, multi-language build tool that is commonly used with JVM projects. To setup an example project using Gradle, follow the instructions below:\n\n1. Create a project folder: `my-project/` and call `devbox init` inside it. Then add these packages: `devbox add jdk` and `devbox add gradle`.\n    - Replace `jdk` with the version of JDK you want. Get the exact nix-pkg name from `search.nixos.org`.\n2. Then do `devbox shell` to get a shell with that `jdk` nix pkg.\n3. Then do: `gradle init`\n    - In the generated `build.gradle` file, put the following text block:\n\n        ```gradle\n        /* build.gradle */\n        apply plugin: 'java'\n        apply plugin: 'application'\n        /* Change these versions to the JDK version you have installed */\n        sourceCompatibility = 17\n        targetCompatibility = 17\n        mainClassName = 'hello.HelloWorld'\n        jar {\n            manifest {\n              /* assuming main class is in src/main/java/hello/HelloWorld.java */\n                attributes 'Main-Class': 'hello.HelloWorld'\n            }\n        }\n        ```\n\n    - While in devbox shell, run `echo $JAVA_HOME` and take note of its value.\n    - Create a `gradle.properties` file like below and put value of `$JAVA_HOME` instead of <JAVA_HOME_VALUE> in the file.\n\n      ```gradle\n      /* gradle.properties */\n      org.gradle.java.home=<JAVA_HOME_VALUE>\n      ```\n\n4. `gradle build` should compile the package and create a `build/` directory that contains an executable jar file.\n5. `gradle run` should print \"Hello World!\".\n6. Add `build/` to `.gitignore`.\n\nAn example `devbox.json` would look like the following:\n\n```json\n{\n  \"packages\": [\n    \"gradle\",\n    \"jdk\",\n    \"binutils\"\n  ],\n  \"shell\": {\n    \"init_hook\": null\n  }\n}\n```\n"
  },
  {
    "path": "examples/development/java/gradle/hello-world/build.gradle",
    "content": "/*\n * This file was generated by the Gradle 'init' task.\n *\n * This is a general purpose Gradle build.\n * Learn more about Gradle by exploring our samples at https://docs.gradle.org/7.5/samples\n */\n\napply plugin: 'java'\napply plugin: 'application'\n\nsourceCompatibility = 17\ntargetCompatibility = 17\nmainClassName = 'hello.HelloWorld'\njar {\n    manifest {\n        attributes 'Main-Class': 'hello.HelloWorld'\n    }\n}\n"
  },
  {
    "path": "examples/development/java/gradle/hello-world/devbox.json",
    "content": "{\n    \"packages\": [\n        \"gradle@latest\",\n        \"jdk@19\",\n        \"binutils@latest\"\n    ],\n    \"shell\": {\n        \"init_hook\": null,\n        \"scripts\": {\n            \"run_test\": [\n                \"gradle build\",\n                \"gradle run\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "examples/development/java/gradle/hello-world/gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\nAPP_HOME=$( cd \"${APP_HOME:-./}\" && pwd -P ) || exit\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=${0##*/}\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n# Collect all arguments for the java command;\n#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of\n#     shell script including quotes and variable substitutions, so put them in\n#     double quotes to make sure that they get re-expanded; and\n#   * put everything else in single quotes, so that it's not re-expanded.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "examples/development/java/gradle/hello-world/settings.gradle",
    "content": "/*\n * This file was generated by the Gradle 'init' task.\n *\n * The settings file is used to specify which projects to include in your build.\n *\n * Detailed information about configuring a multi-project build in Gradle can be found\n * in the user manual at https://docs.gradle.org/7.5/userguide/multi_project_builds.html\n */\n\nrootProject.name = 'gradle-app'\n"
  },
  {
    "path": "examples/development/java/gradle/hello-world/src/main/java/hello/HelloWorld.java",
    "content": "package hello;\n\npublic class HelloWorld {\n  public static void main(String[] args) {\n  \n  System.out.println(\"Hello World from Gradle!\");\n  }\n}"
  },
  {
    "path": "examples/development/java/maven/.gitignore",
    "content": "target/"
  },
  {
    "path": "examples/development/java/maven/hello-world/README.md",
    "content": "# Java\n\nIn addition to installing the JDK, you'll need to install either the Maven or Gradle build systems in your shell.\n\nIn both cases, you'll want to first activate `devbox shell` before generating your Maven or Gradle projects, so that the tools use the right version of the JDK for creating your project.\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/java)\n\n## Adding the JDK to your project\n\n`devbox add jdk binutils`, or in your `devbox.json`\n\n```json\n  \"packages\": [\n    \"jdk@latest\",\n    \"binutils@latest\"\n  ],\n\n```\n\nThis will install the latest version of the JDK. To find other installable versions of the JDK, run `devbox search jdk`.\n\nOther distributions of the JDK (such as OracleJDK and Eclipse Temurin) are available in Nixpkgs, and can be found using [NixPkg Search](https://search.nixos.org/packages?channel=22.05&from=0&size=50&sort=relevance&type=packages&query=jdk#)\n\n## Maven\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/java/maven/hello-world)\n\n\nMaven is an all-in-one CI-CD tool for building testing and deploying Java projects. To setup a sample project with Java and Maven in devbox follow the steps below:\n\n1. Create a dummy folder: `dummy/` and call `devbox init` inside it. Then add the nix-pkg: `devbox add jdk` and `devbox add maven`.\n    - Replace `jdk` with the version of JDK you want. Get the exact nix-pkg name from `search.nixos.org`.\n2. Then do `devbox shell` to get a shell with that `jdk` nix pkg.\n3. Then do: `mvn archetype:generate -DgroupId=com.devbox.mavenapp -DartifactId=devbox-maven-app -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false`\n    - In the generated `pom.xml` file, replace java version in `<maven.compiler.source>` with the specific version you are testing for.\n4. `mvn package` should compile the package and create a `target/` directory.\n5. `java -cp target/devbox-maven-app-1.0-SNAPSHOT.jar com.devbox.mavenapp.App` should print \"Hello World!\".\n6. Add `target/` to `.gitignore`.\n\nAn example `devbox.json` would look like the following:\n\n```json\n{\n  \"packages\": [\n    \"maven\",\n    \"jdk\",\n    \"binutils\"\n  ],\n  \"shell\": {\n    \"init_hook\": null\n  }\n}\n```\n"
  },
  {
    "path": "examples/development/java/maven/hello-world/devbox-maven-app/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <groupId>com.devbox.mavenapp</groupId>\n  <artifactId>devbox-maven-app</artifactId>\n  <version>1.0-SNAPSHOT</version>\n\n  <name>devbox-maven-app</name>\n  <!-- FIXME change it to the project's website -->\n  <url>http://www.example.com</url>\n\n  <properties>\n    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    <maven.compiler.source>1.7</maven.compiler.source>\n    <maven.compiler.target>1.7</maven.compiler.target>\n  </properties>\n\n  <dependencies>\n    <dependency>\n      <groupId>junit</groupId>\n      <artifactId>junit</artifactId>\n      <version>4.13.1</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n\n  <build>\n    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->\n      <plugins>\n        <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->\n        <plugin>\n          <artifactId>maven-clean-plugin</artifactId>\n          <version>3.1.0</version>\n        </plugin>\n        <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->\n        <plugin>\n          <artifactId>maven-resources-plugin</artifactId>\n          <version>3.0.2</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-compiler-plugin</artifactId>\n          <version>3.8.0</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-surefire-plugin</artifactId>\n          <version>2.22.1</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-jar-plugin</artifactId>\n          <version>3.0.2</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-install-plugin</artifactId>\n          <version>2.5.2</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-deploy-plugin</artifactId>\n          <version>2.8.2</version>\n        </plugin>\n        <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->\n        <plugin>\n          <artifactId>maven-site-plugin</artifactId>\n          <version>3.7.1</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-project-info-reports-plugin</artifactId>\n          <version>3.0.0</version>\n        </plugin>\n      </plugins>\n    </pluginManagement>\n  </build>\n</project>\n"
  },
  {
    "path": "examples/development/java/maven/hello-world/devbox.json",
    "content": "{\n    \"packages\": [\n        \"maven@latest\",\n        \"jdk@19\",\n        \"binutils@latest\"\n    ],\n    \"shell\": {\n        \"init_hook\": null,\n        \"scripts\": {\n            \"run_test\": [\n                \"mvn package\",\n                \"java -jar target/devbox-maven-app-1.0-SNAPSHOT.jar\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "examples/development/java/maven/hello-world/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n\n  <groupId>com.devbox.mavenapp</groupId>\n  <artifactId>devbox-maven-app</artifactId>\n  <version>1.0-SNAPSHOT</version>\n\n  <name>devbox-maven-app</name>\n  <!-- FIXME change it to the project's website -->\n  <url>http://www.example.com</url>\n\n  <properties>\n    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n  </properties>\n\n  <dependencies>\n    <dependency>\n      <groupId>junit</groupId>\n      <artifactId>junit</artifactId>\n      <version>4.13.1</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n\n  <build>\n    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->\n      <plugins>\n        <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->\n        <plugin>\n          <artifactId>maven-clean-plugin</artifactId>\n          <version>3.1.0</version>\n        </plugin>\n        <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->\n        <plugin>\n          <artifactId>maven-resources-plugin</artifactId>\n          <version>3.0.2</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-compiler-plugin</artifactId>\n          <version>3.8.0</version>\n          <configuration>\n            <source>1.7</source>\n            <target>1.7</target>\n          </configuration>\n        </plugin>\n        <plugin>\n          <artifactId>maven-surefire-plugin</artifactId>\n          <version>2.22.1</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-jar-plugin</artifactId>\n           <version>3.0.2</version>\n            <configuration>\n              <archive>\n                <manifest>\n                  <addClasspath>true</addClasspath>\n                  <mainClass>com.devbox.mavenapp.App</mainClass>\n                </manifest>\n              </archive>\n            </configuration>\n        </plugin>\n        <plugin>\n          <artifactId>maven-install-plugin</artifactId>\n          <version>2.5.2</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-deploy-plugin</artifactId>\n          <version>2.8.2</version>\n        </plugin>\n        <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->\n        <plugin>\n          <artifactId>maven-site-plugin</artifactId>\n          <version>3.7.1</version>\n        </plugin>\n        <plugin>\n          <artifactId>maven-project-info-reports-plugin</artifactId>\n          <version>3.0.0</version>\n        </plugin>\n      </plugins>\n    </pluginManagement>\n  </build>\n</project>\n"
  },
  {
    "path": "examples/development/java/maven/hello-world/src/main/java/com/devbox/mavenapp/App.java",
    "content": "package com.devbox.mavenapp;\n\n/**\n * Hello world!\n *\n */\npublic class App {\n    public static void main(String[] args) {\n        System.out.println(\"Hello World!\");\n    }\n}\n"
  },
  {
    "path": "examples/development/java/maven/hello-world/src/test/java/com/devbox/mavenapp/AppTest.java",
    "content": "package com.devbox.mavenapp;\n\nimport static org.junit.Assert.assertTrue;\n\nimport org.junit.Test;\n\n/**\n * Unit test for simple App.\n */\npublic class AppTest \n{\n    /**\n     * Rigorous Test :-)\n     */\n    @Test\n    public void shouldAnswerWithTrue()\n    {\n        assertTrue( true );\n    }\n}\n"
  },
  {
    "path": "examples/development/nim/spinnytest/README.md",
    "content": "# Nim Example\n\nSmall test to demonstrate installing a package and running it with nim.\n\n## Building\n\n```bash\ndevbox run nim-build\n```\n\n## Running\n\n```bash\n./spinnytest\n```\n"
  },
  {
    "path": "examples/development/nim/spinnytest/devbox.json",
    "content": "{\n    \"packages\": [\n        \"nim@1.6.12\",\n        \"nimble-unwrapped@0.14\",\n        \"openssl_1_1@latest\"\n    ],\n    \"shell\": {\n        \"init_hook\": null,\n        \"scripts\": {\n            \"nim-build\": \"nimble build --threads:on\"\n        }\n    }\n}\n"
  },
  {
    "path": "examples/development/nim/spinnytest/spinnytest.nim",
    "content": "import spinny, os\n\nvar spinner1 = newSpinny(\"Loading message ..\".fgWhite, skDots)\nspinner1.setSymbolColor(fgBlue)\nspinner1.start\n\nsleep(2000)\n\nspinner1.success(\"Ta da! Hello World\")\n\n"
  },
  {
    "path": "examples/development/nim/spinnytest/spinnytest.nimble",
    "content": "# Package\n\nversion     = \"0.1.0\"\nauthor      = \"Your Name\"\ndescription = \"Example .nimble file.\"\nlicense     = \"MIT\"\n\nbin = @[\"spinnytest\"]\n\n# Deps\n\nrequires \"nim >= 0.10.0\"\nrequires \"spinny\""
  },
  {
    "path": "examples/development/nodejs/.gitignore",
    "content": "node_modules/git "
  },
  {
    "path": "examples/development/nodejs/nodejs-npm/README.md",
    "content": "# NodeJS\n\nMost NodeJS Projects will install their dependencies locally using NPM or Yarn, and thus can work with Devbox with minimal additional configuration. Per project packages can be managed via NPM or Yarn.\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/nodejs)\n\n\n## Adding NodeJS to your Shell\n\n`devbox add nodejs`, or in your `devbox.json`:\n\n```json\n  \"packages\": [\n    \"nodejs@18\"\n  ],\n```\n\nThis will install NodeJS 18, and comes bundled with `npm`. You can find other installable versions of NodeJS by running `devbox search nodejs`. You can also view the available versions on [Nixhub](https://www.nixhub.io/packages/nodejs)\n\n## Installing Global Packages\n\nIn some situations, you may want to install packages using `npm install --global`. This will fail in Devbox since the Nix Store is immutable.\n\nYou can instead install these global packages by adding them to the list of packages in your `devbox.json`. For example: to add `yalc` and `pm2`:\n\n```json\n{\n    \"packages\": [\n        \"nodejs@18\",\n        \"nodePackages.yalc@latest\",\n        \"nodePackages.pm2@latest\"\n    ]\n}\n```\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-npm/devbox.json",
    "content": "{\n  \"packages\": [\n    \"nodejs@18\"\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \"npm install\"\n    ],\n    \"scripts\": {\n      \"run_test\": \"npm run start\"\n    }\n  }\n}"
  },
  {
    "path": "examples/development/nodejs/nodejs-npm/index.js",
    "content": "const NODE_MAJOR_VERSION = process.versions.node.split('.')[0];\nif (!NODE_MAJOR_VERSION) {\n  throw new Error('Node is not installed properly');\n}\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-npm/package.json",
    "content": "{\n  \"name\": \"nodejs-npm\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"echo building...\",\n    \"start\": \"node index.js\"\n  },\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-pnpm/README.md",
    "content": "# NodeJS\n\nMost NodeJS Projects will install their dependencies locally using NPM or Yarn, and thus can work with Devbox with minimal additional configuration. Per project packages can be managed via NPM or Yarn.\n\n\n## Adding NodeJS to your Shell\n\n`devbox add nodejs`, or in your `devbox.json`:\n\n```json\n  \"packages\": [\n    \"nodejs@18\"\n  ],\n```\n\nThis will install NodeJS 18, and comes bundled with `npm`. You can find other installable versions of NodeJS by running `devbox search nodejs`. You can also view the available versions on [Nixhub](https://www.nixhub.io/packages/nodejs)\n\n## Adding pnpm as your Package Manager\n\nWe recommend using Corepack to install and manage your Node Package Manager in Devbox. Corepack comes bundled with all recent Nodejs versions, and you can tell Devbox to automatically configure Corepack using a built-in plugin. When enabled, corepack binaries will be installed in your project's .devbox directory, and automatically added to your path.\n\nTo enable Corepack, set DEVBOX_COREPACK_ENABLED to true in your devbox.json:\n\n```json\n{\n  \"packages\": [\"nodejs@18\"],\n  \"env\": {\n    \"DEVBOX_COREPACK_ENABLED\": \"true\"\n  }\n}\n```\n\nTo disable Corepack, remove the DEVBOX_COREPACK_ENABLED variable from your devbox.json\n\n## Installing Global Packages\n\nIn some situations, you may want to install packages using `npm install --global`. This will fail in Devbox since the Nix Store is immutable.\n\nYou can instead install these global packages by adding them to the list of packages in your `devbox.json`. For example: to add `yalc` and `pm2`:\n\n```json\n{\n    \"packages\": [\n        \"nodejs@18\",\n        \"nodePackages.yalc@latest\",\n        \"nodePackages.pm2@latest\"\n    ]\n}\n```\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-pnpm/devbox.json",
    "content": "{\n  \"packages\": [\"nodejs@latest\"],\n  \"env\": {\n    \"DEVBOX_COREPACK_ENABLED\": \"true\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \"pnpm install\"\n    ],\n    \"scripts\": {\n      \"run_test\": \"pnpm run start\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-pnpm/index.js",
    "content": "const NODE_MAJOR_VERSION = process.versions.node.split('.')[0];\nif (!NODE_MAJOR_VERSION) {\n  throw new Error('Node is not installed properly');\n}\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-pnpm/package.json",
    "content": "{\n  \"name\": \"nodejs-pnpm\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"echo building...\",\n    \"start\": \"node index.js\"\n  },\n  \"dependencies\": {},\n  \"packageManager\": \"pnpm@8.15.4\"\n}"
  },
  {
    "path": "examples/development/nodejs/nodejs-typescript/.gitignore",
    "content": "node_modules/"
  },
  {
    "path": "examples/development/nodejs/nodejs-typescript/devbox.json",
    "content": "{\n    \"packages\": [\n        \"nodejs@18\"\n    ],\n    \"shell\": {\n        \"init_hook\": [\n            \"npm install\",\n            \"npm run build\"\n        ],\n        \"scripts\": {\n            \"run_test\": \"npm run start\"\n        }\n    }\n}"
  },
  {
    "path": "examples/development/nodejs/nodejs-typescript/index.js",
    "content": "var NODE_MAJOR_VERSION = process.versions.node.split('.')[0];\nif (NODE_MAJOR_VERSION !== \"18\") {\n    throw new Error('Node version is not 18');\n}\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-typescript/index.ts",
    "content": "const NODE_MAJOR_VERSION = process.versions.node.split('.')[0];\nif (NODE_MAJOR_VERSION !== \"18\") {\n  throw new Error('Node version is not 18');\n} else {\n  console.log('Node version is 18');\n}\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-typescript/package.json",
    "content": "{\n  \"name\": \"nodejs-typescript\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"tsc index.ts --outfile index.js\",\n    \"start\": \"node index.js\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^18.7.18\",\n    \"typescript\": \"^4.6.4\"\n  },\n  \"engines\": {\n    \"node\": \">=18\"\n  }\n}"
  },
  {
    "path": "examples/development/nodejs/nodejs-typescript/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [],\n  \"compilerOptions\": {\n    /* Visit https://aka.ms/tsconfig to read more about this file */\n    /* Language and Environment */\n    \"target\": \"esnext\",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */\n    /* Modules */\n    \"module\": \"commonjs\",                                /* Specify what module code is generated. */\n    \"esModuleInterop\": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */\n    \"forceConsistentCasingInFileNames\": true,            /* Ensure that casing is correct in imports. */\n    /* Type Checking */\n    \"strict\": true,                                      /* Enable all strict type-checking options. */\n    /* Completeness */\n    \"skipLibCheck\": true                                 /* Skip type checking all .d.ts files. */\n  }\n}\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-yarn/.yarnrc.yml",
    "content": "nodeLinker: node-modules\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-yarn/README.md",
    "content": "# NodeJS\n\nMost NodeJS Projects will install their dependencies locally using NPM or Yarn, and thus can work with Devbox with minimal additional configuration. Per project packages can be managed via NPM or Yarn.\n\n\n## Adding NodeJS to your Shell\n\n`devbox add nodejs`, or in your `devbox.json`:\n\n```json\n  \"packages\": [\n    \"nodejs@18\"\n  ],\n```\n\nThis will install NodeJS 18, and comes bundled with `npm`. You can find other installable versions of NodeJS by running `devbox search nodejs`. You can also view the available versions on [Nixhub](https://www.nixhub.io/packages/nodejs)\n\n## Adding Yarn as your Package Manager\n\n`devbox add yarn`, or in your `devbox.json` add:\n\n```json\n  \"packages\": [\n    \"nodejs@18\",\n    \"yarn@latest\"\n  ],\n```\n\n## Installing Global Packages\n\nIn some situations, you may want to install packages using `npm install --global`. This will fail in Devbox since the Nix Store is immutable.\n\nYou can instead install these global packages by adding them to the list of packages in your `devbox.json`. For example: to add `yalc` and `pm2`:\n\n```json\n{\n    \"packages\": [\n        \"nodejs@18\",\n        \"nodePackages.yalc@latest\",\n        \"nodePackages.pm2@latest\"\n    ]\n}\n```\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-yarn/devbox.json",
    "content": "{\n  \"packages\": [\"nodejs@latest\"],\n  \"env\": {\n    \"DEVBOX_COREPACK_ENABLED\": \"true\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \"yarn install\"\n    ],\n    \"scripts\": {\n      \"run_test\": \"yarn start\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-yarn/index.js",
    "content": "const NODE_MAJOR_VERSION = process.versions.node.split('.')[0];\nif (!NODE_MAJOR_VERSION) {\n  throw new Error('Node is not installed properly');\n}\n"
  },
  {
    "path": "examples/development/nodejs/nodejs-yarn/package.json",
    "content": "{\n  \"name\": \"nodejs-yarn\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"build\": \"echo building...\",\n    \"start\": \"node index.js\"\n  },\n  \"version\": \"0.0.0\",\n  \"packageManager\": \"yarn@4.1.1\"\n}\n"
  },
  {
    "path": "examples/development/php/latest/.envrc",
    "content": "# Automatically sets up your devbox environment whenever you cd into this\n# directory via our direnv integration:\n\neval \"$(devbox generate direnv --print-envrc)\"\n\n# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/\n# for more details\n"
  },
  {
    "path": "examples/development/php/latest/.gitignore",
    "content": "vendor/"
  },
  {
    "path": "examples/development/php/latest/README.md",
    "content": "# PHP\n\nPHP projects can manage most of their dependencies locally with `composer`. Some PHP extensions, however, need to be bundled with PHP at compile time.\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/php/latest)\n\n\n## Adding PHP to your Project\n\nRun `devbox add php php83Packages.composer`, or add the following to your `devbox.json`:\n\n```json\n    \"packages\": [\n        \"php@latest\",\n        \"php83Packages.composer@latest\n    ]\n```\n\nIf you want a different version of PHP for your project, you can search for available versions by running `devbox search php`. You can also view the available versions on [Nixhub](https://www.nixhub.io/packages/php)\n\n## Installing PHP Extensions\n\nYou can compile additional extensions into PHP by adding them to `packages` in your `devbox.json`. Devbox will automatically ensure that your extensions are included in PHP at compile time.\n\nFor example -- to add the `ds` extension, run `devbox add php81Extensions.ds`, or update your packages to include the following:\n\n```json\n    \"packages\": [\n        \"php@latest\",\n        \"php83Packages.composer\",\n        \"php83Extensions.ds\"\n    ]\n```\n\n## PHP Plugin Details\n\nThe PHP Plugin will provide the following configuration when you install a PHP runtime with `devbox add`. You can also manually add the PHP plugin by adding `plugin:php` to your `include` list in `devbox.json`:\n\n```json\n    \"include\": [\n        \"plugin:php\"\n    ]\n```\n\n### Services\n\n* php-fpm\n\nUse `devbox services start|stop php-fpm` to start PHP-FPM in the background.\n\n### Environment Variables\n\n```bash\nPHPFPM_PORT=8082\nPHPFPM_ERROR_LOG_FILE={PROJECT_DIR}/.devbox/virtenv/php/php-fpm.log\nPHPFPM_PID_FILE={PROJECT_DIR}/.devbox/virtenv/php/php-fpm.pid\nPHPRC={PROJECT_DIR}/devbox.d/php/php.ini\n```\n\n### Helper Files\n\n* {PROJECT_DIR}/devbox.d/php81/php-fpm.conf\n* {PROJECT_DIR}/devbox.d/php81/php.ini\n\nYou can modify these files to configure PHP or your PHP-FPM server\n"
  },
  {
    "path": "examples/development/php/latest/composer.json",
    "content": "{\n  \"require\": {\n      \"ext-mbstring\": \"*\",\n      \"ext-imagick\": \"*\",\n      \"php\": \"^8.1\"\n  }\n}\n"
  },
  {
    "path": "examples/development/php/latest/devbox.d/php/php-fpm.conf",
    "content": "[global]\npid = ${PHPFPM_PID_FILE}\nerror_log = ${PHPFPM_ERROR_LOG_FILE}\ndaemonize = yes\n\n[www]\n; user = www-data\n; group = www-data\nlisten = 127.0.0.1:${PHPFPM_PORT}\n; listen.owner = www-data\n; listen.group = www-data\npm = dynamic\npm.max_children = 5\npm.start_servers = 2\npm.min_spare_servers = 1\npm.max_spare_servers = 3\nchdir = /\n"
  },
  {
    "path": "examples/development/php/latest/devbox.d/php/php.ini",
    "content": "[php]\n\n; Put your php.ini directives here. For the latest default php.ini file, see https://github.com/php/php-src/blob/master/php.ini-production\n\n; memory_limit = 128M\n; expose_php = Off\n\nxdebug.mode = debug\n"
  },
  {
    "path": "examples/development/php/latest/devbox.json",
    "content": "{\n  \"packages\": [\n    \"php83Extensions.xdebug@latest\",\n    \"php83Extensions.imagick@latest\",\n    \"php@latest\"\n  ],\n  \"env\": {\n    \"PHPRC\": \"$PWD/devbox.d/php\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \"composer install\"\n    ],\n    \"scripts\": {\n      \"run_test\": \"php public/index.php\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/php/latest/public/index.php",
    "content": "<?php\n\necho 'Hello world';\n\nphpinfo();\n"
  },
  {
    "path": "examples/development/python/pip/.envrc",
    "content": "# Automatically sets up your devbox environment whenever you cd into this\n# directory via our direnv integration:\n\neval \"$(devbox generate direnv --print-envrc)\"\n\n# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/\n# for more details\n"
  },
  {
    "path": "examples/development/python/pip/README.md",
    "content": "# Python\n\nPython by default will attempt to install your packages globally, or in the Nix Store (which it does not have permissions to modify). To use Python with Devbox, we recommend setting up a Virtual Environment using pipenv or Poetry (see below).\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/python)\n\n## Adding Python to your Project\n\n`devbox add python@3.10`, or in your `devbox.json` add:\n\n```json\n  \"packages\": [\n    \"python@3.10\"\n  ],\n```\n\nThis will install Python 3.10 in your shell. You can find other versions of Python by running `devbox search python`. You can also view the available versions on [Nixhub](https://www.nixhub.io/packages/python)\n\n## Installing Packages with Pip\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/python/pip)\n\n\n[pip](https://pip.pypa.io/en/stable/) is the standard package manager for Python. Since it installs python packages globally, we strongly recommend using a virtual environment.\n\nYou can install `pip` by running `devbox add python3xxPackages.pip`, where `3xx` is the version of Python you want to install. This will also install the pip plugin for Devbox, which automatically creates a virtual environment for installing your packages locally\n\nYour virtual environment is created in the root directory of your project by default, and can be activated by running `. $VENV_DIR/bin/activate` in your devbox shell. You can activate the virtual environment automatically using the init_hook of your `devbox.json`:\n\n```json\n{\n    \"packages\": [\n        \"python310\",\n        \"python310Packages.pip\"\n    ],\n    \"shell\": {\n        \"init_hook\": \". $VENV_DIR/bin/activate\"\n    }\n}\n```\n"
  },
  {
    "path": "examples/development/python/pip/devbox.d/python310Packages.pip/venvShellHook.sh",
    "content": "SOURCE_DATE_EPOCH=$(date +%s)\n\nif [ -d \"$VENV_DIR\" ]; then\n    echo \"Skipping venv creation, '${VENV_DIR}' already exists\"\nelse\n    echo \"Creating new venv environment in path: '${VENV_DIR}'\"\n    # Note that the module venv was only introduced in python 3, so for 2.7\n    # this needs to be replaced with a call to virtualenv\n    which python3\n    python3 -m venv \"$VENV_DIR\"\nfi\n"
  },
  {
    "path": "examples/development/python/pip/devbox.json",
    "content": "{\n  \"packages\": [\n    \"python@latest\",\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \". $VENV_DIR/bin/activate\",\n      \"pip install -r requirements.txt\"\n    ],\n    \"scripts\": {\n      \"run_test\": \"python main.py\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/python/pip/main.py",
    "content": "from emoji import emojize\n\nif __name__ == \"__main__\":\n    print(emojize(\":rocket: Devbox with Pip :rocket:\"))\n"
  },
  {
    "path": "examples/development/python/pip/requirements.txt",
    "content": "emoji==2.1.0\npytest\n"
  },
  {
    "path": "examples/development/python/pipenv/Pipfile",
    "content": "[[source]]\nurl = \"https://pypi.org/simple\"\nverify_ssl = true\nname = \"pypi\"\n\n[packages]\nemoji = \"==2.1.0\"\ngrpcio = \"*\"\n\n[dev-packages]\npytest = \"*\"\n\n[requires]\npython_version = \"3.10\"\n"
  },
  {
    "path": "examples/development/python/pipenv/README.md",
    "content": "\n# Python\n\nPython by default will attempt to install your packages globally, or in the Nix Store (which it does not have permissions to modify). To use Python with Devbox, we recommend setting up a Virtual Environment using pipenv or Poetry (see below).\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/python)\n\n## Adding Python to your Project\n\n`devbox add python@3.10`, or in your `devbox.json` add:\n\n```json\n  \"packages\": [\n    \"python@3.10\"\n  ],\n```\n\nThis will install Python 3.10 in your shell. You can find other versions of Python by running `devbox search python`. You can also view the available versions on [Nixhub](https://www.nixhub.io/packages/python)\n\n## Pipenv\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/python/pipenv)\n\n\n[pipenv](https://pipenv.pypa.io/en/latest/) is a tool that will automatically set up a virtual environment for installing your PyPi packages.\n\nYou can install `pipenv` by adding it to the packages in your `devbox.json`. You can then manage your packages and virtual environment via a Pipfile\n\n```json\n{\n    \"packages\": [\n        \"python310\",\n        \"pipenv\"\n    ],\n    \"shell\": {\n        \"init_hook\": \"pipenv shell\"\n    }\n}\n```\n\nThis init_hook will automatically start your virtualenv when you run `devbox shell`.\n"
  },
  {
    "path": "examples/development/python/pipenv/devbox.json",
    "content": "{\n    \"packages\": [\n        \"pipenv@latest\",\n        \"python@3.10\"\n    ],\n    \"shell\": {\n        \"init_hook\": [\n            \"pipenv install --dev\"\n        ],\n        \"scripts\": {\n            \"run_test\": \"pipenv run python main.py\"\n        }\n    }\n}\n"
  },
  {
    "path": "examples/development/python/pipenv/main.py",
    "content": "from emoji import emojize\n\nif __name__ == \"__main__\":\n    print(emojize(\":rocket: Devbox with Pipenv :rocket:\"))\n"
  },
  {
    "path": "examples/development/python/pipenv/requirements.txt",
    "content": "emoji==2.1.0\npytest"
  },
  {
    "path": "examples/development/python/poetry/poetry-demo/.gitignore",
    "content": "poetry_demo/__pycache__/\ntests/__pycahe__/\n.pytest_cache/"
  },
  {
    "path": "examples/development/python/poetry/poetry-demo/README.md",
    "content": "# Python with Poetry Example\n\nPoetry automatically configures a virtual environment for installing your Python packages. This environment can be activated by running `poetry shell` from within your poetry project.\n\nFor more information, see the [Poetry Docs](https://python-poetry.org/docs/basic-usage/)\n\n## How to Run\n\nIn this directory, run:\n\n`devbox shell`\n\nThis will automatically activate your poetry shell via the `init_hook`.\n\nTo exit the poetry shell, use `exit`. To exit your devbox shell, use `exit` again.\n\n## Configuration\n\nSince Poetry automatically configures a virtual environment for you, no additional Devbox configuration is needed. You can mange your packages and projects.\n"
  },
  {
    "path": "examples/development/python/poetry/poetry-demo/devbox.json",
    "content": "{\n  \"packages\": [\n    \"python@latest\",\n    \"poetry@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \"poetry install\"\n    ],\n    \"scripts\": {\n      \"run_test\": \"poetry run python -m poetry_demo\",\n      \"test\": \"poetry run pytest\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/python/poetry/poetry-demo/poetry_demo/__init__.py",
    "content": "__version__ = '0.1.0'\n"
  },
  {
    "path": "examples/development/python/poetry/poetry-demo/poetry_demo/__main__.py",
    "content": "from emoji import emojize\n\nif __name__ == \"__main__\":\n    print(emojize(\":rocket: Devbox with Poetry :rocket:\"))"
  },
  {
    "path": "examples/development/python/poetry/poetry-demo/pyproject.toml",
    "content": "[tool.poetry]\nname = \"poetry-demo\"\nversion = \"0.1.0\"\ndescription = \"\"\nauthors = [\"John Lago <750845+Lagoja@users.noreply.github.com>\"]\n\n[tool.poetry.dependencies]\npython = \"^3.8\"\nemoji = \"^2.1.0\"\n\n[tool.poetry.dev-dependencies]\npytest = \"^7.2.2\"\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "examples/development/python/poetry/poetry-demo/tests/__init__.py",
    "content": ""
  },
  {
    "path": "examples/development/python/poetry/poetry-demo/tests/test_poetry_demo.py",
    "content": "from poetry_demo import __version__\n\n\ndef test_version():\n    assert __version__ == '0.1.0'\n"
  },
  {
    "path": "examples/development/python/poetry/poetry-pyproject-subdir/README.md",
    "content": "## Poetry in a monorepo\n\nThis example demonstrates using a poetry project in a sub-folder (`services` in this case)\nby specifying the `DEVBOX_PYPROJECT_DIR` that the poetry plugin can use.\n"
  },
  {
    "path": "examples/development/python/poetry/poetry-pyproject-subdir/devbox.json",
    "content": "{\n  \"packages\": [\n    \"poetry@latest\",\n    \"python3@latest\"\n  ],\n  \"env\": {\n    \"DEVBOX_PYPROJECT_DIR\": \"$PWD/service\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ],\n    \"scripts\": {\n      \"install-service\":[\n        \"cd service\",\n        \"poetry install\"\n      ],\n      \"run_test\": [\n        \"devbox run install-service\",\n        \"cd service && poetry run pytest\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/python/poetry/poetry-pyproject-subdir/frontend/.empty",
    "content": ""
  },
  {
    "path": "examples/development/python/poetry/poetry-pyproject-subdir/service/pyproject.toml",
    "content": "[tool.poetry]\nname = \"poetry-pyproject-subdir-service\"\nversion = \"0.1.0\"\ndescription = \"\"\nauthors = [\"Savil Srivastava <676452+savil@users.noreply.github.com>\"]\n\n[tool.poetry.dependencies]\npython = \"^3.8\"\nemoji = \"^2.1.0\"\n\n[tool.poetry.dev-dependencies]\npytest = \"^7.2.2\"\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "examples/development/python/poetry/poetry-pyproject-subdir/service/test_with_pytest.py",
    "content": "\ndef test_always_passes():\n    assert True\n\n#def test_always_fails():\n#    assert False\n"
  },
  {
    "path": "examples/development/ruby/.envrc",
    "content": "# Automatically sets up your devbox environment whenever you cd into this\n# directory via our direnv integration:\n\neval \"$(devbox generate direnv --print-envrc)\"\n\n# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/\n# for more details\n"
  },
  {
    "path": "examples/development/ruby/README.md",
    "content": "# Ruby\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/development/ruby)\n\n\nRuby can be automatically configured by Devbox via the built-in Ruby Plugin. This plugin will activate automatically when you install Ruby 2.7 using `devbox add ruby`.\n\n## Adding Ruby to your shell\n\nRun `devbox add ruby@3.1 bundler`, or add the following to your `devbox.json`\n\n```json\n    \"packages\": [\n        \"ruby@3.1\",\n        \"bundler@latest\"\n    ]\n```\n\nThis will install Ruby 3.1 to your shell. You can find other installable versions of Ruby by running `devbox search ruby`. You can also view the available versions on [Nixhub](https://www.nixhub.io/packages/ruby)\n\n## Ruby Plugin Support\n\nDevbox will automatically create the following configuration when you install Ruby with `devbox add`.\n\n### Environment Variables\n\nThese environment variables configure Gem to install your gems locally, and set your Gem Home to a local folder\n\n```bash\nRUBY_CONFDIR={PROJECT_DIR}/.devbox/virtenv/ruby\nGEMRC={PROJECT_DIR}/.devbox/virtenv/ruby/.gemrc\nGEM_HOME={PROJECT_DIR}/.devbox/virtenv/ruby\nPATH={PROJECT_DIR}/.devbox/virtenv/ruby/bin:$PATH\n```\n\n## Bundler\n\nIn case you are using bundler to install gems, bundler config file can still be used to pass configs and flags to install gems.\n\n`.bundle/config` file example:\n\n```dotenv\nBUNDLE_BUILD__SASSC: \"--disable-lto\"\n```\n\n## Related Docs\n\n* [Rails + Devbox](https://www.jetify.com/devbox/docs/devbox_examples/stacks/rails/)\n* [Jekyll + Devbox](https://www.jetify.com/devbox/docs/devbox_examples/stacks/jekyll/)\n"
  },
  {
    "path": "examples/development/ruby/devbox.json",
    "content": "{\n  \"packages\": [\n    \"bundler@2.4\",\n    \"ruby@3.1\"\n  ],\n  \"shell\": {\n    \"init_hook\": [],\n    \"scripts\": {\n      \"run_test\": \"ruby -e 'puts \\\"Hello from Ruby #{RUBY_VERSION}\\\"'\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/ruby/hello.rb",
    "content": "puts \"Hello World!\""
  },
  {
    "path": "examples/development/rust/README.md",
    "content": "# Rust Example\n\nA Devbox Shell for running Rust\n\n## Configuration\n\nThis project adds the `rustup` package to your devbox shell, and then uses that package to install the right Rust toolchain locally in your project directory (set by the `RUSTUP_HOME` environment variable in `conf/set-env.sh`).\n\nTo change the version of Rust you want to use, you should modify the `rustup default stable` line in the `init_hook` of this project's `devbox.json`.\n\n## How to Run\n\n* Build the project\n\n```bash\ncargo build\n```\n\n* Run the project\n\n```bash\ncargo run\n```\n"
  },
  {
    "path": "examples/development/rust/rust-stable-hello-world/.gitignore",
    "content": "target/\n.rustup/"
  },
  {
    "path": "examples/development/rust/rust-stable-hello-world/Cargo.toml",
    "content": "[package]\nname = \"rust-stable-hello-world\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\n"
  },
  {
    "path": "examples/development/rust/rust-stable-hello-world/conf/set-env.sh",
    "content": "echo \"project dir is ${PROJECT_DIR}\"\n\nrustupHomeDir=\"${PROJECT_DIR}/.rustup\"\nmkdir -p \"${rustupHomeDir}\"\nexport RUSTUP_HOME=\"${rustupHomeDir}\"\nexport LIBRARY_PATH=\"${LIBRARY_PATH}:${PROJECT_DIR}/nix/profile/default/lib\"\n"
  },
  {
    "path": "examples/development/rust/rust-stable-hello-world/devbox.json",
    "content": "{\n    \"packages\": [\n        \"rustup@latest\",\n        \"libiconv@latest\"\n    ],\n    \"env\": {\n      \"PROJECT_DIR\": \"$PWD\"\n    },\n    \"shell\": {\n        \"init_hook\": [\n            \". conf/set-env.sh\",\n            \"rustup default stable\",\n            \"cargo fetch\"\n        ],\n        \"scripts\": {\n            \"build-docs\": \"cargo doc\",\n            \"start\": \"cargo run\",\n            \"run_test\": [\n                \"cargo test -- --show-output\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "examples/development/rust/rust-stable-hello-world/src/main.rs",
    "content": "fn main() {\n  println!(\"{}\", hello_world())\n}\n\nfn hello_world() -> &'static str {\n    return \"Hello, world!\";\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn test_hello_world() {\n    assert_eq!(hello_world(), \"Hello, world!\")\n  }\n}\n"
  },
  {
    "path": "examples/development/zig/README.md",
    "content": "# Zig Example\n\nA Devbox Shell for running Zig\n\n## How to Run\n\n* Build the project\n\n```bash\nzig build install\n```\n\n* Run the project\n\n```bash\nzig build run\n```\n"
  },
  {
    "path": "examples/development/zig/zig-hello-world/.gitignore",
    "content": "zig-cache/\nzig-out/\n"
  },
  {
    "path": "examples/development/zig/zig-hello-world/build.zig",
    "content": "const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n    // Standard target options allows the person running zig build to choose what\n    // target to build for. By default, any target is allowed, and no choice means to\n    // target the host system. Other options for restricting supported target set are\n    // available.\n    const target = b.standardTargetOptions(.{});\n\n    // Standard optimization options allow the person running zig build to select\n    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. By default none of\n    // the release options are considered the preferable choice by the build script,\n    // and the user must make a decision in order to create a release build.\n    const optimize = b.standardOptimizeOption(.{});\n\n    const exe = b.addExecutable(.{\n        .name = \"zig-hello-world\",\n        .root_source_file = .{ .path = \"src/main.zig\" },\n        .target = target,\n        .optimize = optimize,\n    });\n    b.installArtifact(exe);\n\n    const run_exe = b.addRunArtifact(exe);\n    const run_step = b.step(\"run\", \"Run the application\");\n    run_step.dependOn(&run_exe.step);\n\n    const test_step = b.step(\"test\", \"Run unit tests\");\n    const unit_tests = b.addTest(.{ .root_source_file = .{ .path = \"src/main.zig\" }, .target = target, .optimize = optimize });\n    const run_unit_tests = b.addRunArtifact(unit_tests);\n    test_step.dependOn(&run_unit_tests.step);\n}\n"
  },
  {
    "path": "examples/development/zig/zig-hello-world/devbox.json",
    "content": "{\n  \"packages\": {\n    \"zig\": \"latest\"\n  },\n  \"shell\": {\n    \"scripts\": {\n      \"run_test\": \"zig build run test --summary all\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/development/zig/zig-hello-world/src/main.zig",
    "content": "const std = @import(\"std\");\n\npub fn main() anyerror!void {\n    std.log.info(\"Hello World! You are running zig.\", .{});\n}\n\ntest \"basic test\" {\n    try std.testing.expectEqual(10, 3 + 7);\n}\n"
  },
  {
    "path": "examples/flakes/README.md",
    "content": "# Flakes\n\nExamples that show how to add custom flakes to your Devbox project. These examples require [Devbox 0.4.7](https://www.jetify.com/blog/devbox-0-4-7/) or later.\n\nFor more details, you can also consult our Docs page on [using flakes](https://www.jetify.com/devbox/docs/guides/using_flakes/)\n\n## Local flakes (usually committed to your project)\n\nIn devbox.json use \"path:/path/to/flake#output\" as the package name.\n\n```json\n{\n  \"packages\": [\n    \"path:my-php-flake#php\",\n    \"path:my-php-flake#hello\"\n  ],\n  \"shell\": {\n    \"init_hook\": null\n  },\n  \"nixpkgs\": {\n    \"commit\": \"f80ac848e3d6f0c12c52758c0f25c10c97ca3b62\"\n  }\n}\n```\n\nThis installs the \"php\" and \"hello\" outputs from the flake at `my-php-flake`. These outputs can also be part of packages or legacyPackages.\n\n## Remote flakes\n\nUse `github:<org>/<repo>/<ref>#<output>` as the package name to install from a Github repo.\n\n```json\n{\n  \"packages\": [\n    \"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello\",\n    \"github:F1bonacc1/process-compose\"\n  ],\n  \"shell\": {\n    \"init_hook\": null\n  },\n  \"nixpkgs\": {\n    \"commit\": \"f80ac848e3d6f0c12c52758c0f25c10c97ca3b62\"\n  }\n}\n```\n\nThis installs the `hello` package from the 5233fd... commit of Nixpkgs, and the `default` output from the `F1bonacc1/process-compose` repo.\n"
  },
  {
    "path": "examples/flakes/go-mod/.gitignore",
    "content": "ory-cli/result/*\n"
  },
  {
    "path": "examples/flakes/go-mod/README.md",
    "content": "# Building a Go Module with Flakes\n\nThis flake shows how to build a custom Go module and add it to your Devbox project. In this case, we're building the [Ory CLI](https://github.com/ory/cli)\n\nThis example uses `buildGoModule` from Nix to build the module as a package in our Flake. You can view the flake.nix file in the ory-cli folder to see a commented example of how this function is used.\n\nWe import the ory CLI in our project by adding it to our packages in `devbox.json`:\n\n```json\n{\n  \"packages\": [\n    \"path:ory-cli\"\n  ],\n   ...\n}\n```\n\nNote: you will need [Devbox 0.4.7](https://www.jetify.com/blog/devbox-0-4-7/) or later for this to work. You can use this as an example to create your own templates.\n\nFor more details on using Flakes with Devbox, read our post on [Using Nix Flakes with Devbox](https://www.jetify.com/blog/using-nix-flakes-with-devbox/)\n"
  },
  {
    "path": "examples/flakes/go-mod/devbox.json",
    "content": "{\n  \"packages\": [\n    \"path:ory-cli#ory-cli\"\n  ],\n  \"shell\": {\n    \"init_hook\": null\n  },\n  \"nixpkgs\": {\n    \"commit\": \"f80ac848e3d6f0c12c52758c0f25c10c97ca3b62\"\n  }\n}\n"
  },
  {
    "path": "examples/flakes/go-mod/ory-cli/flake.nix",
    "content": "{\n  description =\n    \"This flake builds the Ory CLI using Nix's buildGoModule Function.\";\n\n  inputs = {\n    nixpkgs.url = \"nixpkgs/nixos-unstable\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n    # The Ory CLI is not a flake, so we have to use the Github input and build it ourselves.\n    ory-cli = {\n      type = \"github\";\n      owner = \"ory\";\n      repo = \"cli\";\n      ref = \"v0.2.2\";\n      flake = false;\n    };\n  };\n\n  outputs = { self, nixpkgs, flake-utils, ory-cli }:\n    # Use the flake-utils lib to easily create a multi-system flake\n    flake-utils.lib.eachDefaultSystem (system:\n      let\n        # Define some variables that we want to use in our package build. You'll want to update version and `ref` above to use a different version of Ory.\n        version = \"0.2.2\";\n      in\n      {\n        packages =\n          let\n            pkgs = import nixpkgs { inherit system; };\n            pname = \"ory\";\n            name = \"ory-${version}\";\n          in\n          {\n            # Build the Ory CLI using Nix's buildGoModuleFunction\n            ory-cli = pkgs.buildGoModule {\n              inherit version;\n              inherit pname;\n              inherit name;\n\n              # Path to the source code we want to build. In this case, it's the `ory-cli` input we defined above.\n              src = ory-cli;\n\n              # This was in the Makefile in the Ory repo, not sure if it's required\n              tags = [ \"sqlite\" ];\n\n              doCheck = false;\n\n              # If the vendor folder is not checked in, we have to provide a hash for the vendor folder. Nix requires this to ensure the vendor folder is reproducible, and matches what we expect.\n              vendorSha256 = \"sha256-J9jyeLIT+1pFnHOUHrzmblVCJikvY05Sw9zMz5qaDOk=\";\n\n              # The Go Mod is named `cli` by default, so we rename it to `ory`.\n              postInstall = ''\n                mv $out/bin/cli $out/bin/ory\n              '';\n            };\n          };\n      }\n    );\n}\n"
  },
  {
    "path": "examples/flakes/overlay/.nvmrc",
    "content": "16\n"
  },
  {
    "path": "examples/flakes/overlay/README.md",
    "content": "# Adding Overlays with Flakes\n\nFor a more in depth walkthrough of this example, check out our [blog post](https://www.jetify.com/blog/using-nix-flakes-with-devbox/)\n\nThis flake shows how to use an overlay Nix flake to override a Nixpkgs package and use it in your devbox configuration.\n\nIn this example, using the default `yarn` from Nixpkgs will cause `yarn start` to fail. To fix this issue, we use an overlay to modify the `yarn` package to use NodeJS 16 instead of it's default NodeJS 14.\n\n```nix\n\n   overlay = (final: prev: {\n      yarn = prev.yarn.override { nodejs = final.pkgs.nodejs-16_x; };\n   });\n```\n\nThe yarn-overlay flake exports the modified yarn package in it's outputs. We can then use this package in our devbox shell by adding it to `packages` in our `devbox.json` file.\n\n```json\n{\n   \"packages\": [\n      \"path:./yarn-overlay#yarn\"\n      \"fnm\"\n   ]\n   ...\n}\n```\n\nNote: you will need Devbox 0.4.7-dev or later for this to work. You can try it out by exporting `DEVBOX_VERSION=0.4.7-dev` before running `devbox shell`.\n\nYou can use the flake.nix in the yarn-overlay directory as a template for creating your own overlays.\n"
  },
  {
    "path": "examples/flakes/overlay/devbox.json",
    "content": "{\n  \"packages\": [\n    \"path:yarn-overlay#yarn\",\n    \"fnm@latest\",\n    \"nodejs@16.8\"\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \"eval \\\"$(fnm env --use-on-cd)\\\"\"\n    ]\n  },\n  \"nixpkgs\": {\n    \"commit\": \"eabc38219184cc3e04a974fe31857d8e0eac098d\"\n  }\n}\n"
  },
  {
    "path": "examples/flakes/overlay/package.json",
    "content": "{\n  \"name\": \"nix-yarn-issue\",\n  \"version\": \"1.0.0\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"echo 'it should run'\"\n  },\n  \"engines\": {\n    \"node\": \">= 16\"\n  }\n}\n"
  },
  {
    "path": "examples/flakes/overlay/yarn-overlay/flake.nix",
    "content": "{\n  description =\n    \"This flake outputs a modified version of Yarn that uses NodeJS 16\";\n\n  inputs = {\n    nixpkgs.url = \"nixpkgs/fc3de6da83863f8f36fdcac1c199c6066a6a0378\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = { self, nixpkgs, flake-utils }:\n  # Use the flake-utils lib to easily create a multi-system flake\n  flake-utils.lib.eachDefaultSystem (system:\n    let\n      # You can define overlays as functions using the example below\n      # This overlay will modify yarn to use nodejs-16_x\n      overlay = (final: prev: {\n        yarn = prev.yarn.override { nodejs = final.pkgs.nodejs-16_x; };\n      });\n\n      #\n      pkgs =\n        import nixpkgs {\n          inherit system;\n          # Add your overlays to the list below. Note that they will be applied in order\n          overlays = [ overlay ];\n        };\n\n    in rec {\n      # For our outputs, we'll return the modified Yarn package from our overridden nixpkgs.\n      packages = {\n        yarn = pkgs.yarn;\n      };\n    }\n  );\n}\n\n"
  },
  {
    "path": "examples/flakes/php/README.md",
    "content": "# Custom PHP Flakes example\n\nShows how to install a custom version of PHP using flakes. This PHP contains the ds and memcached extensions.\n\nIt also shows how you can import multiple outputs from a single flake. (php, hello)\n"
  },
  {
    "path": "examples/flakes/php/devbox.d/php/php-fpm.conf",
    "content": "[global]\npid = ${PHPFPM_PID_FILE}\nerror_log = ${PHPFPM_ERROR_LOG_FILE}\ndaemonize = yes\n\n[www]\n; user = www-data\n; group = www-data\nlisten = 127.0.0.1:${PHPFPM_PORT}\n; listen.owner = www-data\n; listen.group = www-data\npm = dynamic\npm.max_children = 5\npm.start_servers = 2\npm.min_spare_servers = 1\npm.max_spare_servers = 3\nchdir = /\n"
  },
  {
    "path": "examples/flakes/php/devbox.d/php/php.ini",
    "content": "[php]\n\n; Put your php.ini directives here. For the latest default php.ini file, see https://github.com/php/php-src/blob/master/php.ini-production\n\n; memory_limit = 128M\n; expose_php = Off\n"
  },
  {
    "path": "examples/flakes/php/devbox.json",
    "content": "{\n  \"packages\": [\n    \"path:my-php-flake#php\",\n    \"path:my-php-flake#hello\"\n  ],\n  \"shell\": {\n    \"init_hook\": null,\n    \"scripts\": {\n      \"run_test\": \"php -m | grep ds || exit 1\"\n    }\n  },\n  \"nixpkgs\": {\n    \"commit\": \"f80ac848e3d6f0c12c52758c0f25c10c97ca3b62\"\n  }\n}\n"
  },
  {
    "path": "examples/flakes/php/my-php-flake/flake.nix",
    "content": "{\n  description = \"A flake that outputs PHP with memcached and ds extension and hello pkg.\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = { self, nixpkgs, flake-utils }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let\n        pkgs = nixpkgs.legacyPackages.${system};\n      in\n      {\n        packages = {\n          # Flakes can export multiple packages. To include specific packages in\n          # devbox.json you can use url fragments (e.g. path:my-flake#my-package)\n          php = pkgs.php.withExtensions ({ enabled, all }: enabled ++ (with all; [ ds memcached ]));\n          hello = pkgs.hello;\n\n          # If you only want to export a single package, you can name it default which allows\n          # installation without using url fragment (.e.g. \"path:my-flake\")\n          # default = pkgs.php.withExtensions ({ enabled, all }: enabled ++ (with all; [ ds memcached ]));\n        };\n      });\n}\n"
  },
  {
    "path": "examples/flakes/php-extension/README.md",
    "content": "# Adding a custom PHP Extension\n\nThis example shows how to add a custom PHP extension to a PHP package using Flakes. This uses an extension built from the [PHP Skeleton Extension](https://github.com/improved-php-library/skeleton-php-ext) from [Improved PHP Library](https://github.com/improved-php-library)\n\nTo test this example:\n\n1. Run `devbox shell` to start your shell\n2. Start PHP in interactive mode with `php -a`\n3. Run `echo skeleton_nop(\"Hello World\");` to test the extension. This should print `Hello World` to the screen\n"
  },
  {
    "path": "examples/flakes/php-extension/devbox.json",
    "content": "{\n    \"packages\": [\n        \"path:my-php-flake\"\n    ],\n    \"shell\": {\n        \"init_hook\": null\n    },\n    \"nixpkgs\": {\n        \"commit\": \"f80ac848e3d6f0c12c52758c0f25c10c97ca3b62\"\n    }\n}\n"
  },
  {
    "path": "examples/flakes/php-extension/my-php-flake/flake.nix",
    "content": "{\n  description =\n    \"A flake that outputs PHP with a custom extension (skeleton.so) linked.\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = { self, nixpkgs, flake-utils }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let\n        pkgs = nixpkgs.legacyPackages.${system};\n        # Customize and export the PHP package with some extra configuration\n        php-ext = pkgs.php.buildEnv {\n            # extraConfig will add the line below to the php.ini in our Nix store.\n            # ${self} is a variable representing the current flake\n            extraConfig = ''\n              extension=${self}/skeleton.so\n            '';\n        };\n      in {\n        packages = {\n          # Export the PHP package with our custom extension as the default\n          default = php-ext;\n        };\n      });\n}\n"
  },
  {
    "path": "examples/flakes/remote/devbox.json",
    "content": "{\n  \"packages\": [\n    \"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello\",\n    \"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#cowsay\",\n    \"github:F1bonacc1/process-compose/v0.43.1\"\n  ],\n  \"shell\": {\n    \"init_hook\": null,\n    \"scripts\": {\n      \"run_test\": \"cowsay -- hello\"\n    }\n  },\n  \"nixpkgs\": {\n    \"commit\": \"f80ac848e3d6f0c12c52758c0f25c10c97ca3b62\"\n  }\n}\n"
  },
  {
    "path": "examples/insecure/devbox.json",
    "content": "{\n  \"packages\": {\n    \"nodejs\": {\n      \"version\":        \"16\",\n      \"allow_insecure\": [\"nodejs-16.20.2\"]\n    }\n  },\n  \"shell\": {\n    \"init_hook\": \"echo 'Welcome to devbox!' > /dev/null\",\n    \"scripts\": {\n      \"run_test\": \"node --version\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/plugins/builtin/devbox.d/php84/php-fpm.conf",
    "content": "[global]\npid = ${PHPFPM_PID_FILE}\nerror_log = ${PHPFPM_ERROR_LOG_FILE}\ndaemonize = yes\n\n[www]\n; user = www-data\n; group = www-data\nlisten = 127.0.0.1:${PHPFPM_PORT}\n; listen.owner = www-data\n; listen.group = www-data\npm = dynamic\npm.max_children = 5\npm.start_servers = 2\npm.min_spare_servers = 1\npm.max_spare_servers = 3\nchdir = /\n"
  },
  {
    "path": "examples/plugins/builtin/devbox.d/php84/php.ini",
    "content": "[php]\n\n; Put your php.ini directives here. For the latest default php.ini file, see https://github.com/php/php-src/blob/master/php.ini-production\n\n; memory_limit = 128M\n; expose_php = Off\n"
  },
  {
    "path": "examples/plugins/builtin/devbox.json",
    "content": "{\n  \"packages\": [],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ],\n    \"scripts\": {\n      \"run_test\": [\n        \"test -n \\\"$PHPRC\\\" || exit 1\"\n      ]\n    }\n  },\n  \"include\": [\n    // Installs the php plugin using php84 as trigger package\n    \"plugin:php84\"\n  ]\n}\n"
  },
  {
    "path": "examples/plugins/git/devbox.d/jetify-com.devbox-plugin-example.my-github-plugin/some-file.txt",
    "content": "some data\n"
  },
  {
    "path": "examples/plugins/git/devbox.d/jetpack-io-devbox-plugin-example/some-file.txt",
    "content": "some data\n"
  },
  {
    "path": "examples/plugins/git/devbox.json",
    "content": "{\n  \"packages\": [],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ]\n  },\n  \"include\": [\n    \"git+https://github.com/jetify-com/devbox-plugin-example.git\",\n    \"git+https://github.com/jetify-com/devbox-plugin-example.git?dir=custom-dir\",\n    \"git+https://github.com/jetify-com/devbox-plugin-example.git?ref=test/branch\"\n  ]\n}\n"
  },
  {
    "path": "examples/plugins/git/test.sh",
    "content": "#!/bin/bash\n\nexpected=\"I AM SET (new value)\"\ncustom_expected=\"I AM SET TO CUSTOM (new value)\"\nif [ \"$MY_ENV_VAR\" == \"$expected\" ] && [ \"$MY_ENV_VAR_CUSTOM\" == \"$custom_expected\" ]; then\n  echo \"Success! MY_ENV_VAR is set to '$MY_ENV_VAR'\"\n  echo \"Success! MY_ENV_VAR_CUSTOM is set to '$MY_ENV_VAR_CUSTOM'\"\nelse\n  echo \"ERROR: MY_ENV_VAR environment variable is not set to '$expected' OR MY_ENV_VAR_CUSTOM variable is not set to '$custom_expected'\"\n  exit 1\nfi\n\necho BRANCH_ENV_VAR=$BRANCH_ENV_VAR\nif [ \"$BRANCH_ENV_VAR\" != \"I AM A BRANCH VAR\" ]; then exit 1; fi;\n"
  },
  {
    "path": "examples/plugins/git-with-revision/devbox.d/jetpack-io-devbox-plugin-example/some-file.txt",
    "content": "some data\n"
  },
  {
    "path": "examples/plugins/git-with-revision/devbox.json",
    "content": "{\n  \"packages\": [],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ]\n  },\n  \"include\": [\n    \"git+https://github.com/jetify-com/devbox-plugin-example.git?rev=d9c00334353c9b1294c7bd5dbea128c149b2eb3a\"\n  ]\n}\n"
  },
  {
    "path": "examples/plugins/git-with-revision/test.sh",
    "content": "#!/bin/bash\n\nexpected=\"I AM SET\"\nif [ \"$MY_ENV_VAR\" == \"$expected\" ]; then\n  echo \"Success! MY_ENV_VAR is set to '$MY_ENV_VAR'\"\nelse\n  echo \"MY_ENV_VAR environment variable is not set to '$expected'\"\n  exit 1\nfi\n"
  },
  {
    "path": "examples/plugins/github/devbox.d/jetify-com.devbox-plugin-example.my-github-plugin/some-file.txt",
    "content": "some data\n"
  },
  {
    "path": "examples/plugins/github/devbox.d/jetpack-io-devbox-plugin-example/some-file.txt",
    "content": "some data\n"
  },
  {
    "path": "examples/plugins/github/devbox.json",
    "content": "{\n  \"packages\": [],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ],\n    \"scripts\": {\n      \"run_test\": [\n        \"./test.sh\"\n      ]\n    }\n  },\n  \"include\": [\n    \"github:jetify-com/devbox-plugin-example\",\n    \"github:jetify-com/devbox-plugin-example?dir=custom-dir\",\n    \"github:jetify-com/devbox-plugin-example/test/branch\",\n  ]\n}\n"
  },
  {
    "path": "examples/plugins/github/test.sh",
    "content": "#!/bin/bash\n\nexpected=\"I AM SET (new value)\"\ncustom_expected=\"I AM SET TO CUSTOM (new value)\"\nif [ \"$MY_ENV_VAR\" == \"$expected\" ] && [ \"$MY_ENV_VAR_CUSTOM\" == \"$custom_expected\" ]; then\n  echo \"Success! MY_ENV_VAR is set to '$MY_ENV_VAR'\"\n  echo \"Success! MY_ENV_VAR_CUSTOM is set to '$MY_ENV_VAR_CUSTOM'\"\nelse\n  echo \"ERROR: MY_ENV_VAR environment variable is not set to '$expected' OR MY_ENV_VAR_CUSTOM variable is not set to '$custom_expected'\"\n  exit 1\nfi\n\necho BRANCH_ENV_VAR=$BRANCH_ENV_VAR\nif [ \"$BRANCH_ENV_VAR\" != \"I AM A BRANCH VAR\" ]; then exit 1; fi;\n"
  },
  {
    "path": "examples/plugins/github-with-revision/devbox.d/jetpack-io-devbox-plugin-example/some-file.txt",
    "content": "some data\n"
  },
  {
    "path": "examples/plugins/github-with-revision/devbox.json",
    "content": "{\n  \"packages\": [],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ],\n    \"scripts\": {\n      \"run_test\": [\n        \"./test.sh\"\n      ]\n    }\n  },\n  \"include\": [\n    \"github:jetify-com/devbox-plugin-example/d9c00334353c9b1294c7bd5dbea128c149b2eb3a\"\n  ]\n}\n"
  },
  {
    "path": "examples/plugins/github-with-revision/test.sh",
    "content": "#!/bin/bash\n\nexpected=\"I AM SET\"\nif [ \"$MY_ENV_VAR\" == \"$expected\" ]; then\n  echo \"Success! MY_ENV_VAR is set to '$MY_ENV_VAR'\"\nelse\n  echo \"MY_ENV_VAR environment variable is not set to '$expected'\"\n  exit 1\nfi\n"
  },
  {
    "path": "examples/plugins/local/README.md",
    "content": "# Custom plugin example\n\nShows how to write custom local plugin. Plugins can:\n\n* Install packages\n* Create templatized files (including flakes)\n* Declare services (using process-compose)\n"
  },
  {
    "path": "examples/plugins/local/devbox.d/my-plugin/some-file.txt",
    "content": "some data\n"
  },
  {
    "path": "examples/plugins/local/devbox.json",
    "content": "{\n  \"packages\": [],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ],\n    \"scripts\": {\n      \"run_test\": [\n        \"./test.sh\"\n      ]\n    }\n  },\n  \"include\": [\n    \"path:my-plugin/plugin.json\"\n  ]\n}\n"
  },
  {
    "path": "examples/plugins/local/my-plugin/plugin.json",
    "content": "{\n  \"name\": \"my-plugin\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Example custom plugin\",\n  \"env\": {\n    \"MY_FOO_VAR\": \"BAR\"\n  },\n  \"create_files\": {\n    /*\nthis is a comment inside the create files\n    */\n    \"{{ .Virtenv }}/empty-dir\": \"\",\n    \"{{ .Virtenv }}/some-file\": \"some-file.txt\",\n    \"{{ .DevboxDir }}/some-file.txt\": \"some-file.txt\",\n    \"{{ .Virtenv }}/process-compose.yaml\": \"process-compose.yaml\"\n  },\n  \"shell\": {\n      // this is a comment before init-hooks\n      \"init_hook\": [\n          \"echo \\\"ran local plugin init hook\\\"\",\n          \"export MY_INIT_HOOK_VAR=BAR\"\n      ]\n  }\n}\n"
  },
  {
    "path": "examples/plugins/local/my-plugin/process-compose.yaml",
    "content": "version: \"0.5\"\n\nprocesses:\n  my-plugin-service:\n    command: echo \"success\" && tail -f /dev/null\n    availability:\n      restart: \"always\"\n"
  },
  {
    "path": "examples/plugins/local/my-plugin/some-file.txt",
    "content": "some data\n"
  },
  {
    "path": "examples/plugins/local/test.sh",
    "content": "#!/bin/bash\n\nif [ -z \"$MY_FOO_VAR\" ]; then\n  echo \"MY_FOO_VAR environment variable is not set.\"\n  exit 1\nelse\n  echo \"MY_FOO_VAR is set to '$MY_FOO_VAR'\"\nfi\n\nif [ -z \"$MY_INIT_HOOK_VAR\" ]; then\n  echo \"MY_INIT_HOOK_VAR environment variable is not set.\"\n  exit 1\nelse\n  echo \"MY_INIT_HOOK_VAR is set to '$MY_INIT_HOOK_VAR'\"\nfi\n"
  },
  {
    "path": "examples/plugins/v2-git/devbox.d/jetpack-io-devbox-plugin-example/some-file.txt",
    "content": "some data\n"
  },
  {
    "path": "examples/plugins/v2-git/devbox.json",
    "content": "{\n  \"packages\": [],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ]\n  },\n  \"include\": [\n    \"git+https://github.com/jetify-com/devbox-plugin-example.git\",\n    \"git+https://github.com/jetify-com/devbox-plugin-example?dir=custom-dir\"\n  ]\n}\n"
  },
  {
    "path": "examples/plugins/v2-git/test.sh",
    "content": "#!/bin/bash\n\nexpected=\"I AM SET (new value)\"\ncustom_expected=\"I AM SET TO CUSTOM (new value)\"\nif [ \"$MY_ENV_VAR\" == \"$expected\" ] && [ \"$MY_ENV_VAR_CUSTOM\" == \"$custom_expected\" ]; then\n  echo \"Success! MY_ENV_VAR is set to '$MY_ENV_VAR'\"\n  echo \"Success! MY_ENV_VAR_CUSTOM is set to '$MY_ENV_VAR_CUSTOM'\"\nelse\n  echo \"ERROR: MY_ENV_VAR environment variable is not set to '$expected' OR MY_ENV_VAR_CUSTOM variable is not set to '$custom_expected'\"\n  exit 1\nfi\n"
  },
  {
    "path": "examples/plugins/v2-github/devbox.d/jetpack-io-devbox-plugin-example/some-file.txt",
    "content": "some data\n"
  },
  {
    "path": "examples/plugins/v2-github/devbox.json",
    "content": "{\n  \"packages\": [],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ],\n    \"scripts\": {\n      \"run_test\": [\n        \"./test.sh\"\n      ]\n    }\n  },\n  \"include\": [\n    // TODO, make these more interesting by adding v2 capabilities\n    \"github:jetify-com/devbox-plugin-example\",\n    \"github:jetify-com/devbox-plugin-example?dir=custom-dir\"\n  ]\n}\n"
  },
  {
    "path": "examples/plugins/v2-github/test.sh",
    "content": "#!/bin/bash\n\nexpected=\"I AM SET (new value)\"\ncustom_expected=\"I AM SET TO CUSTOM (new value)\"\nif [ \"$MY_ENV_VAR\" == \"$expected\" ] && [ \"$MY_ENV_VAR_CUSTOM\" == \"$custom_expected\" ]; then\n  echo \"Success! MY_ENV_VAR is set to '$MY_ENV_VAR'\"\n  echo \"Success! MY_ENV_VAR_CUSTOM is set to '$MY_ENV_VAR_CUSTOM'\"\nelse\n  echo \"ERROR: MY_ENV_VAR environment variable is not set to '$expected' OR MY_ENV_VAR_CUSTOM variable is not set to '$custom_expected'\"\n  exit 1\nfi\n"
  },
  {
    "path": "examples/plugins/v2-local/devbox.d/plugin1/foo.txt",
    "content": "something\n"
  },
  {
    "path": "examples/plugins/v2-local/devbox.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/jetify-com/devbox/main/.schema/devbox.schema.json\",\n  \"packages\": [],\n  \"env\": {\n    \"PLUGIN1_ENV2\": \"override\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ],\n    \"scripts\": {\n      \"run_test\": [\n        // This tests init hook and env included from plugin1\n        \"test -n \\\"$PLUGIN1_INIT_HOOK\\\" || exit 1\",\n        \"test -n \\\"$PLUGIN1_ENV\\\" || exit 1\",\n        // This tests init hook and env included from plugin1a (via plugin1, with relative path)\n        \"test -n \\\"$PLUGIN1A_INIT_HOOK\\\" || exit 1\",\n        \"test -n \\\"$PLUGIN1A_ENV\\\" || exit 1\",\n        // Test env override\n        \"if [ \\\"$PLUGIN1_ENV2\\\" != \\\"override\\\" ]; then exit 1; fi;\",\n        // test included scripts\n        \"devbox run plugin_1_script\",\n        \"devbox run plugin_1A_script\",\n        // Test packages included recursively\n        \"hello\",\n        \"cowsay 'Hello, world!'\"\n      ]\n    }\n  },\n  \"include\": [\n    \"./plugin1\",\n    \"path:plugin2\",\n    \"./plugin3/plugin.json\",\n  ]\n}\n"
  },
  {
    "path": "examples/plugins/v2-local/plugin1/foo.txt",
    "content": "something\n"
  },
  {
    "path": "examples/plugins/v2-local/plugin1/plugin.json",
    "content": "{\n  \"name\": \"plugin1\",\n  \"packages\": [\"hello@latest\"],\n  \"env\": {\n    \"PLUGIN1_ENV\": \"success\",\n    \"PLUGIN1_ENV2\": \"success\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \"export PLUGIN1_INIT_HOOK=success\"\n    ],\n    \"scripts\": {\n      \"plugin_1_script\": [\n        \"echo success\"\n      ]\n    }\n  },\n  \"create_files\": {\n    \"{{ .DevboxDir }}/foo.txt\": \"foo.txt\",\n    \"{{ .Virtenv }}/process-compose.yaml\": \"process-compose.yaml\"\n  },\n  \"include\": [\n    \"./plugin1a\"\n  ]\n}\n"
  },
  {
    "path": "examples/plugins/v2-local/plugin1/plugin1a/plugin.json",
    "content": "{\n  \"name\": \"plugin1a\",\n  \"packages\": [\"cowsay@latest\"],\n  \"env\": {\n    \"PLUGIN1A_ENV\": \"success\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \"export PLUGIN1A_INIT_HOOK=success\"\n    ],\n    \"scripts\": {\n      \"plugin_1A_script\": [\n        \"echo success\"\n      ]\n    }\n  },\n  \"include\": [\n    \"../../plugin2\"\n  ]\n}\n"
  },
  {
    "path": "examples/plugins/v2-local/plugin1/process-compose.yaml",
    "content": "version: \"0.5\"\n\nprocesses:\n  plugin1-service:\n    command: \"echo 'Hello from plugin1' && sleep 1000\"\n    availability:\n      restart: \"always\"\n  \n"
  },
  {
    "path": "examples/plugins/v2-local/plugin2/plugin.json",
    "content": "{\n  \"name\": \"plugin2\",\n  \"create_files\": {\n    \"{{ .Virtenv }}/process-compose.yaml\": \"process-compose.yaml\"\n  }\n}\n"
  },
  {
    "path": "examples/plugins/v2-local/plugin2/process-compose.yaml",
    "content": "version: \"0.5\"\n\nprocesses:\n  plugin2-service:\n    command: \"echo 'Hello from plugin2' && sleep 1000\"\n    availability:\n      restart: \"always\"\n  \n"
  },
  {
    "path": "examples/plugins/v2-local/plugin3/plugin.json",
    "content": "{\n  \"name\": \"plugin3\"\n}\n"
  },
  {
    "path": "examples/servers/apache/.gitignore",
    "content": "*.log\n*.pid\n*.sock\n"
  },
  {
    "path": "examples/servers/apache/README.md",
    "content": "# Apache\n\nApache can be automatically configured by Devbox via the built-in Apache Plugin. This plugin will activate automatically when you install Apache using `devbox add apache`.\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/servers/apache)\n\n\n### Adding Apache to your Shell\n\nRun `devbox add apache`, or add the following to your `devbox.json`\n\n```json\n  \"packages\": [\n    \"apache@latest\"\n  ]\n```\n\nThis will install the latest version of Apache. You can find other installable versions of Apache by running `devbox search apache`. You can also view the available versions on [Nixhub](https://www.nixhub.io/packages/apache)\n\n## Apache Plugin Details\n\nThe Apache plugin will automatically create the following configuration when you install Apache with `devbox add`.\n\n### Services\n\n* apache\n\nUse `devbox services start|stop apache` to start and stop httpd in the background.\n\n### Helper Files\n\nThe following helper files will be created in your project directory:\n\n* {PROJECT_DIR}/devbox.d/apacheHttpd/httpd.conf\n* {PROJECT_DIR}/devbox.d/web/index.html\n\nNote that by default, Apache is configured with `./devbox.d/web` as the DocumentRoot. To change this, you should copy and modify the default `./devbox.d/apacheHttpd/httpd.conf`.\n\n### Environment Variables\n\n```bash\nHTTPD_ACCESS_LOG_FILE={PROJECT_DIR}/.devbox/virtenv/apacheHttpd/access.log\nHTTPD_ERROR_LOG_FILE={PROJECT_DIR}/.devbox/virtenv/apacheHttpd/error.log\nHTTPD_PORT=8080\nHTTPD_DEVBOX_CONFIG_DIR={PROJECT_DIR}\nHTTPD_CONFDIR={PROJECT_DIR}/devbox.d/apacheHttpd\n```\n\n### Notes\n\nWe recommend copying your `httpd.conf` file to a new directory and updating HTTPD_CONFDIR if you decide to modify it.\n"
  },
  {
    "path": "examples/servers/apache/devbox.d/apacheHttpd/httpd.conf",
    "content": "ServerAdmin             \"root@localhost\"\nServerName              \"devbox-apache\"\nListen                  \"${HTTPD_PORT}\"\nPidFile                 \"${HTTPD_CONFDIR}/apache.pid\"\n\nLoadModule mpm_event_module modules/mod_mpm_event.so\nLoadModule authz_host_module modules/mod_authz_host.so\nLoadModule authz_core_module modules/mod_authz_core.so\nLoadModule auth_basic_module modules/mod_auth_basic.so\nLoadModule mime_module modules/mod_mime.so\nLoadModule headers_module modules/mod_headers.so\nLoadModule unixd_module modules/mod_unixd.so\nLoadModule status_module modules/mod_status.so\nLoadModule proxy_module modules/mod_proxy.so\nLoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so\nLoadModule dir_module modules/mod_dir.so\nLoadModule alias_module modules/mod_alias.so\n\n<IfModule unixd_module>\n    User daemon\n    Group daemon\n</IfModule>\n\n<Directory />\n    AllowOverride none\n    Require all denied\n</Directory>\n\nDocumentRoot  \"${HTTPD_CONFDIR}/../web\"\n<Directory \"${HTTPD_CONFDIR}/../web\">\n    Options Indexes FollowSymLinks\n    AllowOverride None\n    Require all granted\n</Directory>\n\n<Files \".ht*\">\n    Require all denied\n</Files>\nErrorLog \"${HTTPD_ERROR_LOG_FILE}\"\n<IfModule headers_module>\n    RequestHeader unset Proxy early\n</IfModule>\n\n<VirtualHost \"*:${HTTPD_PORT}\">\n    ServerAdmin webmaster@localhost\n    ServerName  php_localhost\n\n    UseCanonicalName    Off\n    DocumentRoot \"${HTTPD_CONFDIR}/../web\"\n\n    <Directory \"${HTTPD_CONFDIR}/../web\">\n        Options All\n        AllowOverride All\n        <IfModule mod_authz_host.c>\n            Require all granted\n        </IfModule>\n    </Directory>\n\n    ## Added for php-fpm\n    ProxyPassMatch ^/(.*\\.php(/.*)?)$ fcgi://127.0.0.1:8082/${HTTPD_DEVBOX_CONFIG_DIR}/devbox.d/web/$1\n    DirectoryIndex index.html \n\n</VirtualHost>\n"
  },
  {
    "path": "examples/servers/apache/devbox.d/web/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hello World!</title>\n  </head>\n  <body>\n    Hello World!\n  </body>\n</html>\n"
  },
  {
    "path": "examples/servers/apache/devbox.json",
    "content": "{\n  \"$schema\":  \"https://raw.githubusercontent.com/jetify-com/devbox/0.12.0/.schema/devbox.schema.json\",\n  \"packages\": [\"apacheHttpd@2.4.58\"],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!' > /dev/null\"\n    ],\n    \"scripts\": {\n      \"start\": \"apachectl start -f $HTTPD_CONFDIR/httpd.conf -D FOREGROUND\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/servers/caddy/README.md",
    "content": "# Caddy\n\nCaddy can be configured automatically using Devbox's built in Caddy plugin. This plugin will activate automatically when you install Caddy using `devbox add caddy`\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/servers/caddy)\n\n\n### Adding Caddy to your Shell\n\nRun `devbox add caddy`, or add the following to your `devbox.json`\n\n```json\n  \"packages\": [\n    \"caddy@latest\"\n  ]\n```\n\nThis will install the latest version of Caddy. You can find other installable versions of Caddy by running `devbox search caddy`. You can also view the available versions on [Nixhub](https://www.nixhub.io/packages/caddy)\n\n## Caddy Plugin Details\n\nThe Caddy plugin will automatically create the following configuration when you install Caddy with `devbox add`\n\n### Services\n\n* caddy\n\nUse `devbox services start|stop caddy` to start and stop httpd in the background\n\n### Helper Files\n\nThe following helper files will be created in your project directory:\n\n* {PROJECT_DIR}/devbox.d/caddy/Caddyfile\n* {PROJECT_DIR}/devbox.d/web/index.html\n\nNote that by default, Caddy is configured with `./devbox.d/web` as the root. To change this, you should modify the default `./devbox.d/caddy/Caddyfile` or change the `CADDY_ROOT_DIR` environment variable\n\n### Environment Variables\n\n```bash\n* CADDY_CONFIG={PROJECT_DIR}/devbox.d/caddy/Caddyfile\n* CADDY_LOG_DIR={PROJECT_DIR}/.devbox/virtenv/caddy/log\n* CADDY_ROOT_DIR={PROJECT_DIR}/devbox.d/web\n```\n\n### Notes\n\nYou can customize the config used by the caddy service by modifying the Caddyfile in devbox.d/caddy, or by changing the CADDY_CONFIG environment variable to point to a custom config. The custom config must be either JSON or Caddyfile format.\n"
  },
  {
    "path": "examples/servers/caddy/devbox.d/caddy/Caddyfile",
    "content": "# See https://caddyserver.com/docs/caddyfile for more details\n{\n\tadmin 0.0.0.0:2020\n\tauto_https disable_certs\n\thttp_port 8080\n\thttps_port 4443\n}\n\n:8080 {\n\troot * {$CADDY_ROOT_DIR}\n\tlog {\n\t\toutput file {$CADDY_LOG_DIR}/caddy.log\n\t}\n\tfile_server\n}\n"
  },
  {
    "path": "examples/servers/caddy/devbox.d/web/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hello World!</title>\n  </head>\n  <body>\n    Hello World!\n  </body>\n</html>\n"
  },
  {
    "path": "examples/servers/caddy/devbox.json",
    "content": "{\n  \"packages\": [\n    \"caddy@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": [],\n    \"scripts\": {\n      \"start\": \"caddy run --config=devbox.d/caddy/Caddyfile\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/servers/nginx/.envrc",
    "content": "# Automatically sets up your devbox environment whenever you cd into this\n# directory via our direnv integration:\n\neval \"$(devbox generate direnv --print-envrc)\"\n\n# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/\n# for more details\n"
  },
  {
    "path": "examples/servers/nginx/.gitignore",
    "content": "conf/mysql/data\n*.log\n*.pid\n*.sock"
  },
  {
    "path": "examples/servers/nginx/README.md",
    "content": "# Nginx\n\nNGINX can be automatically configured by Devbox via the built-in NGINX Plugin. This plugin will activate automatically when you install NGINX using `devbox add nginx`\n\n[**Example Repo**](https://github.com/jetify-com/devbox/tree/main/examples/servers/nginx)\n\n\n## Adding NGINX to your Shell\n\nRun `devbox add nginx`, or add the following to your `devbox.json`\n\n```json\n  \"packages\": [\n    \"nginx@latest\"\n  ]\n```\n\nThis will install the latest version of NGINX. You can find other installable versions of NGINX by running `devbox search nginx`. You can also view the available versions on [Nixhub](https://www.nixhub.io/packages/nginx)\n\n## NGINX Plugin Details\n\n### Services\n\n* nginx\n\nUse `devbox services start|stop nginx` to start and stop the NGINX service in the background\n\n### Helper Files\n\nThe following helper files will be created in your project directory:\n\n* devbox.d/nginx/nginx.conf\n* devbox.d/nginx/fastcgi.conf\n* devbox.d/web/index.html\n\nNote that by default, NGINX is configured with `./devbox.d/web` as the root directory. To change this, you should modify `./devbox.d/nginx/nginx.conf`\n\n### Environment Variables\n\n```bash\nNGINX_CONFDIR=devbox.d/nginx/nginx.conf\nNGINX_PATH_PREFIX=.devbox/virtenv/nginx\nNGINX_TMPDIR=.devbox/virtenv/nginx/temp\n```\n\n### Notes\n\nYou can easily configure NGINX by modifying these env variables in your shell's `init_hook`\n\nTo customize:\n\n* Use $NGINX_CONFDIR to change the configuration directory\n* Use $NGINX_LOGDIR to change the log directory\n* Use $NGINX_PIDDIR to change the pid directory\n* Use $NGINX_RUNDIR to change the run directory\n* Use $NGINX_SITESDIR to change the sites directory\n* Use $NGINX_TMPDIR to change the tmp directory. Use $NGINX_USER to change the user\n* Use $NGINX_GROUP to customize.\n\nYou can also customize the `nginx.conf` and `fastcgi.conf` stored in `devbox.d/nginx`\n"
  },
  {
    "path": "examples/servers/nginx/devbox.d/nginx/fastcgi.conf",
    "content": "fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;\nfastcgi_param  SERVER_SOFTWARE    nginx;\nfastcgi_param  QUERY_STRING       $query_string;\nfastcgi_param  REQUEST_METHOD     $request_method;\nfastcgi_param  CONTENT_TYPE       $content_type;\nfastcgi_param  CONTENT_LENGTH     $content_length;\nfastcgi_param  SCRIPT_FILENAME    $realpath_root$fastcgi_script_name;\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\nfastcgi_param  REQUEST_URI        $request_uri;\nfastcgi_param  DOCUMENT_URI       $document_uri;\nfastcgi_param  DOCUMENT_ROOT      $document_root;\nfastcgi_param  SERVER_PROTOCOL    $server_protocol;\nfastcgi_param  REMOTE_ADDR        $remote_addr;\nfastcgi_param  REMOTE_PORT        $remote_port;\nfastcgi_param  SERVER_ADDR        $server_addr;\nfastcgi_param  SERVER_PORT        $server_port;\nfastcgi_param  SERVER_NAME        $server_name;\n"
  },
  {
    "path": "examples/servers/nginx/devbox.d/nginx/nginx.conf",
    "content": "events {}\nhttp{\nserver {\n         listen       8081;\n         listen       [::]:8081;\n         server_name  localhost;\n         root         ../../../devbox.d/web;\n\n         error_log error.log error;\n         access_log access.log;\n         client_body_temp_path temp/client_body;\n         proxy_temp_path temp/proxy;\n         fastcgi_temp_path temp/fastcgi;\n         uwsgi_temp_path temp/uwsgi;\n         scgi_temp_path temp/scgi;\n\n         index index.html;\n         server_tokens off;\n    }\n}\n"
  },
  {
    "path": "examples/servers/nginx/devbox.d/nginx/nginx.template",
    "content": "events {}\nhttp{\nserver {\n         listen       $NGINX_WEB_PORT;\n         listen       [::]:$NGINX_WEB_PORT;\n         server_name  $NGINX_WEB_SERVER_NAME;\n         root         $NGINX_WEB_ROOT;\n\n         error_log error.log error;\n         access_log access.log;\n         client_body_temp_path temp/client_body;\n         proxy_temp_path temp/proxy;\n         fastcgi_temp_path temp/fastcgi;\n         uwsgi_temp_path temp/uwsgi;\n         scgi_temp_path temp/scgi;\n\n         index index.html;\n         server_tokens off;\n    }\n}\n"
  },
  {
    "path": "examples/servers/nginx/devbox.d/web/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hello World!</title>\n  </head>\n  <body>\n    Hello World!\n  </body>\n</html>\n"
  },
  {
    "path": "examples/servers/nginx/devbox.json",
    "content": "{\n  \"packages\": [\n    \"nginx@latest\"\n  ],\n  \"env\": {\n    \"NGINX_WEB_PORT\": \"8080\"\n  },\n  \"shell\": {\n    \"scripts\": {\n      \"build\": \"if [ -f $NGINX_CONFDIR/nginx.template ]; then envsubst $(awk 'BEGIN {for (k in ENVIRON) {printf \\\"$\\\"k\\\",\\\"}}') < $NGINX_CONFDIR/nginx.template > $NGINX_CONFDIR/nginx.conf; fi\",\n      \"start\": \"nginx -p $NGINX_PATH_PREFIX -c $NGINX_CONFDIR/nginx.conf -e error.log -g \\\"pid nginx.pid;daemon off;\\\"\"  \n    }\n  }\n}\n"
  },
  {
    "path": "examples/stacks/django/.gitignore",
    "content": ".venv/\n__pycache__\n.devbox.cloud"
  },
  {
    "path": "examples/stacks/django/README.md",
    "content": "# Django Example\n\n[![Built with Devbox](https://www.jetify.com/img/devbox/shield_moon.svg)](https://www.jetify.com/devbox/docs/contributor-quickstart/)\n\n\n## How to Use\n\n1. Install [Devbox](https://www.jetify.com/docs/devbox/installing-devbox/index)\n1. Run `devbox shell` to install your packages and run the init_hook. This will activate your virtual environment and install Django.\n1. Initialize PostgreSQL with `devbox run initdb`.\n1. Start the Postgres service by running `devbox services up postgresql`. You can start it in the background using `devbox services up -b postgresql`.\n1. In the root directory, run `devbox run create_db` to create the database and run your Django migrations\n1. In the root directory, run `devbox run server` to start the server. You can access the Django example at `localhost:8000`\n\n## How to Create this Example from Scratch\n\n### Setting up the Project\n\n1. Install [Devbox](https://www.jetify.com/docs/devbox/installing-devbox/index).\n1. Run `devbox create --template django` to create a new Devbox project in your directory.\n1. Install Python and PostgreSQL with `devbox install`. This will also install the Devbox plugins for pip (which sets up your .venv directory) and PostgreSQL.\n1. Copy the requirements.txt and `todo_project` directory into the root folder of your project\n1. Start a devbox shell with `devbox shell`. This will activate your virtual environment and install your requirements using the commands below.\n\n   ```bash\n   . $VENV_DIR/bin/activate\n   pip install -r requirements.txt\n   ```\n\n   These lines are already added to your `init_hook` to automatically activate your venv.\n\n### Setting up the Database\n\nThe Django example uses a database. To set up the database, we will first create a new PostgreSQL database cluster, create the `todo_db` and user, and run the Django migrations.\n\n1. Initialize your Postgres database cluster with `devbox run initdb`.\n\n1. Start the Postgres service by running `devbox services start postgres`\n\n1. In your `devbox shell`, create the empty `todo_db` database and user with the following commands.\n\n   ```bash\n   createdb todo_db\n   psql todo_db -c \"CREATE USER todo_user WITH PASSWORD 'secretpassword';\"\n   ```\n\n   You can add this as a devbox script in your `devbox.json` file, so you can replicate the setup on other machines.\n\n1. Run the Django migrations to create the tables in your database.\n\n   ```bash\n   python todo_project/manage.py makemigrations\n   python todo_project/manage.py migrate\n   ```\n\nYour database is now ready to use. You can add these commands as a script in your `devbox.json` if you want to automate them for future use. See `create_db` in the projects `devbox.json` for an example.\n\n### Running the Server\n\nYou can now start your Django server by running the following command.\n\n   ```bash\n   python todo_project/manage.py runserver\n   ```\n\nThis should start the development server.\n\n### Related Docs\n\n* [Using Python with Devbox](https://www.jetify.com/devbox/docs/devbox_examples/languages/python/)\n* [Using PostgreSQL with Devbox](https://www.jetify.com/devbox/docs/devbox_examples/stacks/django/)\n"
  },
  {
    "path": "examples/stacks/django/devbox.json",
    "content": "{\n  \"packages\": {\n    \"python\": \"latest\",\n    \"openssl\": {\n      \"version\": \"latest\",\n      \"outputs\": [\"out\", \"dev\"]\n    },\n    \"postgresql\": \"latest\",\n    \"zlib\":       \"latest\"\n  },\n  \"env\": {\n    \"PGPORT\": \"5434\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \". $VENV_DIR/bin/activate\",\n      \"pip install -r requirements.txt --use-pep517\"\n    ],\n    \"scripts\": {\n      \"create_db\": [\n        \"echo \\\"Creating DB\\\"\",\n        \"dropdb --if-exists todo_db\",\n        \"createdb todo_db\",\n        \"psql todo_db -c \\\"CREATE USER todo_user WITH PASSWORD 'secretpassword';\\\"\",\n        \"psql todo_db -c \\\"ALTER DATABASE todo_db OWNER TO todo_user;\\\"\",\n        \"psql todo_db -c \\\"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO todo_user;\\\"\",\n        \"python todo_project/manage.py makemigrations\",\n        \"python todo_project/manage.py migrate\"\n      ],\n      \"initdb\": [\n        \"initdb\"\n      ],\n      \"server\": [\n        \"python todo_project/manage.py runserver\"\n      ],\n      \"test\": [\n        \"initdb\",\n        \"devbox services start\",\n        \"devbox run create_db\",\n        \"python todo_project/manage.py test\",\n        \"devbox services stop\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/stacks/django/process-compose.yml",
    "content": "# Process compose for starting django\nversion: \"0.5\"\n\nprocesses:\n  django:\n   command: python todo_project/manage.py runserver\n   availability:\n    restart: \"always\""
  },
  {
    "path": "examples/stacks/django/requirements.txt",
    "content": "asgiref==3.6.0\nDjango==4.2.27\npsycopg2==2.9.5\nsqlparse==0.5.0"
  },
  {
    "path": "examples/stacks/django/todo_project/manage.py",
    "content": "#!/nix/store/c9ihc3ynkvyjr4piwbdaji8bn145r3yj-python3-3.10.9/bin/python\n\"\"\"Django's command-line utility for administrative tasks.\"\"\"\nimport os\nimport sys\n\n\ndef main():\n    \"\"\"Run administrative tasks.\"\"\"\n    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'todo_project.settings')\n    try:\n        from django.core.management import execute_from_command_line\n    except ImportError as exc:\n        raise ImportError(\n            \"Couldn't import Django. Are you sure it's installed and \"\n            \"available on your PYTHONPATH environment variable? Did you \"\n            \"forget to activate a virtual environment?\"\n        ) from exc\n    execute_from_command_line(sys.argv)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_app/__init__.py",
    "content": ""
  },
  {
    "path": "examples/stacks/django/todo_project/todo_app/admin.py",
    "content": "from django.contrib import admin\n\n# Register your models here.\n"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_app/apps.py",
    "content": "from django.apps import AppConfig\n\n\nclass TodoAppConfig(AppConfig):\n    default_auto_field = 'django.db.models.BigAutoField'\n    name = 'todo_app'\n"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_app/migrations/0001_initial.py",
    "content": "# Generated by Django 3.2.16 on 2023-02-02 01:33\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    initial = True\n\n    dependencies = [\n    ]\n\n    operations = [\n        migrations.CreateModel(\n            name='Todo',\n            fields=[\n                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n                ('title', models.CharField(max_length=255)),\n                ('completed', models.BooleanField(default=False)),\n                ('created_att', models.DateTimeField(auto_now_add=True)),\n            ],\n        ),\n    ]\n"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_app/migrations/__init__.py",
    "content": ""
  },
  {
    "path": "examples/stacks/django/todo_project/todo_app/models.py",
    "content": "from django.db import models\n\n# Create your models here.\nclass Todo(models.Model):\n    title = models.CharField(max_length=255)\n    completed = models.BooleanField(default=False)\n    created_att = models.DateTimeField(auto_now_add=True)\n\n    class Meta: \n        app_label = 'todo_app'"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_app/templates/todo_app/create_todo.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css\" integrity=\"sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm\" crossorigin=\"anonymous\">\n    <title>Create a Todo</title>\n</head>\n\n<body>\n    <div class=\"container\">\n        <h1 class=\"text-center my-5\">Create Todo</h1>\n        <form method=\"post\">\n            {% csrf_token %}\n            <div class=\"form-group\">\n                <label for=\"title\">Todo Title </label>\n                <input type=\"text\" name=\"title\" class=\"form-control\">\n            </div>\n            <input type=\"submit\" value=\"Create\" class=\"btn btn-primary\">\n        </form>\n    </div>\n</body>\n\n</html>"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_app/templates/todo_app/todo_list.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css\" integrity=\"sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm\" , crossorigin=\"anonymous\">\n    <title>Todos</title>\n</head>\n\n<body>\n    <div class=\"container\">\n        <h1 class=\"text-center my-5\">List</h1>\n        <a href=\"{% url 'create_todo' %}\" class=\"btn btn-primary my-3\">Create Todo</a>\n        <table class=\"table table-striped\">\n            <thead>\n                <tr>\n                    <th>Title</th>\n                </tr>\n            </thead>\n            <tbody>\n                {% for todo in todos %}\n                <tr>\n                    <td>\n                        {{todo.title}}\n                    </td>\n                </tr>\n                {% endfor %}\n            </tbody>\n        </table>\n    </div>\n\n</body>\n\n</html>"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_app/tests.py",
    "content": "from django.test import TestCase\n\n# Create your tests here.\n"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_app/views.py",
    "content": "from django.shortcuts import render, redirect\nfrom .models import Todo\n\ndef todo_list(request):\n    todos = Todo.objects.all()\n    context = {'todos': todos}\n    return render(request, 'todo_app/todo_list.html', context)\n\ndef create_todo(request):\n    if request.method == 'POST':\n        title = request.POST.get('title')\n        todo = Todo(title = title)\n        todo.save()\n        return redirect(todo_list)\n    return render(request, 'todo_app/create_todo.html')\n"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_project/__init__.py",
    "content": ""
  },
  {
    "path": "examples/stacks/django/todo_project/todo_project/asgi.py",
    "content": "\"\"\"\nASGI config for todo_project project.\n\nIt exposes the ASGI callable as a module-level variable named ``application``.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/3.2/howto/deployment/asgi/\n\"\"\"\n\nimport os\n\nfrom django.core.asgi import get_asgi_application\n\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'todo_project.settings')\n\napplication = get_asgi_application()\n"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_project/settings.py",
    "content": "\"\"\"\nDjango settings for todo_project project.\n\nGenerated by 'django-admin startproject' using Django 3.2.16.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/3.2/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/3.2/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = 'django-insecure-m*389n=@xec5!ta*^4f8l&@*1nl5#(cm%8rt+-qe2)#ay+o%96'\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = True\n\nALLOWED_HOSTS = []\n\n\n# Application definition\n\nINSTALLED_APPS = [\n    'todo_app',\n    'django.contrib.admin',\n    'django.contrib.auth',\n    'django.contrib.contenttypes',\n    'django.contrib.sessions',\n    'django.contrib.messages',\n    'django.contrib.staticfiles',\n]\n\nMIDDLEWARE = [\n    'django.middleware.security.SecurityMiddleware',\n    'django.contrib.sessions.middleware.SessionMiddleware',\n    'django.middleware.common.CommonMiddleware',\n    'django.middleware.csrf.CsrfViewMiddleware',\n    'django.contrib.auth.middleware.AuthenticationMiddleware',\n    'django.contrib.messages.middleware.MessageMiddleware',\n    'django.middleware.clickjacking.XFrameOptionsMiddleware',\n]\n\nROOT_URLCONF = 'todo_project.urls'\n\nTEMPLATES = [\n    {\n        'BACKEND': 'django.template.backends.django.DjangoTemplates',\n        'DIRS': [],\n        'APP_DIRS': True,\n        'OPTIONS': {\n            'context_processors': [\n                'django.template.context_processors.debug',\n                'django.template.context_processors.request',\n                'django.contrib.auth.context_processors.auth',\n                'django.contrib.messages.context_processors.messages',\n            ],\n        },\n    },\n]\n\nWSGI_APPLICATION = 'todo_project.wsgi.application'\n\n\n# Database\n# https://docs.djangoproject.com/en/3.2/ref/settings/#databases\n\nDATABASES = {\n    'default': {\n        'ENGINE': 'django.db.backends.postgresql',\n        'NAME': 'todo_db',\n        'USER': 'todo_user',\n        'PASSWORD': 'secretpassword',\n        'HOST': 'localhost',\n        'PORT': '5434',\n    }\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n    {\n        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',\n    },\n    {\n        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',\n    },\n    {\n        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',\n    },\n    {\n        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',\n    },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/3.2/topics/i18n/\n\nLANGUAGE_CODE = 'en-us'\n\nTIME_ZONE = 'UTC'\n\nUSE_I18N = True\n\nUSE_L10N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/3.2/howto/static-files/\n\nSTATIC_URL = '/static/'\n\n# Default primary key field type\n# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field\n\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_project/urls.py",
    "content": "\"\"\"todo_project URL Configuration\n\nThe `urlpatterns` list routes URLs to views. For more information please see:\n    https://docs.djangoproject.com/en/3.2/topics/http/urls/\nExamples:\nFunction views\n    1. Add an import:  from my_app import views\n    2. Add a URL to urlpatterns:  path('', views.home, name='home')\nClass-based views\n    1. Add an import:  from other_app.views import Home\n    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')\nIncluding another URLconf\n    1. Import the include() function: from django.urls import include, path\n    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))\n\"\"\"\nfrom django.contrib import admin\nfrom django.urls import path\nfrom todo_app.views import todo_list, create_todo\n\nurlpatterns = [\n    path('admin/', admin.site.urls),\n    path('', todo_list, name = 'todo_list'),\n    path('create/', create_todo, name='create_todo')\n]\n"
  },
  {
    "path": "examples/stacks/django/todo_project/todo_project/wsgi.py",
    "content": "\"\"\"\nWSGI config for todo_project project.\n\nIt exposes the WSGI callable as a module-level variable named ``application``.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/\n\"\"\"\n\nimport os\n\nfrom django.core.wsgi import get_wsgi_application\n\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'todo_project.settings')\n\napplication = get_wsgi_application()\n"
  },
  {
    "path": "examples/stacks/drupal/.editorconfig",
    "content": "# Drupal editor configuration normalization\n# @see http://editorconfig.org/\n\n# This is the top-most .editorconfig file; do not search in parent directories.\nroot = true\n\n# All files.\n[*]\nend_of_line = LF\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[composer.{json,lock}]\nindent_size = 4\n"
  },
  {
    "path": "examples/stacks/drupal/.gitattributes",
    "content": "# Drupal git normalization\n# @see https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html\n# @see https://www.drupal.org/node/1542048\n\n# Normally these settings would be done with macro attributes for improved\n# readability and easier maintenance. However macros can only be defined at the\n# repository root directory. Drupal avoids making any assumptions about where it\n# is installed.\n\n# Define text file attributes.\n# - Treat them as text.\n# - Ensure no CRLF line-endings, neither on checkout nor on checkin.\n# - Detect whitespace errors.\n#   - Exposed by default in `git diff --color` on the CLI.\n#   - Validate with `git diff --check`.\n#   - Deny applying with `git apply --whitespace=error-all`.\n#   - Fix automatically with `git apply --whitespace=fix`.\n\n*.config  text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.css     text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.dist    text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.engine  text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php\n*.html    text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=html\n*.inc     text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php\n*.install text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php\n*.js      text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.json    text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.lock    text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.map     text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.md      text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.module  text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php\n*.php     text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php\n*.po      text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.profile text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php\n*.script  text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.sh      text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php\n*.sql     text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.svg     text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.theme   text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php\n*.twig    text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.txt     text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.xml     text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n*.yml     text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2\n\n# PHPStan's baseline uses tabs instead of spaces.\ncore/.phpstan-baseline.php text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tabwidth=2 diff=php linguist-language=php\n\n# Define binary file attributes.\n# - Do not treat them as text.\n# - Include binary diff in patches instead of \"binary files differ.\"\n*.eot     -text diff\n*.exe     -text diff\n*.gif     -text diff\n*.gz      -text diff\n*.ico     -text diff\n*.jpeg    -text diff\n*.jpg     -text diff\n*.otf     -text diff\n*.phar    -text diff\n*.png     -text diff\n*.svgz    -text diff\n*.ttf     -text diff\n*.woff    -text diff\n*.woff2   -text diff\n"
  },
  {
    "path": "examples/stacks/drupal/.gitignore",
    "content": "# This file contains default .gitignore rules. To use it, copy it to .gitignore,\n# and it will cause files like your settings.php and user-uploaded files to be\n# excluded from Git version control. This is a common strategy to avoid\n# accidentally including private information in public repositories and patch\n# files.\n#\n# Because .gitignore can be specific to your site, this file has a different\n# name; updating Drupal core will not override your custom .gitignore file.\n\n# Ignore core when managing all of a project's dependencies with Composer\n# including Drupal core.\nweb/core/\n\n# Ignore dependencies that are managed with Composer.\n# Generally you should only ignore the root vendor directory. It's important\n# that core/assets/vendor and any other vendor directories within contrib or\n# custom module, theme, etc., are not ignored unless you purposely do so.\n/vendor/\n\n# Ignore configuration files that may contain sensitive information.\nweb/sites/*/settings*.php\nweb/sites/*/services*.yml\n\n# Ignore paths that contain user-generated content.\nweb/sites/*/files\nweb/sites/*/private\n\n# Ignore SimpleTest multi-site environment.\nweb/sites/simpletest\n\n# If you prefer to store your .gitignore file in the sites/ folder, comment\n# or delete the previous settings and uncomment the following ones, instead.\n\n# Ignore configuration files that may contain sensitive information.\n# */settings*.php\n\n# Ignore paths that contain user-generated content.\n*/files\n# */private\n\n# Ignore SimpleTest multi-site environment.\n# simpletest\n\n# Keep settings.php for devbox project\n!web/sites/default/settings.php\n"
  },
  {
    "path": "examples/stacks/drupal/README.md",
    "content": "# Drupal Stack\n\nThis example shows how to run a Drupal application in Devbox. It makes use of the PHP and Apache Plugins, while demonstrating how to configure a MariaDB instance to work with Devbox Cloud.\n\n\n## How to Run the example\n\nIn this directory, run:\n\n`devbox shell`\n\nTo start all your services (PHP, MySQL, and NGINX), run `devbox services up`. To stop the services, run `devbox services stop`\n\nTo create the `devbox_drupal` database and example table, you should run:\n\n`mysql -u root < setup_db.sql`\n\nTo install Drupal and your dependencies, run `composer install`. The Drupal app will be installed in the `/web` directory, and you can configure your site by visiting `localhost:8000/autoload` in your browser and following the interactive instructions\n\nTo exit the shell, use `exit`\n\n## Configuration\n\nBecause the Nix Store is immutable, we need to store our configuration, data, and logs in a local project directory. This is stored in the `devbox.d` directory, in a subfolder for each of the packages that we will be installing.\n"
  },
  {
    "path": "examples/stacks/drupal/composer.json",
    "content": "{\n    \"name\": \"drupal/recommended-project\",\n    \"description\": \"Project template for Drupal 9 projects with a relocated document root\",\n    \"type\": \"project\",\n    \"license\": \"GPL-2.0-or-later\",\n    \"homepage\": \"https://www.drupal.org/project/drupal\",\n    \"support\": {\n        \"docs\": \"https://www.drupal.org/docs/user_guide/en/index.html\",\n        \"chat\": \"https://www.drupal.org/node/314178\"\n    },\n    \"repositories\": [\n        {\n            \"type\": \"composer\",\n            \"url\": \"https://packages.drupal.org/8\"\n        }\n    ],\n    \"require\": {\n        \"composer/installers\": \"^1.9\",\n        \"drupal/core-composer-scaffold\": \"^10.3.14\",\n        \"drupal/core-project-message\": \"^10.3.14\",\n        \"drupal/core-recommended\": \"^10.3.14\",\n        \"drush/drush\": \"^12.4\"\n    },\n    \"conflict\": {\n        \"drupal/drupal\": \"*\"\n    },\n    \"minimum-stability\": \"stable\",\n    \"prefer-stable\": true,\n    \"config\": {\n        \"allow-plugins\": {\n            \"composer/installers\": true,\n            \"drupal/core-composer-scaffold\": true,\n            \"drupal/core-project-message\": true,\n            \"dealerdirect/phpcodesniffer-composer-installer\": true\n        },\n        \"sort-packages\": true\n    },\n    \"extra\": {\n        \"drupal-scaffold\": {\n            \"locations\": {\n                \"web-root\": \"web/\"\n            }\n        },\n        \"installer-paths\": {\n            \"web/core\": [\n                \"type:drupal-core\"\n            ],\n            \"web/libraries/{$name}\": [\n                \"type:drupal-library\"\n            ],\n            \"web/modules/contrib/{$name}\": [\n                \"type:drupal-module\"\n            ],\n            \"web/profiles/contrib/{$name}\": [\n                \"type:drupal-profile\"\n            ],\n            \"web/themes/contrib/{$name}\": [\n                \"type:drupal-theme\"\n            ],\n            \"drush/Commands/contrib/{$name}\": [\n                \"type:drupal-drush\"\n            ],\n            \"web/modules/custom/{$name}\": [\n                \"type:drupal-custom-module\"\n            ],\n            \"web/profiles/custom/{$name}\": [\n                \"type:drupal-custom-profile\"\n            ],\n            \"web/themes/custom/{$name}\": [\n                \"type:drupal-custom-theme\"\n            ]\n        },\n        \"drupal-core-project-message\": {\n            \"include-keys\": [\n                \"homepage\",\n                \"support\"\n            ],\n            \"post-create-project-cmd-message\": [\n                \"<bg=blue;fg=white>                                                         </>\",\n                \"<bg=blue;fg=white>  Congratulations, you’ve installed the Drupal codebase  </>\",\n                \"<bg=blue;fg=white>  from the drupal/recommended-project template!          </>\",\n                \"<bg=blue;fg=white>                                                         </>\",\n                \"\",\n                \"<bg=yellow;fg=black>Next steps</>:\",\n                \"  * Install the site: https://www.drupal.org/docs/8/install\",\n                \"  * Read the user guide: https://www.drupal.org/docs/user_guide/en/index.html\",\n                \"  * Get support: https://www.drupal.org/support\",\n                \"  * Get involved with the Drupal community:\",\n                \"      https://www.drupal.org/getting-involved\",\n                \"  * Remove the plugin that prints this message:\",\n                \"      composer remove drupal/core-project-message\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "examples/stacks/drupal/devbox.d/.gitignore",
    "content": "mysql/data\nnginx/temp\n*.log\n*.pid\n*.sock\n*.swp\n"
  },
  {
    "path": "examples/stacks/drupal/devbox.d/nginx/fastcgi.conf",
    "content": "fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;\nfastcgi_param  SERVER_SOFTWARE    nginx;\nfastcgi_param  QUERY_STRING       $query_string;\nfastcgi_param  REQUEST_METHOD     $request_method;\nfastcgi_param  CONTENT_TYPE       $content_type;\nfastcgi_param  CONTENT_LENGTH     $content_length;\nfastcgi_param  SCRIPT_FILENAME    $realpath_root$fastcgi_script_name;\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\nfastcgi_param  REQUEST_URI        $request_uri;\nfastcgi_param  DOCUMENT_URI       $document_uri;\nfastcgi_param  DOCUMENT_ROOT      $document_root;\nfastcgi_param  SERVER_PROTOCOL    $server_protocol;\nfastcgi_param  REMOTE_ADDR        $remote_addr;\nfastcgi_param  REMOTE_PORT        $remote_port;\nfastcgi_param  SERVER_ADDR        $server_addr;\nfastcgi_param  SERVER_PORT        $server_port;\nfastcgi_param  SERVER_NAME        $server_name;\n"
  },
  {
    "path": "examples/stacks/drupal/devbox.d/nginx/mime.conf",
    "content": "types {\n    text/html                             html htm shtml;\n    text/css                              css;\n    text/xml                              xml rss;\n    image/gif                             gif;\n    image/jpeg                            jpeg jpg;\n    application/x-javascript              js;\n    text/plain                            txt;\n    text/x-component                      htc;\n    text/mathml                           mml;\n    image/png                             png;\n    image/svg+xml                         svg svgz;\n    image/x-icon                          ico;\n    image/x-jng                           jng;\n    image/vnd.wap.wbmp                    wbmp;\n    application/java-archive              jar war ear;\n    application/mac-binhex40              hqx;\n    application/pdf                       pdf;\n    application/x-cocoa                   cco;\n    application/x-java-archive-diff       jardiff;\n    application/x-java-jnlp-file          jnlp;\n    application/x-makeself                run;\n    application/x-perl                    pl pm;\n    application/x-pilot                   prc pdb;\n    application/x-rar-compressed          rar;\n    application/x-redhat-package-manager  rpm;\n    application/x-sea                     sea;\n    application/x-shockwave-flash         swf;\n    application/x-stuffit                 sit;\n    application/x-tcl                     tcl tk;\n    application/x-x509-ca-cert            der pem crt;\n    application/x-xpinstall               xpi;\n    application/zip                       zip;\n    application/octet-stream              deb;\n    application/octet-stream              bin exe dll;\n    application/octet-stream              dmg;\n    application/octet-stream              eot;\n    application/octet-stream              iso img;\n    application/octet-stream              msi msp msm;\n    audio/mpeg                            mp3;\n    audio/ogg                             oga ogg;\n    audio/wav                             wav;\n    audio/x-realaudio                     ra;\n    video/mp4                             mp4;\n    video/mpeg                            mpeg mpg;\n    video/ogg                             ogv;\n    video/quicktime                       mov;\n    video/webm                            webm;\n    video/x-flv                           flv;\n    video/x-msvideo                       avi;\n    video/x-ms-wmv                        wmv;\n    video/x-ms-asf                        asx asf;\n    video/x-mng                           mng;\n}"
  },
  {
    "path": "examples/stacks/drupal/devbox.d/nginx/nginx.conf",
    "content": "events {}\nhttp{\nserver {\n         listen       8081;\n         listen       [::]:8081;\n         server_name  localhost;\n         root         ../../../devbox.d/web;\n\n         error_log error.log error;\n         access_log access.log;\n         client_body_temp_path temp/client_body;\n         proxy_temp_path temp/proxy;\n         fastcgi_temp_path temp/fastcgi;\n         uwsgi_temp_path temp/uwsgi;\n         scgi_temp_path temp/scgi;\n         include mime.conf;\n\n         index index.html;\n         server_tokens off;\n\n         index index.php index.htm index.html;\n\n        location / {\n            try_files $uri /index.php?$query_string; # For Drupal >= 7\n         }\n\n        location @rewrite {\n            rewrite ^ /index.php;\n        }\n\n        location ~ \\.php$ {\n            include fastcgi.conf;\n            fastcgi_split_path_info ^(.+\\.php)(/.+)$;\n            fastcgi_pass 127.0.0.1:8082;\n            fastcgi_param PATH_INFO $fastcgi_path_info;\n            fastcgi_index index.php;\n        }\n\n            # Don't allow direct access to PHP files in the vendor directory.\n        location ~ /vendor/.*\\.php$ {\n            deny all;\n            return 404;\n        }\n\n    }\n}\n"
  },
  {
    "path": "examples/stacks/drupal/devbox.d/nginx/nginx.template",
    "content": "events {}\nhttp{\nserver {\n         listen       $NGINX_WEB_PORT;\n         listen       [::]:$NGINX_WEB_PORT;\n         server_name  $NGINX_WEB_SERVER_NAME;\n         root         $NGINX_WEB_ROOT;\n\n         error_log error.log error;\n         access_log access.log;\n         client_body_temp_path temp/client_body;\n         proxy_temp_path temp/proxy;\n         fastcgi_temp_path temp/fastcgi;\n         uwsgi_temp_path temp/uwsgi;\n         scgi_temp_path temp/scgi;\n         include mime.conf;\n\n         index index.html;\n         server_tokens off;\n\n         index index.php index.htm index.html;\n\n        location / {\n            try_files $uri /index.php?$query_string; # For Drupal >= 7\n         }\n\n        location @rewrite {\n            rewrite ^ /index.php;\n        }\n\n        location ~ \\.php$ {\n            include fastcgi.conf;\n            fastcgi_split_path_info ^(.+\\.php)(/.+)$;\n            fastcgi_pass 127.0.0.1:$PHPFPM_PORT;\n            fastcgi_param PATH_INFO $fastcgi_path_info;\n            fastcgi_index index.php;\n        }\n\n            # Don't allow direct access to PHP files in the vendor directory.\n        location ~ /vendor/.*\\.php$ {\n            deny all;\n            return 404;\n        }\n\n    }\n}\n"
  },
  {
    "path": "examples/stacks/drupal/devbox.d/php/php-fpm.conf",
    "content": "[global]\npid = ${PHPFPM_PID_FILE}\nerror_log = ${PHPFPM_ERROR_LOG_FILE}\ndaemonize = yes\n\n[www]\n; user = www-data\n; group = www-data\nlisten = 127.0.0.1:${PHPFPM_PORT}\n; listen.owner = www-data\n; listen.group = www-data\npm = dynamic\npm.max_children = 5\npm.start_servers = 2\npm.min_spare_servers = 1\npm.max_spare_servers = 3\nchdir = /\n"
  },
  {
    "path": "examples/stacks/drupal/devbox.d/php/php.ini",
    "content": "[php]\n\n; Put your php.ini directives here. For the latest default php.ini file, see https://github.com/php/php-src/blob/master/php.ini-production\n\n; memory_limit = 128M\n; expose_php = Off\n"
  },
  {
    "path": "examples/stacks/drupal/devbox.json",
    "content": "{\n  \"packages\": {\n    \"git\":                    \"latest\",\n    \"php\":                    \"8.1\",\n    \"php81Packages.composer\": \"latest\",\n    \"mariadb\":                \"latest\",\n    \"nginx\":                  \"latest\",\n    \"curl\": {\n      \"version\": \"latest\",\n      \"outputs\": [\"bin\"]\n    }\n  },\n  \"env\": {\n    \"MYSQL_UNIX_PORT\": \"/tmp/devbox/mariadb/run/mysql.sock\"\n  },\n  \"shell\": {\n    \"init_hook\": [],\n    \"scripts\": {\n      \"run_test\": [\n        \"mkdir -p -m 0755 \\\"$(dirname \\\"$MYSQL_UNIX_PORT\\\")\\\"\",\n        \"ls -la .devbox .devbox/virtenv .devbox/virtenv/mariadb .devbox/virtenv/mariadb/data || true\",\n        \"devbox services up -b\",\n        \"echo 'Waiting for services to start' && sleep 5\",\n        \"./install-drupal.sh\",\n        \"curl localhost:8081\",\n        \"devbox services stop\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/stacks/drupal/install-drupal.sh",
    "content": "#!/bin/sh\n\nset -e\n\necho \"Make sure to run this script only after 'devbox shell'\"\n\nmysql -u root < setup_db.sql\ncomposer install\n\necho \"Your Drupal demo website is ready,\"\necho \"Open localhost:8081 in your browser.\"\n"
  },
  {
    "path": "examples/stacks/drupal/setup_db.sql",
    "content": "--- You should run this query using `mysql -u root < setup_db.sql`\n\nDROP DATABASE IF EXISTS devbox_drupal;\nCREATE DATABASE devbox_drupal;\n\nUSE devbox_drupal\n\nCREATE USER IF NOT EXISTS 'devbox_user'@'localhost' IDENTIFIED BY 'password';\nGRANT ALL PRIVILEGES ON devbox_drupal.* TO 'devbox_user'@'localhost' IDENTIFIED BY 'password';\n\n-- Connect in drupal using:\n-- Database: devbox_drupal\n-- User: devbox_user\n-- Password: password\n-- Host: 127.0.0.1\n-- Port: 3306\n"
  },
  {
    "path": "examples/stacks/drupal/web/.csslintrc",
    "content": "--errors=box-model,\n         display-property-grouping,\n         duplicate-background-images,\n         duplicate-properties,\n         empty-rules,\n         ids,\n         import,\n         important,\n         known-properties,\n         outline-none,\n         overqualified-elements,\n         qualified-headings,\n         shorthand,\n         star-property-hack,\n         text-indent,\n         underscore-property-hack,\n         unique-headings,\n         unqualified-attributes,\n         vendor-prefix,\n         zero-units\n--ignore=adjoining-classes,\n         box-sizing,\n         bulletproof-font-face,\n         compatible-vendor-prefixes,\n         errors,\n         fallback-colors,\n         floats,\n         font-faces,\n         font-sizes,\n         gradients,\n         import-ie-limit,\n         order-alphabetical,\n         regex-selectors,\n         rules-count,\n         selector-max,\n         selector-max-approaching,\n         selector-newline,\n         universal-selector\n--exclude-list=core/assets,\n               vendor\n"
  },
  {
    "path": "examples/stacks/drupal/web/.eslintignore",
    "content": "core/**/*\nvendor/**/*\nsites/**/files/**/*\nlibraries/**/*\nsites/**/libraries/**/*\nprofiles/**/libraries/**/*\n**/js_test_files/**/*\n**/node_modules/**/*\n"
  },
  {
    "path": "examples/stacks/drupal/web/.eslintrc.json",
    "content": "{\n  \"extends\": \"./core/.eslintrc.json\"\n}\n"
  },
  {
    "path": "examples/stacks/drupal/web/.gitignore",
    "content": "/autoload.php"
  },
  {
    "path": "examples/stacks/drupal/web/.ht.router.php",
    "content": "<?php\n\n/**\n * @file\n * Router script for the built-in PHP web server.\n *\n * The built-in web server should only be used for development and testing as it\n * has a number of limitations that makes running Drupal on it highly insecure\n * and somewhat limited.\n *\n * Note that:\n * - The server is single-threaded, any requests made during the execution of\n *   the main request will hang until the main request has been completed.\n * - The web server does not enforce any of the settings in .htaccess in\n *   particular a remote user will be able to download files that normally would\n *   be protected from direct access such as .module files.\n *\n * The router script is needed to work around a bug in PHP, see\n * https://bugs.php.net/bug.php?id=61286.\n *\n * Usage:\n * php -S localhost:8888 .ht.router.php\n *\n * @see http://php.net/manual/en/features.commandline.webserver.php\n */\n\nif (PHP_SAPI !== 'cli-server') {\n  // Bail out if this is not PHP's Development Server.\n  header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');\n  exit;\n}\n\n$url = parse_url($_SERVER['REQUEST_URI']);\nif (file_exists(__DIR__ . $url['path'])) {\n  // Serve the requested resource as-is.\n  return FALSE;\n}\n\n// Work around the PHP bug.\n$path = $url['path'];\n$script = 'index.php';\nif (str_contains($path, '.php')) {\n  // Work backwards through the path to check if a script exists. Otherwise\n  // fallback to index.php.\n  do {\n    $path = dirname($path);\n    if (preg_match('/\\.php$/', $path) && is_file(__DIR__ . $path)) {\n      // Discovered that the path contains an existing PHP file. Use that as the\n      // script to include.\n      $script = ltrim($path, '/');\n      break;\n    }\n  } while ($path !== '/' && $path !== '.');\n}\n\n// Update $_SERVER variables to point to the correct index-file.\n$index_file_absolute = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . $script;\n$index_file_relative = DIRECTORY_SEPARATOR . $script;\n\n// SCRIPT_FILENAME will point to the router script itself, it should point to\n// the full path of index.php.\n$_SERVER['SCRIPT_FILENAME'] = $index_file_absolute;\n\n// SCRIPT_NAME and PHP_SELF will either point to index.php or contain the full\n// virtual path being requested depending on the URL being requested. They\n// should always point to index.php relative to document root.\n$_SERVER['SCRIPT_NAME'] = $index_file_relative;\n$_SERVER['PHP_SELF'] = $index_file_relative;\n\n// Require the script and let core take over.\nrequire $_SERVER['SCRIPT_FILENAME'];\n"
  },
  {
    "path": "examples/stacks/drupal/web/.htaccess",
    "content": "#\n# Apache/PHP/Drupal settings:\n#\n\n# Protect files and directories from prying eyes.\n<FilesMatch \"\\.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\\.php)?|xtmpl|yml)(~|\\.sw[op]|\\.bak|\\.orig|\\.save)?$|^(\\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\\.(json|lock)|web\\.config|yarn\\.lock|package\\.json)$|^#.*#$|\\.php(~|\\.sw[op]|\\.bak|\\.orig|\\.save)$\">\n  <IfModule mod_authz_core.c>\n    Require all denied\n  </IfModule>\n  <IfModule !mod_authz_core.c>\n    Order allow,deny\n  </IfModule>\n</FilesMatch>\n\n# Don't show directory listings for URLs which map to a directory.\nOptions -Indexes\n\n# Set the default handler.\nDirectoryIndex index.php index.html index.htm\n\n# Add correct encoding for SVGZ.\nAddType image/svg+xml svg svgz\nAddEncoding gzip svgz\n\n# Most of the following PHP settings cannot be changed at runtime. See\n# sites/default/default.settings.php and\n# Drupal\\Core\\DrupalKernel::bootEnvironment() for settings that can be\n# changed at runtime.\n<IfModule mod_php.c>\n  php_value assert.active                   0\n</IfModule>\n\n# Requires mod_expires to be enabled.\n<IfModule mod_expires.c>\n  # Enable expirations.\n  ExpiresActive On\n\n  # Cache all files for 1 year after access.\n  ExpiresDefault \"access plus 1 year\"\n\n  <FilesMatch \\.php$>\n    # Do not allow PHP scripts to be cached unless they explicitly send cache\n    # headers themselves. Otherwise all scripts would have to overwrite the\n    # headers set by mod_expires if they want another caching behavior. This may\n    # fail if an error occurs early in the bootstrap process, and it may cause\n    # problems if a non-Drupal PHP file is installed in a subdirectory.\n    ExpiresActive Off\n  </FilesMatch>\n</IfModule>\n\n# Set a fallback resource if mod_rewrite is not enabled. This allows Drupal to\n# work without clean URLs. This requires Apache version >= 2.2.16. If Drupal is\n# not accessed by the top level URL (i.e.: http://example.com/drupal/ instead of\n# http://example.com/), the path to index.php will need to be adjusted.\n<IfModule !mod_rewrite.c>\n  FallbackResource /index.php\n</IfModule>\n\n# Various rewrite rules.\n<IfModule mod_rewrite.c>\n  RewriteEngine on\n\n  # Set \"protossl\" to \"s\" if we were accessed via https://.  This is used later\n  # if you enable \"www.\" stripping or enforcement, in order to ensure that\n  # you don't bounce between http and https.\n  RewriteRule ^ - [E=protossl]\n  RewriteCond %{HTTPS} on\n  RewriteRule ^ - [E=protossl:s]\n\n  # Make sure Authorization HTTP header is available to PHP\n  # even when running as CGI or FastCGI.\n  RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n\n  # Block access to \"hidden\" directories whose names begin with a period. This\n  # includes directories used by version control systems such as Subversion or\n  # Git to store control files. Files whose names begin with a period, as well\n  # as the control files used by CVS, are protected by the FilesMatch directive\n  # above.\n  #\n  # NOTE: This only works when mod_rewrite is loaded. Without mod_rewrite, it is\n  # not possible to block access to entire directories from .htaccess because\n  # <DirectoryMatch> is not allowed here.\n  #\n  # If you do not have mod_rewrite installed, you should remove these\n  # directories from your webroot or otherwise protect them from being\n  # downloaded.\n  RewriteRule \"/\\.|^\\.(?!well-known/)\" - [F]\n\n  # If your site can be accessed both with and without the 'www.' prefix, you\n  # can use one of the following settings to redirect users to your preferred\n  # URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option:\n  #\n  # To redirect all users to access the site WITH the 'www.' prefix,\n  # (http://example.com/foo will be redirected to http://www.example.com/foo)\n  # uncomment the following:\n  # RewriteCond %{HTTP_HOST} .\n  # RewriteCond %{HTTP_HOST} !^www\\. [NC]\n  # RewriteRule ^ http%{ENV:protossl}://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301]\n  #\n  # To redirect all users to access the site WITHOUT the 'www.' prefix,\n  # (http://www.example.com/foo will be redirected to http://example.com/foo)\n  # uncomment the following:\n  # RewriteCond %{HTTP_HOST} ^www\\.(.+)$ [NC]\n  # RewriteRule ^ http%{ENV:protossl}://%1%{REQUEST_URI} [L,R=301]\n\n  # Modify the RewriteBase if you are using Drupal in a subdirectory or in a\n  # VirtualDocumentRoot and the rewrite rules are not working properly.\n  # For example if your site is at http://example.com/drupal uncomment and\n  # modify the following line:\n  # RewriteBase /drupal\n  #\n  # If your site is running in a VirtualDocumentRoot at http://example.com/,\n  # uncomment the following line:\n  # RewriteBase /\n\n  # Redirect common PHP files to their new locations.\n  RewriteCond %{REQUEST_URI} ^(.*)?/(install\\.php) [OR]\n  RewriteCond %{REQUEST_URI} ^(.*)?/(rebuild\\.php)\n  RewriteCond %{REQUEST_URI} !core\n  RewriteRule ^ %1/core/%2 [L,QSA,R=301]\n\n  # Rewrite install.php during installation to see if mod_rewrite is working\n  RewriteRule ^core/install\\.php core/install.php?rewrite=ok [QSA,L]\n\n  # Pass all requests not referring directly to files in the filesystem to\n  # index.php.\n  RewriteCond %{REQUEST_FILENAME} !-f\n  RewriteCond %{REQUEST_FILENAME} !-d\n  RewriteCond %{REQUEST_URI} !=/favicon.ico\n  RewriteRule ^ index.php [L]\n\n  # For security reasons, deny access to other PHP files on public sites.\n  # Note: The following URI conditions are not anchored at the start (^),\n  # because Drupal may be located in a subdirectory. To further improve\n  # security, you can replace '!/' with '!^/'.\n  # Allow access to PHP files in /core (like authorize.php or install.php):\n  RewriteCond %{REQUEST_URI} !/core/[^/]*\\.php$\n  # Allow access to test-specific PHP files:\n  RewriteCond %{REQUEST_URI} !/core/modules/system/tests/https?\\.php\n  # Allow access to Statistics module's custom front controller.\n  # Copy and adapt this rule to directly execute PHP files in contributed or\n  # custom modules or to run another PHP application in the same directory.\n  RewriteCond %{REQUEST_URI} !/core/modules/statistics/statistics\\.php$\n  # Deny access to any other PHP files that do not match the rules above.\n  # Specifically, disallow autoload.php from being served directly.\n  RewriteRule \"^(.+/.*|autoload)\\.php($|/)\" - [F]\n\n  # Rules to correctly serve gzip compressed CSS and JS files.\n  # Requires both mod_rewrite and mod_headers to be enabled.\n  <IfModule mod_headers.c>\n    # Serve gzip compressed CSS files if they exist and the client accepts gzip.\n    RewriteCond %{HTTP:Accept-encoding} gzip\n    RewriteCond %{REQUEST_FILENAME}\\.gz -s\n    RewriteRule ^(.*css_[a-zA-Z0-9-_]+)\\.css$ $1\\.css\\.gz [QSA]\n\n    # Serve gzip compressed JS files if they exist and the client accepts gzip.\n    RewriteCond %{HTTP:Accept-encoding} gzip\n    RewriteCond %{REQUEST_FILENAME}\\.gz -s\n    RewriteRule ^(.*js_[a-zA-Z0-9-_]+)\\.js$ $1\\.js\\.gz [QSA]\n\n    # Serve correct content types, and prevent double compression.\n    RewriteRule \\.css\\.gz$ - [T=text/css,E=no-gzip:1,E=no-brotli:1]\n    RewriteRule \\.js\\.gz$ - [T=text/javascript,E=no-gzip:1,E=no-brotli:1]\n\n    <FilesMatch \"(\\.js\\.gz|\\.css\\.gz)$\">\n      # Serve correct encoding type.\n      Header set Content-Encoding gzip\n      # Force proxies to cache gzipped & non-gzipped css/js files separately.\n      Header append Vary Accept-Encoding\n    </FilesMatch>\n  </IfModule>\n</IfModule>\n\n# Various header fixes.\n<IfModule mod_headers.c>\n  # Disable content sniffing for all responses, since it's an attack vector.\n  # This header is also set in FinishResponseSubscriber, which depending on\n  # Apache configuration might get placed in the 'onsuccess' table. To prevent\n  # header duplication, unset that one prior to setting in the 'always' table.\n  # See \"To circumvent this limitation...\" in\n  # https://httpd.apache.org/docs/current/mod/mod_headers.html.\n  Header onsuccess unset X-Content-Type-Options\n  Header always set X-Content-Type-Options nosniff\n  # Disable Proxy header, since it's an attack vector.\n  RequestHeader unset Proxy\n</IfModule>\n"
  },
  {
    "path": "examples/stacks/drupal/web/INSTALL.txt",
    "content": "\nRead core/INSTALL.txt for detailed installation instructions for your Drupal\nwebsite.\n"
  },
  {
    "path": "examples/stacks/drupal/web/README.md",
    "content": "<img alt=\"Drupal Logo\" src=\"https://www.drupal.org/files/Wordmark_blue_RGB.png\" height=\"60px\">\n\nDrupal is an open source content management platform supporting a variety of\nwebsites ranging from personal weblogs to large community-driven websites. For\nmore information, visit the Drupal website, [Drupal.org][Drupal.org], and join\nthe [Drupal community][Drupal community].\n\n## Contributing\n\nDrupal is developed on [Drupal.org][Drupal.org], the home of the international\nDrupal community since 2001!\n\n[Drupal.org][Drupal.org] hosts Drupal's [GitLab repository][GitLab repository],\nits [issue queue][issue queue], and its [documentation][documentation]. Before\nyou start working on code, be sure to search the [issue queue][issue queue] and\ncreate an issue if your aren't able to find an existing issue.\n\nEvery issue on Drupal.org automatically creates a new community-accessible fork\nthat you can contribute to. Learn more about the code contribution process on\nthe [Issue forks & merge requests page][issue forks].\n\n## Usage\n\nFor a brief introduction, see [USAGE.txt](/core/USAGE.txt). You can also find\nguides, API references, and more by visiting Drupal's [documentation\npage][documentation].\n\nYou can quickly extend Drupal's core feature set by installing any of its\n[thousands of free and open source modules][modules]. With Drupal and its\nmodule ecosystem, you can often build most or all of what your project needs\nbefore writing a single line of code.\n\n## Changelog\n\nDrupal keeps detailed [change records][changelog]. You can search Drupal's\nchanges for a record of every notable breaking change and new feature since\n2011.\n\n## Security\n\nFor a list of security announcements, see the [Security advisories\npage][Security advisories] (available as [an RSS feed][security RSS]). This\npage also describes how to subscribe to these announcements via email.\n\nFor information about the Drupal security process, or to find out how to report\na potential security issue to the Drupal security team, see the [Security team\npage][security team].\n\n## Need a helping hand?\n\nVisit the [Support page][support] or browse [over a thousand Drupal\nproviders][service providers] offering design, strategy, development, and\nhosting services.\n\n## Legal matters\n\nKnow your rights when using Drupal by reading Drupal core's\n[license](/core/LICENSE.txt).\n\nLearn about the [Drupal trademark and logo policy here][trademark].\n\n[Drupal.org]: https://www.drupal.org\n[Drupal community]: https://www.drupal.org/community\n[GitLab repository]: https://git.drupalcode.org/project/drupal\n[issue queue]: https://www.drupal.org/project/issues/drupal\n[issue forks]: https://www.drupal.org/drupalorg/docs/gitlab-integration/issue-forks-merge-requests\n[documentation]: https://www.drupal.org/documentation\n[changelog]: https://www.drupal.org/list-changes/drupal\n[modules]: https://www.drupal.org/project/project_module\n[security advisories]: https://www.drupal.org/security\n[security RSS]: https://www.drupal.org/security/rss.xml\n[security team]: https://www.drupal.org/drupal-security-team\n[service providers]: https://www.drupal.org/drupal-services\n[support]: https://www.drupal.org/support\n[trademark]: https://www.drupal.com/trademark\n"
  },
  {
    "path": "examples/stacks/drupal/web/example.gitignore",
    "content": "# This file contains default .gitignore rules. To use it, copy it to .gitignore,\n# and it will cause files like your settings.php and user-uploaded files to be\n# excluded from Git version control. This is a common strategy to avoid\n# accidentally including private information in public repositories and patch\n# files.\n#\n# Because .gitignore can be specific to your site, this file has a different\n# name; updating Drupal core will not override your custom .gitignore file.\n\n# Ignore core when managing all of a project's dependencies with Composer\n# including Drupal core.\n# core\n\n# Ignore dependencies that are managed with Composer.\n# Generally you should only ignore the root vendor directory. It's important\n# that core/assets/vendor and any other vendor directories within contrib or\n# custom module, theme, etc., are not ignored unless you purposely do so.\n/vendor/\n\n# Ignore configuration files that may contain sensitive information.\nsites/*/settings*.php\nsites/*/services*.yml\n\n# Ignore paths that contain user-generated content.\nsites/*/files\nsites/*/private\n\n# Ignore multi-site test environment.\nsites/simpletest\n\n# If you prefer to store your .gitignore file in the sites/ folder, comment\n# or delete the previous settings and uncomment the following ones, instead.\n\n# Ignore configuration files that may contain sensitive information.\n# */settings*.php\n\n# Ignore paths that contain user-generated content.\n# */files\n# */private\n\n# Ignore multi-site test environment.\n# simpletest\n"
  },
  {
    "path": "examples/stacks/drupal/web/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hello World!</title>\n  </head>\n  <body>\n    Hello World!\n  </body>\n</html>\n"
  },
  {
    "path": "examples/stacks/drupal/web/index.php",
    "content": "<?php\n\n/**\n * @file\n * The PHP page that serves all page requests on a Drupal installation.\n *\n * All Drupal code is released under the GNU General Public License.\n * See COPYRIGHT.txt and LICENSE.txt files in the \"core\" directory.\n */\n\nuse Drupal\\Core\\DrupalKernel;\nuse Symfony\\Component\\HttpFoundation\\Request;\n\n$autoloader = require_once 'autoload.php';\n\n$kernel = new DrupalKernel('prod', $autoloader);\n\n$request = Request::createFromGlobals();\n$response = $kernel->handle($request);\n$response->send();\n\n$kernel->terminate($request, $response);\n"
  },
  {
    "path": "examples/stacks/drupal/web/modules/README.txt",
    "content": "Modules extend your site functionality beyond Drupal core.\n\nWHAT TO PLACE IN THIS DIRECTORY?\n--------------------------------\n\nPlacing downloaded and custom modules in this directory separates downloaded and\ncustom modules from Drupal core's modules. This allows Drupal core to be updated\nwithout overwriting these files.\n\nDOWNLOAD ADDITIONAL MODULES\n---------------------------\n\nContributed modules from the Drupal community may be downloaded at\nhttps://www.drupal.org/project/project_module.\n\nORGANIZING MODULES IN THIS DIRECTORY\n------------------------------------\n\nYou may create subdirectories in this directory, to organize your added modules,\nwithout breaking the site. Some common subdirectories include \"contrib\" for\ncontributed modules, and \"custom\" for custom modules. Note that if you move a\nmodule to a subdirectory after it has been enabled, you may need to clear the\nDrupal cache so it can be found.\n\nThere are number of directories that are ignored when looking for modules. These\nare 'src', 'lib', 'vendor', 'assets', 'css', 'files', 'images', 'js', 'misc',\n'templates', 'includes', 'fixtures' and 'Drupal'.\n\nMULTISITE CONFIGURATION\n-----------------------\n\nIn multisite configurations, modules found in this directory are available to\nall sites. You may also put modules in the sites/all/modules directory, and the\nversions in sites/all/modules will take precedence over versions of the same\nmodule that are here. Alternatively, the sites/your_site_name/modules directory\npattern may be used to restrict modules to a specific site instance.\n\nMORE INFORMATION\n----------------\n\nRefer to the “Developing for Drupal” section of the README.md in the Drupal\nroot directory for further information on extending Drupal with custom modules.\n"
  },
  {
    "path": "examples/stacks/drupal/web/profiles/README.txt",
    "content": "Installation profiles define additional steps that run after the base\ninstallation of Drupal is completed. They may also offer additional\nfunctionality and change the behavior of the site.\n\nWHAT TO PLACE IN THIS DIRECTORY?\n--------------------------------\n\nPlace downloaded and custom installation profiles in this directory.\nNote that installation profiles are generally provided as part of a Drupal\ndistribution.\n\nDOWNLOAD ADDITIONAL DISTRIBUTIONS\n---------------------------------\n\nContributed distributions from the Drupal community may be downloaded at\nhttps://www.drupal.org/project/project_distribution.\n\nMULTISITE CONFIGURATION\n-----------------------\n\nIn multisite configurations, installation profiles found in this directory are\navailable to all sites during their initial site installation.\n\nMORE INFORMATION\n----------------\n\nRefer to the \"Installation profiles\" section of the README.md in the Drupal\nroot directory for further information on extending Drupal with custom profiles.\n"
  },
  {
    "path": "examples/stacks/drupal/web/robots.txt",
    "content": "#\n# robots.txt\n#\n# This file is to prevent the crawling and indexing of certain parts\n# of your site by web crawlers and spiders run by sites like Yahoo!\n# and Google. By telling these \"robots\" where not to go on your site,\n# you save bandwidth and server resources.\n#\n# This file will be ignored unless it is at the root of your host:\n# Used:    http://example.com/robots.txt\n# Ignored: http://example.com/site/robots.txt\n#\n# For more information about the robots.txt standard, see:\n# http://www.robotstxt.org/robotstxt.html\n\nUser-agent: *\n# CSS, JS, Images\nAllow: /core/*.css$\nAllow: /core/*.css?\nAllow: /core/*.js$\nAllow: /core/*.js?\nAllow: /core/*.gif\nAllow: /core/*.jpg\nAllow: /core/*.jpeg\nAllow: /core/*.png\nAllow: /core/*.svg\nAllow: /profiles/*.css$\nAllow: /profiles/*.css?\nAllow: /profiles/*.js$\nAllow: /profiles/*.js?\nAllow: /profiles/*.gif\nAllow: /profiles/*.jpg\nAllow: /profiles/*.jpeg\nAllow: /profiles/*.png\nAllow: /profiles/*.svg\n# Directories\nDisallow: /core/\nDisallow: /profiles/\n# Files\nDisallow: /README.md\nDisallow: /composer/Metapackage/README.txt\nDisallow: /composer/Plugin/ProjectMessage/README.md\nDisallow: /composer/Plugin/Scaffold/README.md\nDisallow: /composer/Plugin/VendorHardening/README.txt\nDisallow: /composer/Template/README.txt\nDisallow: /modules/README.txt\nDisallow: /sites/README.txt\nDisallow: /themes/README.txt\nDisallow: /web.config\n# Paths (clean URLs)\nDisallow: /admin/\nDisallow: /comment/reply/\nDisallow: /filter/tips\nDisallow: /node/add/\nDisallow: /search/\nDisallow: /user/register\nDisallow: /user/password\nDisallow: /user/login\nDisallow: /user/logout\nDisallow: /media/oembed\nDisallow: /*/media/oembed\n# Paths (no clean URLs)\nDisallow: /index.php/admin/\nDisallow: /index.php/comment/reply/\nDisallow: /index.php/filter/tips\nDisallow: /index.php/node/add/\nDisallow: /index.php/search/\nDisallow: /index.php/user/password\nDisallow: /index.php/user/register\nDisallow: /index.php/user/login\nDisallow: /index.php/user/logout\nDisallow: /index.php/media/oembed\nDisallow: /index.php/*/media/oembed\n"
  },
  {
    "path": "examples/stacks/drupal/web/sites/README.txt",
    "content": "This directory structure contains the settings and configuration files specific\nto your site or sites and is an integral part of multisite configurations.\n\nIt is now recommended to place your custom and downloaded extensions in the\n/modules, /themes, and /profiles directories located in the Drupal root. The\nsites/all/ subdirectory structure, which was recommended in previous versions\nof Drupal, is still supported.\n\nSee core/INSTALL.txt for information about single-site installation or\nmultisite configuration.\n"
  },
  {
    "path": "examples/stacks/drupal/web/sites/default/default.services.yml",
    "content": "parameters:\n  # Toggles the super user access policy. If your website has at least one user\n  # with the Administrator role, it is advised to set this to false. This allows\n  # you to make user 1 a regular user, strengthening the security of your site.\n  security.enable_super_user: true\n  session.storage.options:\n    # Default ini options for sessions.\n    #\n    # Some distributions of Linux (most notably Debian) ship their PHP\n    # installations with garbage collection (gc) disabled. Since Drupal depends\n    # on PHP's garbage collection for clearing sessions, ensure that garbage\n    # collection occurs by using the most common settings.\n    # @default 1\n    gc_probability: 1\n    # @default 100\n    gc_divisor: 100\n    #\n    # Set session lifetime (in seconds), i.e. the grace period for session\n    # data. Sessions are deleted by the session garbage collector after one\n    # session lifetime has elapsed since the user's last visit. When a session\n    # is deleted, authenticated users are logged out, and the contents of the\n    # user's session is discarded.\n    # @default 200000\n    gc_maxlifetime: 200000\n    #\n    # Set session cookie lifetime (in seconds), i.e. the time from the session\n    # is created to the cookie expires, i.e. when the browser is expected to\n    # discard the cookie. The value 0 means \"until the browser is closed\".\n    # @default 2000000\n    cookie_lifetime: 2000000\n    #\n    # Drupal automatically generates a unique session cookie name based on the\n    # full domain name used to access the site. This mechanism is sufficient\n    # for most use-cases, including multi-site deployments. However, if it is\n    # desired that a session can be reused across different subdomains, the\n    # cookie domain needs to be set to the shared base domain. Doing so assures\n    # that users remain logged in as they cross between various subdomains.\n    # To maximize compatibility and normalize the behavior across user agents,\n    # the cookie domain should start with a dot.\n    #\n    # @default none\n    # cookie_domain: '.example.com'\n    #\n    # Set the SameSite cookie attribute: 'None', 'Lax', or 'Strict'. If set,\n    # this value will override the server value. See\n    # https://www.php.net/manual/en/session.security.ini.php for more\n    # information.\n    # @default no value\n    cookie_samesite: Lax\n    #\n    # Set the session ID string length. The length can be between 22 to 256. The\n    # PHP recommended value is 48. See\n    # https://www.php.net/manual/session.security.ini.php for more information.\n    # This value should be kept in sync with\n    # \\Drupal\\Core\\Session\\SessionConfiguration::__construct()\n    # @default 48\n    sid_length: 48\n    #\n    # Set the number of bits in encoded session ID character. The possible\n    # values are '4' (0-9, a-f), '5' (0-9, a-v), and '6' (0-9, a-z, A-Z, \"-\",\n    # \",\"). The PHP recommended value is 6. See\n    # https://www.php.net/manual/session.security.ini.php for more information.\n    # This value should be kept in sync with\n    # \\Drupal\\Core\\Session\\SessionConfiguration::__construct()\n    # @default 6\n    sid_bits_per_character: 6\n    # By default, Drupal generates a session cookie name based on the full\n    # domain name. Set the name_suffix to a short random string to ensure this\n    # session cookie name is unique on different installations on the same\n    # domain and path (for example, when migrating from Drupal 7).\n    name_suffix: ''\n  twig.config:\n    # Twig debugging:\n    #\n    # When debugging is enabled:\n    # - The markup of each Twig template is surrounded by HTML comments that\n    #   contain theming information, such as template file name suggestions.\n    # - Note that this debugging markup will cause automated tests that directly\n    #   check rendered HTML to fail. When running automated tests, 'debug'\n    #   should be set to FALSE.\n    # - The dump() function can be used in Twig templates to output information\n    #   about template variables.\n    # - Twig templates are automatically recompiled whenever the source code\n    #   changes (see auto_reload below).\n    #\n    # For more information about debugging Twig templates, see\n    # https://www.drupal.org/node/1906392.\n    #\n    # Enabling Twig debugging is not recommended in production environments.\n    # @default false\n    debug: false\n    # Twig auto-reload:\n    #\n    # Automatically recompile Twig templates whenever the source code changes.\n    # If you don't provide a value for auto_reload, it will be determined\n    # based on the value of debug.\n    #\n    # Enabling auto-reload is not recommended in production environments.\n    # @default null\n    auto_reload: null\n    # Twig cache:\n    #\n    # By default, Twig templates will be compiled and stored in the filesystem\n    # to increase performance. Disabling the Twig cache will recompile the\n    # templates from source each time they are used. In most cases the\n    # auto_reload setting above should be enabled rather than disabling the\n    # Twig cache.\n    #\n    # Disabling the Twig cache is not recommended in production environments.\n    # @default true\n    cache: true\n    # File extensions:\n    #\n    # List of file extensions the Twig system is allowed to load via the\n    # twig.loader.filesystem service. Files with other extensions will not be\n    # loaded unless they are added here. For example, to allow a file named\n    # 'example.partial' to be loaded, add 'partial' to this list. To load files\n    # with no extension, add an empty string '' to the list.\n    #\n    # @default ['css', 'html', 'js', 'svg', 'twig']\n    allowed_file_extensions:\n      - css\n      - html\n      - js\n      - svg\n      - twig\n  renderer.config:\n    # Renderer required cache contexts:\n    #\n    # The Renderer will automatically associate these cache contexts with every\n    # render array, hence varying every render array by these cache contexts.\n    #\n    # @default ['languages:language_interface', 'theme', 'user.permissions']\n    required_cache_contexts: ['languages:language_interface', 'theme', 'user.permissions']\n    # Renderer automatic placeholdering conditions:\n    #\n    # Drupal allows portions of the page to be automatically deferred when\n    # rendering to improve cache performance. That is especially helpful for\n    # cache contexts that vary widely, such as the active user. On some sites\n    # those may be different, however, such as sites with only a handful of\n    # users. If you know what the high-cardinality cache contexts are for your\n    # site, specify those here. If you're not sure, the defaults are fairly safe\n    # in general.\n    #\n    # For more information about rendering optimizations see\n    # https://www.drupal.org/developing/api/8/render/arrays/cacheability#optimizing\n    auto_placeholder_conditions:\n      # Max-age at or below which caching is not considered worthwhile.\n      #\n      # Disable by setting to -1.\n      #\n      # @default 0\n      max-age: 0\n      # Cache contexts with a high cardinality.\n      #\n      # Disable by setting to [].\n      #\n      # @default ['session', 'user']\n      contexts: ['session', 'user']\n      # Tags with a high invalidation frequency.\n      #\n      # Disable by setting to [].\n      #\n      # @default []\n      tags: []\n    # Renderer cache debug:\n    #\n    # Allows cache debugging output for each rendered element.\n    #\n    # Enabling render cache debugging is not recommended in production\n    # environments.\n    # @default false\n    debug: false\n  # Cacheability debugging:\n  #\n  # Responses with cacheability metadata (CacheableResponseInterface instances)\n  # get X-Drupal-Cache-Tags, X-Drupal-Cache-Contexts and X-Drupal-Cache-Max-Age\n  # headers.\n  #\n  # For more information about debugging cacheable responses, see\n  # https://www.drupal.org/developing/api/8/response/cacheable-response-interface\n  #\n  # Enabling cacheability debugging is not recommended in production\n  # environments.\n  # @default false\n  http.response.debug_cacheability_headers: false\n  factory.keyvalue: {}\n  # Default key/value storage service to use.\n  # @default keyvalue.database\n  # default: keyvalue.database\n  # Collection-specific overrides.\n  # state: keyvalue.database\n  factory.keyvalue.expirable: {}\n  # Default key/value expirable storage service to use.\n  # @default keyvalue.database.expirable\n  # default: keyvalue.database.expirable\n  # Allowed protocols for URL generation.\n  filter_protocols:\n    - http\n    - https\n    - ftp\n    - news\n    - nntp\n    - tel\n    - telnet\n    - mailto\n    - irc\n    - ssh\n    - sftp\n    - webcal\n    - rtsp\n\n  # Configure Cross-Site HTTP requests (CORS).\n  # Read https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS\n  # for more information about the topic in general.\n  # Note: By default the configuration is disabled.\n  cors.config:\n    enabled: false\n    # Specify allowed headers, like 'x-allowed-header'.\n    allowedHeaders: []\n    # Specify allowed request methods, specify ['*'] to allow all possible ones.\n    allowedMethods: []\n    # Configure requests allowed from specific origins. Do not include trailing\n    # slashes with URLs.\n    allowedOrigins: ['*']\n    # Configure requests allowed from origins, matching against regex patterns.\n    allowedOriginsPatterns: []\n    # Sets the Access-Control-Expose-Headers header.\n    exposedHeaders: false\n    # Sets the Access-Control-Max-Age header.\n    maxAge: false\n    # Sets the Access-Control-Allow-Credentials header.\n    supportsCredentials: false\n\n  queue.config:\n    # The maximum number of seconds to wait if a queue is temporarily suspended.\n    # This is not applicable when a queue is suspended but does not specify\n    # how long to wait before attempting to resume.\n    suspendMaximumWait: 30\n"
  },
  {
    "path": "examples/stacks/drupal/web/sites/default/default.settings.php",
    "content": "<?php\n\n// phpcs:ignoreFile\n\n/**\n * @file\n * Drupal site-specific configuration file.\n *\n * IMPORTANT NOTE:\n * This file may have been set to read-only by the Drupal installation program.\n * If you make changes to this file, be sure to protect it again after making\n * your modifications. Failure to remove write permissions to this file is a\n * security risk.\n *\n * In order to use the selection rules below the multisite aliasing file named\n * sites/sites.php must be present. Its optional settings will be loaded, and\n * the aliases in the array $sites will override the default directory rules\n * below. See sites/example.sites.php for more information about aliases.\n *\n * The configuration directory will be discovered by stripping the website's\n * hostname from left to right and pathname from right to left. The first\n * configuration file found will be used and any others will be ignored. If no\n * other configuration file is found then the default configuration file at\n * 'sites/default' will be used.\n *\n * For example, for a fictitious site installed at\n * https://www.drupal.org:8080/my-site/test/, the 'settings.php' file is searched\n * for in the following directories:\n *\n * - sites/8080.www.drupal.org.my-site.test\n * - sites/www.drupal.org.my-site.test\n * - sites/drupal.org.my-site.test\n * - sites/org.my-site.test\n *\n * - sites/8080.www.drupal.org.my-site\n * - sites/www.drupal.org.my-site\n * - sites/drupal.org.my-site\n * - sites/org.my-site\n *\n * - sites/8080.www.drupal.org\n * - sites/www.drupal.org\n * - sites/drupal.org\n * - sites/org\n *\n * - sites/default\n *\n * Note that if you are installing on a non-standard port number, prefix the\n * hostname with that number. For example,\n * https://www.drupal.org:8080/my-site/test/ could be loaded from\n * sites/8080.www.drupal.org.my-site.test/.\n *\n * @see example.sites.php\n * @see \\Drupal\\Core\\DrupalKernel::getSitePath()\n *\n * In addition to customizing application settings through variables in\n * settings.php, you can create a services.yml file in the same directory to\n * register custom, site-specific service definitions and/or swap out default\n * implementations with custom ones.\n */\n\n/**\n * Database settings:\n *\n * The $databases array specifies the database connection or\n * connections that Drupal may use.  Drupal is able to connect\n * to multiple databases, including multiple types of databases,\n * during the same request.\n *\n * One example of the simplest connection array is shown below. To use the\n * sample settings, copy and uncomment the code below between the @code and\n * @endcode lines and paste it after the $databases declaration. You will need\n * to replace the database username and password and possibly the host and port\n * with the appropriate credentials for your database system.\n *\n * The next section describes how to customize the $databases array for more\n * specific needs.\n *\n * @code\n * $databases['default']['default'] = [\n *   'database' => 'database_name',\n *   'username' => 'sql_username',\n *   'password' => 'sql_password',\n *   'host' => 'localhost',\n *   'port' => '3306',\n *   'driver' => 'mysql',\n *   'prefix' => '',\n *   'collation' => 'utf8mb4_general_ci',\n * ];\n * @endcode\n */\n$databases = [];\n\n/**\n * Customizing database settings.\n *\n * Many of the values of the $databases array can be customized for your\n * particular database system. Refer to the sample in the section above as a\n * starting point.\n *\n * The \"driver\" property indicates what Drupal database driver the\n * connection should use.  This is usually the same as the name of the\n * database type, such as mysql or sqlite, but not always.  The other\n * properties will vary depending on the driver.  For SQLite, you must\n * specify a database file name in a directory that is writable by the\n * webserver.  For most other drivers, you must specify a\n * username, password, host, and database name.\n *\n * Drupal core implements drivers for mysql, pgsql, and sqlite. Other drivers\n * can be provided by contributed or custom modules. To use a contributed or\n * custom driver, the \"namespace\" property must be set to the namespace of the\n * driver. The code in this namespace must be autoloadable prior to connecting\n * to the database, and therefore, prior to when module root namespaces are\n * added to the autoloader. To add the driver's namespace to the autoloader,\n * set the \"autoload\" property to the PSR-4 base directory of the driver's\n * namespace. This is optional for projects managed with Composer if the\n * driver's namespace is in Composer's autoloader.\n *\n * For each database, you may optionally specify multiple \"target\" databases.\n * A target database allows Drupal to try to send certain queries to a\n * different database if it can but fall back to the default connection if not.\n * That is useful for primary/replica replication, as Drupal may try to connect\n * to a replica server when appropriate and if one is not available will simply\n * fall back to the single primary server (The terms primary/replica are\n * traditionally referred to as master/slave in database server documentation).\n *\n * The general format for the $databases array is as follows:\n * @code\n * $databases['default']['default'] = $info_array;\n * $databases['default']['replica'][] = $info_array;\n * $databases['default']['replica'][] = $info_array;\n * $databases['extra']['default'] = $info_array;\n * @endcode\n *\n * In the above example, $info_array is an array of settings described above.\n * The first line sets a \"default\" database that has one primary database\n * (the second level default).  The second and third lines create an array\n * of potential replica databases.  Drupal will select one at random for a given\n * request as needed.  The fourth line creates a new database with a name of\n * \"extra\".\n *\n * For MySQL, MariaDB or equivalent databases the 'isolation_level' option can\n * be set. The recommended transaction isolation level for Drupal sites is\n * 'READ COMMITTED'. The 'REPEATABLE READ' option is supported but can result\n * in deadlocks, the other two options are 'READ UNCOMMITTED' and 'SERIALIZABLE'.\n * They are available but not supported; use them at your own risk. For more\n * info:\n * https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html\n *\n * On your settings.php, change the isolation level:\n * @code\n * $databases['default']['default']['init_commands'] = [\n *   'isolation_level' => 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED',\n * ];\n * @endcode\n *\n * You can optionally set a prefix for all database table names by using the\n * 'prefix' setting. If a prefix is specified, the table name will be prepended\n * with its value. Be sure to use valid database characters only, usually\n * alphanumeric and underscore. If no prefix is desired, do not set the 'prefix'\n * key or set its value to an empty string ''.\n *\n * For example, to have all database table prefixed with 'main_', set:\n * @code\n *   'prefix' => 'main_',\n * @endcode\n *\n * Advanced users can add or override initial commands to execute when\n * connecting to the database server, as well as PDO connection settings. For\n * example, to enable MySQL SELECT queries to exceed the max_join_size system\n * variable, and to reduce the database connection timeout to 5 seconds:\n * @code\n * $databases['default']['default'] = [\n *   'init_commands' => [\n *     'big_selects' => 'SET SQL_BIG_SELECTS=1',\n *   ],\n *   'pdo' => [\n *     PDO::ATTR_TIMEOUT => 5,\n *   ],\n * ];\n * @endcode\n *\n * WARNING: The above defaults are designed for database portability. Changing\n * them may cause unexpected behavior, including potential data loss. See\n * https://www.drupal.org/docs/8/api/database-api/database-configuration for\n * more information on these defaults and the potential issues.\n *\n * More details can be found in the constructor methods for each driver:\n * - \\Drupal\\mysql\\Driver\\Database\\mysql\\Connection::__construct()\n * - \\Drupal\\pgsql\\Driver\\Database\\pgsql\\Connection::__construct()\n * - \\Drupal\\sqlite\\Driver\\Database\\sqlite\\Connection::__construct()\n *\n * Sample Database configuration format for PostgreSQL (pgsql):\n * @code\n *   $databases['default']['default'] = [\n *     'driver' => 'pgsql',\n *     'database' => 'database_name',\n *     'username' => 'sql_username',\n *     'password' => 'sql_password',\n *     'host' => 'localhost',\n *     'prefix' => '',\n *   ];\n * @endcode\n *\n * Sample Database configuration format for SQLite (sqlite):\n * @code\n *   $databases['default']['default'] = [\n *     'driver' => 'sqlite',\n *     'database' => '/path/to/database_filename',\n *   ];\n * @endcode\n *\n * Sample Database configuration format for a driver in a contributed module:\n * @code\n *   $databases['default']['default'] = [\n *     'driver' => 'my_driver',\n *     'namespace' => 'Drupal\\my_module\\Driver\\Database\\my_driver',\n *     'autoload' => 'modules/my_module/src/Driver/Database/my_driver/',\n *     'database' => 'database_name',\n *     'username' => 'sql_username',\n *     'password' => 'sql_password',\n *     'host' => 'localhost',\n *     'prefix' => '',\n *   ];\n * @endcode\n *\n * Sample Database configuration format for a driver that is extending another\n * database driver.\n * @code\n *   $databases['default']['default'] = [\n *     'driver' => 'my_driver',\n *     'namespace' => 'Drupal\\my_module\\Driver\\Database\\my_driver',\n *     'autoload' => 'modules/my_module/src/Driver/Database/my_driver/',\n *     'database' => 'database_name',\n *     'username' => 'sql_username',\n *     'password' => 'sql_password',\n *     'host' => 'localhost',\n *     'prefix' => '',\n *     'dependencies' => [\n *       'parent_module' => [\n *         'namespace' => 'Drupal\\parent_module',\n *         'autoload' => 'core/modules/parent_module/src/',\n *       ],\n *     ],\n *   ];\n * @endcode\n */\n\n/**\n * Location of the site configuration files.\n *\n * The $settings['config_sync_directory'] specifies the location of file system\n * directory used for syncing configuration data. On install, the directory is\n * created. This is used for configuration imports.\n *\n * The default location for this directory is inside a randomly-named\n * directory in the public files path. The setting below allows you to set\n * its location.\n */\n# $settings['config_sync_directory'] = '/directory/outside/webroot';\n\n/**\n * Settings:\n *\n * $settings contains environment-specific configuration, such as the files\n * directory and reverse proxy address, and temporary configuration, such as\n * security overrides.\n *\n * @see \\Drupal\\Core\\Site\\Settings::get()\n */\n\n/**\n * Salt for one-time login links, cancel links, form tokens, etc.\n *\n * This variable will be set to a random value by the installer. All one-time\n * login links will be invalidated if the value is changed. Note that if your\n * site is deployed on a cluster of web servers, you must ensure that this\n * variable has the same value on each server.\n *\n * For enhanced security, you may set this variable to the contents of a file\n * outside your document root, and vary the value across environments (like\n * production and development); you should also ensure that this file is not\n * stored with backups of your database.\n *\n * Example:\n * @code\n *   $settings['hash_salt'] = file_get_contents('/home/example/salt.txt');\n * @endcode\n */\n$settings['hash_salt'] = '';\n\n/**\n * Deployment identifier.\n *\n * Drupal's dependency injection container will be automatically invalidated and\n * rebuilt when the Drupal core version changes. When updating contributed or\n * custom code that changes the container, changing this identifier will also\n * allow the container to be invalidated as soon as code is deployed.\n */\n# $settings['deployment_identifier'] = \\Drupal::VERSION;\n\n/**\n * Access control for update.php script.\n *\n * If you are updating your Drupal installation using the update.php script but\n * are not logged in using either an account with the \"Administer software\n * updates\" permission or the site maintenance account (the account that was\n * created during installation), you will need to modify the access check\n * statement below. Change the FALSE to a TRUE to disable the access check.\n * After finishing the upgrade, be sure to open this file again and change the\n * TRUE back to a FALSE!\n */\n$settings['update_free_access'] = FALSE;\n\n/**\n * Fallback to HTTP for Update Manager and for fetching security advisories.\n *\n * If your site fails to connect to updates.drupal.org over HTTPS (either when\n * fetching data on available updates, or when fetching the feed of critical\n * security announcements), you may uncomment this setting and set it to TRUE to\n * allow an insecure fallback to HTTP. Note that doing so will open your site up\n * to a potential man-in-the-middle attack. You should instead attempt to\n * resolve the issues before enabling this option.\n * @see https://www.drupal.org/docs/system-requirements/php-requirements#openssl\n * @see https://en.wikipedia.org/wiki/Man-in-the-middle_attack\n * @see \\Drupal\\update\\UpdateFetcher\n * @see \\Drupal\\system\\SecurityAdvisories\\SecurityAdvisoriesFetcher\n */\n# $settings['update_fetch_with_http_fallback'] = TRUE;\n\n/**\n * External access proxy settings:\n *\n * If your site must access the Internet via a web proxy then you can enter the\n * proxy settings here. Set the full URL of the proxy, including the port, in\n * variables:\n * - $settings['http_client_config']['proxy']['http']: The proxy URL for HTTP\n *   requests.\n * - $settings['http_client_config']['proxy']['https']: The proxy URL for HTTPS\n *   requests.\n * You can pass in the user name and password for basic authentication in the\n * URLs in these settings.\n *\n * You can also define an array of host names that can be accessed directly,\n * bypassing the proxy, in $settings['http_client_config']['proxy']['no'].\n */\n# $settings['http_client_config']['proxy']['http'] = 'http://proxy_user:proxy_pass@example.com:8080';\n# $settings['http_client_config']['proxy']['https'] = 'http://proxy_user:proxy_pass@example.com:8080';\n# $settings['http_client_config']['proxy']['no'] = ['127.0.0.1', 'localhost'];\n\n/**\n * Reverse Proxy Configuration:\n *\n * Reverse proxy servers are often used to enhance the performance\n * of heavily visited sites and may also provide other site caching,\n * security, or encryption benefits. In an environment where Drupal\n * is behind a reverse proxy, the real IP address of the client should\n * be determined such that the correct client IP address is available\n * to Drupal's logging and access management systems. In the most simple\n * scenario, the proxy server will add an X-Forwarded-For header to the request\n * that contains the client IP address. However, HTTP headers are vulnerable to\n * spoofing, where a malicious client could bypass restrictions by setting the\n * X-Forwarded-For header directly. Therefore, Drupal's proxy configuration\n * requires the IP addresses of all remote proxies to be specified in\n * $settings['reverse_proxy_addresses'] to work correctly.\n *\n * Enable this setting to get Drupal to determine the client IP from the\n * X-Forwarded-For header. If you are unsure about this setting, do not have a\n * reverse proxy, or Drupal operates in a shared hosting environment, this\n * setting should remain commented out.\n *\n * In order for this setting to be used you must specify every possible\n * reverse proxy IP address in $settings['reverse_proxy_addresses'].\n * If a complete list of reverse proxies is not available in your\n * environment (for example, if you use a CDN) you may set the\n * $_SERVER['REMOTE_ADDR'] variable directly in settings.php.\n * Be aware, however, that it is likely that this would allow IP\n * address spoofing unless more advanced precautions are taken.\n */\n# $settings['reverse_proxy'] = TRUE;\n\n/**\n * Reverse proxy addresses.\n *\n * Specify every reverse proxy IP address in your environment, as an array of\n * IPv4/IPv6 addresses or subnets in CIDR notation. This setting is required if\n * $settings['reverse_proxy'] is TRUE.\n */\n# $settings['reverse_proxy_addresses'] = ['a.b.c.d', 'e.f.g.h/24', ...];\n\n/**\n * Reverse proxy trusted headers.\n *\n * Sets which headers to trust from your reverse proxy.\n *\n * Common values are:\n * - \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_FOR\n * - \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_HOST\n * - \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_PORT\n * - \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_PROTO\n * - \\Symfony\\Component\\HttpFoundation\\Request::HEADER_FORWARDED\n *\n * Note the default value of\n * @code\n * \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_FOR | \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_HOST | \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_PORT | \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_PROTO | \\Symfony\\Component\\HttpFoundation\\Request::HEADER_FORWARDED\n * @endcode\n * is not secure by default. The value should be set to only the specific\n * headers the reverse proxy uses. For example:\n * @code\n * \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_FOR | \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_HOST | \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_PORT | \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_PROTO\n * @endcode\n * This would trust the following headers:\n * - X_FORWARDED_FOR\n * - X_FORWARDED_HOST\n * - X_FORWARDED_PROTO\n * - X_FORWARDED_PORT\n *\n * @see \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_FOR\n * @see \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_HOST\n * @see \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_PORT\n * @see \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_PROTO\n * @see \\Symfony\\Component\\HttpFoundation\\Request::HEADER_FORWARDED\n * @see \\Symfony\\Component\\HttpFoundation\\Request::setTrustedProxies\n */\n# $settings['reverse_proxy_trusted_headers'] = \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_FOR | \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_HOST | \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_PORT | \\Symfony\\Component\\HttpFoundation\\Request::HEADER_X_FORWARDED_PROTO | \\Symfony\\Component\\HttpFoundation\\Request::HEADER_FORWARDED;\n\n\n/**\n * Page caching:\n *\n * By default, Drupal sends a \"Vary: Cookie\" HTTP header for anonymous page\n * views. This tells a HTTP proxy that it may return a page from its local\n * cache without contacting the web server, if the user sends the same Cookie\n * header as the user who originally requested the cached page. Without \"Vary:\n * Cookie\", authenticated users would also be served the anonymous page from\n * the cache. If the site has mostly anonymous users except a few known\n * editors/administrators, the Vary header can be omitted. This allows for\n * better caching in HTTP proxies (including reverse proxies), i.e. even if\n * clients send different cookies, they still get content served from the cache.\n * However, authenticated users should access the site directly (i.e. not use an\n * HTTP proxy, and bypass the reverse proxy if one is used) in order to avoid\n * getting cached pages from the proxy.\n */\n# $settings['omit_vary_cookie'] = TRUE;\n\n\n/**\n * Cache TTL for client error (4xx) responses.\n *\n * Items cached per-URL tend to result in a large number of cache items, and\n * this can be problematic on 404 pages which by their nature are unbounded. A\n * fixed TTL can be set for these items, defaulting to one hour, so that cache\n * backends which do not support LRU can purge older entries. To disable caching\n * of client error responses set the value to 0. Currently applies only to\n * page_cache module.\n */\n# $settings['cache_ttl_4xx'] = 3600;\n\n/**\n * Expiration of cached forms.\n *\n * Drupal's Form API stores details of forms in a cache and these entries are\n * kept for at least 6 hours by default. Expired entries are cleared by cron.\n *\n * @see \\Drupal\\Core\\Form\\FormCache::setCache()\n */\n# $settings['form_cache_expiration'] = 21600;\n\n/**\n * Class Loader.\n *\n * If the APCu extension is detected, the classloader will be optimized to use\n * it. Set to FALSE to disable this.\n *\n * @see https://getcomposer.org/doc/articles/autoloader-optimization.md\n */\n# $settings['class_loader_auto_detect'] = FALSE;\n\n/**\n * Authorized file system operations:\n *\n * The Update Manager module included with Drupal provides a mechanism for\n * site administrators to securely install missing updates for the site\n * directly through the web user interface. On securely-configured servers,\n * the Update manager will require the administrator to provide SSH or FTP\n * credentials before allowing the installation to proceed; this allows the\n * site to update the new files as the user who owns all the Drupal files,\n * instead of as the user the webserver is running as. On servers where the\n * webserver user is itself the owner of the Drupal files, the administrator\n * will not be prompted for SSH or FTP credentials (note that these server\n * setups are common on shared hosting, but are inherently insecure).\n *\n * Some sites might wish to disable the above functionality, and only update\n * the code directly via SSH or FTP themselves. This setting completely\n * disables all functionality related to these authorized file operations.\n *\n * @see https://www.drupal.org/node/244924\n *\n * Remove the leading hash signs to disable.\n */\n# $settings['allow_authorize_operations'] = FALSE;\n\n/**\n * Default mode for directories and files written by Drupal.\n *\n * Value should be in PHP Octal Notation, with leading zero.\n */\n# $settings['file_chmod_directory'] = 0775;\n# $settings['file_chmod_file'] = 0664;\n\n/**\n * Optimized assets path:\n *\n * A local file system path where optimized assets will be stored. This directory\n * must exist and be writable by Drupal. This directory must be relative to\n * the Drupal installation directory and be accessible over the web.\n */\n# $settings['file_assets_path'] = 'sites/default/files';\n\n/**\n * Public file base URL:\n *\n * An alternative base URL to be used for serving public files. This must\n * include any leading directory path.\n *\n * A different value from the domain used by Drupal to be used for accessing\n * public files. This can be used for a simple CDN integration, or to improve\n * security by serving user-uploaded files from a different domain or subdomain\n * pointing to the same server. Do not include a trailing slash.\n */\n# $settings['file_public_base_url'] = 'http://downloads.example.com/files';\n\n/**\n * Public file path:\n *\n * A local file system path where public files will be stored. This directory\n * must exist and be writable by Drupal. This directory must be relative to\n * the Drupal installation directory and be accessible over the web.\n */\n# $settings['file_public_path'] = 'sites/default/files';\n\n/**\n * Additional public file schemes:\n *\n * Public schemes are URI schemes that allow download access to all users for\n * all files within that scheme.\n *\n * The \"public\" scheme is always public, and the \"private\" scheme is always\n * private, but other schemes, such as \"https\", \"s3\", \"example\", or others,\n * can be either public or private depending on the site. By default, they're\n * private, and access to individual files is controlled via\n * hook_file_download().\n *\n * Typically, if a scheme should be public, a module makes it public by\n * implementing hook_file_download(), and granting access to all users for all\n * files. This could be either the same module that provides the stream wrapper\n * for the scheme, or a different module that decides to make the scheme\n * public. However, in cases where a site needs to make a scheme public, but\n * is unable to add code in a module to do so, the scheme may be added to this\n * variable, the result of which is that system_file_download() grants public\n * access to all files within that scheme.\n */\n# $settings['file_additional_public_schemes'] = ['example'];\n\n/**\n * File schemes whose paths should not be normalized:\n *\n * Normally, Drupal normalizes '/./' and '/../' segments in file URIs in order\n * to prevent unintended file access. For example, 'private://css/../image.png'\n * is normalized to 'private://image.png' before checking access to the file.\n *\n * On Windows, Drupal also replaces '\\' with '/' in URIs for the local\n * filesystem.\n *\n * If file URIs with one or more scheme should not be normalized like this, then\n * list the schemes here. For example, if 'porcelain://china/./plate.png' should\n * not be normalized to 'porcelain://china/plate.png', then add 'porcelain' to\n * this array. In this case, make sure that the module providing the 'porcelain'\n * scheme does not allow unintended file access when using '/../' to move up the\n * directory tree.\n */\n# $settings['file_sa_core_2023_005_schemes'] = ['porcelain'];\n\n/**\n * Configuration for phpinfo() admin status report.\n *\n * Drupal's admin UI includes a report at admin/reports/status/php which shows\n * the output of phpinfo(). The full output can contain sensitive information\n * so by default Drupal removes some sections.\n *\n * This behavior can be configured by setting this variable to a different\n * value corresponding to the flags parameter of phpinfo().\n *\n * If you need to expose more information in the report - for example to debug a\n * problem - consider doing so temporarily.\n *\n * @see https://www.php.net/manual/function.phpinfo.php\n */\n# $settings['sa_core_2023_004_phpinfo_flags'] = ~ (INFO_VARIABLES | INFO_ENVIRONMENT);\n\n/**\n * Private file path:\n *\n * A local file system path where private files will be stored. This directory\n * must be absolute, outside of the Drupal installation directory and not\n * accessible over the web.\n *\n * Note: Caches need to be cleared when this value is changed to make the\n * private:// stream wrapper available to the system.\n *\n * See https://www.drupal.org/documentation/modules/file for more information\n * about securing private files.\n */\n# $settings['file_private_path'] = '';\n\n/**\n * Temporary file path:\n *\n * A local file system path where temporary files will be stored. This directory\n * must be absolute, outside of the Drupal installation directory and not\n * accessible over the web.\n *\n * If this is not set, the default for the operating system will be used.\n *\n * @see \\Drupal\\Component\\FileSystem\\FileSystem::getOsTemporaryDirectory()\n */\n# $settings['file_temp_path'] = '/tmp';\n\n/**\n * Session write interval:\n *\n * Set the minimum interval between each session write to database.\n * For performance reasons it defaults to 180.\n */\n# $settings['session_write_interval'] = 180;\n\n/**\n * String overrides:\n *\n * To override specific strings on your site with or without enabling the Locale\n * module, add an entry to this list. This functionality allows you to change\n * a small number of your site's default English language interface strings.\n *\n * Remove the leading hash signs to enable.\n *\n * The \"en\" part of the variable name, is dynamic and can be any langcode of\n * any added language. (eg locale_custom_strings_de for german).\n */\n# $settings['locale_custom_strings_en'][''] = [\n#   'Home' => 'Front page',\n#   '@count min' => '@count minutes',\n# ];\n\n/**\n * A custom theme for the offline page:\n *\n * This applies when the site is explicitly set to maintenance mode through the\n * administration page or when the database is inactive due to an error.\n * The template file should also be copied into the theme. It is located inside\n * 'core/modules/system/templates/maintenance-page.html.twig'.\n *\n * Note: This setting does not apply to installation and update pages.\n */\n# $settings['maintenance_theme'] = 'claro';\n\n/**\n * PHP settings:\n *\n * To see what PHP settings are possible, including whether they can be set at\n * runtime (by using ini_set()), read the PHP documentation:\n * http://php.net/manual/ini.list.php\n * See \\Drupal\\Core\\DrupalKernel::bootEnvironment() for required runtime\n * settings and the .htaccess file for non-runtime settings.\n * Settings defined there should not be duplicated here so as to avoid conflict\n * issues.\n */\n\n/**\n * If you encounter a situation where users post a large amount of text, and\n * the result is stripped out upon viewing but can still be edited, Drupal's\n * output filter may not have sufficient memory to process it.  If you\n * experience this issue, you may wish to uncomment the following two lines\n * and increase the limits of these variables.  For more information, see\n * http://php.net/manual/pcre.configuration.php.\n */\n# ini_set('pcre.backtrack_limit', 200000);\n# ini_set('pcre.recursion_limit', 200000);\n\n/**\n * Configuration overrides.\n *\n * To globally override specific configuration values for this site,\n * set them here. You usually don't need to use this feature. This is\n * useful in a configuration file for a vhost or directory, rather than\n * the default settings.php.\n *\n * Note that any values you provide in these variable overrides will not be\n * viewable from the Drupal administration interface. The administration\n * interface displays the values stored in configuration so that you can stage\n * changes to other environments that don't have the overrides.\n *\n * There are particular configuration values that are risky to override. For\n * example, overriding the list of installed modules in 'core.extension' is not\n * supported as module install or uninstall has not occurred. Other examples\n * include field storage configuration, because it has effects on database\n * structure, and 'core.menu.static_menu_link_overrides' since this is cached in\n * a way that is not config override aware. Also, note that changing\n * configuration values in settings.php will not fire any of the configuration\n * change events.\n */\n# $config['system.site']['name'] = 'My Drupal site';\n# $config['user.settings']['anonymous'] = 'Visitor';\n\n/**\n * Load services definition file.\n */\n$settings['container_yamls'][] = $app_root . '/' . $site_path . '/services.yml';\n\n/**\n * Override the default service container class.\n *\n * This is useful for example to trace the service container for performance\n * tracking purposes, for testing a service container with an error condition or\n * to test a service container that throws an exception.\n */\n# $settings['container_base_class'] = '\\Drupal\\Core\\DependencyInjection\\Container';\n\n/**\n * Override the default yaml parser class.\n *\n * Provide a fully qualified class name here if you would like to provide an\n * alternate implementation YAML parser. The class must implement the\n * \\Drupal\\Component\\Serialization\\SerializationInterface interface.\n *\n * This setting is deprecated in Drupal 10.3 and removed in Drupal 11.\n */\n# $settings['yaml_parser_class'] = NULL;\n\n/**\n * Trusted host configuration.\n *\n * Drupal core can use the Symfony trusted host mechanism to prevent HTTP Host\n * header spoofing.\n *\n * To enable the trusted host mechanism, you enable your allowable hosts\n * in $settings['trusted_host_patterns']. This should be an array of regular\n * expression patterns, without delimiters, representing the hosts you would\n * like to allow.\n *\n * For example:\n * @code\n * $settings['trusted_host_patterns'] = [\n *   '^www\\.example\\.com$',\n * ];\n * @endcode\n * will allow the site to only run from www.example.com.\n *\n * If you are running multisite, or if you are running your site from\n * different domain names (eg, you don't redirect http://www.example.com to\n * http://example.com), you should specify all of the host patterns that are\n * allowed by your site.\n *\n * For example:\n * @code\n * $settings['trusted_host_patterns'] = [\n *   '^example\\.com$',\n *   '^.+\\.example\\.com$',\n *   '^example\\.org$',\n *   '^.+\\.example\\.org$',\n * ];\n * @endcode\n * will allow the site to run off of all variants of example.com and\n * example.org, with all subdomains included.\n *\n * @see https://www.drupal.org/docs/installing-drupal/trusted-host-settings\n */\n# $settings['trusted_host_patterns'] = [];\n\n/**\n * The default list of directories that will be ignored by Drupal's file API.\n *\n * By default ignore node_modules and bower_components folders to avoid issues\n * with common frontend tools and recursive scanning of directories looking for\n * extensions.\n *\n * @see \\Drupal\\Core\\File\\FileSystemInterface::scanDirectory()\n * @see \\Drupal\\Core\\Extension\\ExtensionDiscovery::scanDirectory()\n */\n$settings['file_scan_ignore_directories'] = [\n  'node_modules',\n  'bower_components',\n];\n\n/**\n * The default number of entities to update in a batch process.\n *\n * This is used by update and post-update functions that need to go through and\n * change all the entities on a site, so it is useful to increase this number\n * if your hosting configuration (i.e. RAM allocation, CPU speed) allows for a\n * larger number of entities to be processed in a single batch run.\n */\n$settings['entity_update_batch_size'] = 50;\n\n/**\n * Entity update backup.\n *\n * This is used to inform the entity storage handler that the backup tables as\n * well as the original entity type and field storage definitions should be\n * retained after a successful entity update process.\n */\n$settings['entity_update_backup'] = TRUE;\n\n/**\n * State caching.\n *\n * State caching uses the cache collector pattern to cache all requested keys\n * from the state API in a single cache entry, which can greatly reduce the\n * amount of database queries. However, some sites may use state with a\n * lot of dynamic keys which could result in a very large cache.\n */\n$settings['state_cache'] = TRUE;\n\n/**\n * Node migration type.\n *\n * This is used to force the migration system to use the classic node migrations\n * instead of the default complete node migrations. The migration system will\n * use the classic node migration only if there are existing migrate_map tables\n * for the classic node migrations and they contain data. These tables may not\n * exist if you are developing custom migrations and do not want to use the\n * complete node migrations. Set this to TRUE to force the use of the classic\n * node migrations.\n */\n$settings['migrate_node_migrate_type_classic'] = FALSE;\n\n/**\n * The default settings for migration sources.\n *\n * These settings are used as the default settings on the Credential form at\n * /upgrade/credentials.\n *\n * - migrate_source_version - The version of the source database. This can be\n *   '6' or '7'. Defaults to '7'.\n * - migrate_source_connection - The key in the $databases array for the source\n *   site.\n * - migrate_file_public_path - The location of the source Drupal 6 or Drupal 7\n *   public files. This can be a local file directory containing the source\n *   Drupal 6 or Drupal 7 site (e.g /var/www/docroot), or the site address\n *   (e.g http://example.com).\n * - migrate_file_private_path - The location of the source Drupal 7 private\n *   files. This can be a local file directory containing the source Drupal 7\n *   site (e.g /var/www/docroot), or empty to use the same value as Public\n *   files directory.\n *\n * Sample configuration for a drupal 6 source site with the source files in a\n * local directory.\n *\n * @code\n * $settings['migrate_source_version'] = '6';\n * $settings['migrate_source_connection'] = 'migrate';\n * $settings['migrate_file_public_path'] = '/var/www/drupal6';\n * @endcode\n *\n * Sample configuration for a drupal 7 source site with public source files on\n * the source site and the private files in a local directory.\n *\n * @code\n * $settings['migrate_source_version'] = '7';\n * $settings['migrate_source_connection'] = 'migrate';\n * $settings['migrate_file_public_path'] = 'https://drupal7.com';\n * $settings['migrate_file_private_path'] = '/var/www/drupal7';\n * @endcode\n */\n# $settings['migrate_source_connection'] = '';\n# $settings['migrate_source_version'] = '';\n# $settings['migrate_file_public_path'] = '';\n# $settings['migrate_file_private_path'] = '';\n\n/**\n * Load local development override configuration, if available.\n *\n * Create a settings.local.php file to override variables on secondary (staging,\n * development, etc.) installations of this site.\n *\n * Typical uses of settings.local.php include:\n * - Disabling caching.\n * - Disabling JavaScript/CSS compression.\n * - Rerouting outgoing emails.\n *\n * Keep this code block at the end of this file to take full effect.\n */\n#\n# if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {\n#   include $app_root . '/' . $site_path . '/settings.local.php';\n# }\n"
  },
  {
    "path": "examples/stacks/drupal/web/sites/development.services.yml",
    "content": "# Local development services.\n#\n# The development.services.yml file allows the developer to override\n# container parameters for debugging.\n#\n# To activate this feature, follow the instructions at the top of the\n# 'example.settings.local.php' file, which sits next to this file.\n#\n# Be aware that in Drupal's configuration system, all the files that\n# provide container definitions are merged using a shallow merge approach\n# within \\Drupal\\Core\\DependencyInjection\\YamlFileLoader.\n# This means that if you want to override any value of a parameter, the\n# whole parameter array needs to be copied from\n# sites/default/default.services.yml or from core/core.services.yml file.\nparameters:\n  http.response.debug_cacheability_headers: true\nservices:\n  cache.backend.null:\n    class: Drupal\\Core\\Cache\\NullBackendFactory\n"
  },
  {
    "path": "examples/stacks/drupal/web/sites/example.settings.local.php",
    "content": "<?php\n\n// phpcs:ignoreFile\n\n/**\n * @file\n * Local development override configuration feature.\n *\n * To activate this feature, copy and rename it such that its path plus\n * filename is 'sites/default/settings.local.php'. Then, go to the bottom of\n * 'sites/default/settings.php' and uncomment the commented lines that mention\n * 'settings.local.php'.\n *\n * If you are using a site name in the path, such as 'sites/example.com', copy\n * this file to 'sites/example.com/settings.local.php', and uncomment the lines\n * at the bottom of 'sites/example.com/settings.php'.\n */\n\n/**\n * Assertions.\n *\n * The Drupal project primarily uses runtime assertions to enforce the\n * expectations of the API by failing when incorrect calls are made by code\n * under development.\n *\n * @see http://php.net/assert\n * @see https://www.drupal.org/node/2492225\n *\n * It is strongly recommended that you set zend.assertions=1 in the PHP.ini file\n * (It cannot be changed from .htaccess or runtime) on development machines and\n * to 0 or -1 in production.\n */\n\n/**\n * Enable local development services.\n */\n$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';\n\n/**\n * Show all error messages, with backtrace information.\n *\n * In case the error level could not be fetched from the database, as for\n * example the database connection failed, we rely only on this value.\n */\n$config['system.logging']['error_level'] = 'verbose';\n\n/**\n * Disable CSS and JS aggregation.\n */\n$config['system.performance']['css']['preprocess'] = FALSE;\n$config['system.performance']['js']['preprocess'] = FALSE;\n\n/**\n * Disable the render cache.\n *\n * Note: you should test with the render cache enabled, to ensure the correct\n * cacheability metadata is present. However, in the early stages of\n * development, you may want to disable it.\n *\n * This setting disables the render cache by using the Null cache back-end\n * defined by the development.services.yml file above.\n *\n * Only use this setting once the site has been installed.\n */\n# $settings['cache']['bins']['render'] = 'cache.backend.null';\n\n/**\n * Disable caching for migrations.\n *\n * Uncomment the code below to only store migrations in memory and not in the\n * database. This makes it easier to develop custom migrations.\n */\n# $settings['cache']['bins']['discovery_migration'] = 'cache.backend.memory';\n\n/**\n * Disable Internal Page Cache.\n *\n * Note: you should test with Internal Page Cache enabled, to ensure the correct\n * cacheability metadata is present. However, in the early stages of\n * development, you may want to disable it.\n *\n * This setting disables the page cache by using the Null cache back-end\n * defined by the development.services.yml file above.\n *\n * Only use this setting once the site has been installed.\n */\n# $settings['cache']['bins']['page'] = 'cache.backend.null';\n\n/**\n * Disable Dynamic Page Cache.\n *\n * Note: you should test with Dynamic Page Cache enabled, to ensure the correct\n * cacheability metadata is present (and hence the expected behavior). However,\n * in the early stages of development, you may want to disable it.\n */\n# $settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';\n\n/**\n * Allow test modules and themes to be installed.\n *\n * Drupal ignores test modules and themes by default for performance reasons.\n * During development it can be useful to install test extensions for debugging\n * purposes.\n */\n# $settings['extension_discovery_scan_tests'] = TRUE;\n\n/**\n * Enable access to rebuild.php.\n *\n * This setting can be enabled to allow Drupal's php and database cached\n * storage to be cleared via the rebuild.php page. Access to this page can also\n * be gained by generating a query string from rebuild_token_calculator.sh and\n * using these parameters in a request to rebuild.php.\n */\n$settings['rebuild_access'] = TRUE;\n\n/**\n * Skip file system permissions hardening.\n *\n * The system module will periodically check the permissions of your site's\n * site directory to ensure that it is not writable by the website user. For\n * sites that are managed with a version control system, this can cause problems\n * when files in that directory such as settings.php are updated, because the\n * user pulling in the changes won't have permissions to modify files in the\n * directory.\n */\n$settings['skip_permissions_hardening'] = TRUE;\n\n/**\n * Exclude modules from configuration synchronization.\n *\n * On config export sync, no config or dependent config of any excluded module\n * is exported. On config import sync, any config of any installed excluded\n * module is ignored. In the exported configuration, it will be as if the\n * excluded module had never been installed. When syncing configuration, if an\n * excluded module is already installed, it will not be uninstalled by the\n * configuration synchronization, and dependent configuration will remain\n * intact. This affects only configuration synchronization; single import and\n * export of configuration are not affected.\n *\n * Drupal does not validate or sanity check the list of excluded modules. For\n * instance, it is your own responsibility to never exclude required modules,\n * because it would mean that the exported configuration can not be imported\n * anymore.\n *\n * This is an advanced feature and using it means opting out of some of the\n * guarantees the configuration synchronization provides. It is not recommended\n * to use this feature with modules that affect Drupal in a major way such as\n * the language or field module.\n */\n# $settings['config_exclude_modules'] = ['devel', 'stage_file_proxy'];\n"
  },
  {
    "path": "examples/stacks/drupal/web/sites/example.sites.php",
    "content": "<?php\n\n// phpcs:ignoreFile\n\n/**\n * @file\n * Configuration file for multi-site support and directory aliasing feature.\n *\n * This file is required for multi-site support and also allows you to define a\n * set of aliases that map host names, ports, and path names to configuration\n * directories in the sites directory. These aliases are loaded prior to\n * scanning for directories, and they are exempt from the normal discovery\n * rules. See default.settings.php to view how Drupal discovers the\n * configuration directory when no alias is found.\n *\n * Aliases are useful on development servers, where the domain name may not be\n * the same as the domain of the live server. Since Drupal stores file paths in\n * the database (files, system table, etc.) this will ensure the paths are\n * correct when the site is deployed to a live server.\n *\n * To activate this feature, copy and rename it such that its path plus\n * filename is 'sites/sites.php'.\n *\n * Aliases are defined in an associative array named $sites. The array is\n * written in the format: '<port>.<domain>.<path>' => 'directory'. As an\n * example, to map https://www.drupal.org:8080/my-site/test to the configuration\n * directory sites/example.com, the array should be defined as:\n * @code\n * $sites = [\n *   '8080.www.drupal.org.my-site.test' => 'example.com',\n * ];\n * @endcode\n * The URL, https://www.drupal.org:8080/my-site/test/, could be a symbolic link\n * or an Apache Alias directive that points to the Drupal root containing\n * index.php. An alias could also be created for a subdomain. See the\n * @link https://www.drupal.org/documentation/install online Drupal installation guide @endlink\n * for more information on setting up domains, subdomains, and subdirectories.\n *\n * The following examples look for a site configuration in sites/example.com:\n * @code\n * URL: http://dev.drupal.org\n * $sites['dev.drupal.org'] = 'example.com';\n *\n * URL: http://localhost/example\n * $sites['localhost.example'] = 'example.com';\n *\n * URL: http://localhost:8080/example\n * $sites['8080.localhost.example'] = 'example.com';\n *\n * URL: https://www.drupal.org:8080/my-site/test/\n * $sites['8080.www.drupal.org.my-site.test'] = 'example.com';\n * @endcode\n *\n * @see default.settings.php\n * @see \\Drupal\\Core\\DrupalKernel::getSitePath()\n * @see https://www.drupal.org/docs/getting-started/multisite-drupal\n */\n"
  },
  {
    "path": "examples/stacks/drupal/web/themes/README.txt",
    "content": "Themes allow you to change the look and feel of your Drupal site. You can use\nthemes contributed by others or create your own.\n\nWHAT TO PLACE IN THIS DIRECTORY?\n--------------------------------\n\nPlacing downloaded and custom themes in this directory separates downloaded and\ncustom themes from Drupal core's themes. This allows Drupal core to be updated\nwithout overwriting these files.\n\nDOWNLOAD ADDITIONAL THEMES\n--------------------------\n\nContributed themes from the Drupal community may be downloaded at\nhttps://www.drupal.org/project/project_theme.\n\nMULTISITE CONFIGURATION\n-----------------------\n\nIn multisite configurations, themes found in this directory are available to\nall sites. You may also put themes in the sites/all/themes directory, and the\nversions in sites/all/themes will take precedence over versions of the same\nthemes that are here. Alternatively, the sites/your_site_name/themes directory\npattern may be used to restrict themes to a specific site instance.\n\nMORE INFORMATION\n-----------------\n\nRefer to the \"Appearance\" section of the README.md in the Drupal root directory\nfor further information on customizing the appearance of Drupal with custom\nthemes.\n"
  },
  {
    "path": "examples/stacks/drupal/web/update.php",
    "content": "<?php\n\n/**\n * @file\n * The PHP page that handles updating the Drupal installation.\n *\n * All Drupal code is released under the GNU General Public License.\n * See COPYRIGHT.txt and LICENSE.txt files in the \"core\" directory.\n */\n\nuse Drupal\\Core\\Update\\UpdateKernel;\nuse Symfony\\Component\\HttpFoundation\\Request;\n\n$autoloader = require_once 'autoload.php';\n\n// Disable garbage collection during test runs. Under certain circumstances the\n// update path will create so many objects that garbage collection causes\n// segmentation faults.\nif (drupal_valid_test_ua()) {\n  gc_collect_cycles();\n  gc_disable();\n}\n\n$kernel = new UpdateKernel('prod', $autoloader, FALSE);\n$request = Request::createFromGlobals();\n\n$response = $kernel->handle($request);\n$response->send();\n\n$kernel->terminate($request, $response);\n"
  },
  {
    "path": "examples/stacks/drupal/web/web.config",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <system.webServer>\n    <!-- Don't show directory listings for URLs which map to a directory. -->\n    <directoryBrowse enabled=\"false\" />\n\n    <!--\n       Caching configuration was not delegated by default. Some hosters may not\n       delegate the caching configuration to site owners by default and that\n       may cause errors when users install. Uncomment this if you want to and\n       are allowed to enable caching.\n     -->\n    <!--\n    <caching>\n      <profiles>\n        <add extension=\".php\" policy=\"DisableCache\" kernelCachePolicy=\"DisableCache\" />\n        <add extension=\".html\" policy=\"CacheForTimePeriod\" kernelCachePolicy=\"CacheForTimePeriod\" duration=\"14:00:00\" />\n      </profiles>\n    </caching>\n     -->\n\n    <rewrite>\n      <rules>\n        <rule name=\"Protect files and directories from prying eyes\" stopProcessing=\"true\">\n          <match url=\"\\.(engine|inc|install|module|profile|po|sh|.*sql|theme|twig|tpl(\\.php)?|xtmpl|yml|svn-base)$|^(code-style\\.pl|Entries.*|Repository|Root|Tag|Template|all-wcprops|entries|format|composer\\.(json|lock)|\\.htaccess|yarn.lock|package.json)$\" />\n          <action type=\"CustomResponse\" statusCode=\"403\" subStatusCode=\"0\" statusReason=\"Forbidden\" statusDescription=\"Access is forbidden.\" />\n        </rule>\n\n        <rule name=\"Force simple error message for requests for non-existent favicon.ico\" stopProcessing=\"true\">\n          <match url=\"favicon\\.ico\" />\n          <action type=\"CustomResponse\" statusCode=\"404\" subStatusCode=\"1\" statusReason=\"File Not Found\" statusDescription=\"The requested file favicon.ico was not found\" />\n          <conditions>\n            <add input=\"{REQUEST_FILENAME}\" matchType=\"IsFile\" negate=\"true\" />\n          </conditions>\n        </rule>\n    <!-- To redirect all users to access the site WITH the 'www.' prefix,\n     http://example.com/foo will be redirected to http://www.example.com/foo)\n     adapt and uncomment the following:   -->\n    <!--\n        <rule name=\"Redirect to add www\" stopProcessing=\"true\">\n          <match url=\"^(.*)$\" ignoreCase=\"false\" />\n          <conditions>\n            <add input=\"{HTTP_HOST}\" pattern=\"^example\\.com$\" />\n          </conditions>\n          <action type=\"Redirect\" redirectType=\"Permanent\" url=\"http://www.example.com/{R:1}\" />\n        </rule>\n    -->\n\n    <!-- To redirect all users to access the site WITHOUT the 'www.' prefix,\n     http://www.example.com/foo will be redirected to http://example.com/foo)\n     adapt and uncomment the following:   -->\n    <!--\n        <rule name=\"Redirect to remove www\" stopProcessing=\"true\">\n          <match url=\"^(.*)$\" ignoreCase=\"false\" />\n          <conditions>\n            <add input=\"{HTTP_HOST}\" pattern=\"^www\\.example\\.com$\" />\n          </conditions>\n          <action type=\"Redirect\" redirectType=\"Permanent\" url=\"http://example.com/{R:1}\" />\n        </rule>\n    -->\n\n        <!-- Pass all requests not referring directly to files in the filesystem\n         to index.php. -->\n        <rule name=\"Short URLS\" stopProcessing=\"true\">\n          <match url=\"^(.*)$\" ignoreCase=\"false\" />\n          <conditions>\n            <add input=\"{REQUEST_FILENAME}\" matchType=\"IsFile\" ignoreCase=\"false\" negate=\"true\" />\n            <add input=\"{REQUEST_FILENAME}\" matchType=\"IsDirectory\" ignoreCase=\"false\" negate=\"true\" />\n            <add input=\"{URL}\" pattern=\"^/favicon.ico$\" ignoreCase=\"false\" negate=\"true\" />\n          </conditions>\n          <action type=\"Rewrite\" url=\"index.php\" />\n        </rule>\n      </rules>\n    </rewrite>\n\n  <!-- If running Windows Server 2008 R2 this can be commented out -->\n    <!-- httpErrors>\n      <remove statusCode=\"404\" subStatusCode=\"-1\" />\n      <error statusCode=\"404\" prefixLanguageFilePath=\"\" path=\"/index.php\" responseMode=\"ExecuteURL\" />\n    </httpErrors -->\n\n    <defaultDocument>\n     <!-- Set the default document -->\n      <files>\n         <clear />\n        <add value=\"index.php\" />\n      </files>\n    </defaultDocument>\n\n  </system.webServer>\n</configuration>\n"
  },
  {
    "path": "examples/stacks/jekyll/.envrc",
    "content": "# Automatically sets up your devbox environment whenever you cd into this\n# directory via our direnv integration:\n\neval \"$(devbox generate direnv --print-envrc)\"\n\n# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/\n# for more details\n"
  },
  {
    "path": "examples/stacks/jekyll/README.md",
    "content": "# Jekyll Example\n\n[![Built with Devbox](https://www.jetify.com/img/devbox/shield_moon.svg)](https://www.jetify.com/devbox/docs/contributor-quickstart/)\n\n\nInspired by [This Example](https://litchipi.github.io/nix/2023/01/12/build-jekyll-blog-with-nix.html)\n\n## How to Use\n\n1. Install [Devbox](https://www.jetify.com/docs/devbox/installing-devbox/index)\n1. Create a new project with:\n\n    ```bash\n    devbox create --template jekyll\n    devbox install\n    ```\n\n1. Run `devbox shell` to install your packages and run the init hook\n1. In the root directory, run `devbox run generate` to install and package the project with bundler\n1. In the root directory, run `devbox run serve` to start the server. You can access the Jekyll example at `localhost:4000`\n\n## Related Docs\n\n* [Using Ruby with Devbox](https://www.jetify.com/devbox/docs/devbox_examples/languages/ruby/)\n"
  },
  {
    "path": "examples/stacks/jekyll/devbox.json",
    "content": "{\n    \"packages\": [\n        \"bundler@latest\",\n        \"ruby@3.1\",\n        \"libffi@latest\"\n    ],\n    \"shell\": {\n        \"init_hook\": [],\n        \"scripts\": {\n            \"generate\": [\n                \"gem install jekyll --version \\\"~> 3.9.2\\\" --no-document\",\n                \"cd myblog\",\n                \"bundle update\",\n                \"bundle lock\",\n                \"bundle package\",\n                \"rm -rf vendor\"\n            ],\n            \"run_test\": [\n                \"cd myblog\",\n                \"devbox run generate\",\n                \"bundler exec $GEM_HOME/bin/jekyll build --trace\"\n            ],\n            \"serve\": [\n                \"cd myblog\",\n                \"bundler exec $GEM_HOME/bin/jekyll serve --trace\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "examples/stacks/jekyll/myblog/.bundle/config",
    "content": "---\nBUNDLE_CACHE_ALL: \"false\"\n"
  },
  {
    "path": "examples/stacks/jekyll/myblog/.gitignore",
    "content": "_site\n.sass-cache\n.jekyll-metadata\n"
  },
  {
    "path": "examples/stacks/jekyll/myblog/404.html",
    "content": "---\nlayout: default\n---\n\n<style type=\"text/css\" media=\"screen\">\n  .container {\n    margin: 10px auto;\n    max-width: 600px;\n    text-align: center;\n  }\n  h1 {\n    margin: 30px 0;\n    font-size: 4em;\n    line-height: 1;\n    letter-spacing: -1px;\n  }\n</style>\n\n<div class=\"container\">\n  <h1>404</h1>\n\n  <p><strong>Page not found :(</strong></p>\n  <p>The requested page could not be found.</p>\n</div>\n"
  },
  {
    "path": "examples/stacks/jekyll/myblog/Gemfile",
    "content": "source \"https://rubygems.org\"\n\n# Hello! This is where you manage which Jekyll version is used to run.\n# When you want to use a different version, change it below, save the\n# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:\n#\n#     bundle exec jekyll serve\n#\n# This will help ensure the proper Jekyll version is running.\n# Happy Jekylling!\ngem \"jekyll\", \"~> 3.9.2\"\n\n# This is the default theme for new Jekyll sites. You may change this to anything you like.\ngem \"minima\", \"~> 2.0\"\n\n# If you want to use GitHub Pages, remove the \"gem \"jekyll\"\" above and\n# uncomment the line below. To upgrade, run `bundle update github-pages`.\n# gem \"github-pages\", group: :jekyll_plugins\n\n# If you have any plugins, put them here!\ngroup :jekyll_plugins do\n  gem \"jekyll-feed\", \"~> 0.6\"\nend\n\n# Windows does not include zoneinfo files, so bundle the tzinfo-data gem\n# and associated library.\ninstall_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do\n  gem \"tzinfo\", \"~> 1.2\"\n  gem \"tzinfo-data\"\nend\n\n# Performance-booster for watching directories on Windows\ngem \"wdm\", \"~> 0.1.0\", :install_if => Gem.win_platform?\n\n# kramdown v2 ships without the gfm parser by default. If you're using\n# kramdown v1, comment out this line.\ngem \"kramdown-parser-gfm\"\n\n# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem\n# do not have a Java counterpart.\ngem \"http_parser.rb\", \"~> 0.6.0\", :platforms => [:jruby]\n\ngem \"webrick\", \"~> 1.8\"\n"
  },
  {
    "path": "examples/stacks/jekyll/myblog/_config.yml",
    "content": "# Welcome to Jekyll!\n#\n# This config file is meant for settings that affect your whole blog, values\n# which you are expected to set up once and rarely edit after that. If you find\n# yourself editing this file very often, consider using Jekyll's data files\n# feature for the data you need to update frequently.\n#\n# For technical reasons, this file is *NOT* reloaded automatically when you use\n# 'bundle exec jekyll serve'. If you change this file, please restart the server process.\n\n# Site settings\n# These are used to personalize your new site. If you look in the HTML files,\n# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.\n# You can create any custom variable you would like, and they will be accessible\n# in the templates via {{ site.myvariable }}.\ntitle: Your awesome title\nemail: your-email@example.com\ndescription: >- # this means to ignore newlines until \"baseurl:\"\n  Write an awesome description for your new site here. You can edit this\n  line in _config.yml. It will appear in your document head meta (for\n  Google search results) and in your feed.xml site description.\nbaseurl: \"\" # the subpath of your site, e.g. /blog\nurl: \"\" # the base hostname & protocol for your site, e.g. http://example.com\ntwitter_username: jekyllrb\ngithub_username:  jekyll\n\n# Build settings\nmarkdown: kramdown\ntheme: minima\nplugins:\n  - jekyll-feed\n\n# Exclude from processing.\n# The following items will not be processed, by default. Create a custom list\n# to override the default setting.\n# exclude:\n#   - Gemfile\n#   - Gemfile.lock\n#   - node_modules\n#   - vendor/bundle/\n#   - vendor/cache/\n#   - vendor/gems/\n#   - vendor/ruby/\n"
  },
  {
    "path": "examples/stacks/jekyll/myblog/_posts/2023-01-15-welcome-to-jekyll.markdown",
    "content": "---\nlayout: post\ntitle:  \"Hello From Jetpack!\"\ndate:   2023-01-15 14:44:23 -0800\ncategories: jekyll update\n---\nYou’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated.\n\nTo add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works.\n\nJekyll also offers powerful support for code snippets:\n\n{% highlight ruby %}\ndef print_hi(name)\n  puts \"Hi, #{name}\"\nend\nprint_hi('Tom')\n#=> prints 'Hi, Tom' to STDOUT.\n{% endhighlight %}\n\nCheck out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk].\n\n[jekyll-docs]: https://jekyllrb.com/docs/home\n[jekyll-gh]:   https://github.com/jekyll/jekyll\n[jekyll-talk]: https://talk.jekyllrb.com/\n"
  },
  {
    "path": "examples/stacks/jekyll/myblog/about.md",
    "content": "---\nlayout: page\ntitle: About\npermalink: /about/\n---\n\nThis is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](https://jekyllrb.com/)\n\nYou can find the source code for Minima at GitHub:\n[jekyll][jekyll-organization] /\n[minima](https://github.com/jekyll/minima)\n\nYou can find the source code for Jekyll at GitHub:\n[jekyll][jekyll-organization] /\n[jekyll](https://github.com/jekyll/jekyll)\n\n\n[jekyll-organization]: https://github.com/jekyll\n"
  },
  {
    "path": "examples/stacks/jekyll/myblog/index.md",
    "content": "---\n# Feel free to add content and custom Front Matter to this file.\n# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults\n\nlayout: home\n---\n"
  },
  {
    "path": "examples/stacks/jekyll/process-compose.yml",
    "content": "# Process compose for starting jekyll\nversion: \"0.5\"\n\nprocesses:\n  jekyll:\n   command: cd myblog && bundler exec $GEM_HOME/bin/jekyll serve\n   availability:\n    restart: \"always\"\n"
  },
  {
    "path": "examples/stacks/lapp-stack/.gitignore",
    "content": "conf/mysql/data\n*.swp\n*.log\n*.pid\n*.sock"
  },
  {
    "path": "examples/stacks/lapp-stack/.testrc",
    "content": "export foo=bar\nexport foo2=\"This is a test\"\nalias devbox=~/devbox/devbox\n\n"
  },
  {
    "path": "examples/stacks/lapp-stack/README.md",
    "content": "# LAPP Stack\n\nThis example shows how to build a simple application using Apache, PHP, and PostgreSQL. It uses Devbox Plugins for all 3 packages to simplify configuration.\n\n\n## How to Run\n\nThe following steps may be done inside or outside a devbox shell.\n\n1. Initialize a database by running `devbox run init_db`.\n1. Create the database and load the test data by using `devbox run create_db`.\n1. Start Apache, PHP-FPM, and Postgres in the background by run `devbox services start`.\n1. You can now test the app using `localhost:8080` to hit the Apache Server. If you want Apache to listen on a different port, you can change the `HTTPD_PORT` environment variable in the Devbox init_hook.\n\n### How to Recreate this Example\n\n1. Create a new project with:\n    ```bash\n    devbox create --template lapp-stack\n    devbox install\n    ```\n\n1. Update `devbox.d/apache/httpd.conf` to point to the directory with your PHP files. You'll need to update the `DocumentRoot` and `Directory` directives.\n1. Follow the instructions above in the How to Run section to initialize your project.\n\n### Related Docs\n\n* [Using PHP with Devbox](https://www.jetify.com/devbox/docs/devbox_examples/languages/php/)\n* [Using Apache with Devbox](https://www.jetify.com/devbox/docs/devbox_examples/servers/apache/)\n* [Using PostgreSQL with Devbox](https://www.jetify.com/devbox/docs/devbox_examples/databases/postgres/)\n"
  },
  {
    "path": "examples/stacks/lapp-stack/devbox.d/apache/httpd.conf",
    "content": "ServerAdmin             \"root@localhost\"\nServerName              \"devbox-apache\"\nListen                  \"${HTTPD_PORT}\"\nPidFile                 \"${HTTPD_CONFDIR}/apache.pid\"\n\nLoadModule mpm_event_module modules/mod_mpm_event.so\nLoadModule authz_host_module modules/mod_authz_host.so\nLoadModule authz_core_module modules/mod_authz_core.so\nLoadModule auth_basic_module modules/mod_auth_basic.so\nLoadModule mime_module modules/mod_mime.so\nLoadModule headers_module modules/mod_headers.so\nLoadModule unixd_module modules/mod_unixd.so\nLoadModule status_module modules/mod_status.so\nLoadModule proxy_module modules/mod_proxy.so\nLoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so\nLoadModule dir_module modules/mod_dir.so\nLoadModule alias_module modules/mod_alias.so\n\n<IfModule unixd_module>\n    User daemon\n    Group daemon\n</IfModule>\n\n<Directory />\n    AllowOverride none\n    Require all denied\n</Directory>\n\nDocumentRoot  \"${HTTPD_DEVBOX_CONFIG_DIR}/my_app\"\n<Directory \"${HTTPD_DEVBOX_CONFIG_DIR}/my_app\">\n    Options Indexes FollowSymLinks\n    AllowOverride None\n    Require all granted\n</Directory>\n\n<Files \".ht*\">\n    Require all denied\n</Files>\nErrorLog \"${HTTPD_ERROR_LOG_FILE}\"\n<IfModule headers_module>\n    RequestHeader unset Proxy early\n</IfModule>\n\n<VirtualHost \"*:${HTTPD_PORT}\">\n    ServerAdmin webmaster@localhost\n    ServerName  php_localhost\n\n    UseCanonicalName    Off\n    DocumentRoot \"${HTTPD_DEVBOX_CONFIG_DIR}/my_app\"\n\n    <Directory \"${HTTPD_DEVBOX_CONFIG_DIR}/my_app\">\n        Options All\n        AllowOverride All\n        <IfModule mod_authz_host.c>\n            Require all granted\n        </IfModule>\n    </Directory>\n\n    ## Added for php-fpm\n    ProxyPassMatch ^/(.*\\.php(/.*)?)$ fcgi://127.0.0.1:8082/${HTTPD_DEVBOX_CONFIG_DIR}/my_app//$1\n    DirectoryIndex index.php \n\n</VirtualHost>\n"
  },
  {
    "path": "examples/stacks/lapp-stack/devbox.d/php/php-fpm.conf",
    "content": "[global]\npid = ${PHPFPM_PID_FILE}\nerror_log = ${PHPFPM_ERROR_LOG_FILE}\ndaemonize = yes\n\n[www]\n; user = www-data\n; group = www-data\nlisten = 127.0.0.1:${PHPFPM_PORT}\n; listen.owner = www-data\n; listen.group = www-data\npm = dynamic\npm.max_children = 5\npm.start_servers = 2\npm.min_spare_servers = 1\npm.max_spare_servers = 3\nchdir = /\n"
  },
  {
    "path": "examples/stacks/lapp-stack/devbox.d/php/php.ini",
    "content": "[php]\n\n; Put your php.ini directives here. For the latest default php.ini file, see https://github.com/php/php-src/blob/master/php.ini-production\n\n; memory_limit = 128M\n; expose_php = Off\n"
  },
  {
    "path": "examples/stacks/lapp-stack/devbox.d/web/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hello World!</title>\n  </head>\n  <body>\n    Hello World!\n  </body>\n</html>\n"
  },
  {
    "path": "examples/stacks/lapp-stack/devbox.json",
    "content": "{\n  \"packages\": [\n    \"curl@latest\",\n    \"php@latest\",\n    \"php83Extensions.pgsql@latest\",\n    \"apache@latest\",\n    \"postgresql@latest\"\n  ],\n  \"env\": {\n    \"PGHOST\": \"/tmp/devbox/lapp\",\n    \"PGPORT\": \"5432\"\n  },\n  \"shell\": {\n    \"scripts\": {\n      \"create_db\": [\n        \"dropdb --if-exists devbox_lapp\",\n        \"createdb devbox_lapp\",\n        \"psql devbox_lapp < setup_postgres_db.sql\"\n      ],\n      \"init_db\": \"initdb\",\n      \"run_test\": [\n        \"mkdir -p /tmp/devbox/lapp\", \n        \"initdb\",\n        \"devbox services up -b\",\n        \"echo 'sleep 5 second for the postgres server to initialize.' && sleep 5\",\n        \"cat .devbox/compose.log\",\n        \"dropdb --if-exists devbox_lapp\",\n        \"createdb devbox_lapp\",\n        \"psql devbox_lapp < setup_postgres_db.sql\",\n        \"curl localhost:$HTTPD_PORT\",\n        \"devbox services stop\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/stacks/lapp-stack/my_app/config.php",
    "content": "<?php\n\t$db_hostname    = \"127.0.0.1\";\n\t$db_port \t= \"5432\";\n\t$db_database\t= \"devbox_lapp\";\n\t$db_username\t= \"devbox_user\";\n\t$db_password\t= \"password\";\n"
  },
  {
    "path": "examples/stacks/lapp-stack/my_app/index.php",
    "content": "<?php\n\ninclude 'config.php';\n\n$dbconn = pg_connect(\"host=$db_hostname dbname=$db_database user=$db_username password=$db_password\")\n\tor die('Could not connect: ' . pg_last_error());\n\n// Check if the form has been submitted\nif ($_SERVER['REQUEST_METHOD'] == 'POST') {\n\n\t// Get the form data\n\t$first_name = $_POST['first_name'];\n\t$last_name = $_POST['last_name'];\n\t$phone = $_POST['phone'];\n\t$email = $_POST['email'];\n  \n\t// Insert the new record into the database\n\t$query = \"INSERT INTO address_book (first_name, last_name, phone, email,) VALUES ('$first_name', '$last_name', '$phone', '$email')\";\n\t$result = pg_query($dbconn, $query);\n\tif (!$result) {\n\t  die(\"Error: \" . pg_last_error($dbconn));\n\t}\n  }\n  \n  // Query the database for all records\n  $query = \"SELECT * FROM address_book ORDER BY last_name, first_name\";\n  $result = pg_query($dbconn, $query);\n  if (!$result) {\n\tdie(\"Error: \" . pg_last_error($dbconn));\n  }\n  \n  ?>\n  \n  <!-- HTML form for adding new records -->\n  <form method=\"post\" action=\"\">\n\t<label for=\"first_name\">First name:</label><br>\n\t<input type=\"text\" id=\"first_name\" name=\"first_name\"><br>\n\t<label for=\"last_name\">Last name:</label><br>\n\t<input type=\"text\" id=\"last_name\" name=\"last_name\"><br>\n\t<label for=\"phone\">Phone:</label><br>\n\t<input type=\"text\" id=\"phone\" name=\"phone\"><br>\n\t<label for=\"email\">Email:</label><br>\n\t<input type=\"text\" id=\"email\" name=\"email\"><br>\n\t<input type=\"submit\" value=\"Submit\">\n  </form>\n  \n  <!-- HTML table for displaying records -->\n  <table>\n\t<tr>\n\t  <th>Name</th>\n\t  <th>Phone</th>\n\t  <th>Email</th>\n\t</tr>\n\t<?php while ($row = pg_fetch_array($result)) { ?>\n\t  <tr>\n\t\t<td><?php echo $row['first_name'] . ' ' . $row['last_name']; ?></td>\n\t\t<td><?php echo $row['phone']; ?></td>\n\t\t<td><?php echo $row['email']; ?></td>\n\t  </tr>\n\t<?php } ?>\n  </table>\n  \n  <?php\n  \n  // Close the database connection\n  pg_close($dbconn);\n  \n"
  },
  {
    "path": "examples/stacks/lapp-stack/my_app/info.php",
    "content": "<?php \n   phpinfo();"
  },
  {
    "path": "examples/stacks/lapp-stack/setup_postgres_db.sql",
    "content": "--- You should run this query using psql < setup_db.sql`\n\n\nDO\n$do$\nBEGIN\n   IF EXISTS (SELECT FROM pg_catalog.pg_roles WHERE  rolname = 'devbox_user') THEN\n      RAISE NOTICE 'Role \"my_user\" already exists. Skipping.';\n   ELSE\n      CREATE USER devbox_user WITH PASSWORD 'password';\n   END IF;\nEND\n$do$;\n\nDROP TABLE IF EXISTS address_book;\nCREATE TABLE address_book (\n  id SERIAL PRIMARY KEY,\n  first_name VARCHAR(255) NOT NULL,\n  last_name VARCHAR(255) NOT NULL,\n  phone VARCHAR(255) NOT NULL,\n  email VARCHAR(255) NOT NULL\n);\n\nINSERT INTO address_book (first_name, last_name, phone, email) VALUES ('Jim', 'Hawkins', '555-0100', 'jhawk@jetpack.io'), ('Billy', 'Bones', '555-0102', 'bbones@jetpack.io');\n\nGRANT ALL PRIVILEGES ON address_book TO devbox_user;\n"
  },
  {
    "path": "examples/stacks/laravel/README.md",
    "content": "# Laravel\n\nLaravel is a powerful web application framework built with PHP. It's a great choice for building web applications and APIs.\n\nThis example shows how to build a simple Laravel application backed by MariaDB and Redis. It uses Devbox Plugins for all 3 Nix packages to simplify configuration\n\n\n## How to Run\n\n1. Install [Devbox](https://www.jetify.com/docs/devbox/installing-devbox/index)\n\n1. Create a new Laravel App by running `devbox create --template laravel`. This will create a new Laravel project in your current directory.\n\n1. Start your MariaDB and Redis services by running `devbox services up`.\n   1. This step will also create an empty MariaDB Data Directory and initialize your database with the default settings\n   2. This will also start the php-fpm service for serving your PHP project over fcgi. Learn more about [PHP-FPM](https://www.php.net/manual/en/install.fpm.php)\n\n1. Create the laravel database by running `devbox run db:create`, and then run Laravel's initial migrations using `devbox run db:migrate`\n\n1. You can now start the artisan server by running `devbox run serve:dev`. This will start the server on port 8000, which you can access at `localhost:8000`\n\n1. If you're using Laravel on Devbox Cloud, you can test the app by appending `/port/8000` to your Devbox Cloud URL\n\n1. For more details on building and developing your Laravel project, visit the [Laravel Docs](https://laravel.com/docs/10.x)\n\n\n## How to Recreate this Example\n\n### Creating the Laravel Project\n\n1. Create a new project with `devbox init`\n\n2. Add the packages using the command below. Installing the packages with `devbox add` will ensure that the plugins are activated:\n\n    ```bash\n    devbox add mariadb@latest, php@8.1, nodejs@18, redis@latest, php81Packages.composer@latest\n    ```\n\n3. Run `devbox shell` to start your shell. This will also initialize your database by running `initdb` in the init hook.\n\n4. Create your laravel project by running:\n\n    ```bash\n    composer create-project laravel/laravel tmp\n\n    mv tmp/* tmp/.* .\n    ```\n\n### Setting up MariaDB\n\nTo use MariaDB, you need to create the default Laravel database. You can do this by running the following commands in your `devbox shell`:\n\n```bash\n# Start the MariaDB service\ndevbox services up mariadb -b\n\n# Create the database\nmysql -u root -e \"CREATE DATABASE laravel;\"\n\n# Once you're done, stop the MariaDB service\ndevbox services stop mariadb\n```\n"
  },
  {
    "path": "examples/stacks/laravel/devbox.d/php/php-fpm.conf",
    "content": "[global]\npid = ${PHPFPM_PID_FILE}\nerror_log = ${PHPFPM_ERROR_LOG_FILE}\n\n[www]\n; user = www-data\n; group = www-data\nlisten = 0.0.0.0:${PHPFPM_PORT}\n; listen.owner = www-data\n; listen.group = www-data\npm = dynamic\npm.max_children = 5\npm.start_servers = 2\npm.min_spare_servers = 1\npm.max_spare_servers = 3\nchdir = /\n"
  },
  {
    "path": "examples/stacks/laravel/devbox.d/php/php.ini",
    "content": "[php]\n\n; Put your php.ini directives here. For the latest default php.ini file, see https://github.com/php/php-src/blob/master/php.ini-production\n\n; memory_limit = 128M\n; expose_php = Off\n"
  },
  {
    "path": "examples/stacks/laravel/devbox.d/redis/redis.conf",
    "content": "# Redis configuration file example.\n#\n# Note that in order to read the configuration file, Redis must be\n# started with the file path as first argument:\n#\n# ./redis-server /path/to/redis.conf\n\n# Note on units: when memory size is needed, it is possible to specify\n# it in the usual form of 1k 5GB 4M and so forth:\n#\n# 1k => 1000 bytes\n# 1kb => 1024 bytes\n# 1m => 1000000 bytes\n# 1mb => 1024*1024 bytes\n# 1g => 1000000000 bytes\n# 1gb => 1024*1024*1024 bytes\n#\n# units are case insensitive so 1GB 1Gb 1gB are all the same.\n\n################################## INCLUDES ###################################\n\n# Include one or more other config files here.  This is useful if you\n# have a standard template that goes to all Redis servers but also need\n# to customize a few per-server settings.  Include files can include\n# other files, so use this wisely.\n#\n# Notice option \"include\" won't be rewritten by command \"CONFIG REWRITE\"\n# from admin or Redis Sentinel. Since Redis always uses the last processed\n# line as value of a configuration directive, you'd better put includes\n# at the beginning of this file to avoid overwriting config change at runtime.\n#\n# If instead you are interested in using includes to override configuration\n# options, it is better to use include as the last line.\n#\n# include /path/to/local.conf\n# include /path/to/other.conf\n\n################################## MODULES #####################################\n\n# Load modules at startup. If the server is not able to load modules\n# it will abort. It is possible to use multiple loadmodule directives.\n#\n# loadmodule /path/to/my_module.so\n# loadmodule /path/to/other_module.so\n\n################################## NETWORK #####################################\n\n# By default, if no \"bind\" configuration directive is specified, Redis listens\n# for connections from all the network interfaces available on the server.\n# It is possible to listen to just one or multiple selected interfaces using\n# the \"bind\" configuration directive, followed by one or more IP addresses.\n#\n# Examples:\n#\n# bind 192.168.1.100 10.0.0.1\n# bind 127.0.0.1 ::1\n#\n# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the\n# internet, binding to all the interfaces is dangerous and will expose the\n# instance to everybody on the internet. So by default we uncomment the\n# following bind directive, that will force Redis to listen only into\n# the IPv4 lookback interface address (this means Redis will be able to\n# accept connections only from clients running into the same computer it\n# is running).\n#\n# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES\n# JUST COMMENT THE FOLLOWING LINE.\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nbind 127.0.0.1\n\n# Protected mode is a layer of security protection, in order to avoid that\n# Redis instances left open on the internet are accessed and exploited.\n#\n# When protected mode is on and if:\n#\n# 1) The server is not binding explicitly to a set of addresses using the\n#    \"bind\" directive.\n# 2) No password is configured.\n#\n# The server only accepts connections from clients connecting from the\n# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain\n# sockets.\n#\n# By default protected mode is enabled. You should disable it only if\n# you are sure you want clients from other hosts to connect to Redis\n# even if no authentication is configured, nor a specific set of interfaces\n# are explicitly listed using the \"bind\" directive.\nprotected-mode yes\n\n# Accept connections on the specified port, default is 6379 (IANA #815344).\n# If port 0 is specified Redis will not listen on a TCP socket.\nport 6379\n\n# TCP listen() backlog.\n#\n# In high requests-per-second environments you need an high backlog in order\n# to avoid slow clients connections issues. Note that the Linux kernel\n# will silently truncate it to the value of /proc/sys/net/core/somaxconn so\n# make sure to raise both the value of somaxconn and tcp_max_syn_backlog\n# in order to get the desired effect.\ntcp-backlog 511\n\n# Unix socket.\n#\n# Specify the path for the Unix socket that will be used to listen for\n# incoming connections. There is no default, so Redis will not listen\n# on a unix socket when not specified.\n#\n# unixsocket /tmp/redis.sock\n# unixsocketperm 700\n\n# Close the connection after a client is idle for N seconds (0 to disable)\ntimeout 0\n\n# TCP keepalive.\n#\n# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence\n# of communication. This is useful for two reasons:\n#\n# 1) Detect dead peers.\n# 2) Take the connection alive from the point of view of network\n#    equipment in the middle.\n#\n# On Linux, the specified value (in seconds) is the period used to send ACKs.\n# Note that to close the connection the double of the time is needed.\n# On other kernels the period depends on the kernel configuration.\n#\n# A reasonable value for this option is 300 seconds, which is the new\n# Redis default starting with Redis 3.2.1.\ntcp-keepalive 300\n\n################################# GENERAL #####################################\n\n# By default Redis does not run as a daemon. Use 'yes' if you need it.\n# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.\ndaemonize no\n\n# If you run Redis from upstart or systemd, Redis can interact with your\n# supervision tree. Options:\n#   supervised no      - no supervision interaction\n#   supervised upstart - signal upstart by putting Redis into SIGSTOP mode\n#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET\n#   supervised auto    - detect upstart or systemd method based on\n#                        UPSTART_JOB or NOTIFY_SOCKET environment variables\n# Note: these supervision methods only signal \"process is ready.\"\n#       They do not enable continuous liveness pings back to your supervisor.\nsupervised no\n\n# If a pid file is specified, Redis writes it where specified at startup\n# and removes it at exit.\n#\n# When the server runs non daemonized, no pid file is created if none is\n# specified in the configuration. When the server is daemonized, the pid file\n# is used even if not specified, defaulting to \"/var/run/redis.pid\".\n#\n# Creating a pid file is best effort: if Redis is not able to create it\n# nothing bad happens, the server will start and run normally.\npidfile redis.pid\n\n# Specify the server verbosity level.\n# This can be one of:\n# debug (a lot of information, useful for development/testing)\n# verbose (many rarely useful info, but not a mess like the debug level)\n# notice (moderately verbose, what you want in production probably)\n# warning (only very important / critical messages are logged)\nloglevel notice\n\n# Specify the log file name. Also the empty string can be used to force\n# Redis to log on the standard output. Note that if you use standard\n# output for logging but daemonize, logs will be sent to /dev/null\n\n# To enable logging to the system logger, just set 'syslog-enabled' to yes,\n# and optionally update the other syslog parameters to suit your needs.\n# syslog-enabled no\n\n# Specify the syslog identity.\n# syslog-ident redis\n\n# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.\n# syslog-facility local0\n\n# Set the number of databases. The default database is DB 0, you can select\n# a different one on a per-connection basis using SELECT <dbid> where\n# dbid is a number between 0 and 'databases'-1\ndatabases 16\n\n# By default Redis shows an ASCII art logo only when started to log to the\n# standard output and if the standard output is a TTY. Basically this means\n# that normally a logo is displayed only in interactive sessions.\n#\n# However it is possible to force the pre-4.0 behavior and always show a\n# ASCII art logo in startup logs by setting the following option to yes.\nalways-show-logo yes\n\n################################ SNAPSHOTTING  ################################\n#\n# Save the DB on disk:\n#\n#   save <seconds> <changes>\n#\n#   Will save the DB if both the given number of seconds and the given\n#   number of write operations against the DB occurred.\n#\n#   In the example below the behaviour will be to save:\n#   after 900 sec (15 min) if at least 1 key changed\n#   after 300 sec (5 min) if at least 10 keys changed\n#   after 60 sec if at least 10000 keys changed\n#\n#   Note: you can disable saving completely by commenting out all \"save\" lines.\n#\n#   It is also possible to remove all the previously configured save\n#   points by adding a save directive with a single empty string argument\n#   like in the following example:\n#\n#   save \"\"\n\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# By default Redis will stop accepting writes if RDB snapshots are enabled\n# (at least one save point) and the latest background save failed.\n# This will make the user aware (in a hard way) that data is not persisting\n# on disk properly, otherwise chances are that no one will notice and some\n# disaster will happen.\n#\n# If the background saving process will start working again Redis will\n# automatically allow writes again.\n#\n# However if you have setup your proper monitoring of the Redis server\n# and persistence, you may want to disable this feature so that Redis will\n# continue to work as usual even if there are problems with disk,\n# permissions, and so forth.\nstop-writes-on-bgsave-error yes\n\n# Compress string objects using LZF when dump .rdb databases?\n# For default that's set to 'yes' as it's almost always a win.\n# If you want to save some CPU in the saving child set it to 'no' but\n# the dataset will likely be bigger if you have compressible values or keys.\nrdbcompression yes\n\n# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.\n# This makes the format more resistant to corruption but there is a performance\n# hit to pay (around 10%) when saving and loading RDB files, so you can disable it\n# for maximum performances.\n#\n# RDB files created with checksum disabled have a checksum of zero that will\n# tell the loading code to skip the check.\nrdbchecksum yes\n\n# The filename where to dump the DB\ndbfilename dump.rdb\n\n# The working directory.\n#\n# The DB will be written inside this directory, with the filename specified\n# above using the 'dbfilename' configuration directive.\n#\n# The Append Only File will also be created inside this directory.\n#\n# Note that you must specify a directory here, not a file name.\ndir .devbox/virtenv/redis/\n\n################################# REPLICATION #################################\n\n# Master-Slave replication. Use slaveof to make a Redis instance a copy of\n# another Redis server. A few things to understand ASAP about Redis replication.\n#\n# 1) Redis replication is asynchronous, but you can configure a master to\n#    stop accepting writes if it appears to be not connected with at least\n#    a given number of slaves.\n# 2) Redis slaves are able to perform a partial resynchronization with the\n#    master if the replication link is lost for a relatively small amount of\n#    time. You may want to configure the replication backlog size (see the next\n#    sections of this file) with a sensible value depending on your needs.\n# 3) Replication is automatic and does not need user intervention. After a\n#    network partition slaves automatically try to reconnect to masters\n#    and resynchronize with them.\n#\n# slaveof <masterip> <masterport>\n\n# If the master is password protected (using the \"requirepass\" configuration\n# directive below) it is possible to tell the slave to authenticate before\n# starting the replication synchronization process, otherwise the master will\n# refuse the slave request.\n#\n# masterauth <master-password>\n\n# When a slave loses its connection with the master, or when the replication\n# is still in progress, the slave can act in two different ways:\n#\n# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will\n#    still reply to client requests, possibly with out of date data, or the\n#    data set may just be empty if this is the first synchronization.\n#\n# 2) if slave-serve-stale-data is set to 'no' the slave will reply with\n#    an error \"SYNC with master in progress\" to all the kind of commands\n#    but to INFO and SLAVEOF.\n#\nslave-serve-stale-data yes\n\n# You can configure a slave instance to accept writes or not. Writing against\n# a slave instance may be useful to store some ephemeral data (because data\n# written on a slave will be easily deleted after resync with the master) but\n# may also cause problems if clients are writing to it because of a\n# misconfiguration.\n#\n# Since Redis 2.6 by default slaves are read-only.\n#\n# Note: read only slaves are not designed to be exposed to untrusted clients\n# on the internet. It's just a protection layer against misuse of the instance.\n# Still a read only slave exports by default all the administrative commands\n# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve\n# security of read only slaves using 'rename-command' to shadow all the\n# administrative / dangerous commands.\nslave-read-only yes\n\n# Replication SYNC strategy: disk or socket.\n#\n# -------------------------------------------------------\n# WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY\n# -------------------------------------------------------\n#\n# New slaves and reconnecting slaves that are not able to continue the replication\n# process just receiving differences, need to do what is called a \"full\n# synchronization\". An RDB file is transmitted from the master to the slaves.\n# The transmission can happen in two different ways:\n#\n# 1) Disk-backed: The Redis master creates a new process that writes the RDB\n#                 file on disk. Later the file is transferred by the parent\n#                 process to the slaves incrementally.\n# 2) Diskless: The Redis master creates a new process that directly writes the\n#              RDB file to slave sockets, without touching the disk at all.\n#\n# With disk-backed replication, while the RDB file is generated, more slaves\n# can be queued and served with the RDB file as soon as the current child producing\n# the RDB file finishes its work. With diskless replication instead once\n# the transfer starts, new slaves arriving will be queued and a new transfer\n# will start when the current one terminates.\n#\n# When diskless replication is used, the master waits a configurable amount of\n# time (in seconds) before starting the transfer in the hope that multiple slaves\n# will arrive and the transfer can be parallelized.\n#\n# With slow disks and fast (large bandwidth) networks, diskless replication\n# works better.\nrepl-diskless-sync no\n\n# When diskless replication is enabled, it is possible to configure the delay\n# the server waits in order to spawn the child that transfers the RDB via socket\n# to the slaves.\n#\n# This is important since once the transfer starts, it is not possible to serve\n# new slaves arriving, that will be queued for the next RDB transfer, so the server\n# waits a delay in order to let more slaves arrive.\n#\n# The delay is specified in seconds, and by default is 5 seconds. To disable\n# it entirely just set it to 0 seconds and the transfer will start ASAP.\nrepl-diskless-sync-delay 5\n\n# Slaves send PINGs to server in a predefined interval. It's possible to change\n# this interval with the repl_ping_slave_period option. The default value is 10\n# seconds.\n#\n# repl-ping-slave-period 10\n\n# The following option sets the replication timeout for:\n#\n# 1) Bulk transfer I/O during SYNC, from the point of view of slave.\n# 2) Master timeout from the point of view of slaves (data, pings).\n# 3) Slave timeout from the point of view of masters (REPLCONF ACK pings).\n#\n# It is important to make sure that this value is greater than the value\n# specified for repl-ping-slave-period otherwise a timeout will be detected\n# every time there is low traffic between the master and the slave.\n#\n# repl-timeout 60\n\n# Disable TCP_NODELAY on the slave socket after SYNC?\n#\n# If you select \"yes\" Redis will use a smaller number of TCP packets and\n# less bandwidth to send data to slaves. But this can add a delay for\n# the data to appear on the slave side, up to 40 milliseconds with\n# Linux kernels using a default configuration.\n#\n# If you select \"no\" the delay for data to appear on the slave side will\n# be reduced but more bandwidth will be used for replication.\n#\n# By default we optimize for low latency, but in very high traffic conditions\n# or when the master and slaves are many hops away, turning this to \"yes\" may\n# be a good idea.\nrepl-disable-tcp-nodelay no\n\n# Set the replication backlog size. The backlog is a buffer that accumulates\n# slave data when slaves are disconnected for some time, so that when a slave\n# wants to reconnect again, often a full resync is not needed, but a partial\n# resync is enough, just passing the portion of data the slave missed while\n# disconnected.\n#\n# The bigger the replication backlog, the longer the time the slave can be\n# disconnected and later be able to perform a partial resynchronization.\n#\n# The backlog is only allocated once there is at least a slave connected.\n#\n# repl-backlog-size 1mb\n\n# After a master has no longer connected slaves for some time, the backlog\n# will be freed. The following option configures the amount of seconds that\n# need to elapse, starting from the time the last slave disconnected, for\n# the backlog buffer to be freed.\n#\n# Note that slaves never free the backlog for timeout, since they may be\n# promoted to masters later, and should be able to correctly \"partially\n# resynchronize\" with the slaves: hence they should always accumulate backlog.\n#\n# A value of 0 means to never release the backlog.\n#\n# repl-backlog-ttl 3600\n\n# The slave priority is an integer number published by Redis in the INFO output.\n# It is used by Redis Sentinel in order to select a slave to promote into a\n# master if the master is no longer working correctly.\n#\n# A slave with a low priority number is considered better for promotion, so\n# for instance if there are three slaves with priority 10, 100, 25 Sentinel will\n# pick the one with priority 10, that is the lowest.\n#\n# However a special priority of 0 marks the slave as not able to perform the\n# role of master, so a slave with priority of 0 will never be selected by\n# Redis Sentinel for promotion.\n#\n# By default the priority is 100.\nslave-priority 100\n\n# It is possible for a master to stop accepting writes if there are less than\n# N slaves connected, having a lag less or equal than M seconds.\n#\n# The N slaves need to be in \"online\" state.\n#\n# The lag in seconds, that must be <= the specified value, is calculated from\n# the last ping received from the slave, that is usually sent every second.\n#\n# This option does not GUARANTEE that N replicas will accept the write, but\n# will limit the window of exposure for lost writes in case not enough slaves\n# are available, to the specified number of seconds.\n#\n# For example to require at least 3 slaves with a lag <= 10 seconds use:\n#\n# min-slaves-to-write 3\n# min-slaves-max-lag 10\n#\n# Setting one or the other to 0 disables the feature.\n#\n# By default min-slaves-to-write is set to 0 (feature disabled) and\n# min-slaves-max-lag is set to 10.\n\n# A Redis master is able to list the address and port of the attached\n# slaves in different ways. For example the \"INFO replication\" section\n# offers this information, which is used, among other tools, by\n# Redis Sentinel in order to discover slave instances.\n# Another place where this info is available is in the output of the\n# \"ROLE\" command of a master.\n#\n# The listed IP and address normally reported by a slave is obtained\n# in the following way:\n#\n#   IP: The address is auto detected by checking the peer address\n#   of the socket used by the slave to connect with the master.\n#\n#   Port: The port is communicated by the slave during the replication\n#   handshake, and is normally the port that the slave is using to\n#   list for connections.\n#\n# However when port forwarding or Network Address Translation (NAT) is\n# used, the slave may be actually reachable via different IP and port\n# pairs. The following two options can be used by a slave in order to\n# report to its master a specific set of IP and port, so that both INFO\n# and ROLE will report those values.\n#\n# There is no need to use both the options if you need to override just\n# the port or the IP address.\n#\n# slave-announce-ip 5.5.5.5\n# slave-announce-port 1234\n\n################################## SECURITY ###################################\n\n# Require clients to issue AUTH <PASSWORD> before processing any other\n# commands.  This might be useful in environments in which you do not trust\n# others with access to the host running redis-server.\n#\n# This should stay commented out for backward compatibility and because most\n# people do not need auth (e.g. they run their own servers).\n#\n# Warning: since Redis is pretty fast an outside user can try up to\n# 150k passwords per second against a good box. This means that you should\n# use a very strong password otherwise it will be very easy to break.\n#\n# requirepass foobared\n\n# Command renaming.\n#\n# It is possible to change the name of dangerous commands in a shared\n# environment. For instance the CONFIG command may be renamed into something\n# hard to guess so that it will still be available for internal-use tools\n# but not available for general clients.\n#\n# Example:\n#\n# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52\n#\n# It is also possible to completely kill a command by renaming it into\n# an empty string:\n#\n# rename-command CONFIG \"\"\n#\n# Please note that changing the name of commands that are logged into the\n# AOF file or transmitted to slaves may cause problems.\n\n################################### CLIENTS ####################################\n\n# Set the max number of connected clients at the same time. By default\n# this limit is set to 10000 clients, however if the Redis server is not\n# able to configure the process file limit to allow for the specified limit\n# the max number of allowed clients is set to the current file limit\n# minus 32 (as Redis reserves a few file descriptors for internal uses).\n#\n# Once the limit is reached Redis will close all the new connections sending\n# an error 'max number of clients reached'.\n#\n# maxclients 10000\n\n############################## MEMORY MANAGEMENT ################################\n\n# Set a memory usage limit to the specified amount of bytes.\n# When the memory limit is reached Redis will try to remove keys\n# according to the eviction policy selected (see maxmemory-policy).\n#\n# If Redis can't remove keys according to the policy, or if the policy is\n# set to 'noeviction', Redis will start to reply with errors to commands\n# that would use more memory, like SET, LPUSH, and so on, and will continue\n# to reply to read-only commands like GET.\n#\n# This option is usually useful when using Redis as an LRU or LFU cache, or to\n# set a hard memory limit for an instance (using the 'noeviction' policy).\n#\n# WARNING: If you have slaves attached to an instance with maxmemory on,\n# the size of the output buffers needed to feed the slaves are subtracted\n# from the used memory count, so that network problems / resyncs will\n# not trigger a loop where keys are evicted, and in turn the output\n# buffer of slaves is full with DELs of keys evicted triggering the deletion\n# of more keys, and so forth until the database is completely emptied.\n#\n# In short... if you have slaves attached it is suggested that you set a lower\n# limit for maxmemory so that there is some free RAM on the system for slave\n# output buffers (but this is not needed if the policy is 'noeviction').\n#\n# maxmemory <bytes>\n\n# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory\n# is reached. You can select among five behaviors:\n#\n# volatile-lru -> Evict using approximated LRU among the keys with an expire set.\n# allkeys-lru -> Evict any key using approximated LRU.\n# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.\n# allkeys-lfu -> Evict any key using approximated LFU.\n# volatile-random -> Remove a random key among the ones with an expire set.\n# allkeys-random -> Remove a random key, any key.\n# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)\n# noeviction -> Don't evict anything, just return an error on write operations.\n#\n# LRU means Least Recently Used\n# LFU means Least Frequently Used\n#\n# Both LRU, LFU and volatile-ttl are implemented using approximated\n# randomized algorithms.\n#\n# Note: with any of the above policies, Redis will return an error on write\n#       operations, when there are no suitable keys for eviction.\n#\n#       At the date of writing these commands are: set setnx setex append\n#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd\n#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby\n#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby\n#       getset mset msetnx exec sort\n#\n# The default is:\n#\n# maxmemory-policy noeviction\n\n# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated\n# algorithms (in order to save memory), so you can tune it for speed or\n# accuracy. For default Redis will check five keys and pick the one that was\n# used less recently, you can change the sample size using the following\n# configuration directive.\n#\n# The default of 5 produces good enough results. 10 Approximates very closely\n# true LRU but costs more CPU. 3 is faster but not very accurate.\n#\n# maxmemory-samples 5\n\n############################# LAZY FREEING ####################################\n\n# Redis has two primitives to delete keys. One is called DEL and is a blocking\n# deletion of the object. It means that the server stops processing new commands\n# in order to reclaim all the memory associated with an object in a synchronous\n# way. If the key deleted is associated with a small object, the time needed\n# in order to execute the DEL command is very small and comparable to most other\n# O(1) or O(log_N) commands in Redis. However if the key is associated with an\n# aggregated value containing millions of elements, the server can block for\n# a long time (even seconds) in order to complete the operation.\n#\n# For the above reasons Redis also offers non blocking deletion primitives\n# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and\n# FLUSHDB commands, in order to reclaim memory in background. Those commands\n# are executed in constant time. Another thread will incrementally free the\n# object in the background as fast as possible.\n#\n# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.\n# It's up to the design of the application to understand when it is a good\n# idea to use one or the other. However the Redis server sometimes has to\n# delete keys or flush the whole database as a side effect of other operations.\n# Specifically Redis deletes objects independently of a user call in the\n# following scenarios:\n#\n# 1) On eviction, because of the maxmemory and maxmemory policy configurations,\n#    in order to make room for new data, without going over the specified\n#    memory limit.\n# 2) Because of expire: when a key with an associated time to live (see the\n#    EXPIRE command) must be deleted from memory.\n# 3) Because of a side effect of a command that stores data on a key that may\n#    already exist. For example the RENAME command may delete the old key\n#    content when it is replaced with another one. Similarly SUNIONSTORE\n#    or SORT with STORE option may delete existing keys. The SET command\n#    itself removes any old content of the specified key in order to replace\n#    it with the specified string.\n# 4) During replication, when a slave performs a full resynchronization with\n#    its master, the content of the whole database is removed in order to\n#    load the RDB file just transferred.\n#\n# In all the above cases the default is to delete objects in a blocking way,\n# like if DEL was called. However you can configure each case specifically\n# in order to instead release memory in a non-blocking way like if UNLINK\n# was called, using the following configuration directives:\n\nlazyfree-lazy-eviction no\nlazyfree-lazy-expire no\nlazyfree-lazy-server-del no\nslave-lazy-flush no\n\n############################## APPEND ONLY MODE ###############################\n\n# By default Redis asynchronously dumps the dataset on disk. This mode is\n# good enough in many applications, but an issue with the Redis process or\n# a power outage may result into a few minutes of writes lost (depending on\n# the configured save points).\n#\n# The Append Only File is an alternative persistence mode that provides\n# much better durability. For instance using the default data fsync policy\n# (see later in the config file) Redis can lose just one second of writes in a\n# dramatic event like a server power outage, or a single write if something\n# wrong with the Redis process itself happens, but the operating system is\n# still running correctly.\n#\n# AOF and RDB persistence can be enabled at the same time without problems.\n# If the AOF is enabled on startup Redis will load the AOF, that is the file\n# with the better durability guarantees.\n#\n# Please check http://redis.io/topics/persistence for more information.\n\nappendonly no\n\n# The name of the append only file (default: \"appendonly.aof\")\n\nappendfilename \"appendonly.aof\"\n\n# The fsync() call tells the Operating System to actually write data on disk\n# instead of waiting for more data in the output buffer. Some OS will really flush\n# data on disk, some other OS will just try to do it ASAP.\n#\n# Redis supports three different modes:\n#\n# no: don't fsync, just let the OS flush the data when it wants. Faster.\n# always: fsync after every write to the append only log. Slow, Safest.\n# everysec: fsync only one time every second. Compromise.\n#\n# The default is \"everysec\", as that's usually the right compromise between\n# speed and data safety. It's up to you to understand if you can relax this to\n# \"no\" that will let the operating system flush the output buffer when\n# it wants, for better performances (but if you can live with the idea of\n# some data loss consider the default persistence mode that's snapshotting),\n# or on the contrary, use \"always\" that's very slow but a bit safer than\n# everysec.\n#\n# More details please check the following article:\n# http://antirez.com/post/redis-persistence-demystified.html\n#\n# If unsure, use \"everysec\".\n\n# appendfsync always\nappendfsync everysec\n# appendfsync no\n\n# When the AOF fsync policy is set to always or everysec, and a background\n# saving process (a background save or AOF log background rewriting) is\n# performing a lot of I/O against the disk, in some Linux configurations\n# Redis may block too long on the fsync() call. Note that there is no fix for\n# this currently, as even performing fsync in a different thread will block\n# our synchronous write(2) call.\n#\n# In order to mitigate this problem it's possible to use the following option\n# that will prevent fsync() from being called in the main process while a\n# BGSAVE or BGREWRITEAOF is in progress.\n#\n# This means that while another child is saving, the durability of Redis is\n# the same as \"appendfsync none\". In practical terms, this means that it is\n# possible to lose up to 30 seconds of log in the worst scenario (with the\n# default Linux settings).\n#\n# If you have latency problems turn this to \"yes\". Otherwise leave it as\n# \"no\" that is the safest pick from the point of view of durability.\n\nno-appendfsync-on-rewrite no\n\n# Automatic rewrite of the append only file.\n# Redis is able to automatically rewrite the log file implicitly calling\n# BGREWRITEAOF when the AOF log size grows by the specified percentage.\n#\n# This is how it works: Redis remembers the size of the AOF file after the\n# latest rewrite (if no rewrite has happened since the restart, the size of\n# the AOF at startup is used).\n#\n# This base size is compared to the current size. If the current size is\n# bigger than the specified percentage, the rewrite is triggered. Also\n# you need to specify a minimal size for the AOF file to be rewritten, this\n# is useful to avoid rewriting the AOF file even if the percentage increase\n# is reached but it is still pretty small.\n#\n# Specify a percentage of zero in order to disable the automatic AOF\n# rewrite feature.\n\nauto-aof-rewrite-percentage 100\nauto-aof-rewrite-min-size 64mb\n\n# An AOF file may be found to be truncated at the end during the Redis\n# startup process, when the AOF data gets loaded back into memory.\n# This may happen when the system where Redis is running\n# crashes, especially when an ext4 filesystem is mounted without the\n# data=ordered option (however this can't happen when Redis itself\n# crashes or aborts but the operating system still works correctly).\n#\n# Redis can either exit with an error when this happens, or load as much\n# data as possible (the default now) and start if the AOF file is found\n# to be truncated at the end. The following option controls this behavior.\n#\n# If aof-load-truncated is set to yes, a truncated AOF file is loaded and\n# the Redis server starts emitting a log to inform the user of the event.\n# Otherwise if the option is set to no, the server aborts with an error\n# and refuses to start. When the option is set to no, the user requires\n# to fix the AOF file using the \"redis-check-aof\" utility before to restart\n# the server.\n#\n# Note that if the AOF file will be found to be corrupted in the middle\n# the server will still exit with an error. This option only applies when\n# Redis will try to read more data from the AOF file but not enough bytes\n# will be found.\naof-load-truncated yes\n\n# When rewriting the AOF file, Redis is able to use an RDB preamble in the\n# AOF file for faster rewrites and recoveries. When this option is turned\n# on the rewritten AOF file is composed of two different stanzas:\n#\n#   [RDB file][AOF tail]\n#\n# When loading Redis recognizes that the AOF file starts with the \"REDIS\"\n# string and loads the prefixed RDB file, and continues loading the AOF\n# tail.\n#\n# This is currently turned off by default in order to avoid the surprise\n# of a format change, but will at some point be used as the default.\naof-use-rdb-preamble no\n\n################################ LUA SCRIPTING  ###############################\n\n# Max execution time of a Lua script in milliseconds.\n#\n# If the maximum execution time is reached Redis will log that a script is\n# still in execution after the maximum allowed time and will start to\n# reply to queries with an error.\n#\n# When a long running script exceeds the maximum execution time only the\n# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be\n# used to stop a script that did not yet called write commands. The second\n# is the only way to shut down the server in the case a write command was\n# already issued by the script but the user doesn't want to wait for the natural\n# termination of the script.\n#\n# Set it to 0 or a negative value for unlimited execution without warnings.\nlua-time-limit 5000\n\n################################ REDIS CLUSTER  ###############################\n#\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n# WARNING EXPERIMENTAL: Redis Cluster is considered to be stable code, however\n# in order to mark it as \"mature\" we need to wait for a non trivial percentage\n# of users to deploy it in production.\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n#\n# Normal Redis instances can't be part of a Redis Cluster; only nodes that are\n# started as cluster nodes can. In order to start a Redis instance as a\n# cluster node enable the cluster support uncommenting the following:\n#\n# cluster-enabled yes\n\n# Every cluster node has a cluster configuration file. This file is not\n# intended to be edited by hand. It is created and updated by Redis nodes.\n# Every Redis Cluster node requires a different cluster configuration file.\n# Make sure that instances running in the same system do not have\n# overlapping cluster configuration file names.\n#\n# cluster-config-file nodes-6379.conf\n\n# Cluster node timeout is the amount of milliseconds a node must be unreachable\n# for it to be considered in failure state.\n# Most other internal time limits are multiple of the node timeout.\n#\n# cluster-node-timeout 15000\n\n# A slave of a failing master will avoid to start a failover if its data\n# looks too old.\n#\n# There is no simple way for a slave to actually have an exact measure of\n# its \"data age\", so the following two checks are performed:\n#\n# 1) If there are multiple slaves able to failover, they exchange messages\n#    in order to try to give an advantage to the slave with the best\n#    replication offset (more data from the master processed).\n#    Slaves will try to get their rank by offset, and apply to the start\n#    of the failover a delay proportional to their rank.\n#\n# 2) Every single slave computes the time of the last interaction with\n#    its master. This can be the last ping or command received (if the master\n#    is still in the \"connected\" state), or the time that elapsed since the\n#    disconnection with the master (if the replication link is currently down).\n#    If the last interaction is too old, the slave will not try to failover\n#    at all.\n#\n# The point \"2\" can be tuned by user. Specifically a slave will not perform\n# the failover if, since the last interaction with the master, the time\n# elapsed is greater than:\n#\n#   (node-timeout * slave-validity-factor) + repl-ping-slave-period\n#\n# So for example if node-timeout is 30 seconds, and the slave-validity-factor\n# is 10, and assuming a default repl-ping-slave-period of 10 seconds, the\n# slave will not try to failover if it was not able to talk with the master\n# for longer than 310 seconds.\n#\n# A large slave-validity-factor may allow slaves with too old data to failover\n# a master, while a too small value may prevent the cluster from being able to\n# elect a slave at all.\n#\n# For maximum availability, it is possible to set the slave-validity-factor\n# to a value of 0, which means, that slaves will always try to failover the\n# master regardless of the last time they interacted with the master.\n# (However they'll always try to apply a delay proportional to their\n# offset rank).\n#\n# Zero is the only value able to guarantee that when all the partitions heal\n# the cluster will always be able to continue.\n#\n# cluster-slave-validity-factor 10\n\n# Cluster slaves are able to migrate to orphaned masters, that are masters\n# that are left without working slaves. This improves the cluster ability\n# to resist to failures as otherwise an orphaned master can't be failed over\n# in case of failure if it has no working slaves.\n#\n# Slaves migrate to orphaned masters only if there are still at least a\n# given number of other working slaves for their old master. This number\n# is the \"migration barrier\". A migration barrier of 1 means that a slave\n# will migrate only if there is at least 1 other working slave for its master\n# and so forth. It usually reflects the number of slaves you want for every\n# master in your cluster.\n#\n# Default is 1 (slaves migrate only if their masters remain with at least\n# one slave). To disable migration just set it to a very large value.\n# A value of 0 can be set but is useful only for debugging and dangerous\n# in production.\n#\n# cluster-migration-barrier 1\n\n# By default Redis Cluster nodes stop accepting queries if they detect there\n# is at least an hash slot uncovered (no available node is serving it).\n# This way if the cluster is partially down (for example a range of hash slots\n# are no longer covered) all the cluster becomes, eventually, unavailable.\n# It automatically returns available as soon as all the slots are covered again.\n#\n# However sometimes you want the subset of the cluster which is working,\n# to continue to accept queries for the part of the key space that is still\n# covered. In order to do so, just set the cluster-require-full-coverage\n# option to no.\n#\n# cluster-require-full-coverage yes\n\n# This option, when set to yes, prevents slaves from trying to failover its\n# master during master failures. However the master can still perform a\n# manual failover, if forced to do so.\n#\n# This is useful in different scenarios, especially in the case of multiple\n# data center operations, where we want one side to never be promoted if not\n# in the case of a total DC failure.\n#\n# cluster-slave-no-failover no\n\n# In order to setup your cluster make sure to read the documentation\n# available at http://redis.io web site.\n\n########################## CLUSTER DOCKER/NAT support  ########################\n\n# In certain deployments, Redis Cluster nodes address discovery fails, because\n# addresses are NAT-ted or because ports are forwarded (the typical case is\n# Docker and other containers).\n#\n# In order to make Redis Cluster working in such environments, a static\n# configuration where each node knows its public address is needed. The\n# following two options are used for this scope, and are:\n#\n# * cluster-announce-ip\n# * cluster-announce-port\n# * cluster-announce-bus-port\n#\n# Each instruct the node about its address, client port, and cluster message\n# bus port. The information is then published in the header of the bus packets\n# so that other nodes will be able to correctly map the address of the node\n# publishing the information.\n#\n# If the above options are not used, the normal Redis Cluster auto-detection\n# will be used instead.\n#\n# Note that when remapped, the bus port may not be at the fixed offset of\n# clients port + 10000, so you can specify any port and bus-port depending\n# on how they get remapped. If the bus-port is not set, a fixed offset of\n# 10000 will be used as usually.\n#\n# Example:\n#\n# cluster-announce-ip 10.1.1.5\n# cluster-announce-port 6379\n# cluster-announce-bus-port 6380\n\n################################## SLOW LOG ###################################\n\n# The Redis Slow Log is a system to log queries that exceeded a specified\n# execution time. The execution time does not include the I/O operations\n# like talking with the client, sending the reply and so forth,\n# but just the time needed to actually execute the command (this is the only\n# stage of command execution where the thread is blocked and can not serve\n# other requests in the meantime).\n#\n# You can configure the slow log with two parameters: one tells Redis\n# what is the execution time, in microseconds, to exceed in order for the\n# command to get logged, and the other parameter is the length of the\n# slow log. When a new command is logged the oldest one is removed from the\n# queue of logged commands.\n\n# The following time is expressed in microseconds, so 1000000 is equivalent\n# to one second. Note that a negative number disables the slow log, while\n# a value of zero forces the logging of every command.\nslowlog-log-slower-than 10000\n\n# There is no limit to this length. Just be aware that it will consume memory.\n# You can reclaim memory used by the slow log with SLOWLOG RESET.\nslowlog-max-len 128\n\n################################ LATENCY MONITOR ##############################\n\n# The Redis latency monitoring subsystem samples different operations\n# at runtime in order to collect data related to possible sources of\n# latency of a Redis instance.\n#\n# Via the LATENCY command this information is available to the user that can\n# print graphs and obtain reports.\n#\n# The system only logs operations that were performed in a time equal or\n# greater than the amount of milliseconds specified via the\n# latency-monitor-threshold configuration directive. When its value is set\n# to zero, the latency monitor is turned off.\n#\n# By default latency monitoring is disabled since it is mostly not needed\n# if you don't have latency issues, and collecting data has a performance\n# impact, that while very small, can be measured under big load. Latency\n# monitoring can easily be enabled at runtime using the command\n# \"CONFIG SET latency-monitor-threshold <milliseconds>\" if needed.\nlatency-monitor-threshold 0\n\n############################# EVENT NOTIFICATION ##############################\n\n# Redis can notify Pub/Sub clients about events happening in the key space.\n# This feature is documented at http://redis.io/topics/notifications\n#\n# For instance if keyspace events notification is enabled, and a client\n# performs a DEL operation on key \"foo\" stored in the Database 0, two\n# messages will be published via Pub/Sub:\n#\n# PUBLISH __keyspace@0__:foo del\n# PUBLISH __keyevent@0__:del foo\n#\n# It is possible to select the events that Redis will notify among a set\n# of classes. Every class is identified by a single character:\n#\n#  K     Keyspace events, published with __keyspace@<db>__ prefix.\n#  E     Keyevent events, published with __keyevent@<db>__ prefix.\n#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...\n#  $     String commands\n#  l     List commands\n#  s     Set commands\n#  h     Hash commands\n#  z     Sorted set commands\n#  x     Expired events (events generated every time a key expires)\n#  e     Evicted events (events generated when a key is evicted for maxmemory)\n#  A     Alias for g$lshzxe, so that the \"AKE\" string means all the events.\n#\n#  The \"notify-keyspace-events\" takes as argument a string that is composed\n#  of zero or multiple characters. The empty string means that notifications\n#  are disabled.\n#\n#  Example: to enable list and generic events, from the point of view of the\n#           event name, use:\n#\n#  notify-keyspace-events Elg\n#\n#  Example 2: to get the stream of the expired keys subscribing to channel\n#             name __keyevent@0__:expired use:\n#\n#  notify-keyspace-events Ex\n#\n#  By default all notifications are disabled because most users don't need\n#  this feature and the feature has some overhead. Note that if you don't\n#  specify at least one of K or E, no events will be delivered.\nnotify-keyspace-events \"\"\n\n############################### ADVANCED CONFIG ###############################\n\n# Hashes are encoded using a memory efficient data structure when they have a\n# small number of entries, and the biggest entry does not exceed a given\n# threshold. These thresholds can be configured using the following directives.\nhash-max-ziplist-entries 512\nhash-max-ziplist-value 64\n\n# Lists are also encoded in a special way to save a lot of space.\n# The number of entries allowed per internal list node can be specified\n# as a fixed maximum size or a maximum number of elements.\n# For a fixed maximum size, use -5 through -1, meaning:\n# -5: max size: 64 Kb  <-- not recommended for normal workloads\n# -4: max size: 32 Kb  <-- not recommended\n# -3: max size: 16 Kb  <-- probably not recommended\n# -2: max size: 8 Kb   <-- good\n# -1: max size: 4 Kb   <-- good\n# Positive numbers mean store up to _exactly_ that number of elements\n# per list node.\n# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),\n# but if your use case is unique, adjust the settings as necessary.\nlist-max-ziplist-size -2\n\n# Lists may also be compressed.\n# Compress depth is the number of quicklist ziplist nodes from *each* side of\n# the list to *exclude* from compression.  The head and tail of the list\n# are always uncompressed for fast push/pop operations.  Settings are:\n# 0: disable all list compression\n# 1: depth 1 means \"don't start compressing until after 1 node into the list,\n#    going from either the head or tail\"\n#    So: [head]->node->node->...->node->[tail]\n#    [head], [tail] will always be uncompressed; inner nodes will compress.\n# 2: [head]->[next]->node->node->...->node->[prev]->[tail]\n#    2 here means: don't compress head or head->next or tail->prev or tail,\n#    but compress all nodes between them.\n# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]\n# etc.\nlist-compress-depth 0\n\n# Sets have a special encoding in just one case: when a set is composed\n# of just strings that happen to be integers in radix 10 in the range\n# of 64 bit signed integers.\n# The following configuration setting sets the limit in the size of the\n# set in order to use this special memory saving encoding.\nset-max-intset-entries 512\n\n# Similarly to hashes and lists, sorted sets are also specially encoded in\n# order to save a lot of space. This encoding is only used when the length and\n# elements of a sorted set are below the following limits:\nzset-max-ziplist-entries 128\nzset-max-ziplist-value 64\n\n# HyperLogLog sparse representation bytes limit. The limit includes the\n# 16 bytes header. When an HyperLogLog using the sparse representation crosses\n# this limit, it is converted into the dense representation.\n#\n# A value greater than 16000 is totally useless, since at that point the\n# dense representation is more memory efficient.\n#\n# The suggested value is ~ 3000 in order to have the benefits of\n# the space efficient encoding without slowing down too much PFADD,\n# which is O(N) with the sparse encoding. The value can be raised to\n# ~ 10000 when CPU is not a concern, but space is, and the data set is\n# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.\nhll-sparse-max-bytes 3000\n\n# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in\n# order to help rehashing the main Redis hash table (the one mapping top-level\n# keys to values). The hash table implementation Redis uses (see dict.c)\n# performs a lazy rehashing: the more operation you run into a hash table\n# that is rehashing, the more rehashing \"steps\" are performed, so if the\n# server is idle the rehashing is never complete and some more memory is used\n# by the hash table.\n#\n# The default is to use this millisecond 10 times every second in order to\n# actively rehash the main dictionaries, freeing memory when possible.\n#\n# If unsure:\n# use \"activerehashing no\" if you have hard latency requirements and it is\n# not a good thing in your environment that Redis can reply from time to time\n# to queries with 2 milliseconds delay.\n#\n# use \"activerehashing yes\" if you don't have such hard requirements but\n# want to free memory asap when possible.\nactiverehashing yes\n\n# The client output buffer limits can be used to force disconnection of clients\n# that are not reading data from the server fast enough for some reason (a\n# common reason is that a Pub/Sub client can't consume messages as fast as the\n# publisher can produce them).\n#\n# The limit can be set differently for the three different classes of clients:\n#\n# normal -> normal clients including MONITOR clients\n# slave  -> slave clients\n# pubsub -> clients subscribed to at least one pubsub channel or pattern\n#\n# The syntax of every client-output-buffer-limit directive is the following:\n#\n# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>\n#\n# A client is immediately disconnected once the hard limit is reached, or if\n# the soft limit is reached and remains reached for the specified number of\n# seconds (continuously).\n# So for instance if the hard limit is 32 megabytes and the soft limit is\n# 16 megabytes / 10 seconds, the client will get disconnected immediately\n# if the size of the output buffers reach 32 megabytes, but will also get\n# disconnected if the client reaches 16 megabytes and continuously overcomes\n# the limit for 10 seconds.\n#\n# By default normal clients are not limited because they don't receive data\n# without asking (in a push way), but just after a request, so only\n# asynchronous clients may create a scenario where data is requested faster\n# than it can read.\n#\n# Instead there is a default limit for pubsub and slave clients, since\n# subscribers and slaves receive data in a push fashion.\n#\n# Both the hard or the soft limit can be disabled by setting them to zero.\nclient-output-buffer-limit normal 0 0 0\nclient-output-buffer-limit slave 256mb 64mb 60\nclient-output-buffer-limit pubsub 32mb 8mb 60\n\n# Client query buffers accumulate new commands. They are limited to a fixed\n# amount by default in order to avoid that a protocol desynchronization (for\n# instance due to a bug in the client) will lead to unbound memory usage in\n# the query buffer. However you can configure it here if you have very special\n# needs, such us huge multi/exec requests or alike.\n#\n# client-query-buffer-limit 1gb\n\n# In the Redis protocol, bulk requests, that are, elements representing single\n# strings, are normally limited to 512 mb. However you can change this limit\n# here.\n#\n# proto-max-bulk-len 512mb\n\n# Redis calls an internal function to perform many background tasks, like\n# closing connections of clients in timeout, purging expired keys that are\n# never requested, and so forth.\n#\n# Not all tasks are performed with the same frequency, but Redis checks for\n# tasks to perform according to the specified \"hz\" value.\n#\n# By default \"hz\" is set to 10. Raising the value will use more CPU when\n# Redis is idle, but at the same time will make Redis more responsive when\n# there are many keys expiring at the same time, and timeouts may be\n# handled with more precision.\n#\n# The range is between 1 and 500, however a value over 100 is usually not\n# a good idea. Most users should use the default of 10 and raise this up to\n# 100 only in environments where very low latency is required.\nhz 10\n\n# When a child rewrites the AOF file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\naof-rewrite-incremental-fsync yes\n\n# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good\n# idea to start with the default settings and only change them after investigating\n# how to improve the performances and how the keys LFU change over time, which\n# is possible to inspect via the OBJECT FREQ command.\n#\n# There are two tunable parameters in the Redis LFU implementation: the\n# counter logarithm factor and the counter decay time. It is important to\n# understand what the two parameters mean before changing them.\n#\n# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis\n# uses a probabilistic increment with logarithmic behavior. Given the value\n# of the old counter, when a key is accessed, the counter is incremented in\n# this way:\n#\n# 1. A random number R between 0 and 1 is extracted.\n# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).\n# 3. The counter is incremented only if R < P.\n#\n# The default lfu-log-factor is 10. This is a table of how the frequency\n# counter changes with a different number of accesses with different\n# logarithmic factors:\n#\n# +--------+------------+------------+------------+------------+------------+\n# | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |\n# +--------+------------+------------+------------+------------+------------+\n# | 0      | 104        | 255        | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 1      | 18         | 49         | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 10     | 10         | 18         | 142        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 100    | 8          | 11         | 49         | 143        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n#\n# NOTE: The above table was obtained by running the following commands:\n#\n#   redis-benchmark -n 1000000 incr foo\n#   redis-cli object freq foo\n#\n# NOTE 2: The counter initial value is 5 in order to give new objects a chance\n# to accumulate hits.\n#\n# The counter decay time is the time, in minutes, that must elapse in order\n# for the key counter to be divided by two (or decremented if it has a value\n# less <= 10).\n#\n# The default value for the lfu-decay-time is 1. A Special value of 0 means to\n# decay the counter every time it happens to be scanned.\n#\n# lfu-log-factor 10\n# lfu-decay-time 1\n\n########################### ACTIVE DEFRAGMENTATION #######################\n#\n# WARNING THIS FEATURE IS EXPERIMENTAL. However it was stress tested\n# even in production and manually tested by multiple engineers for some\n# time.\n#\n# What is active defragmentation?\n# -------------------------------\n#\n# Active (online) defragmentation allows a Redis server to compact the\n# spaces left between small allocations and deallocations of data in memory,\n# thus allowing to reclaim back memory.\n#\n# Fragmentation is a natural process that happens with every allocator (but\n# less so with Jemalloc, fortunately) and certain workloads. Normally a server\n# restart is needed in order to lower the fragmentation, or at least to flush\n# away all the data and create it again. However thanks to this feature\n# implemented by Oran Agra for Redis 4.0 this process can happen at runtime\n# in an \"hot\" way, while the server is running.\n#\n# Basically when the fragmentation is over a certain level (see the\n# configuration options below) Redis will start to create new copies of the\n# values in contiguous memory regions by exploiting certain specific Jemalloc\n# features (in order to understand if an allocation is causing fragmentation\n# and to allocate it in a better place), and at the same time, will release the\n# old copies of the data. This process, repeated incrementally for all the keys\n# will cause the fragmentation to drop back to normal values.\n#\n# Important things to understand:\n#\n# 1. This feature is disabled by default, and only works if you compiled Redis\n#    to use the copy of Jemalloc we ship with the source code of Redis.\n#    This is the default with Linux builds.\n#\n# 2. You never need to enable this feature if you don't have fragmentation\n#    issues.\n#\n# 3. Once you experience fragmentation, you can enable this feature when\n#    needed with the command \"CONFIG SET activedefrag yes\".\n#\n# The configuration parameters are able to fine tune the behavior of the\n# defragmentation process. If you are not sure about what they mean it is\n# a good idea to leave the defaults untouched.\n\n# Enabled active defragmentation\n# activedefrag yes\n\n# Minimum amount of fragmentation waste to start active defrag\n# active-defrag-ignore-bytes 100mb\n\n# Minimum percentage of fragmentation to start active defrag\n# active-defrag-threshold-lower 10\n\n# Maximum percentage of fragmentation at which we use maximum effort\n# active-defrag-threshold-upper 100\n\n# Minimal effort for defrag in CPU percentage\n# active-defrag-cycle-min 25\n\n# Maximal effort for defrag in CPU percentage\n# active-defrag-cycle-max 75\n"
  },
  {
    "path": "examples/stacks/laravel/devbox.json",
    "content": "{\n  \"packages\": [\n    \"php81Packages.composer@latest\",\n    \"php81Extensions.xdebug@latest\",\n    \"php@8.1\",\n    \"nodejs@18\",\n    \"mariadb@latest\",\n    \"redis@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": [],\n    \"scripts\": {\n      \"create-project\": [\n        \"composer create-project laravel/laravel tmp\",\n        \"mv tmp/* tmp/.* .\"\n      ],\n      \"db:create\": \"mysql -u root -e 'CREATE DATABASE laravel;'\",\n      \"db:migrate\": \"php artisan migrate\",\n      \"db:mysql\": \"mysql -u root -D laravel_test\",\n      \"serve:dev\": \"php artisan serve\"\n    }\n  }\n}"
  },
  {
    "path": "examples/stacks/lepp-stack/.gitignore",
    "content": "conf/mysql/data\n*.log\n*.pid\n*.sock\n*.swp"
  },
  {
    "path": "examples/stacks/lepp-stack/README.md",
    "content": "# LEPP Stack\n\nAn example Devbox shell for NGINX, Postgres, and PHP. This example uses Devbox Plugins for all 3 packages to simplify configuration\n\n\n## How to Run\n\n### Initializing\n\nIn this directory, run: `devbox run init_db` to initialize a db.\n\nTo start the Servers + Postgres service, run: `devbox services start`\n\n### Creating the DB\n\nYou can run the creation script using `devbox run create_db`. This will create a Postgres DB based on `setup_postgres_db.sql`.\n\n### Testing the Example\n\nYou can query Nginx on port 80, which will route to the PHP example.\n\n## How to Recreate this Example\n\n1. Create a new project with:\n   ```bash\n   devbox create --template lapp-stack\n   devbox install\n   ```\n\n2. Update `devbox.d/nginx/httpd.conf` to point to the directory with your PHP files. You'll need to update the `root` directive to point to your project folder\n3. Follow the instructions above in the How to Run section to initialize your project.\n\nNote that the `.sock` filepath can only be maximum 100 characters long. You can point to a different path by setting the `PGHOST` env variable in your `devbox.json` as follows:\n\n```\n\"env\": {\n    \"PGHOST\": \"/<some-shorter-path>\"\n}\n```\n\n### Related Docs\n\n* [Using PHP with Devbox](https://www.jetify.com/devbox/docs/devbox_examples/languages/php/)\n* [Using Nginx with Devbox](https://www.jetify.com/devbox/docs/devbox_examples/servers/nginx/)\n* [Using PostgreSQL with Devbox](https://www.jetify.com/devbox/docs/devbox_examples/databases/postgres/)\n"
  },
  {
    "path": "examples/stacks/lepp-stack/devbox.d/nginx/fastcgi.conf",
    "content": "fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;\nfastcgi_param  SERVER_SOFTWARE    nginx;\nfastcgi_param  QUERY_STRING       $query_string;\nfastcgi_param  REQUEST_METHOD     $request_method;\nfastcgi_param  CONTENT_TYPE       $content_type;\nfastcgi_param  CONTENT_LENGTH     $content_length;\nfastcgi_param  SCRIPT_FILENAME    $realpath_root$fastcgi_script_name;\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\nfastcgi_param  REQUEST_URI        $request_uri;\nfastcgi_param  DOCUMENT_URI       $document_uri;\nfastcgi_param  DOCUMENT_ROOT      $document_root;\nfastcgi_param  SERVER_PROTOCOL    $server_protocol;\nfastcgi_param  REMOTE_ADDR        $remote_addr;\nfastcgi_param  REMOTE_PORT        $remote_port;\nfastcgi_param  SERVER_ADDR        $server_addr;\nfastcgi_param  SERVER_PORT        $server_port;\nfastcgi_param  SERVER_NAME        $server_name;\n"
  },
  {
    "path": "examples/stacks/lepp-stack/devbox.d/nginx/nginx.conf",
    "content": "events {}\nhttp{\nserver {\n         listen       8089;\n         listen       [::]:8089;\n         server_name  localhost;\n         root         ../../../my_app;\n\n         error_log error.log error;\n         access_log access.log;\n         client_body_temp_path temp/client_body;\n         proxy_temp_path temp/proxy;\n         fastcgi_temp_path temp/fastcgi;\n         uwsgi_temp_path temp/uwsgi;\n         scgi_temp_path temp/scgi;\n\n         index index.php index.htm index.html;\n\n         location / {\n                      try_files $uri $uri/ /index.php$is_args$args;\n         }\n\n         location ~ \\.php$ {\n            include fastcgi.conf;\n            fastcgi_split_path_info ^(.+\\.php)(/.+)$;\n            fastcgi_pass 127.0.0.1:8082;\n            fastcgi_index index.php;\n        }\n    }\n}\n"
  },
  {
    "path": "examples/stacks/lepp-stack/devbox.d/nginx/nginx.template",
    "content": "events {}\nhttp{\nserver {\n         listen       $NGINX_WEB_PORT;\n         listen       [::]:$NGINX_WEB_PORT;\n         server_name  $NGINX_WEB_SERVER_NAME;\n         root         $NGINX_WEB_ROOT;\n\n         error_log error.log error;\n         access_log access.log;\n         client_body_temp_path temp/client_body;\n         proxy_temp_path temp/proxy;\n         fastcgi_temp_path temp/fastcgi;\n         uwsgi_temp_path temp/uwsgi;\n         scgi_temp_path temp/scgi;\n\n         index index.php index.htm index.html;\n\n         location / {\n                      try_files $uri $uri/ /index.php$is_args$args;\n         }\n\n         location ~ \\.php$ {\n            include fastcgi.conf;\n            fastcgi_split_path_info ^(.+\\.php)(/.+)$;\n            fastcgi_pass 127.0.0.1:$PHPFPM_PORT;\n            fastcgi_index index.php;\n        }\n    }\n}\n"
  },
  {
    "path": "examples/stacks/lepp-stack/devbox.d/php/php-fpm.conf",
    "content": "[global]\npid = ${PHPFPM_PID_FILE}\nerror_log = ${PHPFPM_ERROR_LOG_FILE}\ndaemonize = yes\n\n[www]\n; user = www-data\n; group = www-data\nlisten = 127.0.0.1:${PHPFPM_PORT}\n; listen.owner = www-data\n; listen.group = www-data\npm = dynamic\npm.max_children = 5\npm.start_servers = 2\npm.min_spare_servers = 1\npm.max_spare_servers = 3\nchdir = /\n"
  },
  {
    "path": "examples/stacks/lepp-stack/devbox.d/php/php.ini",
    "content": "[php]\n\n; Put your php.ini directives here. For the latest default php.ini file, see https://github.com/php/php-src/blob/master/php.ini-production\n\n; memory_limit = 128M\n; expose_php = Off\n"
  },
  {
    "path": "examples/stacks/lepp-stack/devbox.d/web/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hello World!</title>\n  </head>\n  <body>\n    Hello World!\n  </body>\n</html>\n"
  },
  {
    "path": "examples/stacks/lepp-stack/devbox.json",
    "content": "{\n  \"packages\": [\n    \"curl@latest\",\n    \"postgresql@latest\",\n    \"php@latest\",\n    \"php83Extensions.pgsql@latest\",\n    \"nginx@latest\"\n  ],\n  \"env\": {\n    \"NGINX_WEB_PORT\": \"8089\",\n    \"NGINX_WEB_ROOT\": \"../../../my_app\",\n    \"PGPORT\": \"5433\",\n    \"PGHOST\": \"/tmp/devbox/lepp\"\n  },\n  \"shell\": {\n    \"scripts\": {\n      \"create_db\": [\n        \"dropdb --if-exists devbox_lepp\",\n        \"createdb devbox_lepp\",\n        \"psql devbox_lepp < setup_postgres_db.sql\"\n      ],\n      \"init_db\": \"initdb\",\n      \"run_test\": [\n        \"mkdir -p /tmp/devbox/lepp\",\n        \"rm -rf .devbox/virtenv/postgresql/data\",\n        \"initdb\",\n        \"devbox services up -b\",\n        \"echo 'sleep 2 seconds for the postgres server to initialize.' && sleep 2\",\n        \"dropdb --if-exists devbox_lepp\",\n        \"createdb devbox_lepp\",\n        \"psql devbox_lepp < setup_postgres_db.sql\",\n        \"curl localhost:$NGINX_WEB_PORT\",\n        \"devbox services stop\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/stacks/lepp-stack/my_app/config.php",
    "content": "<?php\n\t$db_hostname\t= \"127.0.0.1\";\n\t$db_port \t= \"5433\";\n\t$db_database\t= \"devbox_lepp\";\n\t$db_username\t= \"devbox_user\";\n\t$db_password\t= \"password\";\n"
  },
  {
    "path": "examples/stacks/lepp-stack/my_app/index.php",
    "content": "<?php\n\ninclude 'config.php';\n\n$dbconn = pg_connect(\"host=$db_hostname dbname=$db_database user=$db_username password=$db_password port=$db_port\")\n\tor die('Could not connect: ' . pg_last_error());\n\n// Check if the form has been submitted\nif ($_SERVER['REQUEST_METHOD'] == 'POST') {\n\n\t// Get the form data\n\t$first_name = $_POST['first_name'];\n\t$last_name = $_POST['last_name'];\n\t$phone = $_POST['phone'];\n\t$email = $_POST['email'];\n\n\t// Insert the new record into the database\n\t$query = \"INSERT INTO address_book (first_name, last_name, phone, email,) VALUES ('$first_name', '$last_name', '$phone', '$email')\";\n\t$result = pg_query($dbconn, $query);\n\tif (!$result) {\n\t  die(\"Error: \" . pg_last_error($dbconn));\n\t}\n  }\n\n  // Query the database for all records\n  $query = \"SELECT * FROM address_book ORDER BY last_name, first_name\";\n  $result = pg_query($dbconn, $query);\n  if (!$result) {\n\tdie(\"Error: \" . pg_last_error($dbconn));\n  }\n\n  ?>\n\n  <!-- HTML form for adding new records -->\n  <form method=\"post\" action=\"\">\n\t<label for=\"first_name\">First name:</label><br>\n\t<input type=\"text\" id=\"first_name\" name=\"first_name\"><br>\n\t<label for=\"last_name\">Last name:</label><br>\n\t<input type=\"text\" id=\"last_name\" name=\"last_name\"><br>\n\t<label for=\"phone\">Phone:</label><br>\n\t<input type=\"text\" id=\"phone\" name=\"phone\"><br>\n\t<label for=\"email\">Email:</label><br>\n\t<input type=\"text\" id=\"email\" name=\"email\"><br>\n\t<input type=\"submit\" value=\"Submit\">\n  </form>\n\n  <!-- HTML table for displaying records -->\n  <table>\n\t<tr>\n\t  <th>Name</th>\n\t  <th>Phone</th>\n\t  <th>Email</th>\n\t</tr>\n\t<?php while ($row = pg_fetch_array($result)) { ?>\n\t  <tr>\n\t\t<td><?php echo $row['first_name'] . ' ' . $row['last_name']; ?></td>\n\t\t<td><?php echo $row['phone']; ?></td>\n\t\t<td><?php echo $row['email']; ?></td>\n\t  </tr>\n\t<?php } ?>\n  </table>\n\n  <?php\n\n  // Close the database connection\n  pg_close($dbconn);\n\n"
  },
  {
    "path": "examples/stacks/lepp-stack/my_app/info.php",
    "content": "<?php \n   phpinfo();"
  },
  {
    "path": "examples/stacks/lepp-stack/setup_postgres_db.sql",
    "content": "--- You should run this query using psql < setup_db.sql`\n\n\nDO\n$do$\nBEGIN\n   IF EXISTS (SELECT FROM pg_catalog.pg_roles WHERE  rolname = 'devbox_user') THEN\n      RAISE NOTICE 'Role \"my_user\" already exists. Skipping.';\n   ELSE\n      CREATE USER devbox_user WITH PASSWORD 'password';\n   END IF;\nEND\n$do$;\n\nDROP TABLE IF EXISTS address_book;\nCREATE TABLE address_book (\n  id SERIAL PRIMARY KEY,\n  first_name VARCHAR(255) NOT NULL,\n  last_name VARCHAR(255) NOT NULL,\n  phone VARCHAR(255) NOT NULL,\n  email VARCHAR(255) NOT NULL\n);\n\nINSERT INTO address_book (first_name, last_name, phone, email) VALUES ('Jim', 'Hawkins', '555-0100', 'jhawk@jetpack.io'), ('Billy', 'Bones', '555-0102', 'bbones@jetpack.io');\n\nGRANT ALL PRIVILEGES ON address_book TO devbox_user;\n"
  },
  {
    "path": "examples/stacks/rails/.ruby-version",
    "content": "3.1.2\n"
  },
  {
    "path": "examples/stacks/rails/README.md",
    "content": "# Rails Example in Devbox\n\nThis example demonstrates how to setup a simple Rails application. It makes use of the Ruby Plugin, and installs SQLite to use as a database.\n\n\n## How To Run\n\nRun `devbox shell` to install rails and prepare the project.\n\nOnce the shell starts, you can start the rails app by running:\n\n```bash\ncd blog\nbin/rails server\n```\n\n## How to Recreate this Example\n\n1. Create a new Devbox project with `devbox create --template rails`\n2. Add the packages using\n\n   ```bash\n   devbox install\n   ```\n\n3. Run `devbox shell`, which will install the rails CLI with `gem install rails`\n4. Create your Rails app by running the following in your Devbox Shell\n\n   ```bash\n   rails new blog\n   ```\n\n## Related Docs\n\n* [Using Ruby with Devbox](https://www.jetify.com/devbox/docs/devbox_examples/languages/ruby/)\n"
  },
  {
    "path": "examples/stacks/rails/blog/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files for more about ignoring files.\n#\n# If you find yourself ignoring temporary files generated by your text editor\n# or operating system, you probably want to add a global ignore instead:\n#   git config --global core.excludesfile '~/.gitignore_global'\n\n# Ignore bundler config.\n/.bundle\n\n# Ignore the default SQLite database.\n/db/*.sqlite3\n/db/*.sqlite3-*\n\n# Ignore all logfiles and tempfiles.\n/log/*\n/tmp/*\n!/log/.keep\n!/tmp/.keep\n\n# Ignore pidfiles, but keep the directory.\n/tmp/pids/*\n!/tmp/pids/\n!/tmp/pids/.keep\n\n# Ignore uploaded files in development.\n/storage/*\n!/storage/.keep\n/tmp/storage/*\n!/tmp/storage/\n!/tmp/storage/.keep\n\n/public/assets\n\n# Ignore master key for decrypting credentials and more.\n/config/master.key\n"
  },
  {
    "path": "examples/stacks/rails/blog/.ruby-version",
    "content": "ruby-3.1.2\n"
  },
  {
    "path": "examples/stacks/rails/blog/Gemfile",
    "content": "source \"https://rubygems.org\"\ngit_source(:github) { |repo| \"https://github.com/#{repo}.git\" }\n\nruby \"3.3.0\"\n\n# Bundle edge Rails instead: gem \"rails\", github: \"rails/rails\", branch: \"main\"\ngem \"rails\", \"~> 7.1.5\"\n\n# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]\ngem \"sprockets-rails\"\n\n# Use sqlite3 as the database for Active Record\ngem \"sqlite3\", \"~> 1.4\"\n\n# Use the Puma web server [https://github.com/puma/puma]\ngem \"puma\", \"~> 5.6\"\n\n# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]\ngem \"importmap-rails\"\n\n# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]\ngem \"turbo-rails\"\n\n# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]\ngem \"stimulus-rails\"\n\n# Build JSON APIs with ease [https://github.com/rails/jbuilder]\ngem \"jbuilder\"\n\n# Use Redis adapter to run Action Cable in production\n# gem \"redis\", \"~> 4.0\"\n\n# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]\n# gem \"kredis\"\n\n# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]\n# gem \"bcrypt\", \"~> 3.1.7\"\n\n# Windows does not include zoneinfo files, so bundle the tzinfo-data gem\ngem \"tzinfo-data\", '1.2014.5'\n\n# Reduces boot times through caching; required in config/boot.rb\ngem \"bootsnap\", require: false\n\n# Use Sass to process CSS\n# gem \"sassc-rails\"\n\n# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]\n# gem \"image_processing\", \"~> 1.2\"\n\ngroup :development, :test do\n  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem\n  gem \"debug\", platforms: %i[ mri mingw x64_mingw ]\nend\n\ngroup :development do\n  # Use console on exceptions pages [https://github.com/rails/web-console]\n  gem \"web-console\"\n\n  # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]\n  # gem \"rack-mini-profiler\"\n\n  # Speed up commands on slow machines / big apps [https://github.com/rails/spring]\n  # gem \"spring\"\nend\n\ngroup :test do\n  # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]\n  gem \"capybara\"\n  gem \"selenium-webdriver\"\n  gem \"webdrivers\"\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/Rakefile",
    "content": "# Add your own tasks in files placed in lib/tasks ending in .rake,\n# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.\n\nrequire_relative \"config/application\"\n\nRails.application.load_tasks\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/assets/config/manifest.js",
    "content": "//= link_tree ../images\n//= link_directory ../stylesheets .css\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/assets/images/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/app/assets/stylesheets/application.css",
    "content": "/*\n * This is a manifest file that'll be compiled into application.css, which will include all the files\n * listed below.\n *\n * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's\n * vendor/assets/stylesheets directory can be referenced here using a relative path.\n *\n * You're free to add application-wide styles to this file and they'll appear at the bottom of the\n * compiled file so the styles you add here take precedence over styles defined in any other CSS\n * files in this directory. Styles in this file should be added after the last require_* statement.\n * It is generally better to create a new file per style scope.\n *\n *= require_tree .\n *= require_self\n */\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/channels/application_cable/channel.rb",
    "content": "module ApplicationCable\n  class Channel < ActionCable::Channel::Base\n  end\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/channels/application_cable/connection.rb",
    "content": "module ApplicationCable\n  class Connection < ActionCable::Connection::Base\n  end\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/controllers/application_controller.rb",
    "content": "class ApplicationController < ActionController::Base\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/controllers/concerns/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/app/helpers/application_helper.rb",
    "content": "module ApplicationHelper\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/jobs/application_job.rb",
    "content": "class ApplicationJob < ActiveJob::Base\n  # Automatically retry jobs that encountered a deadlock\n  # retry_on ActiveRecord::Deadlocked\n\n  # Most jobs are safe to ignore if the underlying records are no longer available\n  # discard_on ActiveJob::DeserializationError\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/mailers/application_mailer.rb",
    "content": "class ApplicationMailer < ActionMailer::Base\n  default from: \"from@example.com\"\n  layout \"mailer\"\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/models/application_record.rb",
    "content": "class ApplicationRecord < ActiveRecord::Base\n  primary_abstract_class\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/models/concerns/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/app/views/layouts/application.html.erb",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Blog</title>\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n    <%= csrf_meta_tags %>\n    <%= csp_meta_tag %>\n\n    <%= stylesheet_link_tag \"application\", \"data-turbo-track\": \"reload\" %>\n  </head>\n\n  <body>\n    <%= yield %>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/views/layouts/mailer.html.erb",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <style>\n      /* Email styles need to be inline */\n    </style>\n  </head>\n\n  <body>\n    <%= yield %>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/stacks/rails/blog/app/views/layouts/mailer.text.erb",
    "content": "<%= yield %>\n"
  },
  {
    "path": "examples/stacks/rails/blog/bin/bundle",
    "content": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\n#\n# This file was generated by Bundler.\n#\n# The application 'bundle' is installed as part of a gem, and\n# this file is here to facilitate running it.\n#\n\nrequire \"rubygems\"\n\nm = Module.new do\n  module_function\n\n  def invoked_as_script?\n    File.expand_path($0) == File.expand_path(__FILE__)\n  end\n\n  def env_var_version\n    ENV[\"BUNDLER_VERSION\"]\n  end\n\n  def cli_arg_version\n    return unless invoked_as_script? # don't want to hijack other binstubs\n    return unless \"update\".start_with?(ARGV.first || \" \") # must be running `bundle update`\n    bundler_version = nil\n    update_index = nil\n    ARGV.each_with_index do |a, i|\n      if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN\n        bundler_version = a\n      end\n      next unless a =~ /\\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\\z/\n      bundler_version = $1\n      update_index = i\n    end\n    bundler_version\n  end\n\n  def gemfile\n    gemfile = ENV[\"BUNDLE_GEMFILE\"]\n    return gemfile if gemfile && !gemfile.empty?\n\n    File.expand_path(\"../Gemfile\", __dir__)\n  end\n\n  def lockfile\n    lockfile =\n      case File.basename(gemfile)\n      when \"gems.rb\" then gemfile.sub(/\\.rb$/, gemfile)\n      else \"#{gemfile}.lock\"\n      end\n    File.expand_path(lockfile)\n  end\n\n  def lockfile_version\n    return unless File.file?(lockfile)\n    lockfile_contents = File.read(lockfile)\n    return unless lockfile_contents =~ /\\n\\nBUNDLED WITH\\n\\s{2,}(#{Gem::Version::VERSION_PATTERN})\\n/\n    Regexp.last_match(1)\n  end\n\n  def bundler_requirement\n    @bundler_requirement ||=\n      env_var_version || cli_arg_version ||\n        bundler_requirement_for(lockfile_version)\n  end\n\n  def bundler_requirement_for(version)\n    return \"#{Gem::Requirement.default}.a\" unless version\n\n    bundler_gem_version = Gem::Version.new(version)\n\n    requirement = bundler_gem_version.approximate_recommendation\n\n    return requirement unless Gem.rubygems_version < Gem::Version.new(\"2.7.0\")\n\n    requirement += \".a\" if bundler_gem_version.prerelease?\n\n    requirement\n  end\n\n  def load_bundler!\n    ENV[\"BUNDLE_GEMFILE\"] ||= gemfile\n\n    activate_bundler\n  end\n\n  def activate_bundler\n    gem_error = activation_error_handling do\n      gem \"bundler\", bundler_requirement\n    end\n    return if gem_error.nil?\n    require_error = activation_error_handling do\n      require \"bundler/version\"\n    end\n    return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))\n    warn \"Activating bundler (#{bundler_requirement}) failed:\\n#{gem_error.message}\\n\\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`\"\n    exit 42\n  end\n\n  def activation_error_handling\n    yield\n    nil\n  rescue StandardError, LoadError => e\n    e\n  end\nend\n\nm.load_bundler!\n\nif m.invoked_as_script?\n  load Gem.bin_path(\"bundler\", \"bundle\")\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/bin/rails",
    "content": "#!/usr/bin/env ruby\nAPP_PATH = File.expand_path(\"../config/application\", __dir__)\nrequire_relative \"../config/boot\"\nrequire \"rails/commands\"\n"
  },
  {
    "path": "examples/stacks/rails/blog/bin/rake",
    "content": "#!/usr/bin/env ruby\nrequire_relative \"../config/boot\"\nrequire \"rake\"\nRake.application.run\n"
  },
  {
    "path": "examples/stacks/rails/blog/bin/setup",
    "content": "#!/usr/bin/env ruby\nrequire \"fileutils\"\n\n# path to your application root.\nAPP_ROOT = File.expand_path(\"..\", __dir__)\n\ndef system!(*args)\n  system(*args) || abort(\"\\n== Command #{args} failed ==\")\nend\n\nFileUtils.chdir APP_ROOT do\n  # This script is a way to set up or update your development environment automatically.\n  # This script is idempotent, so that you can run it at any time and get an expectable outcome.\n  # Add necessary setup steps to this file.\n\n  puts \"== Installing dependencies ==\"\n  system! \"gem install bundler --conservative\"\n  system(\"bundle check\") || system!(\"bundle install\")\n\n  # puts \"\\n== Copying sample files ==\"\n  # unless File.exist?(\"config/database.yml\")\n  #   FileUtils.cp \"config/database.yml.sample\", \"config/database.yml\"\n  # end\n\n  puts \"\\n== Preparing database ==\"\n  system! \"bin/rails db:prepare\"\n\n  puts \"\\n== Removing old logs and tempfiles ==\"\n  system! \"bin/rails log:clear tmp:clear\"\n\n  puts \"\\n== Restarting application server ==\"\n  system! \"bin/rails restart\"\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/application.rb",
    "content": "require_relative \"boot\"\n\nrequire \"rails/all\"\n\n# Require the gems listed in Gemfile, including any gems\n# you've limited to :test, :development, or :production.\nBundler.require(*Rails.groups)\n\nmodule Blog\n  class Application < Rails::Application\n    # Initialize configuration defaults for originally generated Rails version.\n    config.load_defaults 7.0\n\n    # Configuration for the application, engines, and railties goes here.\n    #\n    # These settings can be overridden in specific environments using the files\n    # in config/environments, which are processed later.\n    #\n    # config.time_zone = \"Central Time (US & Canada)\"\n    # config.eager_load_paths << Rails.root.join(\"extras\")\n  end\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/boot.rb",
    "content": "ENV[\"BUNDLE_GEMFILE\"] ||= File.expand_path(\"../Gemfile\", __dir__)\n\nrequire \"bundler/setup\" # Set up gems listed in the Gemfile.\nrequire \"bootsnap/setup\" # Speed up boot time by caching expensive operations.\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/cable.yml",
    "content": "development:\n  adapter: async\n\ntest:\n  adapter: test\n\nproduction:\n  adapter: redis\n  url: <%= ENV.fetch(\"REDIS_URL\") { \"redis://localhost:6379/1\" } %>\n  channel_prefix: blog_production\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/credentials.yml.enc",
    "content": "n7n2ZlGCTiWpFnYBrIdjcKX4apuj9a01dx5b/jDPk50RGB2pVCsUNcTTPG7meRILlgr6eYz1b2yH33yhHhAScp5GxJiGgGOBCZ0WJ59MCKDvDmfbggepyW57flZduTfggIcbcX3+KaSiR6tUoppepF8xiqq4MBvcSN2/QsYhiJhhT6Mclr5w5LOaDyrUQvMsRRBuINb78bEJNnrAs/osikgIP60l/Xw9H93s5hvZij6k7VUGET8ZtFoNfxtuieYbN8lJ+u/0NkVm9Nb9pLHEFG+xecKI2XHjRW2xYEUcOtctibAuagNDKBTZINlH5K2PN8kp5JojagKV5GEktul/WZ3GlLPTsK2HPfN8WKSlCb6ShfX2qeegmFFP12DPSelRk88ORqONVQY/Wt2YOrmHJCIXjH5hKt036qnK--pWEeoDmhJgEiyp9v--HcDsg0CciWSLb1jyARqGQA=="
  },
  {
    "path": "examples/stacks/rails/blog/config/database.yml",
    "content": "# SQLite. Versions 3.8.0 and up are supported.\n#   gem install sqlite3\n#\n#   Ensure the SQLite 3 gem is defined in your Gemfile\n#   gem \"sqlite3\"\n#\ndefault: &default\n  adapter: sqlite3\n  pool: <%= ENV.fetch(\"RAILS_MAX_THREADS\") { 5 } %>\n  timeout: 5000\n\ndevelopment:\n  <<: *default\n  database: db/development.sqlite3\n\n# Warning: The database defined as \"test\" will be erased and\n# re-generated from your development database when you run \"rake\".\n# Do not set this db to the same as development or production.\ntest:\n  <<: *default\n  database: db/test.sqlite3\n\nproduction:\n  <<: *default\n  database: db/production.sqlite3\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/environment.rb",
    "content": "# Load the Rails application.\nrequire_relative \"application\"\n\n# Initialize the Rails application.\nRails.application.initialize!\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/environments/development.rb",
    "content": "require \"active_support/core_ext/integer/time\"\n\nRails.application.configure do\n  # Settings specified here will take precedence over those in config/application.rb.\n\n  # In the development environment your application's code is reloaded any time\n  # it changes. This slows down response time but is perfect for development\n  # since you don't have to restart the web server when you make code changes.\n  config.cache_classes = false\n\n  # Do not eager load code on boot.\n  config.eager_load = false\n\n  # Show full error reports.\n  config.consider_all_requests_local = true\n\n  # Enable server timing\n  config.server_timing = true\n\n  # Enable/disable caching. By default caching is disabled.\n  # Run rails dev:cache to toggle caching.\n  if Rails.root.join(\"tmp/caching-dev.txt\").exist?\n    config.action_controller.perform_caching = true\n    config.action_controller.enable_fragment_cache_logging = true\n\n    config.cache_store = :memory_store\n    config.public_file_server.headers = {\n      \"Cache-Control\" => \"public, max-age=#{2.days.to_i}\"\n    }\n  else\n    config.action_controller.perform_caching = false\n\n    config.cache_store = :null_store\n  end\n\n  # Store uploaded files on the local file system (see config/storage.yml for options).\n  config.active_storage.service = :local\n\n  # Don't care if the mailer can't send.\n  config.action_mailer.raise_delivery_errors = false\n\n  config.action_mailer.perform_caching = false\n\n  # Print deprecation notices to the Rails logger.\n  config.active_support.deprecation = :log\n\n  # Raise exceptions for disallowed deprecations.\n  config.active_support.disallowed_deprecation = :raise\n\n  # Tell Active Support which deprecation messages to disallow.\n  config.active_support.disallowed_deprecation_warnings = []\n\n  # Raise an error on page load if there are pending migrations.\n  config.active_record.migration_error = :page_load\n\n  # Highlight code that triggered database queries in logs.\n  config.active_record.verbose_query_logs = true\n\n  # Suppress logger output for asset requests.\n  config.assets.quiet = true\n\n  # Raises error for missing translations.\n  # config.i18n.raise_on_missing_translations = true\n\n  # Annotate rendered view with file names.\n  # config.action_view.annotate_rendered_view_with_filenames = true\n\n  # Uncomment if you wish to allow Action Cable access from any origin.\n  # config.action_cable.disable_request_forgery_protection = true\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/environments/production.rb",
    "content": "require \"active_support/core_ext/integer/time\"\n\nRails.application.configure do\n  # Settings specified here will take precedence over those in config/application.rb.\n\n  # Code is not reloaded between requests.\n  config.cache_classes = true\n\n  # Eager load code on boot. This eager loads most of Rails and\n  # your application in memory, allowing both threaded web servers\n  # and those relying on copy on write to perform better.\n  # Rake tasks automatically ignore this option for performance.\n  config.eager_load = true\n\n  # Full error reports are disabled and caching is turned on.\n  config.consider_all_requests_local       = false\n  config.action_controller.perform_caching = true\n\n  # Ensures that a master key has been made available in either ENV[\"RAILS_MASTER_KEY\"]\n  # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).\n  # config.require_master_key = true\n\n  # Disable serving static files from the `/public` folder by default since\n  # Apache or NGINX already handles this.\n  config.public_file_server.enabled = ENV[\"RAILS_SERVE_STATIC_FILES\"].present?\n\n  # Compress CSS using a preprocessor.\n  # config.assets.css_compressor = :sass\n\n  # Do not fallback to assets pipeline if a precompiled asset is missed.\n  config.assets.compile = false\n\n  # Enable serving of images, stylesheets, and JavaScripts from an asset server.\n  # config.asset_host = \"http://assets.example.com\"\n\n  # Specifies the header that your server uses for sending files.\n  # config.action_dispatch.x_sendfile_header = \"X-Sendfile\" # for Apache\n  # config.action_dispatch.x_sendfile_header = \"X-Accel-Redirect\" # for NGINX\n\n  # Store uploaded files on the local file system (see config/storage.yml for options).\n  config.active_storage.service = :local\n\n  # Mount Action Cable outside main process or domain.\n  # config.action_cable.mount_path = nil\n  # config.action_cable.url = \"wss://example.com/cable\"\n  # config.action_cable.allowed_request_origins = [ \"http://example.com\", /http:\\/\\/example.*/ ]\n\n  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.\n  # config.force_ssl = true\n\n  # Include generic and useful information about system operation, but avoid logging too much\n  # information to avoid inadvertent exposure of personally identifiable information (PII).\n  config.log_level = :info\n\n  # Prepend all log lines with the following tags.\n  config.log_tags = [ :request_id ]\n\n  # Use a different cache store in production.\n  # config.cache_store = :mem_cache_store\n\n  # Use a real queuing backend for Active Job (and separate queues per environment).\n  # config.active_job.queue_adapter     = :resque\n  # config.active_job.queue_name_prefix = \"blog_production\"\n\n  config.action_mailer.perform_caching = false\n\n  # Ignore bad email addresses and do not raise email delivery errors.\n  # Set this to true and configure the email server for immediate delivery to raise delivery errors.\n  # config.action_mailer.raise_delivery_errors = false\n\n  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to\n  # the I18n.default_locale when a translation cannot be found).\n  config.i18n.fallbacks = true\n\n  # Don't log any deprecations.\n  config.active_support.report_deprecations = false\n\n  # Use default logging formatter so that PID and timestamp are not suppressed.\n  config.log_formatter = ::Logger::Formatter.new\n\n  # Use a different logger for distributed setups.\n  # require \"syslog/logger\"\n  # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new \"app-name\")\n\n  if ENV[\"RAILS_LOG_TO_STDOUT\"].present?\n    logger           = ActiveSupport::Logger.new(STDOUT)\n    logger.formatter = config.log_formatter\n    config.logger    = ActiveSupport::TaggedLogging.new(logger)\n  end\n\n  # Do not dump schema after migrations.\n  config.active_record.dump_schema_after_migration = false\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/environments/test.rb",
    "content": "require \"active_support/core_ext/integer/time\"\n\n# The test environment is used exclusively to run your application's\n# test suite. You never need to work with it otherwise. Remember that\n# your test database is \"scratch space\" for the test suite and is wiped\n# and recreated between test runs. Don't rely on the data there!\n\nRails.application.configure do\n  # Settings specified here will take precedence over those in config/application.rb.\n\n  # Turn false under Spring and add config.action_view.cache_template_loading = true.\n  config.cache_classes = true\n\n  # Eager loading loads your whole application. When running a single test locally,\n  # this probably isn't necessary. It's a good idea to do in a continuous integration\n  # system, or in some way before deploying your code.\n  config.eager_load = ENV[\"CI\"].present?\n\n  # Configure public file server for tests with Cache-Control for performance.\n  config.public_file_server.enabled = true\n  config.public_file_server.headers = {\n    \"Cache-Control\" => \"public, max-age=#{1.hour.to_i}\"\n  }\n\n  # Show full error reports and disable caching.\n  config.consider_all_requests_local       = true\n  config.action_controller.perform_caching = false\n  config.cache_store = :null_store\n\n  # Raise exceptions instead of rendering exception templates.\n  config.action_dispatch.show_exceptions = false\n\n  # Disable request forgery protection in test environment.\n  config.action_controller.allow_forgery_protection = false\n\n  # Store uploaded files on the local file system in a temporary directory.\n  config.active_storage.service = :test\n\n  config.action_mailer.perform_caching = false\n\n  # Tell Action Mailer not to deliver emails to the real world.\n  # The :test delivery method accumulates sent emails in the\n  # ActionMailer::Base.deliveries array.\n  config.action_mailer.delivery_method = :test\n\n  # Print deprecation notices to the stderr.\n  config.active_support.deprecation = :stderr\n\n  # Raise exceptions for disallowed deprecations.\n  config.active_support.disallowed_deprecation = :raise\n\n  # Tell Active Support which deprecation messages to disallow.\n  config.active_support.disallowed_deprecation_warnings = []\n\n  # Raises error for missing translations.\n  # config.i18n.raise_on_missing_translations = true\n\n  # Annotate rendered view with file names.\n  # config.action_view.annotate_rendered_view_with_filenames = true\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/initializers/assets.rb",
    "content": "# Be sure to restart your server when you modify this file.\n\n# Version of your assets, change this if you want to expire all your assets.\nRails.application.config.assets.version = \"1.0\"\n\n# Add additional assets to the asset load path.\n# Rails.application.config.assets.paths << Emoji.images_path\n\n# Precompile additional assets.\n# application.js, application.css, and all non-JS/CSS in the app/assets\n# folder are already added.\n# Rails.application.config.assets.precompile += %w( admin.js admin.css )\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/initializers/content_security_policy.rb",
    "content": "# Be sure to restart your server when you modify this file.\n\n# Define an application-wide content security policy.\n# See the Securing Rails Applications Guide for more information:\n# https://guides.rubyonrails.org/security.html#content-security-policy-header\n\n# Rails.application.configure do\n#   config.content_security_policy do |policy|\n#     policy.default_src :self, :https\n#     policy.font_src    :self, :https, :data\n#     policy.img_src     :self, :https, :data\n#     policy.object_src  :none\n#     policy.script_src  :self, :https\n#     policy.style_src   :self, :https\n#     # Specify URI for violation reports\n#     # policy.report_uri \"/csp-violation-report-endpoint\"\n#   end\n#\n#   # Generate session nonces for permitted importmap and inline scripts\n#   config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }\n#   config.content_security_policy_nonce_directives = %w(script-src)\n#\n#   # Report violations without enforcing the policy.\n#   # config.content_security_policy_report_only = true\n# end\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/initializers/filter_parameter_logging.rb",
    "content": "# Be sure to restart your server when you modify this file.\n\n# Configure parameters to be filtered from the log file. Use this to limit dissemination of\n# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported\n# notations and behaviors.\nRails.application.config.filter_parameters += [\n  :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn\n]\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/initializers/inflections.rb",
    "content": "# Be sure to restart your server when you modify this file.\n\n# Add new inflection rules using the following format. Inflections\n# are locale specific, and you may define rules for as many different\n# locales as you wish. All of these examples are active by default:\n# ActiveSupport::Inflector.inflections(:en) do |inflect|\n#   inflect.plural /^(ox)$/i, \"\\\\1en\"\n#   inflect.singular /^(ox)en/i, \"\\\\1\"\n#   inflect.irregular \"person\", \"people\"\n#   inflect.uncountable %w( fish sheep )\n# end\n\n# These inflection rules are supported but not enabled by default:\n# ActiveSupport::Inflector.inflections(:en) do |inflect|\n#   inflect.acronym \"RESTful\"\n# end\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/initializers/permissions_policy.rb",
    "content": "# Define an application-wide HTTP permissions policy. For further\n# information see https://developers.google.com/web/updates/2018/06/feature-policy\n#\n# Rails.application.config.permissions_policy do |f|\n#   f.camera      :none\n#   f.gyroscope   :none\n#   f.microphone  :none\n#   f.usb         :none\n#   f.fullscreen  :self\n#   f.payment     :self, \"https://secure.example.com\"\n# end\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/locales/en.yml",
    "content": "# Files in the config/locales directory are used for internationalization\n# and are automatically loaded by Rails. If you want to use locales other\n# than English, add the necessary files in this directory.\n#\n# To use the locales, use `I18n.t`:\n#\n#     I18n.t \"hello\"\n#\n# In views, this is aliased to just `t`:\n#\n#     <%= t(\"hello\") %>\n#\n# To use a different locale, set it with `I18n.locale`:\n#\n#     I18n.locale = :es\n#\n# This would use the information in config/locales/es.yml.\n#\n# The following keys must be escaped otherwise they will not be retrieved by\n# the default I18n backend:\n#\n# true, false, on, off, yes, no\n#\n# Instead, surround them with single quotes.\n#\n# en:\n#   \"true\": \"foo\"\n#\n# To learn more, please read the Rails Internationalization guide\n# available at https://guides.rubyonrails.org/i18n.html.\n\nen:\n  hello: \"Hello world\"\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/puma.rb",
    "content": "# Puma can serve each request in a thread from an internal thread pool.\n# The `threads` method setting takes two numbers: a minimum and maximum.\n# Any libraries that use thread pools should be configured to match\n# the maximum value specified for Puma. Default is set to 5 threads for minimum\n# and maximum; this matches the default thread size of Active Record.\n#\nmax_threads_count = ENV.fetch(\"RAILS_MAX_THREADS\") { 5 }\nmin_threads_count = ENV.fetch(\"RAILS_MIN_THREADS\") { max_threads_count }\nthreads min_threads_count, max_threads_count\n\n# Specifies the `worker_timeout` threshold that Puma will use to wait before\n# terminating a worker in development environments.\n#\nworker_timeout 3600 if ENV.fetch(\"RAILS_ENV\", \"development\") == \"development\"\n\n# Specifies the `port` that Puma will listen on to receive requests; default is 3000.\n#\nport ENV.fetch(\"PORT\") { 3000 }\n\n# Specifies the `environment` that Puma will run in.\n#\nenvironment ENV.fetch(\"RAILS_ENV\") { \"development\" }\n\n# Specifies the `pidfile` that Puma will use.\npidfile ENV.fetch(\"PIDFILE\") { \"tmp/pids/server.pid\" }\n\n# Specifies the number of `workers` to boot in clustered mode.\n# Workers are forked web server processes. If using threads and workers together\n# the concurrency of the application would be max `threads` * `workers`.\n# Workers do not work on JRuby or Windows (both of which do not support\n# processes).\n#\n# workers ENV.fetch(\"WEB_CONCURRENCY\") { 2 }\n\n# Use the `preload_app!` method when specifying a `workers` number.\n# This directive tells Puma to first boot the application and load code\n# before forking the application. This takes advantage of Copy On Write\n# process behavior so workers use less memory.\n#\n# preload_app!\n\n# Allow puma to be restarted by `bin/rails restart` command.\nplugin :tmp_restart\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/routes.rb",
    "content": "Rails.application.routes.draw do\n  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html\n\n  # Defines the root path route (\"/\")\n  # root \"articles#index\"\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/config/storage.yml",
    "content": "test:\n  service: Disk\n  root: <%= Rails.root.join(\"tmp/storage\") %>\n\nlocal:\n  service: Disk\n  root: <%= Rails.root.join(\"storage\") %>\n\n# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)\n# amazon:\n#   service: S3\n#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>\n#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>\n#   region: us-east-1\n#   bucket: your_own_bucket-<%= Rails.env %>\n\n# Remember not to checkin your GCS keyfile to a repository\n# google:\n#   service: GCS\n#   project: your_project\n#   credentials: <%= Rails.root.join(\"path/to/gcs.keyfile\") %>\n#   bucket: your_own_bucket-<%= Rails.env %>\n\n# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)\n# microsoft:\n#   service: AzureStorage\n#   storage_account_name: your_account_name\n#   storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>\n#   container: your_container_name-<%= Rails.env %>\n\n# mirror:\n#   service: Mirror\n#   primary: local\n#   mirrors: [ amazon, google, microsoft ]\n"
  },
  {
    "path": "examples/stacks/rails/blog/config.ru",
    "content": "# This file is used by Rack-based servers to start the application.\n\nrequire_relative \"config/environment\"\n\nrun Rails.application\nRails.application.load_server\n"
  },
  {
    "path": "examples/stacks/rails/blog/db/schema.rb",
    "content": "# This file is auto-generated from the current state of the database. Instead\n# of editing this file, please use the migrations feature of Active Record to\n# incrementally modify your database, and then regenerate this schema definition.\n#\n# This file is the source Rails uses to define your schema when running `bin/rails\n# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to\n# be faster and is potentially less error prone than running all of your\n# migrations from scratch. Old migrations may fail to apply correctly if those\n# migrations use external dependencies or application code.\n#\n# It's strongly recommended that you check this file into your version control system.\n\nActiveRecord::Schema[7.0].define(version: 0) do\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/db/seeds.rb",
    "content": "# This file should contain all the record creation needed to seed the database with its default values.\n# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).\n#\n# Examples:\n#\n#   movies = Movie.create([{ name: \"Star Wars\" }, { name: \"Lord of the Rings\" }])\n#   Character.create(name: \"Luke\", movie: movies.first)\n"
  },
  {
    "path": "examples/stacks/rails/blog/lib/assets/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/lib/tasks/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/log/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/public/404.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>The page you were looking for doesn't exist (404)</title>\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n  <style>\n  .rails-default-error-page {\n    background-color: #EFEFEF;\n    color: #2E2F30;\n    text-align: center;\n    font-family: arial, sans-serif;\n    margin: 0;\n  }\n\n  .rails-default-error-page div.dialog {\n    width: 95%;\n    max-width: 33em;\n    margin: 4em auto 0;\n  }\n\n  .rails-default-error-page div.dialog > div {\n    border: 1px solid #CCC;\n    border-right-color: #999;\n    border-left-color: #999;\n    border-bottom-color: #BBB;\n    border-top: #B00100 solid 4px;\n    border-top-left-radius: 9px;\n    border-top-right-radius: 9px;\n    background-color: white;\n    padding: 7px 12% 0;\n    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n  }\n\n  .rails-default-error-page h1 {\n    font-size: 100%;\n    color: #730E15;\n    line-height: 1.5em;\n  }\n\n  .rails-default-error-page div.dialog > p {\n    margin: 0 0 1em;\n    padding: 1em;\n    background-color: #F7F7F7;\n    border: 1px solid #CCC;\n    border-right-color: #999;\n    border-left-color: #999;\n    border-bottom-color: #999;\n    border-bottom-left-radius: 4px;\n    border-bottom-right-radius: 4px;\n    border-top-color: #DADADA;\n    color: #666;\n    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n  }\n  </style>\n</head>\n\n<body class=\"rails-default-error-page\">\n  <!-- This file lives in public/404.html -->\n  <div class=\"dialog\">\n    <div>\n      <h1>The page you were looking for doesn't exist.</h1>\n      <p>You may have mistyped the address or the page may have moved.</p>\n    </div>\n    <p>If you are the application owner check the logs for more information.</p>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "examples/stacks/rails/blog/public/422.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>The change you wanted was rejected (422)</title>\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n  <style>\n  .rails-default-error-page {\n    background-color: #EFEFEF;\n    color: #2E2F30;\n    text-align: center;\n    font-family: arial, sans-serif;\n    margin: 0;\n  }\n\n  .rails-default-error-page div.dialog {\n    width: 95%;\n    max-width: 33em;\n    margin: 4em auto 0;\n  }\n\n  .rails-default-error-page div.dialog > div {\n    border: 1px solid #CCC;\n    border-right-color: #999;\n    border-left-color: #999;\n    border-bottom-color: #BBB;\n    border-top: #B00100 solid 4px;\n    border-top-left-radius: 9px;\n    border-top-right-radius: 9px;\n    background-color: white;\n    padding: 7px 12% 0;\n    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n  }\n\n  .rails-default-error-page h1 {\n    font-size: 100%;\n    color: #730E15;\n    line-height: 1.5em;\n  }\n\n  .rails-default-error-page div.dialog > p {\n    margin: 0 0 1em;\n    padding: 1em;\n    background-color: #F7F7F7;\n    border: 1px solid #CCC;\n    border-right-color: #999;\n    border-left-color: #999;\n    border-bottom-color: #999;\n    border-bottom-left-radius: 4px;\n    border-bottom-right-radius: 4px;\n    border-top-color: #DADADA;\n    color: #666;\n    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n  }\n  </style>\n</head>\n\n<body class=\"rails-default-error-page\">\n  <!-- This file lives in public/422.html -->\n  <div class=\"dialog\">\n    <div>\n      <h1>The change you wanted was rejected.</h1>\n      <p>Maybe you tried to change something you didn't have access to.</p>\n    </div>\n    <p>If you are the application owner check the logs for more information.</p>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "examples/stacks/rails/blog/public/500.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>We're sorry, but something went wrong (500)</title>\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n  <style>\n  .rails-default-error-page {\n    background-color: #EFEFEF;\n    color: #2E2F30;\n    text-align: center;\n    font-family: arial, sans-serif;\n    margin: 0;\n  }\n\n  .rails-default-error-page div.dialog {\n    width: 95%;\n    max-width: 33em;\n    margin: 4em auto 0;\n  }\n\n  .rails-default-error-page div.dialog > div {\n    border: 1px solid #CCC;\n    border-right-color: #999;\n    border-left-color: #999;\n    border-bottom-color: #BBB;\n    border-top: #B00100 solid 4px;\n    border-top-left-radius: 9px;\n    border-top-right-radius: 9px;\n    background-color: white;\n    padding: 7px 12% 0;\n    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n  }\n\n  .rails-default-error-page h1 {\n    font-size: 100%;\n    color: #730E15;\n    line-height: 1.5em;\n  }\n\n  .rails-default-error-page div.dialog > p {\n    margin: 0 0 1em;\n    padding: 1em;\n    background-color: #F7F7F7;\n    border: 1px solid #CCC;\n    border-right-color: #999;\n    border-left-color: #999;\n    border-bottom-color: #999;\n    border-bottom-left-radius: 4px;\n    border-bottom-right-radius: 4px;\n    border-top-color: #DADADA;\n    color: #666;\n    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n  }\n  </style>\n</head>\n\n<body class=\"rails-default-error-page\">\n  <!-- This file lives in public/500.html -->\n  <div class=\"dialog\">\n    <div>\n      <h1>We're sorry, but something went wrong.</h1>\n    </div>\n    <p>If you are the application owner check the logs for more information.</p>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "examples/stacks/rails/blog/public/robots.txt",
    "content": "# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file\n"
  },
  {
    "path": "examples/stacks/rails/blog/storage/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/test/application_system_test_case.rb",
    "content": "require \"test_helper\"\n\nclass ApplicationSystemTestCase < ActionDispatch::SystemTestCase\n  driven_by :selenium, using: :chrome, screen_size: [1400, 1400]\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/test/channels/application_cable/connection_test.rb",
    "content": "require \"test_helper\"\n\nclass ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase\n  # test \"connects with cookies\" do\n  #   cookies.signed[:user_id] = 42\n  #\n  #   connect\n  #\n  #   assert_equal connection.user_id, \"42\"\n  # end\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/test/controllers/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/test/fixtures/files/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/test/helpers/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/test/integration/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/test/mailers/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/test/models/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/test/system/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/test/test_helper.rb",
    "content": "ENV[\"RAILS_ENV\"] ||= \"test\"\nrequire_relative \"../config/environment\"\nrequire \"rails/test_help\"\n\nclass ActiveSupport::TestCase\n  # Run tests in parallel with specified workers\n  parallelize(workers: :number_of_processors)\n\n  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.\n  fixtures :all\n\n  # Add more helper methods to be used by all tests here...\nend\n"
  },
  {
    "path": "examples/stacks/rails/blog/tmp/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/tmp/pids/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/blog/tmp/storage/.keep",
    "content": ""
  },
  {
    "path": "examples/stacks/rails/devbox.json",
    "content": "{\n  \"packages\": {\n    \"ruby\":    \"3.3\",\n    \"bundler\": \"2.5\",\n    \"nodejs\":  \"21\",\n    \"yarn\":    \"1.22\",\n    \"curl\":    \"latest\",\n    \"sqlite\":  \"latest\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \"gem install rails\",\n      \"./blog/bin/bundle install\",\n      \"./blog/bin/rails -f ./blog/Rakefile db:prepare\"\n    ],\n    \"scripts\": {\n      \"run_test\":     [\"cd blog && bin/rails test\"],\n      \"start_server\": [\"cd blog && bin/rails server\"]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/stacks/rails/process-compose.yml",
    "content": "# Process compose for starting rails\nversion: \"0.5\"\n\nprocesses:\n  rails:\n   command: ./blog/bin/rails server -b\n   availability:\n    restart: \"always\"\n"
  },
  {
    "path": "examples/stacks/spring/.gitignore",
    "content": "HELP.md\n.gradle\nbuild/\n!gradle/wrapper/gradle-wrapper.jar\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\nbin/\n!**/src/main/**/bin/\n!**/src/test/**/bin/\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\nout/\n!**/src/main/**/out/\n!**/src/test/**/out/\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": "examples/stacks/spring/README.md",
    "content": "# Spring Boot Example\n\nThis example combines Java, Spring Boot, and MySQL to expose a simple REST API. This example is based on the official [Spring Boot Documentation](https://spring.io/guides/gs/accessing-data-mysql/).\n\n## How to Run\n\n1. Install [Devbox](https://www.jetify.com/docs/devbox/installing-devbox/index)\n\n1. Prepare the database by running `devbox run setup_db`. This will create the user and database that Spring expects in `stacks/spring/src/main/resources/application.properties`\n1. You can now start the Spring Boot service by running `devbox run bootRun`. This will start your MySQL service and run the application\n1. You can test the service using `GET localhost:8080/demo/all` or `POST localhost:8080/demo/add`. See the Spring Documentation for more details.\n\n## How to Recreate this Example\n\n1. Create a blank Devbox project with `devbox init`\n2. Add the required packages with `devbox add jdk@17 mysql@latest gradle@latest`\n3. Create a new Spring Boot application using the [Spring Boot initializer](https://start.spring.io/).\n4. Copy the devbox.json and devbox.lock files into the project directory.\n5. Initialize your mysql database by running `devbox services up`, and create the example DB and user using the `setup_db.sql` file in this directory.\n\n## Notes\n\n- This example uses the [Spring Boot initializer](https://start.spring.io/) to create the project. You can use any method you like to create your Spring Boot project, but you will need to make sure that the `devbox.json` and `devbox.lock` files are in the same directory as your `build.gradle` file.\n- This example hardcodes a username and password for development purposes. For production or more secure usecases, you should change them and exclude them from source control.\n- This distribution uses the OpenJDK. You can find other JDK distributions using `devbox search`\n"
  },
  {
    "path": "examples/stacks/spring/build.gradle",
    "content": "plugins {\n\tid 'java'\n\tid 'org.springframework.boot' version '3.1.1'\n\tid 'io.spring.dependency-management' version '1.1.0'\n}\n\ngroup = 'com.devbox.example.spring'\nversion = '0.0.1-SNAPSHOT'\n\njava {\n\tsourceCompatibility = '17'\n}\n\nrepositories {\n\tmavenCentral()\n}\n\ndependencies {\n\timplementation 'org.springframework.boot:spring-boot-starter-data-jpa'\n\timplementation 'org.springframework.boot:spring-boot-starter-data-redis'\n\timplementation 'org.springframework.boot:spring-boot-starter-web'\n\truntimeOnly 'com.mysql:mysql-connector-j'\n\ttestImplementation 'org.springframework.boot:spring-boot-starter-test'\n}\n\ntasks.named('test') {\n\tuseJUnitPlatform()\n}\n"
  },
  {
    "path": "examples/stacks/spring/devbox.json",
    "content": "{\n  \"packages\": [\n    \"jdk@17\",\n    \"gradle@latest\",\n    \"mysql80@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \"echo 'Welcome to devbox!'\\n\",\n      \"echo 'Setup MySQL by running `devbox run setup_db`\",\n      \"echo 'Run the example using `devbox run bootRun\"\n    ],\n    \"scripts\": {\n      \"bootRun\": [\n        \"devbox services up -b\",\n        \"./gradlew bootRun\",\n        \"devbox services stop\"\n      ],\n      \"build\": [\n        \"./gradlew build\"\n      ],\n      \"setup_db\": [\n        \"devbox services up mysql -b\",\n        \"mysql -u root < setup_db.sql\",\n        \"devbox services stop\"\n      ],\n      \"test\": [\n        \"./gradlew test\"\n      ]\n    }\n  }\n}"
  },
  {
    "path": "examples/stacks/spring/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.1.1-bin.zip\nnetworkTimeout=10000\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "examples/stacks/spring/gradle.properties",
    "content": "org.gradle.java.home=/nix/store/csfdcflywb9gj20b1mvsp1ixy6f398bg-zulu17.34.19-ca-jdk-17.0.3\n"
  },
  {
    "path": "examples/stacks/spring/gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\nAPP_HOME=$( cd \"${APP_HOME:-./}\" && pwd -P ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command;\n#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of\n#     shell script including quotes and variable substitutions, so put them in\n#     double quotes to make sure that they get re-expanded; and\n#   * put everything else in single quotes, so that it's not re-expanded.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "examples/stacks/spring/gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "examples/stacks/spring/settings.gradle",
    "content": "rootProject.name = 'spring'\n"
  },
  {
    "path": "examples/stacks/spring/setup_db.sql",
    "content": "-- You should run this query using `mysql -u root < setup_db.sql`\n\nDROP DATABASE IF EXISTS db_example;\nCREATE DATABASE db_example;\n\nUSE db_example;\n\nCREATE USER 'springuser'@'%' IDENTIFIED BY 'password';\nGRANT ALL ON db_example.* TO 'springuser'@'%';"
  },
  {
    "path": "examples/stacks/spring/src/main/java/com/devbox/example/spring/spring/Application.java",
    "content": "package com.devbox.example.spring.spring;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class Application {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(Application.class, args);\n\t}\n\n}\n"
  },
  {
    "path": "examples/stacks/spring/src/main/java/com/devbox/example/spring/spring/MainController.java",
    "content": "package com.devbox.example.spring.spring;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.ResponseBody;\n\n@Controller\n@RequestMapping(path=\"/demo\")\npublic class MainController {\n\n    @Autowired\n    private UserRepository userRepository;\n\n    @PostMapping(path=\"/add\") // Map ONLY POST Requests\n    public @ResponseBody String addNewUser (@RequestParam String name\n        , @RequestParam String email) {\n      // @ResponseBody means the returned String is the response, not a view name\n      // @RequestParam means it is a parameter from the GET or POST request\n\n      User n = new User();\n      n.setName(name);\n      n.setEmail(email);\n      userRepository.save(n);\n      return \"Saved\";\n    }\n\n    @GetMapping(path=\"/all\")\n    public @ResponseBody Iterable<User> getAllUsers() {\n      // This returns a JSON or XML with the users\n      return userRepository.findAll();\n    }\n}\n"
  },
  {
    "path": "examples/stacks/spring/src/main/java/com/devbox/example/spring/spring/User.java",
    "content": "package com.devbox.example.spring.spring;\n\nimport jakarta.persistence.Entity;\nimport jakarta.persistence.GeneratedValue;\nimport jakarta.persistence.GenerationType;\nimport jakarta.persistence.Id;\n\n@Entity\npublic class User {\n    @Id\n    @GeneratedValue(strategy = GenerationType.AUTO)\n    private Integer id;\n\n    private String name;\n\n    private String email;\n\n    public User() {\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public void setId(Integer id){\n        this.id = id;\n    }\n\n    public void setEmail(String email){\n        this.email = email;\n    }\n\n    public Integer getId() {\n        return id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getEmail(){\n        return email;\n    }\n\n}\n"
  },
  {
    "path": "examples/stacks/spring/src/main/java/com/devbox/example/spring/spring/UserRepository.java",
    "content": "package com.devbox.example.spring.spring;\n\nimport org.springframework.data.repository.CrudRepository;\nimport com.devbox.example.spring.spring.User;\n\npublic interface UserRepository extends CrudRepository<User,\nInteger>{\n}\n"
  },
  {
    "path": "examples/stacks/spring/src/main/resources/application.properties",
    "content": "spring.jpa.hibernate.ddl-auto=update\nspring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/db_example\nspring.datasource.username=springuser\nspring.datasource.password=password\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.database-platform=org.hibernate.dialect.MySQLDialect\n#spring.jpa.show-sql: true"
  },
  {
    "path": "examples/stacks/spring/src/test/java/com/devbox/example/spring/spring/ApplicationTests.java",
    "content": "package com.devbox.example.spring.spring;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@SpringBootTest\nclass ApplicationTests {\n\n\t@Test\n\tvoid contextLoads() {\n\t}\n\n}\n"
  },
  {
    "path": "examples/tutorial/README.md",
    "content": "# Devbox Quickstart\n\nThis shell includes a basic `devbox.json` with a few useful packages installed, and an example init_hook and script\n\n\n## Adding New Packages\n\nRun `devbox add <package>` to add a new package. Remove it with `devbox rm <package>`.\n\nFor example: install Python 3.10 by running:\n\n```bash\ndevbox add python310\n```\n\nDevbox can install over 80,000 packages via the Nix Package Manager. Search for packages at [https://search.nixos.org/packages](https://search.nixos.org/packages)\n\n## Running Devbox Scripts\n\nYou can add new scripts by editing the `devbox.json` file\n\nYou can run scripts using `devbox run <script>`\n\nFor example: you can replay this help text with:\n\n```bash\ndevbox run readme\n```\n\n## Next Steps\n\n* Checkout our Docs at [https://www.jetify.com/devbox/docs](https://www.jetify.com/devbox/docs)\n* Try out an Example Project at [https://www.jetify.com/devbox/docs/devbox-examples](https://www.jetify.com/devbox/docs/devbox-examples)\n* Report Issues at [https://github.com/jetify-com/devbox/issues/new/choose](https://github.com/jetify-com/devbox/issues/new/choose)\n"
  },
  {
    "path": "examples/tutorial/devbox.json",
    "content": "{\n  \"packages\": [\n    \"gh\",\n    \"glow\",\n    \"vim@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \"clear && PAGER=cat glow README.md\"\n    ],\n    \"scripts\": {\n      \"readme\": \"clear && PAGER=cat glow README.md\"\n    }\n  }\n}\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"Instant, easy, predictable dev environments\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs =\n    { self\n    , nixpkgs\n    , flake-utils\n    ,\n    }:\n    flake-utils.lib.eachDefaultSystem (\n      system:\n      let\n        pkgs = nixpkgs.legacyPackages.${system};\n\n        lastTag = \"0.17.0\";\n\n        revision = if (self ? shortRev) then \"${self.shortRev}\" else \"${self.dirtyShortRev or \"dirty\"}\";\n\n        # Add the commit to the version string for flake builds\n        version = \"${lastTag}\";\n\n        # Run `devbox run update-flake` to update the vendor-hash\n        vendorHash = if builtins.pathExists ./vendor-hash then builtins.readFile ./vendor-hash else \"\";\n\n        buildGoModule = pkgs.buildGo125Module;\n\n      in\n      {\n        inherit self;\n        packages.default = buildGoModule {\n          pname = \"devbox\";\n          inherit version vendorHash;\n\n          src = ./.;\n\n          subpackage = [ ./cmd/devbox ];\n\n          ldflags = [\n            \"-s\"\n            \"-w\"\n            \"-X go.jetify.com/devbox/internal/build.Version=${version}\"\n            \"-X go.jetify.com/devbox/internal/build.Commit=${revision}\"\n          ];\n\n          # Don't generate test binaries (as we'd include them as a bin)\n          excludedPackages = [ \"testscripts\" ];\n\n          # Disable tests if they require network access or are integration tests\n          doCheck = false;\n\n          nativeBuildInputs = [ pkgs.installShellFiles ];\n\n          postInstall = pkgs.lib.optionalString (pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform) ''\n            installShellCompletion --cmd devbox \\\n              --bash <($out/bin/devbox completion bash) \\\n              --fish <($out/bin/devbox completion fish) \\\n              --zsh <($out/bin/devbox completion zsh)\n          '';\n\n          meta = with pkgs.lib; {\n            description = \"Instant, easy, and predictable development environments\";\n            homepage = \"https://www.jetify.com/devbox\";\n            license = licenses.asl20;\n            maintainers = with maintainers; [ lagoja ];\n          };\n        };\n      }\n    );\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module go.jetify.com/devbox\n\ngo 1.24.5\n\nrequire (\n\tal.essio.dev/pkg/shellescape v1.6.0\n\tgithub.com/AlecAivazis/survey/v2 v2.3.7\n\tgithub.com/MakeNowJust/heredoc/v2 v2.0.1\n\tgithub.com/aws/aws-sdk-go-v2 v1.39.6\n\tgithub.com/aws/aws-sdk-go-v2/config v1.31.17\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.18.21\n\tgithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.90.0\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.39.1\n\tgithub.com/bmatcuk/doublestar/v4 v4.9.1\n\tgithub.com/briandowns/spinner v1.23.2\n\tgithub.com/denisbrodbeck/machineid v1.0.1\n\tgithub.com/f1bonacc1/process-compose v1.64.1\n\tgithub.com/fatih/color v1.18.0\n\tgithub.com/fsnotify/fsnotify v1.9.0\n\tgithub.com/getsentry/sentry-go v0.36.2\n\tgithub.com/go-jose/go-jose/v4 v4.1.3\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/hashicorp/go-envparse v0.1.0\n\tgithub.com/joho/godotenv v1.5.1\n\tgithub.com/mattn/go-isatty v0.0.20\n\tgithub.com/mholt/archives v0.1.5\n\tgithub.com/olekukonko/tablewriter v1.1.0\n\tgithub.com/pelletier/go-toml/v2 v2.2.4\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/rogpeppe/go-internal v1.14.1\n\tgithub.com/samber/lo v1.52.0\n\tgithub.com/segmentio/analytics-go v3.1.0+incompatible\n\tgithub.com/spf13/cobra v1.10.1\n\tgithub.com/spf13/pflag v1.0.10\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a\n\tgithub.com/wk8/go-ordered-map/v2 v2.1.8\n\tgithub.com/zealic/go2node v0.1.0\n\tgo.jetify.com/envsec v0.0.16-0.20250821201727-a2fd18dc57f6\n\tgo.jetify.com/pkg v0.0.0-20250904024813-5ec17279258b\n\tgo.jetify.com/typeid/v2 v2.0.0-alpha.3\n\tgolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546\n\tgolang.org/x/mod v0.29.0\n\tgolang.org/x/oauth2 v0.32.0\n\tgolang.org/x/sync v0.17.0\n\tgolang.org/x/tools v0.38.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\t4d63.com/gocheckcompilerdirectives v1.3.0 // indirect\n\t4d63.com/gochecknoglobals v0.2.2 // indirect\n\tcodeberg.org/chavacava/garif v0.2.0 // indirect\n\tconnectrpc.com/connect v1.19.1 // indirect\n\tgithub.com/4meepo/tagalign v1.4.2 // indirect\n\tgithub.com/Abirdcfly/dupword v0.1.3 // indirect\n\tgithub.com/Antonboom/errname v1.0.0 // indirect\n\tgithub.com/Antonboom/nilnil v1.0.1 // indirect\n\tgithub.com/Antonboom/testifylint v1.5.2 // indirect\n\tgithub.com/BurntSushi/toml v1.5.0 // indirect\n\tgithub.com/Crocmagnon/fatcontext v0.7.1 // indirect\n\tgithub.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect\n\tgithub.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect\n\tgithub.com/InVisionApp/go-health/v2 v2.1.4 // indirect\n\tgithub.com/InVisionApp/go-logger v1.0.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.3.0 // indirect\n\tgithub.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect\n\tgithub.com/STARRY-S/zip v0.2.3 // indirect\n\tgithub.com/alecthomas/go-check-sumtype v0.3.1 // indirect\n\tgithub.com/alexkohler/nakedret/v2 v2.0.5 // indirect\n\tgithub.com/alexkohler/prealloc v1.0.0 // indirect\n\tgithub.com/alingse/asasalint v0.0.11 // indirect\n\tgithub.com/alingse/nilnesserr v0.1.2 // indirect\n\tgithub.com/andybalholm/brotli v1.2.0 // indirect\n\tgithub.com/ashanbrown/forbidigo v1.6.0 // indirect\n\tgithub.com/ashanbrown/makezero v1.2.0 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect\n\tgithub.com/aws/smithy-go v1.23.2 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/bahlo/generic-list-go v0.2.0 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/bkielbasa/cyclop v1.2.3 // indirect\n\tgithub.com/blizzy78/varnamelen v0.8.0 // indirect\n\tgithub.com/bodgit/plumbing v1.3.0 // indirect\n\tgithub.com/bodgit/sevenzip v1.6.1 // indirect\n\tgithub.com/bodgit/windows v1.0.1 // indirect\n\tgithub.com/bombsimon/wsl/v4 v4.5.0 // indirect\n\tgithub.com/breml/bidichk v0.3.2 // indirect\n\tgithub.com/breml/errchkjson v0.4.0 // indirect\n\tgithub.com/buger/jsonparser v1.1.1 // indirect\n\tgithub.com/butuzov/ireturn v0.3.1 // indirect\n\tgithub.com/butuzov/mirror v1.3.0 // indirect\n\tgithub.com/catenacyber/perfsprint v0.8.2 // indirect\n\tgithub.com/cavaliergopher/grab/v3 v3.0.1 // indirect\n\tgithub.com/ccojocar/zxcvbn-go v1.0.2 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/charithe/durationcheck v0.0.10 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.3.3 // indirect\n\tgithub.com/charmbracelet/lipgloss v1.1.0 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.10.3 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.13 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.2 // indirect\n\tgithub.com/ckaznocha/intrange v0.3.0 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.4.1 // indirect\n\tgithub.com/clipperhouse/stringish v0.1.1 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.3.0 // indirect\n\tgithub.com/codeclysm/extract/v4 v4.0.0 // indirect\n\tgithub.com/coreos/go-oidc/v3 v3.16.0 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/creack/pty v1.1.24 // indirect\n\tgithub.com/curioswitch/go-reassign v0.3.0 // indirect\n\tgithub.com/daixiang0/gci v0.13.5 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/denis-tingaikin/go-header v0.5.0 // indirect\n\tgithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect\n\tgithub.com/ettle/strcase v0.2.0 // indirect\n\tgithub.com/fatih/structtag v1.2.0 // indirect\n\tgithub.com/firefart/nonamedreturns v1.0.5 // indirect\n\tgithub.com/fzipp/gocyclo v0.6.0 // indirect\n\tgithub.com/ghostiam/protogetter v0.3.9 // indirect\n\tgithub.com/go-critic/go-critic v0.12.0 // indirect\n\tgithub.com/go-errors/errors v1.5.1 // indirect\n\tgithub.com/go-toolsmith/astcast v1.1.0 // indirect\n\tgithub.com/go-toolsmith/astcopy v1.1.0 // indirect\n\tgithub.com/go-toolsmith/astequal v1.2.0 // indirect\n\tgithub.com/go-toolsmith/astfmt v1.1.0 // indirect\n\tgithub.com/go-toolsmith/astp v1.1.0 // indirect\n\tgithub.com/go-toolsmith/strparse v1.1.0 // indirect\n\tgithub.com/go-toolsmith/typep v1.1.0 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/go-xmlfmt/xmlfmt v1.1.3 // indirect\n\tgithub.com/gobwas/glob v0.2.3 // indirect\n\tgithub.com/goccy/go-yaml v1.18.0 // indirect\n\tgithub.com/gofrs/flock v0.12.1 // indirect\n\tgithub.com/gofrs/uuid/v5 v5.4.0 // indirect\n\tgithub.com/golang/protobuf v1.5.3 // indirect\n\tgithub.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect\n\tgithub.com/golangci/go-printf-func-name v0.1.0 // indirect\n\tgithub.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect\n\tgithub.com/golangci/golangci-lint v1.64.8 // indirect\n\tgithub.com/golangci/misspell v0.6.0 // indirect\n\tgithub.com/golangci/plugin-module-register v0.1.1 // indirect\n\tgithub.com/golangci/revgrep v0.8.0 // indirect\n\tgithub.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect\n\tgithub.com/google/btree v1.1.3 // indirect\n\tgithub.com/google/go-github/v74 v74.0.0 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/google/renameio/v2 v2.0.0 // indirect\n\tgithub.com/gordonklaus/ineffassign v0.1.0 // indirect\n\tgithub.com/gosimple/slug v1.15.0 // indirect\n\tgithub.com/gosimple/unidecode v1.0.1 // indirect\n\tgithub.com/gostaticanalysis/analysisutil v0.7.1 // indirect\n\tgithub.com/gostaticanalysis/comment v1.5.0 // indirect\n\tgithub.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect\n\tgithub.com/gostaticanalysis/nilerr v0.1.1 // indirect\n\tgithub.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect\n\tgithub.com/h2non/filetype v1.1.3 // indirect\n\tgithub.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect\n\tgithub.com/hashicorp/go-version v1.7.0 // indirect\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/hexops/gotextdiff v1.0.3 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jgautheron/goconst v1.7.1 // indirect\n\tgithub.com/jingyugao/rowserrcheck v1.1.1 // indirect\n\tgithub.com/jjti/go-spancheck v0.6.4 // indirect\n\tgithub.com/juju/errors v1.0.0 // indirect\n\tgithub.com/julz/importas v0.2.0 // indirect\n\tgithub.com/karamaru-alpha/copyloopvar v1.2.1 // indirect\n\tgithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect\n\tgithub.com/kisielk/errcheck v1.9.0 // indirect\n\tgithub.com/kkHAIKE/contextcheck v1.1.6 // indirect\n\tgithub.com/klauspost/compress v1.18.1 // indirect\n\tgithub.com/klauspost/pgzip v1.2.6 // indirect\n\tgithub.com/kulti/thelper v0.6.3 // indirect\n\tgithub.com/kunwardeep/paralleltest v1.0.10 // indirect\n\tgithub.com/lasiar/canonicalheader v1.1.2 // indirect\n\tgithub.com/ldez/exptostd v0.4.2 // indirect\n\tgithub.com/ldez/gomoddirectives v0.6.1 // indirect\n\tgithub.com/ldez/grignotin v0.9.0 // indirect\n\tgithub.com/ldez/tagliatelle v0.7.1 // indirect\n\tgithub.com/ldez/usetesting v0.4.2 // indirect\n\tgithub.com/leonklingele/grouper v1.1.2 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0 // indirect\n\tgithub.com/macabu/inamedparam v0.1.3 // indirect\n\tgithub.com/magiconair/properties v1.8.6 // indirect\n\tgithub.com/mailru/easyjson v0.9.1 // indirect\n\tgithub.com/maratori/testableexamples v1.0.0 // indirect\n\tgithub.com/maratori/testpackage v1.1.1 // indirect\n\tgithub.com/matoous/godox v1.1.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.19 // indirect\n\tgithub.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect\n\tgithub.com/mgechev/revive v1.11.0 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect\n\tgithub.com/mikelolasagasti/xz v1.0.1 // indirect\n\tgithub.com/minio/minlz v1.0.1 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/moricho/tparallel v0.3.2 // indirect\n\tgithub.com/muesli/termenv v0.16.0 // indirect\n\tgithub.com/nakabonne/nestif v0.3.1 // indirect\n\tgithub.com/nishanths/exhaustive v0.12.0 // indirect\n\tgithub.com/nishanths/predeclared v0.2.2 // indirect\n\tgithub.com/nunnatsa/ginkgolinter v0.19.1 // indirect\n\tgithub.com/nwaples/rardecode/v2 v2.2.1 // indirect\n\tgithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect\n\tgithub.com/olekukonko/errors v1.1.0 // indirect\n\tgithub.com/olekukonko/ll v0.1.2 // indirect\n\tgithub.com/onsi/ginkgo v1.16.5 // indirect\n\tgithub.com/pelletier/go-toml v1.9.5 // indirect\n\tgithub.com/peterbourgon/diskv v2.0.1+incompatible // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/polyfloyd/go-errorlint v1.7.1 // indirect\n\tgithub.com/prometheus/client_golang v1.12.1 // indirect\n\tgithub.com/prometheus/client_model v0.2.0 // indirect\n\tgithub.com/prometheus/common v0.32.1 // indirect\n\tgithub.com/prometheus/procfs v0.7.3 // indirect\n\tgithub.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect\n\tgithub.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect\n\tgithub.com/quasilyte/gogrep v0.5.0 // indirect\n\tgithub.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect\n\tgithub.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect\n\tgithub.com/raeperd/recvcheck v0.2.0 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/rs/zerolog v1.34.0 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/ryancurrah/gomodguard v1.3.5 // indirect\n\tgithub.com/ryanrolds/sqlclosecheck v0.5.1 // indirect\n\tgithub.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect\n\tgithub.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect\n\tgithub.com/sashamelentyev/interfacebloat v1.1.0 // indirect\n\tgithub.com/sashamelentyev/usestdlibvars v1.28.0 // indirect\n\tgithub.com/securego/gosec/v2 v2.22.2 // indirect\n\tgithub.com/segmentio/backo-go v1.1.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/sivchari/containedctx v1.0.3 // indirect\n\tgithub.com/sivchari/tenv v1.12.1 // indirect\n\tgithub.com/sonatard/noctx v0.1.0 // indirect\n\tgithub.com/sorairolake/lzip-go v0.3.8 // indirect\n\tgithub.com/sourcegraph/go-diff v0.7.0 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.5.0 // indirect\n\tgithub.com/spf13/jwalterweatherman v1.1.0 // indirect\n\tgithub.com/spf13/viper v1.12.0 // indirect\n\tgithub.com/ssgreg/nlreturn/v2 v2.2.1 // indirect\n\tgithub.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/subosito/gotenv v1.4.1 // indirect\n\tgithub.com/tdakkota/asciicheck v0.4.1 // indirect\n\tgithub.com/tetafro/godot v1.5.0 // indirect\n\tgithub.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect\n\tgithub.com/timonwong/loggercheck v0.10.1 // indirect\n\tgithub.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect\n\tgithub.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect\n\tgithub.com/ulikunitz/xz v0.5.15 // indirect\n\tgithub.com/ultraware/funlen v0.2.0 // indirect\n\tgithub.com/ultraware/whitespace v0.2.0 // indirect\n\tgithub.com/uudashr/gocognit v1.2.0 // indirect\n\tgithub.com/uudashr/iface v1.3.1 // indirect\n\tgithub.com/xen0n/gosmopolitan v1.2.2 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgithub.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect\n\tgithub.com/yagipy/maintidx v1.0.0 // indirect\n\tgithub.com/yeya24/promlinter v0.3.0 // indirect\n\tgithub.com/ykadowak/zerologlint v0.1.5 // indirect\n\tgitlab.com/bosi/decorder v0.4.2 // indirect\n\tgo-simpler.org/musttag v0.13.0 // indirect\n\tgo-simpler.org/sloglint v0.9.0 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/automaxprocs v1.6.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.24.0 // indirect\n\tgo4.org v0.0.0-20230225012048-214862532bf5 // indirect\n\tgolang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b // indirect\n\tgolang.org/x/sys v0.37.0 // indirect\n\tgolang.org/x/term v0.36.0 // indirect\n\tgolang.org/x/text v0.30.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.10 // indirect\n\tgopkg.in/ini.v1 v1.67.0 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\thonnef.co/go/tools v0.7.0-0.dev.0.20250523013057-bbc2f4dd71ea // indirect\n\tmvdan.cc/gofumpt v0.8.0 // indirect\n\tmvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect\n)\n\ntool (\n\tgithub.com/golangci/golangci-lint/cmd/golangci-lint\n\tmvdan.cc/gofumpt\n)\n"
  },
  {
    "path": "go.sum",
    "content": "4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A=\n4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=\n4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=\n4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=\nal.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=\nal.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncodeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY=\ncodeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ=\nconnectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=\nconnectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E=\ngithub.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=\ngithub.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=\ngithub.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw=\ngithub.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=\ngithub.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=\ngithub.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA=\ngithub.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI=\ngithub.com/Antonboom/nilnil v1.0.1 h1:C3Tkm0KUxgfO4Duk3PM+ztPncTFlOf0b2qadmS0s4xs=\ngithub.com/Antonboom/nilnil v1.0.1/go.mod h1:CH7pW2JsRNFgEh8B2UaPZTEPhCMuFowP/e8Udp9Nnb0=\ngithub.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk=\ngithub.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=\ngithub.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/Crocmagnon/fatcontext v0.7.1 h1:SC/VIbRRZQeQWj/TcQBS6JmrXcfA+BU4OGSVUt54PjM=\ngithub.com/Crocmagnon/fatcontext v0.7.1/go.mod h1:1wMvv3NXEBJucFGfwOJBxSVWcoIO6emV215SMkW9MFU=\ngithub.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=\ngithub.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=\ngithub.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM=\ngithub.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=\ngithub.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k=\ngithub.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg=\ngithub.com/InVisionApp/go-health/v2 v2.1.4 h1:RjYUtnQWOMcqzQXzMvHgoSlSnpLyBx/7qcNTDN/Yk2s=\ngithub.com/InVisionApp/go-health/v2 v2.1.4/go.mod h1:FVZ3LZI4p+/XmkUQo9xAyvJaIoSVnr2iiq9AtFCXSc0=\ngithub.com/InVisionApp/go-logger v1.0.1 h1:WFL19PViM1mHUmUWfsv5zMo379KSWj2MRmBlzMFDRiE=\ngithub.com/InVisionApp/go-logger v1.0.1/go.mod h1:+cGTDSn+P8105aZkeOfIhdd7vFO5X1afUHcjvanY0L8=\ngithub.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A=\ngithub.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM=\ngithub.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=\ngithub.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=\ngithub.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=\ngithub.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=\ngithub.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=\ngithub.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=\ngithub.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=\ngithub.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=\ngithub.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=\ngithub.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=\ngithub.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=\ngithub.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=\ngithub.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU=\ngithub.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU=\ngithub.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=\ngithub.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=\ngithub.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=\ngithub.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=\ngithub.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=\ngithub.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=\ngithub.com/alingse/nilnesserr v0.1.2 h1:Yf8Iwm3z2hUUrP4muWfW83DF4nE3r1xZ26fGWUKCZlo=\ngithub.com/alingse/nilnesserr v0.1.2/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=\ngithub.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=\ngithub.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/arduino/go-paths-helper v1.13.1 h1:M7SCdLB2VldxOdChnjZkxAZwWZdDtNY4IlHL9nxGQFo=\ngithub.com/arduino/go-paths-helper v1.13.1/go.mod h1:dDodKn2ZX4iwuoBMapdDO+5d0oDLBeM4BS0xS4i40Ak=\ngithub.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY=\ngithub.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU=\ngithub.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU=\ngithub.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4=\ngithub.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=\ngithub.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=\ngithub.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=\ngithub.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4 h1:2fjfz3/G9BRvIKuNZ655GwzpklC2kEH0cowZQGO7uBg=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4/go.mod h1:Ymws824lvMypLFPwyyUXM52SXuGgxpu0+DISLfKvB+c=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=\ngithub.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=\ngithub.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=\ngithub.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=\ngithub.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=\ngithub.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=\ngithub.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=\ngithub.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=\ngithub.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=\ngithub.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=\ngithub.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=\ngithub.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=\ngithub.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=\ngithub.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=\ngithub.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=\ngithub.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=\ngithub.com/bombsimon/wsl/v4 v4.5.0 h1:iZRsEvDdyhd2La0FVi5k6tYehpOR/R7qIUjmKk7N74A=\ngithub.com/bombsimon/wsl/v4 v4.5.0/go.mod h1:NOQ3aLF4nD7N5YPXMruR6ZXDOAqLoM0GEpLwTdvmOSc=\ngithub.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=\ngithub.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs=\ngithub.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos=\ngithub.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk=\ngithub.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8=\ngithub.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w=\ngithub.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=\ngithub.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=\ngithub.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\ngithub.com/butuzov/ireturn v0.3.1 h1:mFgbEI6m+9W8oP/oDdfA34dLisRFCj2G6o/yiI1yZrY=\ngithub.com/butuzov/ireturn v0.3.1/go.mod h1:ZfRp+E7eJLC0NQmk1Nrm1LOrn/gQlOykv+cVPdiXH5M=\ngithub.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc=\ngithub.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI=\ngithub.com/catenacyber/perfsprint v0.8.2 h1:+o9zVmCSVa7M4MvabsWvESEhpsMkhfE7k0sHNGL95yw=\ngithub.com/catenacyber/perfsprint v0.8.2/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM=\ngithub.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=\ngithub.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=\ngithub.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg=\ngithub.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4=\ngithub.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ=\ngithub.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=\ngithub.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.10.3 h1:3WoV9XN8uMEnFRZZ+vBPRy59TaIWa+gJodS4Vg5Fut0=\ngithub.com/charmbracelet/x/ansi v0.10.3/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=\ngithub.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=\ngithub.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=\ngithub.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=\ngithub.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd5ZY=\ngithub.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/clipperhouse/displaywidth v0.4.1 h1:uVw9V8UDfnggg3K2U84VWY1YLQ/x2aKSCtkRyYozfoU=\ngithub.com/clipperhouse/displaywidth v0.4.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=\ngithub.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=\ngithub.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=\ngithub.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=\ngithub.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/codeclysm/extract/v4 v4.0.0 h1:H87LFsUNaJTu2e/8p/oiuiUsOK/TaPQ5wxsjPnwPEIY=\ngithub.com/codeclysm/extract/v4 v4.0.0/go.mod h1:SFju1lj6as7FvUgalpSct7torJE0zttbJUWtryPRG6s=\ngithub.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=\ngithub.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=\ngithub.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=\ngithub.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c=\ngithub.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=\ngithub.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=\ngithub.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=\ngithub.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=\ngithub.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=\ngithub.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=\ngithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=\ngithub.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=\ngithub.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=\ngithub.com/f1bonacc1/process-compose v1.64.1 h1:584tuUOiXMdy1st4yGStDSbVq40r0yhTWfNyOKypsO8=\ngithub.com/f1bonacc1/process-compose v1.64.1/go.mod h1:nuIgmcZ4/NmQSK9P3uBOpDf7F2PrNGQOWaOyX5FoJRM=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=\ngithub.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=\ngithub.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA=\ngithub.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw=\ngithub.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=\ngithub.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=\ngithub.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=\ngithub.com/getsentry/sentry-go v0.36.2 h1:uhuxRPTrUy0dnSzTd0LrYXlBYygLkKY0hhlG5LXarzM=\ngithub.com/getsentry/sentry-go v0.36.2/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c=\ngithub.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ=\ngithub.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=\ngithub.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=\ngithub.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w=\ngithub.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w=\ngithub.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=\ngithub.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=\ngithub.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=\ngithub.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=\ngithub.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=\ngithub.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=\ngithub.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=\ngithub.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=\ngithub.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=\ngithub.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=\ngithub.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=\ngithub.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=\ngithub.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=\ngithub.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=\ngithub.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=\ngithub.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=\ngithub.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=\ngithub.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk=\ngithub.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus=\ngithub.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=\ngithub.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=\ngithub.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=\ngithub.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=\ngithub.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=\ngithub.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=\ngithub.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=\ngithub.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=\ngithub.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=\ngithub.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=\ngithub.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=\ngithub.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=\ngithub.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=\ngithub.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw=\ngithub.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=\ngithub.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU=\ngithub.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s=\ngithub.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=\ngithub.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=\ngithub.com/golangci/golangci-lint v1.64.8 h1:y5TdeVidMtBGG32zgSC7ZXTFNHrsJkDnpO4ItB3Am+I=\ngithub.com/golangci/golangci-lint v1.64.8/go.mod h1:5cEsUQBSr6zi8XI8OjmcY2Xmliqc4iYL7YoPrL+zLJ4=\ngithub.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs=\ngithub.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo=\ngithub.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=\ngithub.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc=\ngithub.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=\ngithub.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=\ngithub.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs=\ngithub.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ=\ngithub.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM=\ngithub.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak=\ngithub.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=\ngithub.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=\ngithub.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=\ngithub.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=\ngithub.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=\ngithub.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=\ngithub.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=\ngithub.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=\ngithub.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=\ngithub.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=\ngithub.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=\ngithub.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=\ngithub.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8=\ngithub.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc=\ngithub.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk=\ngithub.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY=\ngithub.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk=\ngithub.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A=\ngithub.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=\ngithub.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8=\ngithub.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=\ngithub.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=\ngithub.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=\ngithub.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=\ngithub.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY=\ngithub.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc=\ngithub.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=\ngithub.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=\ngithub.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=\ngithub.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY=\ngithub.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk=\ngithub.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4=\ngithub.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs=\ngithub.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c=\ngithub.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc=\ngithub.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=\ngithub.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=\ngithub.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=\ngithub.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI=\ngithub.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M=\ngithub.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=\ngithub.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=\ngithub.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=\ngithub.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=\ngithub.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=\ngithub.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=\ngithub.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs=\ngithub.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I=\ngithub.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs=\ngithub.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY=\ngithub.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4=\ngithub.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI=\ngithub.com/ldez/exptostd v0.4.2 h1:l5pOzHBz8mFOlbcifTxzfyYbgEmoUqjxLFHZkjlbHXs=\ngithub.com/ldez/exptostd v0.4.2/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ=\ngithub.com/ldez/gomoddirectives v0.6.1 h1:Z+PxGAY+217f/bSGjNZr/b2KTXcyYLgiWI6geMBN2Qc=\ngithub.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs=\ngithub.com/ldez/grignotin v0.9.0 h1:MgOEmjZIVNn6p5wPaGp/0OKWyvq42KnzAt/DAb8O4Ow=\ngithub.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk=\ngithub.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk=\ngithub.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I=\ngithub.com/ldez/usetesting v0.4.2 h1:J2WwbrFGk3wx4cZwSMiCQQ00kjGR0+tuuyW0Lqm4lwA=\ngithub.com/ldez/usetesting v0.4.2/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ=\ngithub.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=\ngithub.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=\ngithub.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=\ngithub.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk=\ngithub.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I=\ngithub.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=\ngithub.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=\ngithub.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=\ngithub.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI=\ngithub.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE=\ngithub.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04=\ngithub.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc=\ngithub.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=\ngithub.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=\ngithub.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=\ngithub.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=\ngithub.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/mgechev/revive v1.11.0 h1:b/gLLpBE427o+Xmd8G58gSA+KtBwxWinH/A565Awh0w=\ngithub.com/mgechev/revive v1.11.0/go.mod h1:tI0oLF/2uj+InHCBLrrqfTKfjtFTBCFFfG05auyzgdw=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=\ngithub.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=\ngithub.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=\ngithub.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=\ngithub.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=\ngithub.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=\ngithub.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=\ngithub.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=\ngithub.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg=\ngithub.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=\ngithub.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=\ngithub.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=\ngithub.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4=\ngithub.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s=\ngithub.com/nwaples/rardecode/v2 v2.2.1 h1:DgHK/O/fkTQEKBJxBMC5d9IU8IgauifbpG78+rZJMnI=\ngithub.com/nwaples/rardecode/v2 v2.2.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=\ngithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=\ngithub.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=\ngithub.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=\ngithub.com/olekukonko/ll v0.1.2 h1:lkg/k/9mlsy0SxO5aC+WEpbdT5K83ddnNhAepz7TQc0=\ngithub.com/olekukonko/ll v0.1.2/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=\ngithub.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=\ngithub.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=\ngithub.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=\ngithub.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=\ngithub.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=\ngithub.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=\ngithub.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=\ngithub.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=\ngithub.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=\ngithub.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=\ngithub.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=\ngithub.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=\ngithub.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=\ngithub.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=\ngithub.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/polyfloyd/go-errorlint v1.7.1 h1:RyLVXIbosq1gBdk/pChWA8zWYLsq9UEw7a1L5TVMCnA=\ngithub.com/polyfloyd/go-errorlint v1.7.1/go.mod h1:aXjNb1x2TNhoLsk26iv1yl7a+zTnXPhwEMtEXukiLR8=\ngithub.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=\ngithub.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=\ngithub.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=\ngithub.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=\ngithub.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo=\ngithub.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI=\ngithub.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=\ngithub.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=\ngithub.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=\ngithub.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=\ngithub.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=\ngithub.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=\ngithub.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=\ngithub.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=\ngithub.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI=\ngithub.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=\ngithub.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=\ngithub.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU=\ngithub.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE=\ngithub.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU=\ngithub.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=\ngithub.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=\ngithub.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=\ngithub.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=\ngithub.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=\ngithub.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=\ngithub.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=\ngithub.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ=\ngithub.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8=\ngithub.com/securego/gosec/v2 v2.22.2 h1:IXbuI7cJninj0nRpZSLCUlotsj8jGusohfONMrHoF6g=\ngithub.com/securego/gosec/v2 v2.22.2/go.mod h1:UEBGA+dSKb+VqM6TdehR7lnQtIIMorYJ4/9CW1KVQBE=\ngithub.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80=\ngithub.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=\ngithub.com/segmentio/backo-go v1.1.0 h1:cJIfHQUdmLsd8t9IXqf5J8SdrOMn9vMa7cIvOavHAhc=\ngithub.com/segmentio/backo-go v1.1.0/go.mod h1:ckenwdf+v/qbyhVdNPWHnqh2YdJBED1O9cidYyM5J18=\ngithub.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=\ngithub.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=\ngithub.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=\ngithub.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=\ngithub.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY=\ngithub.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw=\ngithub.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM=\ngithub.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c=\ngithub.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=\ngithub.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=\ngithub.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=\ngithub.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=\ngithub.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=\ngithub.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=\ngithub.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=\ngithub.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=\ngithub.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=\ngithub.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=\ngithub.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=\ngithub.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4=\ngithub.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=\ngithub.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=\ngithub.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a h1:a6TNDN9CgG+cYjaeN8l2mc4kSz2iMiCDQxPEyltUV/I=\ngithub.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=\ngithub.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8=\ngithub.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8=\ngithub.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=\ngithub.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=\ngithub.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=\ngithub.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=\ngithub.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw=\ngithub.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio=\ngithub.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 h1:y4mJRFlM6fUyPhoXuFg/Yu02fg/nIPFMOY8tOqppoFg=\ngithub.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=\ngithub.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg=\ngithub.com/timonwong/loggercheck v0.10.1/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8=\ngithub.com/tomarrell/wrapcheck/v2 v2.10.0 h1:SzRCryzy4IrAH7bVGG4cK40tNUhmVmMDuJujy4XwYDg=\ngithub.com/tomarrell/wrapcheck/v2 v2.10.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo=\ngithub.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=\ngithub.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=\ngithub.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=\ngithub.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI=\ngithub.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA=\ngithub.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g=\ngithub.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=\ngithub.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA=\ngithub.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU=\ngithub.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U=\ngithub.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=\ngithub.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU=\ngithub.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=\ngithub.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngithub.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=\ngithub.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=\ngithub.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=\ngithub.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=\ngithub.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=\ngithub.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=\ngithub.com/zaffka/mongodb-boltdb-mock v0.0.0-20221014194232-b4bb03fbe3a0/go.mod h1:GsDD1qsG+86MeeCG7ndi6Ei3iGthKL3wQ7PTFigDfNY=\ngithub.com/zealic/go2node v0.1.0 h1:ofxpve08cmLJBwFdI0lPCk9jfwGWOSD+s6216x0oAaA=\ngithub.com/zealic/go2node v0.1.0/go.mod h1:GrkFr+HctXwP7vzcU9RsgtAeJjTQ6Ud0IPCQAqpTfBg=\ngitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=\ngitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=\ngo-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=\ngo-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28=\ngo-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE=\ngo-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM=\ngo-simpler.org/sloglint v0.9.0 h1:/40NQtjRx9txvsB/RN022KsUJU+zaaSb/9q9BSefSrE=\ngo-simpler.org/sloglint v0.9.0/go.mod h1:G/OrAF6uxj48sHahCzrbarVMptL2kjWTaUeC8+fOGww=\ngo.jetify.com/envsec v0.0.16-0.20250821201727-a2fd18dc57f6 h1:Fhs8rDyzZ2HQP3goT+oGmxzWxhbLLDG+YMBg5NwbNUc=\ngo.jetify.com/envsec v0.0.16-0.20250821201727-a2fd18dc57f6/go.mod h1:vP3Q43DKSkYvpKfht1qe/nsoNpWc7/mSVZuYADJumRA=\ngo.jetify.com/pkg v0.0.0-20250904024813-5ec17279258b h1:Zf2By95vBkBvGZRMtQcxpT9YGGBpfRRT7UkjYgpaZ94=\ngo.jetify.com/pkg v0.0.0-20250904024813-5ec17279258b/go.mod h1:MOe1T830pQEWt1U+JtUwho7f3pXsoWIXrJT6+HdXCOQ=\ngo.jetify.com/typeid/v2 v2.0.0-alpha.3 h1:T6RPx6bNl10lp0JN2Xz/XcgLZWSlVmL58Xqy9cgTCcc=\ngo.jetify.com/typeid/v2 v2.0.0-alpha.3/go.mod h1:zfD1ZDHDJNgXZANsO9jDOD81XRRQ0zAOnDBEHmIV/Gw=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=\ngo.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=\ngo.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=\ngo4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=\ngo4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=\ngolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=\ngolang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=\ngolang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=\ngolang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b h1:KdrhdYPDUvJTvrDK9gdjfFd6JTk8vA1WJoldYSi0kHo=\ngolang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=\ngolang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=\ngolang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=\ngolang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=\ngolang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=\ngolang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=\ngolang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=\ngolang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=\ngolang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=\ngolang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=\ngolang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=\ngolang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=\ngolang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=\ngolang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=\ngolang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=\ngolang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=\ngolang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=\ngolang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.7.0-0.dev.0.20250523013057-bbc2f4dd71ea h1:fj8r9irJSpolAGUdZBxJIRY3lLc4jH2Dt4lwnWyWwpw=\nhonnef.co/go/tools v0.7.0-0.dev.0.20250523013057-bbc2f4dd71ea/go.mod h1:EPDDhEZqVHhWuPI5zPAsjU0U7v9xNIWjoOVyZ5ZcniQ=\nmvdan.cc/gofumpt v0.8.0 h1:nZUCeC2ViFaerTcYKstMmfysj6uhQrA2vJe+2vwGU6k=\nmvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg=\nmvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U=\nmvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\n"
  },
  {
    "path": "internal/boxcli/add.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/nix\"\n)\n\nconst toSearchForPackages = \"To search for packages, use the `devbox search` command\"\n\ntype addCmdFlags struct {\n\tconfig           configFlags\n\tallowInsecure    []string\n\tdisablePlugin    bool\n\tplatforms        []string\n\texcludePlatforms []string\n\tpatchGlibc       bool\n\tpatch            string\n\toutputs          []string\n}\n\nfunc addCmd() *cobra.Command {\n\tflags := addCmdFlags{}\n\n\tcommand := &cobra.Command{\n\t\tUse:     \"add <pkg>...\",\n\t\tShort:   \"Add a new package to your devbox\",\n\t\tPreRunE: ensureNixInstalled,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif len(args) == 0 {\n\t\t\t\tfmt.Fprintf(\n\t\t\t\t\tcmd.ErrOrStderr(),\n\t\t\t\t\t\"Usage: %s\\n\\n%s\\n\",\n\t\t\t\t\tcmd.UseLine(),\n\t\t\t\t\ttoSearchForPackages,\n\t\t\t\t)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\terr := addCmdFunc(cmd, args, flags)\n\t\t\tif errors.Is(err, nix.ErrPackageNotFound) {\n\t\t\t\treturn usererr.WithUserMessage(err, toSearchForPackages)\n\t\t\t}\n\t\t\treturn err\n\t\t},\n\t}\n\n\tflags.config.register(command)\n\tcommand.Flags().StringSliceVar(\n\t\t&flags.allowInsecure, \"allow-insecure\", []string{},\n\t\t\"allow adding packages marked as insecure.\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.disablePlugin, \"disable-plugin\", false,\n\t\t\"disable plugin (if any) for this package.\")\n\tcommand.Flags().StringSliceVarP(\n\t\t&flags.platforms, \"platform\", \"p\", []string{},\n\t\t\"add packages to run on only this platform.\")\n\tcommand.Flags().StringSliceVarP(\n\t\t&flags.excludePlatforms, \"exclude-platform\", \"e\", []string{},\n\t\t\"exclude packages from a specific platform.\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.patchGlibc, \"patch-glibc\", false,\n\t\t\"patch any ELF binaries to use the latest glibc version in nixpkgs\")\n\tcommand.Flags().StringVar(\n\t\t&flags.patch, \"patch\", \"auto\",\n\t\t\"allow Devbox to patch the package to fix known issues (auto, always, never)\")\n\tcommand.Flags().StringSliceVarP(\n\t\t&flags.outputs, \"outputs\", \"o\", []string{},\n\t\t\"specify the outputs to select for the nix package\")\n\n\t_ = command.Flags().MarkDeprecated(\"patch-glibc\", `use --patch=always instead`)\n\tcommand.MarkFlagsMutuallyExclusive(\"patch\", \"patch-glibc\")\n\n\treturn command\n}\n\nfunc addCmdFunc(cmd *cobra.Command, args []string, flags addCmdFlags) error {\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\topts := devopt.AddOpts{\n\t\tAllowInsecure:    flags.allowInsecure,\n\t\tDisablePlugin:    flags.disablePlugin,\n\t\tPlatforms:        flags.platforms,\n\t\tExcludePlatforms: flags.excludePlatforms,\n\t\tPatch:            flags.patch,\n\t\tOutputs:          flags.outputs,\n\t}\n\tif flags.patchGlibc {\n\t\t// Backwards compatibility so --patch-glibc still works.\n\t\topts.Patch = \"always\"\n\t}\n\treturn box.Add(cmd.Context(), args, opts)\n}\n"
  },
  {
    "path": "internal/boxcli/args.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Functions that help parse arguments\n\nfunc pathArg(args []string) string {\n\tif len(args) > 0 {\n\t\tp, err := filepath.Abs(args[0])\n\t\tif err != nil {\n\t\t\t// Can occur when the current working directory cannot be determined.\n\t\t\tpanic(errors.WithStack(err))\n\t\t}\n\t\treturn p\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/boxcli/auth.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devbox/providers/identity\"\n\t\"go.jetify.com/devbox/internal/ux\"\n\t\"go.jetify.com/pkg/api\"\n)\n\nfunc authCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"auth\",\n\t\tShort: \"Devbox auth commands\",\n\t}\n\n\tcmd.AddCommand(loginCmd())\n\tcmd.AddCommand(logoutCmd())\n\tcmd.AddCommand(whoAmICmd())\n\tcmd.AddCommand(authNewTokenCommand())\n\n\treturn cmd\n}\n\nfunc loginCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"login\",\n\t\tShort: \"Login to devbox\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tc, err := identity.AuthClient(identity.AuthRedirectDefault)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tt, err := c.LoginFlow()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// TODO: all uses of IDClaims() are broken when using a static\n\t\t\t// non-expiring token (i.e. API_TOKEN)\n\t\t\tfmt.Fprintf(cmd.ErrOrStderr(), \"Logged in as: %s\\n\", t.IDClaims().Email)\n\t\t\treturn nil\n\t\t},\n\t}\n\n\treturn cmd\n}\n\nfunc logoutCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"logout\",\n\t\tShort: \"Logout from devbox\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tc, err := identity.AuthClient(identity.AuthRedirectDefault)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\terr = c.LogoutFlow()\n\t\t\tif err == nil {\n\t\t\t\tfmt.Fprintln(cmd.OutOrStdout(), \"Logged out successfully\")\n\t\t\t}\n\t\t\treturn err\n\t\t},\n\t}\n\n\treturn cmd\n}\n\ntype whoAmICmdFlags struct {\n\tshowTokens bool\n}\n\nfunc whoAmICmd() *cobra.Command {\n\tflags := &whoAmICmdFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:   \"whoami\",\n\t\tShort: \"Show the current user\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\twd, err := os.Getwd()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tbox, err := devbox.Open(&devopt.Opts{Dir: wd, Stderr: cmd.ErrOrStderr()})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// TODO: WhoAmI should be a function in opensource/pkg/auth that takes in a session.\n\t\t\t// That way we don't need to handle failed refresh token errors here.\n\t\t\terr = box.UninitializedSecrets(cmd.Context()).\n\t\t\t\tWhoAmI(cmd.Context(), cmd.OutOrStdout(), flags.showTokens)\n\t\t\tif identity.IsRefreshTokenError(err) {\n\t\t\t\tux.Fwarningf(cmd.ErrOrStderr(), \"Your session is expired. Please login again.\\n\")\n\t\t\t\treturn loginCmd().RunE(cmd, args)\n\t\t\t}\n\t\t\treturn err\n\t\t},\n\t}\n\n\tcmd.Flags().BoolVar(\n\t\t&flags.showTokens,\n\t\t\"show-tokens\",\n\t\tfalse,\n\t\t\"Show the access, id, and refresh tokens\",\n\t)\n\n\treturn cmd\n}\n\nfunc authNewTokenCommand() *cobra.Command {\n\ttokensCmd := &cobra.Command{\n\t\tUse:   \"tokens\",\n\t\tShort: \"Manage devbox auth tokens\",\n\t}\n\n\tnewCmd := &cobra.Command{\n\t\tUse:   \"new\",\n\t\tShort: \"Create a new token\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tctx := cmd.Context()\n\t\t\ttoken, err := identity.GenSession(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tclient := api.NewClient(ctx, build.JetpackAPIHost(), token)\n\t\t\tpat, err := client.CreateToken(ctx)\n\t\t\tif err != nil {\n\t\t\t\t// This is a hack because errors are not returning with correct code.\n\t\t\t\t// Once that is fixed, we can switch to use *connect.Error Code() instead.\n\t\t\t\tif strings.Contains(err.Error(), \"permission_denied\") {\n\t\t\t\t\tux.Ferrorf(\n\t\t\t\t\t\tcmd.ErrOrStderr(),\n\t\t\t\t\t\t\"You do not have permission to create a token. Please contact your\"+\n\t\t\t\t\t\t\t\" administrator.\",\n\t\t\t\t\t)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tux.Fsuccessf(cmd.OutOrStdout(), \"Token created.\\n\\n\")\n\t\t\ttable := tablewriter.NewWriter(cmd.OutOrStdout())\n\t\t\t// Row lines are configured through the renderer in the new API\n\t\t\tif err := table.Bulk([][]string{\n\t\t\t\t{\"Token ID\", pat.GetToken().GetId()},\n\t\t\t\t{\"Secret\", pat.GetToken().GetSecret()},\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn table.Render()\n\t\t},\n\t}\n\n\ttokensCmd.AddCommand(newCmd)\n\n\treturn tokensCmd\n}\n"
  },
  {
    "path": "internal/boxcli/cache.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os/user\"\n\t\"slices\"\n\n\t\"github.com/MakeNowJust/heredoc/v2\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"github.com/spf13/cobra\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devbox/providers/identity\"\n\t\"go.jetify.com/devbox/internal/devbox/providers/nixcache\"\n\tnixv1alpha1 \"go.jetify.com/pkg/api/gen/priv/nix/v1alpha1\"\n)\n\ntype cacheFlags struct {\n\tpathFlag\n\tto string\n}\n\ntype credentialsFlags struct {\n\tformat string\n}\n\nfunc cacheCmd() *cobra.Command {\n\tflags := cacheFlags{}\n\tcacheCommand := &cobra.Command{\n\t\tUse:               \"cache\",\n\t\tShort:             \"Collection of commands to interact with nix cache\",\n\t\tPersistentPreRunE: ensureNixInstalled,\n\t}\n\n\tuploadCommand := &cobra.Command{\n\t\tUse:     \"upload [installable]\",\n\t\tAliases: []string{\"copy\"}, // This mimics the nix command\n\t\tShort:   \"upload specified or nix packages in current project to cache\",\n\t\tLong: heredoc.Doc(`\n\t\t\tUpload specified nix installable or nix packages in current project to cache.\n\t\t\tIf [installable] is provided, only that installable will be uploaded.\n\t\t\tOtherwise, all packages in the project will be uploaded.\n\t\t\tTo upload to specific cache, use --to flag. Otherwise, a cache from\n\t\t\tthe cache provider will be used, if available.\n\t\t`),\n\t\tArgs: cobra.MaximumNArgs(1),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif len(args) > 0 {\n\t\t\t\treturn devbox.UploadInstallableToCache(\n\t\t\t\t\tcmd.Context(), cmd.ErrOrStderr(), flags.to, args[0],\n\t\t\t\t)\n\t\t\t}\n\t\t\tbox, err := devbox.Open(&devopt.Opts{\n\t\t\t\tDir:    flags.path,\n\t\t\t\tStderr: cmd.ErrOrStderr(),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\treturn box.UploadProjectToCache(cmd.Context(), flags.to)\n\t\t},\n\t}\n\n\tflags.pathFlag.register(uploadCommand)\n\tuploadCommand.Flags().StringVar(\n\t\t&flags.to, \"to\", \"\", \"URI of the cache to copy to\")\n\n\tcacheCommand.AddCommand(uploadCommand)\n\tcacheCommand.AddCommand(cacheConfigureCmd())\n\tcacheCommand.AddCommand(cacheCredentialsCmd())\n\tcacheCommand.AddCommand(cacheEnableCmd())\n\tcacheCommand.AddCommand(cacheInfoCmd())\n\n\treturn cacheCommand\n}\n\nfunc cacheConfigureCmd() *cobra.Command {\n\tusername := \"\"\n\tcmd := &cobra.Command{\n\t\tUse:   \"configure\",\n\t\tShort: \"Configure Nix to use the Devbox cache as a substituter\",\n\t\tLong: heredoc.Doc(`\n\t\t\tConfigure Nix to use the Devbox cache as a substituter.\n\n\t\t\tIf the current Nix installation is multi-user, this command grants the Nix\n\t\t\tdaemon access to Devbox caches by making the following changes:\n\n\t\t\t- Adds the current user to Nix's list of trusted users in the system nix.conf.\n\t\t\t- Adds the cache credentials to ~root/.aws/config.\n\n\t\t\tConfiguration requires sudo, but only needs to happen once. The changes persist\n\t\t\tacross Devbox accounts and organizations.\n\n\t\t\tThis command is a no-op for single-user Nix installs that aren't running the\n\t\t\tNix daemon.\n\t\t`),\n\t\tHidden: true,\n\t\tArgs:   cobra.MaximumNArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif username == \"\" {\n\t\t\t\tu, _ := user.Current()\n\t\t\t\tusername = u.Username\n\t\t\t}\n\t\t\treturn nixcache.ConfigureReprompt(cmd.Context(), username)\n\t\t},\n\t}\n\tcmd.Flags().StringVar(&username, \"user\", \"\", \"\")\n\treturn cmd\n}\n\nfunc cacheCredentialsCmd() *cobra.Command {\n\tflags := credentialsFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:    \"credentials\",\n\t\tShort:  \"Output S3 cache credentials\",\n\t\tHidden: true,\n\t\tArgs:   cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tcreds, err := nixcache.CachedCredentials(cmd.Context())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif flags.format == \"sh\" {\n\t\t\t\tfmt.Printf(\"export AWS_ACCESS_KEY_ID=%q\\n\", creds.AccessKeyID)\n\t\t\t\tfmt.Printf(\"export AWS_SECRET_ACCESS_KEY=%q\\n\", creds.SecretAccessKey)\n\t\t\t\tfmt.Printf(\"export AWS_SESSION_TOKEN=%q\\n\", creds.SessionToken)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tout, err := json.Marshal(creds)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t_, err = cmd.OutOrStdout().Write(out)\n\t\t\treturn err\n\t\t},\n\t}\n\tcmd.Flags().StringVar(&flags.format, \"format\", \"json\", \"Output format, either json or sh\")\n\treturn cmd\n}\n\nfunc cacheEnableCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"enable\",\n\t\tShort: \"Enable the Devbox Nix cache for your account\",\n\t\tLong: heredoc.Doc(`\n\t\t\tSign up or log in to a Jetify Cloud account and configure Nix to use the\n\t\t\taccount's Nix cache.\n\n\t\t\tFor more about how Devbox configures Nix, see \"devbox cache configure -h\".\n\t\t`),\n\t\tArgs: cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tauth, err := identity.AuthClient(identity.AuthRedirectCache)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tsess, _ := auth.GetSessions()\n\t\t\tneedLogin := len(sess) == 0\n\t\t\tif needLogin {\n\t\t\t\t_, err = auth.LoginFlow()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tneedConfigure := !nixcache.IsConfigured(cmd.Context())\n\t\t\tif needConfigure {\n\t\t\t\tu, _ := user.Current()\n\t\t\t\terr = nixcache.ConfigureReprompt(cmd.Context(), u.Username)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !needConfigure && !needLogin {\n\t\t\t\tfmt.Fprintln(cmd.OutOrStdout(), \"The Devbox cache is already enabled for your account.\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\treturn cmd\n}\n\nfunc cacheInfoCmd() *cobra.Command {\n\treturn &cobra.Command{\n\t\tUse:   \"info\",\n\t\tShort: \"Output information about the nix cache\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\t// TODO(gcurtis): We can also output info about the daemon config status\n\t\t\t// here\n\t\t\tcaches, err := nixcache.Caches(cmd.Context())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif len(caches) == 0 {\n\t\t\t\tfmt.Fprintln(cmd.OutOrStdout(), \"No cache configured\")\n\t\t\t}\n\t\t\tfor _, cache := range caches {\n\t\t\t\tisReadOnly := !slices.Contains(\n\t\t\t\t\tcache.GetPermissions(),\n\t\t\t\t\tnixv1alpha1.Permission_PERMISSION_WRITE,\n\t\t\t\t)\n\t\t\t\tfmt.Fprintf(\n\t\t\t\t\tcmd.OutOrStdout(),\n\t\t\t\t\t\"* %s %s\\n\",\n\t\t\t\t\tcache.GetUri(),\n\t\t\t\t\tlo.Ternary(isReadOnly, \"(read-only)\", \"\"),\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/boxcli/config.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\n// to be composed into xyzCmdFlags structs\ntype configFlags struct {\n\tpathFlag\n\tenvironment string\n}\n\nfunc (flags *configFlags) register(cmd *cobra.Command) {\n\tflags.pathFlag.register(cmd)\n\tcmd.Flags().StringVar(\n\t\t&flags.environment, \"environment\", \"dev\", \"environment to use, when supported (e.g.secrets support dev, prod, preview.)\",\n\t)\n}\n\nfunc (flags *configFlags) registerPersistent(cmd *cobra.Command) {\n\tflags.pathFlag.registerPersistent(cmd)\n\tcmd.PersistentFlags().StringVar(\n\t\t&flags.environment, \"environment\", \"dev\", \"environment to use, when supported (e.g. secrets support dev, prod, preview.)\",\n\t)\n}\n\n// pathFlag is a flag for specifying the path to a devbox.json file\ntype pathFlag struct {\n\tpath string\n}\n\nfunc (flags *pathFlag) register(cmd *cobra.Command) {\n\tcmd.Flags().StringVarP(\n\t\t&flags.path, \"config\", \"c\", \"\", \"path to directory containing a devbox.json config file\",\n\t)\n}\n\nfunc (flags *pathFlag) registerPersistent(cmd *cobra.Command) {\n\tcmd.PersistentFlags().StringVarP(\n\t\t&flags.path, \"config\", \"c\", \"\", \"path to directory containing a devbox.json config file\",\n\t)\n}\n"
  },
  {
    "path": "internal/boxcli/create.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/templates\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\ntype createCmdFlags struct {\n\tshowAll  bool\n\ttemplate string\n\trepo     string\n\tsubdir   string\n}\n\nfunc createCmd() *cobra.Command {\n\tflags := &createCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"create [dir] --template <template>\",\n\t\tShort: \"Initialize a directory as a devbox project using a template\",\n\t\tArgs:  cobra.MaximumNArgs(1),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif flags.template == \"\" && flags.repo == \"\" {\n\t\t\t\tfmt.Fprintf(\n\t\t\t\t\tcmd.ErrOrStderr(),\n\t\t\t\t\t\"Usage: devbox create [dir] --template <template>\\n\\n\",\n\t\t\t\t)\n\t\t\t\ttemplates.List(cmd.ErrOrStderr(), flags.showAll)\n\t\t\t\tif !flags.showAll {\n\t\t\t\t\tfmt.Fprintf(\n\t\t\t\t\t\tcmd.ErrOrStderr(),\n\t\t\t\t\t\t\"\\nTo see all available templates, run `devbox create --show-all`\\n\",\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn runCreateCmd(cmd, args, flags)\n\t\t},\n\t}\n\n\tcommand.Flags().StringVarP(\n\t\t&flags.template, \"template\", \"t\", \"\",\n\t\t\"template to initialize the project with\",\n\t)\n\tcommand.Flags().BoolVar(\n\t\t&flags.showAll, \"show-all\", false,\n\t\t\"show all available templates\",\n\t)\n\tcommand.Flags().StringVarP(\n\t\t&flags.repo, \"repo\", \"r\", \"\",\n\t\t\"Git repository HTTPS URL to import template files from. Example: https://github.com/jetify-com/devbox\",\n\t)\n\tcommand.Flags().StringVarP(\n\t\t&flags.subdir, \"subdir\", \"s\", \"\",\n\t\t\"Subdirectory of the Git repository in which the template files reside. Example: examples/tutorial\",\n\t)\n\t// this command marks a flag as hidden. Error handling for it is not necessary.\n\t_ = command.Flags().MarkHidden(\"repo\")\n\t_ = command.Flags().MarkHidden(\"subdir\")\n\n\treturn command\n}\n\nfunc runCreateCmd(cmd *cobra.Command, args []string, flags *createCmdFlags) error {\n\tpath := handlePath(args, flags)\n\n\tvar err error\n\tif flags.template != \"\" {\n\t\terr = templates.InitFromName(cmd.ErrOrStderr(), flags.template, path)\n\t} else if flags.repo != \"\" {\n\t\terr = templates.InitFromRepo(cmd.ErrOrStderr(), flags.repo, flags.subdir, path)\n\t} else {\n\t\terr = usererr.New(\"either --template or --repo need to be specified\")\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tux.Fsuccessf(\n\t\tcmd.ErrOrStderr(),\n\t\t\"Initialized devbox project using template %s\\n\",\n\t\tflags.template,\n\t)\n\n\treturn nil\n}\n\nfunc handlePath(args []string, flags *createCmdFlags) string {\n\tpath := pathArg(args)\n\twd, _ := os.Getwd()\n\tif path == \"\" {\n\t\tif flags.template != \"\" {\n\t\t\tpath = filepath.Join(wd, flags.template)\n\t\t} else if flags.repo != \"\" && flags.subdir == \"\" {\n\t\t\tpath = filepath.Join(wd, filepath.Base(flags.repo))\n\t\t} else if flags.repo != \"\" && flags.subdir != \"\" {\n\t\t\tpath = filepath.Join(wd, filepath.Base(flags.subdir))\n\t\t}\n\t}\n\treturn path\n}\n"
  },
  {
    "path": "internal/boxcli/env.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/joho/godotenv\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n)\n\n// to be composed into xyzCmdFlags structs\ntype envFlag devopt.EnvFlags\n\nfunc (f *envFlag) register(cmd *cobra.Command) {\n\tcmd.PersistentFlags().StringToStringVarP(\n\t\t&f.EnvMap, \"env\", \"e\", nil, \"environment variables to set in the devbox environment\",\n\t)\n\tcmd.PersistentFlags().StringVar(\n\t\t&f.EnvFile, \"env-file\", \"\", \"path to a file containing environment variables to set in the devbox environment\",\n\t)\n}\n\nfunc (f *envFlag) Env(path string) (map[string]string, error) {\n\tenvs := map[string]string{}\n\tvar err error\n\tif f.EnvFile != \"\" {\n\t\tenvPath := f.EnvFile\n\t\tif !filepath.IsAbs(envPath) {\n\t\t\tenvPath = filepath.Join(path, envPath)\n\t\t}\n\t\tenvs, err = godotenv.Read(envPath)\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithStack(err)\n\t\t}\n\t}\n\n\tfor k, v := range f.EnvMap {\n\t\tenvs[k] = v\n\t}\n\n\treturn envs, nil\n}\n"
  },
  {
    "path": "internal/boxcli/featureflag/auth.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage featureflag\n\nvar Auth = enable(\"AUTH\")\n"
  },
  {
    "path": "internal/boxcli/featureflag/feature.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage featureflag\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"go.jetify.com/devbox/internal/build\"\n)\n\nconst envNamePrefix = \"DEVBOX_FEATURE_\"\n\ntype feature struct {\n\tname    string\n\tenabled bool\n}\n\nvar features = map[string]*feature{}\n\n// Prevent lint complaining about unused function\n//\n//nolint:unparam\nfunc disable(name string) *feature {\n\tif features[name] == nil {\n\t\tfeatures[name] = &feature{name: name}\n\t}\n\tfeatures[name].enabled = false\n\treturn features[name]\n}\n\n// Prevent lint complaining about unused function\n//\n//nolint:unparam\nfunc enable(name string) *feature {\n\tif features[name] == nil {\n\t\tfeatures[name] = &feature{name: name}\n\t}\n\tfeatures[name].enabled = true\n\treturn features[name]\n}\n\nvar logMap = map[string]bool{}\n\nfunc (f *feature) Enabled() bool {\n\tif f == nil {\n\t\treturn false\n\t}\n\tif on, err := strconv.ParseBool(os.Getenv(envNamePrefix + f.name)); err == nil {\n\t\tstatus := \"enabled\"\n\t\tif !on {\n\t\t\tstatus = \"disabled\"\n\t\t}\n\t\tif !logMap[f.name] {\n\t\t\tslog.Debug(\"Feature %q %s via environment variable.\", f.name, status)\n\t\t\tlogMap[f.name] = true\n\t\t}\n\t\treturn on\n\t}\n\treturn f.enabled\n}\n\nfunc (f *feature) EnableOnDev() *feature {\n\tif build.IsDev {\n\t\tf.enabled = true\n\t}\n\treturn f\n}\n\nfunc (f *feature) EnableForTest(t *testing.T) {\n\tt.Setenv(envNamePrefix+f.name, \"1\")\n}\n\n// All returns a map of all known features flags and whether they're enabled.\nfunc All() map[string]bool {\n\tm := make(map[string]bool, len(features))\n\tfor name, feat := range features {\n\t\tm[name] = feat.Enabled()\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "internal/boxcli/featureflag/feature_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage featureflag\n\nimport (\n\t\"testing\"\n)\n\nfunc TestEnabledFeature(t *testing.T) {\n\tname := \"TestEnabledFeature\"\n\tenable(name)\n\tif !features[name].Enabled() {\n\t\tt.Errorf(\"got %s.Enabled() = false, want true.\", name)\n\t}\n}\n\nfunc TestDisabledFeature(t *testing.T) {\n\tname := \"TestDisabledFeature\"\n\tdisable(name)\n\tif features[name].Enabled() {\n\t\tt.Errorf(\"got %s.Enabled() = true, want false.\", name)\n\t}\n}\n\nfunc TestEnabledFeatureEnv(t *testing.T) {\n\tname := \"TestEnabledFeatureEnv\"\n\tdisable(name)\n\tt.Setenv(envNamePrefix+name, \"1\")\n\tif !features[name].Enabled() {\n\t\tt.Errorf(\"got %s.Enabled() = false, want true.\", name)\n\t}\n}\n\nfunc TestNonExistentFeature(t *testing.T) {\n\tname := \"TestNonExistentFeature\"\n\tif features[name].Enabled() {\n\t\tt.Errorf(\"got %s.Enabled() = true, want false.\", name)\n\t}\n}\n"
  },
  {
    "path": "internal/boxcli/featureflag/impure_print_dev_env.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage featureflag\n\n// ImpurePrintDevEnv controls whether the `devbox print-dev-env` command\n// will be called with the `--impure` flag.\n// Using the `--impure` flag will have two consequences:\n//  1. All environment variables will be passed to nix, this will enable\n//     the usage of flakes that rely on environment variables.\n//  2. It will disable nix caching, making the command slower.\nvar ImpurePrintDevEnv = disable(\"IMPURE_PRINT_DEV_ENV\")\n"
  },
  {
    "path": "internal/boxcli/featureflag/resolvev2.go",
    "content": "package featureflag\n\n// ResolveV2 uses the /v2/resolve endpoint when resolving packages.\nvar ResolveV2 = enable(\"RESOLVE_V2\")\n"
  },
  {
    "path": "internal/boxcli/featureflag/script_exit_on_error.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage featureflag\n\n// ScriptExitOnError controls whether scripts defined in devbox.json\n// and executed via `devbox run` should exit if any command within them errors.\nvar ScriptExitOnError = enable(\"SCRIPT_EXIT_ON_ERROR\")\n"
  },
  {
    "path": "internal/boxcli/featureflag/tidywarning.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage featureflag\n\nvar TidyWarning = disable(\"TIDY_WARNING\")\n"
  },
  {
    "path": "internal/boxcli/gen-docs.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/cobra/doc\"\n\n\t\"go.jetify.com/devbox/internal/fileutil\"\n)\n\nfunc genDocsCmd() *cobra.Command {\n\tgenDocsCmd := &cobra.Command{\n\t\tUse:   \"gen-docs <path>\",\n\t\tShort: \"[Internal] Generate documentation for the CLI\",\n\t\tLong: \"[Internal] Generates the documentation for the CLI's Cobra commands. \" +\n\t\t\t\"Docs are placed in the directory specified by <path>.\",\n\t\tHidden: true,\n\t\tArgs:   cobra.ExactArgs(1),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\twd, err := os.Getwd()\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\tdocsPath := filepath.Join(wd, args[0] /* relative path */)\n\n\t\t\t// We clear out the existing directory so that the doc-pages for\n\t\t\t// commands that have been deleted in the CLI will also be removed\n\t\t\t// after we re-generate the docs below\n\t\t\tif err := fileutil.ClearDir(docsPath); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\trootCmd := cmd\n\t\t\tfor rootCmd.HasParent() {\n\t\t\t\trootCmd = rootCmd.Parent()\n\t\t\t}\n\n\t\t\t// Removes the line in the generated docs of the form:\n\t\t\t// ###### Auto generated by spf13/cobra on 18-Jul-2022\n\t\t\trootCmd.DisableAutoGenTag = true\n\n\t\t\treturn errors.WithStack(doc.GenMarkdownTree(rootCmd, docsPath))\n\t\t},\n\t}\n\n\treturn genDocsCmd\n}\n"
  },
  {
    "path": "internal/boxcli/generate.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devbox/docgen\"\n)\n\ntype generateCmdFlags struct {\n\tenvFlag           // only used by generate direnv command\n\tconfig            configFlags\n\tforce             bool\n\tprintEnvrcContent bool\n\trootUser          bool\n\tenvrcDir          string // only used by generate direnv command\n}\n\ntype generateDockerfileCmdFlags struct {\n\tgenerateCmdFlags\n\tforType string\n}\n\ntype GenerateReadmeCmdFlags struct {\n\tgenerateCmdFlags\n\tsaveTemplate bool\n\ttemplate     string\n}\n\ntype GenerateAliasCmdFlags struct {\n\tconfig   configFlags\n\tprefix   string\n\tnoPrefix bool\n}\n\nfunc generateCmd() *cobra.Command {\n\tflags := &generateCmdFlags{}\n\n\tcommand := &cobra.Command{\n\t\tUse:               \"generate\",\n\t\tAliases:           []string{\"gen\"},\n\t\tShort:             \"Generate supporting files for your project\",\n\t\tArgs:              cobra.MaximumNArgs(0),\n\t\tPersistentPreRunE: ensureNixInstalled,\n\t}\n\tcommand.AddCommand(genAliasCmd())\n\tcommand.AddCommand(devcontainerCmd())\n\tcommand.AddCommand(dockerfileCmd())\n\tcommand.AddCommand(debugCmd())\n\tcommand.AddCommand(direnvCmd())\n\tcommand.AddCommand(genReadmeCmd())\n\tflags.config.register(command)\n\n\treturn command\n}\n\nfunc debugCmd() *cobra.Command {\n\tflags := &generateCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:    \"debug\",\n\t\tHidden: true,\n\t\tArgs:   cobra.MaximumNArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn runGenerateCmd(cmd, flags)\n\t\t},\n\t}\n\treturn command\n}\n\nfunc devcontainerCmd() *cobra.Command {\n\tflags := &generateCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"devcontainer\",\n\t\tShort: \"Generate Dockerfile and devcontainer.json files under .devcontainer/ directory\",\n\t\tLong:  \"Generate Dockerfile and devcontainer.json files necessary to run VSCode in remote container environments.\",\n\t\tArgs:  cobra.MaximumNArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn runGenerateCmd(cmd, flags)\n\t\t},\n\t}\n\tcommand.Flags().BoolVarP(\n\t\t&flags.force, \"force\", \"f\", false, \"force overwrite on existing files\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.rootUser, \"root-user\", false, \"Use root as default user inside the container\")\n\treturn command\n}\n\nfunc dockerfileCmd() *cobra.Command {\n\tflags := &generateDockerfileCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"dockerfile\",\n\t\tShort: \"Generate a Dockerfile that replicates devbox shell\",\n\t\tLong: \"Generate a Dockerfile that replicates devbox shell. \" +\n\t\t\t\"Can be used to run devbox shell environment in an OCI container.\",\n\t\tArgs: cobra.MaximumNArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tbox, err := devbox.Open(&devopt.Opts{\n\t\t\t\tDir:         flags.config.path,\n\t\t\t\tEnvironment: flags.config.environment,\n\t\t\t\tStderr:      cmd.ErrOrStderr(),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\treturn box.GenerateDockerfile(cmd.Context(), devopt.GenerateOpts{\n\t\t\t\tForType:  flags.forType,\n\t\t\t\tForce:    flags.force,\n\t\t\t\tRootUser: flags.rootUser,\n\t\t\t})\n\t\t},\n\t}\n\tcommand.Flags().StringVar(\n\t\t&flags.forType, \"for\", \"dev\",\n\t\t\"Generate Dockerfile for a specific type of container (dev, prod)\")\n\tcommand.Flag(\"for\").Hidden = true\n\tcommand.Flags().BoolVarP(\n\t\t&flags.force, \"force\", \"f\", false, \"force overwrite existing files\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.rootUser, \"root-user\", false, \"Use root as default user inside the container\")\n\tflags.config.register(command)\n\treturn command\n}\n\nfunc direnvCmd() *cobra.Command {\n\tflags := &generateCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"direnv\",\n\t\tShort: \"Generate a .envrc file that integrates direnv with this devbox project\",\n\t\tLong: \"Generate a .envrc file that integrates direnv with this devbox project. \" +\n\t\t\t\"Requires direnv to be installed.\",\n\t\tArgs: cobra.MaximumNArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn runGenerateDirenvCmd(cmd, flags)\n\t\t},\n\t}\n\tflags.envFlag.register(command)\n\tcommand.Flags().BoolVarP(\n\t\t&flags.force, \"force\", \"f\", false, \"force overwrite existing files\")\n\tcommand.Flags().BoolVarP(\n\t\t&flags.printEnvrcContent, \"print-envrc\", \"p\", false,\n\t\t\"output contents of devbox configuration to use in .envrc\")\n\t// this command marks a flag as hidden. Error handling for it is not necessary.\n\t_ = command.Flags().MarkHidden(\"print-envrc\")\n\n\t// --envrc-dir allows users to specify a directory where the .envrc file should be generated\n\t// separately from the devbox config directory. Without this flag, the .envrc file\n\t// will be generated in the same directory as the devbox config file (i.e., either the current\n\t// directory or the directory specified by --config). This flag is useful for users who want to\n\t// keep their .envrc and devbox config files in different locations.\n\tcommand.Flags().StringVar(\n\t\t&flags.envrcDir, \"envrc-dir\", \"\",\n\t\t\"path to directory where the .envrc file should be generated.\\n\"+\n\t\t\t\"If not specified, the .envrc file will be generated in the same directory as\\n\"+\n\t\t\t\"the devbox.json.\")\n\n\tflags.config.register(command)\n\treturn command\n}\n\nfunc genReadmeCmd() *cobra.Command {\n\tflags := &GenerateReadmeCmdFlags{}\n\n\tcommand := &cobra.Command{\n\t\tUse:   \"readme [filename]\",\n\t\tShort: \"Generate markdown readme file for this project\",\n\t\tArgs:  cobra.MaximumNArgs(1),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tbox, err := devbox.Open(&devopt.Opts{\n\t\t\t\tDir:         flags.config.path,\n\t\t\t\tEnvironment: flags.config.environment,\n\t\t\t\tStderr:      cmd.ErrOrStderr(),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\toutPath := \"\"\n\t\t\tif len(args) > 0 {\n\t\t\t\toutPath = args[0]\n\t\t\t}\n\t\t\tif flags.saveTemplate {\n\t\t\t\treturn docgen.SaveDefaultReadmeTemplate(outPath)\n\t\t\t}\n\t\t\treturn docgen.GenerateReadme(box, outPath, flags.template)\n\t\t},\n\t}\n\tflags.config.register(command)\n\tcommand.Flags().BoolVar(\n\t\t&flags.saveTemplate, \"save-template\", false, \"Save default template for the README file\")\n\tcommand.Flags().StringVarP(\n\t\t&flags.template, \"template\", \"t\", \"\", \"Path to a custom template for the README file\")\n\n\treturn command\n}\n\nfunc genAliasCmd() *cobra.Command {\n\tflags := &GenerateAliasCmdFlags{}\n\n\tcommand := &cobra.Command{\n\t\tUse:   \"alias\",\n\t\tShort: \"Generate shell script aliases for this project\",\n\t\tLong: \"Generate shell script aliases for this project. \" +\n\t\t\t\"Usage is typically `eval \\\"$(devbox gen alias)\\\"`.\",\n\t\tArgs: cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif flags.prefix != \"\" && flags.noPrefix {\n\t\t\t\treturn usererr.New(\n\t\t\t\t\t\"Cannot use both --prefix and --no-prefix flags together\")\n\t\t\t}\n\t\t\tbox, err := devbox.Open(&devopt.Opts{\n\t\t\t\tDir:    flags.config.path,\n\t\t\t\tStderr: cmd.ErrOrStderr(),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\tre := regexp.MustCompile(\"[^a-zA-Z0-9_-]+\")\n\t\t\tprefix := cmp.Or(flags.prefix, box.Config().Root.Name)\n\t\t\tif prefix == \"\" && !flags.noPrefix {\n\t\t\t\treturn usererr.New(\n\t\t\t\t\t\"To generate aliases, you must specify a prefix, set a name \" +\n\t\t\t\t\t\t\"in devbox.json, or use the --no-prefix flag.\")\n\t\t\t}\n\t\t\tprefix = re.ReplaceAllString(prefix, \"-\")\n\t\t\tfor _, script := range box.ListScripts() {\n\t\t\t\tfmt.Fprintf(\n\t\t\t\t\tcmd.OutOrStdout(),\n\t\t\t\t\t\"alias %s%s='devbox -c \\\"%s\\\" run %s'\\n\",\n\t\t\t\t\tlo.Ternary(flags.noPrefix, \"\", prefix+\"-\"),\n\t\t\t\t\tscript,\n\t\t\t\t\tbox.ProjectDir(),\n\t\t\t\t\tscript,\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\tflags.config.register(command)\n\tcommand.Flags().StringVarP(\n\t\t&flags.prefix, \"prefix\", \"p\", \"\", \"Prefix for the generated aliases\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.noPrefix, \"no-prefix\", false,\n\t\t\"Do not use a prefix for the generated aliases\")\n\n\treturn command\n}\n\nfunc runGenerateCmd(cmd *cobra.Command, flags *generateCmdFlags) error {\n\t// Check the directory exists.\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tgenerateOpts := devopt.GenerateOpts{\n\t\tForce:    flags.force,\n\t\tRootUser: flags.rootUser,\n\t}\n\tswitch cmd.Use {\n\tcase \"debug\":\n\t\treturn box.Generate(cmd.Context())\n\tcase \"devcontainer\":\n\t\treturn box.GenerateDevcontainer(cmd.Context(), generateOpts)\n\t}\n\treturn nil\n}\n\nfunc runGenerateDirenvCmd(cmd *cobra.Command, flags *generateCmdFlags) error {\n\t// --print-envrc is used within the .envrc file and therefore doesn't make sense to also\n\t// use it with --envrc-dir, which specifies a directory where the .envrc file should be generated.\n\tif flags.printEnvrcContent && flags.envrcDir != \"\" {\n\t\treturn usererr.New(\n\t\t\t\"Cannot use --print-envrc with --envrc-dir. \" +\n\t\t\t\t\"Use --envrc-dir to specify the directory where the .envrc file should be generated.\")\n\t}\n\n\tif flags.printEnvrcContent {\n\t\treturn devbox.PrintEnvrcContent(\n\t\t\tcmd.OutOrStdout(), devopt.EnvFlags(flags.envFlag), flags.config.path)\n\t}\n\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tgenerateEnvrcOpts := devopt.EnvrcOpts{\n\t\tEnvFlags:  devopt.EnvFlags(flags.envFlag),\n\t\tForce:     flags.force,\n\t\tEnvrcDir:  flags.envrcDir,\n\t\tConfigDir: flags.config.path,\n\t}\n\n\treturn box.GenerateEnvrcFile(cmd.Context(), generateEnvrcOpts)\n}\n"
  },
  {
    "path": "internal/boxcli/global.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\nfunc globalCmd() *cobra.Command {\n\tglobalCmd := &cobra.Command{}\n\tpersistentPreRunE := setGlobalConfigForDelegatedCommands(globalCmd)\n\t*globalCmd = cobra.Command{\n\t\tUse:   \"global\",\n\t\tShort: \"Manage global devbox packages\",\n\t\t// PersistentPreRunE is inherited only if children do not implement it\n\t\t// (i.e. it's not chained). So this is fragile. Ideally we stop\n\t\t// using PersistentPreRunE. For now a hack is to pass it down to commands\n\t\t// that declare their own.\n\t\tPersistentPreRunE:  persistentPreRunE,\n\t\tPersistentPostRunE: ensureGlobalEnvEnabled,\n\t}\n\n\taddCommandAndHideConfigFlag(globalCmd, addCmd())\n\taddCommandAndHideConfigFlag(globalCmd, installCmd())\n\taddCommandAndHideConfigFlag(globalCmd, pathCmd())\n\taddCommandAndHideConfigFlag(globalCmd, pullCmd())\n\taddCommandAndHideConfigFlag(globalCmd, pushCmd())\n\taddCommandAndHideConfigFlag(globalCmd, removeCmd())\n\taddCommandAndHideConfigFlag(globalCmd, runCmd(runFlagDefaults{\n\t\tomitNixEnv: true,\n\t}))\n\taddCommandAndHideConfigFlag(globalCmd, servicesCmd(persistentPreRunE))\n\taddCommandAndHideConfigFlag(globalCmd, shellEnvCmd(shellenvFlagDefaults{\n\t\tomitNixEnv: true,\n\t}))\n\taddCommandAndHideConfigFlag(globalCmd, updateCmd())\n\taddCommandAndHideConfigFlag(globalCmd, listCmd())\n\n\treturn globalCmd\n}\n\nfunc addCommandAndHideConfigFlag(parent, child *cobra.Command) {\n\tparent.AddCommand(child)\n\t_ = child.Flags().MarkHidden(\"config\")\n}\n\nvar globalConfigPath string\n\nfunc ensureGlobalConfig() (string, error) {\n\tif globalConfigPath != \"\" {\n\t\treturn globalConfigPath, nil\n\t}\n\n\tglobalConfigPath, err := devbox.GlobalDataPath()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\terr = devbox.EnsureConfig(globalConfigPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn globalConfigPath, nil\n}\n\nfunc setGlobalConfigForDelegatedCommands(\n\tglobalCmd *cobra.Command,\n) func(cmd *cobra.Command, args []string) error {\n\treturn func(cmd *cobra.Command, args []string) error {\n\t\tglobalPath, err := ensureGlobalConfig()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, c := range globalCmd.Commands() {\n\t\t\tif f := c.Flag(\"config\"); f != nil && f.Value.Type() == \"string\" {\n\t\t\t\tif err := f.Value.Set(globalPath); err != nil {\n\t\t\t\t\treturn errors.WithStack(err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc ensureGlobalEnvEnabled(cmd *cobra.Command, args []string) error {\n\tif cmd.Name() == \"shellenv\" {\n\t\treturn nil\n\t}\n\tpath, err := ensureGlobalConfig()\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:    path,\n\t\tStderr: cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !box.IsEnvEnabled() {\n\t\tfmt.Fprintln(cmd.ErrOrStderr())\n\t\tux.Fwarningf(\n\t\t\tcmd.ErrOrStderr(),\n\t\t\t`devbox global is not activated.\n\nAdd the following line to your shell's rcfile and restart your shell:\n\nFor bash/zsh (~/.bashrc or ~/.zshrc):\n\teval \"$(devbox global shellenv)\"\n\nFor nushell: See NUSHELL.md for setup instructions\n`,\n\t\t)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/boxcli/info.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n)\n\ntype infoCmdFlags struct {\n\tconfig   configFlags\n\tmarkdown bool\n}\n\nfunc infoCmd() *cobra.Command {\n\tflags := infoCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:     \"info <pkg>\",\n\t\tShort:   \"Display package info\",\n\t\tArgs:    cobra.ExactArgs(1),\n\t\tPreRunE: ensureNixInstalled,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn infoCmdFunc(cmd, args[0], flags)\n\t\t},\n\t}\n\n\tflags.config.register(command)\n\tcommand.Flags().BoolVar(&flags.markdown, \"markdown\", false, \"output in markdown format\")\n\treturn command\n}\n\nfunc infoCmdFunc(cmd *cobra.Command, pkg string, flags infoCmdFlags) error {\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tinfo, err := box.Info(cmd.Context(), pkg, flags.markdown)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tfmt.Fprint(cmd.OutOrStdout(), info)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/boxcli/init.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/ux\"\n\t\"go.jetify.com/devbox/pkg/autodetect\"\n)\n\ntype initFlags struct {\n\tauto   bool\n\tdryRun bool\n}\n\nfunc initCmd() *cobra.Command {\n\tflags := &initFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"init [<dir>]\",\n\t\tShort: \"Initialize a directory as a devbox project\",\n\t\tLong: \"Initialize a directory as a devbox project. \" +\n\t\t\t\"This will create an empty devbox.json in the current directory. \" +\n\t\t\t\"You can then add packages using `devbox add`\",\n\t\tArgs: cobra.MaximumNArgs(1),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\terr := runInitCmd(cmd, args, flags)\n\t\t\tif errors.Is(err, os.ErrExist) {\n\t\t\t\tpath := pathArg(args)\n\t\t\t\tif path == \"\" || path == \".\" {\n\t\t\t\t\tpath, _ = os.Getwd()\n\t\t\t\t}\n\t\t\t\tux.Fwarningf(cmd.ErrOrStderr(), \"devbox.json already exists in %q.\", path)\n\t\t\t\terr = nil\n\t\t\t}\n\t\t\treturn err\n\t\t},\n\t}\n\n\tcommand.Flags().BoolVar(&flags.auto, \"auto\", false, \"Automatically detect packages to add\")\n\tcommand.Flags().BoolVar(&flags.dryRun, \"dry-run\", false, \"Dry run for auto mode. Prints the config that would be used\")\n\t_ = command.Flags().MarkHidden(\"auto\")\n\t_ = command.Flags().MarkHidden(\"dry-run\")\n\n\treturn command\n}\n\nfunc runInitCmd(cmd *cobra.Command, args []string, flags *initFlags) error {\n\tpath := pathArg(args)\n\n\tif flags.auto {\n\t\tif flags.dryRun {\n\t\t\tconfig, err := autodetect.DryRun(cmd.Context(), path)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\tfmt.Fprintln(cmd.OutOrStdout(), string(config))\n\t\t\treturn nil\n\t\t}\n\t\treturn autodetect.InitConfig(cmd.Context(), path)\n\t}\n\n\treturn devbox.InitConfig(path)\n}\n"
  },
  {
    "path": "internal/boxcli/install.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\ntype installCmdFlags struct {\n\trunCmdFlags\n\ttidyLockfile bool\n}\n\nfunc installCmd() *cobra.Command {\n\tflags := installCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:     \"install\",\n\t\tShort:   \"Install all packages mentioned in devbox.json\",\n\t\tArgs:    cobra.MaximumNArgs(0),\n\t\tPreRunE: ensureNixInstalled,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn installCmdFunc(cmd, flags)\n\t\t},\n\t}\n\n\tflags.config.register(command)\n\tcommand.Flags().BoolVar(\n\t\t&flags.tidyLockfile, \"tidy-lockfile\", false,\n\t\t\"Fix missing store paths in the devbox.lock file.\",\n\t\t// Could potentially do more in the future.\n\t)\n\n\treturn command\n}\n\nfunc installCmdFunc(cmd *cobra.Command, flags installCmdFlags) error {\n\t// Check the directory exists.\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tctx := cmd.Context()\n\tif flags.tidyLockfile {\n\t\tctx = ux.HideMessage(ctx, devpkg.MissingStorePathsWarning)\n\t}\n\tif err = box.Install(ctx); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tif flags.tidyLockfile {\n\t\tif err = box.FixMissingStorePaths(ctx); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\tfmt.Fprintln(cmd.ErrOrStderr(), \"Finished installing packages.\")\n\treturn nil\n}\n"
  },
  {
    "path": "internal/boxcli/integrate.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/zealic/go2node\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n)\n\ntype integrateCmdFlags struct {\n\tconfig    configFlags\n\tdebugmode bool\n\tideName   string\n}\n\nfunc integrateCmd() *cobra.Command {\n\tcommand := &cobra.Command{\n\t\tUse:     \"integrate\",\n\t\tShort:   \"integrate with an IDE\",\n\t\tArgs:    cobra.MaximumNArgs(1),\n\t\tHidden:  true,\n\t\tPreRunE: ensureNixInstalled,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn cmd.Help()\n\t\t},\n\t}\n\tcommand.AddCommand(integrateVSCodeCmd())\n\treturn command\n}\n\nfunc integrateVSCodeCmd() *cobra.Command {\n\tflags := integrateCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:    \"vscode\",\n\t\tHidden: true,\n\t\tShort:  \"Integrate devbox environment with VSCode or other VSCode-based editors.\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn runIntegrateVSCodeCmd(cmd, flags)\n\t\t},\n\t}\n\tcommand.Flags().BoolVar(&flags.debugmode, \"debugmode\", false, \"enable debug outputs to a file.\")\n\tcommand.Flags().StringVar(&flags.ideName, \"ide\", \"code\", \"name of the currently open editor to reopen after it's closed.\")\n\tflags.config.register(command)\n\n\treturn command\n}\n\ntype parentMessage struct {\n\tConfigDir string `json:\"configDir\"`\n}\n\nfunc runIntegrateVSCodeCmd(cmd *cobra.Command, flags integrateCmdFlags) error {\n\tdbug := debugMode{\n\t\tenabled: flags.debugmode,\n\t}\n\t// Setup process communication with node as parent\n\tdbug.logToFile(\"Devbox process initiated. Setting up communication channel with the code editor process\")\n\tchannel, err := go2node.RunAsNodeChild()\n\tif err != nil {\n\t\tdbug.logToFile(err.Error())\n\t\treturn err\n\t}\n\t// Get config dir as a message from parent process\n\tmsg, err := channel.Read()\n\tif err != nil {\n\t\tdbug.logToFile(err.Error())\n\t\treturn err\n\t}\n\t// Parse node process' message\n\tvar message parentMessage\n\tif err = json.Unmarshal(msg.Message, &message); err != nil {\n\t\tdbug.logToFile(err.Error())\n\t\treturn err\n\t}\n\n\t// todo: add error handling - consider sending error message to parent process\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:    message.ConfigDir,\n\t\tStderr: cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\tdbug.logToFile(err.Error())\n\t\treturn err\n\t}\n\t// Get env variables of a devbox shell\n\tdbug.logToFile(\"Computing devbox environment\")\n\tenvVars, err := box.EnvVars(cmd.Context())\n\tif err != nil {\n\t\tdbug.logToFile(err.Error())\n\t\treturn err\n\t}\n\tenvVars = slices.DeleteFunc(envVars, func(s string) bool {\n\t\tk, _, ok := strings.Cut(s, \"=\")\n\t\t// DEVBOX_OG_PATH_<hash> being set causes devbox global shellenv to overwrite the\n\t\t// PATH after VSCode opens and resets it to global shellenv. This causes the VSCode\n\t\t// terminal to not be able to find devbox packages after the reopen in devbox\n\t\t// environment action is called.\n\t\t//\n\t\t// ELECTRON_RUN_AS_NODE being set causes this error in WSL:\n\t\t// \"Remote Extension host terminated unexpectedly 3 times within the last 5 minutes.\"\n\t\treturn ok && (strings.HasPrefix(k, \"DEVBOX_OG_PATH\") || k == \"ELECTRON_RUN_AS_NODE\" || k == \"NODE_CHANNEL_FD\")\n\t})\n\n\t// Send message to parent process to terminate\n\tdbug.logToFile(\"Signaling code editor to close\")\n\terr = channel.Write(&go2node.NodeMessage{\n\t\tMessage: []byte(`{\"status\": \"finished\"}`),\n\t})\n\tif err != nil {\n\t\tdbug.logToFile(err.Error())\n\t\treturn err\n\t}\n\t// Open editor with devbox shell environment\n\tcmndName := flags.ideName\n\tcwd, ok := os.LookupEnv(\"VSCODE_CWD\")\n\tif ok {\n\t\t// Specify full path to avoid running the `code` shell script from VS Code Server, which fails under WSL\n\t\tcmndName = cwd + \"/bin/\" + cmndName\n\t}\n\tcmnd := exec.Command(cmndName, message.ConfigDir)\n\tcmnd.Env = append(cmnd.Env, envVars...)\n\tvar outb, errb bytes.Buffer\n\tcmnd.Stdout = &outb\n\tcmnd.Stderr = &errb\n\tdbug.logToFile(\"Re-opening code editor in computed devbox environment\")\n\terr = cmnd.Run()\n\tif err != nil {\n\t\tdbug.logToFile(fmt.Sprintf(\"stdout: %s \\n stderr: %s\", outb.String(), errb.String()))\n\t\tdbug.logToFile(err.Error())\n\t\treturn err\n\t}\n\treturn nil\n}\n\ntype debugMode struct {\n\tenabled bool\n}\n\nfunc (d *debugMode) logToFile(msg string) {\n\t// only write to file when --debugmode=true flag is passed\n\tif d.enabled {\n\t\tfile, err := os.OpenFile(\".devbox/extension.log\", os.O_APPEND|os.O_WRONLY, 0o666)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\ttimestamp := time.Now().UTC().Format(time.RFC1123)\n\t\t_, err = file.WriteString(\"[\" + timestamp + \"] \" + msg + \"\\n\")\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tif err = file.Close(); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/boxcli/list.go",
    "content": "// Copyright 2025 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n)\n\ntype listCmdFlags struct {\n\tconfig   configFlags\n\toutdated bool\n}\n\nfunc listCmd() *cobra.Command {\n\tflags := listCmdFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"list\",\n\t\tAliases: []string{\"ls\"},\n\t\tShort:   \"List installed packages\",\n\t\tPreRunE: ensureNixInstalled,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tbox, err := devbox.Open(&devopt.Opts{\n\t\t\t\tDir:    flags.config.path,\n\t\t\t\tStderr: cmd.ErrOrStderr(),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\n\t\t\tif flags.outdated {\n\t\t\t\treturn printOutdatedPackages(cmd, box)\n\t\t\t}\n\n\t\t\tfor _, pkg := range box.AllPackagesIncludingRemovedTriggerPackages() {\n\t\t\t\tresolvedVersion, err := pkg.ResolvedVersion()\n\t\t\t\tif err != nil {\n\t\t\t\t\t// Continue to print the package even if we can't resolve the version\n\t\t\t\t\t// so that the user can see the error for this package, as well as get the\n\t\t\t\t\t// results for the other packages\n\t\t\t\t\tresolvedVersion = \"<error resolving version>\"\n\t\t\t\t}\n\t\t\t\tmsg := \"\"\n\n\t\t\t\t// Print the resolved version, unless the user has specified a version already\n\t\t\t\tif strings.HasSuffix(pkg.Versioned(), \"latest\") && resolvedVersion != \"\" {\n\t\t\t\t\t// Runx packages have a \"v\" prefix (why?). Trim for consistency.\n\t\t\t\t\tresolvedVersion = strings.TrimPrefix(resolvedVersion, \"v\")\n\t\t\t\t\tmsg = fmt.Sprintf(\"* %s - %s\\n\", pkg.Versioned(), resolvedVersion)\n\t\t\t\t} else {\n\t\t\t\t\tmsg = fmt.Sprintf(\"* %s\\n\", pkg.Versioned())\n\t\t\t\t}\n\t\t\t\tfmt.Fprint(cmd.OutOrStdout(), msg)\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tcmd.Flags().BoolVar(&flags.outdated, \"outdated\", false, \"List outdated packages\")\n\tflags.config.register(cmd)\n\treturn cmd\n}\n\n// printOutdatedPackages prints a list of outdated packages.\nfunc printOutdatedPackages(cmd *cobra.Command, box *devbox.Devbox) error {\n\tresults, err := box.Outdated(cmd.Context())\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tif len(results) == 0 {\n\t\tcmd.Println(\"Your packages are up to date!\")\n\t\treturn nil\n\t}\n\n\tcmd.Println(\"The following packages can be updated:\")\n\tfor pkg, version := range results {\n\t\tcmd.Printf(\" * %-30s %s -> %s\\n\", pkg, version.Current, version.Latest)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/boxcli/log.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/telemetry\"\n)\n\nfunc logCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:    \"log <event-name> [<event-specific-args>]\",\n\t\tHidden: true,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn doLogCommand(cmd, args)\n\t\t},\n\t}\n\n\treturn cmd\n}\n\nfunc doLogCommand(cmd *cobra.Command, args []string) error {\n\tif len(args) < 1 {\n\t\treturn usererr.New(\"expect an <event-name> arg for command: %s\", cmd.CommandPath())\n\t}\n\n\tswitch eventName := args[0]; eventName {\n\tcase \"shell-ready\":\n\t\tif len(args) < 2 {\n\t\t\treturn usererr.New(\"expected a start-time argument for logging the shell-ready event\")\n\t\t}\n\t\ttelemetry.Event(telemetry.EventShellReady, telemetry.Metadata{\n\t\t\tEventStart: telemetry.ParseShellStart(args[1]),\n\t\t})\n\tcase \"shell-interactive\":\n\t\tif len(args) < 2 {\n\t\t\treturn usererr.New(\"expected a start-time argument for logging the shell-interactive event\")\n\t\t}\n\t\ttelemetry.Event(telemetry.EventShellInteractive, telemetry.Metadata{\n\t\t\tEventStart: telemetry.ParseShellStart(args[1]),\n\t\t})\n\t}\n\treturn usererr.New(\"unrecognized event-name %s for command: %s\", args[0], cmd.CommandPath())\n}\n"
  },
  {
    "path": "internal/boxcli/midcobra/debug.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage midcobra\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"os/exec\"\n\t\"strconv\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/telemetry\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\ntype DebugMiddleware struct {\n\tflag *pflag.Flag\n}\n\nvar _ Middleware = (*DebugMiddleware)(nil)\n\nfunc (d *DebugMiddleware) AttachToFlag(flags *pflag.FlagSet, flagName string) {\n\tflags.Bool(\n\t\tflagName,\n\t\tfalse,\n\t\t\"Show full stack traces on errors\",\n\t)\n\td.flag = flags.Lookup(flagName)\n\td.flag.Hidden = true\n}\n\nfunc (d *DebugMiddleware) preRun(cmd *cobra.Command, args []string) {\n\tif d == nil {\n\t\treturn\n\t}\n\n\tif d.flag.Changed {\n\t\tstrVal := d.flag.Value.String()\n\t\tif enabled, _ := strconv.ParseBool(strVal); enabled {\n\t\t\tdebug.Enable()\n\t\t}\n\t}\n}\n\nfunc (d *DebugMiddleware) postRun(cmd *cobra.Command, args []string, runErr error) {\n\tif runErr == nil {\n\t\treturn\n\t}\n\tif userErr, hasUserErr := usererr.Extract(runErr); hasUserErr {\n\t\tif usererr.IsWarning(userErr) {\n\t\t\tux.Fwarning(cmd.ErrOrStderr(), runErr.Error())\n\t\t\treturn\n\t\t}\n\t\tcolor.New(color.FgRed).Fprintf(cmd.ErrOrStderr(), \"\\nError: %s\\n\\n\", userErr.Error())\n\t} else {\n\t\tcolor.New(color.FgRed).Fprintf(cmd.ErrOrStderr(), \"Error: %v\\n\\n\", runErr)\n\t}\n\n\tst := debug.EarliestStackTrace(runErr)\n\tvar exitErr *exec.ExitError\n\tif errors.As(runErr, &exitErr) {\n\t\tslog.Error(\"command error\", \"stderr\", exitErr.Stderr, \"execid\", telemetry.ExecutionID, \"stack\", st)\n\t}\n\tslog.Error(\"command error\", \"execid\", telemetry.ExecutionID, \"stack\", st)\n}\n"
  },
  {
    "path": "internal/boxcli/midcobra/midcobra.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage midcobra\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os/exec\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\ntype Executable interface {\n\tAddMiddleware(mids ...Middleware)\n\tExecute(ctx context.Context, args []string) int\n}\n\ntype Middleware interface {\n\tpreRun(cmd *cobra.Command, args []string)\n\tpostRun(cmd *cobra.Command, args []string, runErr error)\n}\n\nfunc New(cmd *cobra.Command) Executable {\n\treturn &midcobraExecutable{\n\t\tcmd:         cmd,\n\t\tmiddlewares: []Middleware{},\n\t}\n}\n\ntype midcobraExecutable struct {\n\tcmd *cobra.Command\n\n\tmiddlewares []Middleware\n}\n\nvar _ Executable = (*midcobraExecutable)(nil)\n\nfunc (ex *midcobraExecutable) AddMiddleware(mids ...Middleware) {\n\tex.middlewares = append(ex.middlewares, mids...)\n}\n\nfunc (ex *midcobraExecutable) Execute(ctx context.Context, args []string) int {\n\t// Ensure cobra uses the same arguments\n\tex.cmd.SetContext(ctx)\n\t_ = ex.cmd.ParseFlags(args)\n\n\t// Run the 'pre' hooks\n\tfor _, m := range ex.middlewares {\n\t\tm.preRun(ex.cmd, args)\n\t}\n\n\t// set args (needed in case caller transforms args in any way)\n\tex.cmd.SetArgs(args)\n\n\t// Execute the cobra command:\n\terr := ex.cmd.Execute()\n\n\t// Run the 'post' hooks. Note that unlike the default PostRun cobra functionality these\n\t// run even if the command resulted in an error. This is useful when we still want to clean up\n\t// before the program exists or we want to log something. The error, if any, gets passed\n\t// to the post hook.\n\tfor i := len(ex.middlewares) - 1; i >= 0; i-- {\n\t\tex.middlewares[i].postRun(ex.cmd, args, err)\n\t}\n\n\tif err != nil {\n\t\t// If the error is from the exec call, return the exit code of the exec call.\n\t\t// Note: order matters! Check if it is a user exec error before a generic exit error.\n\t\tvar exitErr *exec.ExitError\n\t\tvar userExecErr *usererr.ExitError\n\t\tif errors.As(err, &userExecErr) {\n\t\t\treturn userExecErr.ExitCode()\n\t\t}\n\t\tif errors.As(err, &exitErr) {\n\t\t\tif !debug.IsEnabled() {\n\t\t\t\tux.Ferrorf(ex.cmd.ErrOrStderr(), \"There was an internal error. \"+\n\t\t\t\t\t\"Run with DEVBOX_DEBUG=1 for a detailed error message, and consider reporting it at \"+\n\t\t\t\t\t\"https://github.com/jetify-com/devbox/issues\\n\")\n\t\t\t}\n\t\t\treturn exitErr.ExitCode()\n\t\t}\n\t\treturn 1 // Error exit code\n\t}\n\treturn 0\n}\n"
  },
  {
    "path": "internal/boxcli/midcobra/telemetry.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage midcobra\n\nimport (\n\t\"os\"\n\t\"runtime/trace\"\n\t\"sort\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n\t\"go.jetify.com/devbox/internal/boxcli/featureflag\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/telemetry\"\n)\n\n// We collect some light telemetry to be able to improve devbox over time.\n// We're aware how important privacy is and value it ourselves, so we have\n// the following rules:\n// 1. We only collect anonymized data – nothing that is personally identifiable\n// 2. Data is only stored in SOC 2 compliant systems, and we are SOC 2 compliant ourselves.\n// 3. Users should always have the ability to opt-out.\nfunc Telemetry() Middleware {\n\treturn &telemetryMiddleware{}\n}\n\ntype telemetryMiddleware struct{}\n\n// telemetryMiddleware implements interface Middleware (compile-time check)\nvar _ Middleware = (*telemetryMiddleware)(nil)\n\nfunc (m *telemetryMiddleware) preRun(cmd *cobra.Command, args []string) {\n\ttelemetry.Start()\n}\n\nfunc (m *telemetryMiddleware) postRun(cmd *cobra.Command, args []string, runErr error) {\n\tdefer trace.StartRegion(cmd.Context(), \"telemetryPostRun\").End()\n\tdefer telemetry.Stop()\n\n\tmeta := telemetry.Metadata{\n\t\tFeatureFlags: featureflag.All(),\n\t\tCloudRegion:  os.Getenv(envir.DevboxRegion),\n\t\tCloudCache:   os.Getenv(envir.DevboxCache),\n\t}\n\n\tsubcmd, flags, err := getSubcommand(cmd, args)\n\tif err != nil {\n\t\t// Ignore invalid commands/flags.\n\t\treturn\n\t}\n\tmeta.Command = subcmd.CommandPath()\n\tmeta.CommandFlags = flags\n\n\tmeta.Packages, meta.NixpkgsHash = getPackagesAndCommitHash(cmd)\n\tmeta.InShell = envir.IsDevboxShellEnabled()\n\tmeta.InBrowser = envir.IsInBrowser()\n\tmeta.InCloud = envir.IsDevboxCloud()\n\n\tif runErr != nil {\n\t\ttelemetry.Error(runErr, meta)\n\t\t// TODO: This is skipping event logging of calls that end in error. We probably want to log them.\n\t\treturn\n\t}\n\ttelemetry.Event(telemetry.EventCommandSuccess, meta)\n}\n\nfunc getSubcommand(cmd *cobra.Command, args []string) (subcmd *cobra.Command, flags []string, err error) {\n\tif cmd.TraverseChildren {\n\t\tsubcmd, _, err = cmd.Traverse(args)\n\t} else {\n\t\tsubcmd, _, err = cmd.Find(args)\n\t}\n\n\tsubcmd.Flags().Visit(func(f *pflag.Flag) {\n\t\tflags = append(flags, \"--\"+f.Name)\n\t})\n\tsort.Strings(flags)\n\treturn subcmd, flags, err\n}\n\nfunc getPackagesAndCommitHash(c *cobra.Command) ([]string, string) {\n\tconfigFlag := c.Flag(\"config\")\n\t// for shell, run, and add command, path can be set via --config\n\t// if --config is not set, default to current directory which is \"\"\n\t// the only exception is the init command, for the path can be set with args\n\t// since after running init there will be no packages set in devbox.json\n\t// we can safely ignore this case.\n\tvar path string\n\tif configFlag != nil {\n\t\tpath = configFlag.Value.String()\n\t}\n\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:            path,\n\t\tStderr:         os.Stderr,\n\t\tIgnoreWarnings: true,\n\t})\n\tif err != nil {\n\t\treturn []string{}, \"\"\n\t}\n\n\treturn box.AllPackageNamesIncludingRemovedTriggerPackages(),\n\t\tbox.Lockfile().Stdenv().Rev\n}\n"
  },
  {
    "path": "internal/boxcli/midcobra/telemetry_test.go",
    "content": "package midcobra\n\nimport (\n\t\"testing\"\n\n\t\"github.com/spf13/cobra\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n)\n\nfunc TestGetPackagesAndCommitHash(t *testing.T) {\n\tdir := t.TempDir()\n\terr := devbox.InitConfig(dir)\n\tif err != nil {\n\t\tt.Errorf(\"Expected no error, got %s\", err)\n\t}\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir: dir,\n\t})\n\t// Create a mock cobra command\n\tcmd := &cobra.Command{\n\t\tUse: \"test\",\n\t\tRun: func(cmd *cobra.Command, args []string) {},\n\t}\n\n\t// Add a mock flag to the command\n\tcmd.Flags().String(\"config\", \"\", \"config file\")\n\tif err := cmd.Flags().Set(\"config\", dir); err != nil {\n\t\tt.Errorf(\"Expected no error, got %s\", err)\n\t}\n\n\t// Call the function with the mock command\n\tpackages, commitHash := getPackagesAndCommitHash(cmd)\n\n\t// Check if the returned packages and commitHash are as expected\n\tif len(packages) != 0 {\n\t\tt.Errorf(\"Expected no packages, got %d\", len(packages))\n\t}\n\n\tif err != nil {\n\t\tt.Errorf(\"Expected no error, got %s\", err)\n\t}\n\n\tif commitHash != box.Lockfile().Stdenv().Rev {\n\t\tt.Errorf(\"Expected commitHash %s, got %s\", box.Lockfile().Stdenv().Rev, commitHash)\n\t}\n}\n"
  },
  {
    "path": "internal/boxcli/midcobra/trace.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage midcobra\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"runtime/trace\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n)\n\ntype TraceMiddleware struct {\n\ttracef *os.File\n\tflag   *pflag.Flag\n\ttask   *trace.Task\n}\n\nvar _ Middleware = (*DebugMiddleware)(nil)\n\nfunc (t *TraceMiddleware) AttachToFlag(flags *pflag.FlagSet, flagName string) {\n\tflags.String(flagName, \"\", \"write a trace to a file\")\n\tt.flag = flags.Lookup(flagName)\n\tt.flag.Hidden = true\n\tt.flag.NoOptDefVal = \"trace.out\"\n}\n\nfunc (t *TraceMiddleware) preRun(cmd *cobra.Command, _ []string) {\n\tif t == nil {\n\t\treturn\n\t}\n\tpath := t.flag.Value.String()\n\tif path == \"\" {\n\t\treturn\n\t}\n\tvar err error\n\tt.tracef, err = os.Create(path)\n\tif err != nil {\n\t\tpanic(\"error enabling tracing: \" + err.Error())\n\t}\n\tif err := trace.Start(t.tracef); err != nil {\n\t\tpanic(\"error enabling tracing: \" + err.Error())\n\t}\n\n\tvar ctx context.Context\n\tctx, t.task = trace.NewTask(cmd.Context(), \"cliCommand\")\n\tcmd.SetContext(ctx)\n}\n\nfunc (t *TraceMiddleware) postRun(*cobra.Command, []string, error) {\n\tif t.tracef == nil {\n\t\treturn\n\t}\n\tt.task.End()\n\ttrace.Stop()\n\tif err := t.tracef.Close(); err != nil {\n\t\tpanic(\"error closing trace file: \" + err.Error())\n\t}\n}\n"
  },
  {
    "path": "internal/boxcli/multi/multi.go",
    "content": "package multi\n\nimport (\n\t\"io/fs\"\n\t\"path/filepath\"\n\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devconfig/configfile\"\n)\n\nfunc Open(opts *devopt.Opts) ([]*devbox.Devbox, error) {\n\tdefer debug.FunctionTimer().End()\n\n\tvar boxes []*devbox.Devbox\n\terr := filepath.WalkDir(\n\t\t\".\",\n\t\tfunc(path string, dirEntry fs.DirEntry, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif !dirEntry.IsDir() && filepath.Base(path) == configfile.DefaultName {\n\t\t\t\toptsCopy := *opts\n\t\t\t\toptsCopy.Dir = path\n\t\t\t\tbox, err := devbox.Open(&optsCopy)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tboxes = append(boxes, box)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t)\n\n\treturn boxes, err\n}\n"
  },
  {
    "path": "internal/boxcli/multi/sync.go",
    "content": "package multi\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"go.jetify.com/devbox/internal/cuecfg\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/searcher\"\n)\n\nfunc SyncLockfiles(pkgs []string) error {\n\tlockfilePaths, err := collectLockfiles()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlatestPackages, err := latestPackages(lockfilePaths)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpkgMap := make(map[string]bool)\n\tfor _, pkg := range pkgs {\n\t\tpkgMap[pkg] = true\n\t}\n\n\tfor _, lockfilePath := range lockfilePaths {\n\t\tvar lockFile lock.File\n\t\tif err := cuecfg.ParseFile(lockfilePath, &lockFile); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tchanged := false\n\t\tfor key, latestPkg := range latestPackages {\n\t\t\tif pkg, exists := lockFile.Packages[key]; exists {\n\t\t\t\tname, _, found := searcher.ParseVersionedPackage(key)\n\t\t\t\tif len(pkgMap) > 0 && (!pkgMap[key] && (found && !pkgMap[name])) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif pkg.LastModified != latestPkg.LastModified {\n\t\t\t\t\tlockFile.Packages[key].AllowInsecure = latestPkg.AllowInsecure\n\t\t\t\t\tlockFile.Packages[key].LastModified = latestPkg.LastModified\n\t\t\t\t\t// PluginVersion is intentionally omitted\n\t\t\t\t\tlockFile.Packages[key].Resolved = latestPkg.Resolved\n\t\t\t\t\tlockFile.Packages[key].Source = latestPkg.Source\n\t\t\t\t\tlockFile.Packages[key].Version = latestPkg.Version\n\t\t\t\t\tlockFile.Packages[key].Systems = latestPkg.Systems\n\t\t\t\t\tchanged = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif changed {\n\t\t\tif err = cuecfg.WriteFile(lockfilePath, lockFile); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Printf(\"Updated: %s\\n\", lockfilePath)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc latestPackages(lockfilePaths []string) (map[string]*lock.Package, error) {\n\tlatestPackages := make(map[string]*lock.Package)\n\n\tfor _, lockFilePath := range lockfilePaths {\n\t\tvar lockFile lock.File\n\t\tif err := cuecfg.ParseFile(lockFilePath, &lockFile); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor key, pkg := range lockFile.Packages {\n\t\t\tif latestPkg, exists := latestPackages[key]; exists {\n\t\t\t\t// Ignore error, which makes currentTime.After always false.\n\t\t\t\tcurrentTime, _ := time.Parse(time.RFC3339, pkg.LastModified)\n\t\t\t\tlatestTime, err := time.Parse(time.RFC3339, latestPkg.LastModified)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tif currentTime.After(latestTime) {\n\t\t\t\t\tlatestPackages[key] = pkg\n\t\t\t\t}\n\t\t\t} else if _, err := time.Parse(time.RFC3339, pkg.LastModified); err == nil {\n\t\t\t\tlatestPackages[key] = pkg\n\t\t\t}\n\t\t}\n\t}\n\n\treturn latestPackages, nil\n}\n\nfunc collectLockfiles() ([]string, error) {\n\tdefer debug.FunctionTimer().End()\n\n\tvar lockfiles []string\n\terr := filepath.WalkDir(\n\t\t\".\",\n\t\tfunc(path string, dirEntry fs.DirEntry, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif !dirEntry.IsDir() && filepath.Base(path) == \"devbox.lock\" {\n\t\t\t\tlockfiles = append(lockfiles, path)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t)\n\n\treturn lockfiles, err\n}\n"
  },
  {
    "path": "internal/boxcli/patch.go",
    "content": "package boxcli\n\nimport (\n\t\"github.com/spf13/cobra\"\n\t\"go.jetify.com/devbox/internal/patchpkg\"\n)\n\nfunc patchCmd() *cobra.Command {\n\tbuilder := &patchpkg.DerivationBuilder{}\n\tcmd := &cobra.Command{\n\t\tUse:    \"patch <store-path>\",\n\t\tShort:  \"Apply Devbox patches to a package to fix common linker errors\",\n\t\tArgs:   cobra.ExactArgs(1),\n\t\tHidden: true,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn builder.Build(cmd.Context(), args[0])\n\t\t},\n\t}\n\tcmd.Flags().StringVar(&builder.Glibc, \"glibc\", \"\", \"patch binaries to use a different glibc\")\n\tcmd.Flags().StringVar(&builder.Gcc, \"gcc\", \"\", \"patch binaries to use a different gcc\")\n\tcmd.Flags().BoolVar(&builder.RestoreRefs, \"restore-refs\", false, \"restore references to removed store paths\")\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/boxcli/path.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n)\n\ntype pathCmdFlags struct {\n\tconfig configFlags\n}\n\nfunc pathCmd() *cobra.Command {\n\tflags := pathCmdFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"path\",\n\t\tShort:   \"Show path to [global] devbox config\",\n\t\tPreRunE: ensureNixInstalled,\n\t\tArgs:    cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tfmt.Println(flags.config.path)\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tflags.config.register(cmd)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/boxcli/pull.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/AlecAivazis/survey/v2\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devbox/providers/identity\"\n\t\"go.jetify.com/devbox/internal/goutil\"\n\t\"go.jetify.com/devbox/internal/pullbox/s3\"\n\t\"go.jetify.com/pkg/auth\"\n)\n\ntype pullCmdFlags struct {\n\tconfig configFlags\n\tforce  bool\n}\n\nfunc pullCmd() *cobra.Command {\n\tflags := pullCmdFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"pull <file> | <url>\",\n\t\tShort:   \"Pull a config from a file or URL\",\n\t\tLong:    \"Pull a config from a file or URL. URLs must be prefixed with 'http://' or 'https://'.\",\n\t\tArgs:    cobra.MaximumNArgs(1),\n\t\tPreRunE: ensureNixInstalled,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn pullCmdFunc(cmd, goutil.GetDefaulted(args, 0), &flags)\n\t\t},\n\t}\n\n\tcmd.Flags().BoolVarP(\n\t\t&flags.force, \"force\", \"f\", false,\n\t\t\"Force overwrite of existing [global] config files\",\n\t)\n\n\tflags.config.register(cmd)\n\n\treturn cmd\n}\n\nfunc pullCmdFunc(cmd *cobra.Command, url string, flags *pullCmdFlags) error {\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tpullPath, err := absolutizeIfLocal(url)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tvar creds devopt.Credentials\n\tt, err := identity.GenSession(cmd.Context())\n\tif err != nil && !errors.Is(err, auth.ErrNotLoggedIn) {\n\t\treturn errors.WithStack(err)\n\t} else if t != nil && err == nil {\n\t\tcreds = devopt.Credentials{\n\t\t\tIDToken: t.IDToken,\n\t\t\tEmail:   t.IDClaims().Email,\n\t\t\tSub:     t.IDClaims().Subject,\n\t\t}\n\t}\n\n\terr = box.Pull(cmd.Context(), devopt.PullboxOpts{\n\t\tURL:         pullPath,\n\t\tOverwrite:   flags.force,\n\t\tCredentials: creds,\n\t})\n\tif prompt := pullErrorPrompt(err); prompt != \"\" {\n\t\tprompt := &survey.Confirm{Message: prompt}\n\t\tif err = survey.AskOne(prompt, &flags.force); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\tif !flags.force {\n\t\t\treturn nil\n\t\t}\n\t\terr = box.Pull(cmd.Context(), devopt.PullboxOpts{\n\t\t\tURL:         pullPath,\n\t\t\tOverwrite:   flags.force,\n\t\t\tCredentials: creds,\n\t\t})\n\t}\n\tif errors.Is(err, s3.ErrProfileNotFound) {\n\t\treturn usererr.New(\n\t\t\t\"Profile not found. Use `devbox global push` to create a new profile.\",\n\t\t)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn installCmdFunc(\n\t\tcmd,\n\t\tinstallCmdFlags{\n\t\t\trunCmdFlags: runCmdFlags{config: configFlags{pathFlag: pathFlag{path: flags.config.path}}},\n\t\t},\n\t)\n}\n\nfunc pullErrorPrompt(err error) string {\n\tswitch {\n\tcase errors.Is(err, fs.ErrExist):\n\t\treturn \"Global profile already exists. Overwrite?\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc absolutizeIfLocal(path string) (string, error) {\n\tif _, err := os.Stat(path); err == nil {\n\t\treturn filepath.Abs(path)\n\t}\n\treturn path, nil\n}\n"
  },
  {
    "path": "internal/boxcli/push.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"go.jetify.com/pkg/auth\"\n\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devbox/providers/identity\"\n\t\"go.jetify.com/devbox/internal/goutil\"\n)\n\ntype pushCmdFlags struct {\n\tconfig configFlags\n}\n\nfunc pushCmd() *cobra.Command {\n\tflags := pushCmdFlags{}\n\tcmd := &cobra.Command{\n\t\tUse: \"push <git-repo>\",\n\t\tShort: \"Push a [global] config. Leave empty to use jetify cloud. Can \" +\n\t\t\t\"be a git repo for self storage.\",\n\t\tArgs: cobra.MaximumNArgs(1),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn pushCmdFunc(cmd, goutil.GetDefaulted(args, 0), flags)\n\t\t},\n\t}\n\n\tflags.config.register(cmd)\n\n\treturn cmd\n}\n\nfunc pushCmdFunc(cmd *cobra.Command, url string, flags pushCmdFlags) error {\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tt, err := identity.GenSession(cmd.Context())\n\tvar creds devopt.Credentials\n\tif err != nil && !errors.Is(err, auth.ErrNotLoggedIn) {\n\t\treturn errors.WithStack(err)\n\t} else if t != nil && err == nil {\n\t\tcreds = devopt.Credentials{\n\t\t\tIDToken: t.IDToken,\n\t\t\tEmail:   t.IDClaims().Email,\n\t\t\tSub:     t.IDClaims().Subject,\n\t\t}\n\t}\n\treturn box.Push(cmd.Context(), devopt.PullboxOpts{\n\t\tURL:         url,\n\t\tCredentials: creds,\n\t})\n}\n"
  },
  {
    "path": "internal/boxcli/rm.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n)\n\ntype removeCmdFlags struct {\n\tconfig configFlags\n}\n\nfunc removeCmd() *cobra.Command {\n\tflags := removeCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:     \"rm <pkg>...\",\n\t\tShort:   \"Remove a package from your devbox\",\n\t\tArgs:    cobra.MinimumNArgs(1),\n\t\tPreRunE: ensureNixInstalled,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn runRemoveCmd(cmd, args, flags)\n\t\t},\n\t}\n\n\tflags.config.register(command)\n\treturn command\n}\n\nfunc runRemoveCmd(cmd *cobra.Command, args []string, flags removeCmdFlags) error {\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn box.Remove(cmd.Context(), args...)\n}\n"
  },
  {
    "path": "internal/boxcli/root.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/featureflag\"\n\t\"go.jetify.com/devbox/internal/boxcli/midcobra\"\n\t\"go.jetify.com/devbox/internal/cmdutil\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/telemetry\"\n\t\"go.jetify.com/devbox/internal/vercheck\"\n)\n\ntype cobraFunc func(cmd *cobra.Command, args []string) error\n\nvar (\n\tdebugMiddleware = &midcobra.DebugMiddleware{}\n\ttraceMiddleware = &midcobra.TraceMiddleware{}\n)\n\ntype rootCmdFlags struct {\n\tquiet bool\n}\n\nfunc RootCmd() *cobra.Command {\n\tflags := rootCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"devbox\",\n\t\tShort: \"Instant, easy, predictable development environments\",\n\t\t// Warning, PersistentPreRunE is not called if a subcommand also declares\n\t\t// it. TODO: Figure out a better way to implement this so that subcommands\n\t\t// can't accidentally override it.\n\t\tPersistentPreRun: func(cmd *cobra.Command, args []string) {\n\t\t\tif flags.quiet {\n\t\t\t\tcmd.SetErr(io.Discard)\n\t\t\t}\n\t\t\tvercheck.CheckVersion(cmd.ErrOrStderr(), cmd.CommandPath())\n\t\t},\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn cmd.Help()\n\t\t},\n\t\tSilenceErrors: true,\n\t\tSilenceUsage:  true,\n\t}\n\n\t// Stable commands\n\tcommand.AddCommand(addCmd())\n\tif featureflag.Auth.Enabled() {\n\t\tcommand.AddCommand(authCmd())\n\t}\n\tcommand.AddCommand(cacheCmd())\n\tcommand.AddCommand(createCmd())\n\tcommand.AddCommand(secretsCmd())\n\tcommand.AddCommand(generateCmd())\n\tcommand.AddCommand(globalCmd())\n\tcommand.AddCommand(infoCmd())\n\tcommand.AddCommand(initCmd())\n\tcommand.AddCommand(installCmd())\n\tcommand.AddCommand(integrateCmd())\n\tcommand.AddCommand(listCmd())\n\tcommand.AddCommand(logCmd())\n\tcommand.AddCommand(patchCmd())\n\tcommand.AddCommand(removeCmd())\n\tcommand.AddCommand(runCmd(runFlagDefaults{}))\n\tcommand.AddCommand(searchCmd())\n\tcommand.AddCommand(servicesCmd())\n\tcommand.AddCommand(setupCmd())\n\tcommand.AddCommand(shellCmd(shellFlagDefaults{}))\n\tcommand.AddCommand(shellEnvCmd(shellenvFlagDefaults{\n\t\trecomputeEnv: true,\n\t}))\n\tcommand.AddCommand(updateCmd())\n\tcommand.AddCommand(versionCmd())\n\t// Internal commands\n\tcommand.AddCommand(genDocsCmd())\n\n\t// Register the \"all\" command to list all commands, including hidden ones.\n\t// This makes debugging easier.\n\tcommand.AddCommand(&cobra.Command{\n\t\tUse:    \"all\",\n\t\tShort:  \"List all commands, including hidden ones\",\n\t\tHidden: true,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tlistAllCommands(command, \"\")\n\t\t},\n\t})\n\n\tcommand.PersistentFlags().BoolVarP(\n\t\t&flags.quiet, \"quiet\", \"q\", false, \"suppresses logs\")\n\tdebugMiddleware.AttachToFlag(command.PersistentFlags(), \"debug\")\n\ttraceMiddleware.AttachToFlag(command.PersistentFlags(), \"trace\")\n\n\treturn command\n}\n\nfunc Execute(ctx context.Context, args []string) int {\n\tdefer debug.Recover()\n\trootCmd := RootCmd()\n\texe := midcobra.New(rootCmd)\n\texe.AddMiddleware(traceMiddleware)\n\texe.AddMiddleware(midcobra.Telemetry())\n\texe.AddMiddleware(debugMiddleware)\n\treturn exe.Execute(ctx, wrapArgsForRun(rootCmd, args))\n}\n\nfunc Main() {\n\ttimer := debug.Timer(strings.Join(os.Args, \" \"))\n\tsetSystemBinaryPaths()\n\tctx := context.Background()\n\n\tif len(os.Args) > 1 && os.Args[1] == \"upload-telemetry\" {\n\t\t// This subcommand is hidden and only run by devbox itself as a\n\t\t// child process. We need to really make sure that we always\n\t\t// exit and don't leave orphaned processes laying around.\n\t\ttime.AfterFunc(5*time.Second, func() {\n\t\t\tos.Exit(0)\n\t\t})\n\t\ttelemetry.Upload()\n\t\treturn\n\t}\n\n\tcode := Execute(ctx, os.Args[1:])\n\t// Run out here instead of as a middleware so we can capture any time we spend\n\t// in middlewares as well.\n\ttimer.End()\n\tos.Exit(code)\n}\n\nfunc listAllCommands(cmd *cobra.Command, indent string) {\n\t// Print this command's name and description in table format with indentation\n\tfmt.Printf(\"%s%-20s%s\\n\", indent, cmd.Use, cmd.Short)\n\n\t// Recursively list child commands with increased indentation\n\tfor _, childCmd := range cmd.Commands() {\n\t\tlistAllCommands(childCmd, indent+\"\\t\")\n\t}\n}\n\nfunc setSystemBinaryPaths() {\n\tif os.Getenv(\"DEVBOX_SYSTEM_BASH\") == \"\" {\n\t\tos.Setenv(\"DEVBOX_SYSTEM_BASH\", cmdutil.GetPathOrDefault(\"bash\", \"/bin/bash\"))\n\t}\n\tif os.Getenv(\"DEVBOX_SYSTEM_SED\") == \"\" {\n\t\tos.Setenv(\"DEVBOX_SYSTEM_SED\", cmdutil.GetPathOrDefault(\"sed\", \"/usr/bin/sed\"))\n\t}\n}\n"
  },
  {
    "path": "internal/boxcli/run.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/multi\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/redact\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\ntype runCmdFlags struct {\n\tenvFlag\n\tconfig       configFlags\n\tomitNixEnv   bool\n\tpure         bool\n\tlistScripts  bool\n\trecomputeEnv bool\n\tallProjects  bool\n}\n\n// runFlagDefaults are the flag default values that differ\n// from the `devbox` command versus `devbox global` command.\ntype runFlagDefaults struct {\n\tomitNixEnv bool\n}\n\nfunc runCmd(defaults runFlagDefaults) *cobra.Command {\n\tflags := runCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"run [<script> | <cmd>]\",\n\t\tShort: \"Run a script or command in a shell with access to your packages\",\n\t\tLong: \"Start a new shell and runs your script or command in it, exiting when done.\\n\\n\" +\n\t\t\t\"The script must be defined in `devbox.json`, or else it will be interpreted as an \" +\n\t\t\t\"arbitrary command. You can pass arguments to your script or command. Everything \" +\n\t\t\t\"after `--` will be passed verbatim into your command (see examples).\\n\\n\",\n\t\tExample: \"\\nRun a command directly:\\n\\n  devbox add cowsay\\n  devbox run cowsay hello\\n  \" +\n\t\t\t\"devbox run -- cowsay -d hello\\n\\nRun a script (defined as `\\\"moo\\\": \\\"cowsay moo\\\"`) \" +\n\t\t\t\"in your devbox.json:\\n\\n  devbox run moo\",\n\t\tPreRunE: ensureNixInstalled,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn runScriptCmd(cmd, args, flags)\n\t\t},\n\t}\n\n\tflags.envFlag.register(command)\n\tflags.config.register(command)\n\tcommand.Flags().BoolVar(\n\t\t&flags.pure, \"pure\", false, \"if this flag is specified, devbox runs the script in an isolated environment inheriting almost no variables from the current environment. A few variables, in particular HOME, USER and DISPLAY, are retained.\")\n\tcommand.Flags().BoolVarP(\n\t\t&flags.listScripts, \"list\", \"l\", false, \"list all scripts defined in devbox.json\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.omitNixEnv, \"omit-nix-env\", defaults.omitNixEnv,\n\t\t\"shell environment will omit the env-vars from print-dev-env\",\n\t)\n\t_ = command.Flags().MarkHidden(\"omit-nix-env\")\n\tcommand.Flags().BoolVar(&flags.recomputeEnv, \"recompute\", true, \"recompute environment if needed\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.allProjects,\n\t\t\"all-projects\",\n\t\tfalse,\n\t\t\"run command in all projects in the working directory, recursively. If command is not found in any project, it will be skipped.\",\n\t)\n\n\tcommand.ValidArgs = listScripts(command, flags)\n\n\treturn command\n}\n\nfunc listScripts(cmd *cobra.Command, flags runCmdFlags) []string {\n\tpath := flags.config.path\n\n\t// Special code path for shell completion.\n\t// Landau: I'm not entirely sure why:\n\t// * Flags need to be parsed again\n\t// * cmd.Flag(\"config\") contains the correct value, but flags.config.path is empty\n\t// Give my low confidence, I'm making this a very narrow code path.\n\tif path == \"\" && slices.Contains(os.Args, \"__complete\") {\n\t\t_ = cmd.ParseFlags(os.Args)\n\t\tif flag := cmd.Flag(\"config\"); flag != nil && flag.Value != nil {\n\t\t\tpath = flag.Value.String()\n\t\t}\n\t}\n\n\tdevboxOpts := &devopt.Opts{\n\t\tDir:            path,\n\t\tEnvironment:    flags.config.environment,\n\t\tStderr:         cmd.ErrOrStderr(),\n\t\tIgnoreWarnings: true,\n\t}\n\n\tif flags.allProjects {\n\t\tboxes, err := multi.Open(devboxOpts)\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed to open devbox\", \"err\", err)\n\t\t\treturn nil\n\t\t}\n\t\tscripts := []string{}\n\t\tfor _, box := range boxes {\n\t\t\tscripts = append(scripts, box.ListScripts()...)\n\t\t}\n\t\tsort.Strings(scripts)\n\t\treturn lo.Uniq(scripts)\n\t}\n\tbox, err := devbox.Open(devboxOpts)\n\tif err != nil {\n\t\tslog.Error(\"failed to open devbox\", \"err\", err)\n\t\treturn nil\n\t}\n\treturn box.ListScripts()\n}\n\nfunc runScriptCmd(cmd *cobra.Command, args []string, flags runCmdFlags) error {\n\tctx := cmd.Context()\n\tif len(args) == 0 || flags.listScripts {\n\t\tscripts := listScripts(cmd, flags)\n\t\tif len(scripts) == 0 {\n\t\t\tfmt.Fprintln(cmd.OutOrStdout(), \"no scripts defined in devbox.json\")\n\t\t\treturn nil\n\t\t}\n\t\tfmt.Fprintln(cmd.OutOrStdout(), \"Available scripts:\")\n\t\tfor _, p := range scripts {\n\t\t\tfmt.Fprintf(cmd.OutOrStdout(), \"* %s\\n\", p)\n\t\t}\n\t\treturn nil\n\t}\n\n\tpath, script, scriptArgs, err := parseScriptArgs(args, flags)\n\tif err != nil {\n\t\treturn redact.Errorf(\"error parsing script arguments: %w\", err)\n\t}\n\tslog.Debug(\"run script\", \"script\", script, \"args\", scriptArgs)\n\n\tenv, err := flags.Env(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tboxes := []*devbox.Devbox{}\n\tdevboxOpts := &devopt.Opts{\n\t\tDir:         path,\n\t\tEnv:         env,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t}\n\n\tif flags.allProjects {\n\t\tboxes, err = multi.Open(devboxOpts)\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t} else {\n\t\tbox, err := devbox.Open(devboxOpts)\n\t\tif err != nil {\n\t\t\treturn redact.Errorf(\"error reading devbox.json: %w\", err)\n\t\t}\n\t\tboxes = append(boxes, box)\n\t}\n\n\tenvOpts := devopt.EnvOptions{\n\t\tHooks: devopt.LifecycleHooks{\n\t\t\tOnStaleState: func() {\n\t\t\t\tif !flags.recomputeEnv {\n\t\t\t\t\tux.FHidableWarning(\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\tcmd.ErrOrStderr(),\n\t\t\t\t\t\tdevbox.StateOutOfDateMessage,\n\t\t\t\t\t\t\"with --recompute=true\",\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\tOmitNixEnv:    flags.omitNixEnv,\n\t\tPure:          flags.pure,\n\t\tSkipRecompute: !flags.recomputeEnv,\n\t}\n\n\tif flags.allProjects {\n\t\tboxes = lo.Filter(boxes, func(box *devbox.Devbox, _ int) bool {\n\t\t\treturn slices.Contains(box.ListScripts(), script)\n\t\t})\n\t}\n\n\tfor _, box := range boxes {\n\t\tux.Finfof(\n\t\t\tcmd.ErrOrStderr(),\n\t\t\t\"Running script %q on %s\\n\",\n\t\t\tscript,\n\t\t\tbox.ProjectDir(),\n\t\t)\n\t\tif err := box.RunScript(ctx, envOpts, script, scriptArgs); err != nil {\n\t\t\treturn redact.Errorf(\"error running script %q in Devbox: %w\", script, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc parseScriptArgs(args []string, flags runCmdFlags) (string, string, []string, error) {\n\tif len(args) == 0 {\n\t\t// this should never happen because cobra should prevent it, but it's better to be defensive.\n\t\treturn \"\", \"\", nil, usererr.New(\"no command or script provided\")\n\t}\n\n\tscript := args[0]\n\tscriptArgs := args[1:]\n\n\treturn flags.config.path, script, scriptArgs, nil\n}\n\nfunc wrapArgsForRun(rootCmd *cobra.Command, args []string) []string {\n\t// if the first argument is not \"run\", we don't need to do anything. If there\n\t// are 2 or fewer arguments, we also don't need to do anything because there\n\t// are no flags after a non-run non-flag arg.\n\t// IMPROVEMENT: technically users can pass a flag before the subcommand \"run\"\n\tif len(args) <= 2 || args[0] != \"run\" || slices.Contains(args, \"--\") {\n\t\treturn args\n\t}\n\n\tcmd, found := lo.Find(\n\t\trootCmd.Commands(),\n\t\tfunc(item *cobra.Command) bool { return item.Name() == \"run\" },\n\t)\n\tif !found {\n\t\treturn args\n\t}\n\t_ = cmd.InheritedFlags() // bug in cobra requires this to be called to ensure flags contains inherited flags.\n\trunFlags := cmd.Flags()\n\t// typical args can be of the form:\n\t// run --flag1 val1 -f val2 --flag3=val3 --bool-flag python --version\n\t// We handle each different type of flag\n\t// (flag with equals, long-form, short-form, and defaulted flags)\n\t// Note that defaulted does not mean initial value, it only means flags\n\t// that don't require a value.\n\t// For example, --bool-flag has NoOptDefVal set to \"true\".\n\ti := 1\n\tfor i < len(args) {\n\t\targ := args[i]\n\t\tif !strings.HasPrefix(arg, \"-\") {\n\t\t\t// We found and argument that is not part of the flags, so we can stop\n\t\t\t// This inserts a \"--\" before the first non-flag argument\n\t\t\t// Turning\n\t\t\t// run --flag1 val1 command --flag2 val2\n\t\t\t// into\n\t\t\t// run --flag1 val1 -- command --flag2 val2\n\t\t\treturn append(args[:i+1], append([]string{\"--\"}, args[i+1:]...)...)\n\t\t}\n\n\t\tif strings.HasPrefix(arg, \"-\") && strings.Contains(arg, \"=\") {\n\t\t\t// This is a flag with an equals sign, so we can skip it\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\n\t\tvar flag *pflag.Flag\n\t\tif strings.HasPrefix(arg, \"--\") {\n\t\t\tflag = runFlags.Lookup(strings.TrimLeft(arg, \"-\"))\n\t\t} else {\n\t\t\tflag = runFlags.ShorthandLookup(strings.TrimLeft(arg, \"-\"))\n\t\t}\n\t\tif flag == nil {\n\t\t\t// found an invalid flag, just return args as-is\n\t\t\treturn args\n\t\t}\n\t\tif flag.NoOptDefVal == \"\" {\n\t\t\t// This is a non-boolean flag, e.g. --flag1 val1\n\t\t\ti += 2\n\t\t} else {\n\t\t\t// This is a boolean flag, e.g. --bool-flag\n\t\t\ti++\n\t\t}\n\t}\n\n\t// This means there is no non-flag command. Just return as is.\n\treturn args\n}\n"
  },
  {
    "path": "internal/boxcli/search.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/searcher\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\nconst trimmedVersionsLength = 10\n\ntype searchCmdFlags struct {\n\tshowAll bool\n}\n\nfunc searchCmd() *cobra.Command {\n\tflags := &searchCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"search <pkg>\",\n\t\tShort: \"Search for nix packages\",\n\t\tArgs:  cobra.ExactArgs(1),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tquery := args[0]\n\t\t\tname, version, isVersioned := searcher.ParseVersionedPackage(query)\n\t\t\tif !isVersioned {\n\t\t\t\tresults, err := searcher.Client().Search(cmd.Context(), query)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn printSearchResults(\n\t\t\t\t\tcmd.OutOrStdout(), query, results, flags.showAll)\n\t\t\t}\n\t\t\tpackageVersion, err := searcher.Client().Resolve(name, version)\n\t\t\tif err != nil {\n\t\t\t\t// This is not ideal. Search service should return valid response we\n\t\t\t\t// can parse\n\t\t\t\treturn usererr.WithUserMessage(err, \"No results found for %q\\n\", query)\n\t\t\t}\n\t\t\tfmt.Fprintf(\n\t\t\t\tcmd.OutOrStdout(),\n\t\t\t\t\"%s resolves to: %s@%s\\n\",\n\t\t\t\tquery,\n\t\t\t\tpackageVersion.Name,\n\t\t\t\tpackageVersion.Version,\n\t\t\t)\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tcommand.Flags().BoolVar(\n\t\t&flags.showAll, \"show-all\", false,\n\t\t\"show all available templates\",\n\t)\n\n\treturn command\n}\n\nfunc printSearchResults(\n\tw io.Writer,\n\tquery string,\n\tresults *searcher.SearchResults,\n\tshowAll bool,\n) error {\n\tif len(results.Packages) == 0 {\n\t\tfmt.Fprintf(w, \"No results found for %q\\n\", query)\n\t\treturn nil\n\t}\n\tfmt.Fprintf(\n\t\tw,\n\t\t\"Found %d+ results for %q:\\n\\n\",\n\t\tresults.NumResults,\n\t\tquery,\n\t)\n\n\tresultsAreTrimmed := false\n\tpkgs := results.Packages\n\tif !showAll && len(pkgs) > trimmedVersionsLength {\n\t\tresultsAreTrimmed = true\n\t\tpkgs = results.Packages[:int(math.Min(10, float64(len(results.Packages))))]\n\t}\n\n\tfor _, pkg := range pkgs {\n\t\tnonEmptyVersions := []string{}\n\t\tfor i, v := range pkg.Versions {\n\t\t\tif !showAll && i >= trimmedVersionsLength {\n\t\t\t\tresultsAreTrimmed = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif v.Version != \"\" {\n\t\t\t\tnonEmptyVersions = append(nonEmptyVersions, v.Version)\n\t\t\t}\n\t\t}\n\n\t\tversionString := \"\"\n\t\tif len(nonEmptyVersions) > 0 {\n\t\t\tellipses := lo.Ternary(resultsAreTrimmed && pkg.NumVersions > trimmedVersionsLength, \" ...\", \"\")\n\t\t\tif showAll {\n\t\t\t\tversionString = fmt.Sprintf(\"\\n > %s \\n\", strings.Join(nonEmptyVersions, \"\\n > \"))\n\t\t\t} else {\n\t\t\t\tversionString = fmt.Sprintf(\" (%s%s)\", strings.Join(nonEmptyVersions, \", \"), ellipses)\n\t\t\t}\n\t\t}\n\t\tfmt.Fprintf(w, \"* %s %s\\n\", pkg.Name, versionString)\n\t}\n\n\tif resultsAreTrimmed {\n\t\tfmt.Println()\n\t\tux.Fwarningf(\n\t\t\tw,\n\t\t\t\"Showing top 10 results and truncated versions. Use --show-all to \"+\n\t\t\t\t\"show all.\\n\\n\",\n\t\t)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/boxcli/secrets.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/fileutil\"\n\t\"go.jetify.com/devbox/internal/ux\"\n\t\"go.jetify.com/envsec/pkg/envsec\"\n)\n\ntype secretsFlags struct {\n\tconfig configFlags\n}\n\nfunc (f *secretsFlags) envsec(cmd *cobra.Command) (*envsec.Envsec, error) {\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         f.config.path,\n\t\tEnvironment: f.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\treturn box.Secrets(cmd.Context())\n}\n\ntype secretsInitCmdFlags struct {\n\tforce bool\n}\n\ntype secretsListFlags struct {\n\tshow   bool\n\tformat string\n}\n\ntype secretsDownloadFlags struct {\n\tformat string\n}\n\ntype secretsUploadFlags struct {\n\tformat string\n}\n\nfunc secretsCmd() *cobra.Command {\n\tflags := &secretsFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:               \"secrets\",\n\t\tAliases:           []string{\"envsec\"},\n\t\tShort:             \"Interact with devbox secrets in jetify cloud.\",\n\t\tPersistentPreRunE: ensureNixInstalled,\n\t}\n\tcmd.AddCommand(secretsDownloadCmd(flags))\n\tcmd.AddCommand(secretsInitCmd(flags))\n\tcmd.AddCommand(secretsListCmd(flags))\n\tcmd.AddCommand(secretsRemoveCmd(flags))\n\tcmd.AddCommand(secretsSetCmd(flags))\n\tcmd.AddCommand(secretsUploadCmd(flags))\n\n\tflags.config.registerPersistent(cmd)\n\n\treturn cmd\n}\n\nfunc secretsInitCmd(secretsFlags *secretsFlags) *cobra.Command {\n\tflags := secretsInitCmdFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:   \"init\",\n\t\tShort: \"Initialize secrets management with jetify cloud\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn secretsInitFunc(cmd, flags, secretsFlags)\n\t\t},\n\t}\n\n\tcmd.Flags().BoolVarP(\n\t\t&flags.force,\n\t\t\"force\",\n\t\t\"f\",\n\t\tfalse,\n\t\t\"Force initialization even if already initialized\",\n\t)\n\n\treturn cmd\n}\n\nfunc secretsSetCmd(flags *secretsFlags) *cobra.Command {\n\treturn &cobra.Command{\n\t\tUse:   \"set <NAME1>=<value1> [<NAME2>=<value2>]...\",\n\t\tShort: \"Securely store one or more environment variables\",\n\t\tLong:  \"Securely store one or more environment variables. To test contents of a file as a secret use set=@<file>\",\n\t\tArgs:  cobra.MinimumNArgs(1),\n\t\tPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn envsec.ValidateSetArgs(args)\n\t\t},\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tsecrets, err := flags.envsec(cmd)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\n\t\t\treturn secrets.SetFromArgs(cmd.Context(), args)\n\t\t},\n\t}\n}\n\nfunc secretsRemoveCmd(flags *secretsFlags) *cobra.Command {\n\treturn &cobra.Command{\n\t\tUse:     \"remove <NAME1> [<NAME2>]...\",\n\t\tShort:   \"Remove one or more environment variables\",\n\t\tAliases: []string{\"rm\"},\n\t\tArgs:    cobra.MinimumNArgs(1),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tsecrets, err := flags.envsec(cmd)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\n\t\t\treturn secrets.DeleteAll(cmd.Context(), args...)\n\t\t},\n\t}\n}\n\nfunc secretsListCmd(commonFlags *secretsFlags) *cobra.Command {\n\tflags := secretsListFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"list\",\n\t\tAliases: []string{\"ls\"},\n\t\tShort:   \"List all secrets\",\n\t\tArgs:    cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tsecrets, err := commonFlags.envsec(cmd)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\n\t\t\tvars, err := secrets.List(cmd.Context())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn envsec.PrintEnvVar(\n\t\t\t\tcmd.OutOrStdout(), secrets.EnvID, vars, flags.show, flags.format)\n\t\t},\n\t}\n\n\tcmd.Flags().BoolVarP(\n\t\t&flags.show,\n\t\t\"show\",\n\t\t\"s\",\n\t\tfalse,\n\t\t\"Display secret values in plaintext\",\n\t)\n\tcmd.Flags().StringVarP(\n\t\t&flags.format,\n\t\t\"format\",\n\t\t\"f\",\n\t\t\"table\",\n\t\t\"Display the key values of each secret in the specified format, one of: table | dotenv | json.\",\n\t)\n\treturn cmd\n}\n\nfunc secretsDownloadCmd(commonFlags *secretsFlags) *cobra.Command {\n\tflags := secretsDownloadFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"download <file1>\",\n\t\tShort: \"Download environment variables into the specified file\",\n\t\tArgs:  cobra.ExactArgs(1),\n\t\tPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn envsec.ValidateFormat(flags.format)\n\t\t},\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tsecrets, err := commonFlags.envsec(cmd)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\tabsPaths, err := fileutil.EnsureAbsolutePaths(args)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\treturn secrets.Download(cmd.Context(), absPaths[0], flags.format)\n\t\t},\n\t}\n\n\tcommand.Flags().StringVarP(\n\t\t&flags.format, \"format\", \"f\", \"\", \"file format: dotenv or json\")\n\n\treturn command\n}\n\nfunc secretsUploadCmd(commonFlags *secretsFlags) *cobra.Command {\n\tflags := &secretsUploadFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"upload <file1> [<fileN>]...\",\n\t\tShort: \"Upload variables defined in one or more .env files.\",\n\t\tArgs:  cobra.MinimumNArgs(1),\n\t\tPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn envsec.ValidateFormat(flags.format)\n\t\t},\n\t\tRunE: func(cmd *cobra.Command, paths []string) error {\n\t\t\tsecrets, err := commonFlags.envsec(cmd)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\tabsPaths, err := fileutil.EnsureAbsolutePaths(paths)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\treturn secrets.Upload(cmd.Context(), absPaths, flags.format)\n\t\t},\n\t}\n\n\tcommand.Flags().StringVarP(\n\t\t&flags.format, \"format\", \"f\", \"\", \"File format: dotenv or json\")\n\n\treturn command\n}\n\nfunc secretsInitFunc(\n\tcmd *cobra.Command,\n\tflags secretsInitCmdFlags,\n\tsecretsFlags *secretsFlags,\n) error {\n\tctx := cmd.Context()\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:    secretsFlags.config.path,\n\t\tStderr: cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// devbox.Secrets() by default assumes project is initialized (and shows\n\t// error if not). So we use UninitializedSecrets() here instead.\n\tsecrets := box.UninitializedSecrets(ctx)\n\n\tif _, err := secrets.ProjectConfig(); err == nil &&\n\t\t!box.Config().IsEnvsecEnabled() {\n\t\t// Handle edge case where directory is already set up, but devbox.json is\n\t\t// not configured to use jetpack-cloud.\n\t\tux.Finfof(\n\t\t\tcmd.ErrOrStderr(),\n\t\t\t\"Secrets already initialized. Adding to devbox config.\\n\",\n\t\t)\n\t} else if err := secrets.NewProject(ctx, flags.force); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tbox.Config().Root.SetStringField(\"EnvFrom\", \"jetpack-cloud\")\n\treturn box.Config().Root.SaveTo(box.ProjectDir())\n}\n"
  },
  {
    "path": "internal/boxcli/services.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n)\n\ntype servicesCmdFlags struct {\n\tenvFlag\n\tconfig            configFlags\n\trunInCurrentShell bool\n}\n\ntype serviceUpFlags struct {\n\tbackground          bool\n\tprocessComposeFile  string\n\tprocessComposeFlags []string\n\tpcport              int\n}\n\ntype serviceStopFlags struct {\n\tallProjects bool\n}\n\nfunc (flags *serviceUpFlags) register(cmd *cobra.Command) {\n\tcmd.Flags().StringVar(\n\t\t&flags.processComposeFile,\n\t\t\"process-compose-file\",\n\t\t\"\",\n\t\t\"path to process compose file or directory containing process \"+\n\t\t\t\"compose-file.yaml|yml. Default is directory containing devbox.json\",\n\t)\n\tcmd.Flags().BoolVarP(\n\t\t&flags.background, \"background\", \"b\", false, \"run service in background\")\n\tcmd.Flags().StringArrayVar(\n\t\t&flags.processComposeFlags, \"pcflags\", []string{}, \"pass flags directly to process compose\")\n\tcmd.Flags().IntVarP(\n\t\t&flags.pcport, \"pcport\", \"p\", 0, \"specify the port for process-compose to use. You can also set the pcport by exporting DEVBOX_PC_PORT_NUM\")\n}\n\nfunc (flags *serviceStopFlags) register(cmd *cobra.Command) {\n\tcmd.Flags().BoolVar(\n\t\t&flags.allProjects, \"all-projects\", false, \"stop all running services across all your projects.\\nThis flag cannot be used simultaneously with the [services] argument\")\n}\n\nfunc servicesCmd(persistentPreRunE ...cobraFunc) *cobra.Command {\n\tflags := servicesCmdFlags{}\n\tserviceUpFlags := serviceUpFlags{}\n\tserviceStopFlags := serviceStopFlags{}\n\tservicesCommand := &cobra.Command{\n\t\tUse:   \"services\",\n\t\tShort: \"Interact with devbox services.\",\n\t\tLong: \"Interact with devbox services. Services start in a new shell. \" +\n\t\t\t\"Plugin services use environment variables specified by plugin unless \" +\n\t\t\t\"overridden by the user. To override plugin environment variables, use \" +\n\t\t\t\"the --env or --env-file flag. You may also override in devbox.json by \" +\n\t\t\t\"using the `env` field or exporting an environment variable in the \" +\n\t\t\t\"init hook.\",\n\t\tPersistentPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tpreruns := append([]cobraFunc{ensureNixInstalled}, persistentPreRunE...)\n\t\t\tfor _, fn := range preruns {\n\t\t\t\tif err := fn(cmd, args); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tattachCommand := &cobra.Command{\n\t\tUse:   \"attach\",\n\t\tShort: \"Attach to a running process-compose for the current project\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn attachServices(cmd, flags)\n\t\t},\n\t}\n\n\tlsCommand := &cobra.Command{\n\t\tUse:   \"ls\",\n\t\tShort: \"List available services\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn listServices(cmd, flags)\n\t\t},\n\t}\n\n\tstartCommand := &cobra.Command{\n\t\tUse:   \"start [service]...\",\n\t\tShort: \"Start service. If no service is specified, starts all services\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn startServices(cmd, args, flags)\n\t\t},\n\t}\n\n\tstopCommand := &cobra.Command{\n\t\tUse:     \"stop [service]...\",\n\t\tAliases: []string{\"down\"},\n\t\tShort:   `Stop one or more services in the current project. If no service is specified, stops all services in the current project.`,\n\t\tLong:    `Stop one or more services in the current project. If no service is specified, stops all services in the current project. \\nIf the --all-projects flag is specified, stops all running services across all your projects. This flag cannot be used with [service] arguments.`,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn stopServices(cmd, args, flags, serviceStopFlags)\n\t\t},\n\t}\n\n\trestartCommand := &cobra.Command{\n\t\tUse:   \"restart [service]...\",\n\t\tShort: \"Restart service. If no service is specified, restarts all services\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn restartServices(cmd, args, flags)\n\t\t},\n\t}\n\n\tupCommand := &cobra.Command{\n\t\tUse:   \"up [service]...\",\n\t\tShort: \"Starts process manager with specified services. If no services are listed, starts the process manager with all the services in your project\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn startProcessManager(cmd, args, flags, serviceUpFlags)\n\t\t},\n\t}\n\n\tpcportCommand := &cobra.Command{\n\t\tUse:   \"pcport\",\n\t\tShort: \"Display the port that process-compose is running on\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn showProcessComposePort(cmd, flags)\n\t\t},\n\t}\n\n\tflags.envFlag.register(servicesCommand)\n\tflags.config.registerPersistent(servicesCommand)\n\tservicesCommand.PersistentFlags().BoolVar(\n\t\t&flags.runInCurrentShell,\n\t\t\"run-in-current-shell\",\n\t\tfalse,\n\t\t\"run the command in the current shell instead of a new shell\",\n\t)\n\tservicesCommand.Flag(\"run-in-current-shell\").Hidden = true\n\tserviceUpFlags.register(upCommand)\n\tserviceStopFlags.register(stopCommand)\n\tservicesCommand.AddCommand(attachCommand)\n\tservicesCommand.AddCommand(lsCommand)\n\tservicesCommand.AddCommand(upCommand)\n\tservicesCommand.AddCommand(restartCommand)\n\tservicesCommand.AddCommand(startCommand)\n\tservicesCommand.AddCommand(stopCommand)\n\tservicesCommand.AddCommand(pcportCommand)\n\treturn servicesCommand\n}\n\nfunc attachServices(cmd *cobra.Command, flags servicesCmdFlags) error {\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn box.AttachToProcessManager(cmd.Context())\n}\n\nfunc listServices(cmd *cobra.Command, flags servicesCmdFlags) error {\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn box.ListServices(cmd.Context(), flags.runInCurrentShell)\n}\n\nfunc startServices(cmd *cobra.Command, services []string, flags servicesCmdFlags) error {\n\tenv, err := flags.Env(flags.config.path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tEnv:         env,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn box.StartServices(cmd.Context(), flags.runInCurrentShell, services...)\n}\n\nfunc stopServices(\n\tcmd *cobra.Command,\n\tservices []string,\n\tservicesFlags servicesCmdFlags,\n\tflags serviceStopFlags,\n) error {\n\tenv, err := servicesFlags.Env(servicesFlags.config.path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         servicesFlags.config.path,\n\t\tEnvironment: servicesFlags.config.environment,\n\t\tEnv:         env,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tif len(services) > 0 && flags.allProjects {\n\t\treturn errors.New(\"cannot use both services and --all-projects arguments simultaneously\")\n\t}\n\treturn box.StopServices(\n\t\tcmd.Context(), servicesFlags.runInCurrentShell, flags.allProjects, services...)\n}\n\nfunc restartServices(\n\tcmd *cobra.Command,\n\tservices []string,\n\tflags servicesCmdFlags,\n) error {\n\tenv, err := flags.Env(flags.config.path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tEnv:         env,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn box.RestartServices(cmd.Context(), flags.runInCurrentShell, services...)\n}\n\nfunc startProcessManager(\n\tcmd *cobra.Command,\n\targs []string,\n\tservicesFlags servicesCmdFlags,\n\tflags serviceUpFlags,\n) error {\n\tenv, err := servicesFlags.Env(servicesFlags.config.path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif flags.pcport < 0 {\n\t\treturn errors.Errorf(\"invalid pcport %d: ports cannot be less than 0\", flags.pcport)\n\t}\n\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:                      servicesFlags.config.path,\n\t\tEnv:                      env,\n\t\tEnvironment:              servicesFlags.config.environment,\n\t\tStderr:                   cmd.ErrOrStderr(),\n\t\tCustomProcessComposeFile: flags.processComposeFile,\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn box.StartProcessManager(\n\t\tcmd.Context(),\n\t\tservicesFlags.runInCurrentShell,\n\t\targs,\n\t\tdevopt.ProcessComposeOpts{\n\t\t\tBackground:         flags.background,\n\t\t\tExtraFlags:         flags.processComposeFlags,\n\t\t\tProcessComposePort: flags.pcport,\n\t\t},\n\t)\n}\n\nfunc showProcessComposePort(cmd *cobra.Command, flags servicesCmdFlags) error {\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn box.ShowProcessComposePort(cmd.Context(), cmd.OutOrStdout())\n}\n"
  },
  {
    "path": "internal/boxcli/setup.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"os\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/spf13/cobra\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\nconst nixDaemonFlag = \"daemon\"\n\nfunc setupCmd() *cobra.Command {\n\tsetupCommand := &cobra.Command{\n\t\tUse:    \"setup\",\n\t\tShort:  \"Setup devbox dependencies\",\n\t\tHidden: true,\n\t}\n\n\tinstallNixCommand := &cobra.Command{\n\t\tUse:   \"nix\",\n\t\tShort: \"Install Nix\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn runInstallNixCmd(cmd)\n\t\t},\n\t}\n\n\tinstallNixCommand.Flags().Bool(\n\t\tnixDaemonFlag,\n\t\tfalse,\n\t\t\"Install Nix in multi-user mode. This flag is not supported if you are using DetSys installer\",\n\t)\n\tsetupCommand.AddCommand(installNixCommand)\n\treturn setupCommand\n}\n\nfunc runInstallNixCmd(cmd *cobra.Command) error {\n\tif nix.BinaryInstalled() {\n\t\t// TODO: If existing installation is not detsys, but new installation is detsys can we detect\n\t\t// that and replace it?\n\t\tux.Finfof(\n\t\t\tcmd.ErrOrStderr(),\n\t\t\t\"Nix is already installed. If this is incorrect \"+\n\t\t\t\t\"please remove the nix-shell binary from your path.\\n\",\n\t\t)\n\t}\n\treturn new(nix.Installer).Run(cmd.Context())\n}\n\n// ensureNixInstalled verifies that nix is installed and that it is of a supported version\nfunc ensureNixInstalled(cmd *cobra.Command, _args []string) error {\n\treturn nix.EnsureNixInstalled(cmd.Context(), cmd.ErrOrStderr(), nixDaemonFlagVal(cmd))\n}\n\n// We return a closure to avoid printing the warning every time and just\n// printing it if we actually need the value of the flag.\n//\n// TODO: devbox.Open should run nix.EnsureNixInstalled and do this logic\n// internally. Then setup can decide if it wants to pass in the value of the\n// nixDaemonFlag (if changed).\nfunc nixDaemonFlagVal(cmd *cobra.Command) func() *bool {\n\treturn func() *bool {\n\t\tif !cmd.Flags().Changed(nixDaemonFlag) {\n\t\t\tif os.Geteuid() == 0 {\n\t\t\t\tux.Fwarningf(\n\t\t\t\t\tcmd.ErrOrStderr(),\n\t\t\t\t\t\"Running as root. Installing Nix in multi-user mode.\\n\",\n\t\t\t\t)\n\t\t\t\treturn lo.ToPtr(true)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\tval, err := cmd.Flags().GetBool(nixDaemonFlag)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &val\n\t}\n}\n"
  },
  {
    "path": "internal/boxcli/shell.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\ntype shellCmdFlags struct {\n\tenvFlag\n\tconfig       configFlags\n\tomitNixEnv   bool\n\tprintEnv     bool\n\tpure         bool\n\trecomputeEnv bool\n}\n\n// shellFlagDefaults are the flag default values that differ\n// from the `devbox` command versus `devbox global` command.\ntype shellFlagDefaults struct {\n\tomitNixEnv bool\n}\n\nfunc shellCmd(defaults shellFlagDefaults) *cobra.Command {\n\tflags := shellCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"shell\",\n\t\tShort: \"Start a new shell with access to your packages\",\n\t\tLong: \"Start a new shell with access to your packages.\\n\\n\" +\n\t\t\t\"If the --config flag is set, the shell will be started using the devbox.json found in the --config flag directory. \" +\n\t\t\t\"If --config isn't set, then devbox recursively searches the current directory and its parents.\",\n\t\tArgs:    cobra.NoArgs,\n\t\tPreRunE: ensureNixInstalled,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn runShellCmd(cmd, flags)\n\t\t},\n\t}\n\n\tcommand.Flags().BoolVar(\n\t\t&flags.printEnv, \"print-env\", false, \"print script to setup shell environment\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.pure, \"pure\", false, \"if this flag is specified, devbox creates an isolated shell inheriting almost no variables from the current environment. A few variables, in particular HOME, USER and DISPLAY, are retained.\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.omitNixEnv, \"omit-nix-env\", defaults.omitNixEnv,\n\t\t\"shell environment will omit the env-vars from print-dev-env\",\n\t)\n\t_ = command.Flags().MarkHidden(\"omit-nix-env\")\n\tcommand.Flags().BoolVar(&flags.recomputeEnv, \"recompute\", true, \"recompute environment if needed\")\n\n\tflags.config.register(command)\n\tflags.envFlag.register(command)\n\treturn command\n}\n\nfunc runShellCmd(cmd *cobra.Command, flags shellCmdFlags) error {\n\tctx := cmd.Context()\n\tenv, err := flags.Env(flags.config.path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check the directory exists.\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnv:         env,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tif flags.printEnv {\n\t\t// false for includeHooks is because init hooks is not compatible with .envrc files generated\n\t\t// by versions older than 0.4.6\n\t\tscript, err := box.EnvExports(cmd.Context(), devopt.EnvExportsOpts{})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// explicitly print to stdout instead of stderr so that direnv can read the output\n\t\tfmt.Fprint(cmd.OutOrStdout(), script)\n\t\treturn nil // return here to prevent opening a devbox shell\n\t}\n\n\tif envir.IsDevboxShellEnabled() {\n\t\treturn shellInceptionErrorMsg(\"devbox shell\")\n\t}\n\n\treturn box.Shell(ctx, devopt.EnvOptions{\n\t\tHooks: devopt.LifecycleHooks{\n\t\t\tOnStaleState: func() {\n\t\t\t\tif !flags.recomputeEnv {\n\t\t\t\t\tux.FHidableWarning(\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\tcmd.ErrOrStderr(),\n\t\t\t\t\t\tdevbox.StateOutOfDateMessage,\n\t\t\t\t\t\t\"with --recompute=true\",\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\tOmitNixEnv:    flags.omitNixEnv,\n\t\tPure:          flags.pure,\n\t\tSkipRecompute: !flags.recomputeEnv,\n\t})\n}\n\nfunc shellInceptionErrorMsg(cmdPath string) error {\n\treturn usererr.New(\"You are already in an active %[1]s.\\nRun `exit` before calling `%[1]s` again.\"+\n\t\t\" Shell inception is not supported.\", cmdPath)\n}\n"
  },
  {
    "path": "internal/boxcli/shellenv.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\ntype shellEnvCmdFlags struct {\n\tenvFlag\n\tconfig            configFlags\n\tomitNixEnv        bool\n\tinstall           bool\n\tnoRefreshAlias    bool\n\tpreservePathStack bool\n\tpure              bool\n\trecomputeEnv      bool\n\trunInitHook       bool\n\tformat            string\n}\n\n// shellenvFlagDefaults are the flag default values that differ\n// from the `devbox` command versus `devbox global` command.\ntype shellenvFlagDefaults struct {\n\tomitNixEnv   bool\n\trecomputeEnv bool\n}\n\nfunc shellEnvCmd(defaults shellenvFlagDefaults) *cobra.Command {\n\tflags := shellEnvCmdFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:     \"shellenv\",\n\t\tShort:   \"Print shell commands that create a Devbox Environment in the shell\",\n\t\tArgs:    cobra.ExactArgs(0),\n\t\tPreRunE: ensureNixInstalled,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\ts, err := shellEnvFunc(cmd, flags)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Fprintln(cmd.OutOrStdout(), s)\n\t\t\tif flags.format != \"nushell\" && !strings.HasSuffix(os.Getenv(\"SHELL\"), \"fish\") {\n\t\t\t\tfmt.Fprintln(cmd.OutOrStdout(), \"hash -r\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tcommand.Flags().BoolVar(\n\t\t&flags.runInitHook, \"init-hook\", false, \"runs init hook after exporting shell environment\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.install, \"install\", false, \"install packages before exporting shell environment\")\n\n\tcommand.Flags().BoolVar(\n\t\t&flags.pure, \"pure\", false, \"if this flag is specified, devbox creates an isolated environment inheriting almost no variables from the current environment. A few variables, in particular HOME, USER and DISPLAY, are retained.\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.preservePathStack, \"preserve-path-stack\", false,\n\t\t\"preserves existing PATH order if this project's environment is already in PATH. \"+\n\t\t\t\"Useful if you want to avoid overshadowing another devbox project that is already active\")\n\t_ = command.Flags().MarkHidden(\"preserve-path-stack\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.noRefreshAlias, \"no-refresh-alias\", false,\n\t\t\"by default, devbox will add refresh alias to the environment\"+\n\t\t\t\"Use this flag to disable this behavior.\")\n\t_ = command.Flags().MarkHidden(\"no-refresh-alias\")\n\tcommand.Flags().BoolVar(\n\t\t&flags.omitNixEnv, \"omit-nix-env\", defaults.omitNixEnv,\n\t\t\"shell environment will omit the env-vars from print-dev-env\",\n\t)\n\t_ = command.Flags().MarkHidden(\"omit-nix-env\")\n\n\tcommand.Flags().BoolVarP(\n\t\t&flags.recomputeEnv, \"recompute\", \"r\", defaults.recomputeEnv,\n\t\t\"Recompute environment if needed\",\n\t)\n\n\tcommand.Flags().StringVar(\n\t\t&flags.format, \"format\", \"bash\",\n\t\t\"Output format for shell environment (nushell)\",\n\t)\n\n\tflags.config.register(command)\n\tflags.envFlag.register(command)\n\n\treturn command\n}\n\nfunc shellEnvFunc(\n\tcmd *cobra.Command,\n\tflags shellEnvCmdFlags,\n) (string, error) {\n\tenv, err := flags.Env(flags.config.path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tctx := cmd.Context()\n\tif flags.recomputeEnv {\n\t\tctx = ux.HideMessage(ctx, devbox.StateOutOfDateMessage)\n\t}\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t\tEnv:         env,\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif flags.install {\n\t\tif err := box.Install(ctx); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\t// Convert format string to ShellFormat type\n\tvar shellFormat devopt.ShellFormat\n\tswitch flags.format {\n\tcase \"nushell\":\n\t\tshellFormat = devopt.ShellFormatNushell\n\tdefault:\n\t\tshellFormat = devopt.ShellFormatBash\n\t}\n\n\tenvStr, err := box.EnvExports(ctx, devopt.EnvExportsOpts{\n\t\tEnvOptions: devopt.EnvOptions{\n\t\t\tHooks: devopt.LifecycleHooks{\n\t\t\t\tOnStaleState: func() {\n\t\t\t\t\tif !flags.recomputeEnv {\n\t\t\t\t\t\tux.FHidableWarning(\n\t\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\tcmd.ErrOrStderr(),\n\t\t\t\t\t\t\tdevbox.StateOutOfDateMessage,\n\t\t\t\t\t\t\tbox.RefreshAliasOrCommand(),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t\tOmitNixEnv:        flags.omitNixEnv,\n\t\t\tPreservePathStack: flags.preservePathStack,\n\t\t\tPure:              flags.pure,\n\t\t\tSkipRecompute:     !flags.recomputeEnv,\n\t\t},\n\t\tNoRefreshAlias: flags.noRefreshAlias,\n\t\tRunHooks:       flags.runInitHook,\n\t\tShellFormat:    shellFormat,\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn envStr, nil\n}\n"
  },
  {
    "path": "internal/boxcli/update.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/multi\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n)\n\ntype updateCmdFlags struct {\n\tconfig      configFlags\n\tsync        bool\n\tallProjects bool\n\tnoInstall   bool\n}\n\nfunc updateCmd() *cobra.Command {\n\tflags := &updateCmdFlags{}\n\n\tcommand := &cobra.Command{\n\t\tUse:   \"update [pkg]...\",\n\t\tShort: \"Update packages in your devbox\",\n\t\tLong: \"Update one, many, or all packages in your devbox. \" +\n\t\t\t\"If no packages are specified, all packages will be updated. \" +\n\t\t\t\"Legacy non-versioned packages will be converted to @latest versioned \" +\n\t\t\t\"packages resolved to their current version.\",\n\t\tPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif flags.noInstall {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn ensureNixInstalled(cmd, args)\n\t\t},\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn updateCmdFunc(cmd, args, flags)\n\t\t},\n\t}\n\n\tflags.config.register(command)\n\tcommand.Flags().BoolVar(\n\t\t&flags.sync,\n\t\t\"sync-lock\",\n\t\tfalse,\n\t\t\"sync all devbox.lock dependencies in multiple projects. \"+\n\t\t\t\"Dependencies will sync to the latest local version.\",\n\t)\n\tcommand.Flags().BoolVar(\n\t\t&flags.allProjects,\n\t\t\"all-projects\",\n\t\tfalse,\n\t\t\"update all projects in the working directory, recursively.\",\n\t)\n\tcommand.Flags().BoolVar(\n\t\t&flags.noInstall,\n\t\t\"no-install\",\n\t\tfalse,\n\t\t\"update lockfile but don't install anything\",\n\t)\n\treturn command\n}\n\nfunc updateCmdFunc(cmd *cobra.Command, args []string, flags *updateCmdFlags) error {\n\tif len(args) > 0 && flags.sync {\n\t\treturn usererr.New(\"cannot specify both a package and --sync\")\n\t}\n\n\tif flags.allProjects {\n\t\treturn updateAllProjects(cmd, args)\n\t}\n\n\tif flags.sync {\n\t\treturn multi.SyncLockfiles(args)\n\t}\n\n\tbox, err := devbox.Open(&devopt.Opts{\n\t\tDir:         flags.config.path,\n\t\tEnvironment: flags.config.environment,\n\t\tStderr:      cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn box.Update(cmd.Context(), devopt.UpdateOpts{\n\t\tPkgs:      args,\n\t\tNoInstall: flags.noInstall,\n\t})\n}\n\nfunc updateAllProjects(cmd *cobra.Command, args []string) error {\n\tboxes, err := multi.Open(&devopt.Opts{\n\t\tStderr: cmd.ErrOrStderr(),\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tfor _, box := range boxes {\n\t\tif err := box.Update(cmd.Context(), devopt.UpdateOpts{\n\t\t\tPkgs:                  args,\n\t\t\tIgnoreMissingPackages: true,\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn multi.SyncLockfiles(args)\n}\n"
  },
  {
    "path": "internal/boxcli/usererr/exiterr.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage usererr\n\nimport (\n\t\"errors\"\n\t\"os/exec\"\n)\n\n// ExitError is an ExitError for a command run on behalf of a user\ntype ExitError struct {\n\t*exec.ExitError\n}\n\nfunc NewExecError(source error) error {\n\tif source == nil {\n\t\treturn nil\n\t}\n\n\t// BUG(gcurtis): exec.Cmd.Run can return other error types, such as when the\n\t// binary path isn't found. Those should still be considered a user exec error\n\t// and not reported to Sentry.\n\tvar exitErr *exec.ExitError\n\tif !errors.As(source, &exitErr) {\n\t\treturn source\n\t}\n\treturn &ExitError{ExitError: exitErr}\n}\n"
  },
  {
    "path": "internal/boxcli/usererr/usererr.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage usererr\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/pkg/errors\"\n)\n\ntype level int\n\nconst (\n\tlevelError level = iota\n\tlevelWarning\n)\n\ntype combined struct {\n\tsource      error\n\tuserMessage string\n\tlevel       level\n\tlogged      bool\n}\n\n// New creates new user error with the given message. By default these errors\n// are not logged to Sentry. If you want to log the error, use NewLogged\nfunc New(msg string, args ...any) error {\n\treturn errors.WithStack(&combined{\n\t\tuserMessage: fmt.Sprintf(msg, args...),\n\t})\n}\n\n// NewLogged creates new user error with the given message. These messages are\n// logged to Sentry without the message (for privacy reasons). This is useful\n// for unexpected errors that we want to make sure to log but we also want to\n// attach a good human readable message to.\nfunc NewLogged(msg string, args ...any) error {\n\treturn errors.WithStack(&combined{\n\t\tuserMessage: fmt.Sprintf(msg, args...),\n\t\tlevel:       levelError,\n\t\tlogged:      true,\n\t})\n}\n\nfunc NewWarning(msg string, args ...any) error {\n\treturn errors.WithStack(&combined{\n\t\tuserMessage: fmt.Sprintf(msg, args...),\n\t\tlevel:       levelWarning,\n\t})\n}\n\nfunc WithUserMessage(source error, msg string, args ...any) error {\n\t// We don't want to wrap the error if it already has a user message. Doing\n\t// so would obscure the original error message which is likely more useful.\n\tif source == nil || hasUserMessage(source) {\n\t\treturn source\n\t}\n\treturn &combined{\n\t\tsource:      source,\n\t\tuserMessage: fmt.Sprintf(msg, args...),\n\t}\n}\n\nfunc WithLoggedUserMessage(source error, msg string, args ...any) error {\n\tif source == nil || hasUserMessage(source) {\n\t\treturn source\n\t}\n\treturn &combined{\n\t\tlogged:      true,\n\t\tsource:      source,\n\t\tuserMessage: fmt.Sprintf(msg, args...),\n\t}\n}\n\n// Extract unwraps and returns the user error if it exists.\nfunc Extract(err error) (error, bool) { // nolint: revive\n\tc := &combined{}\n\tif errors.As(err, &c) {\n\t\treturn c, true\n\t}\n\treturn nil, false\n}\n\n// ShouldLogError returns true if the it's a combined error specifically marked to be logged\n// or if it's not an ExitError.\nfunc ShouldLogError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar userExecErr *ExitError\n\tif errors.As(err, &userExecErr) {\n\t\treturn false\n\t}\n\tc := &combined{}\n\tif errors.As(err, &c) {\n\t\treturn c.logged\n\t}\n\treturn true\n}\n\nfunc IsWarning(err error) bool {\n\tc := &combined{}\n\tif errors.As(err, &c) {\n\t\treturn c.level == levelWarning\n\t}\n\treturn false\n}\n\nfunc (c *combined) Error() string {\n\tif c.source == nil {\n\t\treturn c.userMessage\n\t}\n\treturn c.userMessage + \"\\nsource: \" + c.source.Error()\n}\n\n// Is uses the source error for comparisons\nfunc (c *combined) Is(target error) bool {\n\treturn errors.Is(c.source, target)\n}\n\n// Unwrap provides compatibility for Go 1.13 error chains.\nfunc (c *combined) Unwrap() error { return c.Cause() }\n\n// Leverage functionality of errors.Cause\nfunc (c *combined) Cause() error { return errors.Cause(c.source) }\n\n// Format allows us to use %+v as implemented by github.com/pkg/errors.\nfunc (c *combined) Format(s fmt.State, verb rune) {\n\tif c.source == nil {\n\t\t_, _ = io.WriteString(s, c.userMessage)\n\t\treturn\n\t}\n\terrors.Wrap(c.source, c.userMessage).(interface { //nolint:errorlint\n\t\tFormat(s fmt.State, verb rune)\n\t}).Format(s, verb)\n}\n\nfunc hasUserMessage(err error) bool {\n\t_, hasUserMessage := Extract(err)\n\treturn hasUserMessage\n}\n"
  },
  {
    "path": "internal/boxcli/version.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage boxcli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/vercheck\"\n)\n\ntype versionFlags struct {\n\tverbose bool\n}\n\nfunc versionCmd() *cobra.Command {\n\tflags := versionFlags{}\n\tcommand := &cobra.Command{\n\t\tUse:   \"version\",\n\t\tShort: \"Print version information\",\n\t\tArgs:  cobra.NoArgs,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn versionCmdFunc(cmd, args, flags)\n\t\t},\n\t}\n\n\tcommand.Flags().BoolVarP(&flags.verbose, \"verbose\", \"v\", false, // value\n\t\t\"displays additional version information\",\n\t)\n\n\tcommand.AddCommand(selfUpdateCmd())\n\treturn command\n}\n\nfunc selfUpdateCmd() *cobra.Command {\n\tcommand := &cobra.Command{\n\t\tUse:   \"update\",\n\t\tShort: \"Update devbox launcher and binary\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn vercheck.SelfUpdate(cmd.OutOrStdout(), cmd.ErrOrStderr())\n\t\t},\n\t}\n\n\treturn command\n}\n\nfunc versionCmdFunc(cmd *cobra.Command, _ []string, flags versionFlags) error {\n\tw := cmd.OutOrStdout()\n\tinfo := getVersionInfo()\n\tif flags.verbose {\n\t\tfmt.Fprintf(w, \"Version:     %v\\n\", info.Version)\n\t\tfmt.Fprintf(w, \"Platform:    %v\\n\", info.Platform)\n\t\tfmt.Fprintf(w, \"Commit:      %v\\n\", info.Commit)\n\t\tfmt.Fprintf(w, \"Commit Time: %v\\n\", info.CommitDate)\n\t\tfmt.Fprintf(w, \"Go Version:  %v\\n\", info.GoVersion)\n\t\tfmt.Fprintf(w, \"Launcher:    %v\\n\", info.LauncherVersion)\n\n\t} else {\n\t\tfmt.Fprintf(w, \"%v\\n\", info.Version)\n\t}\n\treturn nil\n}\n\ntype versionInfo struct {\n\tVersion         string\n\tIsPrerelease    bool\n\tPlatform        string\n\tCommit          string\n\tCommitDate      string\n\tGoVersion       string\n\tLauncherVersion string\n}\n\nfunc getVersionInfo() *versionInfo {\n\tv := &versionInfo{\n\t\tVersion:         build.Version,\n\t\tPlatform:        fmt.Sprintf(\"%s_%s\", runtime.GOOS, runtime.GOARCH),\n\t\tCommit:          build.Commit,\n\t\tCommitDate:      build.CommitDate,\n\t\tGoVersion:       runtime.Version(),\n\t\tLauncherVersion: os.Getenv(envir.LauncherVersion),\n\t}\n\n\treturn v\n}\n"
  },
  {
    "path": "internal/build/build.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage build\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"go.jetify.com/devbox/internal/fileutil\"\n)\n\nvar forceProd, _ = strconv.ParseBool(os.Getenv(\"DEVBOX_PROD\"))\n\n// Variables in this file are set via ldflags.\nvar (\n\tIsDev      = Version == \"0.0.0-dev\" && !forceProd\n\tVersion    = \"0.0.0-dev\"\n\tCommit     = \"none\"\n\tCommitDate = \"unknown\"\n\n\t// SentryDSN is injected in the build from\n\t// https://jetpack-io.sentry.io/settings/projects/devbox/keys/\n\t// It is disabled by default.\n\tSentryDSN = \"\"\n\t// TelemetryKey is the Segment Write Key\n\t// https://segment.com/docs/connections/sources/catalog/libraries/server/go/quickstart/\n\t// It is disabled by default.\n\tTelemetryKey = \"\"\n)\n\n// User-presentable names of operating systems supported by Devbox.\nconst (\n\tOSLinux  = \"Linux\"\n\tOSDarwin = \"macOS\"\n\tOSWSL    = \"WSL\"\n)\n\nvar (\n\tosName string\n\tosOnce sync.Once\n)\n\nfunc OS() string {\n\tosOnce.Do(func() {\n\t\tswitch runtime.GOOS {\n\t\tcase \"linux\":\n\t\t\tif fileutil.Exists(\"/proc/sys/fs/binfmt_misc/WSLInterop\") || fileutil.Exists(\"/run/WSL\") {\n\t\t\t\tosName = OSWSL\n\t\t\t}\n\t\t\tosName = OSLinux\n\t\tcase \"darwin\":\n\t\t\tosName = OSDarwin\n\t\tdefault:\n\t\t\tosName = runtime.GOOS\n\t\t}\n\t})\n\treturn osName\n}\n\nfunc Issuer() string {\n\tif IsDev {\n\t\treturn \"https://laughing-agnesi-vzh2rap9f6.projects.oryapis.com\"\n\t}\n\treturn \"https://accounts.jetify.com\"\n}\n\nfunc ClientID() string {\n\tif IsDev {\n\t\treturn \"3945b320-bd31-4313-af27-846b67921acb\"\n\t}\n\treturn \"ff3d4c9c-1ac8-42d9-bef1-f5218bb1a9f6\"\n}\n\nfunc JetpackAPIHost() string {\n\tif IsDev {\n\t\treturn \"https://api.jetpack.dev\"\n\t}\n\treturn \"https://api.jetpack.io\"\n}\n\nfunc SuccessRedirect() string {\n\tif IsDev {\n\t\treturn \"https://auth.dev-jetify.com/account/login/success\"\n\t}\n\treturn \"https://auth.jetify.com/account/login/success\"\n}\n\nfunc Audience() []string {\n\treturn []string{\"https://api.jetpack.io\"}\n}\n\nfunc DashboardHostname() string {\n\tif IsDev {\n\t\treturn \"http://localhost:8080\"\n\t}\n\treturn \"https://cloud.jetify.com\"\n}\n\n// SourceDir searches for the source code directory that built the current\n// binary.\nfunc SourceDir() (string, error) {\n\t_, file, _, ok := runtime.Caller(0)\n\tif !ok || file == \"\" {\n\t\treturn \"\", fmt.Errorf(\"build.SourceDir: binary is missing path info\")\n\t}\n\tslog.Debug(\"trying to determine path to devbox source using runtime.Caller\", \"path\", file)\n\n\tdir := filepath.Dir(file)\n\tif _, err := os.Stat(dir); err != nil {\n\t\tif filepath.IsAbs(file) {\n\t\t\treturn \"\", fmt.Errorf(\"build.SourceDir: path to binary source doesn't exist: %v\", err)\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"build.SourceDir: binary was built with -trimpath\")\n\t}\n\n\tfor {\n\t\t_, err := os.Stat(filepath.Join(dir, \"go.mod\"))\n\t\tif err == nil {\n\t\t\tslog.Debug(\"found devbox source directory\", \"path\", dir)\n\t\t\treturn dir, nil\n\t\t}\n\t\tif dir == \"/\" || dir == \".\" {\n\t\t\treturn \"\", fmt.Errorf(\"build.SourceDir: can't find go.mod in any parent directories of %s\", file)\n\t\t}\n\t\tdir = filepath.Dir(dir)\n\t}\n}\n"
  },
  {
    "path": "internal/cachehash/hash.go",
    "content": "// Package cachehash generates non-cryptographic cache keys.\n//\n// The functions in this package make no guarantees about the underlying hashing\n// algorithm. It should only be used for caching, where it's ok if the hash for\n// a given input changes.\npackage cachehash\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"hash\"\n\t\"io\"\n\t\"os\"\n\n\t\"go.jetify.com/devbox/internal/redact\"\n)\n\n// Bytes returns a hex-encoded hash of b.\nfunc Bytes(b []byte) string {\n\th := newHash()\n\th.Write(b)\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\n// Bytes6 returns the first 6 characters of the hash of b.\nfunc Bytes6(b []byte) string {\n\thash := Bytes(b)\n\treturn hash[:min(len(hash), 6)]\n}\n\n// File returns a hex-encoded hash of a file's contents.\nfunc File(path string) (string, error) {\n\tf, err := os.Open(path)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn \"\", nil\n\t}\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer f.Close()\n\n\th := newHash()\n\tif _, err := io.Copy(h, f); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn hex.EncodeToString(h.Sum(nil)), nil\n}\n\n// JSON marshals a to JSON and returns its hex-encoded hash.\nfunc JSON(a any) (string, error) {\n\tb, err := json.Marshal(a)\n\tif err != nil {\n\t\treturn \"\", redact.Errorf(\"marshal to json for hashing: %v\", err)\n\t}\n\treturn Bytes(b), nil\n}\n\n// JSONFile compacts the JSON in a file and returns its hex-encoded hash.\nfunc JSONFile(path string) (string, error) {\n\tb, err := os.ReadFile(path)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn \"\", nil\n\t}\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tbuf := &bytes.Buffer{}\n\tif err := json.Compact(buf, b); err != nil {\n\t\treturn \"\", redact.Errorf(\"compact json for hashing: %v\", err)\n\t}\n\treturn Bytes(buf.Bytes()), nil\n}\n\nfunc newHash() hash.Hash { return sha256.New() }\n"
  },
  {
    "path": "internal/cachehash/hash_test.go",
    "content": "//nolint:varnamelen\npackage cachehash\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestFile(t *testing.T) {\n\tdir := t.TempDir()\n\n\tab := filepath.Join(dir, \"ab.json\")\n\terr := os.WriteFile(ab, []byte(`{\"a\":\"\\n\",\"b\":\"\\u000A\"}`), 0o644)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tba := filepath.Join(dir, \"ba.json\")\n\terr = os.WriteFile(ba, []byte(`{\"b\":\"\\n\",\"a\":\"\\u000A\"}`), 0o644)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tabHash, err := File(ab)\n\tif err != nil {\n\t\tt.Errorf(\"got File(ab) error: %v\", err)\n\t}\n\tbaHash, err := File(ba)\n\tif err != nil {\n\t\tt.Errorf(\"got File(ba) error: %v\", err)\n\t}\n\tif abHash == baHash {\n\t\tt.Errorf(\"got (File(%q) = %q) == (File(%q) = %q), want different hashes\", ab, abHash, ba, baHash)\n\t}\n}\n\nfunc TestFileNotExist(t *testing.T) {\n\tt.TempDir()\n\thash, err := File(t.TempDir() + \"/notafile\")\n\tif err != nil {\n\t\tt.Errorf(\"got error: %v\", err)\n\t}\n\tif hash != \"\" {\n\t\tt.Errorf(\"got non-empty hash %q\", hash)\n\t}\n}\n\nfunc TestJSON(t *testing.T) {\n\ta := struct{ A, B string }{\"a\", \"b\"}\n\taHash, err := JSON(a)\n\tif err != nil {\n\t\tt.Errorf(\"got JSON(%#q) error: %v\", a, err)\n\t}\n\tif aHash == \"\" {\n\t\tt.Errorf(`got JSON(%#q) == \"\"`, a)\n\t}\n\n\tb := map[string]string{\"A\": \"a\", \"B\": \"b\"}\n\tbHash, err := JSON(b)\n\tif err != nil {\n\t\tt.Errorf(\"got JSON(%#q) error: %v\", b, err)\n\t}\n\tif bHash == \"\" {\n\t\tt.Errorf(`got JSON(%#q) == \"\"`, b)\n\t}\n\tif aHash != bHash {\n\t\tt.Errorf(\"got (JSON(%#q) = %q) != (JSON(%#q) = %q), want equal hashes\", a, aHash, b, bHash)\n\t}\n}\n\nfunc TestJSONUnsupportedType(t *testing.T) {\n\tj := struct{ C chan int }{}\n\t_, err := JSON(j)\n\tif err == nil {\n\t\tt.Error(\"got nil error for struct with channel field\")\n\t}\n}\n\nfunc TestJSONFile(t *testing.T) {\n\tdir := t.TempDir()\n\n\tcompact := filepath.Join(dir, \"compact.json\")\n\terr := os.WriteFile(compact, []byte(`{\"key\":\"value\"}`), 0o644)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tspace := filepath.Join(dir, \"space.json\")\n\terr = os.WriteFile(space, []byte(`{ \"key\": \"value\" }`), 0o644)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcompactHash, err := JSONFile(compact)\n\tif err != nil {\n\t\tt.Errorf(\"got JSONFile(ab) error: %v\", err)\n\t}\n\tspaceHash, err := JSONFile(space)\n\tif err != nil {\n\t\tt.Errorf(\"got JSONFile(ba) error: %v\", err)\n\t}\n\tif compactHash != spaceHash {\n\t\tt.Errorf(\"got (JSONFile(%q) = %q) != (JSONFile(%q) = %q), want equal hashes\", compact, compactHash, space, spaceHash)\n\t}\n}\n\nfunc TestJSONFileNotExist(t *testing.T) {\n\tt.TempDir()\n\thash, err := JSONFile(t.TempDir() + \"/notafile\")\n\tif err != nil {\n\t\tt.Errorf(\"got error: %v\", err)\n\t}\n\tif hash != \"\" {\n\t\tt.Errorf(\"got non-empty hash %q\", hash)\n\t}\n}\n"
  },
  {
    "path": "internal/cmdutil/cmdutil.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage cmdutil\n\nimport (\n\t\"os/exec\"\n)\n\n// Exists indicates if the command exists\nfunc Exists(command string) bool {\n\t_, err := exec.LookPath(command)\n\treturn err == nil\n}\n\n// GetPathOrDefault gets the path for the given command.\n// If it's not found, it will return the given value instead.\nfunc GetPathOrDefault(command, def string) string {\n\tpath, err := exec.LookPath(command)\n\tif err != nil {\n\t\tpath = def\n\t}\n\n\treturn path\n}\n"
  },
  {
    "path": "internal/cmdutil/exec.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage cmdutil\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n)\n\nfunc CommandTTY(name string, arg ...string) *exec.Cmd {\n\tcmd := exec.Command(name, arg...)\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\treturn cmd\n}\n\n// CommandTTYWithBuffer returns a command with stdin, stdout, and stderr\n// and a buffer that contains stdout and stderr combined.\nfunc CommandTTYWithBuffer(\n\tname string,\n\targ ...string,\n) (*exec.Cmd, *bytes.Buffer) {\n\tcmd := exec.Command(name, arg...)\n\tcmd.Stdin = os.Stdin\n\n\terrBuf := bytes.NewBuffer(nil)\n\toutBuf := bytes.NewBuffer(nil)\n\tcmd.Stderr = io.MultiWriter(os.Stderr, errBuf)\n\tcmd.Stdout = io.MultiWriter(os.Stdout, outBuf)\n\toutBuf.Write(errBuf.Bytes())\n\treturn cmd, outBuf\n}\n"
  },
  {
    "path": "internal/conf/doc.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\n// Package conf is future home of the config (devbox.json) management code.\n// it will merge exiting plugin and devbox/config.go code.\npackage conf\n"
  },
  {
    "path": "internal/conf/env.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage conf\n\nimport (\n\t\"os\"\n)\n\nfunc OSExpandEnvMap(env, existingEnv map[string]string, projectDir string) map[string]string {\n\tmapperfunc := func(value string) string {\n\t\t// Special variables that should return correct value\n\t\tswitch value {\n\t\tcase \"PWD\":\n\t\t\treturn projectDir\n\t\t}\n\n\t\t// in case existingEnv is nil\n\t\tif existingEnv == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn existingEnv[value]\n\t}\n\n\tres := map[string]string{}\n\tfor k, v := range env {\n\t\tres[k] = os.Expand(v, mapperfunc)\n\t}\n\treturn res\n}\n"
  },
  {
    "path": "internal/cuecfg/cuecfg.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage cuecfg\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// TODO: add support for .cue\n\nfunc Marshal(valuePtr any, extension string) ([]byte, error) {\n\tswitch extension {\n\tcase \".json\", \".lock\":\n\t\treturn MarshalJSON(valuePtr)\n\tcase \".yml\", \".yaml\":\n\t\treturn marshalYaml(valuePtr)\n\tcase \".toml\":\n\t\treturn marshalToml(valuePtr)\n\tcase \".xml\":\n\t\treturn marshalXML(valuePtr)\n\t}\n\treturn nil, errors.Errorf(\"Unsupported file format '%s' for config file\", extension)\n}\n\nfunc Unmarshal(data []byte, extension string, valuePtr any) error {\n\tswitch extension {\n\tcase \".json\", \".lock\":\n\t\treturn errors.WithStack(unmarshalJSON(data, valuePtr))\n\tcase \".yml\", \".yaml\":\n\t\treturn errors.WithStack(unmarshalYaml(data, valuePtr))\n\tcase \".toml\":\n\t\treturn errors.WithStack(unmarshalToml(data, valuePtr))\n\tcase \".xml\":\n\t\treturn errors.WithStack(unmarshalXML(data, valuePtr))\n\t}\n\treturn errors.Errorf(\"Unsupported file format '%s' for config file\", extension)\n}\n\nfunc InitFile(path string, valuePtr any) (bool, error) {\n\t_, err := os.Stat(path)\n\tif err == nil {\n\t\t// File already exists, don't create a new one.\n\t\t// TODO: should we read and write again, in case the schema needs updating?\n\t\treturn false, nil\n\t}\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\t// File does not exist, create a new one:\n\t\treturn true, WriteFile(path, valuePtr)\n\t}\n\t// Error case:\n\treturn false, errors.WithStack(err)\n}\n\nfunc ParseFile(path string, valuePtr any) error {\n\treturn ParseFileWithExtension(path, filepath.Ext(path), valuePtr)\n}\n\n// ParseFileWithExtension lets the caller override the extension of the `path` filename\n// For example, project.csproj files should be treated as having extension .xml\nfunc ParseFileWithExtension(path, ext string, valuePtr any) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn Unmarshal(data, ext, valuePtr)\n}\n\nfunc WriteFile(path string, value any) error {\n\tdata, err := Marshal(value, filepath.Ext(path))\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tdata = append(data, '\\n')\n\treturn errors.WithStack(os.WriteFile(path, data, 0o644))\n}\n\nfunc IsSupportedExtension(ext string) bool {\n\tswitch ext {\n\tcase \".json\", \".lock\", \".yml\", \".yaml\", \".toml\", \".xml\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "internal/cuecfg/doc.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\n// Utilities for working with config files that rely on cue\n// for validation and definition.\npackage cuecfg\n"
  },
  {
    "path": "internal/cuecfg/json.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage cuecfg\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\n\t\"github.com/pkg/errors\"\n)\n\nconst Indent = \"  \"\n\n// MarshalJSON marshals the given value to JSON. It does not HTML escape and\n// adds standard indentation.\n//\n// TODO: consider using cue's JSON marshaller instead of\n// \"encoding/json\" ... it might have extra functionality related\n// to the cue language.\nfunc MarshalJSON(v interface{}) ([]byte, error) {\n\tbuff := &bytes.Buffer{}\n\te := json.NewEncoder(buff)\n\te.SetIndent(\"\", Indent)\n\te.SetEscapeHTML(false)\n\tif err := e.Encode(v); err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn bytes.TrimRight(buff.Bytes(), \"\\n\"), nil\n}\n\nfunc unmarshalJSON(data []byte, v interface{}) error {\n\treturn json.Unmarshal(data, v)\n}\n"
  },
  {
    "path": "internal/cuecfg/toml.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage cuecfg\n\nimport (\n\t\"github.com/pelletier/go-toml/v2\"\n)\n\nfunc marshalToml(v interface{}) ([]byte, error) {\n\treturn toml.Marshal(v)\n}\n\nfunc unmarshalToml(data []byte, v interface{}) error {\n\treturn toml.Unmarshal(data, v)\n}\n"
  },
  {
    "path": "internal/cuecfg/xml.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage cuecfg\n\nimport (\n\t\"encoding/xml\"\n)\n\nfunc marshalXML(v interface{}) ([]byte, error) {\n\treturn xml.Marshal(v)\n}\n\nfunc unmarshalXML(data []byte, v interface{}) error {\n\treturn xml.Unmarshal(data, v)\n}\n"
  },
  {
    "path": "internal/cuecfg/yaml.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage cuecfg\n\nimport \"gopkg.in/yaml.v3\"\n\n// TODO: consider using cue's YAML marshaller.\n// It might have extra functionality related\n// to the cue language.\nfunc marshalYaml(v interface{}) ([]byte, error) {\n\treturn yaml.Marshal(v)\n}\n\nfunc unmarshalYaml(data []byte, v interface{}) error {\n\treturn yaml.Unmarshal(data, v)\n}\n"
  },
  {
    "path": "internal/debug/debug.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage debug\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"runtime\"\n\t\"strconv\"\n\n\t\"github.com/getsentry/sentry-go\"\n\t\"github.com/pkg/errors\"\n)\n\nconst DevboxDebug = \"DEVBOX_DEBUG\"\n\nvar (\n\tlevel = slog.LevelVar{}\n\topts  = slog.HandlerOptions{AddSource: true, Level: &level}\n)\n\nfunc init() {\n\tenabled, _ := strconv.ParseBool(os.Getenv(DevboxDebug))\n\tif enabled {\n\t\tlevel.Set(slog.LevelDebug)\n\t} else {\n\t\t// Pick arbitrarily high level to disable all default log levels\n\t\t// unless DEVBOX_DEBUG is set.\n\t\tlevel.Set(slog.Level(100))\n\t}\n\tslog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &opts)))\n}\n\nfunc Enable()               { level.Set(slog.LevelDebug) }\nfunc IsEnabled() bool       { return slog.Default().Enabled(context.Background(), slog.LevelDebug) }\nfunc SetOutput(w io.Writer) { slog.SetDefault(slog.New(slog.NewTextHandler(w, &opts))) }\n\nfunc Recover() {\n\tr := recover()\n\tif r == nil {\n\t\treturn\n\t}\n\n\tsentry.CurrentHub().Recover(r)\n\tif IsEnabled() {\n\t\tfmt.Fprintln(os.Stderr, \"Allowing panic because debug mode is enabled.\")\n\t\tpanic(r)\n\t}\n\tfmt.Fprintln(os.Stderr, \"Error:\", r)\n}\n\nfunc EarliestStackTrace(err error) error {\n\ttype pkgErrorsStackTracer interface{ StackTrace() errors.StackTrace }\n\ttype redactStackTracer interface{ StackTrace() []runtime.Frame }\n\n\tvar stErr error\n\tfor err != nil {\n\t\t//nolint:errorlint\n\t\tswitch err.(type) {\n\t\tcase redactStackTracer, pkgErrorsStackTracer:\n\t\t\tstErr = err\n\t\t}\n\t\terr = errors.Unwrap(err)\n\t}\n\treturn stErr\n}\n"
  },
  {
    "path": "internal/debug/time.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage debug\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\nvar timerEnabled, _ = strconv.ParseBool(os.Getenv(devboxPrintExecTime))\n\nconst devboxPrintExecTime = \"DEVBOX_PRINT_EXEC_TIME\"\n\nvar headerPrinted = false\n\ntype timer struct {\n\tname string\n\ttime time.Time\n}\n\nfunc Timer(name string) *timer {\n\tif !timerEnabled {\n\t\treturn nil\n\t}\n\treturn &timer{\n\t\tname: name,\n\t\ttime: time.Now(),\n\t}\n}\n\nfunc FunctionTimer() *timer {\n\tif !timerEnabled {\n\t\treturn nil\n\t}\n\tpc := make([]uintptr, 15)\n\tn := runtime.Callers(2, pc)\n\tframes := runtime.CallersFrames(pc[:n])\n\tframe, _ := frames.Next()\n\tparts := strings.Split(frame.Function, \".\")\n\treturn Timer(parts[len(parts)-1])\n}\n\nfunc (t *timer) End() {\n\tif t == nil {\n\t\treturn\n\t}\n\tif !headerPrinted {\n\t\tfmt.Fprintln(os.Stderr, \"\\nExec times over 1ms:\")\n\t\theaderPrinted = true\n\t}\n\tif time.Since(t.time) >= time.Millisecond {\n\t\tfmt.Fprintf(os.Stderr, \"\\\"%s\\\" took %s\\n\", t.name, time.Since(t.time))\n\t}\n}\n"
  },
  {
    "path": "internal/devbox/cache.go",
    "content": "package devbox\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/devbox/providers/identity\"\n\t\"go.jetify.com/devbox/internal/devbox/providers/nixcache\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/ux\"\n\t\"go.jetify.com/pkg/auth\"\n)\n\nfunc (d *Devbox) UploadProjectToCache(\n\tctx context.Context,\n\tcacheURI string,\n) error {\n\tdefer debug.FunctionTimer().End()\n\tif cacheURI == \"\" {\n\t\tvar err error\n\t\tcacheURI, err = getWriteCacheURI(ctx, d.stderr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcreds, err := nixcache.CachedCredentials(ctx)\n\tif err != nil && !errors.Is(err, auth.ErrNotLoggedIn) {\n\t\treturn err\n\t}\n\n\tpackages := lo.Filter(d.InstallablePackages(), devpkg.IsNix)\n\tif err != nil || len(packages) == 0 {\n\t\treturn err\n\t}\n\n\tfor _, pkg := range packages {\n\t\tinCache, err := pkg.AreAllOutputsInCache(ctx, d.stderr, cacheURI)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif inCache {\n\t\t\tux.Finfof(d.stderr, \"Package %s is already in cache, skipping\\n\", pkg.Raw)\n\t\t\tcontinue\n\t\t}\n\t\tux.Finfof(d.stderr, \"Uploading package %s to cache\\n\", pkg.Raw)\n\t\tinstallables, err := pkg.Installables()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, installable := range installables {\n\t\t\terr := nix.CopyInstallableToCache(ctx, d.stderr, cacheURI, installable, creds.Env())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc UploadInstallableToCache(\n\tctx context.Context,\n\tstderr io.Writer,\n\tcacheURI, installable string,\n) error {\n\tif cacheURI == \"\" {\n\t\tvar err error\n\t\tcacheURI, err = getWriteCacheURI(ctx, stderr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcreds, err := nixcache.CachedCredentials(ctx)\n\tif err != nil && !errors.Is(err, auth.ErrNotLoggedIn) {\n\t\treturn err\n\t}\n\treturn nix.CopyInstallableToCache(ctx, stderr, cacheURI, installable, creds.Env())\n}\n\nfunc getWriteCacheURI(\n\tctx context.Context,\n\tw io.Writer,\n) (string, error) {\n\t_, err := identity.GenSession(ctx)\n\tif errors.Is(err, auth.ErrNotLoggedIn) {\n\t\treturn \"\",\n\t\t\tusererr.New(\"You must be logged in to upload to a Nix cache.\")\n\t}\n\tcaches, err := nixcache.WriteCaches(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(caches) == 0 {\n\t\tslug, err := identity.GetOrgSlug(ctx)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn \"\",\n\t\t\tusererr.New(\n\t\t\t\t\"You don't have permission to write to any Nix caches. To configure cache, go to \"+\n\t\t\t\t\t\"%s/teams/%s/devbox\",\n\t\t\t\tbuild.DashboardHostname(),\n\t\t\t\tslug,\n\t\t\t)\n\t}\n\tif len(caches) > 1 {\n\t\tux.Fwarningf(w, \"Multiple caches available, using %s.\\n\", caches[0].GetUri())\n\t}\n\treturn caches[0].GetUri(), nil\n}\n"
  },
  {
    "path": "internal/devbox/devbox.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\n// Package devbox creates isolated development environments.\npackage devbox\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime/trace\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/briandowns/spinner\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/cachehash\"\n\t\"go.jetify.com/devbox/internal/cmdutil\"\n\t\"go.jetify.com/devbox/internal/conf\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devbox/envpath\"\n\t\"go.jetify.com/devbox/internal/devbox/generate\"\n\t\"go.jetify.com/devbox/internal/devconfig\"\n\t\"go.jetify.com/devbox/internal/devconfig/configfile\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/devpkg/pkgtype\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/fileutil\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/plugin\"\n\t\"go.jetify.com/devbox/internal/redact\"\n\t\"go.jetify.com/devbox/internal/searcher\"\n\t\"go.jetify.com/devbox/internal/services\"\n\t\"go.jetify.com/devbox/internal/shellgen\"\n\t\"go.jetify.com/devbox/internal/telemetry\"\n\t\"go.jetify.com/devbox/internal/ux\"\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\nconst (\n\n\t// shellHistoryFile keeps the history of commands invoked inside devbox shell\n\tshellHistoryFile     = \".devbox/shell_history\"\n\tarbitraryCmdFilename = \".cmd\"\n)\n\ntype Devbox struct {\n\tcfg                      *devconfig.Config\n\tenv                      map[string]string\n\tenvironment              string\n\tlockfile                 *lock.File\n\tnix                      nix.Nixer\n\tprojectDir               string\n\tpluginManager            *plugin.Manager\n\tcustomProcessComposeFile string\n\n\t// This is needed because of the --quiet flag.\n\tstderr io.Writer\n}\n\nvar legacyPackagesWarningHasBeenShown = false\n\nfunc InitConfig(dir string) error {\n\t_, err := devconfig.Init(dir)\n\treturn err\n}\n\nfunc EnsureConfig(dir string) error {\n\terr := InitConfig(dir)\n\tif err != nil && !errors.Is(err, os.ErrExist) {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc Open(opts *devopt.Opts) (*Devbox, error) {\n\tvar cfg *devconfig.Config\n\tvar err error\n\tif opts.Dir == \"\" {\n\t\tcfg, err = devconfig.Find(\".\")\n\t\tif errors.Is(err, devconfig.ErrNotFound) {\n\t\t\treturn nil, usererr.New(\"no devbox.json found in the current directory (or any parent directories). Did you run `devbox init` yet?\")\n\t\t}\n\t} else {\n\t\tcfg, err = devconfig.Open(opts.Dir)\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, usererr.New(\"the devbox config path %q does not exist.\", opts.Dir)\n\t\t}\n\t\tif errors.Is(err, devconfig.ErrNotFound) {\n\t\t\treturn nil, usererr.New(\"no devbox.json found in %q. Did you run `devbox init` yet?\", opts.Dir)\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn nil, usererr.WithUserMessage(err, \"Error loading devbox.json.\")\n\t}\n\n\tenvironment, err := validateEnvironment(opts.Environment)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbox := &Devbox{\n\t\tcfg:                      cfg,\n\t\tenv:                      opts.Env,\n\t\tenvironment:              environment,\n\t\tnix:                      &nix.NixInstance{},\n\t\tprojectDir:               filepath.Dir(cfg.Root.AbsRootPath),\n\t\tpluginManager:            plugin.NewManager(),\n\t\tstderr:                   opts.Stderr,\n\t\tcustomProcessComposeFile: opts.CustomProcessComposeFile,\n\t}\n\n\tlock, err := lock.GetFile(box)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := cfg.LoadRecursive(lock); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// if lockfile has any allow insecure, we need to set the env var to ensure\n\t// all nix commands work.\n\tif err := box.moveAllowInsecureFromLockfile(box.stderr, lock, cfg); err != nil {\n\t\tux.Fwarningf(\n\t\t\tbox.stderr,\n\t\t\t\"Failed to move allow_insecure from devbox.lock to devbox.json. An insecure package may \"+\n\t\t\t\t\"not work until you invoke `devbox add <pkg> --allow-insecure=<packages>` again: %s\\n\",\n\t\t\terr,\n\t\t)\n\t\t// continue on, since we do not want to block user.\n\t}\n\n\tbox.pluginManager.ApplyOptions(\n\t\tplugin.WithDevbox(box),\n\t\tplugin.WithLockfile(lock),\n\t)\n\tbox.lockfile = lock\n\n\tif !opts.IgnoreWarnings &&\n\t\t!legacyPackagesWarningHasBeenShown &&\n\t\t// HasDeprecatedPackages required nix to be installed. Since not all\n\t\t// commands require nix to be installed, only show this warning for commands\n\t\t// that ensure nix.\n\t\t// This warning can probably be removed soon.\n\t\tnix.Ensured() &&\n\t\tbox.HasDeprecatedPackages() {\n\t\tlegacyPackagesWarningHasBeenShown = true\n\t\tglobalPath, err := GlobalDataPath()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tux.Fwarningf(\n\t\t\tos.Stderr, // Always stderr. box.writer should probably always be err.\n\t\t\t\"Your devbox.json at %s contains packages in legacy format. \"+\n\t\t\t\t\"Please run `devbox %supdate` to update your devbox.json.\\n\",\n\t\t\tbox.projectDir,\n\t\t\tlo.Ternary(box.projectDir == globalPath, \"global \", \"\"),\n\t\t)\n\t}\n\n\treturn box, nil\n}\n\nfunc (d *Devbox) ProjectDir() string {\n\treturn d.projectDir\n}\n\nfunc (d *Devbox) Config() *devconfig.Config {\n\treturn d.cfg\n}\n\nfunc (d *Devbox) ConfigHash() (string, error) {\n\th, err := d.cfg.Hash()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbuf := bytes.Buffer{}\n\tbuf.WriteString(h)\n\tfor _, pkg := range d.AllPackages() {\n\t\tbuf.WriteString(pkg.Hash())\n\t}\n\tfor _, pluginConfig := range d.cfg.IncludedPluginConfigs() {\n\t\th, err := pluginConfig.Hash()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tbuf.WriteString(h)\n\t}\n\treturn cachehash.Bytes(buf.Bytes()), nil\n}\n\nfunc (d *Devbox) Stdenv() flake.Ref {\n\treturn flake.Ref{\n\t\tType:  flake.TypeGitHub,\n\t\tOwner: \"NixOS\",\n\t\tRepo:  \"nixpkgs\",\n\t\tRef:   \"nixpkgs-unstable\",\n\t\tRev:   d.cfg.NixPkgsCommitHash(),\n\t}\n}\n\nfunc (d *Devbox) Generate(ctx context.Context) error {\n\tctx, task := trace.NewTask(ctx, \"devboxGenerate\")\n\tdefer task.End()\n\n\treturn errors.WithStack(shellgen.GenerateForPrintEnv(ctx, d))\n}\n\nfunc (d *Devbox) Shell(ctx context.Context, envOpts devopt.EnvOptions) error {\n\tctx, task := trace.NewTask(ctx, \"devboxShell\")\n\tdefer task.End()\n\n\tenvs, err := d.ensureStateIsUpToDateAndComputeEnv(ctx, envOpts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Fprintln(d.stderr, \"Starting a devbox shell...\")\n\n\t// Used to determine whether we're inside a shell (e.g. to prevent shell inception)\n\t// TODO: This is likely obsolete but we need to decide what happens when\n\t// the user does shell-ception. One option is to leave the current shell and\n\t// join a new one (that way they are not in nested shells.)\n\tenvs[envir.DevboxShellEnabled] = \"1\"\n\n\tif err = createDevboxSymlink(d); err != nil {\n\t\treturn err\n\t}\n\n\topts := []ShellOption{\n\t\tWithHistoryFile(filepath.Join(d.projectDir, shellHistoryFile)),\n\t\tWithProjectDir(d.projectDir),\n\t\tWithEnvVariables(envs),\n\t\tWithShellStartTime(telemetry.ShellStart()),\n\t}\n\n\tshell, err := d.newShell(envOpts, opts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn shell.Run()\n}\n\nfunc (d *Devbox) RunScript(ctx context.Context, envOpts devopt.EnvOptions, cmdName string, cmdArgs []string) error {\n\tctx, task := trace.NewTask(ctx, \"devboxRun\")\n\tdefer task.End()\n\n\tif err := shellgen.WriteScriptsToFiles(d); err != nil {\n\t\treturn err\n\t}\n\n\tlock.SetIgnoreShellMismatch(true)\n\n\tvar env map[string]string\n\tif d.IsEnvEnabled() {\n\t\t// Skip ensureStateIsUpToDate if we are already in a shell of this devbox-project\n\t\tenv = envir.PairsToMap(os.Environ())\n\n\t\t// We set this to ensure that init-hooks do NOT re-run. They would have\n\t\t// run when initializing the Devbox Environment in the current shell.\n\t\tenv[d.SkipInitHookEnvName()] = \"true\"\n\t} else {\n\t\tvar err error\n\t\tenv, err = d.ensureStateIsUpToDateAndComputeEnv(ctx, envOpts)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Used to determine whether we're inside a shell (e.g. to prevent shell inception)\n\t// This is temporary because StartServices() needs it but should be replaced with\n\t// better alternative since devbox run and devbox shell are not the same.\n\tenv[\"DEVBOX_SHELL_ENABLED\"] = \"1\"\n\n\t// wrap the arg in double-quotes, and escape any double-quotes inside it.\n\t//\n\t// TODO(gcurtis): this breaks quote-removal in parameter expansion,\n\t// command substitution, and arithmetic expansion:\n\t//\n\t//\t$ unset x\n\t//\t$ echo ${x:-\"my file\"}\n\t//\tmy file\n\t//\t$ devbox run -- echo '${x:-\"my file\"}'\n\t//\t\"my file\"\n\tfor idx, arg := range cmdArgs {\n\t\tcmdArgs[idx] = strconv.Quote(arg)\n\t}\n\n\tvar cmdWithArgs []string\n\tif _, ok := d.cfg.Scripts()[cmdName]; ok {\n\t\t// it's a script, so replace the command with the script file's path.\n\t\tscript := shellgen.ScriptPath(d.ProjectDir(), cmdName)\n\t\tcmdWithArgs = append([]string{strconv.Quote(script)}, cmdArgs...)\n\t} else {\n\t\t// Arbitrary commands should also run the hooks, so we write them to a file as well. However, if the\n\t\t// command args include env variable evaluations, then they'll be evaluated _before_ the hooks run,\n\t\t// which we don't want. So, one solution is to write the entire command and its arguments into the\n\t\t// file itself, but that may not be great if the variables contain sensitive information. Instead,\n\t\t// we save the entire command (with args) into the DEVBOX_RUN_CMD var, and then the script evals it.\n\t\tscriptBody, err := shellgen.ScriptBody(d, \"eval $DEVBOX_RUN_CMD\\n\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = shellgen.WriteScriptFile(d, arbitraryCmdFilename, scriptBody)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tscript := shellgen.ScriptPath(d.ProjectDir(), arbitraryCmdFilename)\n\t\tcmdWithArgs = []string{strconv.Quote(script)}\n\t\tenv[\"DEVBOX_RUN_CMD\"] = strings.Join(append([]string{cmdName}, cmdArgs...), \" \")\n\t}\n\n\treturn nix.RunScript(d.projectDir, strings.Join(cmdWithArgs, \" \"), env)\n}\n\n// Install ensures that all the packages in the config are installed\n// but does not run init hooks. It is used to power devbox install cli command.\nfunc (d *Devbox) Install(ctx context.Context) error {\n\tctx, task := trace.NewTask(ctx, \"devboxInstall\")\n\tdefer task.End()\n\n\treturn d.ensureStateIsUpToDate(ctx, ensure)\n}\n\nfunc (d *Devbox) ListScripts() []string {\n\tscripts := d.cfg.Scripts()\n\tkeys := make([]string, len(scripts))\n\ti := 0\n\tfor k := range scripts {\n\t\tkeys[i] = k\n\t\ti++\n\t}\n\n\tslices.Sort(keys)\n\n\treturn keys\n}\n\n// EnvExports returns a string of the env-vars that would need to be applied\n// to define a Devbox environment. The string is of the form `export KEY=VALUE` for each\n// env-var that needs to be applied.\nfunc (d *Devbox) EnvExports(ctx context.Context, opts devopt.EnvExportsOpts) (string, error) {\n\tctx, task := trace.NewTask(ctx, \"devboxEnvExports\")\n\tdefer task.End()\n\n\tvar envs map[string]string\n\tvar err error\n\n\tenvs, err = d.ensureStateIsUpToDateAndComputeEnv(ctx, opts.EnvOptions)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Use the appropriate export format based on shell type\n\tvar envStr string\n\tif opts.ShellFormat == devopt.ShellFormatNushell {\n\t\tenvStr = exportifyNushell(envs)\n\t} else {\n\t\tenvStr = exportify(envs)\n\t}\n\n\tif opts.RunHooks {\n\t\thooksStr := \". \\\"\" + shellgen.ScriptPath(d.ProjectDir(), shellgen.HooksFilename) + \"\\\"\"\n\t\tenvStr = fmt.Sprintf(\"%s\\n%s;\\n\", envStr, hooksStr)\n\t}\n\n\tif !opts.NoRefreshAlias {\n\t\tenvStr += \"\\n\" + d.refreshAliasForShell(string(opts.ShellFormat))\n\t}\n\n\treturn envStr, nil\n}\n\nfunc (d *Devbox) EnvVars(ctx context.Context) ([]string, error) {\n\tctx, task := trace.NewTask(ctx, \"devboxEnvVars\")\n\tdefer task.End()\n\t// this only returns env variables for the shell environment excluding hooks\n\tenvs, err := d.ensureStateIsUpToDateAndComputeEnv(ctx, devopt.EnvOptions{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn envir.MapToPairs(envs), nil\n}\n\nfunc (d *Devbox) shellEnvHashKey() string {\n\t// Don't make this a const so we don't use it by itself accidentally\n\treturn \"__DEVBOX_SHELLENV_HASH_\" + d.ProjectDirHash()\n}\n\nfunc (d *Devbox) Info(ctx context.Context, pkg string, markdown bool) (string, error) {\n\tctx, task := trace.NewTask(ctx, \"devboxInfo\")\n\tdefer task.End()\n\n\tname, version, isVersioned := searcher.ParseVersionedPackage(pkg)\n\tif !isVersioned {\n\t\tname = pkg\n\t\tversion = \"latest\"\n\t}\n\n\tpackageVersion, err := searcher.Client().Resolve(name, version)\n\tif err != nil {\n\t\tif !errors.Is(err, searcher.ErrNotFound) {\n\t\t\treturn \"\", usererr.WithUserMessage(err, \"Package %q not found\\n\", pkg)\n\t\t}\n\n\t\tpackageVersion = nil\n\t\t// fallthrough to below\n\t}\n\n\tif packageVersion == nil {\n\t\treturn \"\", usererr.WithUserMessage(err, \"Package %q not found\\n\", pkg)\n\t}\n\n\t// we should only have one result\n\tinfo := fmt.Sprintf(\n\t\t\"%s%s %s\\n%s\\n\",\n\t\tlo.Ternary(markdown, \"## \", \"\"),\n\t\tpackageVersion.Name,\n\t\tpackageVersion.Version,\n\t\tpackageVersion.Summary,\n\t)\n\treadme, err := plugin.Readme(\n\t\tctx,\n\t\tdevpkg.PackageFromStringWithDefaults(pkg, d.lockfile),\n\t\td.projectDir,\n\t\tmarkdown,\n\t)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn info + readme, nil\n}\n\n// GenerateDevcontainer generates devcontainer.json and Dockerfile for vscode run-in-container\n// and GitHub Codespaces\nfunc (d *Devbox) GenerateDevcontainer(ctx context.Context, generateOpts devopt.GenerateOpts) error {\n\tctx, task := trace.NewTask(ctx, \"devboxGenerateDevcontainer\")\n\tdefer task.End()\n\n\t// construct path to devcontainer directory\n\tdevContainerPath := filepath.Join(d.projectDir, \".devcontainer/\")\n\tdevContainerJSONPath := filepath.Join(devContainerPath, \"devcontainer.json\")\n\tdockerfilePath := filepath.Join(devContainerPath, \"Dockerfile\")\n\n\t// check if devcontainer.json or Dockerfile exist\n\tfilesExist := fileutil.Exists(devContainerJSONPath) || fileutil.Exists(dockerfilePath)\n\tif !generateOpts.Force && filesExist {\n\t\treturn usererr.New(\n\t\t\t\"Files devcontainer.json or Dockerfile are already present in .devcontainer/. \" +\n\t\t\t\t\"Remove the files or use --force to overwrite them.\",\n\t\t)\n\t}\n\n\t// create directory\n\terr := os.MkdirAll(devContainerPath, os.ModePerm)\n\tif err != nil {\n\t\treturn redact.Errorf(\"error creating dev container directory in <project>/%s: %w\",\n\t\t\tredact.Safe(filepath.Base(devContainerPath)), err)\n\t}\n\n\t// Setup generate parameters\n\tgen := &generate.Options{\n\t\tPath:           devContainerPath,\n\t\tRootUser:       generateOpts.RootUser,\n\t\tIsDevcontainer: true,\n\t\tPkgs:           d.AllPackageNamesIncludingRemovedTriggerPackages(),\n\t\tLocalFlakeDirs: d.getLocalFlakesDirs(),\n\t}\n\n\t// generate dockerfile\n\terr = gen.CreateDockerfile(ctx, generate.CreateDockerfileOptions{})\n\tif err != nil {\n\t\treturn redact.Errorf(\"error generating dev container Dockerfile in <project>/%s: %w\",\n\t\t\tredact.Safe(filepath.Base(devContainerPath)), err)\n\t}\n\t// generate devcontainer.json\n\terr = gen.CreateDevcontainer(ctx)\n\tif err != nil {\n\t\treturn redact.Errorf(\"error generating devcontainer.json in <project>/%s: %w\",\n\t\t\tredact.Safe(filepath.Base(devContainerPath)), err)\n\t}\n\treturn nil\n}\n\n// GenerateDockerfile generates a Dockerfile that replicates the devbox shell\nfunc (d *Devbox) GenerateDockerfile(ctx context.Context, generateOpts devopt.GenerateOpts) error {\n\tctx, task := trace.NewTask(ctx, \"devboxGenerateDockerfile\")\n\tdefer task.End()\n\n\tdockerfilePath := filepath.Join(d.projectDir, \"Dockerfile\")\n\t// check if Dockerfile doesn't exist\n\tfilesExist := fileutil.Exists(dockerfilePath)\n\tif !generateOpts.Force && filesExist {\n\t\treturn usererr.New(\n\t\t\t\"Dockerfile is already present in the current directory. \" +\n\t\t\t\t\"Remove it or use --force to overwrite it.\",\n\t\t)\n\t}\n\n\t// Setup Generate parameters\n\tgen := &generate.Options{\n\t\tPath:           d.projectDir,\n\t\tRootUser:       generateOpts.RootUser,\n\t\tIsDevcontainer: false,\n\t\tPkgs:           d.AllPackageNamesIncludingRemovedTriggerPackages(),\n\t\tLocalFlakeDirs: d.getLocalFlakesDirs(),\n\t}\n\n\tscripts := d.cfg.Scripts()\n\n\t// generate dockerfile\n\treturn errors.WithStack(gen.CreateDockerfile(ctx, generate.CreateDockerfileOptions{\n\t\tForType:    generateOpts.ForType,\n\t\tHasBuild:   scripts[\"build\"] != nil,\n\t\tHasInstall: scripts[\"install\"] != nil,\n\t\tHasStart:   scripts[\"start\"] != nil,\n\t}))\n}\n\nfunc PrintEnvrcContent(w io.Writer, envFlags devopt.EnvFlags, configDir string) error {\n\treturn generate.EnvrcContent(w, envFlags, configDir)\n}\n\n// GenerateEnvrcFile generates a .envrc file that makes direnv integration convenient\nfunc (d *Devbox) GenerateEnvrcFile(ctx context.Context, opts devopt.EnvrcOpts) error {\n\tctx, task := trace.NewTask(ctx, \"devboxGenerateEnvrc\")\n\tdefer task.End()\n\n\t// If no envrcDir was specified, use the configDir. This is for backward compatibility\n\t// where the .envrc was placed in the same location as specified by --config. Note that\n\t// if that is also blank, the .envrc will be generated in the current working directory.\n\tif opts.EnvrcDir == \"\" {\n\t\topts.EnvrcDir = opts.ConfigDir\n\t}\n\n\tenvrcFilePath := filepath.Join(opts.EnvrcDir, \".envrc\")\n\tfilesExist := fileutil.Exists(envrcFilePath)\n\tif !opts.Force && filesExist {\n\t\treturn usererr.New(\n\t\t\t\"A .envrc is already present in %q. Remove it or use --force to overwrite it.\",\n\t\t\topts.EnvrcDir,\n\t\t)\n\t}\n\n\t// generate all shell files to ensure we can refer to them in the .envrc script\n\tif err := d.ensureStateIsUpToDate(ctx, ensure); err != nil {\n\t\treturn err\n\t}\n\n\t// .envrc file creation\n\terr := generate.CreateEnvrc(ctx, opts)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tux.Fsuccessf(d.stderr, \"generated .envrc file in %q.\\n\", opts.EnvrcDir)\n\tif cmdutil.Exists(\"direnv\") {\n\t\tcmd := exec.Command(\"direnv\", \"allow\", opts.EnvrcDir)\n\t\terr := cmd.Run()\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\tux.Fsuccessf(d.stderr, \"ran `direnv allow %s`\\n\", opts.EnvrcDir)\n\t}\n\treturn nil\n}\n\n// saveCfg writes the config file to the devbox directory.\nfunc (d *Devbox) saveCfg() error {\n\treturn d.cfg.Root.SaveTo(d.ProjectDir())\n}\n\nfunc (d *Devbox) Services() (services.Services, error) {\n\tpluginSvcs, err := plugin.GetServices(d.cfg.IncludedPluginConfigs())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuserSvcs := services.FromUserProcessCompose(d.projectDir, d.customProcessComposeFile)\n\n\tsvcSet := lo.Assign(pluginSvcs, userSvcs)\n\tkeys := make([]string, 0, len(svcSet))\n\tfor k := range svcSet {\n\t\tkeys = append(keys, k)\n\t}\n\tslices.Sort(keys)\n\n\tresult := services.Services{}\n\tfor _, k := range keys {\n\t\tresult[k] = svcSet[k]\n\t}\n\n\treturn result, nil\n}\n\nfunc (d *Devbox) execPrintDevEnv(ctx context.Context, usePrintDevEnvCache bool) (map[string]string, error) {\n\tvar spinny *spinner.Spinner\n\tif !usePrintDevEnvCache {\n\t\tspinny = spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriter(d.stderr))\n\t\tspinny.FinalMSG = \"✓ Computed the Devbox environment.\\n\"\n\t\tspinny.Suffix = \" Computing the Devbox environment...\\n\"\n\t\tspinny.Start()\n\t}\n\n\tvaf, err := d.nix.PrintDevEnv(ctx, &nix.PrintDevEnvArgs{\n\t\tFlakeDir:             d.flakeDir(),\n\t\tPrintDevEnvCachePath: d.nixPrintDevEnvCachePath(),\n\t\tUsePrintDevEnvCache:  usePrintDevEnvCache,\n\t})\n\tif spinny != nil {\n\t\tspinny.Stop()\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Add environment variables from \"nix print-dev-env\" except for a few\n\t// special ones we need to ignore.\n\tenv := map[string]string{}\n\tfor key, val := range vaf.Variables {\n\t\t// We only care about \"exported\" because the var and array types seem to only be used by nix-defined\n\t\t// functions that we don't need (like genericBuild). For reference, each type translates to bash as follows:\n\t\t// var: export VAR=VAL\n\t\t// exported: export VAR=VAL\n\t\t// array: declare -a VAR=('VAL1' 'VAL2' )\n\t\tif val.Type != \"exported\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// SSL_CERT_FILE is a special-case. We only ignore it if it's\n\t\t// set to a specific value. This emulates the behavior of\n\t\t// \"nix develop\".\n\t\tif key == \"SSL_CERT_FILE\" && val.Value.(string) == \"/no-cert-file.crt\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Certain variables get set to invalid values after Nix builds\n\t\t// the shell environment. For example, HOME=/homeless-shelter\n\t\t// and TMPDIR points to a missing directory. We want to ignore\n\t\t// those values and just use the values from the current\n\t\t// environment instead.\n\t\tif ignoreDevEnvVar[key] {\n\t\t\tcontinue\n\t\t}\n\n\t\tenv[key] = val.Value.(string)\n\t}\n\treturn env, nil\n}\n\n// computeEnv computes the set of environment variables that define a Devbox\n// environment. The \"devbox run\" and \"devbox shell\" commands source these\n// variables into a shell before executing a command or showing an interactive\n// prompt.\n//\n// The process for building the environment involves layering sets of\n// environment variables on top of each other, with each layer overwriting any\n// duplicate keys from the previous:\n//\n//  1. Copy variables from the current environment except for those in\n//     ignoreCurrentEnvVar, such as PWD and SHELL.\n//  2. Copy variables from \"nix print-dev-env\" except for those in\n//     ignoreDevEnvVar, such as TMPDIR and HOME.\n//  3. Copy variables from Devbox plugins.\n//  4. Set PATH to the concatenation of the PATHs from step 3, step 2, and\n//     step 1 (in that order).\n//\n// The final result is a set of environment variables where Devbox plugins have\n// the highest priority, then Nix environment variables, and then variables\n// from the current environment. Similarly, the PATH gives Devbox plugin\n// binaries the highest priority, then Nix packages, and then non-Nix\n// programs.\n//\n// Note that the shellrc.tmpl template (which sources this environment) does\n// some additional processing. The computeEnv environment won't necessarily\n// represent the final \"devbox run\" or \"devbox shell\" environments.\nfunc (d *Devbox) computeEnv(\n\tctx context.Context,\n\tusePrintDevEnvCache bool,\n\tenvOpts devopt.EnvOptions,\n) (map[string]string, error) {\n\tdefer debug.FunctionTimer().End()\n\tdefer trace.StartRegion(ctx, \"devboxComputeEnv\").End()\n\n\t// Append variables from current env if --pure is not passed\n\tcurrentEnv := os.Environ()\n\tenv, err := d.parseEnvAndExcludeSpecialCases(currentEnv, envOpts.Pure)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// check if contents of .envrc is old and print warning\n\tif !usePrintDevEnvCache {\n\t\terr := d.checkOldEnvrc()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tslog.Debug(\"current environment PATH\", \"path\", env[\"PATH\"])\n\n\toriginalEnv := make(map[string]string, len(env))\n\tmaps.Copy(originalEnv, env)\n\n\tif !envOpts.OmitNixEnv {\n\t\tnixEnv, err := d.execPrintDevEnv(ctx, usePrintDevEnvCache)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor k, v := range nixEnv {\n\t\t\tenv[k] = v\n\t\t}\n\t}\n\tslog.Debug(\"nix environment PATH\", \"path\", env[\"PATH\"])\n\n\tenv[\"PATH\"] = envpath.JoinPathLists(\n\t\tnix.ProfileBinPath(d.projectDir),\n\t\tenv[\"PATH\"],\n\t)\n\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Add helpful env vars for a Devbox project\n\tenv[\"DEVBOX_PROJECT_ROOT\"] = d.projectDir\n\tenv[\"DEVBOX_WD\"] = wd\n\tenv[\"DEVBOX_CONFIG_DIR\"] = d.projectDir + \"/devbox.d\"\n\tenv[\"DEVBOX_PACKAGES_DIR\"] = d.projectDir + \"/\" + nix.ProfilePath\n\n\t// Include env variables in devbox.json\n\tconfigEnv, err := d.configEnvs(ctx, env)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\taddEnvIfNotPreviouslySetByDevbox(env, configEnv)\n\n\tmarkEnvsAsSetByDevbox(configEnv)\n\n\t// devboxEnvPath starts with the initial PATH from print-dev-env, and is\n\t// transformed to be the \"PATH of the Devbox environment\"\n\t// TODO: The prior statement is not fully true,\n\t//  since env[\"PATH\"] is written to above and so it is already no longer \"PATH\n\t//  from print-dev-env\". Consider moving devboxEnvPath higher up in this function\n\t//  where env[\"PATH\"] is written to.\n\tdevboxEnvPath := env[\"PATH\"]\n\tslog.Debug(\"PATH after plugins and config\", \"path\", devboxEnvPath)\n\n\t// We filter out nix store paths of devbox-packages (represented here as buildInputs).\n\t// Motivation: if a user removes a package from their devbox it should no longer\n\t// be available in their environment.\n\tbuildInputs := strings.Split(env[\"buildInputs\"], \" \")\n\tvar glibcPatchPath []string\n\tdevboxEnvPath = filterPathList(devboxEnvPath, func(path string) bool {\n\t\t// TODO(gcurtis): this is a massive hack. Please get rid\n\t\t// of this and install the package to the profile.\n\t\tif strings.Contains(path, \"patched-glibc\") {\n\t\t\tglibcPatchPath = append(glibcPatchPath, path)\n\t\t\treturn true\n\t\t}\n\t\tfor _, input := range buildInputs {\n\t\t\t// input is of the form: /nix/store/<hash>-<package-name>-<version>\n\t\t\t// path is of the form: /nix/store/<hash>-<package-name>-<version>/bin\n\t\t\tif strings.TrimSpace(input) != \"\" && strings.HasPrefix(path, input) {\n\t\t\t\tslog.Debug(\"filtering out buildInput from PATH\", \"path\", path, \"input\", input)\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t})\n\tslog.Debug(\"PATH after filtering buildInputs\", \"inputs\", buildInputs, \"path\", devboxEnvPath)\n\n\t// TODO(gcurtis): this is a massive hack. Please get rid\n\t// of this and install the package to the profile.\n\tif len(glibcPatchPath) != 0 {\n\t\tpatchedPath := strings.Join(glibcPatchPath, string(filepath.ListSeparator))\n\t\tdevboxEnvPath = envpath.JoinPathLists(patchedPath, devboxEnvPath)\n\t\tslog.Debug(\"PATH after glibc-patch hack\", \"path\", devboxEnvPath)\n\t}\n\n\trunXPaths, err := d.RunXPaths(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdevboxEnvPath = envpath.JoinPathLists(devboxEnvPath, runXPaths)\n\n\tpathStack := envpath.Stack(env, originalEnv)\n\tpathStack.Push(env, d.ProjectDirHash(), devboxEnvPath, envOpts.PreservePathStack)\n\tenv[\"PATH\"] = pathStack.Path(env)\n\tslog.Debug(\"new path stack is\", \"path_stack\", pathStack)\n\n\tslog.Debug(\"computed environment PATH\", \"path\", env[\"PATH\"])\n\n\tif !envOpts.Pure {\n\t\t// preserve the original XDG_DATA_DIRS by prepending to it\n\t\tenv[\"XDG_DATA_DIRS\"] = envpath.JoinPathLists(env[\"XDG_DATA_DIRS\"], os.Getenv(\"XDG_DATA_DIRS\"))\n\t}\n\n\tfor k, v := range d.env {\n\t\tenv[k] = v\n\t}\n\n\treturn env, d.addHashToEnv(env)\n}\n\n// ensureStateIsUpToDateAndComputeEnv will return a map of the env-vars for the Devbox Environment\n// while ensuring these reflect the current (up to date) state of the project.\nfunc (d *Devbox) ensureStateIsUpToDateAndComputeEnv(\n\tctx context.Context,\n\tenvOpts devopt.EnvOptions,\n) (map[string]string, error) {\n\tdefer debug.FunctionTimer().End()\n\n\tupToDate, err := d.lockfile.IsUpToDateAndInstalled(isFishShell())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !upToDate {\n\t\tif envOpts.Hooks.OnStaleState != nil {\n\t\t\tenvOpts.Hooks.OnStaleState()\n\t\t}\n\t}\n\n\tif !envOpts.SkipRecompute {\n\t\t// When ensureStateIsUpToDate is called with ensure=true, it always\n\t\t// returns early if the lockfile is up to date. So we don't need to check here\n\t\tif err := d.ensureStateIsUpToDate(ctx, ensure); isConnectionError(err) {\n\t\t\tif !fileutil.Exists(d.nixPrintDevEnvCachePath()) {\n\t\t\t\tux.Ferrorf(\n\t\t\t\t\td.stderr,\n\t\t\t\t\t\"Error connecting to the internet and no cached environment found. Aborting.\\n\",\n\t\t\t\t)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tux.Fwarningf(\n\t\t\t\td.stderr,\n\t\t\t\t\"Error connecting to the internet. Will attempt to use cached environment.\\n\",\n\t\t\t)\n\t\t} else if err != nil {\n\t\t\t// Some other non connection error, just return it.\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Since ensureStateIsUpToDate calls computeEnv when not up do date,\n\t// it's ok to use usePrintDevEnvCache=true here always. This does end up\n\t// doing some non-nix work twice if lockfile is not up to date.\n\t// TODO: Improve this to avoid extra work.\n\treturn d.computeEnv(ctx, true /*usePrintDevEnvCache*/, envOpts)\n}\n\nfunc (d *Devbox) nixPrintDevEnvCachePath() string {\n\treturn filepath.Join(d.projectDir, \".devbox/.nix-print-dev-env-cache\")\n}\n\nfunc (d *Devbox) flakeDir() string {\n\treturn filepath.Join(d.projectDir, \".devbox/gen/flake\")\n}\n\n// AllPackageNamesIncludingRemovedTriggerPackages returns the all package names,\n// including those added by plugins and also those removed by builtins.\n// This has a gross name to differentiate it from AllPackages.\n// Some uses cases for this are the lockfile and devbox list command.\n//\n// TODO: We may want to get rid of this function and have callers\n// build their own list. e.g. Some callers need different representations of\n// flakes  (lockfile vs devbox list)\nfunc (d *Devbox) AllPackageNamesIncludingRemovedTriggerPackages() []string {\n\tresult := []string{}\n\tfor _, p := range d.cfg.Packages(true /*includeRemovedTriggerPackages*/) {\n\t\tresult = append(result, p.VersionedName())\n\t}\n\treturn result\n}\n\nfunc (d *Devbox) AllPackagesIncludingRemovedTriggerPackages() []*devpkg.Package {\n\tpackages := d.cfg.Packages(true /*includeRemovedTriggerPackages*/)\n\treturn devpkg.PackagesFromConfig(packages, d.lockfile)\n}\n\n// AllPackages returns the packages that are defined in devbox.json and\n// recursively added by plugins.\n// NOTE: This will not return packages removed by their plugin with the\n// __remove_trigger_package field.\nfunc (d *Devbox) AllPackages() []*devpkg.Package {\n\tpackages := d.cfg.Packages(false /*includeRemovedTriggerPackages*/)\n\treturn devpkg.PackagesFromConfig(packages, d.lockfile)\n}\n\nfunc (d *Devbox) TopLevelPackages() []*devpkg.Package {\n\treturn devpkg.PackagesFromConfig(d.cfg.Root.TopLevelPackages(), d.lockfile)\n}\n\n// InstallablePackages returns the packages that are to be installed\nfunc (d *Devbox) InstallablePackages() []*devpkg.Package {\n\treturn lo.Filter(d.AllPackages(), func(pkg *devpkg.Package, _ int) bool {\n\t\treturn pkg.IsInstallable()\n\t})\n}\n\nfunc (d *Devbox) HasDeprecatedPackages() bool {\n\tfor _, pkg := range d.AllPackages() {\n\t\tif pkg.IsLegacy() {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (d *Devbox) findPackageByName(name string) (*devpkg.Package, error) {\n\tif name == \"\" {\n\t\treturn nil, errors.New(\"package name cannot be empty\")\n\t}\n\tresults := map[*devpkg.Package]bool{}\n\tfor _, pkg := range d.TopLevelPackages() {\n\t\tif pkg.Raw == name || pkg.CanonicalName() == name {\n\t\t\tresults[pkg] = true\n\t\t}\n\t}\n\tif len(results) > 1 {\n\t\treturn nil, usererr.New(\n\t\t\t\"found multiple packages with name %s: %s. Please specify version\",\n\t\t\tname,\n\t\t\tlo.Keys(results),\n\t\t)\n\t}\n\tif len(results) == 0 {\n\t\treturn nil, usererr.WithUserMessage(\n\t\t\tsearcher.ErrNotFound, \"no package found with name %s\", name)\n\t}\n\treturn lo.Keys(results)[0], nil\n}\n\nfunc (d *Devbox) checkOldEnvrc() error {\n\tenvrcPath := filepath.Join(d.ProjectDir(), \".envrc\")\n\tnoUpdate, err := strconv.ParseBool(os.Getenv(\"DEVBOX_NO_ENVRC_UPDATE\"))\n\tif err != nil {\n\t\t// DEVBOX_NO_ENVRC_UPDATE is either not set or invalid\n\t\t// so we consider it the same as false\n\t\tnoUpdate = false\n\t}\n\t// check if user has an old version of envrc\n\tif fileutil.Exists(envrcPath) && !noUpdate {\n\t\tisNewEnvrc, err := fileutil.FileContains(\n\t\t\tenvrcPath,\n\t\t\t\"eval \\\"$(devbox generate direnv --print-envrc)\\\"\",\n\t\t)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !isNewEnvrc {\n\t\t\tux.Fwarningf(\n\t\t\t\td.stderr,\n\t\t\t\t\"Your .envrc file seems to be out of date. \"+\n\t\t\t\t\t\"Run `devbox generate direnv --force` to update it.\\n\"+\n\t\t\t\t\t\"Or silence this warning by setting DEVBOX_NO_ENVRC_UPDATE=1 env variable.\\n\",\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\n// configEnvs takes the existing environment (nix + plugin) and adds env\n// variables defined in Config. It also parses variables in config\n// that are referenced by $VAR or ${VAR} and replaces them with\n// their value in the existing env variables. Note, this doesn't\n// allow env variables from outside the shell to be referenced so\n// no leaked variables are caused by this function.\nfunc (d *Devbox) configEnvs(\n\tctx context.Context,\n\texistingEnv map[string]string,\n) (map[string]string, error) {\n\tdefer debug.FunctionTimer().End()\n\tenv := map[string]string{}\n\tif d.cfg.IsEnvsecEnabled() {\n\t\tsecrets, err := d.Secrets(ctx)\n\t\t// TODO: replace this with error.Is check once envsec exports it.\n\t\tif err != nil && !strings.Contains(err.Error(), \"project not initialized\") {\n\t\t\treturn nil, err\n\t\t} else if err != nil {\n\t\t\tux.Fwarningf(\n\t\t\t\td.stderr,\n\t\t\t\t\"Ignoring env_from directive. jetify cloud secrets is not \"+\n\t\t\t\t\t\"initialized. Run `devbox secrets init` to initialize it.\\n\",\n\t\t\t)\n\t\t} else {\n\t\t\tcloudSecrets, err := secrets.List(ctx)\n\t\t\tif err != nil {\n\t\t\t\tux.Fwarningf(\n\t\t\t\t\tos.Stderr,\n\t\t\t\t\t\"Error reading secrets from jetify cloud: %s\\n\\n\",\n\t\t\t\t\terr,\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tfor _, secret := range cloudSecrets {\n\t\t\t\t\tenv[secret.Name] = secret.Value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if d.cfg.Root.IsdotEnvEnabled() {\n\t\t// if env_from points to a .env file, parse and add it\n\t\tparsedEnvs, err := d.cfg.Root.ParseEnvsFromDotEnv()\n\t\tif err != nil {\n\t\t\t// it's fine to include the error ParseEnvsFromDotEnv here because\n\t\t\t// the error message is relevant to the user\n\t\t\treturn nil, usererr.New(\n\t\t\t\t\"failed parsing %s file. Error: %v\",\n\t\t\t\td.cfg.Root.EnvFrom,\n\t\t\t\terr,\n\t\t\t)\n\t\t}\n\t\tfor k, v := range parsedEnvs {\n\t\t\tenv[k] = v\n\t\t}\n\t} else if d.cfg.Root.EnvFrom != \"\" {\n\t\treturn nil, usererr.New(\n\t\t\t\"unknown env_from value: %s. Supported values are: \\\"%q\\\" or a path to a file ending in \\\".env\\\"\",\n\t\t\td.cfg.Root.EnvFrom,\n\t\t\tconfigfile.JetifyCloudEnvFromValue,\n\t\t)\n\t}\n\tfor k, v := range d.cfg.Env() {\n\t\tenv[k] = v\n\t}\n\treturn conf.OSExpandEnvMap(env, existingEnv, d.ProjectDir()), nil\n}\n\n// ignoreCurrentEnvVar contains environment variables that Devbox should remove\n// from the slice of [os.Environ] variables before sourcing them. These are\n// variables that are set automatically by a new shell.\nvar ignoreCurrentEnvVar = map[string]bool{\n\tenvir.DevboxLatestVersion: true,\n\n\t// Devbox may change the working directory of the shell, so using the\n\t// original PWD and OLDPWD would be wrong.\n\t\"PWD\":    true,\n\t\"OLDPWD\": true,\n\n\t// SHLVL is the number of nested shells. Copying it would give the\n\t// Devbox shell the same level as the parent shell.\n\t\"SHLVL\": true,\n\n\t// The parent shell isn't guaranteed to be the same as the Devbox shell.\n\t\"SHELL\": true,\n\n\t// The \"_\" variable is read-only, so we ignore it to avoid attempting to write it later.\n\t\"_\": true,\n}\n\n// ignoreDevEnvVar contains environment variables that Devbox should remove from\n// the slice of [Devbox.PrintDevEnv] variables before sourcing them.\n//\n// This list comes directly from the \"nix develop\" source:\n// https://github.com/NixOS/nix/blob/f08ad5bdbac02167f7d9f5e7f9bab57cf1c5f8c4/src/nix/develop.cc#L257-L275\nvar ignoreDevEnvVar = map[string]bool{\n\t\"BASHOPTS\":           true,\n\t\"HOME\":               true,\n\t\"NIX_BUILD_TOP\":      true,\n\t\"NIX_ENFORCE_PURITY\": true,\n\t\"NIX_LOG_FD\":         true,\n\t\"NIX_REMOTE\":         true,\n\t\"PPID\":               true,\n\t\"SHELL\":              true,\n\t\"SHELLOPTS\":          true,\n\t\"TEMP\":               true,\n\t\"TEMPDIR\":            true,\n\t\"TERM\":               true,\n\t\"TMP\":                true,\n\t\"TMPDIR\":             true,\n\t\"TZ\":                 true,\n\t\"UID\":                true,\n}\n\nfunc (d *Devbox) ProjectDirHash() string {\n\treturn cachehash.Bytes([]byte(d.projectDir))\n}\n\nfunc (d *Devbox) addHashToEnv(env map[string]string) error {\n\thash, err := cachehash.JSON(env)\n\tif err == nil {\n\t\tenv[d.shellEnvHashKey()] = hash\n\t}\n\treturn err\n}\n\n// parseEnvAndExcludeSpecialCases converts env as []string to map[string]string\n// In case of pure shell, it leaks HOME and it leaks PATH with some modifications\nfunc (d *Devbox) parseEnvAndExcludeSpecialCases(currentEnv []string, pure bool) (map[string]string, error) {\n\tenv := make(map[string]string, len(currentEnv))\n\tfor _, kv := range currentEnv {\n\t\tkey, val, found := strings.Cut(kv, \"=\")\n\t\tif !found {\n\t\t\treturn nil, errors.Errorf(\"expected \\\"=\\\" in keyval: %s\", kv)\n\t\t}\n\t\tif ignoreCurrentEnvVar[key] {\n\t\t\tcontinue\n\t\t}\n\t\t// handling special cases for pure shell\n\t\t// - HOME required for devbox binary to work\n\t\t// - PATH to find the nix installation. It is cleaned for pure mode below.\n\t\t// - TERM to enable colored text in the pure shell\n\t\tif !pure || key == \"HOME\" || key == \"PATH\" || key == \"TERM\" {\n\t\t\tenv[key] = val\n\t\t}\n\t}\n\n\t// handling special case for PATH\n\tif pure {\n\t\t// Setting a custom env variable to differentiate pure and regular shell\n\t\tenv[\"DEVBOX_PURE_SHELL\"] = \"1\"\n\t\t// Finding nix executables in path and passing it through\n\t\t// As well as adding devbox itself to PATH\n\t\t// Both are needed for devbox commands inside pure shell to work\n\t\tnixPath, err := exec.LookPath(\"nix\")\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"could not find any nix executable in PATH. Make sure Nix is installed and in PATH, then try again\")\n\t\t}\n\t\tnixPath = filepath.Dir(nixPath)\n\t\tenv[\"PATH\"] = envpath.JoinPathLists(nixPath, dotdevboxBinPath(d))\n\t}\n\treturn env, nil\n}\n\nfunc (d *Devbox) PluginManager() *plugin.Manager {\n\treturn d.pluginManager\n}\n\nfunc (d *Devbox) Lockfile() *lock.File {\n\treturn d.lockfile\n}\n\nfunc (d *Devbox) RunXPaths(ctx context.Context) (string, error) {\n\trunxBinPath := filepath.Join(d.projectDir, \".devbox\", \"virtenv\", \"runx\", \"bin\")\n\tif err := os.RemoveAll(runxBinPath); err != nil {\n\t\treturn \"\", err\n\t}\n\tif err := os.MkdirAll(runxBinPath, 0o755); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, pkg := range d.InstallablePackages() {\n\t\tif !pkg.IsRunX() {\n\t\t\tcontinue\n\t\t}\n\t\tlockedPkg, err := d.lockfile.Resolve(pkg.Raw)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tpaths, err := pkgtype.RunXClient().Install(ctx, lockedPkg.Resolved)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tfor _, path := range paths {\n\t\t\t// create symlink to all files in p\n\t\t\tfiles, err := os.ReadDir(path)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tfor _, file := range files {\n\t\t\t\tsrc := filepath.Join(path, file.Name())\n\t\t\t\tdst := filepath.Join(runxBinPath, file.Name())\n\t\t\t\tif err := os.Symlink(src, dst); err != nil && !errors.Is(err, os.ErrExist) {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn runxBinPath, nil\n}\n\nfunc validateEnvironment(environment string) (string, error) {\n\tif environment == \"\" {\n\t\treturn \"dev\", nil\n\t}\n\tif environment == \"dev\" || environment == \"prod\" || environment == \"preview\" {\n\t\treturn environment, nil\n\t}\n\treturn \"\", usererr.New(\n\t\t\"invalid environment %q. Environment must be one of dev, prod, or preview.\",\n\t\tenvironment,\n\t)\n}\n"
  },
  {
    "path": "internal/devbox/devbox_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devbox\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/bmatcuk/doublestar/v4\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.jetify.com/devbox/internal/devbox/envpath\"\n\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devconfig\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/nix\"\n)\n\nfunc TestDevbox(t *testing.T) {\n\tt.Setenv(\"TMPDIR\", \"/tmp\")\n\ttestPaths, err := doublestar.FilepathGlob(\"../../examples/**/devbox.json\")\n\trequire.NoError(t, err, \"Reading testdata/ should not fail\")\n\n\tassert.Greater(t, len(testPaths), 0, \"testdata/ and examples/ should contain at least 1 test\")\n\n\tfor _, testPath := range testPaths {\n\t\tif !strings.Contains(testPath, \"/commands/\") {\n\t\t\ttestShellPlan(t, testPath)\n\t\t}\n\t}\n}\n\nfunc testShellPlan(t *testing.T, testPath string) {\n\tbaseDir := filepath.Dir(testPath)\n\ttestName := fmt.Sprintf(\"%s_shell_plan\", filepath.Base(baseDir))\n\tt.Run(testName, func(t *testing.T) {\n\t\tt.Setenv(envir.XDGDataHome, \"/tmp/devbox\")\n\t\tassert := assert.New(t)\n\n\t\t_, err := Open(&devopt.Opts{\n\t\t\tDir:    baseDir,\n\t\t\tStderr: os.Stderr,\n\t\t})\n\t\tassert.NoErrorf(err, \"%s should be a valid devbox project\", baseDir)\n\t})\n}\n\ntype testNix struct {\n\tpath string\n}\n\nfunc (n *testNix) PrintDevEnv(ctx context.Context, args *nix.PrintDevEnvArgs) (*nix.PrintDevEnvOut, error) {\n\treturn &nix.PrintDevEnvOut{\n\t\tVariables: map[string]nix.Variable{\n\t\t\t\"PATH\": {\n\t\t\t\tType:  \"exported\",\n\t\t\t\tValue: n.path,\n\t\t\t},\n\t\t},\n\t}, nil\n}\n\nfunc TestComputeEnv(t *testing.T) {\n\td := devboxForTesting(t)\n\td.nix = &testNix{}\n\tctx := t.Context()\n\tenv, err := d.computeEnv(ctx, false /*use cache*/, devopt.EnvOptions{})\n\trequire.NoError(t, err, \"computeEnv should not fail\")\n\tassert.NotNil(t, env, \"computeEnv should return a valid env\")\n}\n\nfunc TestComputeDevboxPathIsIdempotent(t *testing.T) {\n\tdevbox := devboxForTesting(t)\n\tdevbox.nix = &testNix{\"/tmp/my/path\"}\n\tctx := t.Context()\n\tenv, err := devbox.computeEnv(ctx, false /*use cache*/, devopt.EnvOptions{})\n\trequire.NoError(t, err, \"computeEnv should not fail\")\n\tpath := env[\"PATH\"]\n\tassert.NotEmpty(t, path, \"path should not be nil\")\n\n\tt.Setenv(\"PATH\", path)\n\tt.Setenv(envpath.InitPathEnv, env[envpath.InitPathEnv])\n\tt.Setenv(envpath.PathStackEnv, env[envpath.PathStackEnv])\n\tt.Setenv(envpath.Key(devbox.ProjectDirHash()), env[envpath.Key(devbox.ProjectDirHash())])\n\n\tenv, err = devbox.computeEnv(ctx, false /*use cache*/, devopt.EnvOptions{})\n\trequire.NoError(t, err, \"computeEnv should not fail\")\n\tpath2 := env[\"PATH\"]\n\n\tassert.Equal(t, path, path2, \"path should be the same\")\n}\n\nfunc TestComputeDevboxPathWhenRemoving(t *testing.T) {\n\tdevbox := devboxForTesting(t)\n\tdevbox.nix = &testNix{\"/tmp/my/path\"}\n\tctx := t.Context()\n\tenv, err := devbox.computeEnv(ctx, false /*use cache*/, devopt.EnvOptions{})\n\trequire.NoError(t, err, \"computeEnv should not fail\")\n\tpath := env[\"PATH\"]\n\tassert.NotEmpty(t, path, \"path should not be nil\")\n\tassert.Contains(t, path, \"/tmp/my/path\", \"path should contain /tmp/my/path\")\n\n\tt.Setenv(\"PATH\", path)\n\tt.Setenv(envpath.InitPathEnv, env[envpath.InitPathEnv])\n\tt.Setenv(envpath.PathStackEnv, env[envpath.PathStackEnv])\n\tt.Setenv(envpath.Key(devbox.ProjectDirHash()), env[envpath.Key(devbox.ProjectDirHash())])\n\n\tdevbox.nix.(*testNix).path = \"\"\n\tenv, err = devbox.computeEnv(ctx, false /*use cache*/, devopt.EnvOptions{})\n\trequire.NoError(t, err, \"computeEnv should not fail\")\n\tpath2 := env[\"PATH\"]\n\tassert.NotContains(t, path2, \"/tmp/my/path\", \"path should not contain /tmp/my/path\")\n\n\tassert.NotEqual(t, path, path2, \"path should not be the same\")\n}\n\nfunc devboxForTesting(t *testing.T) *Devbox {\n\tpath := t.TempDir()\n\t_, err := devconfig.Init(path)\n\trequire.NoError(t, err, \"InitConfig should not fail\")\n\td, err := Open(&devopt.Opts{\n\t\tDir:    path,\n\t\tStderr: os.Stderr,\n\t})\n\trequire.NoError(t, err, \"Open should not fail\")\n\n\treturn d\n}\n"
  },
  {
    "path": "internal/devbox/devopt/devboxopts.go",
    "content": "package devopt\n\nimport (\n\t\"io\"\n)\n\n// Naming Convention:\n// - suffix Opts for structs corresponding to a Devbox api function\n// - omit suffix Opts for other structs that are composed into an Opts struct\n\ntype Opts struct {\n\tDir                      string\n\tEnv                      map[string]string\n\tEnvironment              string\n\tIgnoreWarnings           bool\n\tCustomProcessComposeFile string\n\tStderr                   io.Writer\n}\n\ntype ProcessComposeOpts struct {\n\tExtraFlags         []string\n\tBackground         bool\n\tProcessComposePort int\n}\n\ntype GenerateOpts struct {\n\tForType  string\n\tForce    bool\n\tRootUser bool\n}\n\ntype EnvFlags struct {\n\tEnvMap  map[string]string\n\tEnvFile string\n}\n\ntype EnvrcOpts struct {\n\tEnvFlags\n\tForce     bool\n\tEnvrcDir  string\n\tConfigDir string\n}\n\ntype PullboxOpts struct {\n\tOverwrite   bool\n\tURL         string\n\tCredentials Credentials\n}\n\ntype Credentials struct {\n\tIDToken string\n\t// TODO We can just parse these out, but don't want to add a dependency right now\n\tEmail string\n\tSub   string\n}\n\ntype AddOpts struct {\n\tAllowInsecure    []string\n\tPlatforms        []string\n\tExcludePlatforms []string\n\tDisablePlugin    bool\n\tPatch            string\n\tOutputs          []string\n}\n\ntype UpdateOpts struct {\n\tPkgs                  []string\n\tNoInstall             bool\n\tIgnoreMissingPackages bool\n}\n\ntype ShellFormat string\n\nconst (\n\tShellFormatBash    ShellFormat = \"bash\"\n\tShellFormatNushell ShellFormat = \"nushell\"\n)\n\ntype EnvExportsOpts struct {\n\tEnvOptions     EnvOptions\n\tNoRefreshAlias bool\n\tRunHooks       bool\n\tShellFormat    ShellFormat\n}\n\n// EnvOptions configure the Devbox Environment in the `computeEnv` function.\n// - These options are commonly set by flags in some Devbox commands\n// like `shellenv`, `shell` and `run`.\n// - The struct is designed for the \"common case\" to be zero-initialized as `EnvOptions{}`.\ntype EnvOptions struct {\n\tHooks             LifecycleHooks\n\tOmitNixEnv        bool\n\tPreservePathStack bool\n\tPure              bool\n\tSkipRecompute     bool\n}\n\ntype LifecycleHooks struct {\n\t// OnStaleState is called when the Devbox state is out of date\n\tOnStaleState func()\n}\n"
  },
  {
    "path": "internal/devbox/docgen/docgen.go",
    "content": "package docgen\n\nimport (\n\t_ \"embed\"\n\t\"os\"\n\t\"text/template\"\n\n\t\"go.jetify.com/devbox/internal/devbox\"\n\t\"go.jetify.com/devbox/internal/fileutil\"\n)\n\n//go:embed readme.tmpl\nvar defaultReadmeTemplate string\n\nconst (\n\tdefaultName         = \"README.md\"\n\tdefaultTemplateName = \"readme.tmpl\"\n)\n\nfunc GenerateReadme(\n\tdevbox *devbox.Devbox,\n\toutputPath, templatePath string,\n) error {\n\treadmeTemplate := defaultReadmeTemplate\n\tif templatePath != \"\" {\n\t\treadmeTemplateBytes, err := os.ReadFile(templatePath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treadmeTemplate = string(readmeTemplateBytes)\n\t} else if fileutil.Exists(defaultTemplateName) {\n\t\treadmeTemplateBytes, err := os.ReadFile(defaultTemplateName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treadmeTemplate = string(readmeTemplateBytes)\n\t}\n\n\ttmpl, err := template.New(\"readme\").Parse(readmeTemplate)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif outputPath == \"\" {\n\t\toutputPath = defaultName\n\t}\n\n\tf, err := os.Create(outputPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn tmpl.Execute(f, map[string]any{\n\t\t\"Name\":        devbox.Config().Root.Name,\n\t\t\"Description\": devbox.Config().Root.Description,\n\t\t\"Scripts\": devbox.Config().Scripts().\n\t\t\tWithRelativePaths(devbox.ProjectDir()),\n\t\t\"EnvVars\":  devbox.Config().Env(),\n\t\t\"InitHook\": devbox.Config().InitHook(),\n\t\t\"Packages\": devbox.TopLevelPackages(),\n\t\t// TODO add includes\n\t})\n}\n\nfunc SaveDefaultReadmeTemplate(outputPath string) error {\n\tif outputPath == \"\" {\n\t\toutputPath = defaultTemplateName\n\t}\n\treturn os.WriteFile(outputPath, []byte(defaultReadmeTemplate), 0o644)\n}\n"
  },
  {
    "path": "internal/devbox/docgen/readme.tmpl",
    "content": "<!-- gen-readme start - generated by https://github.com/jetify-com/devbox/ -->\n\n{{- if .Name }}\n# {{ .Name }}\n{{ end }}\n\n{{- if .Description }}\n{{ .Description }}\n{{ end }}\n## Getting Started\nThis project uses [devbox](https://github.com/jetify-com/devbox) to manage its development environment.\n\nInstall devbox:\n```sh\ncurl -fsSL https://get.jetpack.io/devbox | bash\n```\n\nStart the devbox shell:\n```sh \ndevbox shell\n```\n\nRun a script in the devbox environment:\n```sh\ndevbox run <script>\n```\n\n{{- if .Scripts }}\n## Scripts\nScripts are custom commands that can be run using this project's environment. This project has the following scripts:\n{{ range $name, $_ := .Scripts }}\n* [{{ $name }}](#devbox-run-{{ $name }})\n{{- end }}\n{{ end }}\n\n{{- if .EnvVars }}\n## Environment\n\n```sh\n{{- range $key, $value := .EnvVars }}\n{{ $key }}=\"{{ $value }}\"\n{{- end }}\n```\n{{ end }}\n\n{{- if .InitHook }}\n## Shell Init Hook\nThe Shell Init Hook is a script that runs whenever the devbox environment is instantiated. It runs \non `devbox shell` and on `devbox run`.\n```sh\n{{ .InitHook }}\n```\n{{ end }}\n\n{{- if .Packages }}\n## Packages\n{{ range .Packages }}\n* {{ if .DocsURL }}[{{ .Raw }}]({{ .DocsURL }}){{ else }}{{ .Raw }}{{ end }}\n{{- end }}\n{{ end }}\n\n{{- if .Scripts }}\n## Script Details\n{{ range $name, $commands := .Scripts }}\n### devbox run {{ $name }}\n{{- if .Comments }}\n{{ .Comments }}\n{{-  end }}\n```sh\n{{ $commands }}\n```\n&ensp;\n{{ end }}\n{{ end }}\n\n<!-- gen-readme end -->\n"
  },
  {
    "path": "internal/devbox/envpath/pathlists.go",
    "content": "package envpath\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// JoinPathLists joins and cleans PATH-style strings of\n// [os.ListSeparator] delimited paths. To clean a path list, it splits it and\n// does the following for each element:\n//\n//  1. Applies [filepath.Clean].\n//  2. Removes the path if it's relative (must begin with '/' and not be '.').\n//  3. Removes the path if it's a duplicate.\nfunc JoinPathLists(pathLists ...string) string {\n\tif len(pathLists) == 0 {\n\t\treturn \"\"\n\t}\n\n\tseen := make(map[string]bool)\n\tvar cleaned []string\n\tfor _, path := range pathLists {\n\t\tfor _, path := range filepath.SplitList(path) {\n\t\t\tpath = filepath.Clean(path)\n\t\t\tif path == \".\" || path[0] != '/' {\n\t\t\t\t// Remove empty paths and don't allow relative\n\t\t\t\t// paths for security reasons.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !seen[path] {\n\t\t\t\tcleaned = append(cleaned, path)\n\t\t\t}\n\t\t\tseen[path] = true\n\t\t}\n\t}\n\treturn strings.Join(cleaned, string(filepath.ListSeparator))\n}\n\nfunc RemoveFromPath(path, pathToRemove string) string {\n\tpaths := filepath.SplitList(path)\n\n\t// Create a new slice to store the modified paths\n\tvar newPaths []string\n\n\t// Iterate through the paths and add them to the newPaths slice if they are not equal to pathToRemove\n\tfor _, p := range paths {\n\t\tif p != pathToRemove {\n\t\t\tnewPaths = append(newPaths, p)\n\t\t}\n\t}\n\n\t// Join the modified paths using \":\" as the delimiter\n\tnewPath := strings.Join(newPaths, string(os.PathListSeparator))\n\n\treturn newPath\n}\n"
  },
  {
    "path": "internal/devbox/envpath/pathlists_test.go",
    "content": "package envpath\n\nimport (\n\t\"testing\"\n)\n\nfunc TestCleanEnvPath(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tinPath  string\n\t\toutPath string\n\t}{\n\t\t{\n\t\t\tname:    \"NoEmptyPaths\",\n\t\t\tinPath:  \"/usr/local/bin::\",\n\t\t\toutPath: \"/usr/local/bin\",\n\t\t},\n\t\t{\n\t\t\tname:    \"NoRelativePaths\",\n\t\t\tinPath:  \"/usr/local/bin:/usr/bin:../test:/bin:/usr/sbin:/sbin:.:..\",\n\t\t\toutPath: \"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tgot := JoinPathLists(test.inPath)\n\t\t\tif got != test.outPath {\n\t\t\t\tt.Errorf(\"Got incorrect cleaned PATH.\\ngot:  %s\\nwant: %s\", got, test.outPath)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/devbox/envpath/stack.go",
    "content": "package envpath\n\nimport (\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\t\"golang.org/x/exp/slices\"\n)\n\nconst (\n\t// PathStackEnv stores the string representation of the stack, as a \":\" separated list.\n\t// Each element in the list is also the key to the env-var that stores the\n\t// devboxEnvPath for that devbox-project. Except for the last element, which is InitPathEnv.\n\tPathStackEnv = \"DEVBOX_PATH_STACK\"\n\n\t// InitPathEnv stores the path prior to any devbox shellenv modifying the environment\n\tInitPathEnv = \"DEVBOX_INIT_PATH\"\n)\n\n// stack has the following design:\n// 1. The stack enables tracking which sub-paths in PATH come from which devbox-project\n// 2. It is an ordered-list of keys to env-vars that store devboxEnvPath values of devbox-projects.\n// 3. The final PATH is reconstructed by concatenating the env-var values of each of these keys.\n// 4. The stack is stored in its own env-var PathStackEnv, shared by all devbox-projects in this shell.\ntype stack struct {\n\t// keys holds the stack elements.\n\t// Earlier (lower index number) keys get higher priority.\n\t// This keeps the string representation of the stack aligned with the PATH value.\n\tkeys []string\n}\n\n// Stack initializes the path stack in the `env` environment.\n// It relies on old state stored in the `originalEnv` environment.\nfunc Stack(env, originalEnv map[string]string) *stack {\n\tstackEnv, ok := originalEnv[PathStackEnv]\n\tif !ok || strings.TrimSpace(stackEnv) == \"\" {\n\t\t// if path stack is empty, then push the current PATH, which is the\n\t\t// external environment prior to any devbox-shellenv being applied to it.\n\t\tstackEnv = InitPathEnv\n\t\tenv[InitPathEnv] = originalEnv[\"PATH\"]\n\t}\n\treturn &stack{\n\t\tkeys: strings.Split(stackEnv, \":\"),\n\t}\n}\n\n// String is the value of the stack stored in its env-var.\nfunc (s *stack) String() string {\n\treturn strings.Join(s.keys, \":\")\n}\n\nfunc (s *stack) Path(env map[string]string) string {\n\t// Look up the paths-list for each stack element, and join them together to get the final PATH.\n\tpathLists := lo.Map(s.keys, func(part string, idx int) string { return env[part] })\n\treturn JoinPathLists(pathLists...)\n}\n\n// Key is the element stored in the stack for a devbox-project. It represents\n// a pointer to the devboxEnvPath value stored in its own env-var, also using this same Key.\nfunc Key(projectHash string) string {\n\treturn \"DEVBOX_NIX_ENV_PATH_\" + projectHash\n}\n\n// Push adds the new PATH for the devbox-project identified by projectHash.\n// This PATH is pushed to the top of the stack (given highest priority),\n// unless preservePathStack is enabled.\n//\n// It also updates the env by modifying the PathStack env-var, and the env-var\n// for storing this path.\nfunc (s *stack) Push(\n\tenv map[string]string,\n\tprojectHash string,\n\tpath string, // new PATH of the devbox-project of projectHash\n\tpreservePathStack bool,\n) {\n\tkey := Key(projectHash)\n\n\t// Add this path to env\n\tenv[key] = path\n\n\t// Common case: ensure this key is at the top of the stack\n\tif !preservePathStack ||\n\t\t// Case preservePathStack == true, usually from bin-wrapper or (in future) shell hook.\n\t\t// Add this key only if absent from the stack\n\t\t!lo.Contains(s.keys, key) {\n\n\t\ts.keys = lo.Uniq(slices.Insert(s.keys, 0, key))\n\t}\n\tenv[PathStackEnv] = s.String()\n}\n\n// Has tests if the stack has the key corresponding to projectHash\nfunc (s *stack) Has(projectHash string) bool {\n\treturn lo.Contains(s.keys, Key(projectHash))\n}\n"
  },
  {
    "path": "internal/devbox/envpath/stack_test.go",
    "content": "package envpath\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestNewStack(t *testing.T) {\n\t// Initialize a new Stack from the existing env\n\toriginalEnv := map[string]string{\n\t\t\"PATH\": \"/init-path\",\n\t}\n\tenv := make(map[string]string)\n\tstack := Stack(env, originalEnv)\n\tif len(stack.keys) == 0 {\n\t\tt.Errorf(\"Stack has no keys but should have %s\", InitPathEnv)\n\t}\n\tif len(stack.keys) != 1 {\n\t\tt.Errorf(\"Stack has should have exactly one key (%s) but has %d keys. Keys are: %s\",\n\t\t\tInitPathEnv, len(stack.keys), strings.Join(stack.keys, \", \"))\n\t}\n\n\t// Each testStep below is applied in order, and the resulting env\n\t// is used implicitly as input into the subsequent test step.\n\t//\n\t// These test steps are NOT independent! These are not \"test cases\" that\n\t// would usually be independent.\n\ttestSteps := []struct {\n\t\tprojectHash        string\n\t\tdevboxEnvPath      string\n\t\tpreservePathStack  bool\n\t\texpectedKeysLength int\n\t\texpectedEnv        map[string]string\n\t}{\n\t\t{\n\t\t\tprojectHash:        \"fooProjectHash\",\n\t\t\tdevboxEnvPath:      \"/foo1:/foo2\",\n\t\t\tpreservePathStack:  false,\n\t\t\texpectedKeysLength: 2,\n\t\t\texpectedEnv: map[string]string{\n\t\t\t\t\"PATH\":                \"/foo1:/foo2:/init-path\",\n\t\t\t\tInitPathEnv:           \"/init-path\",\n\t\t\t\tKey(\"fooProjectHash\"): \"/foo1:/foo2\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tprojectHash:        \"barProjectHash\",\n\t\t\tdevboxEnvPath:      \"/bar1:/bar2\",\n\t\t\tpreservePathStack:  false,\n\t\t\texpectedKeysLength: 3,\n\t\t\texpectedEnv: map[string]string{\n\t\t\t\t\"PATH\":                \"/bar1:/bar2:/foo1:/foo2:/init-path\",\n\t\t\t\tInitPathEnv:           \"/init-path\",\n\t\t\t\tKey(\"fooProjectHash\"): \"/foo1:/foo2\",\n\t\t\t\tKey(\"barProjectHash\"): \"/bar1:/bar2\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tprojectHash:        \"fooProjectHash\",\n\t\t\tdevboxEnvPath:      \"/foo3:/foo2\",\n\t\t\tpreservePathStack:  false,\n\t\t\texpectedKeysLength: 3,\n\t\t\texpectedEnv: map[string]string{\n\t\t\t\t\"PATH\":                \"/foo3:/foo2:/bar1:/bar2:/init-path\",\n\t\t\t\tInitPathEnv:           \"/init-path\",\n\t\t\t\tKey(\"fooProjectHash\"): \"/foo3:/foo2\",\n\t\t\t\tKey(\"barProjectHash\"): \"/bar1:/bar2\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tprojectHash:        \"barProjectHash\",\n\t\t\tdevboxEnvPath:      \"/bar3:/bar2\",\n\t\t\tpreservePathStack:  true,\n\t\t\texpectedKeysLength: 3,\n\t\t\texpectedEnv: map[string]string{\n\t\t\t\t\"PATH\":                \"/foo3:/foo2:/bar3:/bar2:/init-path\",\n\t\t\t\tInitPathEnv:           \"/init-path\",\n\t\t\t\tKey(\"fooProjectHash\"): \"/foo3:/foo2\",\n\t\t\t\tKey(\"barProjectHash\"): \"/bar3:/bar2\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor idx, testStep := range testSteps {\n\t\tt.Run(\n\t\t\tfmt.Sprintf(\"step_%d\", idx), func(t *testing.T) {\n\t\t\t\t// Push to stack and update PATH env\n\t\t\t\tstack.Push(env, testStep.projectHash, testStep.devboxEnvPath, testStep.preservePathStack)\n\t\t\t\tenv[\"PATH\"] = stack.Path(env)\n\n\t\t\t\tif len(stack.keys) != testStep.expectedKeysLength {\n\t\t\t\t\tt.Errorf(\"Stack should have exactly %d keys but has %d keys. Keys are: %s\",\n\t\t\t\t\t\ttestStep.expectedKeysLength, len(stack.keys), strings.Join(stack.keys, \", \"))\n\t\t\t\t}\n\t\t\t\tfor k, v := range testStep.expectedEnv {\n\t\t\t\t\tif env[k] != v {\n\t\t\t\t\t\tt.Errorf(\"env[%s] should be %s but is %s\", k, v, env[k])\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/devbox/envvars.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devbox\n\nimport (\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/internal/devbox/envpath\"\n\t\"go.jetify.com/devbox/internal/envir\"\n)\n\nconst devboxSetPrefix = \"__DEVBOX_SET_\"\n\n// exportify formats vars as a line-separated string of shell export statements.\n// Each line is of the form `export key=\"value\";` with any special characters in\n// value escaped. This means that the shell will always interpret values as\n// literal strings; no variable expansion or command substitution will take\n// place.\nfunc exportify(vars map[string]string) string {\n\tkeys := make([]string, len(vars))\n\ti := 0\n\tfor k := range vars {\n\t\tkeys[i] = k\n\t\ti++\n\t}\n\tslices.Sort(keys) // for reproducibility\n\n\tstrb := strings.Builder{}\n\tfor _, key := range keys {\n\t\tif strings.HasPrefix(key, \"BASH_FUNC_\") && strings.HasSuffix(key, \"%%\") {\n\t\t\t// Bash function\n\t\t\tfuncName := strings.TrimSuffix(key, \"%%\")\n\t\t\tfuncName = strings.TrimPrefix(funcName, \"BASH_FUNC_\")\n\t\t\tstrb.WriteString(funcName)\n\t\t\tstrb.WriteString(\" \")\n\t\t\tstrb.WriteString(vars[key])\n\t\t\tstrb.WriteString(\"\\nexport -f \")\n\t\t\tstrb.WriteString(funcName)\n\t\t\tstrb.WriteString(\"\\n\")\n\t\t} else {\n\t\t\t// Regular variable\n\t\t\tstrb.WriteString(\"export \")\n\t\t\tstrb.WriteString(key)\n\t\t\tstrb.WriteString(`=\"`)\n\t\t\tfor _, r := range vars[key] {\n\t\t\t\tswitch r {\n\t\t\t\t// Special characters inside double quotes:\n\t\t\t\t// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03\n\t\t\t\tcase '$', '`', '\"', '\\\\', '\\n':\n\t\t\t\t\tstrb.WriteRune('\\\\')\n\t\t\t\t}\n\t\t\t\tstrb.WriteRune(r)\n\t\t\t}\n\t\t\tstrb.WriteString(\"\\\";\\n\")\n\t\t}\n\t}\n\treturn strings.TrimSpace(strb.String())\n}\n\n// exportifyNushell formats vars as nushell environment variable assignments.\n// Each line is of the form `$env.KEY = \"value\"` with special characters escaped.\nfunc exportifyNushell(vars map[string]string) string {\n\t// Nushell protected environment variables that cannot be set manually\n\t// See: https://www.nushell.sh/book/environment.html#automatic-environment-variables\n\tprotectedVars := map[string]bool{\n\t\t\"CURRENT_FILE\":    true,\n\t\t\"FILE_PWD\":        true,\n\t\t\"LAST_EXIT_CODE\":  true,\n\t\t\"CMD_DURATION_MS\": true,\n\t\t\"NU_VERSION\":      true,\n\t\t\"PWD\":             true, // Nushell manages this automatically\n\t}\n\n\tkeys := make([]string, len(vars))\n\ti := 0\n\tfor k := range vars {\n\t\tkeys[i] = k\n\t\ti++\n\t}\n\tslices.Sort(keys) // for reproducibility\n\n\tstrb := strings.Builder{}\n\tfor _, key := range keys {\n\t\t// Skip bash functions for nushell\n\t\tif strings.HasPrefix(key, \"BASH_FUNC_\") && strings.HasSuffix(key, \"%%\") {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Skip nushell protected environment variables\n\t\tif protectedVars[key] {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Nushell environment variable syntax: $env.KEY = \"value\"\n\t\tstrb.WriteString(\"$env.\")\n\t\tstrb.WriteString(key)\n\t\tstrb.WriteString(` = \"`)\n\t\tfor _, r := range vars[key] {\n\t\t\tswitch r {\n\t\t\t// Escape special characters for nushell double-quoted strings\n\t\t\tcase '\"', '\\\\':\n\t\t\t\tstrb.WriteRune('\\\\')\n\t\t\t}\n\t\t\tstrb.WriteRune(r)\n\t\t}\n\t\tstrb.WriteString(\"\\\"\\n\")\n\t}\n\treturn strings.TrimSpace(strb.String())\n}\n\n// addEnvIfNotPreviouslySetByDevbox adds the key-value pairs from new to existing,\n// but only if the key was not previously set by devbox\n// Caveat, this won't mark the values as set by devbox automatically. Instead,\n// you need to call markEnvAsSetByDevbox when you are done setting variables.\n// This is so you can add variables from multiple sources (e.g. plugin, devbox.json)\n// that may build on each other (e.g. PATH=$PATH:...)\nfunc addEnvIfNotPreviouslySetByDevbox(existing, new map[string]string) {\n\tfor k, v := range new {\n\t\tif _, alreadySet := existing[devboxSetPrefix+k]; !alreadySet {\n\t\t\texisting[k] = v\n\t\t}\n\t}\n}\n\nfunc markEnvsAsSetByDevbox(envs ...map[string]string) {\n\tfor _, env := range envs {\n\t\tfor key := range env {\n\t\t\tenv[devboxSetPrefix+key] = \"1\"\n\t\t}\n\t}\n}\n\n// IsEnvEnabled checks if the devbox environment is enabled.\n// This allows us to differentiate between global and\n// individual project shells.\nfunc (d *Devbox) IsEnvEnabled() bool {\n\tfakeEnv := map[string]string{}\n\t// the Stack is initialized in the fakeEnv, from the state in the real os.Environ\n\tpathStack := envpath.Stack(fakeEnv, envir.PairsToMap(os.Environ()))\n\treturn pathStack.Has(d.ProjectDirHash())\n}\n\nfunc (d *Devbox) SkipInitHookEnvName() string {\n\treturn \"__DEVBOX_SKIP_INIT_HOOK_\" + d.ProjectDirHash()\n}\n"
  },
  {
    "path": "internal/devbox/errors.go",
    "content": "package devbox\n\nimport \"strings\"\n\nfunc isConnectionError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\treturn strings.Contains(err.Error(), \"no such host\") ||\n\t\tstrings.Contains(err.Error(), \"connection refused\")\n}\n"
  },
  {
    "path": "internal/devbox/flakes.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devbox\n\nimport (\n\t\"strings\"\n)\n\n// getLocalFlakesDirs searches packages and returns list of directories\n// of local flakes that are mentioned in config.\n// e.g., path:./my-flake#packageName -> ./my-flakes\nfunc (d *Devbox) getLocalFlakesDirs() []string {\n\tlocalFlakeDirs := []string{}\n\n\t// searching through installed packages to get location of local flakes\n\tfor _, pkg := range d.AllPackageNamesIncludingRemovedTriggerPackages() {\n\t\t// filtering local flakes packages\n\t\tif strings.HasPrefix(pkg, \"path:\") {\n\t\t\tpkgDirAndName, _ := strings.CutPrefix(pkg, \"path:\")\n\t\t\tpkgDir := strings.Split(pkgDirAndName, \"#\")[0]\n\t\t\tlocalFlakeDirs = append(localFlakeDirs, pkgDir)\n\t\t}\n\t}\n\treturn localFlakeDirs\n}\n"
  },
  {
    "path": "internal/devbox/generate/devcontainer_util.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage generate\n\n// package generate has functionality to implement the `devbox generate` command\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"embed\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime/trace\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n)\n\n//go:embed tmpl/*\nvar tmplFS embed.FS\n\ntype Options struct {\n\tPath           string\n\tRootUser       bool\n\tIsDevcontainer bool\n\tPkgs           []string\n\tLocalFlakeDirs []string\n}\n\ntype devcontainerObject struct {\n\tName           string          `json:\"name\"`\n\tBuild          *build          `json:\"build\"`\n\tCustomizations *customizations `json:\"customizations\"`\n\tRemoteUser     string          `json:\"remoteUser\"`\n}\n\ntype build struct {\n\tDockerfile string `json:\"dockerfile\"`\n\tContext    string `json:\"context\"`\n}\n\ntype customizations struct {\n\tVscode *vscode `json:\"vscode\"`\n}\n\ntype vscode struct {\n\tSettings   any      `json:\"settings\"`\n\tExtensions []string `json:\"extensions\"`\n}\n\ntype CreateDockerfileOptions struct {\n\tForType    string\n\tHasInstall bool\n\tHasBuild   bool\n\tHasStart   bool\n\t// Ideally we also support process-compose services as the dockerfile\n\t// CMD, but I'm currently having trouble getting that to work. Will revisit.\n\t// HasServices bool\n}\n\nfunc (opts CreateDockerfileOptions) Type() string {\n\treturn cmp.Or(opts.ForType, \"dev\")\n}\n\nfunc (opts CreateDockerfileOptions) validate() error {\n\tif opts.Type() == \"dev\" {\n\t\treturn nil\n\t} else if opts.Type() == \"prod\" {\n\t\tif opts.HasStart {\n\t\t\treturn nil\n\t\t}\n\t\treturn usererr.New(\n\t\t\t\"To generate a prod Dockerfile you must have either 'start' script in \" +\n\t\t\t\t\"devbox.json\",\n\t\t)\n\t}\n\treturn usererr.New(\n\t\t\"invalid Dockerfile type. Only 'dev' and 'prod' are supported\")\n}\n\n// CreateDockerfile creates a Dockerfile in path.\nfunc (g *Options) CreateDockerfile(\n\tctx context.Context,\n\topts CreateDockerfileOptions,\n) error {\n\tdefer trace.StartRegion(ctx, \"createDockerfile\").End()\n\n\tif err := opts.validate(); err != nil {\n\t\treturn err\n\t}\n\n\t// create dockerfile\n\tfile, err := os.Create(filepath.Join(g.Path, \"Dockerfile\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\tpath := fmt.Sprintf(\"tmpl/%s.Dockerfile.tmpl\", opts.Type())\n\tt := template.Must(template.ParseFS(tmplFS, path))\n\t// write content into file\n\treturn t.Execute(file, map[string]any{\n\t\t\"IsDevcontainer\": g.IsDevcontainer,\n\t\t\"RootUser\":       g.RootUser,\n\t\t\"LocalFlakeDirs\": g.LocalFlakeDirs,\n\n\t\t// The following are only used for prod Dockerfile\n\t\t\"DevboxRunInstall\": lo.Ternary(opts.HasInstall, \"devbox run install\", \"echo 'No install script found, skipping'\"),\n\t\t\"DevboxRunBuild\":   lo.Ternary(opts.HasBuild, \"devbox run build\", \"echo 'No build script found, skipping'\"),\n\t\t\"Cmd\":              fmt.Sprintf(\"%q, %q, %q\", \"devbox\", \"run\", \"start\"),\n\t})\n}\n\n// CreateDevcontainer creates a devcontainer.json in path and writes getDevcontainerContent's output into it\nfunc (g *Options) CreateDevcontainer(ctx context.Context) error {\n\tdefer trace.StartRegion(ctx, \"createDevcontainer\").End()\n\n\t// create devcontainer.json file\n\tfile, err := os.Create(filepath.Join(g.Path, \"devcontainer.json\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\t// get devcontainer.json's content\n\tdevcontainerContent := g.getDevcontainerContent()\n\tdevcontainerFileBytes, err := json.MarshalIndent(devcontainerContent, \"\", \"  \")\n\tif err != nil {\n\t\treturn err\n\t}\n\t// writing devcontainer's content into json file\n\t_, err = file.Write(devcontainerFileBytes)\n\treturn err\n}\n\nfunc CreateEnvrc(ctx context.Context, opts devopt.EnvrcOpts) error {\n\tdefer trace.StartRegion(ctx, \"createEnvrc\").End()\n\n\t// create .envrc file\n\tfile, err := os.Create(filepath.Join(opts.EnvrcDir, \".envrc\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\tflags := []string{}\n\n\tif len(opts.EnvMap) > 0 {\n\t\tfor k, v := range opts.EnvMap {\n\t\t\tflags = append(flags, fmt.Sprintf(\"--env %s=%s\", k, v))\n\t\t}\n\t}\n\tif opts.EnvFile != \"\" {\n\t\tflags = append(flags, fmt.Sprintf(\"--env-file %s\", opts.EnvFile))\n\t}\n\n\tconfigDir, err := getRelativePathToConfig(opts.EnvrcDir, opts.ConfigDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tt := template.Must(template.ParseFS(tmplFS, \"tmpl/envrc.tmpl\"))\n\n\t// write content into file\n\treturn t.Execute(file, map[string]string{\n\t\t\"EnvFlag\":   strings.Join(flags, \" \"),\n\t\t\"ConfigDir\": formatConfigDirArg(configDir),\n\t})\n}\n\n// Returns the relative path from sourceDir to configDir, or an error if it cannot be determined.\nfunc getRelativePathToConfig(sourceDir, configDir string) (string, error) {\n\tabsConfigDir, err := filepath.Abs(configDir)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get absolute path for config dir: %w\", err)\n\t}\n\n\tabsSourceDir, err := filepath.Abs(sourceDir)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get absolute path for source dir: %w\", err)\n\t}\n\n\t// We don't want the path if the config dir is a parent of the envrc dir. This way\n\t// the config will be found when it recursively searches for it through the parent tree.\n\tif strings.HasPrefix(absSourceDir, absConfigDir) {\n\t\treturn \"\", nil\n\t}\n\n\trelPath, err := filepath.Rel(absSourceDir, absConfigDir)\n\tif err != nil {\n\t\t// If a relative path cannot be computed, return the absolute path of configDir\n\t\treturn absConfigDir, err\n\t}\n\n\treturn relPath, nil\n}\n\nfunc (g *Options) getDevcontainerContent() *devcontainerObject {\n\t// object that gets written in devcontainer.json\n\tdevcontainerContent := &devcontainerObject{\n\t\t// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:\n\t\t// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/debian\n\t\tName: \"Devbox Remote Container\",\n\t\tBuild: &build{\n\t\t\tDockerfile: \"./Dockerfile\",\n\t\t\tContext:    \"..\",\n\t\t},\n\t\tCustomizations: &customizations{\n\t\t\tVscode: &vscode{\n\t\t\t\tSettings: map[string]any{\n\t\t\t\t\t// Add custom vscode settings for remote environment here\n\t\t\t\t},\n\t\t\t\tExtensions: []string{\n\t\t\t\t\t\"jetpack-io.devbox\",\n\t\t\t\t\t// Add custom vscode extensions for remote environment here\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tRemoteUser: \"devbox\",\n\t}\n\tif g.RootUser {\n\t\tdevcontainerContent.RemoteUser = \"root\"\n\t}\n\n\t// match only python3 or python3xx as package names\n\tpy3pattern, err := regexp.Compile(`(python3)$|(python3[0-9]{1,2})$`)\n\tif err != nil {\n\t\tslog.Debug(\"Failed to compile regex\")\n\t\treturn nil\n\t}\n\tfor _, pkg := range g.Pkgs {\n\t\tif py3pattern.MatchString(pkg) {\n\t\t\t// Setup python3 interpreter path to devbox in the container\n\t\t\tdevcontainerContent.Customizations.Vscode.Settings = map[string]any{\n\t\t\t\t\"python.defaultInterpreterPath\": \"/code/.devbox/nix/profile/default/bin/python3\",\n\t\t\t}\n\t\t\t// add python extension if a python3 package is installed\n\t\t\tdevcontainerContent.Customizations.Vscode.Extensions = append(devcontainerContent.Customizations.Vscode.Extensions, \"ms-python.python\")\n\t\t}\n\t\tif strings.Contains(pkg, \"go_1_\") || pkg == \"go\" {\n\t\t\tdevcontainerContent.Customizations.Vscode.Extensions = append(devcontainerContent.Customizations.Vscode.Extensions, \"golang.go\")\n\t\t}\n\t\t// TODO: add support for other common languages\n\t}\n\treturn devcontainerContent\n}\n\nfunc EnvrcContent(w io.Writer, envFlags devopt.EnvFlags, configDir string) error {\n\tt := template.Must(template.ParseFS(tmplFS, \"tmpl/envrcContent.tmpl\"))\n\tenvFlag := \"\"\n\tif len(envFlags.EnvMap) > 0 {\n\t\tfor k, v := range envFlags.EnvMap {\n\t\t\tenvFlag += fmt.Sprintf(\"--env %s=%s \", k, v)\n\t\t}\n\t}\n\n\treturn t.Execute(w, map[string]string{\n\t\t\"EnvFlag\":   envFlag,\n\t\t\"EnvFile\":   envFlags.EnvFile,\n\t\t\"ConfigDir\": formatConfigDirArg(configDir),\n\t})\n}\n\nfunc formatConfigDirArg(configDir string) string {\n\tif configDir == \"\" {\n\t\treturn \"\"\n\t}\n\n\treturn \"--config \" + configDir\n}\n"
  },
  {
    "path": "internal/devbox/generate/tmpl/DevboxImageDockerfile",
    "content": "FROM debian:stable-slim\n\n# Optional arg to install custom devbox version\nARG DEVBOX_USE_VERSION\n\n# Step 1: Installing dependencies\nRUN apt-get update\nRUN apt-get -y install bash binutils git xz-utils wget sudo\n\n# Step 2: Prepare for Nix\nARG TARGETPLATFORM\nRUN mkdir -p /etc/nix/\nRUN if [ \"$TARGETPLATFORM\" = \"linux/arm64\" ] || [ \"$TARGETPLATFORM\" = \"linux/arm64/v8\" ]; then \\\n        echo \"filter-syscalls = false\" >> /etc/nix/nix.conf; \\\n    fi\n\n# Step 3: Setting up devbox user\nENV DEVBOX_USER=devbox\nRUN adduser $DEVBOX_USER\nRUN usermod -aG sudo $DEVBOX_USER\nRUN echo \"devbox ALL=(ALL:ALL) NOPASSWD: ALL\" | sudo tee /etc/sudoers.d/$DEVBOX_USER\nUSER $DEVBOX_USER\n\n# Step 4: Installing Nix\nRUN wget --output-document=/dev/stdout https://nixos.org/nix/install | sh -s -- --no-daemon\nRUN . ~/.nix-profile/etc/profile.d/nix.sh\n\nENV PATH=\"/home/${DEVBOX_USER}/.nix-profile/bin:$PATH\"\n\n# Step 5: Installing devbox\nENV DEVBOX_USE_VERSION=$DEVBOX_USE_VERSION\nRUN wget --quiet --output-document=/dev/stdout https://get.jetify.com/devbox   | bash -s -- -f\nRUN chown -R \"${DEVBOX_USER}:${DEVBOX_USER}\" /usr/local/bin/devbox\n\n# run a devbox command to make launcher download devbox binary\nRUN devbox version\n\nCMD [\"devbox\", \"version\", \"-v\"]\n"
  },
  {
    "path": "internal/devbox/generate/tmpl/DevboxImageDockerfileRootUser",
    "content": "FROM debian:stable-slim\n\n# Optional arg to install custom devbox version\nARG DEVBOX_USE_VERSION\n\n# Step 1: Installing dependencies\nRUN apt-get update\nRUN apt-get -y install bash binutils git xz-utils wget sudo\n\n# Step 2: Installing Nix\nARG TARGETPLATFORM\nRUN mkdir -p /etc/nix/\nRUN if [ \"$TARGETPLATFORM\" = \"linux/arm64\" ] || [ \"$TARGETPLATFORM\" = \"linux/arm64/v8\" ]; then \\\n        echo \"filter-syscalls = false\" >> /etc/nix/nix.conf; \\\n    fi \nRUN wget --output-document=/dev/stdout https://nixos.org/nix/install | sh -s -- --daemon\nRUN . ~/.nix-profile/etc/profile.d/nix.sh\n\nENV PATH=\"/root/.nix-profile/bin:$PATH\"\n\n# Step 3: Installing devbox\nENV DEVBOX_USE_VERSION=$DEVBOX_USE_VERSION\nRUN wget --quiet --output-document=/dev/stdout https://get.jetify.com/devbox   | bash -s -- -f\n# run a devbox command to make launcher download devbox binary\nRUN devbox version\n\nCMD [\"devbox\", \"version\", \"-v\"]\n"
  },
  {
    "path": "internal/devbox/generate/tmpl/Dockerfile.dockerignore.tmpl",
    "content": "node_modules\n.venv\n"
  },
  {
    "path": "internal/devbox/generate/tmpl/dev.Dockerfile.tmpl",
    "content": "{{- if .RootUser }}FROM jetpackio/devbox-root-user:latest\n{{- else }}FROM jetpackio/devbox:latest\n{{- end}}\n\n# Installing your devbox project\nWORKDIR /code\n{{- if not .RootUser }}\nUSER root:root\nRUN mkdir -p /code && chown ${DEVBOX_USER}:${DEVBOX_USER} /code\nUSER ${DEVBOX_USER}:${DEVBOX_USER}\nCOPY --chown=${DEVBOX_USER}:${DEVBOX_USER} devbox.json devbox.json\nCOPY --chown=${DEVBOX_USER}:${DEVBOX_USER} devbox.lock devbox.lock\n{{- else}}\nCOPY devbox.json devbox.json\nCOPY devbox.lock devbox.lock\n{{- end}}\n\n{{if len .LocalFlakeDirs}}\n# Copying local flakes directories\n{{- end}}\n{{range $i, $element := .LocalFlakeDirs -}}\nCOPY {{$element}} {{$element}}\n{{end}}\nRUN devbox run -- echo \"Installed Packages.\" && nix-store --gc && nix-store --optimise\n{{if .IsDevcontainer}}\nRUN devbox shellenv --init-hook >> ~/.profile\n{{- else}}\nCMD [\"devbox\", \"shell\"]\n{{- end}}\n"
  },
  {
    "path": "internal/devbox/generate/tmpl/envrc.tmpl",
    "content": "#!/usr/bin/env bash\n\n# Automatically sets up your devbox environment whenever you cd into this\n# directory via our direnv integration:\n\neval \"$(devbox generate direnv --print-envrc{{ if .EnvFlag}} {{ .EnvFlag }}{{ end }}{{ if .ConfigDir }} {{ .ConfigDir }}{{ end }})\"\n\n# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/\n# for more details\n"
  },
  {
    "path": "internal/devbox/generate/tmpl/envrcContent.tmpl",
    "content": "use_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias{{ if .EnvFlag }} {{ .EnvFlag }}{{ end }}{{ if .ConfigDir }} {{ .ConfigDir }}{{ end }})\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n{{ if .EnvFile }}\ndotenv_if_exists {{ .EnvFile }}\n{{ end }}\n"
  },
  {
    "path": "internal/devbox/generate/tmpl/prod.Dockerfile.tmpl",
    "content": "FROM jetpackio/devbox:latest\n\nWORKDIR /code\nUSER root:root\nRUN mkdir -p /code && chown ${DEVBOX_USER}:${DEVBOX_USER} /code\nUSER ${DEVBOX_USER}:${DEVBOX_USER}\n\n{{- /*\nIdeally, we first copy over devbox.json and devbox.lock and run `devbox install` \nto create a cache layer for the dependencies. This is complicated because\ndevbox.json may include local dependencies (flakes and plugins). We could try\nto copy those in (the way the dev Dockerfile does) but that's brittle because\nthose dependencies may also pull in other local dependencies and so on. Another\nsulution would be to add a new flag `devbox install --skip-errors` that would \njust try to install what it can, and ignore the rest.\n\nA hack to make this simpler is to install from the lockfile instead of the json.\n*/}}\n\nCOPY --chown=${DEVBOX_USER}:${DEVBOX_USER} . .\n\nRUN devbox install\n\nRUN {{ .DevboxRunInstall }}\n\nRUN {{ .DevboxRunBuild }}\n\nCMD [{{ .Cmd }}]\n"
  },
  {
    "path": "internal/devbox/global.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devbox\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.jetify.com/devbox/internal/xdg\"\n)\n\n// In the future we will support multiple global profiles\nconst currentGlobalProfile = \"default\"\n\nfunc GlobalDataPath() (string, error) {\n\tpath := xdg.DataSubpath(filepath.Join(\"devbox/global\", currentGlobalProfile))\n\tif err := os.MkdirAll(path, 0o755); err != nil {\n\t\treturn \"\", errors.WithStack(err)\n\t}\n\n\tnixProfilePath := filepath.Join(path)\n\tcurrentPath := xdg.DataSubpath(\"devbox/global/current\")\n\n\t// For now default is always current. In the future we will support multiple\n\t// and allow user to switch. Remove any existing symlink and create a new one\n\t// because previous versions of devbox may have created a symlink to a\n\t// different profile.\n\texisting, _ := os.Readlink(currentPath)\n\tif existing != nixProfilePath {\n\t\t_ = os.Remove(currentPath)\n\t}\n\n\terr := os.Symlink(nixProfilePath, currentPath)\n\tif err != nil && !errors.Is(err, fs.ErrExist) {\n\t\treturn \"\", errors.WithStack(err)\n\t}\n\n\treturn path, nil\n}\n"
  },
  {
    "path": "internal/devbox/nixprofile.go",
    "content": "package devbox\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/nix/nixprofile\"\n)\n\n// syncNixProfileFromFlake ensures the nix profile has the packages from the buildInputs\n// from the devshell of the generated flake.\n//\n// It also removes any packages from the nix profile that are no longer in the buildInputs.\nfunc (d *Devbox) syncNixProfileFromFlake(ctx context.Context) error {\n\tdefer debug.FunctionTimer().End()\n\t// Get the buildInputs from the generated flake\n\tenv, err := d.execPrintDevEnv(ctx, false /*usePrintDevEnvCache*/)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbuildInputs := env[\"buildInputs\"]\n\n\t// Get the store-paths of the packages we want installed in the nix profile\n\twantStorePaths := []string{}\n\tif buildInputs != \"\" {\n\t\t// env[\"buildInputs\"] can be empty string if there are no packages in the project\n\t\t// if buildInputs is empty, then we don't want wantStorePaths to be an array with a single \"\" entry\n\t\twantStorePaths = strings.Split(buildInputs, \" \")\n\t}\n\n\tprofilePath, err := d.profilePath()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Get the store-paths of the packages currently installed in the nix profile\n\titems, err := nixprofile.ProfileListItems(d.stderr, profilePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nix profile list: %v\", err)\n\t}\n\tgotStorePaths := make([]string, 0, len(items))\n\tfor _, item := range items {\n\t\tgotStorePaths = append(gotStorePaths, item.StorePaths()...)\n\t}\n\n\t// Diff the store paths and install/remove packages as needed\n\tremove, add := lo.Difference(gotStorePaths, wantStorePaths)\n\tif len(remove) > 0 {\n\t\tpackagesToRemove := make([]string, 0, len(remove))\n\t\tfor _, p := range remove {\n\t\t\tstorePath := nix.NewStorePathParts(p)\n\t\t\tpackagesToRemove = append(packagesToRemove, fmt.Sprintf(\"%s@%s\", storePath.Name, storePath.Version))\n\t\t}\n\t\tslog.Debug(\"removing packages from nix profile\", \"pkgs\", strings.Join(packagesToRemove, \", \"))\n\n\t\tif err := nix.ProfileRemove(profilePath, remove...); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif len(add) > 0 {\n\t\tif err = nix.ProfileInstall(ctx, &nix.ProfileInstallArgs{\n\t\t\tInstallables: add,\n\t\t\tProfilePath:  profilePath,\n\t\t\tWriter:       d.stderr,\n\t\t}); errors.Is(err, nix.ErrPriorityConflict) {\n\t\t\t// We need to install the packages one by one because there was possibly a priority conflict\n\t\t\t// This is slower, but uncommon.\n\t\t\tfor _, addPath := range add {\n\t\t\t\tif err = nix.ProfileInstall(ctx, &nix.ProfileInstallArgs{\n\t\t\t\t\tInstallables: []string{addPath},\n\t\t\t\t\tProfilePath:  profilePath,\n\t\t\t\t\tWriter:       d.stderr,\n\t\t\t\t}); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error installing package in nix profile %s: %w\", addPath, err)\n\t\t\t\t}\n\t\t\t}\n\t\t} else if err != nil {\n\t\t\treturn fmt.Errorf(\"error installing packages in nix profile %s: %w\", add, err)\n\t\t}\n\t}\n\tif len(add) > 0 || len(remove) > 0 {\n\t\terr := wipeProfileHistory(profilePath)\n\t\tif err != nil {\n\t\t\t// Log the error, but nothing terrible happens if this\n\t\t\t// fails.\n\t\t\tslog.DebugContext(ctx, \"error cleaning up profile history\", \"err\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// wipeProfileHistory removes all old generations of a Nix profile, similar to\n// nix profile wipe-history. profile should be a path to the \"default\" symlink,\n// like .devbox/nix/profile/default.\nfunc wipeProfileHistory(profile string) error {\n\tlink, err := os.Readlink(profile)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn nil\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdir := filepath.Dir(profile)\n\tentries, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, dent := range entries {\n\t\tif dent.Name() == \"default\" || dent.Name() == link {\n\t\t\tcontinue\n\t\t}\n\t\terr := os.Remove(filepath.Join(dir, dent.Name()))\n\t\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/devbox/packages.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devbox\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime/trace\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devbox/providers/nixcache\"\n\t\"go.jetify.com/devbox/internal/devconfig\"\n\t\"go.jetify.com/devbox/internal/devconfig/configfile\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/devpkg/pkgtype\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/setup\"\n\t\"go.jetify.com/devbox/internal/shellgen\"\n\t\"go.jetify.com/devbox/internal/telemetry\"\n\t\"go.jetify.com/devbox/nix/flake\"\n\t\"go.jetify.com/pkg/auth\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/plugin\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\nconst StateOutOfDateMessage = \"Your devbox environment may be out of date. Run %s to update it.\\n\"\n\n// packages.go has functions for adding, removing and getting info about nix\n// packages\n\ntype UpdateVersion struct {\n\tCurrent string\n\tLatest  string\n}\n\n// Outdated returns a map of package names to their available latest version.\nfunc (d *Devbox) Outdated(ctx context.Context) (map[string]UpdateVersion, error) {\n\tlockfile := d.Lockfile()\n\toutdatedPackages := map[string]UpdateVersion{}\n\tvar warnings []string\n\n\tfor _, pkg := range d.AllPackages() {\n\t\t// For non-devbox packages, like flakes, we can skip for now\n\t\tif !pkg.IsDevboxPackage {\n\t\t\tcontinue\n\t\t}\n\n\t\tlockPackage, err := lockfile.FetchResolvedPackage(pkg.Versioned())\n\t\tif err != nil {\n\t\t\twarnings = append(warnings, fmt.Sprintf(\"Note: unable to check updates for %s\", pkg.CanonicalName()))\n\t\t\tcontinue\n\t\t}\n\t\texistingLockPackage := lockfile.Packages[pkg.Raw]\n\t\tif lockPackage.Version == existingLockPackage.Version {\n\t\t\tcontinue\n\t\t}\n\n\t\toutdatedPackages[pkg.Versioned()] = UpdateVersion{Current: existingLockPackage.Version, Latest: lockPackage.Version}\n\t}\n\n\tfor _, warning := range warnings {\n\t\tfmt.Fprintf(d.stderr, \"%s\\n\", warning)\n\t}\n\n\treturn outdatedPackages, nil\n}\n\n// Add adds the `pkgs` to the config (i.e. devbox.json) and nix profile for this\n// devbox project\nfunc (d *Devbox) Add(ctx context.Context, pkgsNames []string, opts devopt.AddOpts) error {\n\tctx, task := trace.NewTask(ctx, \"devboxAdd\")\n\tdefer task.End()\n\n\t// Track which packages had no changes so we can report that to the user.\n\tunchangedPackageNames := []string{}\n\n\t// Only add packages that are not already in config. If same canonical exists,\n\t// replace it.\n\tpkgs := devpkg.PackagesFromStringsWithOptions(lo.Uniq(pkgsNames), d.lockfile, opts)\n\n\t// addedPackageNames keeps track of the possibly transformed (versioned)\n\t// names of added packages (even if they are already in config). We use this\n\t// to know the exact name to mark as allowed insecure later on.\n\taddedPackageNames := []string{}\n\texistingPackageNames := lo.Map(\n\t\td.cfg.Root.TopLevelPackages(), func(p configfile.Package, _ int) string {\n\t\t\treturn p.VersionedName()\n\t\t})\n\tfor _, pkg := range pkgs {\n\t\t// If exact versioned package is already in the config, we can skip the\n\t\t// next loop that only deals with newPackages.\n\t\tif slices.Contains(existingPackageNames, pkg.Versioned()) {\n\t\t\t// But we still need to add to addedPackageNames. See its comment.\n\t\t\taddedPackageNames = append(addedPackageNames, pkg.Versioned())\n\t\t\tunchangedPackageNames = append(unchangedPackageNames, pkg.Versioned())\n\t\t\tux.Finfof(d.stderr, \"Package %q already in devbox.json\\n\", pkg.Versioned())\n\t\t\tcontinue\n\t\t}\n\n\t\t// On the other hand, if there's a package with same canonical name, replace\n\t\t// it. Ignore error (which is either missing or more than one). We search by\n\t\t// CanonicalName so any legacy or versioned packages will be removed if they\n\t\t// match.\n\t\tfound, _ := d.findPackageByName(pkg.CanonicalName())\n\t\tif found != nil {\n\t\t\tux.Finfof(d.stderr, \"Replacing package %q in devbox.json\\n\", found.Raw)\n\t\t\tif err := d.Remove(ctx, found.Raw); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// validate that the versioned package exists in the search endpoint.\n\t\t// if not, fallback to legacy vanilla nix.\n\t\tversionedPkg := devpkg.PackageFromStringWithOptions(pkg.Versioned(), d.lockfile, opts)\n\n\t\tpackageNameForConfig := pkg.Raw\n\t\tok, err := versionedPkg.ValidateExists(ctx)\n\t\tif (err == nil && ok) || errors.Is(err, devpkg.ErrCannotBuildPackageOnSystem) {\n\t\t\t// Only use versioned if it exists in search. We can disregard the error\n\t\t\t// about not building on the current system, since user's can continue\n\t\t\t// via --exclude-platform flag.\n\t\t\tpackageNameForConfig = pkg.Versioned()\n\t\t} else if !versionedPkg.IsDevboxPackage {\n\t\t\t// This means it didn't validate and we don't want to fallback to legacy\n\t\t\t// Just propagate the error.\n\t\t\treturn err\n\t\t} else {\n\t\t\tinstallable := flake.Installable{\n\t\t\t\tRef:      d.lockfile.Stdenv(),\n\t\t\t\tAttrPath: pkg.Raw,\n\t\t\t}\n\t\t\t_, err := nix.Search(installable.String())\n\t\t\tif err != nil {\n\t\t\t\t// This means it looked like a devbox package or attribute path, but we\n\t\t\t\t// could not find it in search or in the legacy nixpkgs path.\n\t\t\t\treturn usererr.New(\"Package %s not found\", pkg.Raw)\n\t\t\t}\n\t\t}\n\n\t\tux.Finfof(d.stderr, \"Adding package %q to devbox.json\\n\", packageNameForConfig)\n\t\td.cfg.PackageMutator().Add(packageNameForConfig)\n\t\taddedPackageNames = append(addedPackageNames, packageNameForConfig)\n\t}\n\n\t// Options must be set before ensureStateIsUpToDate. See comment in function\n\tif err := d.setPackageOptions(addedPackageNames, opts); err != nil {\n\t\treturn err\n\t}\n\n\tif err := d.ensureStateIsUpToDate(ctx, install); err != nil {\n\t\treturn usererr.WithUserMessage(err, \"There was an error installing nix packages\")\n\t}\n\n\tif err := d.saveCfg(); err != nil {\n\t\treturn err\n\t}\n\n\treturn d.printPostAddMessage(ctx, pkgs, unchangedPackageNames, opts)\n}\n\nfunc (d *Devbox) setPackageOptions(pkgs []string, opts devopt.AddOpts) error {\n\tfor _, pkg := range pkgs {\n\t\tif err := d.cfg.PackageMutator().AddPlatforms(\n\t\t\td.stderr, pkg, opts.Platforms); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := d.cfg.PackageMutator().ExcludePlatforms(\n\t\t\td.stderr, pkg, opts.ExcludePlatforms); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := d.cfg.PackageMutator().SetDisablePlugin(\n\t\t\tpkg, opts.DisablePlugin); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif opts.Patch != \"\" {\n\t\t\tif err := d.cfg.PackageMutator().SetPatch(\n\t\t\t\tpkg, configfile.PatchMode(opts.Patch)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif err := d.cfg.PackageMutator().SetOutputs(\n\t\t\td.stderr, pkg, opts.Outputs); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := d.cfg.PackageMutator().SetAllowInsecure(\n\t\t\td.stderr, pkg, opts.AllowInsecure); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Devbox) printPostAddMessage(\n\tctx context.Context,\n\tpkgs []*devpkg.Package,\n\tunchangedPackageNames []string,\n\topts devopt.AddOpts,\n) error {\n\tfor _, input := range pkgs {\n\t\tif readme, err := plugin.Readme(\n\t\t\tctx,\n\t\t\tinput,\n\t\t\td.projectDir,\n\t\t\tfalse /*markdown*/); err != nil {\n\t\t\treturn err\n\t\t} else if readme != \"\" {\n\t\t\tfmt.Fprintf(d.stderr, \"%s\\n\", readme)\n\t\t}\n\t}\n\n\tif len(opts.Platforms) == 0 && len(opts.ExcludePlatforms) == 0 && len(opts.Outputs) == 0 && len(opts.AllowInsecure) == 0 {\n\t\tif len(unchangedPackageNames) == 1 {\n\t\t\tux.Finfof(d.stderr, \"Package %q was already in devbox.json and was not modified\\n\", unchangedPackageNames[0])\n\t\t} else if len(unchangedPackageNames) > 1 {\n\t\t\tux.Finfof(d.stderr, \"Packages %s were already in devbox.json and were not modified\\n\",\n\t\t\t\tstrings.Join(unchangedPackageNames, \", \"),\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\n// Remove removes the `pkgs` from the config (i.e. devbox.json) and nix profile\n// for this devbox project\nfunc (d *Devbox) Remove(ctx context.Context, pkgs ...string) error {\n\tctx, task := trace.NewTask(ctx, \"devboxRemove\")\n\tdefer task.End()\n\n\tpackagesToUninstall := []string{}\n\tmissingPkgs := []string{}\n\tfor _, pkg := range lo.Uniq(pkgs) {\n\t\tfound, _ := d.findPackageByName(pkg)\n\t\tif found != nil {\n\t\t\tpackagesToUninstall = append(packagesToUninstall, found.Raw)\n\t\t\td.cfg.PackageMutator().Remove(found.Raw)\n\t\t} else {\n\t\t\tmissingPkgs = append(missingPkgs, pkg)\n\t\t}\n\t}\n\n\tif len(missingPkgs) > 0 {\n\t\tux.Fwarningf(\n\t\t\td.stderr,\n\t\t\t\"the following packages were not found in your devbox.json: %s\\n\",\n\t\t\tstrings.Join(missingPkgs, \", \"),\n\t\t)\n\t}\n\n\tif err := plugin.Remove(d.projectDir, packagesToUninstall); err != nil {\n\t\treturn err\n\t}\n\n\t// this will clean up the now-extra package from nix profile and the lockfile\n\tif err := d.ensureStateIsUpToDate(ctx, uninstall); err != nil {\n\t\treturn err\n\t}\n\n\treturn d.saveCfg()\n}\n\n// installMode is an enum for helping with ensureStateIsUpToDate implementation\ntype installMode string\n\nconst (\n\tinstall   installMode = \"install\"\n\tuninstall installMode = \"uninstall\"\n\t// update is both install new package version and uninstall old package version\n\tupdate    installMode = \"update\"\n\tensure    installMode = \"ensure\"\n\tnoInstall installMode = \"noInstall\"\n)\n\n// ensureStateIsUpToDate ensures the Devbox project state is up to date.\n// Namely:\n//  1. Packages are installed, in nix-profile or runx.\n//     Extraneous packages are removed (references purged, not uninstalled).\n//  2. Plugins are installed\n//  3. Files for devbox shellenv are generated\n//  4. The Devbox environment is re-computed, if necessary, and cached\n//  5. Lockfile is synced\n//\n// The `mode` is used for:\n// 1. Skipping certain operations that may not apply.\n// 2. User messaging to explain what operations are happening, because this function may take time to execute.\nfunc (d *Devbox) ensureStateIsUpToDate(ctx context.Context, mode installMode) error {\n\tdefer trace.StartRegion(ctx, \"devboxEnsureStateIsUpToDate\").End()\n\tdefer debug.FunctionTimer().End()\n\n\tupToDate, err := d.lockfile.IsUpToDateAndInstalled(isFishShell())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// if mode is install or uninstall, then we need to compute some state\n\t// like updating the flake or installing packages locally, so must continue\n\t// below\n\tif mode == ensure {\n\t\t// if mode is ensure and we are up to date, then we can skip the rest\n\t\tif upToDate {\n\t\t\treturn nil\n\t\t}\n\t\tux.Finfof(d.stderr, \"Ensuring packages are installed.\\n\")\n\t}\n\n\tif mode != ensure {\n\t\t// Reload includes because added/removed packages might change plugins. Cases:\n\t\t// * New package adds built-in plugin. We wanna make sure the plugin is in config.\n\t\t// * Remove built-in plugin that installs multiple packages (e.g. nginx). We wanna clear them\n\t\t// up so they get removed from lockfile in updateLockfile\n\t\tif err = d.cfg.LoadRecursive(d.lockfile); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif mode == install || mode == update || mode == ensure {\n\t\tif err := d.installPackages(ctx, mode); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\trecomputeState := mode == ensure || d.IsEnvEnabled()\n\tif recomputeState {\n\t\tif err := d.recomputeState(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// If we're in a devbox shell (global or project), then the environment might\n\t// be out of date after the user installs something. If have direnv active\n\t// it should reload automatically so we don't need to refresh.\n\tif d.IsEnvEnabled() && !upToDate && !d.IsDirenvActive() {\n\t\tux.FHidableWarning(\n\t\t\tctx,\n\t\t\td.stderr,\n\t\t\tStateOutOfDateMessage,\n\t\t\td.RefreshAliasOrCommand(),\n\t\t)\n\t}\n\n\treturn d.updateLockfile(recomputeState)\n}\n\n// updateLockfile will ensure devbox.lock is up to date with the current state of the project.update\n// If recomputeState is true, then we will also update the state.json file.\nfunc (d *Devbox) updateLockfile(recomputeState bool) error {\n\t// Ensure we clean out packages that are no longer needed.\n\td.lockfile.Tidy()\n\n\t// Update lockfile with new packages that are not to be installed\n\tfor _, pkg := range d.AllPackages() {\n\t\tif err := pkg.EnsureUninstallableIsInLockfile(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Update plugin versions in lockfile.\n\tfor _, pluginConfig := range d.Config().IncludedPluginConfigs() {\n\t\tif err := d.PluginManager().UpdateLockfileVersion(pluginConfig); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Save the lockfile at the very end, after all other operations were successful.\n\tif err := d.lockfile.Save(); err != nil {\n\t\treturn err\n\t}\n\n\t// If we are recomputing state, then we need to update the local.lock file.\n\t// If not, we leave the local.lock in a stale state, so that state is recomputed\n\t// on the next ensureStateIsUpToDate call with mode=ensure.\n\tif recomputeState {\n\t\tconfigHash, err := d.ConfigHash()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn lock.UpdateAndSaveStateHashFile(lock.UpdateStateHashFileArgs{\n\t\t\tProjectDir: d.projectDir,\n\t\t\tConfigHash: configHash,\n\t\t\tIsFish:     isFishShell(),\n\t\t})\n\t}\n\treturn nil\n}\n\n// recomputeState updates the local state comprising of:\n// - plugins directories\n// - devbox.lock file\n// - the generated flake\n// - the nix-profile\nfunc (d *Devbox) recomputeState(ctx context.Context) error {\n\tdefer debug.FunctionTimer().End()\n\tif err := shellgen.GenerateForPrintEnv(ctx, d); err != nil {\n\t\treturn err\n\t}\n\n\t// TODO: should this be moved into GenerateForPrintEnv?\n\t// OR into a plugin.GenerateFiles() along with d.pluginManager().Create()?\n\tif err := plugin.RemoveInvalidSymlinks(d.projectDir); err != nil {\n\t\treturn err\n\t}\n\n\treturn d.syncNixProfileFromFlake(ctx)\n}\n\nfunc (d *Devbox) profilePath() (string, error) {\n\tabsPath := filepath.Join(d.projectDir, nix.ProfilePath)\n\n\tif err := resetProfileDirForFlakes(absPath); err != nil {\n\t\tslog.Error(\"resetProfileDirForFlakes error\", \"err\", err)\n\t}\n\n\treturn absPath, errors.WithStack(os.MkdirAll(filepath.Dir(absPath), 0o755))\n}\n\nvar resetCheckDone = false\n\n// resetProfileDirForFlakes ensures the profileDir directory is cleared of old\n// state if the Flakes feature has been changed, from the previous execution of a devbox command.\nfunc resetProfileDirForFlakes(profileDir string) (err error) {\n\tif resetCheckDone {\n\t\treturn nil\n\t}\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tresetCheckDone = true\n\t\t}\n\t}()\n\n\tdir, err := filepath.EvalSymlinks(profileDir)\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil\n\t}\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// older nix profiles have a manifest.nix file present\n\t_, err = os.Stat(filepath.Join(dir, \"manifest.nix\"))\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil\n\t}\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn errors.WithStack(os.Remove(profileDir))\n}\n\nfunc (d *Devbox) installPackages(ctx context.Context, mode installMode) error {\n\tdefer debug.FunctionTimer().End()\n\t// Create plugin directories first because packages might need them\n\tfor _, pluginConfig := range d.Config().IncludedPluginConfigs() {\n\t\tif err := d.PluginManager().CreateFilesForConfig(pluginConfig); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := d.installNixPackagesToStore(ctx, mode); err != nil {\n\t\tif caches, _ := nixcache.CachedReadCaches(ctx); len(caches) > 0 {\n\t\t\terr = d.handleInstallFailure(ctx, mode)\n\t\t}\n\t\treturn err\n\t}\n\n\treturn d.InstallRunXPackages(ctx)\n}\n\nfunc (d *Devbox) handleInstallFailure(ctx context.Context, mode installMode) error {\n\tux.Fwarningf(d.stderr, \"Failed to build from cache, building from source.\\n\")\n\ttelemetry.Event(telemetry.EventNixBuildWithSubstitutersFailed, telemetry.Metadata{\n\t\tPackages: lo.Map(\n\t\t\td.InstallablePackages(), func(p *devpkg.Package, _ int) string { return p.Raw }),\n\t})\n\tnixcache.DisableReadCaches()\n\tdevpkg.ClearNarInfoCache()\n\treturn d.installNixPackagesToStore(ctx, mode)\n}\n\nfunc (d *Devbox) InstallRunXPackages(ctx context.Context) error {\n\tfor _, pkg := range lo.Filter(d.InstallablePackages(), devpkg.IsRunX) {\n\t\tlockedPkg, err := d.lockfile.Resolve(pkg.Raw)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, err := pkgtype.RunXClient().Install(\n\t\t\tctx,\n\t\t\tlockedPkg.Resolved,\n\t\t); err != nil {\n\t\t\treturn fmt.Errorf(\"error installing runx package %s: %w\", pkg, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// installNixPackagesToStore will install all the packages in the nix store, if\n// mode is install or update, and we're not in a devbox environment.\n// This is done by running `nix build` on the flake. We do this so that the\n// packages will be available in the nix store when computing the devbox environment\n// and installing in the nix profile (even if offline).\nfunc (d *Devbox) installNixPackagesToStore(ctx context.Context, mode installMode) error {\n\tdefer debug.FunctionTimer().End()\n\tpackages, err := d.packagesToInstallInStore(ctx, mode)\n\tif err != nil || len(packages) == 0 {\n\t\treturn err\n\t}\n\n\t// --no-link to avoid generating the result objects\n\tflags := []string{\"--no-link\"}\n\tif mode == update {\n\t\tflags = append(flags, \"--refresh\")\n\t}\n\n\targs := &nix.BuildArgs{\n\t\tFlags:  flags,\n\t\tWriter: d.stderr,\n\t}\n\terr = d.appendExtraSubstituters(ctx, args)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpackageNames := lo.Map(\n\t\tpackages,\n\t\tfunc(p *devpkg.Package, _ int) string { return p.Raw },\n\t)\n\tux.Finfof(\n\t\td.stderr,\n\t\t\"Installing the following packages to the nix store: %s\\n\",\n\t\tstrings.Join(packageNames, \", \"),\n\t)\n\n\tinstallables := map[bool][]string{false: {}, true: {}}\n\tfor _, pkg := range packages {\n\t\tpkgInstallables, err := pkg.Installables()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tinstallables[pkg.HasAllowInsecure()] = append(\n\t\t\tinstallables[pkg.HasAllowInsecure()],\n\t\t\tpkgInstallables...,\n\t\t)\n\t}\n\n\tfor allowInsecure, installables := range installables {\n\t\tif len(installables) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\teventStart := time.Now()\n\t\targs.AllowInsecure = allowInsecure\n\t\terr = nix.Build(ctx, args, installables...)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttelemetry.Event(telemetry.EventNixBuildSuccess, telemetry.Metadata{\n\t\t\tEventStart: eventStart,\n\t\t\tPackages:   packageNames,\n\t\t})\n\t}\n\n\treturn nil\n}\n\nfunc (d *Devbox) appendExtraSubstituters(ctx context.Context, args *nix.BuildArgs) error {\n\tcreds, err := nixcache.CachedCredentials(ctx)\n\tif errors.Is(err, auth.ErrNotLoggedIn) {\n\t\treturn nil\n\t}\n\tif err != nil {\n\t\tux.Fwarningf(d.stderr, \"Devbox was unable to authenticate with the Jetify Nix cache. Some packages might be built from source.\\n\")\n\t\treturn nil //nolint:nilerr\n\t}\n\n\tcaches, err := nixcache.CachedReadCaches(ctx)\n\tif err != nil {\n\t\tslog.Error(\"error getting list of caches from the Jetify API, assuming the user doesn't have access to any\", \"err\", err)\n\t\treturn nil\n\t}\n\tif len(caches) == 0 {\n\t\treturn nil\n\t}\n\n\terr = nixcache.Configure(ctx)\n\tif errors.Is(err, setup.ErrAlreadyRefused) {\n\t\tslog.Debug(\"user previously refused to configure nix cache, not re-prompting\")\n\t\treturn nil\n\t}\n\tif errors.Is(err, setup.ErrUserRefused) {\n\t\tux.Finfof(d.stderr, \"Skipping cache setup. Run `devbox cache configure` to enable the cache at a later time.\\n\")\n\t\treturn nil\n\t}\n\tvar daemonErr *nix.DaemonError\n\tif errors.As(err, &daemonErr) {\n\t\t// Error here to give the user a chance to restart the daemon.\n\t\treturn usererr.New(\"Devbox configured Nix to use a new cache. Please restart the Nix daemon and re-run Devbox.\")\n\t}\n\t// Other errors indicate we couldn't update nix.conf, so just warn and\n\t// continue by building from source if necessary.\n\tif err != nil {\n\t\tslog.Error(\"error configuring nix cache\", \"err\", err)\n\t\tux.Fwarningf(d.stderr, \"Devbox was unable to configure Nix to use the Jetify Nix cache. Some packages might be built from source.\\n\")\n\t\treturn nil\n\t}\n\n\tfor _, cache := range caches {\n\t\targs.ExtraSubstituters = append(args.ExtraSubstituters, cache.GetUri())\n\t}\n\targs.Env = append(args.Env, creds.Env()...)\n\treturn nil\n}\n\nfunc (d *Devbox) packagesToInstallInStore(ctx context.Context, mode installMode) ([]*devpkg.Package, error) {\n\tdefer debug.FunctionTimer().End()\n\t// First, get and prepare all the packages that must be installed in this project\n\t// and remove non-nix packages from the list\n\tpackages := lo.Filter(d.InstallablePackages(), devpkg.IsNix)\n\tif err := devpkg.FillNarInfoCache(ctx, packages...); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Second, check which packages are not in the nix store\n\tpackagesToInstall := []*devpkg.Package{}\n\tstorePathsForPackage := map[*devpkg.Package][]string{}\n\tfor _, pkg := range packages {\n\t\tif mode == update {\n\t\t\tpackagesToInstall = append(packagesToInstall, pkg)\n\t\t\tcontinue\n\t\t}\n\t\tvar err error\n\t\tstorePathsForPackage[pkg], err = pkg.GetStorePaths(ctx, d.stderr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Batch this for perf\n\tstorePathMap, err := nix.StorePathsAreInStore(ctx, lo.Flatten(lo.Values(storePathsForPackage)))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor pkg, storePaths := range storePathsForPackage {\n\t\tfor _, storePath := range storePaths {\n\t\t\tif !storePathMap[storePath] {\n\t\t\t\tpackagesToInstall = append(packagesToInstall, pkg)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn lo.Uniq(packagesToInstall), nil\n}\n\n// moveAllowInsecureFromLockfile will modernize a Devbox project by moving the allow_insecure: boolean\n// setting from the devbox.lock file to the corresponding package in devbox.json.\n//\n// NOTE: ideally, this function would be in devconfig, but it leads to an import cycle with devpkg, so\n// leaving in this \"top-level\" devbox package where we can import devconfig, devpkg and lock.\nfunc (d *Devbox) moveAllowInsecureFromLockfile(writer io.Writer, lockfile *lock.File, cfg *devconfig.Config) error {\n\tif !lockfile.HasAllowInsecurePackages() {\n\t\treturn nil\n\t}\n\n\tinsecurePackages := []string{}\n\tfor name, pkg := range lockfile.Packages {\n\t\tif pkg.AllowInsecure {\n\t\t\tinsecurePackages = append(insecurePackages, name)\n\t\t}\n\t\tpkg.AllowInsecure = false\n\t}\n\n\t// Set the devbox.json packages to allow_insecure\n\tfor _, versionedName := range insecurePackages {\n\t\tpkg := devpkg.PackageFromStringWithDefaults(versionedName, lockfile)\n\t\tstoreName, err := pkg.StoreName()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get package's store name for package %q with error %w\", versionedName, err)\n\t\t}\n\t\tif err := cfg.PackageMutator().SetAllowInsecure(writer, versionedName, []string{storeName}); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to set allow_insecure in devbox.json for package %q with error %w\", versionedName, err)\n\t\t}\n\t}\n\n\tif err := d.saveCfg(); err != nil {\n\t\treturn err\n\t}\n\n\t// Now, clear it from the lockfile\n\tif err := lockfile.Save(); err != nil {\n\t\treturn err\n\t}\n\n\tux.Finfof(\n\t\twriter,\n\t\t\"Modernized the allow_insecure setting for package %q by moving it from devbox.lock to devbox.json. Please commit the changes.\\n\",\n\t\tstrings.Join(insecurePackages, \", \"),\n\t)\n\n\treturn nil\n}\n\nfunc (d *Devbox) FixMissingStorePaths(ctx context.Context) error {\n\tpackages := d.InstallablePackages()\n\tfor _, pkg := range packages {\n\t\tif !pkg.IsDevboxPackage || pkg.IsRunX() {\n\t\t\tcontinue\n\t\t}\n\t\texistingStorePaths, err := pkg.GetResolvedStorePaths()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif len(existingStorePaths) > 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tinstallables, err := pkg.Installables()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\toutputs := []lock.Output{}\n\t\tfor _, installable := range installables {\n\t\t\tstorePaths, err := nix.StorePathsFromInstallable(ctx, installable, pkg.HasAllowInsecure())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif len(storePaths) == 0 {\n\t\t\t\treturn fmt.Errorf(\"no store paths found for package %s\", pkg.Raw)\n\t\t\t}\n\t\t\tfor _, storePath := range storePaths {\n\t\t\t\tparts := nix.NewStorePathParts(storePath)\n\t\t\t\toutputs = append(outputs, lock.Output{\n\t\t\t\t\tPath: storePath,\n\t\t\t\t\tName: parts.Output,\n\t\t\t\t\t// Ugh, not sure this is true, but it's more true than not.\n\t\t\t\t\tDefault: true,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif err = d.lockfile.SetOutputsForPackage(pkg.Raw, outputs); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn d.lockfile.Save()\n}\n"
  },
  {
    "path": "internal/devbox/providers/identity/identity.go",
    "content": "package identity\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/go-jose/go-jose/v4\"\n\t\"github.com/go-jose/go-jose/v4/jwt\"\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/ux\"\n\t\"go.jetify.com/pkg/api\"\n\t\"go.jetify.com/pkg/auth\"\n\t\"go.jetify.com/pkg/auth/session\"\n\t\"go.jetify.com/pkg/ids\"\n\t\"go.jetify.com/typeid/v2\"\n\t\"golang.org/x/oauth2\"\n)\n\n// Common redirect URLs for use with [AuthClient].\nvar (\n\t// AuthRedirectDefault redirects to a generic success page.\n\tAuthRedirectDefault = build.SuccessRedirect()\n\n\t// AuthRedirectCache redirects to the \"Cache\" tab in the dashboard for\n\t// the authenticated organization.\n\tAuthRedirectCache = path.Join(build.DashboardHostname(), \"team\", \"cache\")\n)\n\nvar scopes = []string{\"openid\", \"offline_access\", \"email\", \"profile\"}\n\nvar cachedAccessTokenFromAPIToken *session.Token\n\n// parseAPIToken parses an API token string following the same pattern as other Parse functions\nfunc parseAPIToken(s string) (ids.APIToken, error) {\n\tvar zero ids.APIToken\n\ttid, err := typeid.Parse(s)\n\tif err != nil {\n\t\treturn zero, err\n\t}\n\tif tid.Prefix() != ids.APITokenPrefix {\n\t\treturn zero, fmt.Errorf(\"invalid api_token ID: %s\", s)\n\t}\n\treturn ids.APIToken{TypeID: tid}, nil\n}\n\nfunc GenSession(ctx context.Context) (*session.Token, error) {\n\tif t, err := getAccessTokenFromAPIToken(ctx); err != nil || t != nil {\n\t\treturn t, err\n\t}\n\n\tc, err := AuthClient(AuthRedirectDefault)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttok, err := c.GetSession(ctx)\n\tif IsRefreshTokenError(err) {\n\t\tux.Fwarningf(os.Stderr, \"Your session is expired. Please login again.\\n\")\n\t\treturn c.LoginFlow()\n\t}\n\treturn tok, err\n}\n\nfunc Peek() (*session.Token, error) {\n\tif cachedAccessTokenFromAPIToken != nil {\n\t\treturn cachedAccessTokenFromAPIToken, nil\n\t}\n\n\tc, err := AuthClient(AuthRedirectDefault)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttokens, err := c.GetSessions()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(tokens) == 0 {\n\t\treturn nil, auth.ErrNotLoggedIn\n\t}\n\n\treturn tokens[0].Peek(), nil\n}\n\n// AuthClient returns a new client that redirects to a given URL upon success.\nfunc AuthClient(redirect string) (*auth.Client, error) {\n\treturn auth.NewClient(\n\t\tbuild.Issuer(),\n\t\tbuild.ClientID(),\n\t\tscopes,\n\t\tredirect,\n\t\tbuild.Audience(),\n\t)\n}\n\nfunc getAccessTokenFromAPIToken(\n\tctx context.Context,\n) (*session.Token, error) {\n\tif cachedAccessTokenFromAPIToken == nil {\n\t\tapiTokenRaw := os.Getenv(\"DEVBOX_API_TOKEN\")\n\t\tif apiTokenRaw == \"\" {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tapiToken, err := parseAPIToken(apiTokenRaw)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tapiClient := api.NewClient(ctx, build.JetpackAPIHost(), &session.Token{})\n\t\tresponse, err := apiClient.GetAccessToken(ctx, apiToken)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// This is not the greatest. This token is missing id, refresh, etc.\n\t\t// It may be better to change api.NewClient() to take a token string instead.\n\t\tcachedAccessTokenFromAPIToken = &session.Token{\n\t\t\tToken: oauth2.Token{\n\t\t\t\tAccessToken: response.AccessToken,\n\t\t\t},\n\t\t}\n\t}\n\n\treturn cachedAccessTokenFromAPIToken, nil\n}\n\nfunc GetOrgSlug(ctx context.Context) (string, error) {\n\ttok, err := GenSession(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif tok.IDToken == \"\" {\n\t\treturn \"\", errors.New(\"ID token is not present\")\n\t}\n\n\tjwt, err := jwt.ParseSigned(tok.IDToken, []jose.SignatureAlgorithm{jose.RS256})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclaims := map[string]any{}\n\tif err = jwt.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn claims[\"org_trusted_metadata\"].(map[string]any)[\"slug\"].(string), nil\n}\n\n// invalid_grant or invalid_request usually means the refresh token is expired, revoked, or\n// malformed. this belongs in opensource/pkg/auth\nfunc IsRefreshTokenError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn strings.Contains(err.Error(), \"invalid_grant\") ||\n\t\tstrings.Contains(err.Error(), \"invalid_request\")\n}\n"
  },
  {
    "path": "internal/devbox/providers/nixcache/nixcache.go",
    "content": "package nixcache\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/cachehash\"\n\t\"go.jetify.com/devbox/internal/devbox/providers/identity\"\n\t\"go.jetify.com/devbox/internal/goutil\"\n\t\"go.jetify.com/devbox/internal/redact\"\n\t\"go.jetify.com/pkg/api\"\n\tnixv1alpha1 \"go.jetify.com/pkg/api/gen/priv/nix/v1alpha1\"\n\t\"go.jetify.com/pkg/auth\"\n\t\"go.jetify.com/pkg/auth/session\"\n\t\"go.jetify.com/pkg/filecache\"\n)\n\nvar cachedCredentials = goutil.OnceValuesWithContext(\n\tfunc(ctx context.Context) (AWSCredentials, error) {\n\t\t// Adding version to caches to avoid conflicts if we want to update the schema\n\t\t// or while working on dev.\n\t\tcache := filecache.New[AWSCredentials](fmt.Sprintf(\n\t\t\t\"devbox/%s/providers/nixcache\",\n\t\t\tbuild.Version,\n\t\t))\n\t\ttoken, err := identity.GenSession(ctx)\n\t\tif err != nil {\n\t\t\treturn AWSCredentials{}, err\n\t\t}\n\t\tcreds, err := cache.GetOrSetWithTime(\n\t\t\t\"credentials-\"+getSubOrAccessTokenHash(token),\n\t\t\tfunc() (AWSCredentials, time.Time, error) {\n\t\t\t\ttoken, err := identity.GenSession(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn AWSCredentials{}, time.Time{}, err\n\t\t\t\t}\n\t\t\t\tclient := api.NewClient(ctx, build.JetpackAPIHost(), token)\n\t\t\t\tcreds, err := client.GetAWSCredentials(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn AWSCredentials{}, time.Time{}, err\n\t\t\t\t}\n\t\t\t\texp := time.Time{}\n\t\t\t\tif t := creds.GetExpiration(); t != nil {\n\t\t\t\t\texp = t.AsTime()\n\t\t\t\t}\n\t\t\t\treturn newAWSCredentials(creds), exp, nil\n\t\t\t},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn AWSCredentials{}, redact.Errorf(\"nixcache: get credentials: %w\", redact.Safe(err))\n\t\t}\n\t\treturn creds, nil\n\t})\n\n// CachedCredentials fetches short-lived credentials that grant access to the user's\n// private cache.\nfunc CachedCredentials(ctx context.Context) (AWSCredentials, error) {\n\treturn cachedCredentials.Do(ctx)\n}\n\n// Caches return the list of caches the user has access to. If user is not\n// logged in, it returns nil, nil. (no error).\nfunc Caches(\n\tctx context.Context,\n) ([]*nixv1alpha1.NixBinCache, error) {\n\ttoken, err := identity.GenSession(ctx)\n\tif errors.Is(err, auth.ErrNotLoggedIn) {\n\t\treturn nil, nil\n\t} else if err != nil {\n\t\treturn nil, err\n\t}\n\tclient := api.NewClient(ctx, build.JetpackAPIHost(), token)\n\tresp, err := client.GetBinCache(ctx)\n\tif err != nil {\n\t\treturn nil, redact.Errorf(\"nixcache: get caches: %w\", redact.Safe(err))\n\t}\n\treturn resp.GetCaches(), nil\n}\n\nvar cachedReadCaches = goutil.OnceValuesWithContext(\n\tfunc(ctx context.Context) ([]*nixv1alpha1.NixBinCache, error) {\n\t\tcaches, err := Caches(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn slices.DeleteFunc(caches, func(c *nixv1alpha1.NixBinCache) bool {\n\t\t\treturn !slices.Contains(c.GetPermissions(), nixv1alpha1.Permission_PERMISSION_READ)\n\t\t}), nil\n\t},\n)\n\nfunc CachedReadCaches(ctx context.Context) ([]*nixv1alpha1.NixBinCache, error) {\n\treturn cachedReadCaches.Do(ctx)\n}\n\nfunc DisableReadCaches() {\n\tcachedReadCaches = goutil.OnceValuesWithContext(\n\t\tfunc(ctx context.Context) ([]*nixv1alpha1.NixBinCache, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t)\n}\n\nfunc WriteCaches(\n\tctx context.Context,\n) ([]*nixv1alpha1.NixBinCache, error) {\n\tcaches, err := Caches(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn lo.Filter(caches, func(c *nixv1alpha1.NixBinCache, _ int) bool {\n\t\treturn slices.Contains(\n\t\t\tc.GetPermissions(),\n\t\t\tnixv1alpha1.Permission_PERMISSION_WRITE,\n\t\t)\n\t}), nil\n}\n\nfunc S3Client(\n\tctx context.Context,\n) (*s3.Client, error) {\n\tcreds, err := CachedCredentials(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconfig, err := config.LoadDefaultConfig(\n\t\tctx,\n\t\tconfig.WithCredentialsProvider(\n\t\t\tcredentials.NewStaticCredentialsProvider(\n\t\t\t\tcreds.AccessKeyID,\n\t\t\t\tcreds.SecretAccessKey,\n\t\t\t\tcreds.SessionToken,\n\t\t\t),\n\t\t),\n\t)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\treturn s3.NewFromConfig(config), nil\n}\n\n// AWSCredentials are short-lived credentials that grant access to a private Nix\n// cache in S3. It marshals to JSON per the schema described in\n// `aws help config-vars` under \"Sourcing Credentials From External Processes\".\ntype AWSCredentials struct {\n\t// Version must always be 1.\n\tVersion         int       `json:\"Version\"`\n\tAccessKeyID     string    `json:\"AccessKeyId\"`\n\tSecretAccessKey string    `json:\"SecretAccessKey\"`\n\tSessionToken    string    `json:\"SessionToken\"`\n\tExpiration      time.Time `json:\"Expiration\"`\n}\n\nfunc newAWSCredentials(proto *nixv1alpha1.AWSCredentials) AWSCredentials {\n\tcreds := AWSCredentials{\n\t\tVersion:         1,\n\t\tAccessKeyID:     proto.AccessKeyId,\n\t\tSecretAccessKey: proto.SecretKey,\n\t\tSessionToken:    proto.SessionToken,\n\t}\n\tif proto.Expiration != nil {\n\t\tcreds.Expiration = proto.Expiration.AsTime()\n\t}\n\treturn creds\n}\n\n// Env returns the credentials as a slice of environment variables.\nfunc (a AWSCredentials) Env() []string {\n\treturn []string{\n\t\t\"AWS_ACCESS_KEY_ID=\" + a.AccessKeyID,\n\t\t\"AWS_SECRET_ACCESS_KEY=\" + a.SecretAccessKey,\n\t\t\"AWS_SESSION_TOKEN=\" + a.SessionToken,\n\t}\n}\n\nfunc getSubOrAccessTokenHash(token *session.Token) string {\n\t// We need this because the token is missing IDToken when used in CICD.\n\t// TODO: Implement AccessToken Parsing so we can extract sub form that.\n\tif token.IDClaims() != nil && token.IDClaims().Subject != \"\" {\n\t\treturn token.IDClaims().Subject\n\t}\n\treturn cachehash.Bytes([]byte(token.AccessToken))\n}\n"
  },
  {
    "path": "internal/devbox/providers/nixcache/setup.go",
    "content": "package nixcache\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/redact\"\n\t\"go.jetify.com/devbox/internal/setup\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\nconst setupKey = \"nixcache-setup\"\n\nfunc IsConfigured(ctx context.Context) bool {\n\tu, err := user.Current()\n\tif err != nil {\n\t\treturn false\n\t}\n\ttask := &setupTask{u.Username}\n\tstatus := setup.Status(ctx, setupKey, task)\n\treturn status == setup.TaskDone\n}\n\nfunc Configure(ctx context.Context) error {\n\tu, err := user.Current()\n\tif err != nil {\n\t\treturn redact.Errorf(\"nixcache: lookup current user: %v\", err)\n\t}\n\n\ttask := &setupTask{u.Username}\n\n\t// This function might be called from other Devbox commands\n\t// (such as devbox add), so we need to provide some context in the sudo\n\t// prompt.\n\tconst sudoPrompt = \"You're logged into a Devbox account, but Nix isn't setup to use your account's caches. \" +\n\t\t\"Allow sudo to configure Nix?\"\n\terr = setup.ConfirmRun(ctx, setupKey, task, sudoPrompt)\n\tif err != nil {\n\t\treturn redact.Errorf(\"nixcache: run setup: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc ConfigureReprompt(ctx context.Context, username string) error {\n\tsetup.Reset(setupKey)\n\ttask := &setupTask{username}\n\n\t// We're reprompting, so the user explicitly asked to configure the\n\t// cache. We can keep the sudo prompt short.\n\terr := setup.ConfirmRun(ctx, setupKey, task, \"Allow sudo to configure Nix?\")\n\tif err != nil {\n\t\treturn redact.Errorf(\"nixcache: run setup: %w\", err)\n\t}\n\treturn nil\n}\n\n// setupTask adds the user to Nix's trusted-users list and updates\n// ~root/.aws/config so that they can use their Devbox cache with the\n// Nix daemon.\ntype setupTask struct {\n\t// username is the OS username to trust.\n\tusername string\n}\n\nfunc (s *setupTask) NeedsRun(ctx context.Context, lastRun setup.RunInfo) bool {\n\tif _, err := nix.DaemonVersion(ctx); err != nil {\n\t\t// This looks like a single-user install, so no need to\n\t\t// configure the daemon or root's AWS credentials.\n\t\tslog.Error(\"nixcache: skipping setup: error connecting to nix daemon, assuming single-user install\", \"err\", err)\n\t\treturn false\n\t}\n\n\tif lastRun.Time.IsZero() {\n\t\tslog.Debug(\"nixcache: running setup: first time setup\")\n\t\treturn true\n\t}\n\tcfg, err := nix.CurrentConfig(ctx)\n\tif err != nil {\n\t\tslog.Error(\"nixcache: running setup: error getting current nix config, assuming user isn't trusted\", \"user\", s.username)\n\t\treturn true\n\t}\n\ttrusted, err := cfg.IsUserTrusted(ctx, s.username)\n\tif err != nil {\n\t\tslog.Error(\"nixcache: running setup: error checking if user is trusted, assuming they aren't\", \"user\", s.username)\n\t\treturn true\n\t}\n\tif !trusted {\n\t\tslog.Debug(\"nixcache: running setup: user isn't trusted\", \"user\", s.username)\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (s *setupTask) Run(ctx context.Context) error {\n\tran, err := setup.SudoDevbox(ctx, \"cache\", \"configure\", \"--user\", s.username)\n\tif ran || err != nil {\n\t\treturn err\n\t}\n\n\t// Update the AWS config before configuring and restarting the Nix\n\t// daemon.\n\terr = s.updateAWSConfig()\n\tif err != nil {\n\t\treturn redact.Errorf(\"update root aws config: %v\", err)\n\t}\n\n\ttrusted := false\n\tcfg, err := nix.CurrentConfig(ctx)\n\tif err == nil {\n\t\ttrusted, _ = cfg.IsUserTrusted(ctx, s.username)\n\t}\n\tif !trusted {\n\t\terr = nix.IncludeDevboxConfig(ctx, s.username)\n\t\tif errors.Is(err, nix.ErrUnknownServiceManager) {\n\t\t\tux.Fwarningf(os.Stderr, \"Devbox configured Nix to use a new cache. Please restart the Nix daemon and re-run Devbox.\\n\")\n\t\t} else if err != nil {\n\t\t\treturn redact.Errorf(\"update nix config: %v\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *setupTask) updateAWSConfig() error {\n\texe, err := devboxExecutable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tsudo, err := sudoExecutable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tconfigPath, err := rootAWSConfigPath()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Clear out and backup any existing .aws directory. We need to\n\t// do this with the entire directory and not just .aws/config\n\t// because there are other files that can affect credentials.\n\tbackup, err := backupDirectory(filepath.Dir(configPath))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tflag := os.O_WRONLY | os.O_CREATE | os.O_EXCL\n\tperm := fs.FileMode(0o644)\n\tconfig, err := os.OpenFile(configPath, flag, perm)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\t// Avoid os.MkdirAll because we shouldn't be creating anything\n\t\t// above the user's home directory.\n\t\tif err = os.Mkdir(filepath.Dir(configPath), 0o755); err != nil {\n\t\t\treturn redact.Errorf(\"create ~root/.aws directory: %v\", err)\n\t\t}\n\t\tconfig, err = os.OpenFile(configPath, flag, perm)\n\t}\n\tif err != nil {\n\t\treturn redact.Errorf(\"open ~root/.aws/config: %v\", err)\n\t}\n\tdefer config.Close()\n\n\t// TODO(gcurtis): it would be nice to use a non-default profile\n\t// if https://github.com/NixOS/nix/issues/5525 gets fixed.\n\theader := \"# This file was generated by Devbox.\\n\"\n\tif backup != \"\" {\n\t\theader += \"# The old .aws directory was moved to \" + backup + \".\\n\"\n\t}\n\t_, err = fmt.Fprintf(config, `%s\n[default]\n# sudo as the configured user so that their cached credential files have the\n# correct ownership.\ncredential_process = %s -u %s -i %s-- %s cache credentials\n`, header, sudo, s.username, propagatedEnv(), exe)\n\tif err != nil {\n\t\treturn redact.Errorf(\"write to ~root/.aws/config: %v\", err)\n\t}\n\tif err := config.Close(); err != nil {\n\t\treturn redact.Errorf(\"close ~root/.aws/config: %v\", err)\n\t}\n\treturn nil\n}\n\n// propagatedEnv returns a string of space-separated VAR=value pairs of\n// environment variables that should be propagated to the credential_process\n// command in ~root/.aws/config. This is especially important for CI because the\n// Nix daemon won't otherwise see any environment variables set by the job.\nfunc propagatedEnv() string {\n\tenvs := []string{\n\t\t\"DEVBOX_API_TOKEN\",\n\t\t\"DEVBOX_PROD\",\n\t\t\"DEVBOX_USE_VERSION\",\n\t\t\"XDG_CACHE_HOME\",\n\t\t\"XDG_CONFIG_DIRS\",\n\t\t\"XDG_CONFIG_HOME\",\n\t\t\"XDG_DATA_DIRS\",\n\t\t\"XDG_DATA_HOME\",\n\t\t\"XDG_RUNTIME_DIR\",\n\t\t\"XDG_STATE_HOME\",\n\t}\n\tstrb := strings.Builder{}\n\tfor _, name := range envs {\n\t\tval := os.Getenv(name)\n\t\tif val == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tnotPrintable := strings.ContainsFunc(val, func(r rune) bool {\n\t\t\treturn !unicode.IsPrint(r)\n\t\t})\n\t\tif notPrintable {\n\t\t\tslog.Debug(\"nixcache: not including environment variable in ~root/.aws/config because it contains nonprintable runes: %q=%q\", name, val)\n\t\t\tcontinue\n\t\t}\n\n\t\tstrb.WriteString(name)\n\t\tstrb.WriteString(`=\"`)\n\t\tfor _, r := range val {\n\t\t\tswitch r {\n\t\t\t// Special characters inside double quotes:\n\t\t\t// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03\n\t\t\tcase '$', '`', '\"', '\\\\':\n\t\t\t\tstrb.WriteByte('\\\\')\n\t\t\t}\n\t\t\tstrb.WriteRune(r)\n\t\t}\n\t\tstrb.WriteString(`\" `)\n\t}\n\treturn strb.String()\n}\n\n// rootAWSConfigPath returns the default AWS config path for the root user. In a\n// shell this is ~root/.aws/config.\nfunc rootAWSConfigPath() (string, error) {\n\tu, err := user.LookupId(\"0\")\n\tif err != nil {\n\t\treturn \"\", redact.Errorf(\"lookup root user: %s\", err)\n\t}\n\tif u.HomeDir == \"\" {\n\t\treturn \"\", redact.Errorf(\"empty root user home directory: %s\", u.Username, err)\n\t}\n\treturn filepath.Join(u.HomeDir, \".aws\", \"config\"), nil\n}\n\n// backupDirectory creates a backup of a directory and then deletes it. Upon\n// success, it returns the path to the backup copy.\nfunc backupDirectory(path string) (string, error) {\n\t// Remember this function is running as root, so be careful when\n\t// moving/creating/deleting things.\n\n\tpath = filepath.Clean(path)\n\tif path == \"/\" {\n\t\treturn \"\", redact.Errorf(\"refusing to backup root directory\")\n\t}\n\n\tbackup := fmt.Sprintf(\"%s-%d.bak\", path, time.Now().Unix())\n\terr := os.Rename(path, backup)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\t// No pre-existing .aws directory.\n\t\treturn \"\", nil\n\t}\n\tif err != nil {\n\t\treturn \"\", redact.Errorf(\"backup existing directory %s: %v\", path, err)\n\t}\n\treturn backup, nil\n}\n\n// devboxExecutable returns the path to the Devbox launcher script or the\n// current binary if the launcher is unavailable.\nfunc devboxExecutable() (string, error) {\n\tif exe := os.Getenv(envir.LauncherPath); exe != \"\" {\n\t\tif abs, err := filepath.Abs(exe); err == nil {\n\t\t\treturn abs, nil\n\t\t}\n\t}\n\n\texe, err := os.Executable()\n\tif err != nil {\n\t\treturn \"\", redact.Errorf(\"get path to devbox executable: %v\", err)\n\t}\n\treturn exe, nil\n}\n\n// sudoExecutable searches the PATH for sudo.\nfunc sudoExecutable() (string, error) {\n\tsudo, err := exec.LookPath(\"sudo\")\n\tif err != nil {\n\t\treturn \"\", redact.Errorf(\"get path to sudo executable: %v\", err)\n\t}\n\treturn sudo, nil\n}\n"
  },
  {
    "path": "internal/devbox/pure_shell.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devbox\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Creates a symlink for devbox in .devbox/bin\n// so that devbox can be available inside a pure shell\nfunc createDevboxSymlink(d *Devbox) error {\n\t// Get absolute path for where devbox is called\n\tdevboxPath, err := os.Executable()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to create devbox symlink. Devbox command won't be available inside the shell\")\n\t}\n\t// ensure .devbox/bin directory exists\n\tbinPath := dotdevboxBinPath(d)\n\tif err := os.MkdirAll(binPath, 0o755); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\t// Create a symlink between devbox and .devbox/bin\n\terr = os.Symlink(devboxPath, filepath.Join(binPath, \"devbox\"))\n\tif err != nil && !errors.Is(err, fs.ErrExist) {\n\t\treturn errors.Wrap(err, \"failed to create devbox symlink. Devbox command won't be available inside the shell\")\n\t}\n\treturn nil\n}\n\nfunc dotdevboxBinPath(d *Devbox) string {\n\treturn filepath.Join(d.ProjectDir(), \".devbox/bin\")\n}\n"
  },
  {
    "path": "internal/devbox/pushpull.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devbox\n\nimport (\n\t\"context\"\n\t\"runtime/trace\"\n\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/pullbox\"\n)\n\nfunc (d *Devbox) Pull(ctx context.Context, opts devopt.PullboxOpts) error {\n\tctx, task := trace.NewTask(ctx, \"devboxPull\")\n\tdefer task.End()\n\treturn pullbox.New(d, opts).Pull(ctx)\n}\n\nfunc (d *Devbox) Push(ctx context.Context, opts devopt.PullboxOpts) error {\n\tctx, task := trace.NewTask(ctx, \"devboxPush\")\n\tdefer task.End()\n\treturn pullbox.New(d, opts).Push(ctx)\n}\n"
  },
  {
    "path": "internal/devbox/refresh.go",
    "content": "package devbox\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc (d *Devbox) IsDirenvActive() bool {\n\treturn strings.TrimPrefix(os.Getenv(\"DIRENV_DIR\"), \"-\") == d.projectDir\n}\n\nfunc (d *Devbox) isRefreshAliasSet() bool {\n\treturn os.Getenv(d.refreshAliasEnvVar()) == d.refreshCmd()\n}\n\nfunc (d *Devbox) refreshAliasEnvVar() string {\n\treturn \"DEVBOX_REFRESH_ALIAS_\" + d.ProjectDirHash()\n}\n\nfunc (d *Devbox) isGlobal() bool {\n\tglobalPath, _ := GlobalDataPath()\n\treturn d.projectDir == globalPath\n}\n\n// In some cases (e.g. 2 non-global projects somehow active at the same time),\n// refresh might not match. This is a tiny edge case, so no need to make UX\n// great, we just print out the entire command.\nfunc (d *Devbox) RefreshAliasOrCommand() string {\n\tif !d.isRefreshAliasSet() {\n\t\t// even if alias is not set, it might still be set by the end of this process\n\t\treturn fmt.Sprintf(\"`%s` or `%s`\", d.refreshAliasName(), d.refreshCmd())\n\t}\n\treturn d.refreshAliasName()\n}\n\nfunc (d *Devbox) refreshAliasName() string {\n\tif d.isGlobal() {\n\t\treturn \"refresh-global\"\n\t}\n\treturn \"refresh\"\n}\n\nfunc (d *Devbox) refreshCmd() string {\n\tdevboxCmd := fmt.Sprintf(\"shellenv --preserve-path-stack -c %q\", d.projectDir)\n\tif d.isGlobal() {\n\t\tdevboxCmd = \"global shellenv --preserve-path-stack -r\"\n\t}\n\tif isFishShell() {\n\t\treturn fmt.Sprintf(`eval (devbox %s  | string collect)`, devboxCmd)\n\t}\n\treturn fmt.Sprintf(`eval \"$(devbox %s)\" && hash -r`, devboxCmd)\n}\n\nfunc (d *Devbox) refreshAlias() string {\n\tif isFishShell() {\n\t\treturn fmt.Sprintf(\n\t\t\t`if not type %[1]s >/dev/null 2>&1\n\texport %[2]s='%[3]s'\n\talias %[1]s='%[3]s'\nend`,\n\t\t\td.refreshAliasName(),\n\t\t\td.refreshAliasEnvVar(),\n\t\t\td.refreshCmd(),\n\t\t)\n\t}\n\treturn fmt.Sprintf(\n\t\t`if ! type %[1]s >/dev/null 2>&1; then\n\texport %[2]s='%[3]s'\n\talias %[1]s='%[3]s'\nfi`,\n\t\td.refreshAliasName(),\n\t\td.refreshAliasEnvVar(),\n\t\td.refreshCmd(),\n\t)\n}\n\nfunc (d *Devbox) refreshAliasForShell(format string) string {\n\t// For nushell format, provide instructions as a comment since aliases with pipes are complex\n\tif format == \"nushell\" {\n\t\tdevboxCmd := \"global shellenv --preserve-path-stack -r --format nushell\"\n\t\tif !d.isGlobal() {\n\t\t\tdevboxCmd = fmt.Sprintf(\"shellenv --preserve-path-stack -c %q --format nushell\", d.projectDir)\n\t\t}\n\t\treturn fmt.Sprintf(\n\t\t\t`# To refresh your devbox environment in nushell, run:\n# devbox %s | save -f ~/.cache/devbox-env.nu; source ~/.cache/devbox-env.nu`,\n\t\t\tdevboxCmd,\n\t\t)\n\t}\n\t// Otherwise use the original refreshAlias function\n\treturn d.refreshAlias()\n}\n"
  },
  {
    "path": "internal/devbox/secrets.go",
    "content": "package devbox\n\nimport (\n\t\"context\"\n\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/envsec/pkg/envsec\"\n\t\"go.jetify.com/envsec/pkg/stores/jetstore\"\n\t\"go.jetify.com/pkg/envvar\"\n)\n\nfunc (d *Devbox) UninitializedSecrets(ctx context.Context) *envsec.Envsec {\n\treturn &envsec.Envsec{\n\t\tAPIHost: build.JetpackAPIHost(),\n\t\tAuth: envsec.AuthConfig{\n\t\t\tClientID: envvar.Get(\"ENVSEC_CLIENT_ID\", build.ClientID()),\n\t\t\tIssuer:   envvar.Get(\"ENVSEC_ISSUER\", build.Issuer()),\n\t\t},\n\t\tIsDev:      build.IsDev,\n\t\tStderr:     d.stderr,\n\t\tStore:      &jetstore.JetpackAPIStore{},\n\t\tWorkingDir: d.ProjectDir(),\n\t}\n}\n\nfunc (d *Devbox) Secrets(ctx context.Context) (*envsec.Envsec, error) {\n\tenvsecInstance := d.UninitializedSecrets(ctx)\n\n\tproject, err := envsecInstance.ProjectConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tenvsecInstance.EnvID = envsec.EnvID{\n\t\tEnvName:   d.environment,\n\t\tOrgID:     project.OrgID.String(),\n\t\tProjectID: project.ProjectID.String(),\n\t}\n\n\tif _, err := envsecInstance.InitForUser(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn envsecInstance, nil\n}\n"
  },
  {
    "path": "internal/devbox/services.go",
    "content": "package devbox\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"text/tabwriter\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/services\"\n)\n\nfunc (d *Devbox) StartServices(\n\tctx context.Context, runInCurrentShell bool, serviceNames ...string,\n) error {\n\tif !runInCurrentShell {\n\t\treturn d.runDevboxServicesScript(ctx,\n\t\t\tappend(\n\t\t\t\t[]string{\"start\", \"--run-in-current-shell\"},\n\t\t\t\tserviceNames...,\n\t\t\t),\n\t\t)\n\t}\n\n\tif !services.ProcessManagerIsRunning(d.projectDir) {\n\t\tfmt.Fprintln(d.stderr, \"Process-compose is not running. Starting it now...\")\n\t\tfmt.Fprintln(d.stderr, \"\\nNOTE: We recommend using `devbox services up` to start process-compose and your services\")\n\t\treturn d.StartProcessManager(ctx, runInCurrentShell, serviceNames, devopt.ProcessComposeOpts{Background: true})\n\t}\n\n\tsvcSet, err := d.Services()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(svcSet) == 0 {\n\t\treturn usererr.New(\"No services found in your project\")\n\t}\n\n\tfor _, s := range serviceNames {\n\t\tif _, ok := svcSet[s]; !ok {\n\t\t\treturn usererr.New(\"Service %s not found in your project\", s)\n\t\t}\n\t}\n\n\tfor _, s := range serviceNames {\n\t\terr := services.StartServices(ctx, d.stderr, s, d.projectDir)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(d.stderr, \"Error starting service %s: %s\", s, err)\n\t\t} else {\n\t\t\tfmt.Fprintf(d.stderr, \"Service %s started successfully\", s)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *Devbox) StopServices(ctx context.Context, runInCurrentShell, allProjects bool, serviceNames ...string) error {\n\tif !runInCurrentShell {\n\t\targs := []string{\"stop\", \"--run-in-current-shell\"}\n\t\targs = append(args, serviceNames...)\n\t\tif allProjects {\n\t\t\targs = append(args, \"--all-projects\")\n\t\t}\n\t\treturn d.runDevboxServicesScript(ctx, args)\n\t}\n\n\tif allProjects {\n\t\treturn services.StopAllProcessManagers(ctx, d.stderr)\n\t}\n\n\tif !services.ProcessManagerIsRunning(d.projectDir) {\n\t\treturn usererr.New(\"Process manager is not running. Run `devbox services up` to start it.\")\n\t}\n\n\tif len(serviceNames) == 0 {\n\t\treturn services.StopProcessManager(ctx, d.projectDir, d.stderr)\n\t}\n\n\tsvcSet, err := d.Services()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, s := range serviceNames {\n\t\tif _, ok := svcSet[s]; !ok {\n\t\t\treturn usererr.New(\"Service %s not found in your project\", s)\n\t\t}\n\t\terr := services.StopServices(ctx, s, d.projectDir, d.stderr)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(d.stderr, \"Error stopping service %s: %s\", s, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *Devbox) ListServices(ctx context.Context, runInCurrentShell bool) error {\n\tif !runInCurrentShell {\n\t\treturn d.runDevboxServicesScript(ctx, []string{\"ls\", \"--run-in-current-shell\"})\n\t}\n\n\tsvcSet, err := d.Services()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(svcSet) == 0 {\n\t\tfmt.Fprintln(d.stderr, \"No services found in your project\")\n\t\treturn nil\n\t}\n\n\tif !services.ProcessManagerIsRunning(d.projectDir) {\n\t\tfmt.Fprintln(d.stderr, \"No services currently running. Run `devbox services up` to start them:\")\n\t\tfmt.Fprintln(d.stderr, \"\")\n\t\tfor _, s := range svcSet {\n\t\t\tfmt.Fprintf(d.stderr, \"  %s\\n\", s.Name)\n\t\t}\n\t\treturn nil\n\t}\n\ttw := tabwriter.NewWriter(d.stderr, 3, 2, 8, ' ', tabwriter.TabIndent)\n\tpcSvcs, err := services.ListServices(ctx, d.projectDir, d.stderr)\n\tif err != nil {\n\t\tfmt.Fprintln(d.stderr, \"Error listing services: \", err)\n\t} else {\n\t\tfmt.Fprintln(d.stderr, \"Services running in process-compose:\")\n\t\tfmt.Fprintln(tw, \"PID\\tNAME\\tNAMESPACE\\tSTATUS\\tAGE\\tHEALTH\\tRESTARTS\\tEXIT CODE\")\n\t\tfor _, s := range pcSvcs {\n\t\t\tfmt.Fprintf(tw, \"%d\\t%s\\t%s\\t%s\\t%s\\t%s\\t%d\\t%d\\n\", s.PID, s.Name, s.Namespace, s.Status, s.Age, s.Health, s.Restarts, s.ExitCode)\n\t\t}\n\t\ttw.Flush()\n\t}\n\treturn nil\n}\n\nfunc (d *Devbox) RestartServices(\n\tctx context.Context, runInCurrentShell bool, serviceNames ...string,\n) error {\n\tif !runInCurrentShell {\n\t\treturn d.runDevboxServicesScript(ctx,\n\t\t\tappend(\n\t\t\t\t[]string{\"restart\", \"--run-in-current-shell\"},\n\t\t\t\tserviceNames...,\n\t\t\t),\n\t\t)\n\t}\n\n\tif !services.ProcessManagerIsRunning(d.projectDir) {\n\t\tfmt.Fprintln(d.stderr, \"Process-compose is not running. Starting it now...\")\n\t\tfmt.Fprintln(d.stderr, \"\\nTip: We recommend using `devbox services up` to start process-compose and your services\")\n\t\treturn d.StartProcessManager(ctx, runInCurrentShell, serviceNames, devopt.ProcessComposeOpts{Background: true})\n\t}\n\n\t// TODO: Restart with no services should restart the _currently running_ services. This means we should get the list of running services from the process-compose, then restart them all.\n\n\tsvcSet, err := d.Services()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, s := range serviceNames {\n\t\tif _, ok := svcSet[s]; !ok {\n\t\t\treturn usererr.New(\"Service %s not found in your project\", s)\n\t\t}\n\t\terr := services.RestartServices(ctx, s, d.projectDir, d.stderr)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Error restarting service %s: %s\", s, err)\n\t\t} else {\n\t\t\tfmt.Printf(\"Service %s restarted\", s)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *Devbox) AttachToProcessManager(ctx context.Context) error {\n\tif !services.ProcessManagerIsRunning(d.projectDir) {\n\t\treturn usererr.New(\"Process manager is not running. Run `devbox services up` to start it.\")\n\t}\n\n\terr := initDevboxUtilityProject(ctx, d.stderr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tprocessComposeBinPath, err := utilityLookPath(\"process-compose\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn services.AttachToProcessManager(\n\t\tctx,\n\t\td.stderr,\n\t\td.projectDir,\n\t\tservices.ProcessComposeOpts{\n\t\t\tBinPath: processComposeBinPath,\n\t\t},\n\t)\n}\n\nfunc (d *Devbox) StartProcessManager(\n\tctx context.Context,\n\trunInCurrentShell bool,\n\trequestedServices []string,\n\tprocessComposeOpts devopt.ProcessComposeOpts,\n) error {\n\tif !runInCurrentShell {\n\t\targs := []string{\"up\", \"--run-in-current-shell\"}\n\t\targs = append(args, requestedServices...)\n\n\t\t// TODO: Here we're attempting to reconstruct arguments from the original command, so that we can reinvoke it in devbox shell.\n\t\t// \t\t Instead, we should consider refactoring this so that we can preserve and re-use the original command string,\n\t\t//\t\t because the current approach is fragile and will need to be updated each time we add new flags.\n\t\tif d.customProcessComposeFile != \"\" {\n\t\t\targs = append(args, \"--process-compose-file\", d.customProcessComposeFile)\n\t\t}\n\t\tif processComposeOpts.Background {\n\t\t\targs = append(args, \"--background\")\n\t\t}\n\t\tfor _, flag := range processComposeOpts.ExtraFlags {\n\t\t\targs = append(args, \"--pcflags\", flag)\n\t\t}\n\t\tif processComposeOpts.ProcessComposePort != 0 {\n\t\t\targs = append(args, \"--pcport\", strconv.Itoa(processComposeOpts.ProcessComposePort))\n\t\t}\n\n\t\treturn d.runDevboxServicesScript(ctx, args)\n\t}\n\n\tsvcs, err := d.Services()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(svcs) == 0 {\n\t\treturn usererr.New(\"No services found in your project\")\n\t}\n\n\tfor _, s := range requestedServices {\n\t\tif _, ok := svcs[s]; !ok {\n\t\t\treturn usererr.New(\"Service %s not found in your project\", s)\n\t\t}\n\t}\n\n\terr = initDevboxUtilityProject(ctx, d.stderr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tprocessComposeBinPath, err := utilityLookPath(\"process-compose\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Start the process manager\n\n\treturn services.StartProcessManager(\n\t\td.stderr,\n\t\trequestedServices,\n\t\tsvcs,\n\t\td.projectDir,\n\t\tservices.ProcessComposeOpts{\n\t\t\tBinPath:            processComposeBinPath,\n\t\t\tBackground:         processComposeOpts.Background,\n\t\t\tExtraFlags:         processComposeOpts.ExtraFlags,\n\t\t\tProcessComposePort: processComposeOpts.ProcessComposePort,\n\t\t},\n\t)\n}\n\n// runDevboxServicesScript invokes RunScript with the envOptions set to the appropriate\n// defaults for the `devbox services` scenario.\nfunc (d *Devbox) runDevboxServicesScript(ctx context.Context, cmdArgs []string) error {\n\tcmdArgs = append([]string{\"services\"}, cmdArgs...)\n\treturn d.RunScript(ctx, devopt.EnvOptions{}, \"devbox\", cmdArgs)\n}\n\nfunc (d *Devbox) ShowProcessComposePort(ctx context.Context, writer io.Writer) error {\n\tport, err := services.GetProcessManagerPort(d.projectDir)\n\tif err != nil {\n\t\treturn err // Error already contains user-friendly message from services layer\n\t}\n\n\tfmt.Fprintf(writer, \"%d\\n\", port)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/devbox/shell.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devbox\n\nimport (\n\t\"bytes\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"al.essio.dev/pkg/shellescape\"\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/shellgen\"\n\t\"go.jetify.com/devbox/internal/telemetry\"\n\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/xdg\"\n)\n\n//go:embed shellrc.tmpl\nvar shellrcText string\nvar shellrcTmpl = template.Must(template.New(\"shellrc\").Funcs(template.FuncMap{\"dirPath\": filepath.Dir}).Parse(shellrcText))\n\n//go:embed shellrc_fish.tmpl\nvar fishrcText string\nvar fishrcTmpl = template.Must(template.New(\"shellrc_fish\").Parse(fishrcText))\n\ntype name string\n\nconst (\n\tshUnknown name = \"\"\n\tshBash    name = \"bash\"\n\tshZsh     name = \"zsh\"\n\tshKsh     name = \"ksh\"\n\tshFish    name = \"fish\"\n\tshPosix   name = \"posix\"\n)\n\nvar ErrNoRecognizableShellFound = errors.New(\"SHELL in undefined, and couldn't find any common shells in PATH\")\n\n// TODO consider splitting this struct's functionality so that there is a simpler\n// `nix.Shell` that can produce a raw nix shell once again.\n\n// DevboxShell configures a user's shell to run in Devbox. Its zero value is a\n// fallback shell that launches a regular Nix shell.\ntype DevboxShell struct {\n\tdevbox          *Devbox\n\tname            name\n\tbinPath         string\n\tprojectDir      string // path to where devbox.json config resides\n\tenv             map[string]string\n\tuserShellrcPath string\n\n\thistoryFile string\n\n\t// shellStartTime is the unix timestamp for when the command was invoked\n\tshellStartTime time.Time\n}\n\ntype ShellOption func(*DevboxShell)\n\n// newShell initializes the DevboxShell struct so it can be used to start a shell environment\n// for the devbox project.\nfunc (d *Devbox) newShell(envOpts devopt.EnvOptions, opts ...ShellOption) (*DevboxShell, error) {\n\tshPath, err := d.shellPath(envOpts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsh := initShellBinaryFields(shPath)\n\tsh.devbox = d\n\n\tfor _, opt := range opts {\n\t\topt(sh)\n\t}\n\n\tslog.Debug(\"detected user shell\", \"shell\", sh.binPath, \"initrc\", sh.userShellrcPath)\n\treturn sh, nil\n}\n\n// shellPath returns the path to a shell binary, or error if none found.\nfunc (d *Devbox) shellPath(envOpts devopt.EnvOptions) (path string, err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tpath = filepath.Clean(path)\n\t\t}\n\t}()\n\n\tif !envOpts.Pure {\n\t\t// First, check the SHELL environment variable.\n\t\tpath = os.Getenv(envir.Shell)\n\t\tif path != \"\" {\n\t\t\tslog.Debug(\"using SHELL env var for shell binary path\", \"shell\", path)\n\t\t\treturn path, nil\n\t\t}\n\t}\n\n\t// Second, fallback to using the bash that nix uses by default.\n\n\tvar bashNixStorePath string // of the form /nix/store/{hash}-bash-{version}/\n\n\tcmd := exec.Command(\n\t\t\"nix\", \"eval\", \"--raw\",\n\t\tfmt.Sprintf(\"%s#bashInteractive\", d.Lockfile().Stdenv().String()),\n\t)\n\tcmd.Args = append(cmd.Args, nix.ExperimentalFlags()...)\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", errors.WithStack(err)\n\t}\n\tbashNixStorePath = string(out)\n\n\t// install bashInteractive in nix/store without creating a symlink to local directory (--no-link)\n\tcmd = exec.Command(\"nix\", \"build\", bashNixStorePath, \"--no-link\")\n\tcmd.Args = append(cmd.Args, nix.ExperimentalFlags()...)\n\terr = cmd.Run()\n\tif err != nil {\n\t\treturn \"\", errors.WithStack(err)\n\t}\n\n\tif bashNixStorePath != \"\" {\n\t\t// the output is the raw path to the bash installation in the /nix/store\n\t\treturn fmt.Sprintf(\"%s/bin/bash\", bashNixStorePath), nil\n\t}\n\n\t// Else, return an error\n\treturn \"\", ErrNoRecognizableShellFound\n}\n\n// initShellBinaryFields initializes the fields specific to the shell binary that will be used\n// for the devbox shell.\nfunc initShellBinaryFields(path string) *DevboxShell {\n\tshell := &DevboxShell{binPath: path}\n\tbase := filepath.Base(path)\n\t// Login shell\n\tif base[0] == '-' {\n\t\tbase = base[1:]\n\t}\n\tswitch base {\n\tcase \"bash\":\n\t\tshell.name = shBash\n\t\tshell.userShellrcPath = rcfilePath(\".bashrc\")\n\tcase \"zsh\":\n\t\tshell.name = shZsh\n\t\tif zdotdir := os.Getenv(\"ZDOTDIR\"); zdotdir != \"\" {\n\t\t\tshell.userShellrcPath = filepath.Join(os.ExpandEnv(zdotdir), \".zshrc\")\n\t\t} else {\n\t\t\tshell.userShellrcPath = rcfilePath(\".zshrc\")\n\t\t}\n\tcase \"ksh\":\n\t\tshell.name = shKsh\n\t\tshell.userShellrcPath = rcfilePath(\".kshrc\")\n\tcase \"fish\":\n\t\tshell.name = shFish\n\t\tshell.userShellrcPath = fishConfig()\n\tcase \"dash\", \"ash\", \"shell\":\n\t\tshell.name = shPosix\n\t\tshell.userShellrcPath = os.Getenv(envir.Env)\n\n\t\t// Just make up a name if there isn't already an init file set\n\t\t// so we have somewhere to put a new one.\n\t\tif shell.userShellrcPath == \"\" {\n\t\t\tshell.userShellrcPath = \".shinit\"\n\t\t}\n\tdefault:\n\t\tshell.name = shUnknown\n\t}\n\treturn shell\n}\n\nfunc WithHistoryFile(historyFile string) ShellOption {\n\treturn func(s *DevboxShell) {\n\t\ts.historyFile = historyFile\n\t}\n}\n\n// TODO: Consider removing this once plugins add env vars directly to binaries via wrapper scripts.\nfunc WithEnvVariables(envVariables map[string]string) ShellOption {\n\treturn func(s *DevboxShell) {\n\t\ts.env = envVariables\n\t}\n}\n\nfunc WithProjectDir(projectDir string) ShellOption {\n\treturn func(s *DevboxShell) {\n\t\ts.projectDir = projectDir\n\t}\n}\n\nfunc WithShellStartTime(t time.Time) ShellOption {\n\treturn func(s *DevboxShell) {\n\t\ts.shellStartTime = t\n\t}\n}\n\n// rcfilePath returns the absolute path for an rcfile, which is usually in the\n// user's home directory. It doesn't guarantee that the file exists.\nfunc rcfilePath(basename string) string {\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn filepath.Join(home, basename)\n}\n\nfunc fishConfig() string {\n\treturn xdg.ConfigSubpath(\"fish/config.fish\")\n}\n\nfunc (s *DevboxShell) Run() error {\n\tvar cmd *exec.Cmd\n\tshellrc, err := s.writeDevboxShellrc()\n\tif err != nil {\n\t\t// We don't have a good fallback here, since all the variables we need for anything to work\n\t\t// are in the shellrc file. For now let's fail. Later on, we should remove the vars from the\n\t\t// shellrc file. That said, one of the variables we have to evaluate ($shellHook), so we need\n\t\t// the shellrc file anyway (unless we remove the hook somehow).\n\t\tslog.Error(\"failed to write devbox shellrc\", \"err\", err)\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// Setup other files that affect the shell settings and environments.\n\ts.setupShellStartupFiles(filepath.Dir(shellrc))\n\textraEnv, extraArgs := s.shellRCOverrides(shellrc)\n\tenv := s.env\n\tfor k, v := range extraEnv {\n\t\tenv[k] = v\n\t}\n\tenv[\"SHELL\"] = s.binPath\n\n\tcmd = exec.Command(s.binPath)\n\tcmd.Env = envir.MapToPairs(env)\n\tcmd.Args = append(cmd.Args, extraArgs...)\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tslog.Debug(\"Executing shell %s with args: %v\", s.binPath, cmd.Args)\n\terr = cmd.Run()\n\n\t// If the error is an ExitError, this means the shell started up fine but there was\n\t// an error from executing a shell command or script.\n\t//\n\t// This could be from one of the generated shellrc commands, but more likely is from\n\t// a user's command or script. So, we want to return nil for this.\n\tif exitErr := (&exec.ExitError{}); errors.As(err, &exitErr) {\n\t\treturn nil\n\t}\n\n\t// This means that there was an error from devbox's code or nix's code. Not a user\n\t// error and so we do return it.\n\treturn errors.WithStack(err)\n}\n\nfunc (s *DevboxShell) shellRCOverrides(shellrc string) (extraEnv map[string]string, extraArgs []string) {\n\t// Shells have different ways of overriding the shellrc, so we need to\n\t// look at the name to know which env vars or args to set when launching the shell.\n\tswitch s.name {\n\tcase shBash:\n\t\textraArgs = []string{\"--rcfile\", shellescape.Quote(shellrc)}\n\tcase shZsh:\n\t\textraEnv = map[string]string{\"ZDOTDIR\": shellescape.Quote(filepath.Dir(shellrc))}\n\tcase shKsh, shPosix:\n\t\textraEnv = map[string]string{\"ENV\": shellescape.Quote(shellrc)}\n\tcase shFish:\n\t\textraArgs = []string{\"-C\", \". \" + shellrc}\n\t}\n\treturn extraEnv, extraArgs\n}\n\nfunc (s *DevboxShell) writeDevboxShellrc() (path string, err error) {\n\t// We need a temp dir (as opposed to a temp file) because zsh uses\n\t// ZDOTDIR to point to a new directory containing the .zshrc.\n\ttmp, err := os.MkdirTemp(\"\", \"devbox\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"create temp dir for shell init file: %v\", err)\n\t}\n\n\t// This is a best-effort to include the user's existing shellrc.\n\tuserShellrc := []byte{}\n\tif s.userShellrcPath != \"\" {\n\t\t// If we can't read it, then just omit it from the devbox shellrc.\n\t\tuserShellrc, _ = os.ReadFile(s.userShellrcPath)\n\t}\n\n\t// If the user already has a shellrc file, then give the devbox shellrc\n\t// file the same name. Otherwise, use an arbitrary name of \"shellrc\".\n\tshellrcName := \"shellrc\"\n\tif s.userShellrcPath != \"\" {\n\t\tshellrcName = filepath.Base(s.userShellrcPath)\n\t}\n\tpath = filepath.Join(tmp, shellrcName)\n\tshellrcf, err := os.Create(path)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"write to shell init file: %v\", err)\n\t}\n\tdefer func() {\n\t\tcerr := shellrcf.Close()\n\t\tif err == nil {\n\t\t\terr = cerr\n\t\t}\n\t}()\n\n\ttmpl := shellrcTmpl\n\tif s.name == shFish {\n\t\ttmpl = fishrcTmpl\n\t}\n\n\terr = tmpl.Execute(shellrcf, struct {\n\t\tProjectDir       string\n\t\tOriginalInit     string\n\t\tOriginalInitPath string\n\t\tHooksFilePath    string\n\t\tShellStartTime   string\n\t\tHistoryFile      string\n\t\tExportEnv        string\n\t\tShellName        string\n\n\t\tRefreshAliasName   string\n\t\tRefreshCmd         string\n\t\tRefreshAliasEnvVar string\n\t}{\n\t\tProjectDir:         s.projectDir,\n\t\tOriginalInit:       string(bytes.TrimSpace(userShellrc)),\n\t\tOriginalInitPath:   s.userShellrcPath,\n\t\tHooksFilePath:      shellgen.ScriptPath(s.projectDir, shellgen.HooksFilename),\n\t\tShellStartTime:     telemetry.FormatShellStart(s.shellStartTime),\n\t\tHistoryFile:        strings.TrimSpace(s.historyFile),\n\t\tExportEnv:          exportify(s.env),\n\t\tShellName:          string(s.name),\n\t\tRefreshAliasName:   s.devbox.refreshAliasName(),\n\t\tRefreshCmd:         s.devbox.refreshCmd(),\n\t\tRefreshAliasEnvVar: s.devbox.refreshAliasEnvVar(),\n\t})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"execute shellrc template: %v\", err)\n\t}\n\n\tslog.Debug(\"wrote devbox shellrc\", \"path\", path)\n\treturn path, nil\n}\n\n// setupShellStartupFiles creates initialization files for the shell by sourcing the user's originals.\n// We do this instead of linking or copying, so that we can set correct ZDOTDIR when sourcing\n// user's config files which may use the ZDOTDIR env var inside them.\n// This also allows us to make sure any devbox config is run after correctly sourcing the user's config.\n//\n// We do not link the .{shell}rc files, since devbox modifies them. See writeDevboxShellrc\nfunc (s *DevboxShell) setupShellStartupFiles(shellSettingsDir string) {\n\t// For now, we only need to do this for zsh shell\n\tif s.name == shZsh {\n\t\t// List of zsh startup files: https://zsh.sourceforge.io/Intro/intro_3.html\n\t\tfilenames := []string{\".zshenv\", \".zprofile\", \".zlogin\", \".zlogout\"}\n\n\t\t// zim framework\n\t\t// https://zimfw.sh/docs/install/\n\t\tfilenames = append(filenames, \".zimrc\")\n\n\t\tfor _, filename := range filenames {\n\t\t\t// The userShellrcPath should be set to ZDOTDIR already.\n\t\t\tuserFile := filepath.Join(filepath.Dir(s.userShellrcPath), filename)\n\t\t\t_, err := os.Stat(userFile)\n\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\t// this file may not be relevant for the user's setup.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tslog.Debug(\"os.Stat error for %s is %v\", userFile, err)\n\t\t\t}\n\n\t\t\tfileNew := filepath.Join(shellSettingsDir, filename)\n\n\t\t\t// Create template content that sources the original file\n\t\t\ttemplateContent := `if [[ -f \"{{.UserFile}}\" ]]; then\n    local DEVBOX_ZDOTDIR=\"$ZDOTDIR\"\n    export ZDOTDIR=\"{{.ZDOTDIR}}\"\n    . \"{{.UserFile}}\"\n    export ZDOTDIR=\"$DEVBOX_ZDOTDIR\"\nfi`\n\n\t\t\t// Parse and execute the template\n\t\t\ttmpl, err := template.New(\"shellrc\").Parse(templateContent)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"error parsing template for zsh setting file\", \"filename\", filename, \"err\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Create the new file with template content\n\t\t\tfile, err := os.Create(fileNew)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"error creating zsh setting file\", \"filename\", filename, \"err\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tdefer file.Close()\n\n\t\t\t// Execute template with data\n\t\t\tdata := struct {\n\t\t\t\tUserFile string\n\t\t\t\tZDOTDIR  string\n\t\t\t}{\n\t\t\t\tUserFile: userFile,\n\t\t\t\tZDOTDIR:  filepath.Dir(s.userShellrcPath),\n\t\t\t}\n\n\t\t\tif err := tmpl.Execute(file, data); err != nil {\n\t\t\t\tslog.Error(\"error executing template for zsh setting file\", \"filename\", filename, \"err\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc filterPathList(pathList string, keep func(string) bool) string {\n\tfiltered := []string{}\n\tfor _, path := range filepath.SplitList(pathList) {\n\t\tif keep(path) {\n\t\t\tfiltered = append(filtered, path)\n\t\t}\n\t}\n\treturn strings.Join(filtered, string(filepath.ListSeparator))\n}\n\nfunc isFishShell() bool {\n\treturn filepath.Base(os.Getenv(\"SHELL\")) == \"fish\" ||\n\t\tos.Getenv(\"FISH_VERSION\") != \"\"\n}\n"
  },
  {
    "path": "internal/devbox/shell_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devbox\n\nimport (\n\t\"errors\"\n\t\"flag\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/shellgen\"\n\t\"go.jetify.com/devbox/internal/xdg\"\n)\n\n// updateFlag overwrites golden files with the new test results.\nvar updateFlag = flag.Bool(\"update\", false, \"update the golden files with the test results\")\n\nfunc TestWriteDevboxShellrc(t *testing.T) {\n\ttestdirs, err := filepath.Glob(\"testdata/shellrc/*\")\n\tif err != nil {\n\t\tt.Fatal(\"Error globbing testdata:\", err)\n\t}\n\ttestWriteDevboxShellrc(t, testdirs)\n}\n\nfunc testWriteDevboxShellrc(t *testing.T, testdirs []string) {\n\tprojectDir := \"/path/to/projectDir\"\n\n\t// Load up all the necessary data from each internal/nix/testdata/shellrc directory\n\t// into a slice of tests cases.\n\ttests := make([]struct {\n\t\tname            string\n\t\tenv             map[string]string\n\t\thooksFilePath   string\n\t\tshellrcPath     string\n\t\tgoldShellrcPath string\n\t\tgoldShellrc     []byte\n\t}, len(testdirs))\n\tvar err error\n\tfor i, path := range testdirs {\n\t\ttest := &tests[i]\n\t\ttest.name = filepath.Base(path)\n\t\tif b, err := os.ReadFile(filepath.Join(path, \"env\")); err == nil {\n\t\t\ttest.env = envir.PairsToMap(strings.Split(string(b), \"\\n\"))\n\t\t}\n\n\t\ttest.hooksFilePath = shellgen.ScriptPath(projectDir, shellgen.HooksFilename)\n\n\t\ttest.shellrcPath = filepath.Join(path, \"shellrc\")\n\t\tif _, err := os.Stat(test.shellrcPath); errors.Is(err, fs.ErrNotExist) {\n\t\t\ttest.shellrcPath = \"\"\n\t\t}\n\t\ttest.goldShellrcPath = filepath.Join(path, \"shellrc.golden\")\n\t\ttest.goldShellrc, err = os.ReadFile(test.goldShellrcPath)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"Got error reading golden file:\", err)\n\t\t}\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ts := &DevboxShell{\n\t\t\t\tdevbox:          &Devbox{projectDir: projectDir},\n\t\t\t\tenv:             test.env,\n\t\t\t\tprojectDir:      \"/path/to/projectDir\",\n\t\t\t\tuserShellrcPath: test.shellrcPath,\n\t\t\t}\n\t\t\t// Set shell name based on test name for zsh tests\n\t\t\tif strings.Contains(test.name, \"zsh\") {\n\t\t\t\ts.name = shZsh\n\t\t\t}\n\t\t\tgotPath, err := s.writeDevboxShellrc()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"Got writeDevboxShellrc error:\", err)\n\t\t\t}\n\t\t\tgotShellrc, err := os.ReadFile(gotPath)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Got error reading generated shellrc at %s: %v\", gotPath, err)\n\t\t\t}\n\n\t\t\t// Overwrite the golden file if the -update flag was\n\t\t\t// set, and then proceed normally. The test should\n\t\t\t// always pass in this case.\n\t\t\tif *updateFlag {\n\t\t\t\terr = os.WriteFile(test.goldShellrcPath, gotShellrc, 0o666)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(\"Error updating golden files:\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tgoldShellrc, err := os.ReadFile(test.goldShellrcPath)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"Got error reading golden file:\", err)\n\t\t\t}\n\t\t\tdiff := cmp.Diff(goldShellrc, gotShellrc)\n\t\t\tif diff != \"\" {\n\t\t\t\tt.Errorf(strings.TrimSpace(`\nGenerated shellrc != shellrc.golden (-shellrc.golden +shellrc):\n\n\t%s\nIf the new shellrc is correct, you can update the golden file with:\n\n\tgo test -run \"^%s$\" -update`), diff, t.Name())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestShellPath(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tenvOpts  devopt.EnvOptions\n\t\texpected string\n\t\tenv      map[string]string\n\t}{\n\t\t{\n\t\t\tname: \"pure mode enabled\",\n\t\t\tenvOpts: devopt.EnvOptions{\n\t\t\t\tPure: true,\n\t\t\t},\n\t\t\texpected: `^/nix/store/.*/bin/bash$`,\n\t\t},\n\t\t{\n\t\t\tname: \"pure mode disabled\",\n\t\t\tenvOpts: devopt.EnvOptions{\n\t\t\t\tPure: false,\n\t\t\t},\n\t\t\tenv: map[string]string{\n\t\t\t\tenvir.Shell: \"/usr/local/bin/bash\",\n\t\t\t},\n\t\t\texpected: \"^/usr/local/bin/bash$\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tfor k, v := range test.env {\n\t\t\t\tt.Setenv(k, v)\n\t\t\t}\n\t\t\ttmpDir := t.TempDir()\n\t\t\terr := InitConfig(tmpDir)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"Got InitConfig error:\", err)\n\t\t\t}\n\t\t\td, err := Open(&devopt.Opts{\n\t\t\t\tDir:    tmpDir,\n\t\t\t\tStderr: os.Stderr,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"Got Open error:\", err)\n\t\t\t}\n\t\t\tgotPath, err := d.shellPath(test.envOpts)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"Got shellPath error:\", err)\n\t\t\t}\n\t\t\tmatched, err := regexp.MatchString(test.expected, gotPath)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"Got regexp.MatchString error:\", err)\n\t\t\t}\n\t\t\tif !matched {\n\t\t\t\tt.Errorf(\"Expected shell path %s, but got %s\", test.expected, gotPath)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestInitShellBinaryFields(t *testing.T) {\n\ttests := []struct {\n\t\tname               string\n\t\tpath               string\n\t\tenv                map[string]string\n\t\texpectedName       name\n\t\texpectedRcPath     string\n\t\texpectedRcPathBase string\n\t}{\n\t\t{\n\t\t\tname:               \"bash shell\",\n\t\t\tpath:               \"/usr/bin/bash\",\n\t\t\texpectedName:       shBash,\n\t\t\texpectedRcPathBase: \".bashrc\",\n\t\t},\n\t\t{\n\t\t\tname:               \"zsh shell without ZDOTDIR\",\n\t\t\tpath:               \"/usr/bin/zsh\",\n\t\t\texpectedName:       shZsh,\n\t\t\texpectedRcPathBase: \".zshrc\",\n\t\t},\n\t\t{\n\t\t\tname: \"zsh shell with ZDOTDIR\",\n\t\t\tpath: \"/usr/bin/zsh\",\n\t\t\tenv: map[string]string{\n\t\t\t\t\"ZDOTDIR\": \"/custom/zsh/config\",\n\t\t\t},\n\t\t\texpectedName:   shZsh,\n\t\t\texpectedRcPath: \"/custom/zsh/config/.zshrc\",\n\t\t},\n\t\t{\n\t\t\tname:               \"ksh shell\",\n\t\t\tpath:               \"/usr/bin/ksh\",\n\t\t\texpectedName:       shKsh,\n\t\t\texpectedRcPathBase: \".kshrc\",\n\t\t},\n\t\t{\n\t\t\tname:           \"fish shell\",\n\t\t\tpath:           \"/usr/bin/fish\",\n\t\t\texpectedName:   shFish,\n\t\t\texpectedRcPath: xdg.ConfigSubpath(\"fish/config.fish\"),\n\t\t},\n\t\t{\n\t\t\tname:           \"dash shell\",\n\t\t\tpath:           \"/usr/bin/dash\",\n\t\t\texpectedName:   shPosix,\n\t\t\texpectedRcPath: \".shinit\",\n\t\t},\n\t\t{\n\t\t\tname:               \"unknown shell\",\n\t\t\tpath:               \"/usr/bin/unknown\",\n\t\t\texpectedName:       shUnknown,\n\t\t\texpectedRcPathBase: \"\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t// Set up environment variables\n\t\t\tfor k, v := range test.env {\n\t\t\t\tt.Setenv(k, v)\n\t\t\t}\n\n\t\t\tshell := initShellBinaryFields(test.path)\n\n\t\t\tif shell.name != test.expectedName {\n\t\t\t\tt.Errorf(\"Expected shell name %v, got %v\", test.expectedName, shell.name)\n\t\t\t}\n\n\t\t\tif test.expectedRcPath != \"\" {\n\t\t\t\tif shell.userShellrcPath != test.expectedRcPath {\n\t\t\t\t\tt.Errorf(\"Expected rc path %s, got %s\", test.expectedRcPath, shell.userShellrcPath)\n\t\t\t\t}\n\t\t\t} else if test.expectedRcPathBase != \"\" {\n\t\t\t\t// For tests that expect a path relative to home directory,\n\t\t\t\t// check that the path ends with the expected basename\n\t\t\t\texpectedBasename := test.expectedRcPathBase\n\t\t\t\tactualBasename := filepath.Base(shell.userShellrcPath)\n\t\t\t\tif actualBasename != expectedBasename {\n\t\t\t\t\tt.Errorf(\"Expected rc path basename %s, got %s (full path: %s)\", expectedBasename, actualBasename, shell.userShellrcPath)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetupShellStartupFiles(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\t// Create a mock zsh shell\n\tshell := &DevboxShell{\n\t\tname:            shZsh,\n\t\tuserShellrcPath: filepath.Join(tmpDir, \".zshrc\"),\n\t}\n\n\t// Create some test zsh startup files\n\tstartupFiles := []string{\".zshenv\", \".zprofile\", \".zlogin\", \".zlogout\", \".zimrc\"}\n\tfor _, filename := range startupFiles {\n\t\tfilePath := filepath.Join(tmpDir, filename)\n\t\terr := os.WriteFile(filePath, []byte(\"# Test content for \"+filename), 0o644)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to create test file %s: %v\", filename, err)\n\t\t}\n\t}\n\n\t// Create a temporary directory for shell settings\n\tshellSettingsDir := t.TempDir()\n\n\t// Call setupShellStartupFiles\n\tshell.setupShellStartupFiles(shellSettingsDir)\n\n\t// Check that all startup files were created in the shell settings directory\n\tfor _, filename := range startupFiles {\n\t\texpectedPath := filepath.Join(shellSettingsDir, filename)\n\t\t_, err := os.Stat(expectedPath)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Expected startup file %s to be created, but got error: %v\", filename, err)\n\t\t}\n\n\t\t// Check that the file contains the expected template content\n\t\tcontent, err := os.ReadFile(expectedPath)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Failed to read created file %s: %v\", filename, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tcontentStr := string(content)\n\t\texpectedOldPath := filepath.Join(tmpDir, filename)\n\t\tif !strings.Contains(contentStr, expectedOldPath) {\n\t\t\tt.Errorf(\"Expected file %s to contain path %s, but content was: %s\", filename, expectedOldPath, contentStr)\n\t\t}\n\n\t\tif !strings.Contains(contentStr, \"DEVBOX_ZDOTDIR\") {\n\t\t\tt.Errorf(\"Expected file %s to contain ZDOTDIR handling, but content was: %s\", filename, contentStr)\n\t\t}\n\t}\n}\n\nfunc TestWriteDevboxShellrcBash(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\t// Create a test bash rc file\n\tbashrcPath := filepath.Join(tmpDir, \".bashrc\")\n\tbashrcContent := \"# Test bash configuration\\nexport TEST_VAR=value\"\n\terr := os.WriteFile(bashrcPath, []byte(bashrcContent), 0o644)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create test .bashrc: %v\", err)\n\t}\n\n\t// Create a mock devbox\n\tdevbox := &Devbox{projectDir: \"/test/project\"}\n\n\t// Create a bash shell\n\tshell := &DevboxShell{\n\t\tdevbox:          devbox,\n\t\tname:            shBash,\n\t\tuserShellrcPath: bashrcPath,\n\t\tprojectDir:      \"/test/project\",\n\t\tenv:             map[string]string{\"TEST_ENV\": \"test_value\"},\n\t}\n\n\t// Write the devbox shellrc\n\tshellrcPath, err := shell.writeDevboxShellrc()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to write devbox shellrc: %v\", err)\n\t}\n\n\t// Read and verify the content\n\tcontent, err := os.ReadFile(shellrcPath)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to read generated shellrc: %v\", err)\n\t}\n\n\tcontentStr := string(content)\n\n\t// Check that it does NOT contain zsh-specific ZDOTDIR handling\n\tif strings.Contains(contentStr, \"DEVBOX_ZDOTDIR\") {\n\t\tt.Error(\"Expected shellrc to NOT contain ZDOTDIR handling for bash\")\n\t}\n\n\t// Check that it sources the original .bashrc\n\tif !strings.Contains(contentStr, bashrcPath) {\n\t\tt.Error(\"Expected shellrc to source the original .bashrc file\")\n\t}\n}\n\nfunc TestWriteDevboxShellrcWithZDOTDIR(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\t// Set up ZDOTDIR environment\n\tt.Setenv(\"ZDOTDIR\", tmpDir)\n\n\t// Create a test zsh rc file in the custom ZDOTDIR\n\tcustomZshrcPath := filepath.Join(tmpDir, \".zshrc\")\n\tzshrcContent := \"# Custom zsh configuration\\nexport CUSTOM_VAR=value\"\n\terr := os.WriteFile(customZshrcPath, []byte(zshrcContent), 0o644)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create test .zshrc: %v\", err)\n\t}\n\n\t// Create a mock devbox\n\tdevbox := &Devbox{projectDir: \"/test/project\"}\n\n\t// Create a zsh shell - this should pick up the ZDOTDIR\n\tshell := initShellBinaryFields(\"/usr/bin/zsh\")\n\tshell.devbox = devbox\n\tshell.projectDir = \"/test/project\"\n\n\tif shell.userShellrcPath != customZshrcPath {\n\t\tt.Error(\"Expected shellrc path to respect ZDOTDIR\")\n\t}\n\n\t// Write the devbox shellrc\n\tshellrcPath, err := shell.writeDevboxShellrc()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to write devbox shellrc: %v\", err)\n\t}\n\n\t// Read and verify the content\n\tcontent, err := os.ReadFile(shellrcPath)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to read generated shellrc: %v\", err)\n\t}\n\n\tcontentStr := string(content)\n\t// Check that it contains zsh-specific ZDOTDIR handling\n\tif !strings.Contains(contentStr, \"DEVBOX_ZDOTDIR\") {\n\t\tt.Error(\"Expected shellrc to contain ZDOTDIR handling for zsh\")\n\t}\n\n\t// Check that it sources the custom .zshrc\n\tif !strings.Contains(contentStr, customZshrcPath) {\n\t\tt.Error(\"Expected shellrc to source the custom .zshrc file\")\n\t}\n}\n"
  },
  {
    "path": "internal/devbox/shellcmd/command.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage shellcmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"go.jetify.com/devbox/internal/cuecfg\"\n)\n\n// Formats for marshalling and unmarshalling a series of shell commands in a\n// devbox config.\nconst (\n\t// CmdArray formats shell commands as an array of strings.\n\tCmdArray CmdFormat = iota\n\n\t// CmdString formats shell commands as a single string.\n\tCmdString\n)\n\n// CmdFormat defines a way of formatting shell commands in a devbox config.\ntype CmdFormat int\n\nfunc (c CmdFormat) String() string {\n\tswitch c {\n\tcase CmdArray:\n\t\treturn \"array\"\n\tcase CmdString:\n\t\treturn \"string\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"invalid (%d)\", c)\n\t}\n}\n\n// Commands marshals and unmarshals shell commands from a devbox config\n// as either a single string or an array of strings. It preserves the original\n// value such that:\n//\n//\tdata == marshal(unmarshal(data)))\ntype Commands struct {\n\t// MarshalAs determines how MarshalJSON encodes the shell commands.\n\t// UnmarshalJSON will set MarshalAs automatically so that commands\n\t// marshal back to their original format. The default zero-value\n\t// formats them as an array.\n\tMarshalAs CmdFormat\n\tCmds      []string\n}\n\n// AppendScript appends each line of a script to s.Cmds. It also applies the\n// following formatting rules:\n//\n//   - Trim leading newlines from the script.\n//   - Trim trailing whitespace from the script.\n//   - If the first line of the script begins with one or more tabs ('\\t'), then\n//     unindent each line by that same number of tabs.\n//   - Remove trailing whitespace from each line.\n//\n// Note that unindenting only happens when a line starts with at least as many\n// tabs as the first line. If it starts with fewer tabs, then it is not\n// unindented at all.\nfunc (s *Commands) AppendScript(script string) {\n\tscript = strings.TrimLeft(script, \"\\r\\n \")\n\tscript = strings.TrimRightFunc(script, unicode.IsSpace)\n\tif len(script) == 0 {\n\t\treturn\n\t}\n\tprefixLen := strings.IndexFunc(script, func(r rune) bool { return r != '\\t' })\n\tprefix := strings.Repeat(\"\\t\", prefixLen)\n\tfor _, line := range strings.Split(script, \"\\n\") {\n\t\tline = strings.TrimRightFunc(line, unicode.IsSpace)\n\t\tline = strings.TrimPrefix(line, prefix)\n\t\ts.Cmds = append(s.Cmds, line)\n\t}\n}\n\n// MarshalJSON marshals shell commands according to s.MarshalAs. It marshals\n// commands to a string by joining s.Cmds with newlines.\nfunc (s Commands) MarshalJSON() ([]byte, error) {\n\tswitch s.MarshalAs {\n\tcase CmdArray:\n\t\treturn cuecfg.MarshalJSON(s.Cmds)\n\tcase CmdString:\n\t\treturn cuecfg.MarshalJSON(s.String())\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"invalid command format: %s\", s.MarshalAs))\n\t}\n}\n\n// UnmarshalJSON unmarshals shell commands from a string, an array of strings,\n// or null. When the JSON value is a string, it unmarshals into the first index\n// of s.Cmds.\nfunc (s *Commands) UnmarshalJSON(data []byte) error {\n\tif len(data) == 0 || string(data) == \"null\" {\n\t\ts.MarshalAs = CmdArray\n\t\ts.Cmds = nil\n\t\treturn nil\n\t}\n\n\tswitch data[0] {\n\tcase '\"':\n\t\ts.MarshalAs = CmdString\n\t\ts.Cmds = []string{\"\"}\n\t\treturn json.Unmarshal(data, &s.Cmds[0])\n\n\tcase '[':\n\t\ts.MarshalAs = CmdArray\n\t\treturn json.Unmarshal(data, &s.Cmds)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// String formats the commands as a single string by joining them with newlines.\nfunc (s *Commands) String() string {\n\tif s == nil {\n\t\treturn \"\"\n\t}\n\treturn strings.Join(s.Cmds, \"\\n\")\n}\n"
  },
  {
    "path": "internal/devbox/shellcmd/command_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage shellcmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"go.jetify.com/devbox/internal/cuecfg\"\n)\n\nfunc TestCommandsUnmarshalString(t *testing.T) {\n\ttests := []struct {\n\t\tjsonIn string\n\t\twant   Commands\n\t}{\n\t\t{\n\t\t\tjsonIn: `null`,\n\t\t\twant: Commands{\n\t\t\t\tMarshalAs: CmdArray,\n\t\t\t\tCmds:      nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tjsonIn: `\"\"`,\n\t\t\twant: Commands{\n\t\t\t\tMarshalAs: CmdString,\n\t\t\t\tCmds:      []string{\"\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tjsonIn: `\"\\n\"`,\n\t\t\twant: Commands{\n\t\t\t\tMarshalAs: CmdString,\n\t\t\t\tCmds:      []string{\"\\n\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tjsonIn: `\"echo 'line1'\\necho 'line2'\"`,\n\t\t\twant: Commands{\n\t\t\t\tMarshalAs: CmdString,\n\t\t\t\tCmds:      []string{\"echo 'line1'\\necho 'line2'\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tjsonIn: `\"echo '\\nline1'\\necho 'line2'\\n\"`,\n\t\t\twant: Commands{\n\t\t\t\tMarshalAs: CmdString,\n\t\t\t\tCmds:      []string{\"echo '\\nline1'\\necho 'line2'\\n\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tjsonIn: `\"echo 'line1'\\n\\necho 'line2'\"`,\n\t\t\twant: Commands{\n\t\t\t\tMarshalAs: CmdString,\n\t\t\t\tCmds:      []string{\"echo 'line1'\\n\\necho 'line2'\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tjsonIn: `\"echo 'line1'\\necho '\\tline2'\"`,\n\t\t\twant: Commands{\n\t\t\t\tMarshalAs: CmdString,\n\t\t\t\tCmds:      []string{\"echo 'line1'\\necho '\\tline2'\"},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.jsonIn, func(t *testing.T) {\n\t\t\tgot := Commands{}\n\t\t\tif err := json.Unmarshal([]byte(test.jsonIn), &got); err != nil {\n\t\t\t\tt.Fatal(\"Got error unmarshalling test input:\", err)\n\t\t\t}\n\t\t\tif got.MarshalAs != test.want.MarshalAs {\n\t\t\t\tt.Errorf(\"Got MarshalAs == %s after unmarshalling, want \"+\n\t\t\t\t\t\"MarshalAs == %s.\", got.MarshalAs, test.want.MarshalAs)\n\t\t\t}\n\t\t\tif len(got.Cmds) != len(test.want.Cmds) {\n\t\t\t\tt.Fatalf(\"len(got.Cmds) != len(want.Cmds)\\ngot:  %q (len %d)\\nwant: %q (len %d)\",\n\t\t\t\t\tgot.Cmds, len(got.Cmds), test.want.Cmds, len(test.want.Cmds))\n\t\t\t}\n\t\t\tfor i := range got.Cmds {\n\t\t\t\tgot, want := got.Cmds[i], test.want.Cmds[i]\n\t\t\t\tif got != want {\n\t\t\t\t\tt.Fatalf(\"got.Cmds[%[1]d] != want.Cmds[%[1]d]\\ngot:  %q\\nwant: %q\",\n\t\t\t\t\t\ti, got, want)\n\t\t\t\t}\n\t\t\t}\n\t\t\tb, err := cuecfg.MarshalJSON(got)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"Got error marshalling back to JSON:\", err)\n\t\t\t}\n\t\t\tif diff := cmp.Diff(test.jsonIn, string(b)); diff != \"\" {\n\t\t\t\tt.Errorf(\"Got different JSON after unmarshalling and re-marshalling (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCommandsString(t *testing.T) {\n\ttests := []struct {\n\t\tjsonIn string\n\t\twant   string\n\t}{\n\t\t{\n\t\t\tjsonIn: `null`,\n\t\t\twant:   \"\",\n\t\t},\n\t\t{\n\t\t\tjsonIn: `[]`,\n\t\t\twant:   \"\",\n\t\t},\n\t\t{\n\t\t\tjsonIn: `[\"\"]`,\n\t\t\twant:   \"\",\n\t\t},\n\t\t{\n\t\t\tjsonIn: `[\"\\n\"]`,\n\t\t\twant:   \"\\n\",\n\t\t},\n\t\t{\n\t\t\tjsonIn: `[\"echo 'line1'\\necho 'line2'\"]`,\n\t\t\twant:   \"echo 'line1'\\necho 'line2'\",\n\t\t},\n\t\t{\n\t\t\tjsonIn: `[\"echo 'line1'\", \"echo 'line2'\"]`,\n\t\t\twant:   \"echo 'line1'\\necho 'line2'\",\n\t\t},\n\t\t{\n\t\t\tjsonIn: `[\"echo 'line1'\\n\\necho 'line2'\"]`,\n\t\t\twant:   \"echo 'line1'\\n\\necho 'line2'\",\n\t\t},\n\t\t{\n\t\t\tjsonIn: `[\"echo 'line1'\", \"\", \"echo 'line2'\"]`,\n\t\t\twant:   \"echo 'line1'\\n\\necho 'line2'\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.jsonIn, func(t *testing.T) {\n\t\t\tgot := Commands{}\n\t\t\tif err := json.Unmarshal([]byte(test.jsonIn), &got); err != nil {\n\t\t\t\tt.Fatal(\"Got error unmarshalling test input:\", err)\n\t\t\t}\n\t\t\tif got.String() != test.want {\n\t\t\t\tt.Errorf(\"got.String() != want\\ngot:  %q\\nwant: %q\",\n\t\t\t\t\tgot.String(), test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc ExampleCommands_AppendScript() {\n\tshCmds := Commands{}\n\tshCmds.AppendScript(`\n\t\t# This script will be unindented by the number of leading tabs\n\t\t# on the first line.\n\t\tif true; then\n\t\t\techo \"this is always printed\"\n\t\tfi`,\n\t)\n\tb, _ := cuecfg.MarshalJSON(&shCmds)\n\tfmt.Println(string(b))\n\n\t// Output:\n\t// [\n\t//   \"# This script will be unindented by the number of leading tabs\",\n\t//   \"# on the first line.\",\n\t//   \"if true; then\",\n\t//   \"\\techo \\\"this is always printed\\\"\",\n\t//   \"fi\"\n\t// ]\n}\n\nfunc TestAppendScript(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tscript   string\n\t\twantCmds []string\n\t}{\n\t\t{\n\t\t\tname:     \"Empty\",\n\t\t\tscript:   \"\",\n\t\t\twantCmds: nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"OnlySpaces\",\n\t\t\tscript:   \" \",\n\t\t\twantCmds: nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"Only newlines\",\n\t\t\tscript:   \"\\r\\n\",\n\t\t\twantCmds: nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"Simple\",\n\t\t\tscript:   \"echo test\",\n\t\t\twantCmds: []string{\"echo test\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"LeadingNewline\",\n\t\t\tscript:   \"\\necho test\",\n\t\t\twantCmds: []string{\"echo test\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"LeadingNewlineAndSpace\",\n\t\t\tscript:   \"\\n    echo test\",\n\t\t\twantCmds: []string{\"echo test\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"TrailingWhitespace\",\n\t\t\tscript:   \"echo test  \\n\",\n\t\t\twantCmds: []string{\"echo test\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"SecondLineIndent\",\n\t\t\tscript: \"if true; then\\n\\techo test\\nfi\",\n\t\t\twantCmds: []string{\n\t\t\t\t\"if true; then\",\n\t\t\t\t\"\\techo test\",\n\t\t\t\t\"fi\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"Unindent\",\n\t\t\tscript: \"\\n\\tif true; then\\n\\t\\techo test\\n\\tfi\",\n\t\t\twantCmds: []string{\n\t\t\t\t\"if true; then\",\n\t\t\t\t\"\\techo test\",\n\t\t\t\t\"fi\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"UnindentTooFewTabs\",\n\t\t\tscript: \"\\t\\tif true; then\\n\\techo test\\n\\t\\tfi\",\n\t\t\twantCmds: []string{\n\t\t\t\t\"if true; then\",\n\t\t\t\t\"\\techo test\",\n\t\t\t\t\"fi\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tshCmds := Commands{}\n\t\t\tshCmds.AppendScript(test.script)\n\t\t\tgotCmds := shCmds.Cmds\n\t\t\tif diff := cmp.Diff(test.wantCmds, gotCmds); diff != \"\" {\n\t\t\t\tt.Errorf(\"Got incorrect commands slice (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/devbox/shellrc.tmpl",
    "content": "{{- /*\n\nIF YOU EDIT THIS FILE, REMEMBER TO MAKE EQUIVALENT CHANGES TO shellrc_fish.tmpl\n\nThis template defines the shellrc file that the devbox shell will run at\nstartup.\n\nIt includes the user's original shellrc, which varies depending on their shell.\nIt will either be ~/.bashrc, ~/.zshrc, a path set in ENV, or something else. It\nalso appends any user-defined shell hooks from devbox.json.\n\nDevbox needs to ensure that the shell's PATH, prompt, and a few other things are\nset correctly after the user's shellrc runs. The commands to do this are in\nthe \"Devbox Post-init Hook\" section.\n\nThis file is useful for debugging shell errors, so try to keep the generated\ncontent readable.\n\n*/ -}}\n\n{{- if .OriginalInitPath -}}\n{{- if eq .ShellName \"zsh\" -}}\nif [ -f {{ .OriginalInitPath }} ]; then\n  local DEVBOX_ZDOTDIR=\"$ZDOTDIR\"\n  export ZDOTDIR=\"{{dirPath .OriginalInitPath}}\"\n  . \"{{ .OriginalInitPath }}\"\n  export ZDOTDIR=\"$DEVBOX_ZDOTDIR\"\nfi\n{{ else -}}\nif [ -f {{ .OriginalInitPath }} ]; then\n  . \"{{ .OriginalInitPath }}\"\nfi\n{{ end -}}\n{{ end -}}\n\n# Begin Devbox Post-init Hook\n\n{{ with .ExportEnv -}}\n{{ . }}\n{{- end }}\n\n{{- /*\nWe need to set HISTFILE here because when starting a new shell, the shell will\nignore the existing value of HISTFILE.\n*/ -}}\n{{- if .HistoryFile }}\nHISTFILE=\"{{ .HistoryFile }}\"\n{{- end }}\n\n# If the user hasn't specified they want to handle the prompt themselves,\n# prepend to the prompt to make it clear we're in a devbox shell.\nif [ -z \"$DEVBOX_NO_PROMPT\" ]; then\n  export PS1=\"(devbox) $PS1\"\nfi\n\n{{- if .ShellStartTime }}\n# log that the shell is ready now!\ndevbox log shell-ready {{ .ShellStartTime }}\n{{ end }}\n\n# End Devbox Post-init Hook\n\n# Run plugin and user init hooks from the devbox.json directory.\nworking_dir=\"$(pwd)\"\ncd \"{{ .ProjectDir }}\" || exit\n\n# Source the hooks file, which contains the project's init hooks and plugin hooks.\n. \"{{ .HooksFilePath }}\"\n\ncd \"$working_dir\" || exit\n\n{{- if .ShellStartTime }}\n# log that the shell is interactive now!\ndevbox log shell-interactive {{ .ShellStartTime }}\n{{ end }}\n\n# Add refresh alias (only if it doesn't already exist)\nif ! type {{ .RefreshAliasName }} >/dev/null 2>&1; then\n  export {{ .RefreshAliasEnvVar }}='{{ .RefreshCmd }}'\n  alias {{ .RefreshAliasName }}='{{ .RefreshCmd }}'\nfi\n"
  },
  {
    "path": "internal/devbox/shellrc_fish.tmpl",
    "content": "{{- /*\n\nThis template defines the shellrc file that the devbox shell will run at\nstartup when using the fish shell.\n\nIt does _not_ include the user's original fish config, because unlike other\nshells, fish has multiple files as part of its config, and it's difficult\nto start a fish shell with a custom fish config. Instead, we let fish read\nthe user's original config directly, and run these commands next.\n\nDevbox needs to ensure that the shell's PATH, prompt, and a few other things are\nset correctly after the user's shellrc runs. The commands to do this are in\nthe \"Devbox Post-init Hook\" section.\n\nThis file is useful for debugging shell errors, so try to keep the generated\ncontent readable.\n\n*/ -}}\n\n# Begin Devbox Post-init Hook\n\n{{- /*\nNOTE: fish_add_path doesn't play nicely with colon:separated:paths, and I'd rather not\nadd string-splitting logic here nor parametrize computeNixEnv based on the shell being\nused. So here we (ab)use the fact that using \"export\" ahead of the variable definition\nmakes fish do exactly what we want and behave in the same way as other shells.\n*/ -}}\n{{ with .ExportEnv }}\n{{ . }}\n{{- end }}\n\n{{- /*\nSet the history file by setting fish_history. This is not exactly the same as with other\nshells, because we're not setting the file, but rather the session name, but it's a good\nenough approximation for now.\n*/ -}}\n{{- if .HistoryFile }}\nset fish_history devbox\n{{- end }}\n\n# If the user hasn't specified they want to handle the prompt themselves,\n# prepend to the prompt to make it clear we're in a devbox shell.\nif not set -q devbox_no_prompt\n    functions -c fish_prompt __devbox_fish_prompt_orig\n    function fish_prompt\n        echo \"(devbox)\" (__devbox_fish_prompt_orig)\n    end\nend\n\n{{- if .ShellStartTime }}\n# log that the shell is ready now!\ndevbox log shell-ready {{ .ShellStartTime }}\n{{ end }}\n\n# End Devbox Post-init Hook\n\n# Switch to the directory where devbox.json config is\nset workingDir (pwd)\ncd \"{{ .ProjectDir }}\" || exit\n\n# Source the hooks file, which contains the project's init hooks and plugin hooks.\nsource \"{{ .HooksFilePath }}\"\n\ncd \"$workingDir\" || exit\n\n{{- if .ShellStartTime }}\n# log that the shell is interactive now!\ndevbox log shell-interactive {{ .ShellStartTime }}\n{{ end }}\n\n# Add refresh alias (only if it doesn't already exist)\nif not type {{ .RefreshAliasName }} >/dev/null 2>&1\n  export {{ .RefreshAliasEnvVar }}='{{ .RefreshCmd }}'\n  alias {{ .RefreshAliasName }}='{{ .RefreshCmd }}'\nend\n"
  },
  {
    "path": "internal/devbox/testdata/shellrc/basic/env",
    "content": "simple=value\nspace=quote me\nquote=they said, \"lasers\"\nspecial=$`\"\\\nBASH_FUNC_echo_simple%%=() { echo \"${simple}\"; }\n"
  },
  {
    "path": "internal/devbox/testdata/shellrc/basic/shellrc",
    "content": "# Set up the prompt\n\nautoload -Uz promptinit\npromptinit\n#prompt adam1\n\nsetopt histignorealldups sharehistory\n\n# Use emacs keybindings even if our EDITOR is set to vi\nbindkey -e\n\n# Keep 1000 lines of history within the shell and save it to ~/.zsh_history:\nHISTSIZE=1000\nSAVEHIST=1000\nHISTFILE=~/.zsh_history\n\n# Use modern completion system\nautoload -Uz compinit\ncompinit\n\nzstyle ':completion:*' auto-description 'specify: %d'\nzstyle ':completion:*' completer _expand _complete _correct _approximate\nzstyle ':completion:*' format 'Completing %d'\nzstyle ':completion:*' group-name ''\nzstyle ':completion:*' menu select=2\neval \"$(dircolors -b)\"\nzstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS}\nzstyle ':completion:*' list-colors ''\nzstyle ':completion:*' list-prompt %SAt %p: Hit TAB for more, or the character to insert%s\nzstyle ':completion:*' matcher-list '' 'm:{a-z}={A-Z}' 'm:{a-zA-Z}={A-Za-z}' 'r:|[._-]=* r:|=* l:|=*'\nzstyle ':completion:*' menu select=long\nzstyle ':completion:*' select-prompt %SScrolling active: current selection at %p%s\nzstyle ':completion:*' use-compctl false\nzstyle ':completion:*' verbose true\n\nzstyle ':completion:*:*:kill:*:processes' list-colors '=(#b) #([0-9]#)*=0=01;31'\nzstyle ':completion:*:kill:*' command 'ps -u $USER -o pid,%cpu,tty,cputime,cmd'\n"
  },
  {
    "path": "internal/devbox/testdata/shellrc/basic/shellrc.golden",
    "content": "if [ -f testdata/shellrc/basic/shellrc ]; then\n  . \"testdata/shellrc/basic/shellrc\"\nfi\n# Begin Devbox Post-init Hook\n\necho_simple () { echo \"${simple}\"; }\nexport -f echo_simple\nexport quote=\"they said, \\\"lasers\\\"\";\nexport simple=\"value\";\nexport space=\"quote me\";\nexport special=\"\\$\\`\\\"\\\\\";\n\n# If the user hasn't specified they want to handle the prompt themselves,\n# prepend to the prompt to make it clear we're in a devbox shell.\nif [ -z \"$DEVBOX_NO_PROMPT\" ]; then\n  export PS1=\"(devbox) $PS1\"\nfi\n\n# End Devbox Post-init Hook\n\n# Run plugin and user init hooks from the devbox.json directory.\nworking_dir=\"$(pwd)\"\ncd \"/path/to/projectDir\" || exit\n\n# Source the hooks file, which contains the project's init hooks and plugin hooks.\n. \"/path/to/projectDir/.devbox/gen/scripts/.hooks.sh\"\n\ncd \"$working_dir\" || exit\n\n# Add refresh alias (only if it doesn't already exist)\nif ! type refresh >/dev/null 2>&1; then\n  export DEVBOX_REFRESH_ALIAS_11c3c7a2e9a24e16e714a53a46351e31be8beac32de3f19854be1ef14e556903='eval \"$(devbox shellenv --preserve-path-stack -c \"/path/to/projectDir\")\" && hash -r'\n  alias refresh='eval \"$(devbox shellenv --preserve-path-stack -c \"/path/to/projectDir\")\" && hash -r'\nfi\n"
  },
  {
    "path": "internal/devbox/testdata/shellrc/noshellrc/shellrc.golden",
    "content": "# Begin Devbox Post-init Hook\n\n\n\n# If the user hasn't specified they want to handle the prompt themselves,\n# prepend to the prompt to make it clear we're in a devbox shell.\nif [ -z \"$DEVBOX_NO_PROMPT\" ]; then\n  export PS1=\"(devbox) $PS1\"\nfi\n\n# End Devbox Post-init Hook\n\n# Run plugin and user init hooks from the devbox.json directory.\nworking_dir=\"$(pwd)\"\ncd \"/path/to/projectDir\" || exit\n\n# Source the hooks file, which contains the project's init hooks and plugin hooks.\n. \"/path/to/projectDir/.devbox/gen/scripts/.hooks.sh\"\n\ncd \"$working_dir\" || exit\n\n# Add refresh alias (only if it doesn't already exist)\nif ! type refresh >/dev/null 2>&1; then\n  export DEVBOX_REFRESH_ALIAS_11c3c7a2e9a24e16e714a53a46351e31be8beac32de3f19854be1ef14e556903='eval \"$(devbox shellenv --preserve-path-stack -c \"/path/to/projectDir\")\" && hash -r'\n  alias refresh='eval \"$(devbox shellenv --preserve-path-stack -c \"/path/to/projectDir\")\" && hash -r'\nfi\n"
  },
  {
    "path": "internal/devbox/testdata/shellrc/zsh_zdotdir/env",
    "content": "simple=value\nspace=quote me\nquote=they said, \"lasers\"\nspecial=$`\"\\\nBASH_FUNC_echo_simple%%=() { echo \"${simple}\"; }\n"
  },
  {
    "path": "internal/devbox/testdata/shellrc/zsh_zdotdir/shellrc",
    "content": "# Set up the prompt\n\nautoload -Uz promptinit\npromptinit\n#prompt adam1\n\nsetopt histignorealldups sharehistory\n\n# Use emacs keybindings even if our EDITOR is set to vi\nbindkey -e\n\n# Keep 1000 lines of history within the shell and save it to ~/.zsh_history:\nHISTSIZE=1000\nSAVEHIST=1000\nHISTFILE=~/.zsh_history\n\n# Use modern completion system\nautoload -Uz compinit\ncompinit\n\nzstyle ':completion:*' auto-description 'specify: %d'\nzstyle ':completion:*' completer _expand _complete _correct _approximate\nzstyle ':completion:*' format 'Completing %d'\nzstyle ':completion:*' group-name ''\nzstyle ':completion:*' menu select=2\neval \"$(dircolors -b)\"\nzstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS}\nzstyle ':completion:*' list-colors ''\nzstyle ':completion:*' list-prompt %SAt %p: Hit TAB for more, or the character to insert%s\nzstyle ':completion:*' matcher-list '' 'm:{a-z}={A-Z}' 'm:{a-zA-Z}={A-Za-z}' 'r:|[._-]=* r:|=* l:|=*'\nzstyle ':completion:*' menu select=long\nzstyle ':completion:*' select-prompt %SScrolling active: current selection at %p%s\nzstyle ':completion:*' use-compctl false\nzstyle ':completion:*' verbose true\n\nzstyle ':completion:*:*:kill:*:processes' list-colors '=(#b) #([0-9]#)*=0=01;31'\nzstyle ':completion:*:kill:*' command 'ps -u $USER -o pid,%cpu,tty,cputime,cmd'\n"
  },
  {
    "path": "internal/devbox/testdata/shellrc/zsh_zdotdir/shellrc.golden",
    "content": "if [ -f testdata/shellrc/zsh_zdotdir/shellrc ]; then\n  local DEVBOX_ZDOTDIR=\"$ZDOTDIR\"\n  export ZDOTDIR=\"testdata/shellrc/zsh_zdotdir\"\n  . \"testdata/shellrc/zsh_zdotdir/shellrc\"\n  export ZDOTDIR=\"$DEVBOX_ZDOTDIR\"\nfi\n# Begin Devbox Post-init Hook\n\necho_simple () { echo \"${simple}\"; }\nexport -f echo_simple\nexport quote=\"they said, \\\"lasers\\\"\";\nexport simple=\"value\";\nexport space=\"quote me\";\nexport special=\"\\$\\`\\\"\\\\\";\n\n# If the user hasn't specified they want to handle the prompt themselves,\n# prepend to the prompt to make it clear we're in a devbox shell.\nif [ -z \"$DEVBOX_NO_PROMPT\" ]; then\n  export PS1=\"(devbox) $PS1\"\nfi\n\n# End Devbox Post-init Hook\n\n# Run plugin and user init hooks from the devbox.json directory.\nworking_dir=\"$(pwd)\"\ncd \"/path/to/projectDir\" || exit\n\n# Source the hooks file, which contains the project's init hooks and plugin hooks.\n. \"/path/to/projectDir/.devbox/gen/scripts/.hooks.sh\"\n\ncd \"$working_dir\" || exit\n\n# Add refresh alias (only if it doesn't already exist)\nif ! type refresh >/dev/null 2>&1; then\n  export DEVBOX_REFRESH_ALIAS_11c3c7a2e9a24e16e714a53a46351e31be8beac32de3f19854be1ef14e556903='eval \"$(devbox shellenv --preserve-path-stack -c \"/path/to/projectDir\")\" && hash -r'\n  alias refresh='eval \"$(devbox shellenv --preserve-path-stack -c \"/path/to/projectDir\")\" && hash -r'\nfi\n"
  },
  {
    "path": "internal/devbox/update.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devbox\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"slices\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/nix/nixprofile\"\n\t\"go.jetify.com/devbox/internal/plugin\"\n\t\"go.jetify.com/devbox/internal/searcher\"\n\t\"go.jetify.com/devbox/internal/shellgen\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\nfunc (d *Devbox) Update(ctx context.Context, opts devopt.UpdateOpts) error {\n\tif len(opts.Pkgs) == 0 || slices.Contains(opts.Pkgs, \"nixpkgs\") {\n\t\tif err := d.lockfile.UpdateStdenv(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// if nixpkgs is the only package to update, just return here.\n\t\tif len(opts.Pkgs) == 1 {\n\t\t\treturn nil\n\t\t}\n\t\t// Otherwise, remove nixpkgs and continue\n\t\topts.Pkgs = slices.DeleteFunc(opts.Pkgs, func(pkg string) bool {\n\t\t\treturn pkg == \"nixpkgs\"\n\t\t})\n\t}\n\n\tinputs, err := d.inputsToUpdate(opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpendingPackagesToUpdate := []*devpkg.Package{}\n\tfor _, pkg := range inputs {\n\t\tif pkg.IsLegacy() {\n\t\t\tfmt.Fprintf(d.stderr, \"Updating %s -> %s\\n\", pkg.Raw, pkg.LegacyToVersioned())\n\n\t\t\t// Get the package from the config to get the Platforms and ExcludedPlatforms later\n\t\t\tcfgPackage, ok := d.cfg.Root.GetPackage(pkg.Raw)\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"package %s not found in config\", pkg.Raw)\n\t\t\t}\n\n\t\t\tif err := d.Remove(ctx, pkg.Raw); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// Calling Add function with the original package names, since\n\t\t\t// Add will automatically append @latest if search is able to handle that.\n\t\t\t// If not, it will fallback to the nixpkg format.\n\t\t\tif err := d.Add(ctx, []string{pkg.Raw}, devopt.AddOpts{\n\t\t\t\tPlatforms:        cfgPackage.Platforms,\n\t\t\t\tExcludePlatforms: cfgPackage.ExcludedPlatforms,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tpendingPackagesToUpdate = append(pendingPackagesToUpdate, pkg)\n\t\t}\n\t}\n\n\tfor _, pkg := range pendingPackagesToUpdate {\n\t\tif _, _, isVersioned := searcher.ParseVersionedPackage(pkg.Raw); !isVersioned {\n\t\t\tif err = d.attemptToUpgradeFlake(pkg); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tif err = d.updateDevboxPackage(pkg); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tmode := update\n\tif opts.NoInstall {\n\t\tmode = noInstall\n\t}\n\tif err := d.ensureStateIsUpToDate(ctx, mode); err != nil {\n\t\treturn err\n\t}\n\n\t// I'm not entirely sure this is even needed, so ignoring the error.\n\t// It's definitely not needed for non-flakes. (which is 99.9% of packages)\n\t// It will return an error if .devbox/gen/flake is missing\n\t// TODO: Remove this if it's not needed.\n\t_ = nix.FlakeUpdate(shellgen.FlakePath(d))\n\n\t// fix any missing store paths.\n\tif err = d.FixMissingStorePaths(ctx); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn plugin.Update()\n}\n\nfunc (d *Devbox) inputsToUpdate(\n\topts devopt.UpdateOpts,\n) ([]*devpkg.Package, error) {\n\tif len(opts.Pkgs) == 0 {\n\t\treturn d.AllPackages(), nil\n\t}\n\n\tvar pkgsToUpdate []*devpkg.Package\n\tfor _, pkg := range opts.Pkgs {\n\t\tfound, err := d.findPackageByName(pkg)\n\t\tif opts.IgnoreMissingPackages && errors.Is(err, searcher.ErrNotFound) {\n\t\t\tcontinue\n\t\t} else if err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpkgsToUpdate = append(pkgsToUpdate, found)\n\t}\n\treturn pkgsToUpdate, nil\n}\n\nfunc (d *Devbox) updateDevboxPackage(pkg *devpkg.Package) error {\n\tresolved, err := d.lockfile.FetchResolvedPackage(pkg.Raw)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resolved == nil {\n\t\treturn nil\n\t}\n\n\treturn d.mergeResolvedPackageToLockfile(pkg, resolved, d.lockfile)\n}\n\nfunc (d *Devbox) mergeResolvedPackageToLockfile(\n\tpkg *devpkg.Package,\n\tresolved *lock.Package,\n\tlockfile *lock.File,\n) error {\n\texisting := lockfile.Packages[pkg.Raw]\n\tif existing == nil {\n\t\tux.Finfof(d.stderr, \"Resolved %s to %[1]s %[2]s\\n\", pkg, resolved.Resolved)\n\t\tlockfile.Packages[pkg.Raw] = resolved\n\t\treturn nil\n\t}\n\n\tif existing.Version != resolved.Version {\n\t\tif existing.LastModified > resolved.LastModified {\n\t\t\tux.Fwarningf(\n\t\t\t\td.stderr,\n\t\t\t\t\"Resolved version for %s has older last_modified time. Not updating\\n\",\n\t\t\t\tpkg,\n\t\t\t)\n\t\t\treturn nil\n\t\t}\n\t\tux.Finfof(d.stderr, \"Updating %s %s -> %s\\n\", pkg, existing.Version, resolved.Version)\n\t\tuseResolvedPackageInLockfile(lockfile, pkg, resolved, existing)\n\t\treturn nil\n\t}\n\n\t// Add any missing system infos for packages whose versions did not change.\n\tif lockfile.Packages[pkg.Raw].Systems == nil {\n\t\tlockfile.Packages[pkg.Raw].Systems = map[string]*lock.SystemInfo{}\n\t}\n\n\tuserSystem := nix.System()\n\tupdated := false\n\tfor sysName, newSysInfo := range resolved.Systems {\n\t\t// Check whether we are actually updating any system info.\n\t\tif sysName == userSystem {\n\t\t\t// The resolved pkg has a system info for the user's system, so add/overwrite it.\n\t\t\tif !newSysInfo.Equals(existing.Systems[userSystem]) {\n\t\t\t\t// We only guard this so that the ux messaging is accurate. We could overwrite every time.\n\t\t\t\tupdated = true\n\t\t\t}\n\t\t} else {\n\t\t\t// Add other system infos if they don't exist, or if we have a different StorePath. This may\n\t\t\t// overwrite an existing StorePath, but to ensure correctness we should ensure that all StorePaths\n\t\t\t// come from the same package version.\n\t\t\texistingSysInfo, exists := existing.Systems[sysName]\n\t\t\tif !exists || !existingSysInfo.Equals(newSysInfo) {\n\t\t\t\tupdated = true\n\t\t\t}\n\t\t}\n\t}\n\tif updated {\n\t\t// if we are updating the system info, then we should also update the other fields\n\t\tuseResolvedPackageInLockfile(lockfile, pkg, resolved, existing)\n\n\t\tux.Finfof(d.stderr, \"Updated system information for %s\\n\", pkg)\n\t\treturn nil\n\t}\n\n\tux.Finfof(d.stderr, \"Already up-to-date %s %s\\n\", pkg, existing.Version)\n\treturn nil\n}\n\n// attemptToUpgradeFlake attempts to upgrade a flake using `nix profile upgrade`\n// and prints an error if it fails, but does not propagate upgrade errors.\nfunc (d *Devbox) attemptToUpgradeFlake(pkg *devpkg.Package) error {\n\tprofilePath, err := d.profilePath()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tux.Finfof(\n\t\td.stderr,\n\t\t\"Attempting to upgrade %s using `nix profile upgrade`\\n\",\n\t\tpkg.Raw,\n\t)\n\n\terr = nixprofile.ProfileUpgrade(profilePath, pkg, d.lockfile)\n\tif err != nil {\n\t\tux.Fwarningf(\n\t\t\td.stderr,\n\t\t\t\"Failed to upgrade %s using `nix profile upgrade`: %s\\n\",\n\t\t\tpkg.Raw,\n\t\t\terr,\n\t\t)\n\t}\n\n\treturn nil\n}\n\nfunc useResolvedPackageInLockfile(\n\tlockfile *lock.File,\n\tpkg *devpkg.Package,\n\tresolved *lock.Package,\n\texisting *lock.Package,\n) {\n\tlockfile.Packages[pkg.Raw] = resolved\n\tlockfile.Packages[pkg.Raw].AllowInsecure = existing.AllowInsecure\n}\n"
  },
  {
    "path": "internal/devbox/update_test.go",
    "content": "package devbox\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/nix\"\n)\n\nfunc TestUpdateNewPackageIsAdded(t *testing.T) {\n\tdevbox := devboxForTesting(t)\n\n\traw := \"hello@1.2.3\"\n\tdevPkg := devpkg.PackageFromStringWithDefaults(raw, nil)\n\tresolved := &lock.Package{\n\t\tResolved: \"resolved-flake-reference\",\n\t}\n\tlockfile := &lock.File{\n\t\tPackages: map[string]*lock.Package{}, // empty\n\t}\n\n\terr := devbox.mergeResolvedPackageToLockfile(devPkg, resolved, lockfile)\n\trequire.NoError(t, err, \"update failed\")\n\n\trequire.Contains(t, lockfile.Packages, raw)\n}\n\nfunc TestUpdateNewCurrentSysInfoIsAdded(t *testing.T) {\n\tdevbox := devboxForTesting(t)\n\n\traw := \"hello@1.2.3\"\n\tsys := currentSystem(t)\n\tdevPkg := devpkg.PackageFromStringWithDefaults(raw, nil)\n\tresolved := &lock.Package{\n\t\tResolved: \"resolved-flake-reference\",\n\t\tSystems: map[string]*lock.SystemInfo{\n\t\t\tsys: {\n\t\t\t\tOutputs: []lock.Output{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"out\",\n\t\t\t\t\t\tDefault: true,\n\t\t\t\t\t\tPath:    \"store_path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tlockfile := &lock.File{\n\t\tPackages: map[string]*lock.Package{\n\t\t\traw: {\n\t\t\t\tResolved: \"resolved-flake-reference\",\n\t\t\t\t// No system infos.\n\t\t\t},\n\t\t},\n\t}\n\n\terr := devbox.mergeResolvedPackageToLockfile(devPkg, resolved, lockfile)\n\trequire.NoError(t, err, \"update failed\")\n\n\trequire.Contains(t, lockfile.Packages, raw)\n\trequire.Contains(t, lockfile.Packages[raw].Systems, sys)\n\trequire.Equal(t, \"store_path1\", lockfile.Packages[raw].Systems[sys].Outputs[0].Path)\n}\n\nfunc TestUpdateNewSysInfoIsAdded(t *testing.T) {\n\tdevbox := devboxForTesting(t)\n\n\traw := \"hello@1.2.3\"\n\tsys1 := currentSystem(t)\n\tsys2 := \"system2\"\n\tdevPkg := devpkg.PackageFromStringWithDefaults(raw, nil)\n\tresolved := &lock.Package{\n\t\tResolved: \"resolved-flake-reference\",\n\t\tSystems: map[string]*lock.SystemInfo{\n\t\t\tsys1: {\n\t\t\t\tOutputs: []lock.Output{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"out\",\n\t\t\t\t\t\tDefault: true,\n\t\t\t\t\t\tPath:    \"store_path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tsys2: {\n\t\t\t\tOutputs: []lock.Output{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"out\",\n\t\t\t\t\t\tDefault: true,\n\t\t\t\t\t\tPath:    \"store_path2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tlockfile := &lock.File{\n\t\tPackages: map[string]*lock.Package{\n\t\t\traw: {\n\t\t\t\tResolved: \"resolved-flake-reference\",\n\t\t\t\tSystems: map[string]*lock.SystemInfo{\n\t\t\t\t\tsys1: {\n\t\t\t\t\t\tOutputs: []lock.Output{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:    \"out\",\n\t\t\t\t\t\t\t\tDefault: true,\n\t\t\t\t\t\t\t\tPath:    \"store_path1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t// Missing sys2\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr := devbox.mergeResolvedPackageToLockfile(devPkg, resolved, lockfile)\n\trequire.NoError(t, err, \"update failed\")\n\n\trequire.Contains(t, lockfile.Packages, raw)\n\trequire.Contains(t, lockfile.Packages[raw].Systems, sys1)\n\trequire.Contains(t, lockfile.Packages[raw].Systems, sys2)\n\trequire.Equal(t, \"store_path2\", lockfile.Packages[raw].Systems[sys2].Outputs[0].Path)\n}\n\nfunc TestUpdateOtherSysInfoIsReplaced(t *testing.T) {\n\tdevbox := devboxForTesting(t)\n\n\traw := \"hello@1.2.3\"\n\tsys1 := currentSystem(t)\n\tsys2 := \"system2\"\n\tdevPkg := devpkg.PackageFromStringWithDefaults(raw, nil)\n\tresolved := &lock.Package{\n\t\tResolved: \"resolved-flake-reference\",\n\t\tSystems: map[string]*lock.SystemInfo{\n\t\t\tsys1: {\n\t\t\t\tOutputs: []lock.Output{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"out\",\n\t\t\t\t\t\tDefault: true,\n\t\t\t\t\t\tPath:    \"store_path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tsys2: {\n\t\t\t\tOutputs: []lock.Output{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"out\",\n\t\t\t\t\t\tDefault: true,\n\t\t\t\t\t\tPath:    \"store_path2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tlockfile := &lock.File{\n\t\tPackages: map[string]*lock.Package{\n\t\t\traw: {\n\t\t\t\tResolved: \"resolved-flake-reference\",\n\t\t\t\tSystems: map[string]*lock.SystemInfo{\n\t\t\t\t\tsys1: {\n\t\t\t\t\t\tOutputs: []lock.Output{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:    \"out\",\n\t\t\t\t\t\t\t\tDefault: true,\n\t\t\t\t\t\t\t\tPath:    \"store_path1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tsys2: {\n\t\t\t\t\t\tOutputs: []lock.Output{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:    \"out\",\n\t\t\t\t\t\t\t\tDefault: true,\n\t\t\t\t\t\t\t\tPath:    \"mismatching_store_path\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr := devbox.mergeResolvedPackageToLockfile(devPkg, resolved, lockfile)\n\trequire.NoError(t, err, \"update failed\")\n\n\trequire.Contains(t, lockfile.Packages, raw)\n\trequire.Contains(t, lockfile.Packages[raw].Systems, sys1)\n\trequire.Contains(t, lockfile.Packages[raw].Systems, sys2)\n\trequire.Equal(t, \"store_path1\", lockfile.Packages[raw].Systems[sys1].Outputs[0].Path)\n\trequire.Equal(t, \"store_path2\", lockfile.Packages[raw].Systems[sys2].Outputs[0].Path)\n}\n\nfunc currentSystem(*testing.T) string {\n\tsys := nix.System() // NOTE: we could mock this too, if it helps.\n\treturn sys\n}\n"
  },
  {
    "path": "internal/devbox/util.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devbox\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/xdg\"\n)\n\nconst processComposeVersion = \"1.87.0\"\n\nvar utilProjectConfigPath string\n\nfunc initDevboxUtilityProject(ctx context.Context, stderr io.Writer) error {\n\tdevboxUtilityProjectPath, err := ensureDevboxUtilityConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbox, err := Open(&devopt.Opts{\n\t\tDir:    devboxUtilityProjectPath,\n\t\tStderr: stderr,\n\t})\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// Add all utilities here.\n\tutilities := []string{\n\t\t\"process-compose@\" + processComposeVersion,\n\t}\n\tif err = box.Add(ctx, utilities, devopt.AddOpts{}); err != nil {\n\t\treturn err\n\t}\n\n\treturn box.Install(ctx)\n}\n\nfunc ensureDevboxUtilityConfig() (string, error) {\n\tif utilProjectConfigPath != \"\" {\n\t\treturn utilProjectConfigPath, nil\n\t}\n\n\tpath, err := utilityDataPath()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\terr = EnsureConfig(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Avoids unnecessarily initializing the config again by caching the path\n\tutilProjectConfigPath = path\n\n\treturn path, nil\n}\n\nfunc utilityLookPath(binName string) (string, error) {\n\tbinPath, err := utilityBinPath()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tabsPath := filepath.Join(binPath, binName)\n\t_, err = os.Stat(absPath)\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn \"\", err\n\t}\n\treturn absPath, nil\n}\n\nfunc utilityDataPath() (string, error) {\n\tpath := xdg.DataSubpath(\"devbox/util\")\n\treturn path, errors.WithStack(os.MkdirAll(path, 0o755))\n}\n\nfunc utilityNixProfilePath() (string, error) {\n\tpath, err := utilityDataPath()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(path, \".devbox/nix/profile\"), nil\n}\n\nfunc utilityBinPath() (string, error) {\n\tnixProfilePath, err := utilityNixProfilePath()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn filepath.Join(nixProfilePath, \"default/bin\"), nil\n}\n"
  },
  {
    "path": "internal/devconfig/config.go",
    "content": "package devconfig\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"github.com/samber/lo/mutable\"\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/cachehash\"\n\t\"go.jetify.com/devbox/internal/devbox/shellcmd\"\n\t\"go.jetify.com/devbox/internal/devconfig/configfile\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/plugin\"\n)\n\n// ErrNotFound occurs when [Open] or [Find] cannot find a devbox config file\n// after searching a directory (and possibly its parent directories).\nvar ErrNotFound = errors.New(\"no devbox config file found\")\n\n// errIsDirectory indicates that a file can't be opened because it's a\n// directory.\nconst errIsDirectory = syscall.EISDIR\n\n// errNotDirectory indicates that a file can't be opened because the directory\n// portion of its path is not a directory.\nconst errNotDirectory = syscall.ENOTDIR\n\n// Config represents a base devbox.json as well as any included plugins it may have.\ntype Config struct {\n\tRoot configfile.ConfigFile\n\n\tpluginData *plugin.PluginOnlyData // pointer by design, to allow for nil\n\n\tincluded []*Config\n}\n\nconst defaultInitHook = \"echo 'Welcome to devbox!' > /dev/null\"\n\nfunc DefaultConfig() *Config {\n\tcfg, err := loadBytes([]byte(fmt.Sprintf(`{\n\t\t\"$schema\": \"https://raw.githubusercontent.com/jetify-com/devbox/%s/.schema/devbox.schema.json\",\n\t\t\"packages\": [],\n\t\t\"shell\": {\n\t\t\t\"init_hook\": [\n\t\t\t\t\"%s\"\n\t\t\t],\n\t\t\t\"scripts\": {\n\t\t\t\t\"test\": [\n\t\t\t\t\t\"echo \\\"Error: no test specified\\\" && exit 1\"\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t}\n\t`,\n\t\tlo.Ternary(build.IsDev, \"main\", build.Version),\n\t\tdefaultInitHook,\n\t)))\n\tif err != nil {\n\t\tpanic(\"default devbox.json is invalid: \" + err.Error())\n\t}\n\treturn cfg\n}\n\nfunc IsDefault(path string) bool {\n\tcfg, err := readFromFile(path)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn cfg.Root.Equals(&DefaultConfig().Root)\n}\n\n// Open loads a Devbox config from a file or project directory. If path is a\n// directory, Open looks for a well-known config name (such as devbox.json)\n// within it. The error will be [ErrNotFound] if path is a valid directory\n// without a config file.\n//\n// Open does not recursively search outside of path. See [Find] to load a config\n// by walking up the directory tree.\nfunc Open(path string) (*Config, error) {\n\tstart := time.Now()\n\tslog.Debug(\"searching for config file (excluding parent directories)\", \"path\", path)\n\n\tcfg, err := open(path)\n\n\tif err == nil {\n\t\tslog.Debug(\"config file found\", \"path\", cfg.Root.AbsRootPath, \"dur\", time.Since(start))\n\t} else {\n\t\tslog.Error(\"config file search error\", \"err\", err.Error(), \"dur\", time.Since(start))\n\t}\n\treturn cfg, err\n}\n\nfunc open(path string) (*Config, error) {\n\t// First try the happy path by assuming that path is a directory\n\t// containing a devbox.json.\n\tcfg, err := searchDir(path)\n\tif errors.Is(err, ErrNotFound) || errors.Is(err, errNotDirectory) {\n\t\t// Try reading path directly as a config file.\n\t\tslog.Debug(\"trying config file\", \"path\", path)\n\t\tcfg, err = readFromFile(path)\n\t\tif errors.Is(err, errIsDirectory) {\n\t\t\treturn nil, ErrNotFound\n\t\t}\n\t}\n\treturn cfg, err\n}\n\n// Find is like [Open] except it recursively searches up the directory tree,\n// starting in path. It returns [ErrNotFound] if path is a valid directory and\n// neither it nor any of its parents contain a config file.\n//\n// Find stops searching as soon as it encounters a file with a well-known config\n// name (such as devbox.json), even if that config fails to load.\nfunc Find(path string) (*Config, error) {\n\tstart := time.Now()\n\tslog.Debug(\"searching for config file (including parent directories)\", \"path\", path)\n\n\tcfg, err := open(path)\n\tif errors.Is(err, ErrNotFound) {\n\t\tcfg, err = searchParentDirs(path)\n\t}\n\n\tif err == nil {\n\t\tslog.Debug(\"config file found\", \"path\", cfg.Root.AbsRootPath, \"dur\", time.Since(start))\n\t} else {\n\t\tslog.Error(\"config file search error\", \"err\", err.Error(), \"dur\", time.Since(start))\n\t}\n\treturn cfg, err\n}\n\n// searchDir looks for a config file in dir. It does not search parent\n// directories.\nfunc searchDir(dir string) (*Config, error) {\n\ttry := []string{configfile.DefaultName}\n\tfor _, name := range try {\n\t\tpath := filepath.Join(dir, name)\n\t\tslog.Debug(\"trying config file\", \"path\", path)\n\n\t\tcfg, err := readFromFile(path)\n\t\tif err == nil {\n\t\t\treturn cfg, nil\n\t\t}\n\n\t\t// Keep searching for other valid config filenames.\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tcontinue\n\t\t}\n\t\t// Ignore directories named devbox.json.\n\t\tif errors.Is(err, errIsDirectory) {\n\t\t\tcontinue\n\t\t}\n\t\t// Stop if we found a config but couldn't load it.\n\t\treturn cfg, err\n\t}\n\treturn nil, ErrNotFound\n}\n\n// searchParentDirs recursively searches parent directories for a config. It\n// starts with filepath.Dir(path) and does not search path itself.\nfunc searchParentDirs(path string) (cfg *Config, err error) {\n\tabs, err := filepath.Abs(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"devconfig: search parent directories: %v\", err)\n\t}\n\n\terr = ErrNotFound\n\tfor abs != \"/\" && errors.Is(err, ErrNotFound) {\n\t\tabs = filepath.Dir(abs)\n\t\tcfg, err = searchDir(abs)\n\t}\n\treturn cfg, err\n}\n\nfunc readFromFile(path string) (*Config, error) {\n\tb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconfig, err := loadBytes(b)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconfig.Root.AbsRootPath, err = filepath.Abs(path)\n\treturn config, err\n}\n\nfunc LoadConfigFromURL(ctx context.Context, url string) (*Config, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tres, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\n\tdata, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn loadBytes(data)\n}\n\nfunc loadBytes(b []byte) (*Config, error) {\n\troot, err := configfile.LoadBytes(b)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Config{\n\t\tRoot: *root,\n\t}, nil\n}\n\nfunc (c *Config) LoadRecursive(lockfile *lock.File) error {\n\treturn c.loadRecursive(lockfile, map[string]bool{}, \"\" /*cyclePath*/)\n}\n\n// loadRecursive loads all the included plugins and their included plugins, etc.\n// seen should be a cloned map because loading plugins twice is allowed if they\n// are in different paths.\nfunc (c *Config) loadRecursive(\n\tlockfile *lock.File,\n\tseen map[string]bool,\n\tcyclePath string,\n) error {\n\tincluded := make([]*Config, 0, len(c.Root.Include))\n\n\tfor _, includeRef := range c.Root.Include {\n\t\tpluginConfig, err := plugin.LoadConfigFromInclude(\n\t\t\tincludeRef, lockfile, filepath.Dir(c.Root.AbsRootPath))\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\n\t\tnewCyclePath := fmt.Sprintf(\"%s -> %s\", cyclePath, includeRef)\n\t\tif seen[pluginConfig.Source.Hash()] {\n\t\t\t// Note that duplicate includes are allowed if they are in different paths\n\t\t\t// e.g. 2 different plugins can include the same plugin.\n\t\t\t// We do not allow a single plugin to include duplicates.\n\t\t\treturn errors.Errorf(\n\t\t\t\t\"circular or duplicate include detected:\\n%s\", newCyclePath)\n\t\t}\n\t\tseen[pluginConfig.Source.Hash()] = true\n\n\t\tincludable := createIncludableFromPluginConfig(pluginConfig)\n\n\t\tif err := includable.loadRecursive(\n\t\t\tlockfile, maps.Clone(seen), newCyclePath); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\n\t\tincluded = append(included, includable)\n\t}\n\n\tbuiltIns, err := plugin.GetBuiltinsForPackages(\n\t\tc.Root.TopLevelPackages(),\n\t\tlockfile,\n\t)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tfor _, builtIn := range builtIns {\n\t\tincludable := &Config{\n\t\t\tRoot:       builtIn.ConfigFile,\n\t\t\tpluginData: &builtIn.PluginOnlyData,\n\t\t}\n\t\tnewCyclePath := fmt.Sprintf(\"%s -> %s\", cyclePath, builtIn.Source.LockfileKey())\n\t\tif err := includable.loadRecursive(\n\t\t\tlockfile, maps.Clone(seen), newCyclePath); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\tincluded = append(included, includable)\n\t}\n\n\tc.included = included\n\treturn nil\n}\n\nfunc (c *Config) PackageMutator() *configfile.PackagesMutator {\n\treturn &c.Root.PackagesMutator\n}\n\nfunc (c *Config) IncludedPluginConfigs() []*plugin.Config {\n\tconfigs := []*plugin.Config{}\n\tfor _, i := range c.included {\n\t\tconfigs = append(configs, i.IncludedPluginConfigs()...)\n\t}\n\tif c.pluginData != nil {\n\t\tconfigs = append(configs, &plugin.Config{\n\t\t\tConfigFile:     c.Root,\n\t\t\tPluginOnlyData: *c.pluginData,\n\t\t})\n\t}\n\treturn configs\n}\n\n// Returns all packages including those from included plugins.\n// If includeRemovedTriggerPackages is true, then trigger packages that have\n// been removed will also be returned. These are only used for built-ins\n// (e.g. php) when the plugin creates a flake that is meant to replace the\n// original package.\nfunc (c *Config) Packages(\n\tincludeRemovedTriggerPackages bool,\n) []configfile.Package {\n\tpackages := []configfile.Package{}\n\tpackagesToRemove := map[string]bool{}\n\n\tfor _, i := range c.included {\n\t\tpackages = append(packages, i.Packages(includeRemovedTriggerPackages)...)\n\t\tif i.pluginData.RemoveTriggerPackage && !includeRemovedTriggerPackages {\n\t\t\tpackagesToRemove[i.pluginData.Source.LockfileKey()] = true\n\t\t}\n\t}\n\n\t// Packages to remove in built ins only affect the devbox.json where they are defined.\n\t// They should not remove packages that are part of other imports.\n\tfor _, pkg := range c.Root.TopLevelPackages() {\n\t\tif !packagesToRemove[pkg.VersionedName()] {\n\t\t\tpackages = append(packages, pkg)\n\t\t}\n\t}\n\n\t// Keep only the last occurrence of each package (by name).\n\tmutable.Reverse(packages)\n\tpackages = lo.UniqBy(\n\t\tpackages,\n\t\tfunc(p configfile.Package) string { return p.Name },\n\t)\n\tmutable.Reverse(packages)\n\n\treturn packages\n}\n\nfunc (c *Config) NixPkgsCommitHash() string {\n\treturn c.Root.NixPkgsCommitHash()\n}\n\nfunc (c *Config) Env() map[string]string {\n\tenv := map[string]string{}\n\tfor _, i := range c.included {\n\t\texpandedEnvFromPlugin := OSExpandIfPossible(i.Env(), env)\n\t\tmaps.Copy(env, expandedEnvFromPlugin)\n\t}\n\trootConfigEnv := OSExpandIfPossible(c.Root.Env, env)\n\tmaps.Copy(env, rootConfigEnv)\n\treturn env\n}\n\nfunc (c *Config) InitHook() *shellcmd.Commands {\n\tcommands := shellcmd.Commands{}\n\tfor _, i := range c.included {\n\t\tcommands.Cmds = append(commands.Cmds, i.InitHook().Cmds...)\n\t}\n\tcommands.Cmds = append(commands.Cmds, c.Root.InitHook().Cmds...)\n\treturn &commands\n}\n\nfunc (c *Config) Scripts() configfile.Scripts {\n\tscripts := configfile.Scripts{}\n\tfor _, i := range c.included {\n\t\tmaps.Copy(scripts, i.Scripts())\n\t}\n\tmaps.Copy(scripts, c.Root.Scripts())\n\treturn scripts\n}\n\nfunc (c *Config) Hash() (string, error) {\n\tdata := []byte{}\n\tfor _, i := range c.included {\n\t\thash, err := i.Hash()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdata = append(data, hash...)\n\t}\n\thash, err := c.Root.Hash()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdata = append(data, hash...)\n\treturn cachehash.Bytes(data), nil\n}\n\nfunc (c *Config) IsEnvsecEnabled() bool {\n\tfor _, i := range c.included {\n\t\tif i.IsEnvsecEnabled() {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn c.Root.IsEnvsecEnabled()\n}\n\nfunc createIncludableFromPluginConfig(pluginConfig *plugin.Config) *Config {\n\tincludable := &Config{\n\t\tRoot:       pluginConfig.ConfigFile,\n\t\tpluginData: &pluginConfig.PluginOnlyData,\n\t}\n\tif localPlugin, ok := pluginConfig.Source.(*plugin.LocalPlugin); ok {\n\t\tincludable.Root.AbsRootPath = localPlugin.Path()\n\t}\n\treturn includable\n}\n\nfunc OSExpandIfPossible(env, existingEnv map[string]string) map[string]string {\n\tmapping := func(value string) string {\n\t\t// If the value is not set in existingEnv, return the value wrapped in ${...}\n\t\tif existingEnv == nil || existingEnv[value] == \"\" {\n\t\t\treturn fmt.Sprintf(\"${%s}\", value)\n\t\t}\n\t\treturn existingEnv[value]\n\t}\n\n\tres := map[string]string{}\n\tfor k, v := range env {\n\t\tres[k] = os.Expand(v, mapping)\n\t}\n\treturn res\n}\n"
  },
  {
    "path": "internal/devconfig/config_test.go",
    "content": "package devconfig\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/tailscale/hujson\"\n\t\"go.jetify.com/devbox/internal/devconfig/configfile\"\n)\n\nfunc TestOpen(t *testing.T) {\n\tt.Run(\"Dir\", func(t *testing.T) {\n\t\troot, _, _ := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\n\t\tcfg, err := Open(root)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Open(%q) error: %v\", root, err)\n\t\t}\n\t\tgotDir := filepath.Dir(cfg.Root.AbsRootPath)\n\t\tif gotDir != root {\n\t\t\tt.Errorf(\"filepath.Dir(cfg.Root.AbsRootPath) = %q, want %q\", gotDir, root)\n\t\t}\n\t})\n\tt.Run(\"File\", func(t *testing.T) {\n\t\troot, _, _ := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\t\tpath := filepath.Join(root, \"devbox.json\")\n\n\t\tcfg, err := Open(path)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Open(%q) error: %v\", path, err)\n\t\t}\n\t\tgotDir := filepath.Dir(cfg.Root.AbsRootPath)\n\t\tif gotDir != root {\n\t\t\tt.Errorf(\"filepath.Dir(cfg.Root.AbsRootPath) = %q, want %q\", gotDir, root)\n\t\t}\n\t})\n}\n\nfunc TestOpenError(t *testing.T) {\n\tt.Run(\"NotExist\", func(t *testing.T) {\n\t\troot, _, _ := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\n\t\tpath := filepath.Join(root, \"notafile.json\")\n\t\tcfg, err := Open(path)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"Open(%q) = %q, want error\", root, cfg.Root.AbsRootPath)\n\t\t}\n\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\tt.Error(\"errors.Is(err, fs.ErrNotExist) = false, want true\")\n\t\t}\n\t\tif errors.Is(err, ErrNotFound) {\n\t\t\tt.Error(\"errors.Is(err, ErrNotFound) = true, want false\")\n\t\t}\n\t})\n\tt.Run(\"NotFound\", func(t *testing.T) {\n\t\troot, _, _ := mkNestedDirs(t)\n\n\t\tcfg, err := Open(root)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"Open(%q) = %q, want error\", root, cfg.Root.AbsRootPath)\n\t\t}\n\t\tif !errors.Is(err, ErrNotFound) {\n\t\t\tt.Error(\"errors.Is(err, ErrNotFound) = false, want true\")\n\t\t}\n\t})\n\tt.Run(\"ParentNotFound\", func(t *testing.T) {\n\t\troot, child, _ := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\n\t\tcfg, err := Open(child)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"Open(%q) = %q, want error\", root, cfg.Root.AbsRootPath)\n\t\t}\n\t\tif !errors.Is(err, ErrNotFound) {\n\t\t\tt.Error(\"errors.Is(err, ErrNotFound) = false, want true\")\n\t\t}\n\t})\n}\n\nfunc TestFind(t *testing.T) {\n\tt.Run(\"StartInSameDir\", func(t *testing.T) {\n\t\troot, child, _ := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\t\tif _, err := Init(child); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", child, err)\n\t\t}\n\n\t\tcfg, err := Find(child)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Find(%q) error: %v\", child, err)\n\t\t}\n\t\tgotDir := filepath.Dir(cfg.Root.AbsRootPath)\n\t\tif gotDir != child {\n\t\t\tt.Errorf(\"filepath.Dir(cfg.Root.AbsRootPath) = %q, want %q\", gotDir, child)\n\t\t}\n\t})\n\tt.Run(\"StartInChildDir\", func(t *testing.T) {\n\t\troot, child, _ := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\n\t\tcfg, err := Find(child)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Find(%q) error: %v\", child, err)\n\t\t}\n\t\tgotDir := filepath.Dir(cfg.Root.AbsRootPath)\n\t\tif gotDir != root {\n\t\t\tt.Errorf(\"filepath.Dir(cfg.Root.AbsRootPath) = %q, want %q\", gotDir, root)\n\t\t}\n\t})\n\tt.Run(\"StartInNestedChildDir\", func(t *testing.T) {\n\t\troot, child, nested := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\t\tif _, err := Init(child); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", child, err)\n\t\t}\n\n\t\tcfg, err := Find(nested)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Find(%q) error: %v\", nested, err)\n\t\t}\n\t\tgotDir := filepath.Dir(cfg.Root.AbsRootPath)\n\t\tif gotDir != child {\n\t\t\tt.Errorf(\"filepath.Dir(cfg.Root.AbsRootPath) = %q, want %q\", gotDir, child)\n\t\t}\n\t})\n\tt.Run(\"IgnoreDirsWithMatchingName\", func(t *testing.T) {\n\t\troot, child, _ := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\n\t\ttrickyDir := filepath.Join(child, \"devbox.json\")\n\t\tperm := fs.FileMode(0o777)\n\t\tif err := os.Mkdir(trickyDir, perm); err != nil {\n\t\t\tt.Fatalf(\"Mkdir(%q, %O) error: %v\", trickyDir, perm, err)\n\t\t}\n\n\t\tcfg, err := Find(child)\n\t\tif errors.Is(err, errIsDirectory) {\n\t\t\tt.Fatalf(\"Find(%q) did not ignore a directory named devbox.json: %v\", child, err)\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Find(%q) error: %v\", child, err)\n\t\t}\n\t\tgotDir := filepath.Dir(cfg.Root.AbsRootPath)\n\t\tif gotDir != root {\n\t\t\tt.Errorf(\"filepath.Dir(cfg.Root.AbsRootPath) = %q, want %q\", gotDir, root)\n\t\t}\n\t})\n\tt.Run(\"ExactFile\", func(t *testing.T) {\n\t\troot, _, _ := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\n\t\tpath := filepath.Join(root, \"devbox.json\")\n\t\tcfg, err := Find(path)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Find(%q) error: %v\", path, err)\n\t\t}\n\t\tif cfg.Root.AbsRootPath != path {\n\t\t\tt.Errorf(\"cfg.Root.AbsRootPath = %q, want %q\", cfg.Root.AbsRootPath, path)\n\t\t}\n\t})\n}\n\nfunc TestFindError(t *testing.T) {\n\tt.Run(\"NotExist\", func(t *testing.T) {\n\t\troot, _, _ := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\n\t\tpath := filepath.Join(root, \"notafile.json\")\n\t\tcfg, err := Find(path)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"Find(%q) = %q, want error\", path, cfg.Root.AbsRootPath)\n\t\t}\n\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\tt.Error(\"errors.Is(err, fs.ErrNotExist) = false, want true\")\n\t\t}\n\t\tif errors.Is(err, ErrNotFound) {\n\t\t\tt.Error(\"errors.Is(err, ErrNotFound) = true, want false\")\n\t\t}\n\t})\n\tt.Run(\"NotFound\", func(t *testing.T) {\n\t\troot, child, _ := mkNestedDirs(t)\n\t\tif _, err := Init(child); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\n\t\tcfg, err := Find(root)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"Find(%q) = %q, want error\", root, cfg.Root.AbsRootPath)\n\t\t}\n\t\tif !errors.Is(err, ErrNotFound) {\n\t\t\tt.Error(\"errors.Is(err, ErrNotFound) = false, want true\")\n\t\t}\n\t})\n\tt.Run(\"Permissions\", func(t *testing.T) {\n\t\troot, child, _ := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\t\tif _, err := Init(child); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", child, err)\n\t\t}\n\t\tpath := filepath.Join(child, \"devbox.json\")\n\t\tif err := os.Chmod(path, 0o000); err != nil {\n\t\t\tt.Fatalf(\"os.Chmod(%q, 0o000) error: %v\", path, err)\n\t\t}\n\t\tt.Cleanup(func() { _ = os.Chmod(path, 0o666) })\n\n\t\tcfg, err := Find(child)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"Find(%q) = %q, want error\", child, cfg.Root.AbsRootPath)\n\t\t}\n\t\tif !errors.Is(err, fs.ErrPermission) {\n\t\t\tt.Error(\"errors.Is(err, fs.ErrPermission) = false, want true\")\n\t\t}\n\t})\n\tt.Run(\"ExactFileBadSyntax\", func(t *testing.T) {\n\t\troot, _, _ := mkNestedDirs(t)\n\n\t\tvar (\n\t\t\tpath = filepath.Join(root, \"devbox.json\")\n\t\t\tdata = []byte(\"this isn't json!\")\n\t\t\tperm = fs.FileMode(0o666)\n\t\t)\n\t\tif err := os.WriteFile(path, data, perm); err != nil {\n\t\t\tt.Fatalf(\"os.WriteFile(%q, []byte(%q), %O) error: %v\", path, data, perm, err)\n\t\t}\n\n\t\tcfg, err := Find(path)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"Find(%q) = %q, want error\", path, cfg.Root.AbsRootPath)\n\t\t}\n\t})\n\tt.Run(\"ExactFilePermissions\", func(t *testing.T) {\n\t\troot, _, _ := mkNestedDirs(t)\n\t\tif _, err := Init(root); err != nil {\n\t\t\tt.Fatalf(\"Init(%q) error: %v\", root, err)\n\t\t}\n\t\tpath := filepath.Join(root, \"devbox.json\")\n\t\tif err := os.Chmod(path, 0o000); err != nil {\n\t\t\tt.Fatalf(\"os.Chmod(%q, 0o000) error: %v\", path, err)\n\t\t}\n\t\tt.Cleanup(func() { _ = os.Chmod(path, 0o666) })\n\n\t\tcfg, err := Find(path)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"Find(%q) = %q, want error\", path, cfg.Root.AbsRootPath)\n\t\t}\n\t\tif !errors.Is(err, fs.ErrPermission) {\n\t\t\tt.Error(\"errors.Is(err, fs.ErrPermission) = false, want true\")\n\t\t}\n\t})\n}\n\n// mkNestedDirs sets up a nested directory structure for Find and Open tests.\nfunc mkNestedDirs(t *testing.T) (root, child, nested string) {\n\tt.Helper()\n\n\troot = t.TempDir()\n\tchild = filepath.Join(root, \"child\")\n\tnested = filepath.Join(child, \"nested\")\n\tt.Cleanup(func() { _ = os.RemoveAll(root) })\n\n\tperm := fs.FileMode(0o777)\n\tif err := os.MkdirAll(nested, perm); err != nil {\n\t\tt.Fatalf(\"os.MkdirAll(%q, %O) error: %v\", nested, perm, err)\n\t}\n\treturn root, child, nested\n}\n\nfunc TestDefault(t *testing.T) {\n\tpath := filepath.Join(t.TempDir())\n\tcfg := DefaultConfig()\n\tinBytes := cfg.Root.Bytes()\n\tif _, err := hujson.Parse(inBytes); err != nil {\n\t\tt.Fatalf(\"default config JSON is invalid: %v\\n%s\", err, inBytes)\n\t}\n\terr := cfg.Root.SaveTo(path)\n\tif err != nil {\n\t\tt.Fatal(\"got save error:\", err)\n\t}\n\tout, err := Open(filepath.Join(path, configfile.DefaultName))\n\tif err != nil {\n\t\tt.Fatal(\"got load error:\", err)\n\t}\n\tif diff := cmp.Diff(\n\t\tcfg,\n\t\tout,\n\t\tcmpopts.IgnoreUnexported(configfile.ConfigFile{}, configfile.PackagesMutator{}, Config{}),\n\t\tcmpopts.IgnoreFields(configfile.ConfigFile{}, \"AbsRootPath\"),\n\t); diff != \"\" {\n\t\tt.Errorf(\"configs not equal (-in +out):\\n%s\", diff)\n\t}\n\n\toutBytes := out.Root.Bytes()\n\tif _, err := hujson.Parse(outBytes); err != nil {\n\t\tt.Fatalf(\"loaded default config JSON is invalid: %v\\n%s\", err, outBytes)\n\t}\n\tif string(inBytes) != string(outBytes) {\n\t\tt.Errorf(\"got different JSON after load/save/load:\\ninput:\\n%s\\noutput:\\n%s\", inBytes, outBytes)\n\t}\n}\n\nfunc TestOSExpandIfPossible(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tenv         map[string]string\n\t\texistingEnv map[string]string\n\t\twant        map[string]string\n\t}{\n\t\t{\n\t\t\tname: \"basic expansion\",\n\t\t\tenv: map[string]string{\n\t\t\t\t\"FOO\": \"$BAR\",\n\t\t\t\t\"BAZ\": \"${QUX}\",\n\t\t\t},\n\t\t\texistingEnv: map[string]string{\n\t\t\t\t\"BAR\": \"bar_value\",\n\t\t\t\t\"QUX\": \"qux_value\",\n\t\t\t},\n\t\t\twant: map[string]string{\n\t\t\t\t\"FOO\": \"bar_value\",\n\t\t\t\t\"BAZ\": \"qux_value\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"missing values remain as template\",\n\t\t\tenv: map[string]string{\n\t\t\t\t\"FOO\": \"$BAR\",\n\t\t\t\t\"BAZ\": \"${QUX}\",\n\t\t\t},\n\t\t\texistingEnv: map[string]string{\n\t\t\t\t\"BAR\": \"bar_value\",\n\t\t\t\t// QUX is missing\n\t\t\t},\n\t\t\twant: map[string]string{\n\t\t\t\t\"FOO\": \"bar_value\",\n\t\t\t\t\"BAZ\": \"${QUX}\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"nil existing env\",\n\t\t\tenv: map[string]string{\n\t\t\t\t\"FOO\": \"$BAR\",\n\t\t\t\t\"BAZ\": \"${QUX}\",\n\t\t\t},\n\t\t\texistingEnv: nil,\n\t\t\twant: map[string]string{\n\t\t\t\t\"FOO\": \"${BAR}\",\n\t\t\t\t\"BAZ\": \"${QUX}\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty existing env\",\n\t\t\tenv: map[string]string{\n\t\t\t\t\"FOO\": \"$BAR\",\n\t\t\t},\n\t\t\texistingEnv: map[string]string{},\n\t\t\twant: map[string]string{\n\t\t\t\t\"FOO\": \"${BAR}\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed literal and variable\",\n\t\t\tenv: map[string]string{\n\t\t\t\t\"FOO\": \"prefix_${BAR}_suffix\",\n\t\t\t},\n\t\t\texistingEnv: map[string]string{\n\t\t\t\t\"BAR\": \"bar_value\",\n\t\t\t},\n\t\t\twant: map[string]string{\n\t\t\t\t\"FOO\": \"prefix_bar_value_suffix\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"path special case\",\n\t\t\tenv: map[string]string{\n\t\t\t\t\"FOO\": \"/my/config:$FOO\",\n\t\t\t},\n\t\t\texistingEnv: map[string]string{\n\t\t\t\t\"FOO\": \"/my/plugin:$FOO\",\n\t\t\t},\n\t\t\twant: map[string]string{\n\t\t\t\t\"FOO\": \"/my/config:/my/plugin:$FOO\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := OSExpandIfPossible(tt.env, tt.existingEnv)\n\t\t\tif len(got) != len(tt.want) {\n\t\t\t\tt.Errorf(\"OSExpandIfPossible() got %v entries, want %v entries\", len(got), len(tt.want))\n\t\t\t}\n\t\t\tfor k, v := range tt.want {\n\t\t\t\tif got[k] != v {\n\t\t\t\t\tt.Errorf(\"OSExpandIfPossible() for key %q = %q, want %q\", k, got[k], v)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/devconfig/configfile/ast.go",
    "content": "package configfile\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"regexp\"\n\t\"slices\"\n\n\t\"github.com/tailscale/hujson\"\n)\n\n// configAST is a hujson syntax tree that represents a devbox.json\n// configuration. An AST allows the CLI to modify specific parts of a user's\n// devbox.json instead of overwriting the entire file. This is important\n// because a devbox.json can have user comments that must be preserved when\n// saving changes.\n//\n//   - Unmarshalling is still done with encoding/json.\n//   - Marshalling is done by calling configAST.root.Pack to encode the AST as\n//     hujson/JWCC. Therefore, any changes to a Config struct will NOT\n//     automatically be marshaled back to JSON. Support for modifying a part of\n//     the JSON must be explicitly implemented in configAST.\n//   - Validation with the AST is complex, so it doesn't do any. It will happily\n//     append duplicate object keys and panic on invalid types. The higher-level\n//     Config type is responsible for tracking state and making valid edits to\n//     the AST.\n//\n// Be aware that there are 4 ways of representing a package in devbox.json that\n// the AST needs to handle:\n//\n//  1. [\"name\"] or [\"name@version\"] (versioned name array)\n//  2. {\"name\": \"version\"} (packages object member with version string)\n//  3. {\"name\": {\"version\": \"1.2.3\"}} (packages object member with package object)\n//  4. {\"github:F1bonacc1/process-compose/v0.40.2\": {}} (packages object member with flakeref)\ntype configAST struct {\n\troot hujson.Value\n}\n\n// parseConfig parses the bytes of a devbox.json and returns a syntax tree.\nfunc parseConfig(b []byte) (*configAST, error) {\n\troot, err := hujson.Parse(b)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &configAST{root: root}, nil\n}\n\n// packagesField gets the \"packages\" field, initializing it if necessary. The\n// member value will either be an array of strings or an object. When it's an\n// object, the keys will always be package names and the values will be a\n// string or another object. Examples are:\n//\n//   - {\"packages\": [\"go\", \"hello\"]}\n//   - {\"packages\": {\"go\": \"1.20\", \"hello: {\"platforms\": [\"aarch64-darwin\"]}}}\n//\n// When migrate is true, the packages value will be migrated from the legacy\n// array format to the object format. For example, the array:\n//\n//\t[\"go@latest\", \"hello\"]\n//\n// will become:\n//\n//\t{\n//\t\t\"go\": \"latest\",\n//\t\t\"hello\": \"\"\n//\t}\nfunc (c *configAST) packagesField(migrate bool) *hujson.ObjectMember {\n\trootObject := c.root.Value.(*hujson.Object)\n\ti := c.memberIndex(rootObject, \"packages\")\n\tif i != -1 {\n\t\tswitch rootObject.Members[i].Value.Value.Kind() {\n\t\tcase '[':\n\t\t\tif migrate {\n\t\t\t\tc.migratePackagesArray(&rootObject.Members[i].Value)\n\t\t\t\tc.root.Format()\n\t\t\t}\n\t\tcase 'n':\n\t\t\t// Initialize a null packages field to an empty object.\n\t\t\trootObject.Members[i].Value.Value = &hujson.Object{\n\t\t\t\tAfterExtra: []byte{'\\n'},\n\t\t\t}\n\t\t\tc.root.Format()\n\t\t}\n\t\treturn &rootObject.Members[i]\n\t}\n\n\t// Add a packages field to the root config object and initialize it with\n\t// an empty object.\n\trootObject.Members = append(rootObject.Members, hujson.ObjectMember{\n\t\tName: hujson.Value{\n\t\t\tValue:       hujson.String(\"packages\"),\n\t\t\tBeforeExtra: []byte{'\\n'},\n\t\t},\n\t\tValue: hujson.Value{Value: &hujson.Object{}},\n\t})\n\tc.root.Format()\n\treturn &rootObject.Members[len(rootObject.Members)-1]\n}\n\n// appendPackage appends a package to the packages field.\nfunc (c *configAST) appendPackage(name, version string) {\n\tpkgs := c.packagesField(false)\n\tswitch val := pkgs.Value.Value.(type) {\n\tcase *hujson.Object:\n\t\tc.appendPackageToObject(val, name, version)\n\tcase *hujson.Array:\n\t\tc.appendPackageToArray(val, joinNameVersion(name, version))\n\tdefault:\n\t\tpanic(\"packages field must be an object or array\")\n\t}\n\n\t// Ensure the packages field is on its own line.\n\tif !slices.Contains(pkgs.Name.BeforeExtra, '\\n') {\n\t\tpkgs.Name.BeforeExtra = append(pkgs.Name.BeforeExtra, '\\n')\n\t}\n\tc.root.Format()\n}\n\nfunc (c *configAST) appendPackageToObject(pkgs *hujson.Object, name, version string) {\n\ti := c.memberIndex(pkgs, name)\n\tif i != -1 {\n\t\treturn\n\t}\n\n\t// Add a new member to the packages object with the package name and\n\t// version.\n\tpkgs.Members = append(pkgs.Members, hujson.ObjectMember{\n\t\tName:  hujson.Value{Value: hujson.String(name), BeforeExtra: []byte{'\\n'}},\n\t\tValue: hujson.Value{Value: hujson.String(version)},\n\t})\n}\n\nfunc (c *configAST) appendPackageToArray(arr *hujson.Array, versionedName string) {\n\tvar extra []byte\n\tif len(arr.Elements) > 0 {\n\t\t// Put each element on its own line if there\n\t\t// will be more than 1.\n\t\textra = []byte{'\\n'}\n\t}\n\tarr.Elements = append(arr.Elements, hujson.Value{\n\t\tBeforeExtra: extra,\n\t\tValue:       hujson.String(versionedName),\n\t})\n}\n\n// removePackage removes a package from the packages field.\nfunc (c *configAST) removePackage(name string) {\n\tswitch val := c.packagesField(false).Value.Value.(type) {\n\tcase *hujson.Object:\n\t\tc.removePackageMember(val, name)\n\tcase *hujson.Array:\n\t\tc.removePackageElement(val, name)\n\tdefault:\n\t\tpanic(\"packages field must be an object or array\")\n\t}\n\tc.root.Format()\n}\n\nfunc (c *configAST) removePackageMember(pkgs *hujson.Object, name string) {\n\ti := c.memberIndex(pkgs, name)\n\tif i == -1 {\n\t\treturn\n\t}\n\tpkgs.Members = slices.Delete(pkgs.Members, i, i+1)\n}\n\nfunc (c *configAST) removePackageElement(arr *hujson.Array, name string) {\n\ti := c.packageElementIndex(arr, name)\n\tif i == -1 {\n\t\treturn\n\t}\n\tarr.Elements = slices.Delete(arr.Elements, i, i+1)\n}\n\n// setPackageBool sets a bool field on a package.\nfunc (c *configAST) setPackageBool(name, fieldName string, val bool) {\n\tpkgObject := c.findPkgObject(name)\n\tif pkgObject == nil {\n\t\treturn\n\t}\n\tif i := c.memberIndex(pkgObject, fieldName); i == -1 {\n\t\tpkgObject.Members = append(pkgObject.Members, hujson.ObjectMember{\n\t\t\tName: hujson.Value{\n\t\t\t\tValue:       hujson.String(fieldName),\n\t\t\t\tBeforeExtra: []byte{'\\n'},\n\t\t\t},\n\t\t\tValue: hujson.Value{Value: hujson.Bool(val)},\n\t\t})\n\t} else {\n\t\tpkgObject.Members[i].Value.Value = hujson.Bool(val)\n\t}\n\n\tc.root.Format()\n}\n\nfunc (c *configAST) appendPlatforms(name, fieldName string, platforms []string) {\n\tif len(platforms) == 0 {\n\t\treturn\n\t}\n\n\tc.appendStringSliceField(name, fieldName, platforms)\n}\n\nfunc (c *configAST) appendOutputs(name, fieldName string, outputs []string) {\n\tif len(outputs) == 0 {\n\t\treturn\n\t}\n\n\tc.appendStringSliceField(name, fieldName, outputs)\n}\n\nfunc (c *configAST) appendAllowInsecure(name, fieldName string, whitelist []string) {\n\tif len(whitelist) == 0 {\n\t\treturn\n\t}\n\n\tc.appendStringSliceField(name, fieldName, whitelist)\n}\n\n// removePatch removes the patch field from the named package.\nfunc (c *configAST) removePatch(name string) {\n\tpkgs := c.packagesField(false)\n\tobj, ok := pkgs.Value.Value.(*hujson.Object)\n\tif !ok {\n\t\t// Packages field is an array.\n\t\treturn\n\t}\n\ti := c.memberIndex(obj, name)\n\tif i == -1 {\n\t\t// Package not found.\n\t\treturn\n\t}\n\n\tobj, ok = obj.Members[i].Value.Value.(*hujson.Object)\n\tif !ok {\n\t\t// Package is a string, not an object.\n\t\treturn\n\t}\n\ti = c.memberIndex(obj, \"patch\")\n\tif i == -1 {\n\t\t// Patch field doesn't exist.\n\t\treturn\n\t}\n\n\tobj.Members = slices.Delete(obj.Members, i, i+1)\n\tc.root.Format()\n}\n\n// setPatch sets the patch field of the named package.\nfunc (c *configAST) setPatch(name string, mode PatchMode) {\n\tpkgObject := c.findPkgObject(name)\n\tif pkgObject == nil {\n\t\treturn\n\t}\n\n\tglibcIndex := c.memberIndex(pkgObject, \"patch_glibc\") // deprecated\n\tpatchIndex := c.memberIndex(pkgObject, \"patch\")\n\tswitch {\n\t// Neither patch_glibc or patch exist - append a new field.\n\tcase patchIndex == -1 && glibcIndex == -1:\n\t\tpkgObject.Members = append(pkgObject.Members, hujson.ObjectMember{\n\t\t\tName: hujson.Value{\n\t\t\t\tBeforeExtra: []byte{'\\n'},\n\t\t\t},\n\t\t})\n\t\tpatchIndex = len(pkgObject.Members) - 1\n\t\tdefer c.root.Format()\n\t// patch_glibc exists and patch doesn't - rename patch_glibc to\n\t// preserve formatting/comments.\n\tcase patchIndex == -1 && glibcIndex != -1:\n\t\tpatchIndex = glibcIndex\n\t// Both patch_glibc and patch exist - delete patch_glibc.\n\tcase patchIndex != -1 && glibcIndex != -1:\n\t\tpkgObject.Members = slices.Delete(pkgObject.Members, glibcIndex, glibcIndex+1)\n\t\tif patchIndex > glibcIndex {\n\t\t\tpatchIndex--\n\t\t}\n\t\tdefer c.root.Format()\n\t}\n\n\tpkgObject.Members[patchIndex].Name.Value = hujson.String(\"patch\")\n\tpkgObject.Members[patchIndex].Value.Value = hujson.String(string(mode))\n}\n\nfunc (c *configAST) findPkgObject(name string) *hujson.Object {\n\tpkgs := c.packagesField(true).Value.Value.(*hujson.Object)\n\ti := c.memberIndex(pkgs, name)\n\tif i == -1 {\n\t\treturn nil\n\t}\n\n\t// We need to ensure that the package value is a full object\n\t// (not a version string) before we can set a custom field on it.\n\tc.convertVersionToObject(&pkgs.Members[i].Value)\n\n\tpkgObject := pkgs.Members[i].Value.Value.(*hujson.Object)\n\treturn pkgObject\n}\n\n// migratePackagesArray migrates a legacy array of package versionedNames to an\n// object. See packagesField for details.\nfunc (c *configAST) migratePackagesArray(pkgs *hujson.Value) {\n\tarr := pkgs.Value.(*hujson.Array)\n\tobj := &hujson.Object{Members: make([]hujson.ObjectMember, len(arr.Elements))}\n\tfor i, elem := range arr.Elements {\n\t\tname, version := parseVersionedName(elem.Value.(hujson.Literal).String())\n\n\t\t// Preserve any comments above the array elements.\n\t\tvar before []byte\n\t\tif comment := bytes.TrimSpace(elem.BeforeExtra); len(comment) > 0 {\n\t\t\tbefore = append([]byte{'\\n'}, comment...)\n\t\t}\n\t\tbefore = append(before, '\\n')\n\n\t\tobj.Members[i] = hujson.ObjectMember{\n\t\t\tName: hujson.Value{\n\t\t\t\tValue:       hujson.String(name),\n\t\t\t\tBeforeExtra: before,\n\t\t\t},\n\t\t\tValue: hujson.Value{Value: hujson.String(version)},\n\t\t}\n\t}\n\tpkgs.Value = obj\n}\n\n// convertVersionToObject transforms a version string into an object with the\n// version as a field.\nfunc (c *configAST) convertVersionToObject(pkg *hujson.Value) {\n\tif pkg.Value.Kind() == '{' {\n\t\treturn\n\t}\n\n\tobj := &hujson.Object{}\n\tif version, ok := pkg.Value.(hujson.Literal); ok && version.String() != \"\" {\n\t\tobj.Members = append(obj.Members, hujson.ObjectMember{\n\t\t\tName: hujson.Value{\n\t\t\t\tValue:       hujson.String(\"version\"),\n\t\t\t\tBeforeExtra: []byte{'\\n'},\n\t\t\t},\n\t\t\tValue: hujson.Value{Value: version},\n\t\t})\n\t}\n\tpkg.Value = obj\n}\n\n// memberIndex returns the index of an object member.\nfunc (*configAST) memberIndex(obj *hujson.Object, name string) int {\n\treturn slices.IndexFunc(obj.Members, func(m hujson.ObjectMember) bool {\n\t\treturn m.Name.Value.(hujson.Literal).String() == name\n\t})\n}\n\n// packageElementIndex returns the index of a package from an array of\n// versionedName strings.\nfunc (*configAST) packageElementIndex(arr *hujson.Array, name string) int {\n\treturn slices.IndexFunc(arr.Elements, func(v hujson.Value) bool {\n\t\telemName, _ := parseVersionedName(v.Value.(hujson.Literal).String())\n\t\treturn elemName == name\n\t})\n}\n\nfunc joinNameVersion(name, version string) string {\n\tif version == \"\" {\n\t\treturn name\n\t}\n\treturn name + \"@\" + version\n}\n\nfunc (c *configAST) appendStringSliceField(name, fieldName string, fieldValues []string) {\n\tpkgObject := c.findPkgObject(name)\n\tif pkgObject == nil {\n\t\treturn\n\t}\n\n\tvar arr *hujson.Array\n\tif i := c.memberIndex(pkgObject, fieldName); i == -1 {\n\t\tarr = &hujson.Array{\n\t\t\tElements: make([]hujson.Value, 0, len(fieldValues)),\n\t\t}\n\t\tpkgObject.Members = append(pkgObject.Members, hujson.ObjectMember{\n\t\t\tName: hujson.Value{\n\t\t\t\tValue:       hujson.String(fieldName),\n\t\t\t\tBeforeExtra: []byte{'\\n'},\n\t\t\t},\n\t\t\tValue: hujson.Value{Value: arr},\n\t\t})\n\t} else {\n\t\tarr = pkgObject.Members[i].Value.Value.(*hujson.Array)\n\t\tarr.Elements = slices.Grow(arr.Elements, len(fieldValues))\n\t}\n\n\tfor _, p := range fieldValues {\n\t\tarr.Elements = append(arr.Elements, hujson.Value{Value: hujson.String(p)})\n\t}\n\tc.root.Format()\n}\n\nfunc (c *configAST) beforeComment(path ...any) []byte {\n\telem := c.root\n\tfor _, pathItem := range path {\n\t\tobj := elem.Value.(*hujson.Object)\n\t\ti, ok := pathItem.(int)\n\t\tif !ok {\n\t\t\ti = c.memberIndex(obj, pathItem.(string))\n\t\t}\n\t\tif i == -1 {\n\t\t\treturn nil\n\t\t}\n\t\telem = obj.Members[i].Value\n\t}\n\n\t// Match all single are multi line comments.\n\tre := regexp.MustCompile(`(?:\\/\\/(.*?)\\n)|(?s:\\/\\*(.*?)\\*\\/)`)\n\n\treturn bytes.TrimSpace(\n\t\tre.ReplaceAllFunc(elem.BeforeExtra, func(s []byte) []byte {\n\t\t\tsingleLineRe := regexp.MustCompile(`\\/\\/(.*?)\\n`)\n\t\t\tmultiLineRe := regexp.MustCompile(`(?s:\\/\\*(.*?)\\*\\/)`)\n\n\t\t\tif singleLineRe.Match(s) {\n\t\t\t\treturn singleLineRe.ReplaceAll(s, []byte(\"$1\\n\"))\n\t\t\t} else if multiLineRe.Match(s) {\n\t\t\t\treturn multiLineRe.ReplaceAll(s, []byte(\"$1\"))\n\t\t\t}\n\t\t\treturn s\n\t\t}),\n\t)\n}\n\nfunc (c *configAST) createMemberIfMissing(key string) *hujson.ObjectMember {\n\ti := c.memberIndex(c.root.Value.(*hujson.Object), key)\n\tif i == -1 {\n\t\tc.root.Value.(*hujson.Object).Members = append(c.root.Value.(*hujson.Object).Members, hujson.ObjectMember{\n\t\t\tName: hujson.Value{\n\t\t\t\tValue:       hujson.String(key),\n\t\t\t\tBeforeExtra: []byte{'\\n'},\n\t\t\t},\n\t\t})\n\t\ti = len(c.root.Value.(*hujson.Object).Members) - 1\n\t}\n\treturn &c.root.Value.(*hujson.Object).Members[i]\n}\n\nfunc mapToObjectMembers(env map[string]string) []hujson.ObjectMember {\n\tmembers := make([]hujson.ObjectMember, 0, len(env))\n\tfor k, v := range env {\n\t\tmembers = append(members, hujson.ObjectMember{\n\t\t\tName: hujson.Value{\n\t\t\t\tValue:       hujson.String(k),\n\t\t\t\tBeforeExtra: []byte{'\\n'},\n\t\t\t},\n\t\t\tValue: hujson.Value{Value: hujson.String(v)},\n\t\t})\n\t}\n\t// Make the order deterministic so we don't keep moving fields around.\n\tslices.SortFunc(members, func(a, b hujson.ObjectMember) int {\n\t\treturn cmp.Compare(a.Name.Value.(hujson.Literal).String(), b.Name.Value.(hujson.Literal).String())\n\t})\n\treturn members\n}\n\nfunc (c *configAST) setEnv(env map[string]string) {\n\tc.createMemberIfMissing(\"env\").Value.Value = &hujson.Object{\n\t\tMembers: mapToObjectMembers(env),\n\t}\n\tc.root.Format()\n}\n"
  },
  {
    "path": "internal/devconfig/configfile/env.go",
    "content": "package configfile\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/hashicorp/go-envparse\"\n)\n\nvar JetifyCloudEnvFromValue = \"jetify-cloud\"\n\nfunc (c *ConfigFile) IsEnvsecEnabled() bool {\n\t// envsec for legacy. jetpack-cloud for legacy\n\treturn c.EnvFrom == \"envsec\" || c.EnvFrom == \"jetpack-cloud\" || c.EnvFrom == JetifyCloudEnvFromValue\n}\n\nfunc (c *ConfigFile) IsdotEnvEnabled() bool {\n\t// filename has to end with .env\n\treturn filepath.Ext(c.EnvFrom) == \".env\"\n}\n\nfunc (c *ConfigFile) ParseEnvsFromDotEnv() (map[string]string, error) {\n\t// This check should never happen because we call IsdotEnvEnabled\n\t// before calling this method. But having it makes it more robust\n\t// in case if anyone uses this method without the IsdotEnvEnabled\n\tif !c.IsdotEnvEnabled() {\n\t\treturn nil, fmt.Errorf(\"env file does not have a .env extension\")\n\t}\n\tenvFileAbsPath := c.EnvFrom\n\tif !filepath.IsAbs(c.EnvFrom) {\n\t\tenvFileAbsPath = filepath.Join(filepath.Dir(c.AbsRootPath), c.EnvFrom)\n\t}\n\tfile, err := os.Open(envFileAbsPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open file: %s\", envFileAbsPath)\n\t}\n\tdefer file.Close()\n\n\tenvMap, err := envparse.Parse(file)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse env file: %v\", err)\n\t}\n\n\treturn envMap, nil\n}\n\nfunc (c *ConfigFile) SetEnv(env map[string]string) {\n\tc.Env = env\n\tc.ast.setEnv(env)\n}\n"
  },
  {
    "path": "internal/devconfig/configfile/field.go",
    "content": "package configfile\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/tailscale/hujson\"\n)\n\nfunc (c *ConfigFile) SetStringField(fieldName, val string) {\n\tvalueOfStruct := reflect.ValueOf(c).Elem()\n\n\tfield := valueOfStruct.FieldByName(fieldName)\n\tfield.SetString(val)\n\n\tc.ast.setStringField(c.jsonNameOfField(fieldName), val)\n}\n\nfunc (c *ConfigFile) jsonNameOfField(fieldName string) string {\n\tvalueOfStruct := reflect.ValueOf(c).Elem()\n\n\tvar name string\n\tfor i := 0; i < valueOfStruct.NumField(); i++ {\n\t\tfield := valueOfStruct.Type().Field(i)\n\t\tif field.Name != fieldName {\n\t\t\tcontinue\n\t\t}\n\n\t\tname = field.Name\n\t\tjsonTag := field.Tag.Get(\"json\")\n\t\tparts := strings.Split(jsonTag, \",\")\n\t\tif len(parts) > 0 && parts[0] != \"\" && parts[0] != \"-\" {\n\t\t\tname = parts[0]\n\t\t}\n\n\t\tbreak\n\t}\n\treturn name\n}\n\nfunc (c *configAST) setStringField(key, val string) {\n\trootObject := c.root.Value.(*hujson.Object)\n\ti := c.memberIndex(rootObject, key)\n\tif i == -1 {\n\t\trootObject.Members = append(rootObject.Members, hujson.ObjectMember{\n\t\t\tName:  hujson.Value{Value: hujson.String(key)},\n\t\t\tValue: hujson.Value{Value: hujson.String(val)},\n\t\t})\n\t} else if val != \"\" {\n\t\trootObject.Members[i].Value = hujson.Value{Value: hujson.String(val)}\n\t} else {\n\t\trootObject.Members = append(rootObject.Members[:i], rootObject.Members[i+1:]...)\n\t}\n\n\tc.root.Format()\n}\n"
  },
  {
    "path": "internal/devconfig/configfile/file.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage configfile\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/tailscale/hujson\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/cachehash\"\n\t\"go.jetify.com/devbox/internal/devbox/shellcmd\"\n)\n\nconst (\n\tDefaultName = \"devbox.json\"\n)\n\n// ConfigFile defines a devbox environment as JSON.\ntype ConfigFile struct {\n\t// AbsRootPath is the absolute path to the devbox.json or plugin.json file\n\t// it will not be set for github plugins.\n\tAbsRootPath string `json:\"-\"`\n\n\tName        string `json:\"name,omitempty\"`\n\tDescription string `json:\"description,omitempty\"`\n\n\t// PackagesMutator is the slice of Nix packages that devbox makes available in\n\t// its environment. Deliberately do not omitempty.\n\tPackagesMutator PackagesMutator `json:\"packages\"`\n\n\t// Env allows specifying env variables\n\tEnv map[string]string `json:\"env,omitempty\"`\n\n\t// Only allows \"envsec\" for now\n\tEnvFrom string `json:\"env_from,omitempty\"`\n\n\t// Shell configures the devbox shell environment.\n\tShell *shellConfig `json:\"shell,omitempty\"`\n\t// Nixpkgs specifies the repository to pull packages from\n\t// Deprecated: Versioned packages don't need this\n\tNixpkgs *NixpkgsConfig `json:\"nixpkgs,omitempty\"`\n\n\t// Reserved to allow including other config files. Proposed format is:\n\t// path: for local files\n\t// https:// for remote files\n\t// plugin: for built-in plugins\n\t// This is a similar format to nix inputs\n\tInclude []string `json:\"include,omitempty\"`\n\n\tast *configAST\n}\n\ntype shellConfig struct {\n\t// InitHook contains commands that will run at shell startup.\n\tInitHook *shellcmd.Commands            `json:\"init_hook,omitempty\"`\n\tScripts  map[string]*shellcmd.Commands `json:\"scripts,omitempty\"`\n}\n\ntype NixpkgsConfig struct {\n\tCommit string `json:\"commit,omitempty\"`\n}\n\n// Stage contains a subset of fields from plansdk.Stage\ntype Stage struct {\n\tCommand string `json:\"command\"`\n}\n\nfunc (c *ConfigFile) Bytes() []byte {\n\tb := c.ast.root.Pack()\n\treturn bytes.ReplaceAll(b, []byte(\"\\t\"), []byte(\"  \"))\n}\n\nfunc (c *ConfigFile) Hash() (string, error) {\n\tif c.ast == nil {\n\t\treturn cachehash.JSON(c)\n\t}\n\tast := c.ast.root.Clone()\n\tast.Minimize()\n\treturn cachehash.Bytes(ast.Pack()), nil\n}\n\nfunc (c *ConfigFile) Equals(other *ConfigFile) bool {\n\thash1, _ := c.Hash()\n\thash2, _ := other.Hash()\n\treturn hash1 == hash2\n}\n\nfunc (c *ConfigFile) NixPkgsCommitHash() string {\n\tif c == nil || c.Nixpkgs == nil {\n\t\treturn \"\"\n\t}\n\treturn c.Nixpkgs.Commit\n}\n\nfunc (c *ConfigFile) InitHook() *shellcmd.Commands {\n\tif c == nil || c.Shell == nil || c.Shell.InitHook == nil {\n\t\treturn &shellcmd.Commands{}\n\t}\n\treturn c.Shell.InitHook\n}\n\n// SaveTo writes the config to a file.\nfunc (c *ConfigFile) SaveTo(path string) error {\n\treturn os.WriteFile(filepath.Join(path, DefaultName), c.Bytes(), 0o644)\n}\n\n// TODO: Can we remove SaveTo and just use Save()?\nfunc (c *ConfigFile) Save() error {\n\treturn c.SaveTo(c.AbsRootPath)\n}\n\n// Get returns the package with the given versionedName\nfunc (c *ConfigFile) GetPackage(versionedName string) (*Package, bool) {\n\tname, version := parseVersionedName(versionedName)\n\ti := c.PackagesMutator.index(name, version)\n\tif i == -1 {\n\t\treturn nil, false\n\t}\n\treturn &c.PackagesMutator.collection[i], true\n}\n\n// TopLevelPackages returns the packages in the config file, but not the included ones.\n// Semi-awkwardly named to avoid confusion with the Packages method on Config.\nfunc (c *ConfigFile) TopLevelPackages() []Package {\n\treturn c.PackagesMutator.collection\n}\n\nfunc LoadBytes(b []byte) (*ConfigFile, error) {\n\tjsonb, err := hujson.Standardize(slices.Clone(b))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tast, err := parseConfig(b)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcfg := &ConfigFile{\n\t\tPackagesMutator: PackagesMutator{ast: ast},\n\t\tast:             ast,\n\t}\n\tif err := json.Unmarshal(jsonb, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\treturn cfg, validateConfig(cfg)\n}\n\nfunc validateConfig(cfg *ConfigFile) error {\n\tfns := []func(cfg *ConfigFile) error{\n\t\tValidateNixpkg,\n\t\tvalidateScripts,\n\t}\n\n\tfor _, fn := range fns {\n\t\tif err := fn(cfg); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nvar whitespace = regexp.MustCompile(`\\s`)\n\nfunc validateScripts(cfg *ConfigFile) error {\n\tscripts := cfg.Scripts()\n\tfor k := range scripts {\n\t\tif strings.TrimSpace(k) == \"\" {\n\t\t\treturn errors.New(\"cannot have script with empty name in devbox.json\")\n\t\t}\n\t\tif whitespace.MatchString(k) {\n\t\t\treturn errors.Errorf(\n\t\t\t\t\"cannot have script name with whitespace in devbox.json: %s\", k)\n\t\t}\n\t\tif strings.TrimSpace(scripts[k].String()) == \"\" {\n\t\t\treturn errors.Errorf(\n\t\t\t\t\"cannot have an empty script body in devbox.json: %s\", k)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ValidateNixpkg(cfg *ConfigFile) error {\n\thash := cfg.NixPkgsCommitHash()\n\tif hash == \"\" {\n\t\treturn nil\n\t}\n\n\tconst commitLength = 40\n\tif len(hash) != commitLength {\n\t\treturn usererr.New(\n\t\t\t\"Expected nixpkgs.commit to be of length %d but it has length %d\",\n\t\t\tcommitLength,\n\t\t\tlen(hash),\n\t\t)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/devconfig/configfile/file_test.go",
    "content": "//nolint:varnamelen\npackage configfile\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/tailscale/hujson\"\n\t\"golang.org/x/tools/txtar\"\n)\n\n/*\nThe tests in this file use txtar to define test input and expected output.\nThis makes the JSON a lot easier to read vs. defining it in variables or structs\nwith weird indentation.\n\nTests begin by defining their JSON with:\n\n  in, want := parseConfigTxtarTest(t, `an optional comment that will be logged with t.Log\n  -- in --\n  { }\n  -- want --\n  { \"packages\": { \"go\": \"latest\" } }`)\n*/\n\nfunc parseConfigTxtarTest(t *testing.T, test string) (in *ConfigFile, want []byte) {\n\tt.Helper()\n\n\tar := txtar.Parse([]byte(test))\n\tif comment := strings.TrimSpace(string(ar.Comment)); comment != \"\" {\n\t\tt.Log(comment)\n\t}\n\tfor _, f := range ar.Files {\n\t\tswitch f.Name {\n\t\tcase \"in\":\n\t\t\tvar err error\n\t\t\tin, err = LoadBytes(f.Data)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"input devbox.json is invalid: %v\\n%s\", err, f.Data)\n\t\t\t}\n\n\t\tcase \"want\":\n\t\t\twant = f.Data\n\t\t}\n\t}\n\treturn in, want\n}\n\nfunc optBytesToStrings() cmp.Option {\n\treturn cmp.Transformer(\"bytesToStrings\", func(b []byte) string {\n\t\treturn string(b)\n\t})\n}\n\nfunc optParseHujson() cmp.Option {\n\tf := func(b []byte) map[string]any {\n\t\tgotMin, err := hujson.Minimize(b)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tvar m map[string]any\n\t\tif err := json.Unmarshal(gotMin, &m); err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn m\n\t}\n\treturn cmp.Transformer(\"parseHujson\", f)\n}\n\nfunc TestNoChanges(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `a config that's loaded and saved without any changes should have unchanged json\n-- in --\n{ \"packages\": { \"go\": \"latest\" } }\n-- want --\n{ \"packages\": { \"go\": \"latest\" } }`)\n\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPackageEmptyConfig(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{}\n-- want --\n{\n  \"packages\": {\n    \"go\": \"latest\"\n  }\n}`)\n\n\tin.PackagesMutator.Add(\"go@latest\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPackageEmptyConfigWhitespace(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n\n}\n-- want --\n{\n  \"packages\": {\n    \"go\": \"latest\"\n  }\n}`)\n\n\tin.PackagesMutator.Add(\"go@latest\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPackageEmptyConfigComment(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n// Comment\n{}\n-- want --\n// Comment\n{\n  \"packages\": {\n    \"go\": \"latest\",\n  },\n}`)\n\n\tin.PackagesMutator.Add(\"go@latest\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPackageNull(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{ \"packages\": null }\n-- want --\n{\n  \"packages\": {\n    \"go\": \"latest\"\n  }\n}`)\n\n\tin.PackagesMutator.Add(\"go@latest\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPackageObject(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": {\n    \"go\": \"latest\"\n  }\n}\n-- want --\n{\n  \"packages\": {\n    \"go\":     \"latest\",\n    \"python\": \"3.10\"\n  }\n}`)\n\n\tin.PackagesMutator.Add(\"python@3.10\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPackageObjectComment(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": {\n    // Package comment\n    \"go\": \"latest\"\n  }\n}\n-- want --\n{\n  \"packages\": {\n    // Package comment\n    \"go\":     \"latest\",\n    \"python\": \"3.10\",\n  },\n}`)\n\n\tin.PackagesMutator.Add(\"python@3.10\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPackageEmptyArray(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": []\n}\n-- want --\n{\n  \"packages\": [\"go@latest\"]\n}`)\n\n\tin.PackagesMutator.Add(\"go@latest\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPackageOneLineArray(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": [\"go\"]\n}\n-- want --\n{\n  \"packages\": [\n    \"go\",\n    \"python@3.10\"\n  ]\n}`)\n\n\tin.PackagesMutator.Add(\"python@3.10\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPackageMultiLineArray(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": [\n    \"go\"\n  ]\n}\n-- want --\n{\n  \"packages\": [\n    \"go\",\n    \"python@3.10\"\n  ]\n}`)\n\n\tin.PackagesMutator.Add(\"python@3.10\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPackageArrayComments(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": [\n    // Go package comment\n    \"go\",\n\n    // Python package comment\n    \"python@3.10\"\n  ]\n}\n-- want --\n{\n  \"packages\": [\n    // Go package comment\n    \"go\",\n\n    // Python package comment\n    \"python@3.10\",\n    \"hello@latest\",\n  ],\n}`)\n\n\tin.PackagesMutator.Add(\"hello@latest\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestRemovePackageObject(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": {\n    \"go\": \"latest\",\n    \"python\": \"3.10\"\n  }\n}\n-- want --\n{\n  \"packages\": {\n    \"python\": \"3.10\"\n  }\n}`)\n\n\tin.PackagesMutator.Remove(\"go@latest\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestRemovePackageLastMember(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"env\": {\"NAME\": \"value\"},\n  \"packages\": {\n    \"go\": \"latest\"\n  }\n}\n-- want --\n{\n  \"env\":      {\"NAME\": \"value\"},\n  \"packages\": {}\n}`)\n\n\tin.PackagesMutator.Remove(\"go@latest\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes(), optBytesToStrings()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestRemovePackageArray(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": [\"go@latest\", \"python@3.10\"]\n}\n-- want --\n{\n  \"packages\": [\"python@3.10\"]\n}`)\n\n\tin.PackagesMutator.Remove(\"go@latest\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestRemovePackageLastElement(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": [\"go@latest\"],\n  \"env\": {\n    \"NAME\": \"value\"\n  }\n}\n-- want --\n{\n  \"packages\": [],\n  \"env\": {\n    \"NAME\": \"value\"\n  }\n}`)\n\n\tin.PackagesMutator.Remove(\"go@latest\")\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPlatforms(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": {\n    \"go\": {\n      \"version\": \"1.20\"\n    },\n    \"python\": {\n      \"version\": \"3.10\",\n      \"platforms\": [\n        \"x86_64-linux\"\n      ]\n    },\n    \"hello\": {\n      \"version\": \"latest\",\n      \"platforms\": [\"x86_64-linux\"]\n    },\n    \"vim\": {\n      \"version\": \"latest\"\n    }\n  }\n}\n-- want --\n{\n  \"packages\": {\n    \"go\": {\n      \"version\":   \"1.20\",\n      \"platforms\": [\"aarch64-darwin\", \"x86_64-darwin\"]\n    },\n    \"python\": {\n      \"version\": \"3.10\",\n      \"platforms\": [\n        \"x86_64-linux\",\n        \"x86_64-darwin\"\n      ]\n    },\n    \"hello\": {\n      \"version\":   \"latest\",\n      \"platforms\": [\"x86_64-linux\", \"x86_64-darwin\"]\n    },\n    \"vim\": {\n      \"version\": \"latest\"\n    }\n  }\n}`)\n\n\terr := in.PackagesMutator.AddPlatforms(io.Discard, \"go@1.20\", []string{\"aarch64-darwin\", \"x86_64-darwin\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\terr = in.PackagesMutator.AddPlatforms(io.Discard, \"python@3.10\", []string{\"x86_64-darwin\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\terr = in.PackagesMutator.AddPlatforms(io.Discard, \"hello@latest\", []string{\"x86_64-darwin\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPlatformsMigrateArray(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": [\"go\", \"python@3.10\", \"hello\"]\n}\n-- want --\n{\n  \"packages\": {\n    \"go\": {\n      \"platforms\": [\"aarch64-darwin\"]\n    },\n    \"python\": {\n      \"version\":   \"3.10\",\n      \"platforms\": [\"x86_64-darwin\", \"x86_64-linux\"]\n    },\n    \"hello\": \"\"\n  }\n}`)\n\n\terr := in.PackagesMutator.AddPlatforms(io.Discard, \"go\", []string{\"aarch64-darwin\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\terr = in.PackagesMutator.AddPlatforms(io.Discard, \"python@3.10\", []string{\"x86_64-darwin\", \"x86_64-linux\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAddPlatformsMigrateArrayComments(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": [\n    // Go comment\n    \"go\",\n\n    // Python comment\n    \"python@3.10\"\n  ]\n}\n-- want --\n{\n  \"packages\": {\n    // Go comment\n    \"go\": \"\",\n    // Python comment\n    \"python\": {\n      \"version\":   \"3.10\",\n      \"platforms\": [\"x86_64-darwin\", \"x86_64-linux\"],\n    },\n  },\n}`)\n\n\terr := in.PackagesMutator.AddPlatforms(io.Discard, \"python@3.10\", []string{\"x86_64-darwin\", \"x86_64-linux\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestExcludePlatforms(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": {\n    \"go\": {\n      \"version\": \"1.20\"\n    }\n  }\n}\n-- want --\n{\n  \"packages\": {\n    \"go\": {\n      \"version\":            \"1.20\",\n      \"excluded_platforms\": [\"aarch64-darwin\"]\n    }\n  }\n}`)\n\n\terr := in.PackagesMutator.ExcludePlatforms(io.Discard, \"go@1.20\", []string{\"aarch64-darwin\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestSetOutputs(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": {\n    \"prometheus\": {\n      \"version\": \"latest\"\n    }\n  }\n}\n-- want --\n{\n  \"packages\": {\n    \"prometheus\": {\n      \"version\": \"latest\",\n      \"outputs\": [\"cli\"]\n    }\n  }\n}`)\n\n\terr := in.PackagesMutator.SetOutputs(io.Discard, \"prometheus@latest\", []string{\"cli\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestSetOutputsMigrateArray(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": [\"go\", \"python@3.10\", \"prometheus@latest\"]\n}\n-- want --\n{\n  \"packages\": {\n    \"go\":     \"\",\n    \"python\": \"3.10\",\n    \"prometheus\": {\n      \"version\": \"latest\",\n      \"outputs\": [\"cli\"]\n    }\n  }\n}`)\n\n\terr := in.PackagesMutator.SetOutputs(io.Discard, \"prometheus@latest\", []string{\"cli\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestSetAllowInsecure(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"packages\": {\n    \"python\": {\n      \"version\": \"2.7\"\n    }\n  }\n}\n-- want --\n{\n  \"packages\": {\n    \"python\": {\n      \"version\":        \"2.7\",\n      \"allow_insecure\": [\"python-2.7.18.1\"]\n    }\n  }\n}`)\n\n\terr := in.PackagesMutator.SetAllowInsecure(io.Discard, \"python@2.7\", []string{\"python-2.7.18.1\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestSetEnv(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{}\n-- want --\n{\n  \"env\": {\n    \"BAZ\": \"qux\",\n    \"FOO\": \"bar\"\n  }\n}`)\n\n\tin.SetEnv(map[string]string{\n\t\t\"FOO\": \"bar\",\n\t\t\"BAZ\": \"qux\",\n\t})\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestSetEnvExisting(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"env\": {\n    \"EXISTING\": \"value\"\n  }\n}\n-- want --\n{\n  \"env\": {\n    \"FOO\": \"bar\"\n  }\n}`)\n\n\tin.SetEnv(map[string]string{\n\t\t\"FOO\": \"bar\",\n\t})\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestSetEnvClear(t *testing.T) {\n\tin, want := parseConfigTxtarTest(t, `\n-- in --\n{\n  \"env\": {\n    \"EXISTING\": \"value\"\n  }\n}\n-- want --\n{\n  \"env\": {}\n}`)\n\n\tin.SetEnv(map[string]string{})\n\tif diff := cmp.Diff(want, in.Bytes(), optParseHujson()); diff != \"\" {\n\t\tt.Errorf(\"wrong parsed config json (-want +got):\\n%s\", diff)\n\t}\n\tif diff := cmp.Diff(want, in.Bytes()); diff != \"\" {\n\t\tt.Errorf(\"wrong raw config hujson (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestNixpkgsValidation(t *testing.T) {\n\ttestCases := map[string]struct {\n\t\tcommit   string\n\t\tisErrant bool\n\t}{\n\t\t\"invalid_nixpkg_commit\": {\"1234545\", true},\n\t\t\"valid_nixpkg_commit\":   {\"af9e00071d0971eb292fd5abef334e66eda3cb69\", false},\n\t}\n\n\tfor name, testCase := range testCases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tassert := assert.New(t)\n\n\t\t\terr := ValidateNixpkg(&ConfigFile{\n\t\t\t\tNixpkgs: &NixpkgsConfig{\n\t\t\t\t\tCommit: testCase.commit,\n\t\t\t\t},\n\t\t\t})\n\t\t\tif testCase.isErrant {\n\t\t\t\tassert.Error(err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/devconfig/configfile/packages.go",
    "content": "//nolint:dupl // add/exclude platforms needs refactoring\npackage configfile\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\torderedmap \"github.com/wk8/go-ordered-map/v2\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/searcher\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\ntype PackagesMutator struct {\n\t// collection contains the set of package definitions\n\tcollection []Package\n\n\tast *configAST\n}\n\n// Add adds a package to the list of packages\nfunc (pkgs *PackagesMutator) Add(versionedName string) {\n\tname, version := parseVersionedName(versionedName)\n\tif pkgs.index(name, version) != -1 {\n\t\treturn\n\t}\n\tpkgs.collection = append(pkgs.collection, NewVersionOnlyPackage(name, version))\n\tpkgs.ast.appendPackage(name, version)\n}\n\n// Remove removes a package from the list of packages\nfunc (pkgs *PackagesMutator) Remove(versionedName string) {\n\tname, version := parseVersionedName(versionedName)\n\ti := pkgs.index(name, version)\n\tif i == -1 {\n\t\treturn\n\t}\n\tpkgs.collection = slices.Delete(pkgs.collection, i, i+1)\n\tpkgs.ast.removePackage(name)\n}\n\n// AddPlatforms adds a platform to the list of platforms for a given package\nfunc (pkgs *PackagesMutator) AddPlatforms(writer io.Writer, versionedname string, platforms []string) error {\n\tif len(platforms) == 0 {\n\t\treturn nil\n\t}\n\tif err := nix.EnsureValidPlatform(platforms...); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tname, version := parseVersionedName(versionedname)\n\ti := pkgs.index(name, version)\n\tif i == -1 {\n\t\treturn errors.Errorf(\"package %s not found\", versionedname)\n\t}\n\n\t// Adding any platform will restrict installation to it, so\n\t// the ExcludedPlatforms are no longer needed\n\tpkg := &pkgs.collection[i]\n\tif len(pkg.ExcludedPlatforms) > 0 {\n\t\treturn usererr.New(\n\t\t\t\"cannot add any platform for package %s because it already has `excluded_platforms` defined. \"+\n\t\t\t\t\"Please delete the `excluded_platforms` for this package from devbox.json and retry.\",\n\t\t\tpkg.VersionedName(),\n\t\t)\n\t}\n\n\t// Append if the platform is not already present\n\toldLen := len(pkg.Platforms)\n\tfor _, p := range platforms {\n\t\tif !slices.Contains(pkg.Platforms, p) {\n\t\t\tpkg.Platforms = append(pkg.Platforms, p)\n\t\t}\n\t}\n\tif len(pkg.Platforms) > oldLen {\n\t\tpkgs.ast.appendPlatforms(pkg.Name, \"platforms\", pkg.Platforms[oldLen:])\n\t\tux.Finfof(writer,\n\t\t\t\"Added platform %s to package %s\\n\", strings.Join(platforms, \", \"),\n\t\t\tpkg.VersionedName(),\n\t\t)\n\t}\n\treturn nil\n}\n\n// ExcludePlatforms adds a platform to the list of excluded platforms for a given package\nfunc (pkgs *PackagesMutator) ExcludePlatforms(writer io.Writer, versionedName string, platforms []string) error {\n\tif len(platforms) == 0 {\n\t\treturn nil\n\t}\n\tif err := nix.EnsureValidPlatform(platforms...); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tname, version := parseVersionedName(versionedName)\n\ti := pkgs.index(name, version)\n\tif i == -1 {\n\t\treturn errors.Errorf(\"package %s not found\", versionedName)\n\t}\n\n\tpkg := &pkgs.collection[i]\n\tif len(pkg.Platforms) > 0 {\n\t\treturn usererr.New(\n\t\t\t\"cannot exclude any platform for package %s because it already has `platforms` defined. \"+\n\t\t\t\t\"Please delete the `platforms` for this package from devbox.json and re-try.\",\n\t\t\tpkg.VersionedName(),\n\t\t)\n\t}\n\n\toldLen := len(pkg.ExcludedPlatforms)\n\tfor _, p := range platforms {\n\t\tif !slices.Contains(pkg.ExcludedPlatforms, p) {\n\t\t\tpkg.ExcludedPlatforms = append(pkg.ExcludedPlatforms, p)\n\t\t}\n\t}\n\tif len(pkg.ExcludedPlatforms) > oldLen {\n\t\tpkgs.ast.appendPlatforms(pkg.Name, \"excluded_platforms\", pkg.ExcludedPlatforms[oldLen:])\n\t\tux.Finfof(writer, \"Excluded platform %s for package %s\\n\", strings.Join(platforms, \", \"),\n\t\t\tpkg.VersionedName())\n\t}\n\treturn nil\n}\n\nfunc (pkgs *PackagesMutator) UnmarshalJSON(data []byte) error {\n\t// First, attempt to unmarshal as a list of strings (legacy format)\n\tvar packages []string\n\tif err := json.Unmarshal(data, &packages); err == nil {\n\t\tpkgs.collection = packagesFromLegacyList(packages)\n\t\treturn nil\n\t}\n\n\t// Second, attempt to unmarshal as a map of Packages\n\t// We use orderedmap to preserve the order of the packages. While the JSON\n\t// specification specifies that maps are unordered, we do rely on the order\n\t// for certain functionality.\n\torderedMap := orderedmap.New[string, Package]()\n\terr := json.Unmarshal(data, &orderedMap)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// Convert the ordered map to a list of packages, and set the name field\n\t// from the map's key\n\tpackagesList := []Package{}\n\tfor pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {\n\t\tpkg := pair.Value\n\t\tpkg.Name = pair.Key\n\t\tpackagesList = append(packagesList, pkg)\n\t}\n\tpkgs.collection = packagesList\n\treturn nil\n}\n\nfunc (pkgs *PackagesMutator) SetPatch(versionedName string, mode PatchMode) error {\n\tif err := mode.validate(); err != nil {\n\t\treturn fmt.Errorf(\"set patch field for %s: %v\", versionedName, err)\n\t}\n\n\tname, version := parseVersionedName(versionedName)\n\ti := pkgs.index(name, version)\n\tif i == -1 {\n\t\treturn errors.Errorf(\"package %s not found\", versionedName)\n\t}\n\n\tpkgs.collection[i].PatchGlibc = false\n\tpkgs.collection[i].Patch = mode\n\tif mode == PatchAuto {\n\t\t// PatchAuto is the default behavior, so just remove the field.\n\t\tpkgs.ast.removePatch(name)\n\t} else {\n\t\tpkgs.ast.setPatch(name, mode)\n\t}\n\treturn nil\n}\n\nfunc (pkgs *PackagesMutator) SetDisablePlugin(versionedName string, v bool) error {\n\tname, version := parseVersionedName(versionedName)\n\ti := pkgs.index(name, version)\n\tif i == -1 {\n\t\treturn errors.Errorf(\"package %s not found\", versionedName)\n\t}\n\tif pkgs.collection[i].DisablePlugin != v {\n\t\tpkgs.collection[i].DisablePlugin = v\n\t\tpkgs.ast.setPackageBool(name, \"disable_plugin\", v)\n\t}\n\treturn nil\n}\n\nfunc (pkgs *PackagesMutator) SetOutputs(writer io.Writer, versionedName string, outputs []string) error {\n\tname, version := parseVersionedName(versionedName)\n\ti := pkgs.index(name, version)\n\tif i == -1 {\n\t\treturn errors.Errorf(\"package %s not found\", versionedName)\n\t}\n\n\ttoAdd := []string{}\n\tfor _, o := range outputs {\n\t\tif !slices.Contains(pkgs.collection[i].Outputs, o) {\n\t\t\ttoAdd = append(toAdd, o)\n\t\t}\n\t}\n\n\tif len(toAdd) > 0 {\n\t\tpkg := &pkgs.collection[i]\n\t\tpkgs.ast.appendOutputs(pkg.Name, \"outputs\", toAdd)\n\t\tux.Finfof(writer, \"Added outputs %s to package %s\\n\", strings.Join(toAdd, \", \"), versionedName)\n\t}\n\treturn nil\n}\n\nfunc (pkgs *PackagesMutator) SetAllowInsecure(writer io.Writer, versionedName string, whitelist []string) error {\n\tname, version := parseVersionedName(versionedName)\n\ti := pkgs.index(name, version)\n\tif i == -1 {\n\t\treturn errors.Errorf(\"package %s not found\", versionedName)\n\t}\n\n\ttoAdd := []string{}\n\tfor _, w := range whitelist {\n\t\tif !slices.Contains(pkgs.collection[i].AllowInsecure, w) {\n\t\t\ttoAdd = append(toAdd, w)\n\t\t}\n\t}\n\n\tif len(toAdd) > 0 {\n\t\tpkg := &pkgs.collection[i]\n\t\tpkgs.ast.appendAllowInsecure(pkg.Name, \"allow_insecure\", toAdd)\n\t\tpkg.AllowInsecure = append(pkg.AllowInsecure, toAdd...)\n\t\tux.Finfof(writer, \"Allowed insecure %s for package %s\\n\", strings.Join(toAdd, \", \"), versionedName)\n\t}\n\treturn nil\n}\n\nfunc (pkgs *PackagesMutator) index(name, version string) int {\n\treturn slices.IndexFunc(pkgs.collection, func(p Package) bool {\n\t\treturn p.Name == name && p.Version == version\n\t})\n}\n\n// PatchMode specifies when to patch packages.\ntype PatchMode string\n\nconst (\n\t// PatchAuto automatically applies patches to fix known issues with\n\t// certain packages. It is the default behavior when the config doesn't\n\t// specify a patching mode.\n\tPatchAuto PatchMode = \"auto\"\n\n\t// PatchAlways always applies patches to a package, overriding the\n\t// default behavior of PatchAuto. It might cause problems with untested\n\t// packages.\n\tPatchAlways PatchMode = \"always\"\n\n\t// PatchNever disables all patching for a package.\n\tPatchNever PatchMode = \"never\"\n)\n\nfunc (p PatchMode) validate() error {\n\tswitch p {\n\tcase PatchAuto, PatchAlways, PatchNever:\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid patch mode %q (must be %s, %s or %s)\",\n\t\t\tp, PatchAuto, PatchAlways, PatchNever)\n\t}\n}\n\ntype Package struct {\n\tName    string\n\tVersion string `json:\"version,omitempty\"`\n\n\tDisablePlugin     bool     `json:\"disable_plugin,omitempty\"`\n\tPlatforms         []string `json:\"platforms,omitempty\"`\n\tExcludedPlatforms []string `json:\"excluded_platforms,omitempty\"`\n\n\t// PatchGlibc applies a function to the package's derivation that\n\t// patches any ELF binaries to use the latest version of nixpkgs#glibc.\n\t//\n\t// Deprecated: Use Patch instead, which also patches glibc.\n\tPatchGlibc bool `json:\"patch_glibc,omitempty\"`\n\n\t// Patch controls when to patch the package. If empty, it defaults to\n\t// [PatchAuto].\n\tPatch PatchMode `json:\"patch,omitempty\"`\n\n\t// Outputs is the list of outputs to use for this package, assuming\n\t// it is a nix package. If empty, the default output is used.\n\tOutputs []string `json:\"outputs,omitempty\"`\n\n\t// AllowInsecure is a whitelist of packages that may be marked insecure\n\t// in nixpkgs, but are allowed by the user to be installed.\n\tAllowInsecure []string `json:\"allow_insecure,omitempty\"`\n}\n\nfunc NewVersionOnlyPackage(name, version string) Package {\n\treturn Package{\n\t\tName:    name,\n\t\tVersion: version,\n\t}\n}\n\n// enabledOnPlatform returns whether the package is enabled on the given platform.\n// If the package has a list of platforms, it is enabled only on those platforms.\n// If the package has a list of excluded platforms, it is enabled on all platforms\n// except those.\nfunc (p *Package) IsEnabledOnPlatform() bool {\n\tplatform := nix.System()\n\tif len(p.Platforms) > 0 {\n\t\tfor _, plt := range p.Platforms {\n\t\t\tif plt == platform {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\tfor _, plt := range p.ExcludedPlatforms {\n\t\tif plt == platform {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (p *Package) VersionedName() string {\n\tname := p.Name\n\tif p.Version != \"\" {\n\t\tname += \"@\" + p.Version\n\t}\n\treturn name\n}\n\nfunc (p *Package) UnmarshalJSON(data []byte) error {\n\t// First, attempt to unmarshal as a version-only string\n\tif err := json.Unmarshal(data, &p.Version); err != nil {\n\t\t// Second, attempt to unmarshal as a Package struct\n\t\ttype packageAlias Package // Use an alias-type to avoid infinite recursion\n\t\talias := &packageAlias{}\n\t\tif err := json.Unmarshal(data, alias); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\t*p = Package(*alias)\n\t}\n\n\tif p.Patch == \"\" {\n\t\tif p.PatchGlibc {\n\t\t\t// Force patching if the user has an old config with the deprecated\n\t\t\t// patch_glibc field set to true.\n\t\t\tp.Patch = PatchAlways\n\t\t} else {\n\t\t\t// Default to PatchAuto if the field is missing, null,\n\t\t\t// or empty.\n\t\t\tp.Patch = PatchAuto\n\t\t}\n\t}\n\treturn p.Patch.validate()\n}\n\n// parseVersionedName parses the name and version from package@version representation\nfunc parseVersionedName(versionedName string) (name, version string) {\n\tvar found bool\n\tname, version, found = searcher.ParseVersionedPackage(versionedName)\n\tif !found {\n\t\t// Case without any @version in the versionedName\n\t\t// We deliberately do not set version to `latest`\n\t\treturn versionedName, \"\" /*version*/\n\t}\n\treturn name, version\n}\n\n// packagesFromLegacyList converts a list of strings to a list of packages\n// Example inputs: `[\"python@latest\", \"hello\", \"cowsay@1\"]`\nfunc packagesFromLegacyList(packages []string) []Package {\n\tpackagesList := []Package{}\n\tfor _, p := range packages {\n\t\tname, version := parseVersionedName(p)\n\t\tpackagesList = append(packagesList, NewVersionOnlyPackage(name, version))\n\t}\n\treturn packagesList\n}\n"
  },
  {
    "path": "internal/devconfig/configfile/packages_test.go",
    "content": "package configfile\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/tailscale/hujson\"\n)\n\n// TestJsonifyConfigPackages tests the jsonMarshal and jsonUnmarshal of the Config.Packages field\nfunc TestJsonifyConfigPackages(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\tjsonConfig string\n\t\texpected   PackagesMutator\n\t}{\n\t\t{\n\t\t\tname:       \"empty-list\",\n\t\t\tjsonConfig: `{\"packages\":[]}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"empty-map\",\n\t\t\tjsonConfig: `{\"packages\":{}}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"flat-list\",\n\t\t\tjsonConfig: `{\"packages\":[\"python\",\"hello@latest\",\"go@1.20\"]}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: packagesFromLegacyList([]string{\"python\", \"hello@latest\", \"go@1.20\"}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"map-with-string-value\",\n\t\t\tjsonConfig: `{\"packages\":{\"python\":\"latest\",\"go\":\"1.20\"}}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{\n\t\t\t\t\tNewVersionOnlyPackage(\"python\", \"latest\"),\n\t\t\t\t\tNewVersionOnlyPackage(\"go\", \"1.20\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tname:       \"map-with-struct-value\",\n\t\t\tjsonConfig: `{\"packages\":{\"python\":{\"version\":\"latest\"}}}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"python\",\n\t\t\t\t\t\tVersion: \"latest\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"map-with-string-and-struct-values\",\n\t\t\tjsonConfig: `{\"packages\":{\"go\":\"1.20\",\"emacs\":\"latest\",\"python\":{\"version\":\"latest\"}}}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{\n\t\t\t\t\tNewVersionOnlyPackage(\"go\", \"1.20\"),\n\t\t\t\t\tNewVersionOnlyPackage(\"emacs\", \"latest\"),\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"python\",\n\t\t\t\t\t\tVersion: \"latest\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"map-with-platforms\",\n\t\t\tjsonConfig: `{\"packages\":{\"python\":{\"version\":\"latest\",` +\n\t\t\t\t`\"platforms\":[\"x86_64-darwin\",\"aarch64-linux\"]}}}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:      \"python\",\n\t\t\t\t\t\tVersion:   \"latest\",\n\t\t\t\t\t\tPlatforms: []string{\"x86_64-darwin\", \"aarch64-linux\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"map-with-excluded-platforms\",\n\t\t\tjsonConfig: `{\"packages\":{\"python\":{\"version\":\"latest\",` +\n\t\t\t\t`\"excluded_platforms\":[\"x86_64-linux\"]}}}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:              \"python\",\n\t\t\t\t\t\tVersion:           \"latest\",\n\t\t\t\t\t\tExcludedPlatforms: []string{\"x86_64-linux\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"map-with-platforms-and-excluded-platforms\",\n\t\t\tjsonConfig: `{\"packages\":{\"python\":{\"version\":\"latest\",` +\n\t\t\t\t`\"platforms\":[\"x86_64-darwin\",\"aarch64-linux\"],` +\n\t\t\t\t`\"excluded_platforms\":[\"x86_64-linux\"]}}}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:              \"python\",\n\t\t\t\t\t\tVersion:           \"latest\",\n\t\t\t\t\t\tPlatforms:         []string{\"x86_64-darwin\", \"aarch64-linux\"},\n\t\t\t\t\t\tExcludedPlatforms: []string{\"x86_64-linux\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"map-with-platforms-and-excluded-platforms-local-flake\",\n\t\t\tjsonConfig: `{\"packages\":{\"path:my-php-flake#hello\":{\"version\":\"latest\",` +\n\t\t\t\t`\"platforms\":[\"x86_64-darwin\",\"aarch64-linux\"],` +\n\t\t\t\t`\"excluded_platforms\":[\"x86_64-linux\"]}}}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:              \"path:my-php-flake#hello\",\n\t\t\t\t\t\tVersion:           \"latest\",\n\t\t\t\t\t\tPlatforms:         []string{\"x86_64-darwin\", \"aarch64-linux\"},\n\t\t\t\t\t\tExcludedPlatforms: []string{\"x86_64-linux\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"map-with-platforms-and-excluded-platforms-remote-flake\",\n\t\t\tjsonConfig: `{\"packages\":{\"github:F1bonacc1/process-compose/v0.43.1\":` +\n\t\t\t\t`{\"version\":\"latest\",` +\n\t\t\t\t`\"platforms\":[\"x86_64-darwin\",\"aarch64-linux\"],` +\n\t\t\t\t`\"excluded_platforms\":[\"x86_64-linux\"]}}}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:              \"github:F1bonacc1/process-compose/v0.43.1\",\n\t\t\t\t\t\tVersion:           \"latest\",\n\t\t\t\t\t\tPlatforms:         []string{\"x86_64-darwin\", \"aarch64-linux\"},\n\t\t\t\t\t\tExcludedPlatforms: []string{\"x86_64-linux\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"map-with-platforms-and-excluded-platforms-nixpkgs-reference\",\n\t\t\tjsonConfig: `{\"packages\":{\"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello\":` +\n\t\t\t\t`{\"version\":\"latest\",` +\n\t\t\t\t`\"platforms\":[\"x86_64-darwin\",\"aarch64-linux\"],` +\n\t\t\t\t`\"excluded_platforms\":[\"x86_64-linux\"]}}}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:              \"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello\",\n\t\t\t\t\t\tVersion:           \"latest\",\n\t\t\t\t\t\tPlatforms:         []string{\"x86_64-darwin\", \"aarch64-linux\"},\n\t\t\t\t\t\tExcludedPlatforms: []string{\"x86_64-linux\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"map-with-platforms-and-excluded-platforms-and-outputs-nixpkgs-reference\",\n\t\t\tjsonConfig: `{\"packages\":{\"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello\":` +\n\t\t\t\t`{\"version\":\"latest\",` +\n\t\t\t\t`\"platforms\":[\"x86_64-darwin\",\"aarch64-linux\"],` +\n\t\t\t\t`\"excluded_platforms\":[\"x86_64-linux\"],` +\n\t\t\t\t`\"outputs\":[\"cli\"]` +\n\t\t\t\t`}}}`,\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:              \"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello\",\n\t\t\t\t\t\tVersion:           \"latest\",\n\t\t\t\t\t\tPlatforms:         []string{\"x86_64-darwin\", \"aarch64-linux\"},\n\t\t\t\t\t\tExcludedPlatforms: []string{\"x86_64-linux\"},\n\t\t\t\t\t\tOutputs:           []string{\"cli\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"map-with-allow-insecure-nixpkgs-reference\",\n\t\t\tjsonConfig: `{\"packages\":{\"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#python\":` +\n\t\t\t\t`{\"version\":\"2.7\",` +\n\t\t\t\t`\"allow_insecure\":[\"python-2.7.18.1\"]` +\n\t\t\t\t`}}}`,\n\n\t\t\texpected: PackagesMutator{\n\t\t\t\tcollection: []Package{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:          \"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#python\",\n\t\t\t\t\t\tVersion:       \"2.7\",\n\t\t\t\t\t\tAllowInsecure: []string{\"python-2.7.18.1\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tconfig, err := LoadBytes([]byte(testCase.jsonConfig))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"load error: %v\", err)\n\t\t\t}\n\t\t\tif diff := diffPackages(t, config.PackagesMutator, testCase.expected); diff != \"\" {\n\t\t\t\tt.Errorf(\"got wrong packages (-want +got):\\n%s\", diff)\n\t\t\t}\n\n\t\t\tgot, err := hujson.Minimize(config.Bytes())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif string(got) != testCase.jsonConfig {\n\t\t\t\tt.Errorf(\"expected: %v, got: %v\", testCase.jsonConfig, string(got))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc diffPackages(t *testing.T, got, want PackagesMutator) string {\n\tt.Helper()\n\n\treturn cmp.Diff(want, got, cmpopts.IgnoreUnexported(PackagesMutator{}, Package{}))\n}\n\nfunc TestParseVersionedName(t *testing.T) {\n\ttestCases := []struct {\n\t\tname            string\n\t\tinput           string\n\t\texpectedName    string\n\t\texpectedVersion string\n\t}{\n\t\t{\n\t\t\tname:            \"no-version\",\n\t\t\tinput:           \"python\",\n\t\t\texpectedName:    \"python\",\n\t\t\texpectedVersion: \"\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with-version-latest\",\n\t\t\tinput:           \"python@latest\",\n\t\t\texpectedName:    \"python\",\n\t\t\texpectedVersion: \"latest\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with-version\",\n\t\t\tinput:           \"python@1.2.3\",\n\t\t\texpectedName:    \"python\",\n\t\t\texpectedVersion: \"1.2.3\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with-two-@-signs\",\n\t\t\tinput:           \"emacsPackages.@@latest\",\n\t\t\texpectedName:    \"emacsPackages.@\",\n\t\t\texpectedVersion: \"latest\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with-trailing-@-sign\",\n\t\t\tinput:           \"emacsPackages.@\",\n\t\t\texpectedName:    \"emacsPackages.@\",\n\t\t\texpectedVersion: \"\",\n\t\t},\n\t\t{\n\t\t\tname:            \"local-flake\",\n\t\t\tinput:           \"path:my-php-flake#hello\",\n\t\t\texpectedName:    \"path:my-php-flake#hello\",\n\t\t\texpectedVersion: \"\",\n\t\t},\n\t\t{\n\t\t\tname:            \"remote-flake\",\n\t\t\tinput:           \"github:F1bonacc1/process-compose/v0.43.1\",\n\t\t\texpectedName:    \"github:F1bonacc1/process-compose/v0.43.1\",\n\t\t\texpectedVersion: \"\",\n\t\t},\n\t\t{\n\t\t\tname:            \"nixpkgs-reference\",\n\t\t\tinput:           \"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello\",\n\t\t\texpectedName:    \"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello\",\n\t\t\texpectedVersion: \"\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tname, version := parseVersionedName(testCase.input)\n\t\t\tif name != testCase.expectedName {\n\t\t\t\tt.Errorf(\"expected: %v, got: %v\", testCase.expectedName, name)\n\t\t\t}\n\t\t\tif version != testCase.expectedVersion {\n\t\t\t\tt.Errorf(\"expected: %v, got: %v\", testCase.expectedVersion, version)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/devconfig/configfile/scripts.go",
    "content": "package configfile\n\nimport (\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/internal/devbox/shellcmd\"\n)\n\ntype script struct {\n\tshellcmd.Commands\n\tComments string\n}\n\ntype Scripts map[string]*script\n\nfunc (c *ConfigFile) Scripts() Scripts {\n\tif c == nil || c.Shell == nil {\n\t\treturn nil\n\t}\n\tresult := make(Scripts)\n\tfor name, commands := range c.Shell.Scripts {\n\t\tcomments := \"\"\n\t\tif c.ast != nil {\n\t\t\tcomments = string(c.ast.beforeComment(\"shell\", \"scripts\", name))\n\t\t}\n\t\tresult[name] = &script{\n\t\t\tCommands: *commands,\n\t\t\tComments: comments,\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc (s Scripts) WithRelativePaths(projectDir string) Scripts {\n\tresult := make(Scripts, len(s))\n\tfor name, s := range s {\n\t\tcommandsWithRelativePaths := shellcmd.Commands{}\n\t\tfor _, c := range s.Commands.Cmds {\n\t\t\tcommandsWithRelativePaths.Cmds = append(\n\t\t\t\tcommandsWithRelativePaths.Cmds,\n\t\t\t\tstrings.ReplaceAll(c, projectDir, \".\"),\n\t\t\t)\n\t\t}\n\t\tresult[name] = &script{\n\t\t\tCommands: commandsWithRelativePaths,\n\t\t\tComments: s.Comments,\n\t\t}\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "internal/devconfig/init.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devconfig\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"go.jetify.com/devbox/internal/devconfig/configfile\"\n)\n\nfunc Init(dir string) (*Config, error) {\n\tfile, err := os.OpenFile(\n\t\tfilepath.Join(dir, configfile.DefaultName),\n\t\tos.O_RDWR|os.O_CREATE|os.O_EXCL,\n\t\t0o644,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tos.Remove(file.Name())\n\t\t}\n\t}()\n\n\tnewConfig := DefaultConfig()\n\t_, err = file.Write(newConfig.Root.Bytes())\n\tdefer file.Close()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn newConfig, nil\n}\n"
  },
  {
    "path": "internal/devpkg/narinfo_cache.go",
    "content": "package devpkg\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/devbox/providers/nixcache\"\n\t\"go.jetify.com/devbox/internal/goutil\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// binaryCache is the store from which to fetch this package's binaries.\n// It is used as FromStore in builtins.fetchClosure.\nconst binaryCache = \"https://cache.nixos.org\"\n\n// useDefaultOutputs is a special value for the outputName parameter of\n// fetchNarInfoStatusOnce, which indicates that the default outputs should be\n// used.\nconst useDefaultOutputs = \"__default_outputs__\"\n\nfunc (p *Package) IsOutputInBinaryCache(outputName string) (bool, error) {\n\tif eligible, err := p.isEligibleForBinaryCache(); err != nil {\n\t\treturn false, err\n\t} else if !eligible {\n\t\treturn false, nil\n\t}\n\n\treturn p.areExpectedOutputsInCacheOnce(outputName)\n}\n\n// IsInBinaryCache returns true if the package is in the binary cache.\n// ALERT: Callers in a perf-sensitive code path should call FillNarInfoCache\n// before calling this function.\nfunc (p *Package) IsInBinaryCache() (bool, error) {\n\tif eligible, err := p.isEligibleForBinaryCache(); err != nil {\n\t\treturn false, err\n\t} else if !eligible {\n\t\treturn false, nil\n\t}\n\n\treturn p.areExpectedOutputsInCacheOnce(useDefaultOutputs)\n}\n\n// FillNarInfoCache checks the remote binary cache for the narinfo of each\n// package in the list, and caches the result.\n// Callers of IsInBinaryCache may call this function first as a perf-optimization.\nfunc FillNarInfoCache(ctx context.Context, packages ...*Package) error {\n\tdefer debug.FunctionTimer().End()\n\n\teligiblePackages := []*Package{}\n\tfor _, p := range packages {\n\t\t// IMPORTANT: isEligibleForBinaryCache will call resolve() which is NOT\n\t\t// concurrency safe. Hence, we call it outside of the go-routine.\n\t\tisEligible, err := p.isEligibleForBinaryCache()\n\t\t// If the package is not eligible or there is an error in determining that, then skip it.\n\t\tif isEligible && err == nil {\n\t\t\teligiblePackages = append(eligiblePackages, p)\n\t\t}\n\t}\n\tif len(eligiblePackages) == 0 {\n\t\treturn nil\n\t}\n\n\tgroup, _ := errgroup.WithContext(ctx)\n\tfor _, p := range eligiblePackages {\n\t\tpkg := p // copy the loop variable since its used in a closure below\n\t\toutputNames, err := pkg.GetOutputNames()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, outputName := range outputNames {\n\t\t\tname := outputName\n\t\t\tgroup.Go(func() error {\n\t\t\t\t_, err := pkg.fetchNarInfoStatusOnce(name)\n\t\t\t\treturn err\n\t\t\t})\n\t\t}\n\t}\n\treturn group.Wait()\n}\n\n// areExpectedOutputsInCacheOnce wraps fetchNarInfoStatusOnce and returns true\n// if the expected outputs are in the cache.\nfunc (p *Package) areExpectedOutputsInCacheOnce(outputName string) (bool, error) {\n\toutputToCache, err := p.fetchNarInfoStatusOnce(outputName)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif outputName == useDefaultOutputs {\n\t\toutputs, err := p.outputsForOutputName(outputName)\n\t\t// If we don't have default outputs, then we can't check if they are in the cache.\n\t\tif err != nil || len(outputs) == 0 {\n\t\t\treturn false, err\n\t\t}\n\t\treturn len(outputToCache) == len(outputs), nil\n\t}\n\treturn len(outputToCache) == 1, nil\n}\n\n// fetchNarInfoStatusOnce fetches the cache status for the package and output.\n// It returns a map of outputs to cache URIs for each cache hit. Missing\n// outputs are not returned, and if no outputs are found, an nil map is returned.\n//\n// This function caches the result of the first call to avoid multiple calls\n// even if there are multiple package structs for the same package.\n//\n// The outputName parameter is the name of the output to check for in the cache.\n// If outputName is UseDefaultOutput, all default outputs will be checked.\nfunc (p *Package) fetchNarInfoStatusOnce(\n\toutputName string,\n) (map[string]string, error) {\n\tctx := context.TODO()\n\n\toutputToCache := map[string]string{}\n\tcaches, err := readCaches(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toutputs, err := p.outputsForOutputName(outputName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, output := range outputs {\n\t\tpathParts := nix.NewStorePathParts(output.Path)\n\t\thash := pathParts.Hash\n\t\tfor _, cache := range caches {\n\t\t\tinCache := false\n\t\t\tif strings.HasPrefix(cache, \"s3\") {\n\t\t\t\tinCache, err = fetchNarInfoStatusFromS3(ctx, cache, hash)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tinCache, err = fetchNarInfoStatusFromHTTP(ctx, cache, hash)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif inCache {\n\t\t\t\t// Found it, no need to check more caches.\n\t\t\t\toutputToCache[output.Name] = cache\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn outputToCache, nil\n}\n\nfunc (p *Package) AreAllOutputsInCache(\n\tctx context.Context, w io.Writer, cacheURI string,\n) (bool, error) {\n\tstorePaths, err := p.GetStorePaths(ctx, w)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tfor _, storePath := range storePaths {\n\t\tpathParts := nix.NewStorePathParts(storePath)\n\t\thash := pathParts.Hash\n\t\tif strings.HasPrefix(cacheURI, \"s3\") {\n\t\t\tinCache, err := fetchNarInfoStatusFromS3(ctx, cacheURI, hash)\n\t\t\tif err != nil || !inCache {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t} else {\n\t\t\tinCache, err := fetchNarInfoStatusFromHTTP(ctx, cacheURI, hash)\n\t\t\tif err != nil || !inCache {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t}\n\t}\n\treturn true, nil\n}\n\nfunc (p *Package) outputsForOutputName(output string) ([]lock.Output, error) {\n\tsysInfo, err := p.sysInfoIfExists()\n\tif err != nil || sysInfo == nil {\n\t\treturn nil, err\n\t}\n\n\tvar outputs []lock.Output\n\tif output == useDefaultOutputs {\n\t\toutputs = sysInfo.DefaultOutputs()\n\t} else {\n\t\tout, err := sysInfo.Output(output)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\toutputs = []lock.Output{out}\n\t}\n\treturn outputs, nil\n}\n\n// isEligibleForBinaryCache returns true if we have additional metadata about\n// the package to query it from the binary cache.\nfunc (p *Package) isEligibleForBinaryCache() (bool, error) {\n\tdefer debug.FunctionTimer().End()\n\t// Patched packages are not in the binary cache.\n\tif p.Patch {\n\t\treturn false, nil\n\t}\n\tsysInfo, err := p.sysInfoIfExists()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn sysInfo != nil, nil\n}\n\n// sysInfoIfExists returns the system info for the user's system. If the sysInfo\n// is missing, then nil is returned\n// NOTE: this is called from multiple go-routines and needs to be concurrency safe.\n// Hence, we compute nix.Version, nix.System and lockfile.Resolve prior to calling this\n// function from within a goroutine.\nfunc (p *Package) sysInfoIfExists() (*lock.SystemInfo, error) {\n\tif !p.isVersioned() {\n\t\treturn nil, nil\n\t}\n\n\t// disable for nix < 2.17\n\tif !nix.AtLeast(nix.Version2_17) {\n\t\treturn nil, nil\n\t}\n\n\tentry, err := p.lockfile.Resolve(p.Raw)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif entry.Systems == nil {\n\t\treturn nil, nil\n\t}\n\n\t// Check if the user's system's info is present in the lockfile\n\tsysInfo, ok := entry.Systems[nix.System()]\n\tif !ok {\n\t\treturn nil, nil\n\t}\n\treturn sysInfo, nil\n}\n\nvar narInfoStatusFnCache = sync.Map{}\n\nfunc fetchNarInfoStatusFromHTTP(\n\tctx context.Context,\n\turi string,\n\thash string,\n) (bool, error) {\n\tkey := fmt.Sprintf(\"%s/%s\", uri, hash)\n\tfetch, _ := narInfoStatusFnCache.LoadOrStore(key, sync.OnceValues(\n\t\tfunc() (bool, error) {\n\t\t\tctx, cancel := context.WithTimeout(ctx, 5*time.Second)\n\t\t\tdefer cancel()\n\t\t\turl := fmt.Sprintf(\"%s/%s.narinfo\", uri, hash)\n\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\tres, err := http.DefaultClient.Do(req)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\tdefer res.Body.Close()\n\t\t\treturn res.StatusCode == http.StatusOK, nil\n\t\t},\n\t))\n\treturn fetch.(func() (bool, error))()\n}\n\nfunc fetchNarInfoStatusFromS3(\n\tctx context.Context,\n\turi string,\n\thash string,\n) (bool, error) {\n\tkey := fmt.Sprintf(\"%s/%s\", uri, hash)\n\tfetch, _ := narInfoStatusFnCache.LoadOrStore(key, sync.OnceValues(\n\t\tfunc() (bool, error) {\n\t\t\ts3Client, err := nixcache.S3Client(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\n\t\t\tbucketURI, err := url.Parse(uri)\n\t\t\tif err != nil {\n\t\t\t\treturn false, errors.WithStack(err)\n\t\t\t}\n\n\t\t\t_, err = s3Client.GetObject(ctx,\n\t\t\t\t&s3.GetObjectInput{\n\t\t\t\t\tBucket: aws.String(bucketURI.Hostname()),\n\t\t\t\t\tKey:    aws.String(hash + \".narinfo\"),\n\t\t\t\t},\n\t\t\t\tfunc(o *s3.Options) {\n\t\t\t\t\tif bucketURI.Query().Get(\"region\") != \"\" {\n\t\t\t\t\t\to.Region = bucketURI.Query().Get(\"region\")\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t)\n\t\t\treturn err == nil, nil\n\t\t},\n\t))\n\treturn fetch.(func() (bool, error))()\n}\n\nvar nixCacheIsConfigured = goutil.OnceValueWithContext(nixcache.IsConfigured)\n\nfunc readCaches(ctx context.Context) ([]string, error) {\n\tcacheURIs := []string{binaryCache}\n\tif !nixCacheIsConfigured.Do(ctx) {\n\t\treturn cacheURIs, nil\n\t}\n\n\totherCaches, err := nixcache.CachedReadCaches(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, c := range otherCaches {\n\t\tcacheURIs = append(cacheURIs, c.GetUri())\n\t}\n\treturn cacheURIs, nil\n}\n\nfunc ClearNarInfoCache() {\n\tnarInfoStatusFnCache = sync.Map{}\n}\n"
  },
  {
    "path": "internal/devpkg/outputs.go",
    "content": "package devpkg\n\ntype Output struct {\n\tName     string\n\tCacheURI string\n}\n\n// outputs are the nix package outputs\ntype outputs struct {\n\tselectedNames []string\n\tdefaultNames  []string\n}\n\nfunc (out *outputs) GetNames(pkg *Package) ([]string, error) {\n\tif len(out.selectedNames) > 0 {\n\t\treturn out.selectedNames, nil\n\t}\n\n\t// else, get the default outputs from the lockfile\n\t// if we haven't already\n\tif out.defaultNames == nil {\n\t\tif err := out.initDefaultNames(pkg); err != nil {\n\t\t\treturn []string{}, err\n\t\t}\n\t}\n\treturn out.defaultNames, nil\n}\n\n// initDefaultNames initializes the defaultNames field of the Outputs object.\n// We run this lazily (rather than eagerly in initOutputs) because it depends on the Package,\n// and initOutputs is called from the Package constructor, so cannot depend on Package.\nfunc (out *outputs) initDefaultNames(pkg *Package) error {\n\tsysInfo, err := pkg.sysInfoIfExists()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tout.defaultNames = []string{}\n\tif sysInfo == nil {\n\t\treturn nil\n\t}\n\n\tfor _, output := range sysInfo.DefaultOutputs() {\n\t\tout.defaultNames = append(out.defaultNames, output.Name)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/devpkg/package.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devpkg\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/boxcli/featureflag\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/cachehash\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devconfig/configfile\"\n\t\"go.jetify.com/devbox/internal/devpkg/pkgtype\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/ux\"\n\t\"go.jetify.com/devbox/nix/flake\"\n\t\"go.jetify.com/devbox/plugins\"\n)\n\n// Package represents a \"package\" added to the devbox.json config.\n// A unique feature of flakes is that they have well-defined \"inputs\" and \"outputs\".\n// This Package will be aggregated into a specific \"flake input\" (see shellgen.flakeInput).\ntype Package struct {\n\tplugins.BuiltIn\n\tlockfile        lock.Locker\n\tIsDevboxPackage bool\n\n\t// If package triggers a built-in plugin, setting this to true will disable it.\n\t// If package does not trigger plugin, this will have no effect.\n\tDisablePlugin bool\n\n\t// installable is the flake attribute that the package resolves to.\n\t// When it gets set depends on the original package string:\n\t//\n\t// - If the parsed package string is unambiguously a flake installable\n\t//   (not \"name\" or \"name@version\"), then it is set immediately.\n\t// - Otherwise, it's set after calling resolve.\n\t//\n\t// This is done for performance reasons. Some commands don't require the\n\t// fully-resolved package, so we don't want to waste time computing it.\n\tinstallable flake.Installable\n\n\t// resolve resolves a Devbox package string to a Nix installable.\n\t//\n\t// - If the package exists in the lockfile, it resolves to the\n\t//   lockfile's installable.\n\t// - If the package doesn't exist in the lockfile, it resolves to the\n\t//   installable returned by the search index (/v1/resolve).\n\t//\n\t// After resolving the installable, it also sets storePath when the\n\t// package exists in the Nix binary cache.\n\t//\n\t// For flake packages (non-devbox packages), resolve is a no-op.\n\tresolve func() error\n\n\t// Raw is the devbox package name from the devbox.json config.\n\t// Raw has a few forms:\n\t// 1. Devbox Packages\n\t//    a. versioned packages\n\t//       examples:  go@1.20, python@latest\n\t//    b. any others?\n\t// 2. Local\n\t//    flakes in a relative sub-directory\n\t//    example: ./local_flake_subdir#myPackage\n\t// 3. Github\n\t//    remote flakes with raw name starting with `Github:`\n\t//    example: github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello\n\tRaw string\n\n\t// Outputs is a list of outputs to build from the package's derivation.\n\toutputs outputs\n\n\t// Patch controls if Devbox environments will do additional patching to\n\t// address known issues with the package.\n\tPatch bool\n\n\t// AllowInsecure are a list of nix packages that are whitelisted to be\n\t// installed even if they are marked as insecure.\n\tAllowInsecure []string\n\n\t// isInstallable is true if the package may be enabled on the current platform.\n\t// It's a function to allow deferring nix System call until it's needed.\n\tisInstallable func() bool\n\n\tnormalizedPackageAttributePathCache string // memoized value from normalizedPackageAttributePath()\n}\n\nfunc PackagesFromStringsWithOptions(rawNames []string, l lock.Locker, opts devopt.AddOpts) []*Package {\n\tpackages := []*Package{}\n\tfor _, name := range rawNames {\n\t\tpackages = append(packages, PackageFromStringWithOptions(name, l, opts))\n\t}\n\treturn packages\n}\n\nfunc PackagesFromConfig(packages []configfile.Package, l lock.Locker) []*Package {\n\tresult := []*Package{}\n\tfor _, cfgPkg := range packages {\n\t\tpkg := newPackage(cfgPkg.VersionedName(), cfgPkg.IsEnabledOnPlatform, l)\n\t\tpkg.DisablePlugin = cfgPkg.DisablePlugin\n\t\tpkg.Patch = pkgNeedsPatch(pkg.CanonicalName(), cfgPkg.Patch)\n\t\tpkg.outputs.selectedNames = lo.Uniq(append(pkg.outputs.selectedNames, cfgPkg.Outputs...))\n\t\tpkg.AllowInsecure = cfgPkg.AllowInsecure\n\t\tresult = append(result, pkg)\n\t}\n\treturn result\n}\n\nfunc PackageFromStringWithDefaults(raw string, locker lock.Locker) *Package {\n\treturn newPackage(raw, func() bool { return true } /*isInstallable*/, locker)\n}\n\nfunc PackageFromStringWithOptions(raw string, locker lock.Locker, opts devopt.AddOpts) *Package {\n\tpkg := PackageFromStringWithDefaults(raw, locker)\n\tpkg.DisablePlugin = opts.DisablePlugin\n\tpkg.Patch = pkgNeedsPatch(pkg.CanonicalName(), configfile.PatchMode(opts.Patch))\n\tpkg.outputs.selectedNames = lo.Uniq(append(pkg.outputs.selectedNames, opts.Outputs...))\n\tpkg.AllowInsecure = opts.AllowInsecure\n\treturn pkg\n}\n\nfunc newPackage(raw string, isInstallable func() bool, locker lock.Locker) *Package {\n\tpkg := &Package{\n\t\tRaw:           raw,\n\t\tlockfile:      locker,\n\t\tisInstallable: sync.OnceValue(isInstallable),\n\t}\n\n\t// The raw string is either a Devbox package (\"name\" or \"name@version\")\n\t// or it's a flake installable. In some cases they're ambiguous\n\t// (\"nixpkgs\" is a devbox package and a flake). When that happens, we\n\t// assume a Devbox package.\n\tparsed, err := flake.ParseInstallable(raw)\n\tif err != nil || pkgtype.IsAmbiguous(raw, parsed) {\n\t\t// TODO: This sets runx packages as devbox packages. Not sure if that's what we want.\n\t\tpkg.IsDevboxPackage = true\n\t\tpkg.resolve = sync.OnceValue(func() error { return resolve(pkg) })\n\t\treturn pkg\n\t}\n\n\tpkg.resolve = sync.OnceValue(func() error {\n\t\t// Don't lock flakes that are local paths.\n\t\tif parsed.Ref.Type == flake.TypePath {\n\t\t\treturn nil\n\t\t}\n\t\treturn resolve(pkg)\n\t})\n\tpkg.setInstallable(parsed, locker.ProjectDir())\n\tpkg.outputs = outputs{selectedNames: strings.Split(parsed.Outputs, \",\")}\n\tpkg.Patch = pkgNeedsPatch(pkg.CanonicalName(), configfile.PatchAuto)\n\treturn pkg\n}\n\n// resolve is the implementation of Package.resolve, where it is wrapped in a\n// sync.OnceValue function. It should not be called directly.\nfunc resolve(pkg *Package) error {\n\tresolved, err := pkg.lockfile.Resolve(pkg.LockfileKey())\n\tif err != nil {\n\t\treturn err\n\t}\n\tparsed, err := flake.ParseInstallable(resolved.Resolved)\n\tif err != nil {\n\t\treturn err\n\t}\n\tparsed.Outputs = strings.Join(pkg.outputs.selectedNames, \",\")\n\n\tpkg.setInstallable(parsed, pkg.lockfile.ProjectDir())\n\treturn nil\n}\n\nfunc (p *Package) setInstallable(i flake.Installable, projectDir string) {\n\tif i.Ref.Type == flake.TypePath && !filepath.IsAbs(i.Ref.Path) {\n\t\ti.Ref.Path = filepath.Join(projectDir, i.Ref.Path)\n\t}\n\tp.installable = i\n}\n\nfunc pkgNeedsPatch(canonicalName string, mode configfile.PatchMode) (patch bool) {\n\tmode = cmp.Or(mode, configfile.PatchAuto)\n\tswitch mode {\n\tcase configfile.PatchAuto:\n\t\tpatch = canonicalName == \"python\"\n\tcase configfile.PatchAlways:\n\t\tpatch = true\n\tcase configfile.PatchNever:\n\t\tpatch = false\n\t}\n\tif patch {\n\t\tslog.Debug(\"package needs patching\", \"pkg\", canonicalName, \"mode\", mode)\n\t} else {\n\t\tslog.Debug(\"package doesn't need patching\", \"pkg\", canonicalName, \"mode\", mode)\n\t}\n\treturn patch\n}\n\nvar inputNameRegex = regexp.MustCompile(\"[^a-zA-Z0-9-]+\")\n\n// FlakeInputName generates a name for the input that will be used in the\n// generated flake.nix to import this package. This name must be unique in that\n// flake so we attach a hash to (quasi) ensure uniqueness.\n// Input name will be different from raw package name\nfunc (p *Package) FlakeInputName() string {\n\t_ = p.resolve()\n\n\tresult := \"\"\n\tswitch p.installable.Ref.Type {\n\tcase flake.TypePath:\n\t\tresult = filepath.Base(p.installable.Ref.Path) + \"-\" + p.Hash()\n\tcase flake.TypeGitHub:\n\t\tisNixOS := strings.ToLower(p.installable.Ref.Owner) == \"nixos\"\n\t\tisNixpkgs := isNixOS && strings.ToLower(p.installable.Ref.Repo) == \"nixpkgs\"\n\t\tif isNixpkgs && p.IsDevboxPackage {\n\t\t\tcommitHash := nix.HashFromNixPkgsURL(p.installable.Ref.String())\n\t\t\tresult = \"nixpkgs-\" + commitHash[:min(6, len(commitHash))]\n\t\t} else {\n\t\t\tresult = \"gh-\" + p.installable.Ref.Owner + \"-\" + p.installable.Ref.Repo\n\t\t\tif p.installable.Ref.Rev != \"\" {\n\t\t\t\tresult += \"-\" + p.installable.Ref.Rev\n\t\t\t} else if p.installable.Ref.Ref != \"\" {\n\t\t\t\tresult += \"-\" + p.installable.Ref.Ref\n\t\t\t}\n\t\t}\n\tdefault:\n\t\tresult = p.installable.Ref.String() + \"-\" + p.Hash()\n\t}\n\n\t// replace all non-alphanumeric with dashes\n\treturn inputNameRegex.ReplaceAllString(result, \"-\")\n}\n\n// URLForFlakeInput returns the input url to be used in a flake.nix file. This\n// input can be used to import the package.\nfunc (p *Package) URLForFlakeInput() string {\n\tif err := p.resolve(); err != nil {\n\t\t// TODO(landau): handle error\n\t\tpanic(err)\n\t}\n\treturn p.installable.Ref.String()\n}\n\n// IsInstallable returns whether this package is installable. Not to be confused\n// with the Installable() method which returns the corresponding nix concept.\nfunc (p *Package) IsInstallable() bool {\n\treturn p.isInstallable()\n}\n\n// Installables for this package. Installables is a nix concept defined here:\n// https://nixos.org/manual/nix/stable/command-ref/new-cli/nix.html#installables\nfunc (p *Package) Installables() ([]string, error) {\n\toutputNames, err := p.GetOutputNames()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tinstallables := []string{}\n\tfor _, outputName := range outputNames {\n\t\ti, err := p.InstallableForOutput(outputName)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinstallables = append(installables, i)\n\t}\n\tif len(installables) == 0 {\n\t\t// This means that the package is not in the binary cache\n\t\t// OR it is a flake (??)\n\t\tinstallable, err := p.urlForInstall()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn []string{installable}, nil\n\t}\n\treturn installables, nil\n}\n\nfunc (p *Package) InstallableForOutput(output string) (string, error) {\n\tinCache, err := p.IsOutputInBinaryCache(output)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif inCache {\n\t\tinstallable, err := p.InputAddressedPathForOutput(output)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn installable, nil\n\t}\n\n\t// TODO savil: does this work for outputs?\n\tinstallable, err := p.urlForInstall()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn installable, nil\n}\n\n// FlakeInstallable returns a flake installable. The raw string must contain\n// a valid flake reference parsable by ParseFlakeRef, optionally followed by an\n// #attrpath and/or an ^output.\nfunc (p *Package) FlakeInstallable() (flake.Installable, error) {\n\tif err := p.resolve(); err != nil {\n\t\treturn flake.Installable{}, err\n\t}\n\treturn p.installable, nil\n}\n\n// urlForInstall is used during `nix profile install`.\n// The key difference with URLForFlakeInput is that it has a suffix of\n// `#attributePath`\nfunc (p *Package) urlForInstall() (string, error) {\n\tif err := p.resolve(); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn p.installable.String(), nil\n}\n\nfunc (p *Package) NormalizedDevboxPackageReference() (string, error) {\n\tinstallable, err := p.FlakeInstallable()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif installable.AttrPath == \"\" {\n\t\treturn \"\", nil\n\t}\n\tinstallable.AttrPath = fmt.Sprintf(\"legacyPackages.%s.%s\", nix.System(), installable.AttrPath)\n\tinstallable.Outputs = \"\"\n\treturn installable.String(), nil\n}\n\n// PackageAttributePath returns the short attribute path for a package which\n// does not include packages/legacyPackages or the system name.\nfunc (p *Package) PackageAttributePath() (string, error) {\n\tif err := p.resolve(); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn p.installable.AttrPath, nil\n}\n\n// FullPackageAttributePath returns the attribute path for a package. It is not\n// always normalized which means it should not be used to compare packages.\n// During happy paths (devbox packages and nix flakes that contains a fragment)\n// it is much faster than NormalizedPackageAttributePath\nfunc (p *Package) FullPackageAttributePath() (string, error) {\n\tif p.IsDevboxPackage {\n\t\treference, err := p.NormalizedDevboxPackageReference()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\t_, fragment, _ := strings.Cut(reference, \"#\")\n\t\treturn fragment, nil\n\t}\n\treturn p.NormalizedPackageAttributePath()\n}\n\n// NormalizedPackageAttributePath returns an attribute path normalized by nix\n// search. This is useful for comparing different attribute paths that may\n// point to the same package. Note, it may be an expensive call.\nfunc (p *Package) NormalizedPackageAttributePath() (string, error) {\n\tif p.normalizedPackageAttributePathCache != \"\" {\n\t\treturn p.normalizedPackageAttributePathCache, nil\n\t}\n\tpath, err := p.normalizePackageAttributePath()\n\tif err != nil {\n\t\treturn path, err\n\t}\n\tp.normalizedPackageAttributePathCache = path\n\treturn p.normalizedPackageAttributePathCache, nil\n}\n\n// normalizePackageAttributePath calls nix search to find the normalized attribute\n// path. It may be an expensive call (~100ms).\nfunc (p *Package) normalizePackageAttributePath() (string, error) {\n\tinstallable, err := p.FlakeInstallable()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tinstallable.Outputs = \"\"\n\tquery := installable.String()\n\tif query == \"\" {\n\t\tquery = p.Raw\n\t}\n\n\t// We prefer nix.Search over just trying to parse the package's \"URL\" because\n\t// nix.Search will guarantee that the package exists for the current system.\n\tvar infos map[string]*nix.PkgInfo\n\tif p.IsDevboxPackage && !p.IsRunX() {\n\t\t// Perf optimization: For queries of the form nixpkgs/<commit>#foo, we can\n\t\t// use a nix.Search cache.\n\t\t//\n\t\t// This will be slow if its the first time on the user's machine that this\n\t\t// query is running. Otherwise, it will be cached and fast.\n\t\tif infos, err = nix.SearchNixpkgsAttribute(query); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t} else {\n\t\t// fallback to the slow but generalized nix.Search\n\t\tif infos, err = nix.Search(query); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tif len(infos) == 1 {\n\t\treturn lo.Keys(infos)[0], nil\n\t}\n\n\t// If ambiguous, try to find a default output\n\tif len(infos) > 1 && p.installable.AttrPath == \"\" {\n\t\tfor key := range infos {\n\t\t\tif strings.HasSuffix(key, \".default\") {\n\t\t\t\treturn key, nil\n\t\t\t}\n\t\t}\n\t\tfor key := range infos {\n\t\t\tif strings.HasPrefix(key, \"defaultPackage.\") {\n\t\t\t\treturn key, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// Still ambiguous, return error\n\tif len(infos) > 1 {\n\t\toutputs := fmt.Sprintf(\"It has %d possible outputs\", len(infos))\n\t\tif len(infos) < 10 {\n\t\t\toutputs = \"It has the following possible outputs: \\n\" +\n\t\t\t\tstrings.Join(lo.Keys(infos), \", \")\n\t\t}\n\t\treturn \"\", usererr.New(\n\t\t\t\"Package \\\"%s\\\" is ambiguous. %s\",\n\t\t\tp.Raw,\n\t\t\toutputs,\n\t\t)\n\t}\n\n\tif nix.PkgExistsForAnySystem(query) {\n\t\treturn \"\", usererr.WithUserMessage(\n\t\t\tErrCannotBuildPackageOnSystem,\n\t\t\t\"Package \\\"%s\\\" was found, but we're unable to build it for your system.\"+\n\t\t\t\t\" You may need to choose another version or write a custom flake.\",\n\t\t\tp.Raw,\n\t\t)\n\t}\n\n\treturn \"\", usererr.New(\"Package \\\"%s\\\" was not found\", p.Raw)\n}\n\nvar ErrCannotBuildPackageOnSystem = errors.New(\"unable to build for system\")\n\nfunc (p *Package) Hash() string {\n\tsum := \"\"\n\tif p.installable.Ref.Type == flake.TypePath {\n\t\t// For local flakes, use content hash of the flake.nix file to ensure\n\t\t// user always gets newest flake.\n\t\tsum, _ = cachehash.File(filepath.Join(p.installable.Ref.Path, \"flake.nix\"))\n\t}\n\n\tif sum == \"\" {\n\t\tsum = cachehash.Bytes([]byte(p.installable.String()))\n\t}\n\treturn sum[:min(len(sum), 6)]\n}\n\n// Equals compares two Packages. This may be an expensive operation since it\n// may have to normalize a Package's attribute path, which may require a network\n// call.\nfunc (p *Package) Equals(other *Package) bool {\n\tif p.Raw == other.Raw || p.installable == other.installable {\n\t\treturn true\n\t}\n\n\t// check inputs without fragments as optimization. Next step is expensive\n\tif p.URLForFlakeInput() != other.URLForFlakeInput() {\n\t\treturn false\n\t}\n\n\tname, err := p.NormalizedPackageAttributePath()\n\tif err != nil {\n\t\treturn false\n\t}\n\totherName, err := other.NormalizedPackageAttributePath()\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn name == otherName\n}\n\n// CanonicalName returns the name of the package without the version\n// it only applies to devbox packages\nfunc (p *Package) CanonicalName() string {\n\tif !p.IsDevboxPackage {\n\t\treturn \"\"\n\t}\n\tname, _, _ := strings.Cut(p.Raw, \"@\")\n\treturn name\n}\n\nfunc (p *Package) Versioned() string {\n\tif p.IsDevboxPackage && !p.isVersioned() {\n\t\treturn p.Raw + \"@latest\"\n\t}\n\treturn p.Raw\n}\n\nfunc (p *Package) IsLegacy() bool {\n\treturn p.IsDevboxPackage && !p.isVersioned() && p.lockfile.Get(p.Raw).GetSource() == \"\"\n}\n\nfunc (p *Package) LegacyToVersioned() string {\n\tif !p.IsLegacy() {\n\t\treturn p.Raw\n\t}\n\treturn p.Raw + \"@latest\"\n}\n\n// EnsureNixpkgsPrefetched will prefetch flake for the nixpkgs registry for the package.\n// This is an internal method, and should not be called directly.\nfunc EnsureNixpkgsPrefetched(ctx context.Context, w io.Writer, pkgs []*Package) error {\n\tfor _, input := range pkgs {\n\t\tif err := input.ensureNixpkgsPrefetched(w); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// ensureNixpkgsPrefetched should be called via the public EnsureNixpkgsPrefetched.\n// See function comment there.\nfunc (p *Package) ensureNixpkgsPrefetched(w io.Writer) error {\n\tinCache, err := p.IsInBinaryCache()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif inCache {\n\t\t// We can skip prefetching nixpkgs, if this package is in the binary\n\t\t// cache store.\n\t\treturn nil\n\t}\n\n\thash := p.HashFromNixPkgsURL()\n\tif hash == \"\" {\n\t\treturn nil\n\t}\n\treturn nix.EnsureNixpkgsPrefetched(w, hash)\n}\n\n// version returns the version of the package\n// it only applies to devbox packages\nfunc (p *Package) version() string {\n\tif !p.IsDevboxPackage {\n\t\treturn \"\"\n\t}\n\t_, version, _ := strings.Cut(p.Raw, \"@\")\n\treturn version\n}\n\nfunc (p *Package) isVersioned() bool {\n\treturn p.IsDevboxPackage && strings.Contains(p.Raw, \"@\")\n}\n\nfunc (p *Package) HashFromNixPkgsURL() string {\n\treturn nix.HashFromNixPkgsURL(p.URLForFlakeInput())\n}\n\n// InputAddressedPaths is the input-addressed path in /nix/store\n// It is also the key in the BinaryCache for this package\nfunc (p *Package) InputAddressedPaths() ([]string, error) {\n\tif inCache, err := p.IsInBinaryCache(); err != nil {\n\t\treturn nil, err\n\t} else if !inCache {\n\t\treturn nil,\n\t\t\terrors.Errorf(\"Package %q cannot be fetched from binary cache store\", p.Raw)\n\t}\n\n\tentry, err := p.lockfile.Resolve(p.LockfileKey())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsysInfo := entry.Systems[nix.System()]\n\toutputs := sysInfo.DefaultOutputs()\n\n\tpaths := []string{}\n\tfor _, output := range outputs {\n\t\tp, err := p.InputAddressedPathForOutput(output.Name)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpaths = append(paths, p)\n\t}\n\treturn paths, nil\n}\n\nfunc (p *Package) InputAddressedPathForOutput(output string) (string, error) {\n\tif inCache, err := p.IsInBinaryCache(); err != nil {\n\t\treturn \"\", err\n\t} else if !inCache {\n\t\treturn \"\",\n\t\t\terrors.Errorf(\"Package %q cannot be fetched from binary cache store\", p.Raw)\n\t}\n\n\tentry, err := p.lockfile.Resolve(p.LockfileKey())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tsysInfo := entry.Systems[nix.System()]\n\tfor _, out := range sysInfo.Outputs {\n\t\tif out.Name == output {\n\t\t\treturn out.Path, nil\n\t\t}\n\t}\n\treturn \"\", errors.Errorf(\"Output %q not found for package %q\", output, p.Raw)\n}\n\nfunc (p *Package) HasAllowInsecure() bool {\n\treturn len(p.AllowInsecure) > 0\n}\n\n// StoreName returns the last section of the store path. Example:\n// /nix/store/abc123-foo-1.0.0 -> foo-1.0.0\n// Warning, this is probably slowish. If you need to call this multiple times,\n// consider caching the result.\nfunc (p *Package) StoreName() (string, error) {\n\tu, err := p.urlForInstall()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tname, err := nix.EvalPackageName(u)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn name, nil\n}\n\nfunc (p *Package) EnsureUninstallableIsInLockfile() error {\n\t// TODO savil: Should !p.isInstallable() be the opposite i.e. p.IsInstallable()?\n\t// TODO savil: Do we need the IsDevboxPackage check here?\n\tif !p.IsInstallable() || !p.IsDevboxPackage {\n\t\treturn nil\n\t}\n\t_, err := p.lockfile.Resolve(p.LockfileKey())\n\treturn err\n}\n\nfunc (p *Package) IsRunX() bool {\n\treturn pkgtype.IsRunX(p.Raw)\n}\n\nfunc (p *Package) IsNix() bool {\n\treturn IsNix(p, 0)\n}\n\nfunc (p *Package) RunXPath() string {\n\treturn strings.TrimPrefix(p.Raw, pkgtype.RunXPrefix)\n}\n\nfunc (p *Package) String() string {\n\tif p.installable.AttrPath != \"\" {\n\t\treturn p.installable.AttrPath\n\t}\n\treturn p.Raw\n}\n\nfunc (p *Package) LockfileKey() string {\n\t// Use p.Raw instead of p.installable.Ref.String() because that will have\n\t// absolute paths. TODO: We may want to change SetInstallable to avoid making\n\t// flake ref absolute.\n\treturn p.Raw\n}\n\nfunc IsNix(p *Package, _ int) bool {\n\treturn !p.IsRunX()\n}\n\nfunc IsRunX(p *Package, _ int) bool {\n\treturn p.IsRunX()\n}\n\nfunc (p *Package) DocsURL() string {\n\tif p.IsRunX() {\n\t\tpath, _, _ := strings.Cut(p.RunXPath(), \"@\")\n\t\treturn fmt.Sprintf(\"https://www.github.com/%s\", path)\n\t}\n\tif p.IsDevboxPackage {\n\t\treturn fmt.Sprintf(\"https://www.nixhub.io/packages/%s\", p.CanonicalName())\n\t}\n\treturn \"\"\n}\n\n// GetOutputNames returns the names of the nix package outputs. Outputs can be\n// specified in devbox.json package fields or as part of the flake reference.\nfunc (p *Package) GetOutputNames() ([]string, error) {\n\tif p.IsRunX() {\n\t\treturn []string{}, nil\n\t}\n\n\treturn p.outputs.GetNames(p)\n}\n\n// GetOutputsWithCache return outputs and their cache URIs if the package is in the binary cache.\n// n+1 WARNING: This will make an http request if FillNarInfoCache is not called before.\n// Grep note: this is used in flake template\nfunc (p *Package) GetOutputsWithCache() ([]Output, error) {\n\tdefer debug.FunctionTimer().End()\n\n\tnames, err := p.GetOutputNames()\n\tif err != nil || len(names) == 0 {\n\t\treturn nil, err\n\t}\n\n\tisEligibleForBinaryCache, err := p.isEligibleForBinaryCache()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toutputs := []Output{}\n\tfor _, name := range names {\n\t\toutput := Output{Name: name}\n\t\tif isEligibleForBinaryCache {\n\t\t\tstatus, err := p.fetchNarInfoStatusOnce(name)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\toutput.CacheURI = status[name]\n\t\t}\n\t\toutputs = append(outputs, output)\n\t}\n\treturn outputs, nil\n}\n\n// GetResolvedStorePaths returns the store paths that are resolved (in lockfile)\nfunc (p *Package) GetResolvedStorePaths() ([]string, error) {\n\tnames, err := p.GetOutputNames()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstorePaths := []string{}\n\tfor _, name := range names {\n\t\toutputs, err := p.outputsForOutputName(name)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, output := range outputs {\n\t\t\tstorePaths = append(storePaths, output.Path)\n\t\t}\n\t}\n\treturn storePaths, nil\n}\n\nconst MissingStorePathsWarning = \"Outputs for %s are not in lockfile. To fix this issue and improve performance, please run \" +\n\t\"`devbox install --tidy-lockfile`\\n\"\n\nfunc (p *Package) GetStorePaths(ctx context.Context, w io.Writer) ([]string, error) {\n\tstorePathsForPackage, err := p.GetResolvedStorePaths()\n\tif err != nil || len(storePathsForPackage) > 0 {\n\t\treturn storePathsForPackage, err\n\t}\n\n\tif featureflag.TidyWarning.Enabled() && p.IsDevboxPackage {\n\t\t// No fast path, we need to query nix.\n\t\tux.FHidableWarning(ctx, w, MissingStorePathsWarning, p.Raw)\n\t}\n\n\tinstallables, err := p.Installables()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, installable := range installables {\n\t\tstorePathsForInstallable, err := nix.StorePathsFromInstallable(\n\t\t\tctx, installable, p.HasAllowInsecure())\n\t\tif err != nil {\n\t\t\treturn nil, packageInstallErrorHandler(err, p, installable)\n\t\t}\n\t\tstorePathsForPackage = append(storePathsForPackage, storePathsForInstallable...)\n\t}\n\treturn storePathsForPackage, nil\n}\n\n// packageInstallErrorHandler checks for two kinds of errors to print custom messages for so that Devbox users\n// can work around them:\n// 1. Packages that cannot be installed on the current system, but may be installable on other systems.packageInstallErrorHandler\n// 2. Packages marked insecure by nix\nfunc packageInstallErrorHandler(err error, pkg *Package, installableOrEmpty string) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\t// Check if the user is installing a package that cannot be installed on their platform.\n\t// For example, glibcLocales on MacOS will give the following error:\n\t// flake output attribute 'legacyPackages.x86_64-darwin.glibcLocales' is not a derivation or path\n\t// This is because glibcLocales is only available on Linux.\n\t// The user should try `devbox add` again with `--exclude-platform`\n\terrMessage := strings.TrimSpace(err.Error())\n\n\t// Sample error from `devbox add glibcLocales` on a mac:\n\t// error: flake output attribute 'legacyPackages.x86_64-darwin.glibcLocales' is not a derivation or path\n\tmaybePackageSystemCompatibilityErrorType1 := strings.Contains(errMessage, \"error: flake output attribute\") &&\n\t\tstrings.Contains(errMessage, \"is not a derivation or path\")\n\t// Sample error from `devbox add sublime4` on a mac:\n\t// error: Package ‘sublimetext4-4169’ in /nix/store/nlbjx0mp83p2qzf1rkmzbgvq1wxfir81-source/pkgs/applications/editors/sublime/4/common.nix:168 is not available on the requested hostPlatform:\n\t//     hostPlatform.config = \"x86_64-apple-darwin\"\n\t//     package.meta.platforms = [\n\t//       \"aarch64-linux\"\n\t//       \"x86_64-linux\"\n\t//    ]\n\tmaybePackageSystemCompatibilityErrorType2 := strings.Contains(errMessage, \"is not available on the requested hostPlatform\")\n\n\tif maybePackageSystemCompatibilityErrorType1 || maybePackageSystemCompatibilityErrorType2 {\n\t\tplatform := nix.System()\n\t\treturn usererr.WithUserMessage(\n\t\t\terr,\n\t\t\t\"package %s cannot be installed on your platform %s.\\n\"+\n\t\t\t\t\"If you know this package is incompatible with %[2]s, then \"+\n\t\t\t\t\"you could run `devbox add %[1]s --exclude-platform %[2]s` and re-try.\\n\"+\n\t\t\t\t\"If you think this package should be compatible with %[2]s, then \"+\n\t\t\t\t\"it's possible this particular version is not available yet from the nix registry. \"+\n\t\t\t\t\"You could try `devbox add` with a different version for this package.\\n\\n\"+\n\t\t\t\t\"Underlying Error from nix is:\",\n\t\t\tpkg.Versioned(),\n\t\t\tplatform,\n\t\t)\n\t}\n\n\tif isInsecureErr, userErr := nix.IsExitErrorInsecurePackage(err, pkg.Versioned(), installableOrEmpty); isInsecureErr {\n\t\treturn userErr\n\t}\n\n\treturn usererr.WithUserMessage(err, \"error installing package %s\", pkg.Raw)\n}\n\nfunc (p *Package) ResolvedVersion() (string, error) {\n\tif err := p.resolve(); err != nil {\n\t\treturn \"\", err\n\t}\n\tlockPackage := p.lockfile.Get(p.Raw)\n\t// Flake packages don't have any values in the lockfile\n\tif lockPackage == nil {\n\t\treturn \"\", nil\n\t}\n\treturn p.lockfile.Get(p.Raw).Version, nil\n}\n"
  },
  {
    "path": "internal/devpkg/package_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage devpkg\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\nconst nixCommitHash = \"hsdafkhsdafhas\"\n\ntype inputTestCase struct {\n\tpkg                string\n\tisFlake            bool\n\tname               string\n\turlWithoutFragment string\n\turlForInput        string\n}\n\nfunc TestInput(t *testing.T) {\n\tprojectDir := \"/tmp/my-project\"\n\tcases := []inputTestCase{\n\t\t{\n\t\t\tpkg:                \"path:path/to/my-flake#my-package\",\n\t\t\tisFlake:            true,\n\t\t\tname:               \"my-flake-9a897d\",\n\t\t\turlWithoutFragment: \"path:\" + filepath.Join(projectDir, \"path/to/my-flake\"),\n\t\t\turlForInput:        \"path:\" + filepath.Join(projectDir, \"path/to/my-flake\"),\n\t\t},\n\t\t{\n\t\t\tpkg:                \"path:.#my-package\",\n\t\t\tisFlake:            true,\n\t\t\tname:               \"my-project-45b022\",\n\t\t\turlWithoutFragment: \"path:\" + projectDir,\n\t\t\turlForInput:        \"path:\" + projectDir,\n\t\t},\n\t\t{\n\t\t\tpkg:                \"path:/tmp/my-project/path/to/my-flake#my-package\",\n\t\t\tisFlake:            true,\n\t\t\tname:               \"my-flake-9a897d\",\n\t\t\turlWithoutFragment: \"path:\" + filepath.Join(projectDir, \"path/to/my-flake\"),\n\t\t\turlForInput:        \"path:\" + filepath.Join(projectDir, \"path/to/my-flake\"),\n\t\t},\n\t\t{\n\t\t\tpkg:                \"path:/tmp/my-project/path/to/my-flake\",\n\t\t\tisFlake:            true,\n\t\t\tname:               \"my-flake-7d03be\",\n\t\t\turlWithoutFragment: \"path:\" + filepath.Join(projectDir, \"path/to/my-flake\"),\n\t\t\turlForInput:        \"path:\" + filepath.Join(projectDir, \"path/to/my-flake\"),\n\t\t},\n\t\t{\n\t\t\tpkg:                \"hello\",\n\t\t\tisFlake:            false,\n\t\t\tname:               \"nixpkgs-hsdafk\",\n\t\t\turlWithoutFragment: \"hello\",\n\t\t\turlForInput:        fmt.Sprintf(\"github:NixOS/nixpkgs/%s\", nixCommitHash),\n\t\t},\n\t\t{\n\t\t\tpkg:                \"hello@123\",\n\t\t\tisFlake:            false,\n\t\t\tname:               \"nixpkgs-hsdafk\",\n\t\t\turlWithoutFragment: \"hello@123\",\n\t\t\turlForInput:        fmt.Sprintf(\"github:NixOS/nixpkgs/%s\", nixCommitHash),\n\t\t},\n\t\t{\n\t\t\tpkg:                \"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello\",\n\t\t\tisFlake:            true,\n\t\t\tname:               \"gh-nixos-nixpkgs-5233fd2ba76a3accb5aaa999c00509a11fd0793c\",\n\t\t\turlWithoutFragment: \"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c\",\n\t\t\turlForInput:        \"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c\",\n\t\t},\n\t\t{\n\t\t\tpkg:                \"github:F1bonacc1/process-compose\",\n\t\t\tisFlake:            true,\n\t\t\tname:               \"gh-F1bonacc1-process-compose\",\n\t\t\turlWithoutFragment: \"github:F1bonacc1/process-compose\",\n\t\t\turlForInput:        \"github:F1bonacc1/process-compose\",\n\t\t},\n\t}\n\n\tfor _, testCase := range cases {\n\t\ti := testInputFromString(testCase.pkg, projectDir)\n\t\tif name := i.FlakeInputName(); testCase.name != name {\n\t\t\tt.Errorf(\"Name() = %v, want %v\", name, testCase.name)\n\t\t}\n\t\tif urlForInput := i.URLForFlakeInput(); testCase.urlForInput != urlForInput {\n\t\t\tt.Errorf(\"URLForFlakeInput() = %v, want %v\", urlForInput, testCase.urlForInput)\n\t\t}\n\t}\n}\n\ntype testInput struct {\n\t*Package\n}\n\ntype lockfile struct {\n\tprojectDir string\n}\n\nfunc (l *lockfile) ProjectDir() string {\n\treturn l.projectDir\n}\n\nfunc (l *lockfile) Stdenv() flake.Ref {\n\treturn flake.Ref{\n\t\tType:  flake.TypeGitHub,\n\t\tOwner: \"NixOS\",\n\t\tRepo:  \"nixpkgs\",\n\t\tRev:   nixCommitHash,\n\t}\n}\n\nfunc (l *lockfile) Get(pkg string) *lock.Package {\n\treturn nil\n}\n\nfunc (l *lockfile) Resolve(pkg string) (*lock.Package, error) {\n\tswitch {\n\tcase strings.Contains(pkg, \"path:\"):\n\t\treturn &lock.Package{Resolved: pkg}, nil\n\tcase strings.Contains(pkg, \"github:\"):\n\t\treturn &lock.Package{Resolved: pkg}, nil\n\tdefault:\n\t\treturn &lock.Package{\n\t\t\tResolved: flake.Installable{\n\t\t\t\tRef:      l.Stdenv(),\n\t\t\t\tAttrPath: pkg,\n\t\t\t}.String(),\n\t\t}, nil\n\t}\n}\n\nfunc testInputFromString(s, projectDir string) *testInput {\n\treturn lo.ToPtr(testInput{Package: PackageFromStringWithDefaults(s, &lockfile{projectDir})})\n}\n\nfunc TestHashFromNixPkgsURL(t *testing.T) {\n\ttests := []struct {\n\t\turl      string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\turl:      \"github:NixOS/nixpkgs/12345\",\n\t\t\texpected: \"12345\",\n\t\t},\n\t\t{\n\t\t\turl:      \"github:NixOS/nixpkgs/abcdef#hello\",\n\t\t\texpected: \"abcdef\",\n\t\t},\n\t\t{\n\t\t\turl:      \"github:NixOS/nixpkgs/\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\turl:      \"github:NixOS/nixpkgs\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\turl:      \"github:NixOS/other-repo/12345\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\turl:      \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tresult := nix.HashFromNixPkgsURL(test.url)\n\t\tif result != test.expected {\n\t\t\tt.Errorf(\n\t\t\t\t\"Expected hash '%s' for URL '%s', but got '%s'\",\n\t\t\t\ttest.expected,\n\t\t\t\ttest.url,\n\t\t\t\tresult,\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestCanonicalName(t *testing.T) {\n\ttests := []struct {\n\t\tpkgName      string\n\t\texpectedName string\n\t}{\n\t\t{\"go\", \"go\"},\n\t\t{\"go@latest\", \"go\"},\n\t\t{\"go@1.21\", \"go\"},\n\t\t{\"runx:golangci/golangci-lint@latest\", \"runx:golangci/golangci-lint\"},\n\t\t{\"runx:golangci/golangci-lint@v0.0.2\", \"runx:golangci/golangci-lint\"},\n\t\t{\"runx:golangci/golangci-lint\", \"runx:golangci/golangci-lint\"},\n\t\t{\"github:NixOS/nixpkgs/12345\", \"\"},\n\t\t{\"path:/to/my/file\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.pkgName, func(t *testing.T) {\n\t\t\tpkg := PackageFromStringWithDefaults(tt.pkgName, &lockfile{})\n\t\t\tgot := pkg.CanonicalName()\n\t\t\tif got != tt.expectedName {\n\t\t\t\tt.Errorf(\"Expected canonical name %q, but got %q\", tt.expectedName, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/devpkg/pkgtype/flake.go",
    "content": "package pkgtype\n\nimport (\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\nfunc IsFlake(s string) bool {\n\tif IsRunX(s) {\n\t\treturn false\n\t}\n\tparsed, err := flake.ParseInstallable(s)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif IsAmbiguous(s, parsed) {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// IsAmbiguous returns true if a package string could be a Devbox package or\n// a flake installable. For example, \"nixpkgs\" is both a Devbox package and a\n// flake.\nfunc IsAmbiguous(raw string, parsed flake.Installable) bool {\n\t// Devbox package strings never have a #attr_path in them.\n\tif parsed.AttrPath != \"\" {\n\t\treturn false\n\t}\n\n\t// Indirect installables must have a \"flake:\" scheme to disambiguate\n\t// them from legacy (unversioned) devbox package strings.\n\tif parsed.Ref.Type == flake.TypeIndirect {\n\t\treturn !strings.HasPrefix(raw, \"flake:\")\n\t}\n\n\t// Path installables must have a \"path:\" scheme, start with \"/\" or start\n\t// with \"./\" to disambiguate them from devbox package strings.\n\tif parsed.Ref.Type == flake.TypePath {\n\t\tif raw[0] == '.' || raw[0] == '/' {\n\t\t\treturn false\n\t\t}\n\t\tif strings.HasPrefix(raw, \"path:\") {\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t}\n\n\t// All other flakeref types must have a scheme, so we know those can't\n\t// be devbox package strings.\n\treturn false\n}\n"
  },
  {
    "path": "internal/devpkg/pkgtype/runx.go",
    "content": "package pkgtype\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"strings\"\n\n\t\"go.jetify.com/pkg/runx/impl/registry\"\n\t\"go.jetify.com/pkg/runx/impl/runx\"\n)\n\nconst (\n\tRunXScheme            = \"runx\"\n\tRunXPrefix            = RunXScheme + \":\"\n\tgithubAPITokenVarName = \"GITHUB_TOKEN\"\n\t// Keep for backwards compatibility\n\toldGithubAPITokenVarName = \"DEVBOX_GITHUB_API_TOKEN\"\n)\n\nvar cachedRegistry *registry.Registry\n\nfunc IsRunX(s string) bool {\n\treturn strings.HasPrefix(s, RunXPrefix)\n}\n\nfunc RunXClient() *runx.RunX {\n\treturn &runx.RunX{\n\t\tGithubAPIToken: getGithubToken(),\n\t}\n}\n\nfunc RunXRegistry(ctx context.Context) (*registry.Registry, error) {\n\tif cachedRegistry == nil {\n\t\tvar err error\n\t\tcachedRegistry, err = registry.NewLocalRegistry(ctx, getGithubToken())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn cachedRegistry, nil\n}\n\nfunc getGithubToken() string {\n\ttoken := os.Getenv(githubAPITokenVarName)\n\tif token == \"\" {\n\t\ttoken = os.Getenv(oldGithubAPITokenVarName)\n\t}\n\treturn token\n}\n"
  },
  {
    "path": "internal/devpkg/validation.go",
    "content": "package devpkg\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/nix\"\n)\n\nfunc (p *Package) ValidateExists(ctx context.Context) (bool, error) {\n\tif p.IsRunX() {\n\t\t_, err := p.lockfile.Resolve(p.Raw)\n\t\treturn err == nil, err\n\t}\n\tif p.isVersioned() && p.version() == \"\" {\n\t\treturn false, usererr.New(\"No version specified for %q.\", p.Raw)\n\t}\n\n\tinCache, err := p.IsInBinaryCache()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif inCache {\n\t\treturn true, nil\n\t}\n\n\tinfo, err := p.NormalizedPackageAttributePath()\n\treturn info != \"\", err\n}\n\nfunc (p *Package) ValidateInstallsOnSystem() (bool, error) {\n\tu, err := p.urlForInstall()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tinfo, _ := nix.Search(u)\n\tif len(info) == 0 {\n\t\treturn false, nil\n\t}\n\tif out, err := nix.Eval(u); err != nil &&\n\t\tstrings.Contains(string(out), \"is not available on the requested hostPlatform\") {\n\t\treturn false, nil\n\t}\n\t// There's other stuff that may cause this evaluation to fail, but we don't\n\t// want to handle all of them here. (e.g. unfree packages)\n\treturn true, nil\n}\n"
  },
  {
    "path": "internal/envir/env.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage envir\n\nconst (\n\tDevboxCache   = \"DEVBOX_CACHE\"\n\tDevboxGateway = \"DEVBOX_GATEWAY\"\n\t// DevboxLatestVersion is the latest version available of the devbox CLI binary.\n\t// NOTE: it should NOT start with v (like 0.4.8)\n\tDevboxLatestVersion  = \"DEVBOX_LATEST_VERSION\"\n\tDevboxRegion         = \"DEVBOX_REGION\"\n\tDevboxSearchHost     = \"DEVBOX_SEARCH_HOST\"\n\tDevboxShellEnabled   = \"DEVBOX_SHELL_ENABLED\"\n\tDevboxShellStartTime = \"DEVBOX_SHELL_START_TIME\"\n\tDevboxVM             = \"DEVBOX_VM\"\n\n\tLauncherVersion = \"LAUNCHER_VERSION\"\n\tLauncherPath    = \"LAUNCHER_PATH\"\n\n\tGitHubUsername = \"GITHUB_USER_NAME\"\n\tSSHTTY         = \"SSH_TTY\"\n\n\tXDGDataHome   = \"XDG_DATA_HOME\"\n\tXDGConfigHome = \"XDG_CONFIG_HOME\"\n\tXDGCacheHome  = \"XDG_CACHE_HOME\"\n\tXDGStateHome  = \"XDG_STATE_HOME\"\n)\n\n// system\nconst (\n\tEnv   = \"ENV\"\n\tHome  = \"HOME\"\n\tPath  = \"PATH\"\n\tShell = \"SHELL\"\n\tUser  = \"USER\"\n)\n"
  },
  {
    "path": "internal/envir/util.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage envir\n\nimport (\n\t\"os\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc IsDevboxCloud() bool {\n\treturn os.Getenv(DevboxRegion) != \"\"\n}\n\nfunc IsDevboxShellEnabled() bool {\n\tinDevboxShell, _ := strconv.ParseBool(os.Getenv(DevboxShellEnabled))\n\treturn inDevboxShell\n}\n\nfunc DoNotTrack() bool {\n\t// https://consoledonottrack.com/\n\tdoNotTrack, _ := strconv.ParseBool(os.Getenv(\"DO_NOT_TRACK\"))\n\treturn doNotTrack\n}\n\nfunc IsInBrowser() bool { // TODO: a better name\n\tinBrowser, _ := strconv.ParseBool(os.Getenv(\"START_WEB_TERMINAL\"))\n\treturn inBrowser\n}\n\nfunc IsCI() bool {\n\tci, err := strconv.ParseBool(os.Getenv(\"CI\"))\n\treturn ci && err == nil\n}\n\n// GetValueOrDefault gets the value of an environment variable.\n// If it's empty, it will return the given default value instead.\nfunc GetValueOrDefault(key, def string) string {\n\tval := os.Getenv(key)\n\tif val == \"\" {\n\t\tval = def\n\t}\n\n\treturn val\n}\n\n// MapToPairs creates a slice of environment variable \"key=value\" pairs from a\n// map.\nfunc MapToPairs(m map[string]string) []string {\n\tpairs := make([]string, len(m))\n\ti := 0\n\tfor k, v := range m {\n\t\tpairs[i] = k + \"=\" + v\n\t\ti++\n\t}\n\tslices.Sort(pairs) // for reproducibility\n\treturn pairs\n}\n\n// PairsToMap creates a map from a slice of \"key=value\" environment variable\n// pairs. Note that maps are not ordered, which can affect the final variable\n// values when pairs contains duplicate keys.\nfunc PairsToMap(pairs []string) map[string]string {\n\tvars := make(map[string]string, len(pairs))\n\tfor _, p := range pairs {\n\t\tk, v, ok := strings.Cut(p, \"=\")\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tvars[k] = v\n\t}\n\treturn vars\n}\n"
  },
  {
    "path": "internal/fileutil/dir.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage fileutil\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.jetify.com/devbox/internal/cmdutil\"\n)\n\nfunc CopyAll(src, dst string) error {\n\tentries, err := os.ReadDir(src)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tfor _, entry := range entries {\n\t\tcmd := cmdutil.CommandTTY(\"cp\", \"-rf\", filepath.Join(src, entry.Name()), dst)\n\t\tif err := cmd.Run(); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ClearDir(dir string) error {\n\t// if the dir doesn't exist, use default filemode 0755 to create it\n\t// if the dir exists, use its own filemode to re-create it\n\tvar mode os.FileMode\n\tf, err := os.Stat(dir)\n\tif err == nil {\n\t\tmode = f.Mode()\n\t} else if errors.Is(err, fs.ErrNotExist) {\n\t\tmode = 0o755\n\t} else {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tif err := os.RemoveAll(dir); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\treturn errors.WithStack(os.MkdirAll(dir, mode))\n}\n\nfunc CreateDevboxTempDir() (string, error) {\n\ttmpDir, err := os.MkdirTemp(\"\", \"devbox\")\n\treturn tmpDir, errors.WithStack(err)\n}\n"
  },
  {
    "path": "internal/fileutil/fileutil.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage fileutil\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// TODO: publish as it's own shared package that other binaries can use.\n\n// IsDir returns true if the path exists *and* it is pointing to a directory.\n//\n// This function will traverse symbolic links to query information about the\n// destination file.\n//\n// This is a convenience function that coerces errors to false. If it cannot\n// read the path for any reason (including a permission error, or a broken\n// symbolic link) it returns false.\nfunc IsDir(path string) bool {\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn info.IsDir()\n}\n\n// IsFile returns true if the path exists *and* it is pointing to a regular file.\n//\n// This function will traverse symbolic links to query information about the\n// destination file.\n//\n// This is a convenience function that coerces errors to false. If it cannot\n// read the path for any reason (including a permission error, or a broken\n// symbolic link) it returns false.\nfunc IsFile(path string) bool {\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn info.Mode().IsRegular()\n}\n\nfunc Exists(path string) bool {\n\t_, err := os.Stat(path)\n\treturn err == nil\n}\n\nfunc IsDirEmpty(path string) (bool, error) {\n\tentries, err := os.ReadDir(path)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn len(entries) == 0, nil\n}\n\n// FileContains checks if a given file at 'path' contains the 'substring'\nfunc FileContains(path, substring string) (bool, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn strings.Contains(string(data), substring), nil\n}\n\nfunc EnsureDirExists(path string, perm fs.FileMode, chmod bool) error {\n\tif err := os.MkdirAll(path, perm); err != nil && !errors.Is(err, fs.ErrExist) {\n\t\treturn errors.WithStack(err)\n\t}\n\tif chmod {\n\t\tif err := os.Chmod(path, perm); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc EnsureAbsolutePaths(paths []string) ([]string, error) {\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tabsPaths := make([]string, len(paths))\n\tfor i, path := range paths {\n\t\tif filepath.IsAbs(path) {\n\t\t\tabsPaths[i] = path\n\t\t} else {\n\t\t\tabsPaths[i] = filepath.Join(wd, path)\n\t\t}\n\t}\n\treturn absPaths, nil\n}\n"
  },
  {
    "path": "internal/fileutil/fileutil_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage fileutil\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIsDirEmpty(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tsetup    func(string) error\n\t\texpected bool\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"empty directory\",\n\t\t\tsetup: func(dir string) error {\n\t\t\t\treturn nil // Directory is already empty\n\t\t\t},\n\t\t\texpected: true,\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname: \"directory with files\",\n\t\t\tsetup: func(dir string) error {\n\t\t\t\tfile := filepath.Join(dir, \"test.txt\")\n\t\t\t\treturn os.WriteFile(file, []byte(\"test content\"), 0o644)\n\t\t\t},\n\t\t\texpected: false,\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname: \"directory with subdirectories\",\n\t\t\tsetup: func(dir string) error {\n\t\t\t\tsubdir := filepath.Join(dir, \"subdir\")\n\t\t\t\treturn os.MkdirAll(subdir, 0o755)\n\t\t\t},\n\t\t\texpected: false,\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname: \"directory with hidden files\",\n\t\t\tsetup: func(dir string) error {\n\t\t\t\tfile := filepath.Join(dir, \".hidden\")\n\t\t\t\treturn os.WriteFile(file, []byte(\"hidden content\"), 0o644)\n\t\t\t},\n\t\t\texpected: false,\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname: \"non-existent directory\",\n\t\t\tsetup: func(dir string) error {\n\t\t\t\treturn os.RemoveAll(dir)\n\t\t\t},\n\t\t\texpected: false,\n\t\t\twantErr:  true,\n\t\t},\n\t}\n\n\tfor _, curTest := range tests {\n\t\tt.Run(curTest.name, func(t *testing.T) {\n\t\t\t// Create temporary directory for test\n\t\t\ttempDir := t.TempDir()\n\n\t\t\t// Setup test case\n\t\t\tif curTest.setup != nil {\n\t\t\t\terr := curTest.setup(tempDir)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\t// Run the function\n\t\t\tisEmpty, err := IsDirEmpty(tempDir)\n\n\t\t\t// Check results\n\t\t\tif curTest.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, curTest.expected, isEmpty)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/fileutil/untar.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage fileutil\n\n// TODO: publish as it's own shared package that other binaries can use.\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/mholt/archives\"\n)\n\nfunc Untar(archive io.Reader, destPath string) error {\n\t_, err := os.Stat(destPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// We assume `tar.gz` since that's the only format we need for now.\n\tformat := archives.CompressedArchive{\n\t\tCompression: archives.Gz{},\n\t\tArchival:    archives.Tar{},\n\t}\n\n\t// The handler will be called for each entry in the archive.\n\thandler := func(ctx context.Context, fromFile archives.FileInfo) error {\n\t\t// TODO: consider whether the path provided in the archive is a valid\n\t\t// relative path to begin with.\n\t\trel := filepath.Clean(fromFile.NameInArchive)\n\t\tabs := filepath.Join(destPath, rel)\n\n\t\tmode := fromFile.Mode()\n\n\t\t// TODO: handle symlink case\n\t\tswitch {\n\t\tcase mode.IsRegular():\n\t\t\treturn untarFile(fromFile, abs)\n\t\tcase mode.IsDir():\n\t\t\treturn os.MkdirAll(abs, 0o755)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"archive contained entry %s of unsupported file type %v\", fromFile.Name(), mode)\n\t\t}\n\t}\n\n\t// Start extraction using our handler.\n\treturn format.Extract(context.Background(), archive, handler)\n}\n\nfunc untarFile(fromFile archives.FileInfo, abs string) error {\n\tfromReader, err := fromFile.Open()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tcloseErr := fromReader.Close()\n\t\tif closeErr != nil {\n\t\t\tlog.Fatal(closeErr)\n\t\t}\n\t}()\n\n\t// We assume the directory exists because if the archive is constructed correctly\n\t// there should have been a directory entry already. If we want to be safer,\n\t// we could ensure the path exists before opening the file, although we should then\n\t// cache which directories we've already created for performance reasons.\n\ttoWriter, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fromFile.Mode().Perm())\n\tif err != nil {\n\t\treturn err\n\t}\n\tnumBytes, err := io.Copy(toWriter, fromReader)\n\tif closeErr := toWriter.Close(); closeErr != nil && err == nil {\n\t\terr = closeErr\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing to %s: %v\", abs, err)\n\t}\n\tif numBytes != fromFile.Size() {\n\t\treturn fmt.Errorf(\"only wrote %d bytes to %s; expected %d\", numBytes, abs, fromFile.Size())\n\t}\n\tmodTime := fromFile.ModTime()\n\tif !modTime.IsZero() {\n\t\tif err := os.Chtimes(abs, modTime, modTime); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/goutil/goutil.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage goutil\n\nfunc PickByKeysSorted[K comparable, V any](in map[K]V, keys []K) []V {\n\tout := make([]V, len(keys))\n\tfor i, key := range keys {\n\t\tout[i] = in[key]\n\t}\n\treturn out\n}\n\nfunc GetDefaulted[T any](in []T, index int) T {\n\tvar t T\n\tif index >= len(in) {\n\t\treturn t\n\t}\n\treturn in[index]\n}\n"
  },
  {
    "path": "internal/goutil/sync.go",
    "content": "package goutil\n\nimport (\n\t\"context\"\n\t\"sync\"\n)\n\ntype onceValue[T any] struct {\n\tonce   sync.Once\n\tfn     func(context.Context) T\n\tresult T\n}\n\nfunc OnceValueWithContext[T any](fn func(context.Context) T) *onceValue[T] {\n\treturn &onceValue[T]{fn: fn}\n}\n\nfunc (o *onceValue[T]) Do(ctx context.Context) T {\n\to.once.Do(func() {\n\t\to.result = o.fn(ctx)\n\t})\n\treturn o.result\n}\n\ntype onceValues[T any] struct {\n\tonce   sync.Once\n\tfn     func(context.Context) (T, error)\n\tresult T\n\terr    error\n}\n\nfunc OnceValuesWithContext[T any](fn func(context.Context) (T, error)) *onceValues[T] {\n\treturn &onceValues[T]{fn: fn}\n}\n\nfunc (o *onceValues[T]) Do(ctx context.Context) (T, error) {\n\to.once.Do(func() {\n\t\to.result, o.err = o.fn(ctx)\n\t})\n\treturn o.result, o.err\n}\n"
  },
  {
    "path": "internal/lock/interfaces.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage lock\n\nimport \"go.jetify.com/devbox/nix/flake\"\n\ntype devboxProject interface {\n\tConfigHash() (string, error)\n\tStdenv() flake.Ref\n\tAllPackageNamesIncludingRemovedTriggerPackages() []string\n\tProjectDir() string\n}\n\ntype Locker interface {\n\tGet(string) *Package\n\tStdenv() flake.Ref\n\tProjectDir() string\n\tResolve(string) (*Package, error)\n}\n"
  },
  {
    "path": "internal/lock/lockfile.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage lock\n\nimport (\n\t\"context\"\n\t\"io/fs\"\n\t\"maps\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/cachehash\"\n\t\"go.jetify.com/devbox/internal/devpkg/pkgtype\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/searcher\"\n\t\"go.jetify.com/devbox/nix/flake\"\n\t\"go.jetify.com/pkg/runx/impl/types\"\n\n\t\"go.jetify.com/devbox/internal/cuecfg\"\n)\n\nconst lockFileVersion = \"1\"\n\n// Lightly inspired by package-lock.json\ntype File struct {\n\tdevboxProject `json:\"-\"`\n\n\tLockFileVersion string `json:\"lockfile_version\"`\n\n\t// Packages is keyed by \"canonicalName@version\"\n\tPackages map[string]*Package `json:\"packages\"`\n}\n\nfunc GetFile(project devboxProject) (*File, error) {\n\tlockFile := &File{\n\t\tdevboxProject: project,\n\n\t\tLockFileVersion: lockFileVersion,\n\t\tPackages:        map[string]*Package{},\n\t}\n\terr := cuecfg.ParseFile(lockFilePath(project.ProjectDir()), lockFile)\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn lockFile, nil\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// If the lockfile has legacy StorePath fields, we need to convert them to the new format\n\tensurePackagesHaveOutputs(lockFile.Packages)\n\n\treturn lockFile, nil\n}\n\nfunc (f *File) Add(pkgs ...string) error {\n\tfor _, p := range pkgs {\n\t\tif _, err := f.Resolve(p); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn f.Save()\n}\n\nfunc (f *File) Remove(pkgs ...string) error {\n\tfor _, p := range pkgs {\n\t\tdelete(f.Packages, p)\n\t}\n\treturn f.Save()\n}\n\n// Resolve updates the in memory copy for performance but does not write to disk\n// This avoids writing values that may need to be removed in case of error.\nfunc (f *File) Resolve(pkg string) (*Package, error) {\n\tentry, hasEntry := f.Packages[pkg]\n\tif hasEntry && entry.Resolved != \"\" {\n\t\treturn f.Packages[pkg], nil\n\t}\n\n\tlocked := &Package{}\n\t_, _, versioned := searcher.ParseVersionedPackage(pkg)\n\tif pkgtype.IsRunX(pkg) || versioned || pkgtype.IsFlake(pkg) {\n\t\tresolved, err := f.FetchResolvedPackage(pkg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif resolved != nil {\n\t\t\tlocked = resolved\n\t\t}\n\t} else if IsLegacyPackage(pkg) {\n\t\t// These are legacy packages without a version. Resolve to nixpkgs with\n\t\t// whatever hash is in the devbox.json\n\t\tlocked = &Package{\n\t\t\tResolved: flake.Installable{\n\t\t\t\tRef:      f.Stdenv(),\n\t\t\t\tAttrPath: pkg,\n\t\t\t}.String(),\n\t\t\tSource: nixpkgSource,\n\t\t}\n\t}\n\tf.Packages[pkg] = locked\n\n\treturn f.Packages[pkg], nil\n}\n\n// TODO:\n// Consider a design change to have the File struct match disk to make this system\n// easier to reason about, and have isDirty() compare the in-memory struct to the\n// on-disk struct.\n//\n// Proposal:\n// 1. Have an OutputsRaw field and a method called Outputs() to access it.\n// Outputs() will check if OutputsRaw is zero-value and fills it in from StorePath.\n// 2. Then, in Save(), we can check if OutputsRaw is zero and fill it in prior to writing\n// to disk.\nfunc (f *File) Save() error {\n\tisDirty, err := f.isDirty()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !isDirty {\n\t\treturn nil\n\t}\n\n\t// In SystemInfo, preserve legacy StorePath field and clear out modern Outputs before writing\n\t// Reason: We want to update `devbox.lock` file only upon a user action\n\t// such as `devbox update` or `devbox add` or `devbox remove`.\n\tfor pkgName, pkg := range f.Packages {\n\t\tfor sys, sysInfo := range pkg.Systems {\n\t\t\tif sysInfo.outputIsFromStorePath {\n\t\t\t\tf.Packages[pkgName].Systems[sys].Outputs = nil\n\t\t\t}\n\t\t}\n\t}\n\t// We set back the Outputs, if needed, after writing the file, so that future\n\t// users of the `lock.File` struct will have the correct data.\n\tdefer ensurePackagesHaveOutputs(f.Packages)\n\n\treturn cuecfg.WriteFile(lockFilePath(f.devboxProject.ProjectDir()), f)\n}\n\nfunc (f *File) UpdateStdenv() error {\n\tif err := nix.ClearFlakeCache(f.devboxProject.Stdenv()); err != nil {\n\t\treturn err\n\t}\n\tif err := f.Remove(f.devboxProject.Stdenv().String()); err != nil {\n\t\treturn err\n\t}\n\treturn f.Add(f.devboxProject.Stdenv().String())\n}\n\n// TODO: We should improve a few issues with this function:\n// * It shared the same name as Devbox.Stdenv() which is confusing.\n// * Since File implements DevboxProject, IDEs really struggle to accurately find call sites.\n// (side note, we should remove DevboxProject interface)\n// * This function forces a resolution of the stdenv flake which is slow and doesn't give us a\n// chance to \"prep\" the user for some waiting.\n// * Should we rename to Nixpkgs() ? Stdenv feels a bit ambiguous.\nfunc (f *File) Stdenv() flake.Ref {\n\tunlocked := f.devboxProject.Stdenv()\n\tpkg, err := f.Resolve(unlocked.String())\n\tif err != nil {\n\t\treturn unlocked\n\t}\n\tref, err := flake.ParseRef(pkg.Resolved)\n\tif err != nil {\n\t\treturn unlocked\n\t}\n\treturn ref\n}\n\nfunc (f *File) Get(pkg string) *Package {\n\tentry, hasEntry := f.Packages[pkg]\n\tif !hasEntry || entry.Resolved == \"\" {\n\t\treturn nil\n\t}\n\treturn entry\n}\n\nfunc (f *File) HasAllowInsecurePackages() bool {\n\tfor _, pkg := range f.Packages {\n\t\tif pkg.AllowInsecure {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// This probably belongs in input.go but can't add it there because it will\n// create a circular dependency. We could move Input into own package.\nfunc IsLegacyPackage(pkg string) bool {\n\t_, _, versioned := searcher.ParseVersionedPackage(pkg)\n\treturn !versioned &&\n\t\t!strings.Contains(pkg, \":\") &&\n\t\t// We don't support absolute paths without \"path:\" prefix, but adding here\n\t\t// just in case we ever do.\n\t\t// Landau note: I don't think we should support it, it's hard to read and a\n\t\t// bit ambiguous.\n\t\t!strings.HasPrefix(pkg, \"/\")\n}\n\n// Tidy ensures that the lockfile has the set of packages corresponding to the devbox.json config.\n// It gets rid of older packages that are no longer needed.\nfunc (f *File) Tidy() {\n\tkeep := f.devboxProject.AllPackageNamesIncludingRemovedTriggerPackages()\n\tkeep = append(keep, f.devboxProject.Stdenv().String())\n\tmaps.DeleteFunc(f.Packages, func(key string, pkg *Package) bool {\n\t\treturn !slices.Contains(keep, key)\n\t})\n}\n\n// IsUpToDateAndInstalled returns true if the lockfile is up to date and the\n// local hashes match, which generally indicates all packages are correctly\n// installed and print-dev-env has been computed and cached.\nfunc (f *File) IsUpToDateAndInstalled(isFish bool) (bool, error) {\n\tif dirty, err := f.isDirty(); err != nil {\n\t\treturn false, err\n\t} else if dirty {\n\t\treturn false, nil\n\t}\n\tconfigHash, err := f.devboxProject.ConfigHash()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn isStateUpToDate(UpdateStateHashFileArgs{\n\t\tProjectDir: f.devboxProject.ProjectDir(),\n\t\tConfigHash: configHash,\n\t\tIsFish:     isFish,\n\t})\n}\n\nfunc (f *File) SetOutputsForPackage(pkg string, outputs []Output) error {\n\tp, err := f.Resolve(pkg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif p.Systems == nil {\n\t\tp.Systems = map[string]*SystemInfo{}\n\t}\n\tif p.Systems[nix.System()] == nil {\n\t\tp.Systems[nix.System()] = &SystemInfo{}\n\t}\n\tp.Systems[nix.System()].Outputs = outputs\n\treturn f.Save()\n}\n\nfunc (f *File) isDirty() (bool, error) {\n\tcurrentHash, err := cachehash.JSON(f)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tfileSystemLockFile, err := GetFile(f.devboxProject)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tfilesystemHash, err := cachehash.JSON(fileSystemLockFile)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn currentHash != filesystemHash, nil\n}\n\nfunc lockFilePath(projectDir string) string {\n\treturn filepath.Join(projectDir, \"devbox.lock\")\n}\n\nfunc ResolveRunXPackage(ctx context.Context, pkg string) (types.PkgRef, error) {\n\tref, err := types.NewPkgRef(strings.TrimPrefix(pkg, pkgtype.RunXPrefix))\n\tif err != nil {\n\t\treturn types.PkgRef{}, err\n\t}\n\n\tregistry, err := pkgtype.RunXRegistry(ctx)\n\tif err != nil {\n\t\treturn types.PkgRef{}, err\n\t}\n\treturn registry.ResolveVersion(ref)\n}\n"
  },
  {
    "path": "internal/lock/package.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage lock\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n)\n\nconst (\n\tnixpkgSource       string = \"nixpkg\"\n\tdevboxSearchSource string = \"devbox-search\"\n)\n\ntype Package struct {\n\tAllowInsecure bool   `json:\"allow_insecure,omitempty\"`\n\tLastModified  string `json:\"last_modified,omitempty\"`\n\tPluginVersion string `json:\"plugin_version,omitempty\"`\n\tResolved      string `json:\"resolved,omitempty\"`\n\tSource        string `json:\"source,omitempty\"`\n\tVersion       string `json:\"version,omitempty\"`\n\t// Systems is keyed by the system name\n\tSystems map[string]*SystemInfo `json:\"systems,omitempty\"`\n\n\t// NOTE: if you add more fields, please update SyncLockfiles\n}\n\ntype SystemInfo struct {\n\tOutputs []Output `json:\"outputs,omitempty\"`\n\n\t// Legacy Format\n\tStorePath             string `json:\"store_path,omitempty\"`\n\toutputIsFromStorePath bool\n}\n\n// Output refers to a nix package output. This struct is derived from searcher.Output\ntype Output struct {\n\t// Name is the output's name. Nix appends the name to\n\t// the output's store path unless it's the default name\n\t// of \"out\". Output names can be anything, but\n\t// conventionally they follow the various \"make install\"\n\t// directories such as \"bin\", \"lib\", \"src\", \"man\", etc.\n\tName string `json:\"name,omitempty\"`\n\n\t// Path is the absolute store path (with the /nix/store/\n\t// prefix) of the output.\n\tPath string `json:\"path,omitempty\"`\n\n\t// Default indicates if Nix installs this output by\n\t// default.\n\tDefault bool `json:\"default,omitempty\"`\n}\n\nfunc (p *Package) GetSource() string {\n\tif p == nil {\n\t\treturn \"\"\n\t}\n\treturn p.Source\n}\n\nfunc (p *Package) IsAllowInsecure() bool {\n\tif p == nil {\n\t\treturn false\n\t}\n\treturn p.AllowInsecure\n}\n\n// Useful for debugging when we print the struct\nfunc (i *SystemInfo) String() string {\n\treturn fmt.Sprintf(\"%+v\", *i)\n}\n\nfunc (i *SystemInfo) Output(name string) (Output, error) {\n\tif i == nil {\n\t\treturn Output{}, nil\n\t}\n\n\tfor _, output := range i.Outputs {\n\t\tif output.Name == name {\n\t\t\treturn output, nil\n\t\t}\n\t}\n\n\treturn Output{}, fmt.Errorf(\"Output %s not found\", name)\n}\n\nfunc (i *SystemInfo) DefaultOutputs() []Output {\n\tif i == nil {\n\t\treturn nil\n\t}\n\n\tif len(i.Outputs) == 0 {\n\t\treturn nil\n\t}\n\n\tres := []Output{}\n\tfor _, output := range i.Outputs {\n\t\tif output.Default {\n\t\t\tres = append(res, output)\n\t\t}\n\t}\n\tif len(res) > 0 {\n\t\treturn res\n\t}\n\n\t// If no default outputs, return the first one\n\treturn []Output{i.Outputs[0]}\n}\n\nfunc (i *SystemInfo) Equals(other *SystemInfo) bool {\n\tif i == nil || other == nil {\n\t\treturn i == other\n\t}\n\n\treturn slices.Equal(i.Outputs, other.Outputs)\n}\n\n// If we have a StorePath and no Outputs, we need to convert to the new format.\n// Note: non-empty Outputs may have StorePath alongside it for backwards-compatibility\n// but we should ignore that legacy StorePath.\nfunc (i *SystemInfo) addOutputFromLegacyStorePath() {\n\tif i.StorePath != \"\" && len(i.Outputs) == 0 {\n\t\ti.Outputs = []Output{\n\t\t\t{\n\t\t\t\tDefault: true,\n\t\t\t\tName:    \"out\",\n\t\t\t\tPath:    i.StorePath,\n\t\t\t},\n\t\t}\n\t\ti.outputIsFromStorePath = true\n\t}\n}\n\n// ensurePackagesHaveOutputs is used for backwards-compatibility with the old\n// lockfile format where each SystemInfo had a StorePath but no Outputs.\nfunc ensurePackagesHaveOutputs(packages map[string]*Package) {\n\tfor _, pkg := range packages {\n\t\tfor sys, sysInfo := range pkg.Systems {\n\t\t\tsysInfo.addOutputFromLegacyStorePath()\n\t\t\tpkg.Systems[sys] = sysInfo\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/lock/resolve.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage lock\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/boxcli/featureflag\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/devpkg/pkgtype\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/redact\"\n\t\"go.jetify.com/devbox/internal/searcher\"\n\t\"go.jetify.com/devbox/nix/flake\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// FetchResolvedPackage fetches a resolution but does not write it to the lock\n// struct. This allows testing new versions of packages without writing to the\n// lock. This is useful to avoid changing nixpkgs commit hashes when version has\n// not changed. This can happen when doing `devbox update` and search has\n// a newer hash than the lock file but same version. In that case we don't want\n// to update because it would be slow and wasteful.\nfunc (f *File) FetchResolvedPackage(pkg string) (*Package, error) {\n\tif pkgtype.IsFlake(pkg) {\n\t\tinstallable, err := flake.ParseInstallable(pkg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"package %q: %v\", pkg, err)\n\t\t}\n\t\tinstallable.Ref, err = lockFlake(context.TODO(), installable.Ref)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &Package{\n\t\t\tResolved:     installable.String(),\n\t\t\tLastModified: time.Unix(installable.Ref.LastModified, 0).UTC().Format(time.RFC3339),\n\t\t}, nil\n\t}\n\n\tname, version, _ := searcher.ParseVersionedPackage(pkg)\n\tif version == \"\" {\n\t\treturn nil, usererr.New(\"No version specified for %q.\", name)\n\t}\n\n\tif pkgtype.IsRunX(pkg) {\n\t\tref, err := ResolveRunXPackage(context.TODO(), pkg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &Package{\n\t\t\tResolved: ref.String(),\n\t\t\tVersion:  ref.Version,\n\t\t}, nil\n\t}\n\tif featureflag.ResolveV2.Enabled() {\n\t\treturn resolveV2(context.TODO(), name, version)\n\t}\n\n\tpackageVersion, err := searcher.Client().Resolve(name, version)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(nix.ErrPackageNotFound, \"%s@%s\", name, version)\n\t}\n\n\tsysInfos, err := buildLockSystemInfos(packageVersion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpackageInfo, err := selectForSystem(packageVersion.Systems)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"no systems found for package %q\", name)\n\t}\n\n\tif len(packageInfo.AttrPaths) == 0 {\n\t\treturn nil, fmt.Errorf(\"no attr paths found for package %q\", name)\n\t}\n\n\treturn &Package{\n\t\tLastModified: time.Unix(int64(packageInfo.LastUpdated), 0).UTC().\n\t\t\tFormat(time.RFC3339),\n\t\tResolved: fmt.Sprintf(\n\t\t\t\"github:NixOS/nixpkgs/%s#%s\",\n\t\t\tpackageInfo.CommitHash,\n\t\t\tpackageInfo.AttrPaths[0],\n\t\t),\n\t\tVersion: packageInfo.Version,\n\t\tSource:  devboxSearchSource,\n\t\tSystems: sysInfos,\n\t}, nil\n}\n\nfunc resolveV2(ctx context.Context, name, version string) (*Package, error) {\n\tresolved, err := searcher.Client().ResolveV2(ctx, name, version)\n\tif errors.Is(err, searcher.ErrNotFound) {\n\t\treturn nil, redact.Errorf(\"%s@%s: %w\", name, version, nix.ErrPackageNotFound)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// /v2/resolve never returns a success with no systems.\n\tsysPkg, _ := selectForSystem(resolved.Systems)\n\tpkg := &Package{\n\t\tLastModified: sysPkg.LastUpdated.Format(time.RFC3339),\n\t\tResolved:     sysPkg.FlakeInstallable.String(),\n\t\tSource:       devboxSearchSource,\n\t\tVersion:      resolved.Version,\n\t\tSystems:      make(map[string]*SystemInfo, len(resolved.Systems)),\n\t}\n\tfor sys, info := range resolved.Systems {\n\t\tif len(info.Outputs) != 0 {\n\t\t\toutputs := make([]Output, len(info.Outputs))\n\t\t\tfor i, out := range info.Outputs {\n\t\t\t\toutputs[i] = Output{\n\t\t\t\t\tName:    out.Name,\n\t\t\t\t\tPath:    out.Path,\n\t\t\t\t\tDefault: out.Default,\n\t\t\t\t}\n\t\t\t}\n\t\t\tstorePath := \"\"\n\t\t\tif len(outputs) > 0 {\n\t\t\t\t// We pick the first output as the store path. Note, this is sub-optimal because\n\t\t\t\t// it may not include all the default outputs of the nix package, but is what older\n\t\t\t\t// Devbox used to do. And this code is for backwards-compatibility.\n\t\t\t\t//\n\t\t\t\t// Unlike /v2/resolve, the /v1/resolve endpoint does not return the store path. It\n\t\t\t\t// returns the commit hash and we run `nix store path-from-hash-part` to get the store path.\n\t\t\t\t// For some packages, this would return the store path of the first default output.\n\t\t\t\t//\n\t\t\t\t// For example, curl has default outputs `bin` and `man`. Previously, we would only install\n\t\t\t\t// the `bin` output as `v1/resolve`'s commit hash would match that. With /v2/resolve, we\n\t\t\t\t// install both outputs. So, team members on older Devbox will see just `bin` installed while\n\t\t\t\t// team members on newer Devbox will see both `bin` and `man` installed.\n\t\t\t\tstorePath = outputs[0].Path\n\t\t\t}\n\t\t\tpkg.Systems[sys] = &SystemInfo{\n\t\t\t\tOutputs:   outputs,\n\t\t\t\tStorePath: storePath,\n\t\t\t}\n\t\t}\n\t}\n\treturn pkg, nil\n}\n\nfunc selectForSystem[V any](systems map[string]V) (v V, err error) {\n\tif v, ok := systems[nix.System()]; ok {\n\t\treturn v, nil\n\t}\n\tif v, ok := systems[\"x86_64-linux\"]; ok {\n\t\treturn v, nil\n\t}\n\tfor _, v := range systems {\n\t\treturn v, nil\n\t}\n\treturn v, redact.Errorf(\"no systems found\")\n}\n\nfunc buildLockSystemInfos(pkg *searcher.PackageVersion) (map[string]*SystemInfo, error) {\n\t// guard against missing search data\n\tsystems := lo.PickBy(pkg.Systems, func(sysName string, sysInfo searcher.PackageInfo) bool {\n\t\treturn sysInfo.StoreHash != \"\" && sysInfo.StoreName != \"\"\n\t})\n\n\tgroup, ctx := errgroup.WithContext(context.Background())\n\n\tvar storePathLock sync.RWMutex\n\tsysStorePaths := map[string]string{}\n\tfor _sysName, _sysInfo := range systems {\n\t\tsysName := _sysName // capture range variable\n\t\tsysInfo := _sysInfo // capture range variable\n\n\t\tgroup.Go(func() error {\n\t\t\t// We should use devpkg.BinaryCache here, but it'll cause a circular reference\n\t\t\t// Just hardcoding for now. Maybe we should move that to nix.DefaultBinaryCache?\n\t\t\tpath, err := nix.StorePathFromHashPart(ctx, sysInfo.StoreHash, \"https://cache.nixos.org\")\n\t\t\tif err != nil {\n\t\t\t\t// Should we report this to sentry to collect data?\n\t\t\t\tslog.Error(\"failed to resolve store path\", \"system\", sysName, \"store_hash\", sysInfo.StoreHash, \"err\", err)\n\t\t\t\t// Instead of erroring, we can just skip this package. It can install via the slow path.\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tstorePathLock.Lock()\n\t\t\tsysStorePaths[sysName] = path\n\t\t\tstorePathLock.Unlock()\n\t\t\treturn nil\n\t\t})\n\t}\n\terr := group.Wait()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsysInfos := map[string]*SystemInfo{}\n\tfor sysName, storePath := range sysStorePaths {\n\t\tsysInfo := &SystemInfo{\n\t\t\tStorePath: storePath,\n\t\t}\n\t\tsysInfo.addOutputFromLegacyStorePath()\n\t\tsysInfos[sysName] = sysInfo\n\t}\n\treturn sysInfos, nil\n}\n\nfunc lockFlake(ctx context.Context, ref flake.Ref) (flake.Ref, error) {\n\tif ref.Locked() {\n\t\treturn ref, nil\n\t}\n\n\t// Nix requires a NAR hash for GitHub flakes to be locked. A Devbox lock\n\t// file is a bit more lenient and only requires a revision so that we\n\t// don't need to download the nixpkgs source for cached packages. If the\n\t// search index is ever able to return the NAR hash then we can remove\n\t// this check.\n\tif ref.Type == flake.TypeGitHub && (ref.Rev != \"\") {\n\t\treturn ref, nil\n\t}\n\n\tvar meta nix.FlakeMetadata\n\tvar err error\n\t// For nixpkgs, we cache resolutions (currently flakeCacheTTL=30 days) to avoid downloading\n\t// new nixpkgs too often which is really slow and rarely changes anything.\n\t//\n\t// Ideally we can do something similar for all packages (flake and otherwise)\n\t// Specifically, if user adds python@3.12 (or python@latest that resolves to 3.12) and that\n\t// package is already installed, we should use it instead of using 3.12 from search service\n\t// (which may have different store path). This would allow all devbox projects to share packages\n\t// if the version resolution is the same.\n\t//\n\t// That said, the logic for caching resolved versions and non-locked flake references would not\n\t// be the same.\n\tif ref.IsNixpkgs() {\n\t\tmeta, err = nix.ResolveCachedFlake(ctx, ref)\n\t} else {\n\t\tmeta, err = nix.ResolveFlake(ctx, ref)\n\t}\n\n\tif err != nil {\n\t\treturn ref, err\n\t}\n\treturn meta.Locked, nil\n}\n"
  },
  {
    "path": "internal/lock/statehash.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage lock\n\nimport (\n\t\"errors\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/cachehash\"\n\t\"go.jetify.com/devbox/internal/cuecfg\"\n)\n\nvar ignoreShellMismatch = false\n\n// stateHashFile is a non-shared lock file that helps track the state of the\n// local devbox environment. It contains hashes that may not be the same across\n// machines (e.g. manifest hash).\n// When we do implement a shared lock file, it may contain some shared fields\n// with this one but not all.\ntype stateHashFile struct {\n\tConfigHash    string `json:\"config_hash\"`\n\tDevboxVersion string `json:\"devbox_version\"`\n\t// fish has different generated scripts so we need to recompute them if user\n\t// changes shell.\n\tIsFish                 bool   `json:\"is_fish\"`\n\tLockFileHash           string `json:\"lock_file_hash\"`\n\tNixPrintDevEnvHash     string `json:\"nix_print_dev_env_hash\"`\n\tNixProfileManifestHash string `json:\"nix_profile_manifest_hash\"`\n}\n\ntype UpdateStateHashFileArgs struct {\n\tProjectDir string\n\tConfigHash string\n\t// IsFish is an arg because in the future we may allow the user\n\t// to specify shell in devbox.json which should be passed in here.\n\tIsFish bool\n}\n\nfunc UpdateAndSaveStateHashFile(args UpdateStateHashFileArgs) error {\n\tnewLock, err := getCurrentStateHash(args)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn cuecfg.WriteFile(stateHashFilePath(args.ProjectDir), newLock)\n}\n\n// SetIgnoreShellMismatch is used to disable the shell comparison when checking\n// if the state is up to date. This is useful when we don't load shellrc (e.g. running)\nfunc SetIgnoreShellMismatch(ignore bool) {\n\tignoreShellMismatch = ignore\n}\n\nfunc isStateUpToDate(args UpdateStateHashFileArgs) (bool, error) {\n\tfilesystemStateHash, err := readStateHashFile(args.ProjectDir)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tnewStateHash, err := getCurrentStateHash(args)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif ignoreShellMismatch {\n\t\tfilesystemStateHash.IsFish = newStateHash.IsFish\n\t}\n\n\treturn *filesystemStateHash == *newStateHash, nil\n}\n\nfunc readStateHashFile(projectDir string) (*stateHashFile, error) {\n\thashFile := &stateHashFile{}\n\terr := cuecfg.ParseFile(stateHashFilePath(projectDir), hashFile)\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn hashFile, nil\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn hashFile, nil\n}\n\nfunc getCurrentStateHash(args UpdateStateHashFileArgs) (*stateHashFile, error) {\n\tnixHash, err := manifestHash(args.ProjectDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprintDevEnvCacheHash, err := printDevEnvCacheHash(args.ProjectDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlockfileHash, err := getLockfileHash(args.ProjectDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnewLock := &stateHashFile{\n\t\tConfigHash:             args.ConfigHash,\n\t\tDevboxVersion:          build.Version,\n\t\tIsFish:                 args.IsFish,\n\t\tLockFileHash:           lockfileHash,\n\t\tNixPrintDevEnvHash:     printDevEnvCacheHash,\n\t\tNixProfileManifestHash: nixHash,\n\t}\n\n\treturn newLock, nil\n}\n\nfunc stateHashFilePath(projectDir string) string {\n\treturn filepath.Join(projectDir, \".devbox\", \"state.json\")\n}\n\nfunc manifestHash(profileDir string) (string, error) {\n\treturn cachehash.JSONFile(filepath.Join(profileDir, \".devbox/nix/profile/default/manifest.json\"))\n}\n\nfunc printDevEnvCacheHash(profileDir string) (string, error) {\n\treturn cachehash.JSONFile(filepath.Join(profileDir, \".devbox/.nix-print-dev-env-cache\"))\n}\n\nfunc getLockfileHash(projectDir string) (string, error) {\n\treturn cachehash.JSONFile(lockFilePath(projectDir))\n}\n"
  },
  {
    "path": "internal/nix/build.go",
    "content": "package nix\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/internal/debug\"\n)\n\ntype BuildArgs struct {\n\tAllowInsecure     bool\n\tEnv               []string\n\tExtraSubstituters []string\n\tFlags             []string\n\tWriter            io.Writer\n}\n\nfunc Build(ctx context.Context, args *BuildArgs, installables ...string) error {\n\tdefer debug.FunctionTimer().End()\n\n\tFixInstallableArgs(installables)\n\n\t// --impure is required for allowUnfreeEnv/allowInsecureEnv to work.\n\tcmd := Command(\"build\", \"--impure\")\n\tcmd.Args = appendArgs(cmd.Args, args.Flags)\n\tcmd.Args = appendArgs(cmd.Args, installables)\n\t// Adding extra substituters only here to be conservative, but this could also\n\t// be added to ExperimentalFlags() in the future.\n\tif len(args.ExtraSubstituters) > 0 {\n\t\tcmd.Args = append(cmd.Args,\n\t\t\t\"--extra-substituters\",\n\t\t\tstrings.Join(args.ExtraSubstituters, \" \"),\n\t\t)\n\t}\n\tcmd.Env = append(allowUnfreeEnv(os.Environ()), args.Env...)\n\tif args.AllowInsecure {\n\t\tslog.Debug(\"Setting Allow-insecure env-var\\n\")\n\t\tcmd.Env = allowInsecureEnv(cmd.Env)\n\t}\n\n\t// If nix build runs as tty, the output is much nicer. If we ever\n\t// need to change this to our own writers, consider that you may need\n\t// to implement your own nicer output. --print-build-logs flag may be useful.\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = args.Writer\n\tcmd.Stderr = args.Writer\n\treturn cmd.Run(ctx)\n}\n"
  },
  {
    "path": "internal/nix/cache.go",
    "content": "package nix\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n)\n\nfunc CopyInstallableToCache(\n\tctx context.Context,\n\tout io.Writer,\n\t// Note: installable is a string instead of a flake.Installable\n\t// because flake.Installable does not support store paths yet. It converts\n\t// paths into \"path\" flakes which is not what we want for /nix/store paths.\n\t// TODO: Add support for store paths in flake.Installable\n\tto, installable string,\n\tenv []string,\n) error {\n\tfmt.Fprintf(out, \"Copying %s to %s\\n\", installable, to)\n\tcmd := Command(\n\t\t\"copy\", \"--to\", to,\n\t\t// --impure makes NIXPKGS_ALLOW_* environment variables work.\n\t\t\"--impure\",\n\t\t// --refresh checks the cache to ensure it is up to date. Otherwise if\n\t\t// anything has was copied previously from this machine and then purged\n\t\t// it may not be copied again. It's fairly fast, but not instant.\n\t\t\"--refresh\",\n\t\tinstallable,\n\t)\n\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = out\n\tcmd.Stderr = out\n\tcmd.Env = append(allowUnfreeEnv(allowInsecureEnv(os.Environ())), env...)\n\n\treturn cmd.Run(ctx)\n}\n"
  },
  {
    "path": "internal/nix/command.go",
    "content": "package nix\n\nimport \"os\"\n\nfunc init() {\n\tDefault.ExtraArgs = Args{\n\t\t\"--extra-experimental-features\", \"ca-derivations\",\n\t\t\"--option\", \"experimental-features\", \"nix-command flakes fetch-closure\",\n\t}\n\n\t// Add GitHub access token if available to avoid rate limiting\n\t// This is a backup in case the config file isn't picked up properly\n\tif token := os.Getenv(\"GITHUB_TOKEN\"); token != \"\" {\n\t\tDefault.ExtraArgs = append(Default.ExtraArgs,\n\t\t\t\"--option\", \"access-tokens\", \"github.com=\"+token)\n\t}\n}\n\nfunc appendArgs[E any](args Args, new []E) Args {\n\tfor _, elem := range new {\n\t\targs = append(args, elem)\n\t}\n\treturn args\n}\n\nfunc allowUnfreeEnv(curEnv []string) []string {\n\treturn append(curEnv, \"NIXPKGS_ALLOW_UNFREE=1\")\n}\n\nfunc allowInsecureEnv(curEnv []string) []string {\n\treturn append(curEnv, \"NIXPKGS_ALLOW_INSECURE=1\")\n}\n"
  },
  {
    "path": "internal/nix/config.go",
    "content": "package nix\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/internal/redact\"\n\t\"go.jetify.com/devbox/nix\"\n)\n\n// Config is a parsed Nix configuration.\ntype Config struct {\n\tExperimentalFeatures ConfigField[[]string] `json:\"experimental-features\"`\n\tSubstitute           ConfigField[bool]     `json:\"substitute\"`\n\tSubstituters         ConfigField[[]string] `json:\"substituters\"`\n\tSystem               ConfigField[string]   `json:\"system\"`\n\tTrustedSubstituters  ConfigField[[]string] `json:\"trusted-substituters\"`\n\tTrustedUsers         ConfigField[[]string] `json:\"trusted-users\"`\n}\n\n// ConfigField is a Nix configuration setting.\ntype ConfigField[T any] struct {\n\tValue T `json:\"value\"`\n}\n\n// CurrentConfig reads the current Nix configuration.\nfunc CurrentConfig(ctx context.Context) (Config, error) {\n\t// `nix show-config` is deprecated in favor of `nix config show`, but we\n\t// want to remain compatible with older Nix versions.\n\tcmd := Command(\"show-config\", \"--json\")\n\tout, err := cmd.Output(ctx)\n\tvar exitErr *exec.ExitError\n\tif errors.As(err, &exitErr) && len(exitErr.Stderr) != 0 {\n\t\treturn Config{}, redact.Errorf(\"command %s: %v: %s\", redact.Safe(cmd), err, exitErr.Stderr)\n\t}\n\tif err != nil {\n\t\treturn Config{}, redact.Errorf(\"command %s: %v\", cmd, err)\n\t}\n\tcfg := Config{}\n\tif err := json.Unmarshal(out, &cfg); err != nil {\n\t\treturn Config{}, redact.Errorf(\"unmarshal JSON output from %s: %v\", redact.Safe(cmd), err)\n\t}\n\treturn cfg, nil\n}\n\n// IsUserTrusted reports if the current OS user is in the trusted-users list. If\n// there are any groups in the list, it also checks if the user belongs to any\n// of them.\nfunc (c Config) IsUserTrusted(ctx context.Context, username string) (bool, error) {\n\ttrusted := c.TrustedUsers.Value\n\tif len(trusted) == 0 {\n\t\treturn false, nil\n\t}\n\n\tcurrent, err := user.Lookup(username)\n\tif err != nil {\n\t\treturn false, redact.Errorf(\"lookup user: %v\", err)\n\t}\n\tif slices.Contains(trusted, current.Username) {\n\t\treturn true, nil\n\t}\n\n\t// trusted-user entries that start with an @ are group names\n\t// (for example, @wheel). Lookup each group ID to see if the user\n\t// belongs to a trusted group.\n\tvar currentGids []string\n\tfor i := range trusted {\n\t\tgroupName := strings.TrimPrefix(trusted[i], \"@\")\n\t\tif groupName == trusted[i] || groupName == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tgroup, err := user.LookupGroup(groupName)\n\t\tvar unknownErr user.UnknownGroupError\n\t\tif errors.As(err, &unknownErr) {\n\t\t\tslog.Debug(\"skipping unknown trusted-user group found in nix.conf\", \"group\", groupName)\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn false, redact.Errorf(\"lookup trusted-user group from nix.conf: %v\", err)\n\t\t}\n\n\t\t// Be lazy about looking up the current user's groups until we\n\t\t// encounter one in the trusted-users list.\n\t\tif currentGids == nil {\n\t\t\tcurrentGids, err = current.GroupIds()\n\t\t\tif err != nil {\n\t\t\t\treturn false, redact.Errorf(\"lookup current user group IDs: %v\", err)\n\t\t\t}\n\t\t}\n\t\tif slices.Contains(currentGids, group.Gid) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\nfunc IncludeDevboxConfig(ctx context.Context, username string) error {\n\tinfo, _ := nix.Default.Info()\n\tpath := cmp.Or(info.SystemConfig, \"/etc/nix/nix.conf\")\n\tincludePath := filepath.Join(filepath.Dir(path), \"devbox-nix.conf\")\n\tb := fmt.Appendf(nil, \"# This config was auto-generated by Devbox.\\n\\nextra-trusted-users = %s\\n\", username)\n\tif err := os.WriteFile(includePath, b, 0o664); err != nil {\n\t\treturn redact.Errorf(\"write devbox nix.conf: %v\", err)\n\t}\n\n\tappended, err := appendConfigInclude(path, includePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif appended {\n\t\treturn restartDaemon(ctx)\n\t}\n\treturn nil\n}\n\nfunc appendConfigInclude(srcPath, includePath string) (appended bool, err error) {\n\tnixConf, err := os.OpenFile(srcPath, os.O_RDWR, 0)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer nixConf.Close()\n\n\tconfb, err := io.ReadAll(nixConf)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tfor _, line := range strings.Split(string(confb), \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\t// <whitespace>\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(line, \"#\") {\n\t\t\t// # comment\n\t\t\tcontinue\n\t\t}\n\n\t\tpath := strings.TrimSpace(strings.TrimPrefix(line, \"include\"))\n\t\tif path == includePath {\n\t\t\t// include devbox-nix.conf\n\t\t\treturn false, nil\n\t\t}\n\t\tpath = strings.TrimSpace(strings.TrimPrefix(line, \"!include\"))\n\t\tif path == includePath {\n\t\t\t// !include devbox-nix.conf\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\tinclude := \"\\ninclude \" + includePath + \"\\n\"\n\tif _, err := nixConf.WriteString(include); err != nil {\n\t\treturn false, redact.Errorf(\"append %q to %s: %v\", redact.Safe(include), srcPath, err)\n\t}\n\tif err := nixConf.Close(); err != nil {\n\t\treturn false, redact.Errorf(\"append %q to %s: %v\", redact.Safe(include), srcPath, err)\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "internal/nix/config_test.go",
    "content": "package nix\n\nimport (\n\t\"os/user\"\n\t\"testing\"\n)\n\n//nolint:revive\nfunc TestConfigIsUserTrusted(t *testing.T) {\n\tcurUser, err := user.Current()\n\tif err != nil {\n\t\tt.Fatal(\"lookup current user:\", err)\n\t}\n\n\tt.Run(\"UsernameInList\", func(t *testing.T) {\n\t\tt.Setenv(\"NIX_CONFIG\", \"trusted-users = \"+curUser.Username)\n\n\t\tctx := t.Context()\n\t\tcfg, err := CurrentConfig(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\ttrusted, err := cfg.IsUserTrusted(ctx, curUser.Username)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !trusted {\n\t\t\tt.Error(\"got trusted = false, want true\")\n\t\t}\n\t})\n\tt.Run(\"UserGroupInList\", func(t *testing.T) {\n\t\tg, err := user.LookupGroupId(curUser.Gid)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tt.Setenv(\"NIX_CONFIG\", \"trusted-users = @\"+g.Name)\n\n\t\tctx := t.Context()\n\t\tcfg, err := CurrentConfig(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\ttrusted, err := cfg.IsUserTrusted(ctx, curUser.Username)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !trusted {\n\t\t\tt.Error(\"got trusted = false, want true\")\n\t\t}\n\t})\n\tt.Run(\"NotInList\", func(t *testing.T) {\n\t\tt.Setenv(\"NIX_CONFIG\", \"trusted-users = root\")\n\n\t\tctx := t.Context()\n\t\tcfg, err := CurrentConfig(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\ttrusted, err := cfg.IsUserTrusted(ctx, curUser.Username)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif trusted {\n\t\t\tt.Error(\"got trusted = true, want false\")\n\t\t}\n\t})\n\tt.Run(\"EmptyList\", func(t *testing.T) {\n\t\tt.Setenv(\"NIX_CONFIG\", \"trusted-users =\")\n\n\t\tctx := t.Context()\n\t\tcfg, err := CurrentConfig(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\ttrusted, err := cfg.IsUserTrusted(ctx, curUser.Username)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif trusted {\n\t\t\tt.Error(\"got trusted = true, want false\")\n\t\t}\n\t})\n\tt.Run(\"UnknownGroup\", func(t *testing.T) {\n\t\tt.Setenv(\"NIX_CONFIG\", \"trusted-users = @dummygroup\")\n\n\t\tctx := t.Context()\n\t\tcfg, err := CurrentConfig(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\ttrusted, err := cfg.IsUserTrusted(ctx, curUser.Username)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif trusted {\n\t\t\tt.Error(\"got trusted = true, want false\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/nix/doc.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\n// Package nix provides Go API for nix.\n// Internally this is a wrapper around the nix command line utilities.\n// I'd love to use a go SDK instead, and drop the dependency on the CLI.\n// The dependency means that users need to install nix, before using devbox.\n// Unfortunately, that go sdk does not exist. We would have to implement it.\npackage nix\n"
  },
  {
    "path": "internal/nix/eval.go",
    "content": "package nix\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"strconv\"\n)\n\nfunc EvalPackageName(path string) (string, error) {\n\tcmd := Command(\"eval\", \"--raw\", path+\".name\")\n\tout, err := cmd.Output(context.TODO())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(out), nil\n}\n\n// PackageIsInsecure is a fun little nix eval that maybe works.\nfunc PackageIsInsecure(path string) bool {\n\tcmd := Command(\"eval\", path+\".meta.insecure\")\n\tout, err := cmd.Output(context.TODO())\n\tif err != nil {\n\t\t// We can't know for sure, but probably not.\n\t\treturn false\n\t}\n\tvar insecure bool\n\tif err := json.Unmarshal(out, &insecure); err != nil {\n\t\t// We can't know for sure, but probably not.\n\t\treturn false\n\t}\n\treturn insecure\n}\n\nfunc PackageKnownVulnerabilities(path string) []string {\n\tcmd := Command(\"eval\", path+\".meta.knownVulnerabilities\")\n\tout, err := cmd.Output(context.TODO())\n\tif err != nil {\n\t\t// We can't know for sure, but probably not.\n\t\treturn nil\n\t}\n\tvar vulnerabilities []string\n\tif err := json.Unmarshal(out, &vulnerabilities); err != nil {\n\t\t// We can't know for sure, but probably not.\n\t\treturn nil\n\t}\n\treturn vulnerabilities\n}\n\n// Eval is raw nix eval. Needs to be parsed. Useful for stuff like\n// nix eval --raw nixpkgs/9ef09e06806e79e32e30d17aee6879d69c011037#fuse3\n// to determine if a package if a package can be installed in system.\nfunc Eval(path string) ([]byte, error) {\n\tcmd := Command(\"eval\", \"--raw\", path)\n\treturn cmd.CombinedOutput(context.TODO())\n}\n\nfunc IsInsecureAllowed() bool {\n\tallowed, _ := strconv.ParseBool(os.Getenv(\"NIXPKGS_ALLOW_INSECURE\"))\n\treturn allowed\n}\n"
  },
  {
    "path": "internal/nix/flake.go",
    "content": "package nix\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"go.jetify.com/devbox/nix/flake\"\n\t\"go.jetify.com/pkg/filecache\"\n)\n\nconst flakeCacheTTL = time.Hour * 24 * 30\n\nvar flakeFileCache = filecache.New[FlakeMetadata](\"devbox/flakes\")\n\ntype FlakeMetadata struct {\n\tDescription  string    `json:\"description\"`\n\tLastModified int64     `json:\"lastModified\"`\n\tLocked       flake.Ref `json:\"locked\"`\n\tOriginal     flake.Ref `json:\"original\"`\n\tPath         string    `json:\"path\"`\n\tResolved     flake.Ref `json:\"resolved\"`\n}\n\nfunc ResolveFlake(ctx context.Context, ref flake.Ref) (FlakeMetadata, error) {\n\tcmd := Command(\"flake\", \"metadata\", \"--json\", ref)\n\tout, err := cmd.Output(ctx)\n\tif err != nil {\n\t\treturn FlakeMetadata{}, err\n\t}\n\tmeta := FlakeMetadata{}\n\terr = json.Unmarshal(out, &meta)\n\tif err != nil {\n\t\treturn FlakeMetadata{}, err\n\t}\n\treturn meta, nil\n}\n\nfunc ResolveCachedFlake(ctx context.Context, ref flake.Ref) (FlakeMetadata, error) {\n\treturn flakeFileCache.GetOrSet(ref.String(), func() (FlakeMetadata, time.Duration, error) {\n\t\tmeta, err := ResolveFlake(ctx, ref)\n\t\tif err != nil {\n\t\t\treturn FlakeMetadata{}, 0, err\n\t\t}\n\t\treturn meta, flakeCacheTTL, nil\n\t})\n}\n\nfunc ClearFlakeCache(ref flake.Ref) error {\n\t// TODO: Add unset to filecache\n\treturn flakeFileCache.Set(ref.String(), FlakeMetadata{}, -1)\n}\n"
  },
  {
    "path": "internal/nix/install.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nix\n\nimport (\n\t\"context\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/briandowns/spinner\"\n\t\"github.com/fatih/color\"\n\t\"github.com/mattn/go-isatty\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/cmdutil\"\n\t\"go.jetify.com/devbox/internal/fileutil\"\n\t\"go.jetify.com/devbox/nix\"\n)\n\nfunc BinaryInstalled() bool {\n\treturn cmdutil.Exists(\"nix\")\n}\n\nfunc dirExistsAndIsNotEmpty(dir string) bool {\n\tempty, err := fileutil.IsDirEmpty(dir)\n\treturn err == nil && !empty\n}\n\nvar ensured = false\n\nfunc Ensured() bool {\n\treturn ensured\n}\n\nfunc EnsureNixInstalled(ctx context.Context, writer io.Writer, withDaemonFunc func() *bool) (err error) {\n\tensured = true\n\tdefer func() {\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\t// ensure minimum nix version installed\n\t\tif !nix.AtLeast(MinVersion) {\n\t\t\terr = usererr.New(\n\t\t\t\t\"Devbox requires nix of version >= %s. Your version is %s. \"+\n\t\t\t\t\t\"Please upgrade nix and try again.\\n\",\n\t\t\t\tMinVersion,\n\t\t\t\tnix.Version(),\n\t\t\t)\n\t\t\treturn\n\t\t}\n\t}()\n\n\tif BinaryInstalled() {\n\t\treturn nil\n\t}\n\tif dirExistsAndIsNotEmpty(\"/nix\") {\n\t\tif _, err = SourceProfile(); err != nil {\n\t\t\treturn err\n\t\t} else if BinaryInstalled() {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn usererr.New(\n\t\t\t\"We found a /nix directory but nix binary is not in your PATH and we \" +\n\t\t\t\t\"were not able to find it in the usual locations. Your nix installation \" +\n\t\t\t\t\"might be broken. If restarting your terminal or reinstalling nix \" +\n\t\t\t\t\"doesn't work, please create an issue at \" +\n\t\t\t\t\"https://github.com/jetify-com/devbox/issues\",\n\t\t)\n\t}\n\n\tcolor.Yellow(\"\\nNix is not installed. Devbox will attempt to install it.\\n\\n\")\n\n\tinstaller := nix.Installer{}\n\tif isatty.IsTerminal(os.Stdout.Fd()) {\n\t\tcolor.Yellow(\"Press enter to continue or ctrl-c to exit.\\n\")\n\t\tfmt.Scanln() //nolint:errcheck\n\n\t\tspinny := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriter(writer))\n\t\tspinny.Suffix = \" Downloading the Nix installer...\"\n\t\tspinny.Start()\n\t\tdefer spinny.Stop() // reset the terminal in case of a panic\n\n\t\terr = installer.Download(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tspinny.Stop()\n\t} else {\n\t\tfmt.Fprint(writer, \"Downloading the Nix installer...\")\n\t\terr = installer.Download(ctx)\n\t\tif err != nil {\n\t\t\tfmt.Fprintln(writer)\n\t\t\treturn err\n\t\t}\n\t\tfmt.Fprintln(writer, \" done.\")\n\t}\n\terr = installer.Run(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Fprintln(writer, \"Nix installed successfully. Devbox is ready to use!\")\n\treturn nil\n}\n"
  },
  {
    "path": "internal/nix/install_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nix\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDirExistsAndIsNotEmpty(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tsetup    func(string) error\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"empty directory\",\n\t\t\tsetup: func(dir string) error {\n\t\t\t\treturn nil // Directory is already empty\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"directory with files\",\n\t\t\tsetup: func(dir string) error {\n\t\t\t\tfile := filepath.Join(dir, \"test.txt\")\n\t\t\t\treturn os.WriteFile(file, []byte(\"test content\"), 0o644)\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"directory with subdirectories\",\n\t\t\tsetup: func(dir string) error {\n\t\t\t\tsubdir := filepath.Join(dir, \"subdir\")\n\t\t\t\treturn os.MkdirAll(subdir, 0o755)\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"directory with hidden files\",\n\t\t\tsetup: func(dir string) error {\n\t\t\t\tfile := filepath.Join(dir, \".hidden\")\n\t\t\t\treturn os.WriteFile(file, []byte(\"hidden content\"), 0o644)\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"non-existent directory\",\n\t\t\tsetup: func(dir string) error {\n\t\t\t\treturn os.RemoveAll(dir)\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, curTest := range tests {\n\t\tt.Run(curTest.name, func(t *testing.T) {\n\t\t\t// Create temporary directory for test\n\t\t\ttempDir := t.TempDir()\n\n\t\t\t// Setup test case\n\t\t\tif curTest.setup != nil {\n\t\t\t\terr := curTest.setup(tempDir)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\t// Run the function\n\t\t\tresult := dirExistsAndIsNotEmpty(tempDir)\n\n\t\t\t// Check results\n\t\t\tassert.Equal(t, curTest.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/nix/instance.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nix\n\nimport \"context\"\n\n// These make it easier to stub out nix for testing\ntype NixInstance struct{}\n\ntype Nixer interface {\n\tPrintDevEnv(ctx context.Context, args *PrintDevEnvArgs) (*PrintDevEnvOut, error)\n}\n"
  },
  {
    "path": "internal/nix/nix.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nix\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"runtime/trace\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/boxcli/featureflag\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/redact\"\n\t\"go.jetify.com/devbox/nix/flake\"\n\n\t\"go.jetify.com/devbox/internal/debug\"\n)\n\n// ProfilePath contains the contents of the profile generated via `nix-env --profile ProfilePath <command>`\n// or `nix profile install --profile ProfilePath <package...>`\n// Instead of using directory, prefer using the devbox.ProfileDir() function that ensures the directory exists.\nconst ProfilePath = \".devbox/nix/profile/default\"\n\ntype PrintDevEnvOut struct {\n\tVariables map[string]Variable // the key is the name.\n}\n\ntype Variable struct {\n\tType  string // valid types are var, exported, and array.\n\tValue any    // can be a string or an array of strings (iff type is array).\n}\n\ntype PrintDevEnvArgs struct {\n\tFlakeDir             string\n\tPrintDevEnvCachePath string\n\tUsePrintDevEnvCache  bool\n}\n\n// PrintDevEnv calls `nix print-dev-env -f <path>` and returns its output. The output contains\n// all the environment variables and bash functions required to create a nix shell.\nfunc (*NixInstance) PrintDevEnv(ctx context.Context, args *PrintDevEnvArgs) (*PrintDevEnvOut, error) {\n\tdefer debug.FunctionTimer().End()\n\tdefer trace.StartRegion(ctx, \"nixPrintDevEnv\").End()\n\n\tvar data []byte\n\tvar err error\n\tvar out PrintDevEnvOut\n\n\tif args.UsePrintDevEnvCache {\n\t\tdata, err = os.ReadFile(args.PrintDevEnvCachePath)\n\t\tif err == nil {\n\t\t\tif err := json.Unmarshal(data, &out); err != nil {\n\t\t\t\treturn nil, errors.WithStack(err)\n\t\t\t}\n\t\t} else if !errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn nil, errors.WithStack(err)\n\t\t}\n\t}\n\n\tflakeDirResolved, err := filepath.EvalSymlinks(args.FlakeDir)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tref := flake.Ref{Type: flake.TypePath, Path: flakeDirResolved}\n\n\tif len(data) == 0 {\n\t\tcmd := Command(\"print-dev-env\", \"--json\")\n\t\tif featureflag.ImpurePrintDevEnv.Enabled() {\n\t\t\tcmd.Args = append(cmd.Args, \"--impure\")\n\t\t}\n\t\tcmd.Args = append(cmd.Args, ref)\n\t\tslog.Debug(\"running print-dev-env cmd\", \"cmd\", cmd)\n\t\tdata, err = cmd.Output(ctx)\n\t\tif insecure, insecureErr := IsExitErrorInsecurePackage(err, \"\" /*pkgName*/, \"\" /*installable*/); insecure {\n\t\t\treturn nil, insecureErr\n\t\t} else if err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif err := json.Unmarshal(data, &out); err != nil {\n\t\t\treturn nil, redact.Errorf(\"unmarshal nix print-dev-env output: %w\", redact.Safe(err))\n\t\t}\n\n\t\tif err = savePrintDevEnvCache(args.PrintDevEnvCachePath, out); err != nil {\n\t\t\treturn nil, redact.Errorf(\"savePrintDevEnvCache: %w\", redact.Safe(err))\n\t\t}\n\t}\n\n\treturn &out, nil\n}\n\nfunc savePrintDevEnvCache(path string, out PrintDevEnvOut) error {\n\tdata, err := json.Marshal(out)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t_ = os.WriteFile(path, data, 0o644)\n\treturn nil\n}\n\n// FlakeNixpkgs returns a flakes-compatible reference to the nixpkgs registry.\n// TODO savil. Ensure this works with the nixed cache service.\nfunc FlakeNixpkgs(commit string) string {\n\t// Using nixpkgs/<commit> means:\n\t// The nixpkgs entry in the flake registry, with its Git revision overridden to a specific value.\n\treturn \"github:NixOS/nixpkgs/\" + commit\n}\n\nfunc ExperimentalFlags() []string {\n\toptions := []string{\"nix-command\", \"flakes\", \"fetch-closure\"}\n\treturn []string{\n\t\t\"--extra-experimental-features\", \"ca-derivations\",\n\t\t\"--option\", \"experimental-features\", strings.Join(options, \" \"),\n\t}\n}\n\nfunc SystemIsLinux() bool {\n\treturn strings.Contains(System(), \"linux\")\n}\n\nvar nixPlatforms = []string{\n\t\"aarch64-darwin\",\n\t\"aarch64-linux\",\n\t\"i686-linux\",\n\t\"x86_64-darwin\",\n\t\"x86_64-linux\",\n\t// not technically supported, but should work?\n\t// ref. https://nixos.wiki/wiki/Nix_on_ARM\n\t// ref. https://github.com/jetify-com/devbox/pull/1300\n\t\"armv7l-linux\",\n}\n\n// EnsureValidPlatform returns an error if the platform is not supported by nix.\n// https://nixos.org/manual/nix/stable/installation/supported-platforms.html\nfunc EnsureValidPlatform(platforms ...string) error {\n\tensureValid := func(platform string) error {\n\t\tfor _, p := range nixPlatforms {\n\t\t\tif p == platform {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\treturn usererr.New(\"Unsupported platform: %s. Valid platforms are: %v\", platform, nixPlatforms)\n\t}\n\n\tfor _, p := range platforms {\n\t\tif err := ensureValid(p); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Warning: be careful using the bins in default/bin, they won't always match bins\n// produced by the flakes.nix. Use devbox.NixBins() instead.\nfunc ProfileBinPath(projectDir string) string {\n\treturn filepath.Join(projectDir, ProfilePath, \"bin\")\n}\n\nfunc IsExitErrorInsecurePackage(err error, pkgNameOrEmpty, installableOrEmpty string) (bool, error) {\n\tvar exitErr *exec.ExitError\n\tif errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {\n\t\tif strings.Contains(string(exitErr.Stderr), \"is marked as insecure\") {\n\t\t\tpackageRegex := regexp.MustCompile(`Package ([^ ]+)`)\n\t\t\tpackageMatch := packageRegex.FindStringSubmatch(string(exitErr.Stderr))\n\n\t\t\tknownVulnerabilities := []string{}\n\t\t\tif installableOrEmpty != \"\" {\n\t\t\t\tknownVulnerabilities = PackageKnownVulnerabilities(installableOrEmpty)\n\t\t\t}\n\n\t\t\tinsecurePackages := parseInsecurePackagesFromExitError(string(exitErr.Stderr))\n\n\t\t\t// Construct the error message.\n\t\t\terrMessages := []string{}\n\t\t\terrMessages = append(errMessages, fmt.Sprintf(\"Package %s is insecure.\", packageMatch[1]))\n\t\t\tif len(knownVulnerabilities) > 0 {\n\t\t\t\terrMessages = append(errMessages,\n\t\t\t\t\tfmt.Sprintf(\"Known vulnerabilities:\\n%s\", strings.Join(knownVulnerabilities, \"\\n\")))\n\t\t\t}\n\t\t\tpkgName := pkgNameOrEmpty\n\t\t\tif pkgName == \"\" {\n\t\t\t\tpkgName = \"<pkg>\"\n\t\t\t}\n\t\t\terrMessages = append(errMessages,\n\t\t\t\tfmt.Sprintf(\"To override, use `devbox add %s --allow-insecure=%s`\", pkgName, strings.Join(insecurePackages, \", \")))\n\n\t\t\treturn true, usererr.New(\"%s\", strings.Join(errMessages, \"\\n\\n\"))\n\t\t}\n\t}\n\treturn false, nil\n}\n\nfunc parseInsecurePackagesFromExitError(errorMsg string) []string {\n\tinsecurePackages := []string{}\n\n\t// permittedRegex is designed to match the following:\n\t// permittedInsecurePackages = [\n\t//    \"package-one\"\n\t//    \"package-two\"\n\t// ];\n\tpermittedRegex := regexp.MustCompile(`permittedInsecurePackages\\s*=\\s*\\[([\\s\\S]*?)\\]`)\n\tpermittedMatch := permittedRegex.FindStringSubmatch(errorMsg)\n\tif len(permittedMatch) > 1 {\n\t\tpackagesList := permittedMatch[1]\n\t\t// pick out the package name strings inside the quotes\n\t\tpackageMatches := regexp.MustCompile(`\"([^\"]+)\"`).FindAllStringSubmatch(packagesList, -1)\n\n\t\t// Extract the insecure package names from the matches\n\t\tfor _, packageMatch := range packageMatches {\n\t\t\tif len(packageMatch) > 1 {\n\t\t\t\tinsecurePackages = append(insecurePackages, packageMatch[1])\n\t\t\t}\n\t\t}\n\t}\n\n\treturn insecurePackages\n}\n\nvar ErrUnknownServiceManager = errors.New(\"unknown service manager\")\n\nfunc restartDaemon(ctx context.Context) error {\n\tif runtime.GOOS != \"darwin\" {\n\t\terr := fmt.Errorf(\"don't know how to restart nix daemon: %w\", ErrUnknownServiceManager)\n\t\treturn &DaemonError{err: err}\n\t}\n\n\tcmd := exec.CommandContext(ctx, \"launchctl\", \"bootout\", \"system\", \"/Library/LaunchDaemons/org.nixos.nix-daemon.plist\")\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn &DaemonError{\n\t\t\tcmd:    cmd.String(),\n\t\t\tstderr: out,\n\t\t\terr:    fmt.Errorf(\"stop nix daemon: %w\", err),\n\t\t}\n\t}\n\tcmd = exec.CommandContext(ctx, \"launchctl\", \"bootstrap\", \"system\", \"/Library/LaunchDaemons/org.nixos.nix-daemon.plist\")\n\tout, err = cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn &DaemonError{\n\t\t\tcmd:    cmd.String(),\n\t\t\tstderr: out,\n\t\t\terr:    fmt.Errorf(\"start nix daemon: %w\", err),\n\t\t}\n\t}\n\n\t// TODO(gcurtis): poll for daemon to come back instead.\n\ttime.Sleep(2 * time.Second)\n\treturn nil\n}\n\n// FixInstallableArgs removes the narHash and lastModifiedDate query parameters\n// from any args that are valid installables and the Nix version is <2.25.\n// Otherwise it returns them unchanged.\n//\n// This fixes an issues with some older versions of Nix where specifying a\n// narHash without a lastModifiedDate results in an error.\nfunc FixInstallableArgs(args []string) {\n\tif AtLeast(Version2_25) {\n\t\treturn\n\t}\n\n\tfor i := range args {\n\t\tparsed, _ := flake.ParseInstallable(args[i])\n\t\tif parsed.Ref.NARHash == \"\" && parsed.Ref.LastModified == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif parsed.Ref.NARHash != \"\" && parsed.Ref.LastModified != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tparsed.Ref.NARHash = \"\"\n\t\tparsed.Ref.LastModified = 0\n\t\targs[i] = parsed.String()\n\t}\n}\n\n// fixInstallableArg calls fixInstallableArgs with a single argument.\nfunc FixInstallableArg(arg string) string {\n\targs := []string{arg}\n\tFixInstallableArgs(args)\n\treturn args[0]\n}\n"
  },
  {
    "path": "internal/nix/nix_test.go",
    "content": "package nix\n\nimport (\n\t\"testing\"\n)\n\nfunc TestParseInsecurePackagesFromExitError(t *testing.T) {\n\terrorText := `\n  at /nix/store/xwl0am98klc8mz074jdyvpnyc6vwzlla-source/lib/customisation.nix:267:17:\n\n          266|     in commonAttrs // {\n          267|       drvPath = assert condition; drv.drvPath;\n             |                 ^\n          268|       outPath = assert condition; drv.outPath;\n\n       … while evaluating the attribute 'handled'\n\n         at /nix/store/xwl0am98klc8mz074jdyvpnyc6vwzlla-source/pkgs/stdenv/generic/check-meta.nix:490:7:\n\n          489|       # or, alternatively, just output a warning message.\n          490|       handled =\n             |       ^\n          491|         (\n\n       (stack trace truncated; use '--show-trace' to show the full trace)\n\n       error: Package ‘python-2.7.18.7’ in /nix/store/xwl0am98klc8mz074jdyvpnyc6vwzlla-source/pkgs/development/interpreters/python/cpython/2.7/default.nix:335 is marked as insecure, refusing to evaluate.\n\n\n       Known issues:\n        - Python 2.7 has reached its end of life after 2020-01-01. See https://www.python.org/doc/sunset-python-2/.\n\n       You can install it anyway by allowing this package, using the\n       following methods:\n\n       a) To temporarily allow all insecure packages, you can use an environment\n          variable for a single invocation of the nix tools:\n\n            $ export NIXPKGS_ALLOW_INSECURE=1\n\n          Note: When using nix shell, nix build, nix develop, etc with a flake,\n                then pass --impure in order to allow use of environment variables.\n\n       b) for nixos-rebuild you can add ‘python-2.7.18.7’ to\n          nixpkgs.config.permittedInsecurePackages in the configuration.nix,\n          like so:\n\n            {\n              nixpkgs.config.permittedInsecurePackages = [\n                \"python-2.7.18.7\"\n              ];\n            }\n\n       c) For nix-env, nix-build, nix-shell or any other Nix command you can add\n          ‘python-2.7.18.7’ to permittedInsecurePackages in\n          ~/.config/nixpkgs/config.nix, like so:\n\n            {\n              permittedInsecurePackages = [\n                \"python-2.7.18.7\"\n              ];\n              `\n\tpackages := parseInsecurePackagesFromExitError(errorText)\n\tif len(packages) != 1 {\n\t\tt.Errorf(\"Expected 1 package, got %d\", len(packages))\n\t}\n\tif packages[0] != \"python-2.7.18.7\" {\n\t\tt.Errorf(\"Expected package 'python-2.7.18.7', got %s\", packages[0])\n\t}\n}\n"
  },
  {
    "path": "internal/nix/nixpkgs.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nix\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/fileutil\"\n\t\"go.jetify.com/devbox/internal/xdg\"\n)\n\n// EnsureNixpkgsPrefetched runs the prefetch step to download the flake of the registry\nfunc EnsureNixpkgsPrefetched(w io.Writer, commit string) error {\n\t// Look up the cached map of commitHash:nixStoreLocation\n\tcommitToLocation, err := nixpkgsCommitFileContents()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check if this nixpkgs.Commit is located in the local /nix/store\n\tlocation, isPresent := commitToLocation[commit]\n\tif isPresent {\n\t\tif fi, err := os.Stat(location); err == nil && fi.IsDir() {\n\t\t\t// The nixpkgs for this commit hash is present, so we don't need to prefetch\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tfmt.Fprintf(w, \"Ensuring nixpkgs registry is downloaded.\\n\")\n\tcmd := Command(\n\t\t\"flake\", \"prefetch\",\n\t\tFlakeNixpkgs(commit),\n\t)\n\tcmd.Stdout = w\n\tcmd.Stderr = cmd.Stdout\n\tif err := cmd.Run(context.TODO()); err != nil {\n\t\tfmt.Fprintf(w, \"Ensuring nixpkgs registry is downloaded: \")\n\t\tcolor.New(color.FgRed).Fprintf(w, \"Fail\\n\")\n\t\treturn err\n\t}\n\tfmt.Fprintf(w, \"Ensuring nixpkgs registry is downloaded: \")\n\tcolor.New(color.FgGreen).Fprintf(w, \"Success\\n\")\n\n\treturn saveToNixpkgsCommitFile(commit, commitToLocation)\n}\n\nfunc nixpkgsCommitFileContents() (map[string]string, error) {\n\tpath := nixpkgsCommitFilePath()\n\tif !fileutil.Exists(path) {\n\t\treturn map[string]string{}, nil\n\t}\n\n\tcontents, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\tcommitToLocation := map[string]string{}\n\treturn commitToLocation, errors.WithStack(json.Unmarshal(contents, &commitToLocation))\n}\n\nfunc saveToNixpkgsCommitFile(commit string, commitToLocation map[string]string) error {\n\t// Make a query to get the /nix/store path for this commit hash.\n\tcmd := Command(\"flake\", \"prefetch\", \"--json\",\n\t\tFlakeNixpkgs(commit),\n\t)\n\tout, err := cmd.Output(context.TODO())\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// read the json response\n\tvar prefetchData struct {\n\t\tStorePath string `json:\"storePath\"`\n\t}\n\tif err := json.Unmarshal(out, &prefetchData); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// Ensure the nixpkgs commit file path exists so we can write an update to it\n\tpath := nixpkgsCommitFilePath()\n\terr = os.MkdirAll(filepath.Dir(path), 0o755)\n\tif err != nil && !errors.Is(err, fs.ErrExist) {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// write to the map, jsonify it, and write that json to the nixpkgsCommit file\n\tcommitToLocation[commit] = prefetchData.StorePath\n\tserialized, err := json.Marshal(commitToLocation)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\treturn errors.WithStack(os.WriteFile(path, serialized, 0o644))\n}\n\nfunc nixpkgsCommitFilePath() string {\n\tcacheDir := xdg.CacheSubpath(\"devbox\")\n\treturn filepath.Join(cacheDir, \"nixpkgs.json\")\n}\n\n// IsGithubNixpkgsURL returns true if the package is a flake of the form:\n// github:NixOS/nixpkgs/...\n//\n// While there are many ways to specify this input, devbox always uses\n// github:NixOS/nixpkgs/<hash> as the URL. If the user wishes to reference nixpkgs\n// themselves, this function may not return true.\nfunc IsGithubNixpkgsURL(url string) bool {\n\treturn strings.HasPrefix(strings.ToLower(url), \"github:nixos/nixpkgs/\")\n}\n\nvar hashFromNixPkgsRegex = regexp.MustCompile(`(?i)github:nixos/nixpkgs/([^#]+).*`)\n\n// HashFromNixPkgsURL will (for example) return 5233fd2ba76a3accb5aaa999c00509a11fd0793c\n// from github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello\nfunc HashFromNixPkgsURL(url string) string {\n\tmatches := hashFromNixPkgsRegex.FindStringSubmatch(url)\n\tif len(matches) == 2 {\n\t\treturn matches[1]\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/nix/nixprofile/item.go",
    "content": "package nixprofile\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/redact\"\n)\n\n// NixProfileListItem is a go-struct of a line of printed output from `nix profile list`\n// docs: https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-profile-list.html\ntype NixProfileListItem struct {\n\t// An integer that can be used to unambiguously identify the package in\n\t// invocations of nix profile remove and nix profile upgrade.\n\tindex int\n\n\t// name of the package\n\t// nix 2.20 introduced a new format for the output of nix profile list, which includes the package name.\n\t// This field is used instead of index for `list`, `remove` and `upgrade` subcommands of `nix profile`.\n\tname string\n\n\t// The original (\"unlocked\") flake reference and output attribute path used at installation time.\n\t// NOTE that this will be empty if the package was added to the nix profile via store path.\n\tunlockedReference string\n\n\t// The locked flake reference to which the unlocked flake reference was resolved.\n\t// NOTE that this will be empty if the package was added to the nix profile via store path.\n\tlockedReference string\n\n\t// The store path(s) of the package. Should have at least 1 path, and should have exactly 1 path\n\t// if the item was added to the profile through a store path.\n\tnixStorePaths []string\n}\n\n// AttributePath parses the package attribute from the NixProfileListItem.lockedReference\n//\n// For example:\n// if NixProfileListItem.lockedReference = github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.go_1_19\n// then AttributePath = legacyPackages.x86_64-darwin.go_1_19\nfunc (i *NixProfileListItem) AttributePath() (string, error) {\n\t// lockedReference example:\n\t// github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.go_1_19\n\n\t// AttributePath example:\n\t// legacyPackages.x86_64.go_1_19\n\t_ /*nixpkgs*/, attrPath, found := strings.Cut(i.lockedReference, \"#\")\n\tif !found {\n\t\treturn \"\", redact.Errorf(\n\t\t\t\"expected to find # in lockedReference: %s from NixProfileListItem: %s\",\n\t\t\tredact.Safe(i.lockedReference),\n\t\t\ti,\n\t\t)\n\t}\n\treturn attrPath, nil\n}\n\n// Matches compares a devpkg.Package with this profile item and returns true if the profile item\n// was the result of adding the Package to the nix profile.\nfunc (i *NixProfileListItem) Matches(pkg *devpkg.Package, locker lock.Locker) bool {\n\tif i.addedByStorePath() {\n\t\t// If an Item was added via store path, the best we can do when comparing to a Package is to check\n\t\t// if its store path matches that of the Package. Note that the item should only have 1 store path.\n\t\tpaths, err := pkg.InputAddressedPaths()\n\t\tif err != nil {\n\t\t\t// pkg couldn't have been added by store path if we can't get the store path for it, so return\n\t\t\t// false. There are some edge cases (e.g. cache is down, index changed, etc., but it's OK to\n\t\t\t// err on the side of false).\n\t\t\treturn false\n\t\t}\n\t\tfor _, path := range paths {\n\t\t\treturn len(i.nixStorePaths) == 1 && i.nixStorePaths[0] == path\n\t\t}\n\t\treturn false\n\t}\n\n\treturn pkg.Equals(devpkg.PackageFromStringWithDefaults(i.unlockedReference, locker))\n}\n\nfunc (i *NixProfileListItem) MatchesUnlockedReference(installable string) bool {\n\treturn i.unlockedReference == installable\n}\n\nfunc (i *NixProfileListItem) addedByStorePath() bool {\n\treturn i.unlockedReference == \"\"\n}\n\n// String serializes the NixProfileListItem for debuggability\nfunc (i *NixProfileListItem) String() string {\n\treturn fmt.Sprintf(\"{nameOrIndex:%s unlockedRef:%s lockedRef:%s, nixStorePaths:%s}\",\n\t\ti.NameOrIndex(),\n\t\ti.unlockedReference,\n\t\ti.lockedReference,\n\t\ti.nixStorePaths,\n\t)\n}\n\nfunc (i *NixProfileListItem) StorePaths() []string {\n\treturn i.nixStorePaths\n}\n\n// NameOrIndex is a helper method to get the name of the package if it exists, or the index if it doesn't.\n// `nix profile` subcommands `list`, `remove`, and `upgrade` use either name (nix >= 2.20) or index (nix < 2.20)\n// to identify the package.\nfunc (i *NixProfileListItem) NameOrIndex() string {\n\tif i.name != \"\" {\n\t\treturn i.name\n\t}\n\treturn fmt.Sprintf(\"%d\", i.index)\n}\n"
  },
  {
    "path": "internal/nix/nixprofile/profile.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nixprofile\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/redact\"\n)\n\n// ProfileListItems returns a list of the installed packages.\nfunc ProfileListItems(\n\twriter io.Writer,\n\tprofileDir string,\n) ([]*NixProfileListItem, error) {\n\tdefer debug.FunctionTimer().End()\n\toutput, err := nix.ProfileList(writer, profileDir, true /*useJSON*/)\n\tif err != nil {\n\t\t// fallback to legacy profile list\n\t\t// NOTE: maybe we should check the nix version first, instead of falling back on _any_ error.\n\t\treturn profileListLegacy(writer, profileDir)\n\t}\n\n\ttype ProfileListElement struct {\n\t\tActive      bool     `json:\"active\"`\n\t\tAttrPath    string   `json:\"attrPath\"`\n\t\tOriginalURL string   `json:\"originalUrl\"`\n\t\tPriority    int      `json:\"priority\"`\n\t\tStorePaths  []string `json:\"storePaths\"`\n\t\tURL         string   `json:\"url\"`\n\t}\n\ttype ProfileListOutput struct {\n\t\tElements map[string]ProfileListElement `json:\"elements\"`\n\t\tVersion  int                           `json:\"version\"`\n\t}\n\n\t// Modern nix profiles: nix >= 2.20\n\tvar structOutput ProfileListOutput\n\tif err := json.Unmarshal([]byte(output), &structOutput); err == nil {\n\t\titems := []*NixProfileListItem{}\n\t\tfor name, element := range structOutput.Elements {\n\t\t\titems = append(items, &NixProfileListItem{\n\t\t\t\tname:              name,\n\t\t\t\tunlockedReference: lo.Ternary(element.OriginalURL != \"\", element.OriginalURL+\"#\"+element.AttrPath, \"\"),\n\t\t\t\tlockedReference:   lo.Ternary(element.URL != \"\", element.URL+\"#\"+element.AttrPath, \"\"),\n\t\t\t\tnixStorePaths:     element.StorePaths,\n\t\t\t})\n\t\t}\n\t\treturn items, nil\n\t}\n\t// Fall back to trying format for nix < version 2.20\n\n\t// ProfileListOutputJSONLegacy is for parsing `nix profile list --json` in nix < version 2.20\n\t// that relied on index instead of name for each package installed.\n\ttype ProfileListOutputJSONLegacy struct {\n\t\tElements []ProfileListElement `json:\"elements\"`\n\t\tVersion  int                  `json:\"version\"`\n\t}\n\tvar structOutput2 ProfileListOutputJSONLegacy\n\tif err := json.Unmarshal([]byte(output), &structOutput2); err != nil {\n\t\treturn nil, err\n\t}\n\titems := []*NixProfileListItem{}\n\tfor index, element := range structOutput2.Elements {\n\t\titems = append(items, &NixProfileListItem{\n\t\t\tindex:             index,\n\t\t\tunlockedReference: lo.Ternary(element.OriginalURL != \"\", element.OriginalURL+\"#\"+element.AttrPath, \"\"),\n\t\t\tlockedReference:   lo.Ternary(element.URL != \"\", element.URL+\"#\"+element.AttrPath, \"\"),\n\t\t\tnixStorePaths:     element.StorePaths,\n\t\t})\n\t}\n\n\treturn items, nil\n}\n\n// profileListLegacy lists the items in a nix profile before nix 2.17.0 introduced --json.\nfunc profileListLegacy(\n\twriter io.Writer,\n\tprofileDir string,\n) ([]*NixProfileListItem, error) {\n\toutput, err := nix.ProfileList(writer, profileDir, false /*useJSON*/)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\tlines := strings.Split(output, \"\\n\")\n\n\t// The `line` output is of the form:\n\t// <index> <UnlockedReference> <LockedReference> <NixStorePath>\n\t//\n\t// Using an example:\n\t// 0 github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.go_1_19 github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.go_1_19 /nix/store/w0lyimyyxxfl3gw40n46rpn1yjrl3q85-go-1.19.3\n\t// 1 github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.vim github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.vim /nix/store/gapbqxx1d49077jk8ay38z11wgr12p23-vim-9.0.0609\n\n\titems := []*NixProfileListItem{}\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\titem, err := parseNixProfileListItemLegacy(line)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\titems = append(items, item)\n\t}\n\treturn items, nil\n}\n\ntype ProfileListNameOrIndexArgs struct {\n\t// For performance, you can reuse the same list in multiple operations if you\n\t// are confident index has not changed.\n\tItems      []*NixProfileListItem\n\tLockfile   *lock.File\n\tWriter     io.Writer\n\tPackage    *devpkg.Package\n\tProfileDir string\n}\n\n// ProfileListNameOrIndex returns the name or index of args.Package in the nix profile specified by args.ProfileDir,\n// or nix.ErrPackageNotFound if it's not found. Callers can pass in args.Items to avoid having to call `nix-profile list` again.\nfunc ProfileListNameOrIndex(args *ProfileListNameOrIndexArgs) (string, error) {\n\tvar err error\n\titems := args.Items\n\tif items == nil {\n\t\titems, err = ProfileListItems(args.Writer, args.ProfileDir)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tinCache, err := args.Package.IsInBinaryCache()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif !inCache && args.Package.IsDevboxPackage {\n\t\t// This is an optimization for happy path when packages are added by flake reference. A resolved devbox\n\t\t// package *which was added by flake reference* (not by store path) should match the unlockedReference\n\t\t// of an existing profile item.\n\t\tref, err := args.Package.NormalizedDevboxPackageReference()\n\t\tif err != nil {\n\t\t\treturn \"\", errors.Wrapf(err, \"failed to get installable for %s\", args.Package.String())\n\t\t}\n\n\t\tfor _, item := range items {\n\t\t\tif ref == item.unlockedReference {\n\t\t\t\treturn item.NameOrIndex(), nil\n\t\t\t}\n\t\t}\n\t\treturn \"\", errors.Wrap(nix.ErrPackageNotFound, args.Package.String())\n\t}\n\n\tfor _, item := range items {\n\t\tif item.Matches(args.Package, args.Lockfile) {\n\t\t\treturn item.NameOrIndex(), nil\n\t\t}\n\t}\n\treturn \"\", errors.Wrap(nix.ErrPackageNotFound, args.Package.String())\n}\n\n// parseNixProfileListItemLegacy reads each line of output (from `nix profile list`) and converts\n// into a golang struct. Refer to NixProfileListItem struct definition for explanation of each field.\n// NOTE: this API is for legacy nix. Newer nix versions use --json output.\nfunc parseNixProfileListItemLegacy(line string) (*NixProfileListItem, error) {\n\tscanner := bufio.NewScanner(strings.NewReader(line))\n\tscanner.Split(bufio.ScanWords)\n\n\tif !scanner.Scan() {\n\t\treturn nil, redact.Errorf(\"error parsing \\\"nix profile list\\\" output: line is missing index: %s\", line)\n\t}\n\n\tindex, err := strconv.Atoi(scanner.Text())\n\tif err != nil {\n\t\treturn nil, redact.Errorf(\"error parsing \\\"nix profile list\\\" output: %w: %s\", err, line)\n\t}\n\n\tif !scanner.Scan() {\n\t\treturn nil, redact.Errorf(\"error parsing \\\"nix profile list\\\" output: line is missing unlockedReference: %s\", line)\n\t}\n\tunlockedReference := scanner.Text()\n\n\tif !scanner.Scan() {\n\t\treturn nil, redact.Errorf(\"error parsing \\\"nix profile list\\\" output: line is missing lockedReference: %s\", line)\n\t}\n\tlockedReference := scanner.Text()\n\n\tif !scanner.Scan() {\n\t\treturn nil, redact.Errorf(\"error parsing \\\"nix profile list\\\" output: line is missing nixStorePaths: %s\", line)\n\t}\n\tnixStorePaths := strings.Fields(scanner.Text())\n\n\treturn &NixProfileListItem{\n\t\tindex:             index,\n\t\tunlockedReference: unlockedReference,\n\t\tlockedReference:   lockedReference,\n\t\tnixStorePaths:     nixStorePaths,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/nix/nixprofile/profile_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nixprofile\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n)\n\ntype expectedTestData struct {\n\titem        *NixProfileListItem\n\tattrPath    string\n\tpackageName string\n}\n\n// TestNixProfileListItemLegacy tests the parsing of legacy nix profile list items.\n// It only applies to much older nix versions. Newer nix versions rely on the --json output\n// instead parsing the legacy output.\nfunc TestNixProfileListItemLegacy(t *testing.T) {\n\ttestCases := map[string]struct {\n\t\tline     string\n\t\texpected expectedTestData\n\t}{\n\t\t\"go_1_19\": {\n\t\t\tline: fmt.Sprintf(\n\t\t\t\t\"%d %s %s %s\",\n\t\t\t\t0,\n\t\t\t\t\"flake:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.go_1_19\",\n\t\t\t\t\"github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.go_1_19\",\n\t\t\t\t\"/nix/store/w0lyimyyxxfl3gw40n46rpn1yjrl3q85-go-1.19.3\",\n\t\t\t),\n\t\t\texpected: expectedTestData{\n\t\t\t\titem: &NixProfileListItem{\n\t\t\t\t\tindex:             0,\n\t\t\t\t\tunlockedReference: \"flake:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.go_1_19\",\n\t\t\t\t\tlockedReference:   \"github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.go_1_19\",\n\t\t\t\t\tnixStorePaths:     []string{\"/nix/store/w0lyimyyxxfl3gw40n46rpn1yjrl3q85-go-1.19.3\"},\n\t\t\t\t},\n\t\t\t\tattrPath:    \"legacyPackages.x86_64-darwin.go_1_19\",\n\t\t\t\tpackageName: \"go_1_19\",\n\t\t\t},\n\t\t},\n\t\t\"numpy\": {\n\t\t\tline: fmt.Sprintf(\"%d %s %s %s\",\n\t\t\t\t2,\n\t\t\t\t\"github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.python39Packages.numpy\",\n\t\t\t\t\"github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.\"+\n\t\t\t\t\t\"python39Packages.numpy \",\n\t\t\t\t\"/nix/store/qly36iy1p4q1h5p4rcbvsn3ll0zsd9pd-python3.9-numpy-1.23.3\",\n\t\t\t),\n\t\t\texpected: expectedTestData{\n\t\t\t\titem: &NixProfileListItem{\n\t\t\t\t\tindex:             2,\n\t\t\t\t\tunlockedReference: \"github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.python39Packages.numpy\",\n\t\t\t\t\tlockedReference:   \"github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.python39Packages.numpy\",\n\t\t\t\t\tnixStorePaths:     []string{\"/nix/store/qly36iy1p4q1h5p4rcbvsn3ll0zsd9pd-python3.9-numpy-1.23.3\"},\n\t\t\t\t},\n\t\t\t\tattrPath:    \"legacyPackages.x86_64-darwin.python39Packages.numpy\",\n\t\t\t\tpackageName: \"python39Packages.numpy\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor name, testCase := range testCases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttestItem(t, testCase.line, testCase.expected)\n\t\t})\n\t}\n}\n\nfunc testItem(t *testing.T, line string, expected expectedTestData) {\n\titem, err := parseNixProfileListItemLegacy(line)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error %v\", err)\n\t}\n\tif item == nil {\n\t\tt.Fatalf(\"expected NixProfileListItem to be non-nil\")\n\t}\n\n\tif !reflect.DeepEqual(item, expected.item) {\n\t\tt.Fatalf(\"expected parsed NixProfileListItem to be %s but got %s\",\n\t\t\texpected.item,\n\t\t\titem,\n\t\t)\n\t}\n\n\tgotAttrPath, err := item.AttributePath()\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error %v\", err)\n\t}\n\tif gotAttrPath != expected.attrPath {\n\t\tt.Errorf(\"expected attribute path %s but got %s\", expected.attrPath, gotAttrPath)\n\t}\n}\n"
  },
  {
    "path": "internal/nix/nixprofile/upgrade.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nixprofile\n\nimport (\n\t\"os\"\n\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/nix\"\n)\n\nfunc ProfileUpgrade(ProfileDir string, pkg *devpkg.Package, lock *lock.File) error {\n\tnameOrIndex, err := ProfileListNameOrIndex(\n\t\t&ProfileListNameOrIndexArgs{\n\t\t\tLockfile:   lock,\n\t\t\tWriter:     os.Stderr,\n\t\t\tPackage:    pkg,\n\t\t\tProfileDir: ProfileDir,\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nix.ProfileUpgrade(ProfileDir, nameOrIndex)\n}\n"
  },
  {
    "path": "internal/nix/profiles.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nix\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/redact\"\n)\n\nfunc ProfileList(writer io.Writer, profilePath string, useJSON bool) (string, error) {\n\tcmd := Command(\"profile\", \"list\", \"--profile\", profilePath)\n\tif useJSON {\n\t\tcmd.Args = append(cmd.Args, \"--json\")\n\t}\n\tout, err := cmd.Output(context.TODO())\n\tif err != nil {\n\t\treturn \"\", redact.Errorf(\"error running \\\"nix profile list\\\": %w\", err)\n\t}\n\treturn string(out), nil\n}\n\ntype ProfileInstallArgs struct {\n\tInstallables []string\n\tProfilePath  string\n\tWriter       io.Writer\n}\n\nvar ErrPriorityConflict = errors.New(\"priority conflict\")\n\nfunc ProfileInstall(ctx context.Context, args *ProfileInstallArgs) error {\n\tdefer debug.FunctionTimer().End()\n\n\tcmd := Command(\n\t\t\"profile\", \"install\",\n\t\t\"--profile\", args.ProfilePath,\n\t\t\"--offline\", // makes it faster. Package is already in store\n\t\t\"--impure\",  // for NIXPKGS_ALLOW_UNFREE\n\t\t// Using an arbitrary priority to avoid conflicts with other packages.\n\t\t// Note that this is not really the priority we care about, since we\n\t\t// use the flake.nix to specify the priority.\n\t\t\"--priority\", nextPriority(args.ProfilePath),\n\t)\n\n\tFixInstallableArgs(args.Installables)\n\tcmd.Args = appendArgs(cmd.Args, args.Installables)\n\tcmd.Env = allowUnfreeEnv(os.Environ())\n\n\t// We used to attach this function to stdout and in in order to get the more interactive output.\n\t// However, now we do the building in nix.Build, by the time we install in profile everything\n\t// should already be in the store. We need to capture the output so we can decide if a conflict\n\t// happened.\n\tout, err := cmd.CombinedOutput(ctx)\n\tif bytes.Contains(out, []byte(\"error: An existing package already provides the following file\")) {\n\t\treturn ErrPriorityConflict\n\t}\n\treturn err\n}\n\n// ProfileRemove removes packages from a profile.\n// WARNING, don't use indexes, they are not supported by nix 2.20+\nfunc ProfileRemove(profilePath string, packageNames ...string) error {\n\tdefer debug.FunctionTimer().End()\n\tcmd := Command(\n\t\t\"profile\", \"remove\",\n\t\t\"--profile\", profilePath,\n\t\t\"--impure\", // for NIXPKGS_ALLOW_UNFREE\n\t)\n\n\tFixInstallableArgs(packageNames)\n\tcmd.Args = appendArgs(cmd.Args, packageNames)\n\tcmd.Env = allowUnfreeEnv(allowInsecureEnv(os.Environ()))\n\treturn cmd.Run(context.TODO())\n}\n\ntype manifest struct {\n\tElements []struct {\n\t\tPriority int\n\t}\n}\n\nfunc readManifest(profilePath string) (manifest, error) {\n\tdata, err := os.ReadFile(filepath.Join(profilePath, \"manifest.json\"))\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn manifest{}, nil\n\t}\n\tif err != nil {\n\t\treturn manifest{}, err\n\t}\n\n\ttype manifestModern struct {\n\t\tElements map[string]struct {\n\t\t\tPriority int `json:\"priority\"`\n\t\t} `json:\"elements\"`\n\t}\n\tvar modernMani manifestModern\n\tif err := json.Unmarshal(data, &modernMani); err == nil {\n\t\t// Convert to the result format\n\t\tresult := manifest{}\n\t\tfor _, e := range modernMani.Elements {\n\t\t\tresult.Elements = append(result.Elements, struct{ Priority int }{e.Priority})\n\t\t}\n\t\treturn result, nil\n\t}\n\n\ttype manifestLegacy struct {\n\t\tElements []struct {\n\t\t\tPriority int `json:\"priority\"`\n\t\t} `json:\"elements\"`\n\t}\n\tvar legacyMani manifestLegacy\n\tif err := json.Unmarshal(data, &legacyMani); err != nil {\n\t\treturn manifest{}, err\n\t}\n\n\t// Convert to the result format\n\tresult := manifest{}\n\tfor _, e := range legacyMani.Elements {\n\t\tresult.Elements = append(result.Elements, struct{ Priority int }{e.Priority})\n\t}\n\treturn result, nil\n}\n\nconst DefaultPriority = 5\n\nfunc nextPriority(profilePath string) string {\n\t// error is ignored because it's ok if the file doesn't exist\n\tm, _ := readManifest(profilePath)\n\tmax := DefaultPriority\n\tfor _, e := range m.Elements {\n\t\tif e.Priority > max {\n\t\t\tmax = e.Priority\n\t\t}\n\t}\n\t// Each subsequent package gets a lower priority. This matches how flake.nix\n\t// behaves\n\treturn fmt.Sprintf(\"%d\", max+1)\n}\n"
  },
  {
    "path": "internal/nix/run.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nix\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/cmdutil\"\n)\n\nfunc RunScript(projectDir, cmdWithArgs string, env map[string]string) error {\n\tif cmdWithArgs == \"\" {\n\t\treturn errors.New(\"attempted to run an empty command or script\")\n\t}\n\n\tenvPairs := []string{}\n\tfor k, v := range env {\n\t\tenvPairs = append(envPairs, fmt.Sprintf(\"%s=%s\", k, v))\n\t}\n\n\t// Try to find sh in the PATH, if not, default to a well known absolute path.\n\tshPath := cmdutil.GetPathOrDefault(\"sh\", \"/bin/sh\")\n\tcmd := exec.Command(shPath, \"-c\", cmdWithArgs)\n\tcmd.Env = envPairs\n\tcmd.Dir = projectDir\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tslog.Debug(\"executing script\", \"cmd\", cmd.Args)\n\t// Report error as exec error when executing scripts.\n\treturn usererr.NewExecError(cmd.Run())\n}\n"
  },
  {
    "path": "internal/nix/search.go",
    "content": "package nix\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/xdg\"\n\t\"go.jetify.com/pkg/filecache\"\n)\n\nvar (\n\tErrPackageNotFound     = errors.New(\"package not found\")\n\tErrPackageNotInstalled = errors.New(\"package not installed\")\n)\n\ntype PkgInfo struct {\n\t// attribute key is different in flakes vs legacy so we should only use it\n\t// if we know exactly which version we are using\n\tAttributeKey string `json:\"attribute\"`\n\tPName        string `json:\"pname\"`\n\tSummary      string `json:\"summary\"`\n\tVersion      string `json:\"version\"`\n}\n\nfunc (i *PkgInfo) String() string {\n\treturn fmt.Sprintf(\"%s-%s\", i.PName, i.Version)\n}\n\nfunc Search(url string) (map[string]*PkgInfo, error) {\n\tif strings.HasPrefix(url, \"runx:\") {\n\t\t// TODO implement runx search. Also, move this check outside this function: nix package\n\t\t// should not be handling runx logic.\n\t\treturn map[string]*PkgInfo{}, nil\n\t}\n\treturn searchSystem(url, \"\" /* system */)\n}\n\nfunc parseSearchResults(data []byte) map[string]*PkgInfo {\n\tvar results map[string]map[string]any\n\terr := json.Unmarshal(data, &results)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tinfos := map[string]*PkgInfo{}\n\tfor key, result := range results {\n\t\tinfos[key] = &PkgInfo{\n\t\t\tAttributeKey: key,\n\t\t\tPName:        result[\"pname\"].(string),\n\t\t\tVersion:      result[\"version\"].(string),\n\t\t}\n\t}\n\treturn infos\n}\n\n// PkgExistsForAnySystem is a bit slow (~600ms). Only use it if there's already\n// been an error and we want to provide a better error message.\nfunc PkgExistsForAnySystem(pkg string) bool {\n\tsystems := []string{\n\t\t// Check most common systems first.\n\t\t\"x86_64-linux\",\n\t\t\"x86_64-darwin\",\n\t\t\"aarch64-linux\",\n\t\t\"aarch64-darwin\",\n\n\t\t\"armv5tel-linux\",\n\t\t\"armv6l-linux\",\n\t\t\"armv7l-linux\",\n\t\t\"i686-linux\",\n\t\t\"mipsel-linux\",\n\t\t\"powerpc64le-linux\",\n\t\t\"riscv64-linux\",\n\t}\n\tfor _, system := range systems {\n\t\tresults, _ := searchSystem(pkg, system)\n\t\tif len(results) > 0 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc searchSystem(url, system string) (map[string]*PkgInfo, error) {\n\t// Eventually we may pass a writer here, but for now it is safe to use stderr\n\twriter := os.Stderr\n\t// Search will download nixpkgs if it's not already downloaded. Adding this\n\t// check here provides a slightly better UX.\n\tif IsGithubNixpkgsURL(url) {\n\t\thash := HashFromNixPkgsURL(url)\n\t\t// purposely ignore error here. The function already prints an error.\n\t\t// We don't want to panic or stop execution if we can't prefetch.\n\t\t_ = EnsureNixpkgsPrefetched(writer, hash)\n\t}\n\n\t// The `^` is added to indicate we want to show all packages\n\tcmd := Command(\"search\", url, \"^\" /*regex*/, \"--json\")\n\tif system != \"\" {\n\t\tcmd.Args = append(cmd.Args, \"--system\", system)\n\t}\n\tout, err := cmd.Output(context.TODO())\n\tif err != nil {\n\t\t// for now, assume all errors are invalid packages.\n\t\t// TODO: check the error string for \"did not find attribute\" and\n\t\t// return ErrPackageNotFound only for that case.\n\t\treturn nil, fmt.Errorf(\"error searching for pkg %s: %w\", url, err)\n\t}\n\tparsed := parseSearchResults(out)\n\tif len(parsed) == 0 {\n\t\treturn nil, fmt.Errorf(\"package not found: %s\", url)\n\t}\n\treturn parsed, nil\n}\n\n// allowableQuery specifies the regex that queries for SearchNixpkgsAttribute must match.\nvar allowableQuery = regexp.MustCompile(\"^github:NixOS/nixpkgs/[0-9a-f]{40}#[^#]+$\")\n\n// SearchNixpkgsAttribute is a wrapper around searchSystem that caches results.\n// NOTE: we should be very conservative in where we use this function. `nix search`\n// accepts generalized `installable regex` as arguments but is slow. For certain\n// queries of the form `nixpkgs/<commit-hash>#attribute`, we can know for sure that\n// once `nix search` returns a valid result, it will always be the very same result.\n// Hence we can cache it locally and answer future queries fast, by not calling `nix search`.\nfunc SearchNixpkgsAttribute(query string) (map[string]*PkgInfo, error) {\n\tif !allowableQuery.MatchString(query) {\n\t\treturn nil, errors.Errorf(\"invalid query: %s, must match regex: %s\", query, allowableQuery)\n\t}\n\n\tkey := cacheKey(query)\n\n\t// Check if the query was already cached, and return the result if so\n\tcache := filecache.New(\n\t\t\"devbox/nix\",\n\t\tfilecache.WithCacheDir[map[string]*PkgInfo](xdg.CacheSubpath(\"\")),\n\t)\n\n\tif results, err := cache.Get(key); err == nil {\n\t\treturn results, nil\n\t} else if !filecache.IsCacheMiss(err) {\n\t\treturn nil, err // genuine error\n\t}\n\n\t// If not cached, or an update is needed, then call searchSystem\n\tinfos, err := searchSystem(query, \"\" /*system*/)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Save the results to the cache\n\t// TODO savil: add a SetForever API that does not expire. Time based expiration is not needed here\n\t// because we're caching results that are guaranteed to be stable.\n\t// TODO savil: Make filecache.cache a public struct so it can be passed into other functions\n\tconst oneYear = 12 * 30 * 24 * time.Hour\n\tif err := cache.Set(key, infos, oneYear); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn infos, nil\n}\n\n// cacheKey sanitizes the search query to be a valid unix filename.\n// This cache key is used as the filename to store the cache value, and having a\n// representation of the query is important for debuggability.\nfunc cacheKey(query string) string {\n\t// Replace disallowed characters with underscores.\n\tre := regexp.MustCompile(`[:/#@+]`)\n\tsanitized := re.ReplaceAllString(query, \"_\")\n\n\t// Remove any remaining invalid characters.\n\tsanitized = regexp.MustCompile(`[^\\w\\.-]`).ReplaceAllString(sanitized, \"\")\n\n\t// Ensure the filename doesn't exceed the maximum length.\n\tconst maxLen = 255\n\tif len(sanitized) > maxLen {\n\t\tsanitized = sanitized[:maxLen]\n\t}\n\n\treturn sanitized\n}\n"
  },
  {
    "path": "internal/nix/search_test.go",
    "content": "package nix\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestSearchCacheKey(t *testing.T) {\n\ttestCases := []struct {\n\t\tin  string\n\t\tout string\n\t}{\n\t\t{\n\t\t\t\"github:NixOS/nixpkgs/8670e496ffd093b60e74e7fa53526aa5920d09eb#go_1_19\",\n\t\t\t\"github_NixOS_nixpkgs_8670e496ffd093b60e74e7fa53526aa5920d09eb_go_1_19\",\n\t\t},\n\t\t{\n\t\t\t\"github:nixos/nixpkgs/7d0ed7f2e5aea07ab22ccb338d27fbe347ed2f11#emacsPackages.@\",\n\t\t\t\"github_nixos_nixpkgs_7d0ed7f2e5aea07ab22ccb338d27fbe347ed2f11_emacsPackages._\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.out, func(t *testing.T) {\n\t\t\tout := cacheKey(testCase.in)\n\t\t\tif out != testCase.out {\n\t\t\t\tt.Errorf(\"got %s, want %s\", out, testCase.out)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAllowableQuery(t *testing.T) {\n\ttestCases := []struct {\n\t\tin       string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t\"github:NixOS/nixpkgs/8670e496ffd093b60e74e7fa53526aa5920d09eb#go_1_19\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"github:NixOS/nixpkgs/8670e496ffd093b60e74e7fa53526aa5920d09eb\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"github:NixOS/nixpkgs/8670e496ffd093b60e74e7fa53526aa5920d09eb#\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"github:NixOS/nixpkgs/nixpkgs-unstable#go_1_19\",\n\t\t\tfalse,\n\t\t},\n\t}\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.in, func(t *testing.T) {\n\t\t\tout := allowableQuery.MatchString(testCase.in)\n\t\t\tif out != testCase.expected {\n\t\t\t\tt.Errorf(\"got %t, want %t\", out, testCase.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseSearchResults(t *testing.T) {\n\ttestCases := []struct {\n\t\tname           string\n\t\tinput          []byte\n\t\texpectedResult map[string]*PkgInfo\n\t}{\n\t\t{\n\t\t\tname: \"Valid JSON input\",\n\t\t\tinput: []byte(`{\n\t\t\t\t\"go\": {\n\t\t\t\t\t\"pname\": \"go\",\n\t\t\t\t\t\"version\": \"1.20.4\"\n\t\t\t\t},\n\t\t\t\t\"python3\": {\n\t\t\t\t\t\"pname\": \"python3\",\n\t\t\t\t\t\"version\": \"3.9.16\"\n\t\t\t\t}\n\t\t\t}`),\n\t\t\texpectedResult: map[string]*PkgInfo{\n\t\t\t\t\"go\": {\n\t\t\t\t\tAttributeKey: \"go\",\n\t\t\t\t\tPName:        \"go\",\n\t\t\t\t\tVersion:      \"1.20.4\",\n\t\t\t\t},\n\t\t\t\t\"python3\": {\n\t\t\t\t\tAttributeKey: \"python3\",\n\t\t\t\t\tPName:        \"python3\",\n\t\t\t\t\tVersion:      \"3.9.16\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"Empty JSON input\",\n\t\t\tinput:          []byte(`{}`),\n\t\t\texpectedResult: map[string]*PkgInfo{},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := parseSearchResults(tc.input)\n\n\t\t\tif !reflect.DeepEqual(result, tc.expectedResult) {\n\t\t\t\tt.Errorf(\"Expected result %v, got %v\", tc.expectedResult, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/nix/shim.go",
    "content": "package nix\n\nimport (\n\t\"go.jetify.com/devbox/nix\"\n)\n\n// The types and functions in this file act a shim for the non-internal version\n// of this package (go.jetify.com/devbox/nix). That way callers don't need to\n// import two nix packages and alias one of them.\n\nconst (\n\tVersion2_12 = nix.Version2_12\n\tVersion2_13 = nix.Version2_13\n\tVersion2_14 = nix.Version2_14\n\tVersion2_15 = nix.Version2_15\n\tVersion2_16 = nix.Version2_16\n\tVersion2_17 = nix.Version2_17\n\tVersion2_18 = nix.Version2_18\n\tVersion2_19 = nix.Version2_19\n\tVersion2_20 = nix.Version2_20\n\tVersion2_21 = nix.Version2_21\n\tVersion2_22 = nix.Version2_22\n\tVersion2_23 = nix.Version2_23\n\tVersion2_24 = nix.Version2_24\n\tVersion2_25 = nix.Version2_25\n\n\tMinVersion = nix.Version2_18\n)\n\ntype (\n\tNix       = nix.Nix\n\tCmd       = nix.Cmd\n\tArgs      = nix.Args\n\tInfo      = nix.Info\n\tInstaller = nix.Installer\n)\n\nvar Default = nix.Default\n\nfunc AtLeast(version string) bool              { return nix.AtLeast(version) }\nfunc Command(args ...any) *Cmd                 { return nix.Command(args...) }\nfunc SourceProfile() (sourced bool, err error) { return nix.SourceProfile() }\nfunc System() string                           { return nix.System() }\nfunc Version() string                          { return nix.Version() }\n"
  },
  {
    "path": "internal/nix/store.go",
    "content": "package nix\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/redact\"\n\t\"go.jetify.com/devbox/nix\"\n\t\"golang.org/x/exp/maps\"\n)\n\nfunc StorePathFromHashPart(ctx context.Context, hash, storeAddr string) (string, error) {\n\tcmd := Command(\"store\", \"path-from-hash-part\", \"--store\", storeAddr, hash)\n\tresultBytes, err := cmd.Output(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(string(resultBytes)), nil\n}\n\nfunc StorePathsFromInstallable(ctx context.Context, installable string, allowInsecure bool) ([]string, error) {\n\tdefer debug.FunctionTimer().End()\n\n\t// --impure for NIXPKGS_ALLOW_UNFREE\n\tcmd := Command(\"path-info\", FixInstallableArg(installable), \"--json\", \"--impure\")\n\tcmd.Env = allowUnfreeEnv(os.Environ())\n\n\tif allowInsecure {\n\t\tslog.Debug(\"Setting Allow-insecure env-var\\n\")\n\t\tcmd.Env = allowInsecureEnv(cmd.Env)\n\t}\n\n\tresultBytes, err := cmd.Output(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpaths, err := parseStorePathFromInstallableOutput(resultBytes)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse path-info for %s: %w\", installable, err)\n\t}\n\n\treturn maps.Keys(paths), nil\n}\n\n// StorePathsAreInStore a map of store paths to whether they are in the store.\nfunc StorePathsAreInStore(ctx context.Context, storePaths []string) (map[string]bool, error) {\n\tdefer debug.FunctionTimer().End()\n\tif len(storePaths) == 0 {\n\t\treturn map[string]bool{}, nil\n\t}\n\tcmd := Command(\"path-info\", \"--offline\", \"--json\")\n\tcmd.Args = appendArgs(cmd.Args, storePaths)\n\toutput, err := cmd.Output(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn parseStorePathFromInstallableOutput(output)\n}\n\n// Older nix versions (like 2.17) are an array of objects that contain path and valid fields\ntype LegacyPathInfo struct {\n\tPath  string `json:\"path\"`\n\tValid bool   `json:\"valid\"` // this means path is in store\n}\n\n// parseStorePathFromInstallableOutput parses the output of `nix store path-from-installable --json`\n// into a map of store paths to whether they are in the store.\n// This function is decomposed out of StorePathFromInstallable to make it testable.\nfunc parseStorePathFromInstallableOutput(output []byte) (map[string]bool, error) {\n\tresult := map[string]bool{}\n\n\t// Newer nix versions (like 2.20) have output of the form\n\t// {\"<store-path>\": {}}\n\t// Note that values will be null if paths are not in store.\n\tvar modernPathInfo map[string]any\n\tif err := json.Unmarshal(output, &modernPathInfo); err == nil {\n\t\tfor path, val := range modernPathInfo {\n\t\t\tresult[path] = val != nil\n\t\t}\n\t\treturn result, nil\n\t}\n\n\tvar legacyPathInfos []LegacyPathInfo\n\n\tif err := json.Unmarshal(output, &legacyPathInfos); err == nil {\n\t\tfor _, outValue := range legacyPathInfos {\n\t\t\tresult[outValue.Path] = outValue.Valid\n\t\t}\n\t\treturn result, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"failed to parse path-info output: %s\", output)\n}\n\n// DaemonError reports an unsuccessful attempt to connect to the Nix daemon.\ntype DaemonError struct {\n\tcmd    string\n\tstderr []byte\n\terr    error\n}\n\nfunc (e *DaemonError) Error() string {\n\tif len(e.stderr) != 0 {\n\t\treturn e.Redact() + \": \" + string(e.stderr)\n\t}\n\treturn e.Redact()\n}\n\nfunc (e *DaemonError) Unwrap() error {\n\treturn e.err\n}\n\nfunc (e *DaemonError) Redact() string {\n\t// Don't include e.stderr in redacted messages because it can contain\n\t// things like paths and usernames.\n\tif e.cmd != \"\" {\n\t\treturn fmt.Sprintf(\"command %s: %s\", e.cmd, e.err)\n\t}\n\treturn e.err.Error()\n}\n\n// DaemonVersion returns the version of the currently running Nix daemon.\nfunc DaemonVersion(ctx context.Context) (string, error) {\n\tstoreCmd := \"ping\"\n\tif nix.AtLeast(nix.Version2_19) {\n\t\t// \"nix store ping\" is deprecated as of 2.19 in favor of\n\t\t// \"nix store info\".\n\t\tstoreCmd = \"info\"\n\t}\n\tcanJSON := nix.AtLeast(nix.Version2_14)\n\n\tcmd := Command(\"store\", storeCmd, \"--store\", \"daemon\")\n\tif canJSON {\n\t\tcmd.Args = append(cmd.Args, \"--json\")\n\t}\n\tout, err := cmd.Output(ctx)\n\n\t// ExitError means the command ran, but couldn't connect.\n\tvar exitErr *exec.ExitError\n\tif errors.As(err, &exitErr) {\n\t\treturn \"\", &DaemonError{\n\t\t\tcmd:    cmd.String(),\n\t\t\tstderr: exitErr.Stderr,\n\t\t\terr:    err,\n\t\t}\n\t}\n\n\t// All other errors mean we couldn't launch the Nix CLI (either it is\n\t// missing or not executable).\n\tif err != nil {\n\t\treturn \"\", redact.Errorf(\"command %s: %s\", redact.Safe(cmd), err)\n\t}\n\n\tif len(out) == 0 {\n\t\treturn \"\", redact.Errorf(\"command %s: empty output\", redact.Safe(cmd), err)\n\t}\n\tif canJSON {\n\t\tinfo := struct{ Version string }{}\n\t\tif err := json.Unmarshal(out, &info); err != nil {\n\t\t\treturn \"\", redact.Errorf(\"command %s: unmarshal JSON output: %s\", redact.Safe(cmd), err)\n\t\t}\n\t\treturn info.Version, nil\n\t}\n\n\t// Example output:\n\t//\n\t// Store URL: daemon\n\t// Version: 2.21.1\n\tlines := strings.Split(string(out), \"\\n\")\n\tfor _, line := range lines {\n\t\tname, value, found := strings.Cut(line, \": \")\n\t\tif found && name == \"Version\" {\n\t\t\treturn value, nil\n\t\t}\n\t}\n\treturn \"\", redact.Errorf(\"parse nix daemon version: %s\", redact.Safe(lines[0]))\n}\n"
  },
  {
    "path": "internal/nix/store_test.go",
    "content": "package nix\n\nimport (\n\t\"testing\"\n\n\t\"golang.org/x/exp/maps\"\n)\n\nfunc TestParseStorePathFromInstallableOutput(t *testing.T) {\n\ttestCases := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected map[string]bool\n\t}{\n\t\t{\n\t\t\tname: \"go-basic-nix-2-20-1\",\n\t\t\t// snipped the actual output for brevity. We mainly care about the first key in the JSON.\n\t\t\tinput: `{\"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0\":{\"deriver\":\"/nix/store/clr3bm8njqysvyw4r4x4xmldhz4knrff-go-1.22.0.drv\"}}`,\n\t\t\texpected: map[string]bool{\n\t\t\t\t\"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0\": true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"go-basic-nix-2-20-1\",\n\t\t\t// snipped the actual output for brevity. We mainly care about the first key in the JSON.\n\t\t\tinput: `{\"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0\":null}`,\n\t\t\texpected: map[string]bool{\n\t\t\t\t\"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0\": false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"go-basic-nix-2-17-0\",\n\t\t\tinput: `[{\"path\":\"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0\"}]`,\n\t\t\texpected: map[string]bool{\n\t\t\t\t\"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0\": false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"go-basic-nix-2-17-0\",\n\t\t\tinput: `[{\"path\":\"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0\", \"valid\": true}]`,\n\t\t\texpected: map[string]bool{\n\t\t\t\t\"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0\": true,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tactual, err := parseStorePathFromInstallableOutput([]byte(tc.input))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Expected no error but got error: %s\", err)\n\t\t\t}\n\t\t\tif !maps.Equal(tc.expected, actual) {\n\t\t\t\tt.Errorf(\"Expected store path %v but got %v\", tc.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/nix/storepath.go",
    "content": "package nix\n\nimport (\n\t\"strings\"\n\t\"unicode\"\n)\n\n// StorePathParts are the constituent parts of\n// /nix/store/<hash>-<name>-<version>\n//\n// This is a helper struct for analyzing the string representation\ntype StorePathParts struct {\n\tHash    string\n\tName    string\n\tVersion string\n\tOutput  string\n}\n\n// NewStorePathParts splits a Nix store path into its hash, name and version\n// components in the same way that Nix does.\n//\n// See https://nixos.org/manual/nix/stable/language/builtins.html#builtins-parseDrvName\nfunc NewStorePathParts(path string) StorePathParts {\n\tpath = strings.TrimPrefix(path, \"/nix/store/\")\n\t// path is now <hash>-<name>-<version>[-output]\n\n\thash, name := path[:32], path[33:]\n\tdashIndex := 0\n\tfor i, r := range name {\n\t\tif dashIndex != 0 && !unicode.IsLetter(r) {\n\t\t\tversion, output, _ := strings.Cut(name[i:], \"-\")\n\t\t\treturn StorePathParts{Hash: hash, Name: name[:dashIndex], Version: version, Output: output}\n\t\t}\n\t\tdashIndex = 0\n\t\tif r == '-' {\n\t\t\tdashIndex = i\n\t\t}\n\t}\n\treturn StorePathParts{Hash: hash, Name: name}\n}\n"
  },
  {
    "path": "internal/nix/storepath_test.go",
    "content": "package nix\n\nimport (\n\t\"testing\"\n)\n\nfunc TestStorePathParts(t *testing.T) {\n\ttestCases := []struct {\n\t\tstorePath string\n\t\texpected  StorePathParts\n\t}{\n\t\t// simple case:\n\t\t{\n\t\t\tstorePath: \"/nix/store/cvrn84c1hshv2wcds7n1rhydi6lacqns-gnumake-4.4.1\",\n\t\t\texpected: StorePathParts{\n\t\t\t\tHash:    \"cvrn84c1hshv2wcds7n1rhydi6lacqns\",\n\t\t\t\tName:    \"gnumake\",\n\t\t\t\tVersion: \"4.4.1\",\n\t\t\t},\n\t\t},\n\t\t// the package name can have dashes:\n\t\t{\n\t\t\tstorePath: \"/nix/store/q2xdxsswjqmqcbax81pmazm367s7jzyb-cctools-binutils-darwin-wrapper-973.0.1\",\n\t\t\texpected: StorePathParts{\n\t\t\t\tHash:    \"q2xdxsswjqmqcbax81pmazm367s7jzyb\",\n\t\t\t\tName:    \"cctools-binutils-darwin-wrapper\",\n\t\t\t\tVersion: \"973.0.1\",\n\t\t\t},\n\t\t},\n\t\t// version is optional. This is an artificial example I constructed\n\t\t{\n\t\t\tstorePath: \"/nix/store/gfxwrd5nggc68pjj3g3jhlldim9rpg0p-coreutils\",\n\t\t\texpected: StorePathParts{\n\t\t\t\tHash: \"gfxwrd5nggc68pjj3g3jhlldim9rpg0p\",\n\t\t\t\tName: \"coreutils\",\n\t\t\t},\n\t\t},\n\t\t// With output\n\t\t{\n\t\t\tstorePath: \"/nix/store/0z1zq1zq1zq1zq1zq1zq1zq1zq1zq1zq-foo-1.0.0-bar\",\n\t\t\texpected: StorePathParts{\n\t\t\t\tHash:    \"0z1zq1zq1zq1zq1zq1zq1zq1zq1zq1zq\",\n\t\t\t\tName:    \"foo\",\n\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\tOutput:  \"bar\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.storePath, func(t *testing.T) {\n\t\t\tparts := NewStorePathParts(testCase.storePath)\n\t\t\tif parts != testCase.expected {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", testCase.expected, parts)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/nix/upgrade.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nix\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"go.jetify.com/devbox/internal/ux\"\n\t\"go.jetify.com/devbox/nix\"\n)\n\nfunc ProfileUpgrade(ProfileDir, indexOrName string) error {\n\treturn Command(\n\t\t\"profile\", \"upgrade\",\n\t\t\"--profile\", ProfileDir,\n\t\tindexOrName,\n\t).Run(context.TODO())\n}\n\nfunc FlakeUpdate(ProfileDir string) error {\n\tux.Finfof(os.Stderr, \"Running \\\"nix flake update\\\"\\n\")\n\tcmd := Command(\"flake\", \"update\")\n\tif nix.AtLeast(Version2_19) {\n\t\tcmd.Args = append(cmd.Args, \"--flake\")\n\t}\n\tcmd.Args = append(cmd.Args, ProfileDir)\n\treturn cmd.Run(context.TODO())\n}\n"
  },
  {
    "path": "internal/nix/writer.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage nix\n\nimport (\n\t\"io\"\n\t\"log/slog\"\n\t\"strings\"\n)\n\n// packageInstallIgnore will skip lines that have the strings in the keys of this map.\n// The boolean values inform the writer whether to log the line to debug.Log.\nvar packageInstallIgnore = map[string]bool{\n\t`replacing old 'devbox-development'`: false,\n\t`installing 'devbox-development'`:    false,\n}\n\ntype PackageInstallWriter struct {\n\tio.Writer\n}\n\nfunc (fw *PackageInstallWriter) Write(p []byte) (n int, err error) {\n\tlines := strings.Split(string(p), \"\\n\")\n\tfor _, line := range lines {\n\t\tif line != \"\" && !fw.ignore(line) {\n\t\t\t_, err = io.WriteString(fw.Writer, \"\\t\"+line+\"\\n\")\n\t\t\tif err != nil {\n\t\t\t\treturn n, err\n\t\t\t}\n\t\t}\n\t}\n\treturn len(p), nil\n}\n\nfunc (*PackageInstallWriter) ignore(line string) bool {\n\tfor filter, shouldLog := range packageInstallIgnore {\n\t\tif strings.Contains(line, filter) {\n\t\t\tif shouldLog {\n\t\t\t\tslog.Debug(\"hiding output from user\", \"line\", line)\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/patchpkg/builder.go",
    "content": "// patchpkg patches packages to fix common linker errors.\npackage patchpkg\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"cmp\"\n\t\"context\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"iter\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n//go:embed glibc-patch.bash\nvar glibcPatchScript []byte\n\n// DerivationBuilder patches an existing package.\ntype DerivationBuilder struct {\n\t// Out is the output directory that will contain the built derivation.\n\t// If empty it defaults to $out, which is typically set by Nix.\n\tOut string\n\n\t// Glibc is an optional store path to an alternative glibc version. If\n\t// it's set, the builder will patch ELF binaries to use its shared\n\t// libraries and dynamic linker.\n\tGlibc string\n\n\t// Gcc is an optional store path to an alternative gcc version. If\n\t// it's set, the builder will patch ELF binaries to use its shared\n\t// libraries (such as libstdc++.so).\n\tGcc string\n\n\tglibcPatcher *libPatcher\n\n\tRestoreRefs bool\n\tbytePatches map[string][]fileSlice\n\n\t// src contains the source files of the derivation. For flakes, this is\n\t// anything in the flake.nix directory.\n\tsrc *packageFS\n}\n\n// NewDerivationBuilder initializes a new DerivationBuilder from the current\n// Nix build environment.\nfunc NewDerivationBuilder() (*DerivationBuilder, error) {\n\td := &DerivationBuilder{}\n\tif err := d.init(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn d, nil\n}\n\nfunc (d *DerivationBuilder) init() error {\n\tif d.Out == \"\" {\n\t\td.Out = os.Getenv(\"out\")\n\t\tif d.Out == \"\" {\n\t\t\treturn fmt.Errorf(\"patchpkg: $out is empty (is this being run from a nix build?)\")\n\t\t}\n\t}\n\tif d.Glibc != \"\" {\n\t\tif d.glibcPatcher == nil {\n\t\t\td.glibcPatcher = &libPatcher{}\n\t\t}\n\t\terr := d.glibcPatcher.setGlibc(newPackageFS(d.Glibc))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"patchpkg: can't patch glibc using %s: %v\", d.Glibc, err)\n\t\t}\n\t}\n\tif d.Gcc != \"\" {\n\t\tif d.glibcPatcher == nil {\n\t\t\td.glibcPatcher = &libPatcher{}\n\t\t}\n\t\terr := d.glibcPatcher.setGcc(newPackageFS(d.Gcc))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"patchpkg: can't patch gcc using %s: %v\", d.Gcc, err)\n\t\t}\n\t}\n\tif src := os.Getenv(\"src\"); src != \"\" {\n\t\td.src = newPackageFS(src)\n\t}\n\treturn nil\n}\n\n// Build applies patches to a package store path and puts the result in the\n// d.Out directory.\nfunc (d *DerivationBuilder) Build(ctx context.Context, pkgStorePath string) error {\n\tif err := d.init(); err != nil {\n\t\treturn err\n\t}\n\n\tslog.DebugContext(ctx, \"starting build to patch package\",\n\t\t\"pkg\", pkgStorePath, \"glibc\", d.Glibc, \"out\", d.Out)\n\treturn d.build(ctx, newPackageFS(pkgStorePath), newPackageFS(d.Out))\n}\n\nfunc (d *DerivationBuilder) build(ctx context.Context, pkg, out *packageFS) error {\n\t// Create the derivation's $out directory.\n\tif err := d.copyDir(out, \".\"); err != nil {\n\t\treturn err\n\t}\n\n\tif d.RestoreRefs {\n\t\tif err := d.restoreMissingRefs(ctx, pkg); err != nil {\n\t\t\t// Don't break the flake build if we're unable to\n\t\t\t// restore some of the refs. Having some is still an\n\t\t\t// improvement.\n\t\t\tslog.ErrorContext(ctx, \"unable to restore all removed refs\", \"err\", err)\n\t\t}\n\t}\n\tif err := d.findCUDA(ctx, out); err != nil {\n\t\tslog.ErrorContext(ctx, \"unable to patch CUDA libraries\", \"err\", err)\n\t}\n\n\tvar err error\n\tfor path, entry := range allFiles(pkg, \".\") {\n\t\tif ctx.Err() != nil {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tif path == \".\" {\n\t\t\t// Skip the $out directory - we already created it.\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch {\n\t\tcase entry.IsDir():\n\t\t\terr = d.copyDir(out, path)\n\t\tcase isSymlink(entry.Type()):\n\t\t\terr = d.copySymlink(pkg, out, path)\n\t\tdefault:\n\t\t\terr = d.copyFile(ctx, pkg, out, path)\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcmd := exec.CommandContext(ctx, lookPath(\"bash\"), \"-s\")\n\tcmd.Stdin = bytes.NewReader(glibcPatchScript)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\treturn cmd.Run()\n}\n\nfunc (d *DerivationBuilder) restoreMissingRefs(ctx context.Context, pkg *packageFS) error {\n\t// Find store path references to build inputs that were removed\n\t// from Python.\n\trefs, err := d.findRemovedRefs(ctx, pkg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Group the references we want to restore by file path.\n\td.bytePatches = make(map[string][]fileSlice, len(refs))\n\tfor _, ref := range refs {\n\t\td.bytePatches[ref.path] = append(d.bytePatches[ref.path], ref)\n\t}\n\n\t// If any of those references have shared libraries, add them\n\t// back to Python's RPATH.\n\tif d.glibcPatcher != nil {\n\t\tnixStore := cmp.Or(os.Getenv(\"NIX_STORE\"), \"/nix/store\")\n\t\tseen := make(map[string]bool)\n\t\tfor _, ref := range refs {\n\t\t\tstorePath := filepath.Join(nixStore, string(ref.data))\n\t\t\tif seen[storePath] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[storePath] = true\n\t\t\td.glibcPatcher.prependRPATH(newPackageFS(storePath))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *DerivationBuilder) copyDir(out *packageFS, path string) error {\n\tpath, err := out.OSPath(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.MkdirAll(path, 0o777)\n}\n\nfunc (d *DerivationBuilder) copyFile(ctx context.Context, pkg, out *packageFS, path string) error {\n\tsrcFile, err := pkg.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer srcFile.Close()\n\n\tsrc := bufio.NewReader(srcFile)\n\tif d.needsGlibcPatch(src, path) {\n\t\tsrcPath, err := pkg.OSPath(path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdstPath, err := out.OSPath(path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// No need to copy the file, patchelf will do it for us.\n\t\treturn d.glibcPatcher.patch(ctx, srcPath, dstPath)\n\t}\n\n\tstat, err := srcFile.Stat()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// We only need to copy the executable permissions of a file.\n\t// Nix ends up making everything in the store read-only after\n\t// the build is done.\n\tperm := fs.FileMode(0o666)\n\tif isExecutable(stat.Mode()) {\n\t\tperm = fs.FileMode(0o777)\n\t}\n\n\tdstPath, err := out.OSPath(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, perm)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer dst.Close()\n\n\t_, err = io.Copy(dst, src)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, patch := range d.bytePatches[path] {\n\t\t_, err := dst.WriteAt(patch.data, patch.offset)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn dst.Close()\n}\n\nfunc (d *DerivationBuilder) copySymlink(pkg, out *packageFS, path string) error {\n\tlink, err := out.OSPath(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttarget, err := pkg.Readlink(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.Symlink(target, link)\n}\n\nfunc (d *DerivationBuilder) needsGlibcPatch(file *bufio.Reader, filePath string) bool {\n\tif d.Glibc == \"\" || d.glibcPatcher == nil {\n\t\treturn false\n\t}\n\tif path.Dir(filePath) != \"bin\" {\n\t\treturn false\n\t}\n\n\t// ELF binaries are identifiable by the first 4 magic bytes:\n\t// 0x7F E L F\n\tmagic, err := file.Peek(4)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn magic[0] == 0x7F && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'\n}\n\nfunc (d *DerivationBuilder) findRemovedRefs(ctx context.Context, pkg *packageFS) ([]fileSlice, error) {\n\tvar refs []fileSlice\n\tmatches, err := fs.Glob(pkg, \"lib/python*/_sysconfigdata_*.py\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, name := range matches {\n\t\tif ctx.Err() != nil {\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\t\tmatches, err := searchFile(pkg, name, reRemovedRefs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trefs = append(refs, matches...)\n\t}\n\n\tpkgNameToHash := make(map[string]string, len(refs))\n\tfor _, ref := range refs {\n\t\tif ctx.Err() != nil {\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\n\t\tname := string(ref.data[33:])\n\t\tif hash, ok := pkgNameToHash[name]; ok {\n\t\t\tcopy(ref.data, hash)\n\t\t\tcontinue\n\t\t}\n\n\t\tre, err := regexp.Compile(`[0123456789abcdfghijklmnpqrsvwxyz]{32}-` + regexp.QuoteMeta(name) + `([$\"'{}/[\\] \\t\\r\\n]|$)`)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmatch := searchEnv(re)\n\t\tif match == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"can't find hash to restore store path reference %q in %q: regexp %q returned 0 matches\", ref.data, ref.path, re)\n\t\t}\n\t\thash := match[:32]\n\t\tpkgNameToHash[name] = hash\n\t\tcopy(ref.data, hash)\n\t\tslog.DebugContext(ctx, \"restored store ref\", \"ref\", ref)\n\t}\n\treturn refs, nil\n}\n\nfunc (d *DerivationBuilder) findCUDA(ctx context.Context, out *packageFS) error {\n\tif d.src == nil {\n\t\treturn fmt.Errorf(\"patch flake didn't set $src to the path to its source tree\")\n\t}\n\n\tpattern := \"lib/libcuda.so*\"\n\tslog.DebugContext(ctx, \"looking for system CUDA libraries in flake\", \"glob\", filepath.Join(d.src.storePath, \"lib/libcuda.so*\"))\n\tglob, err := fs.Glob(d.src, pattern)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"glob system libraries: %v\", err)\n\t}\n\tif len(glob) == 0 {\n\t\tslog.DebugContext(ctx, \"no system CUDA libraries found in flake\")\n\t} else {\n\t\terr := d.copyDir(out, \"lib\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"copy system library: %v\", err)\n\t\t}\n\t}\n\tfor _, lib := range glob {\n\t\tslog.DebugContext(ctx, \"found system CUDA library in flake\", \"path\", lib)\n\n\t\terr := d.copyFile(ctx, d.src, out, lib)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"copy system library: %v\", err)\n\t\t}\n\t\tneed, err := out.OSPath(lib)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get absolute path to library: %v\", err)\n\t\t}\n\t\td.glibcPatcher.needed = append(d.glibcPatcher.needed, need)\n\n\t\tslog.DebugContext(ctx, \"added DT_NEEDED entry for system CUDA library\", \"path\", need)\n\t}\n\n\tslog.DebugContext(ctx, \"looking for nix libraries in $patchDependencies\")\n\tdeps := os.Getenv(\"patchDependencies\")\n\tif strings.TrimSpace(deps) == \"\" {\n\t\tslog.DebugContext(ctx, \"$patchDependencies is empty\")\n\t\treturn nil\n\t}\n\tfor _, pkg := range strings.Split(deps, \" \") {\n\t\tslog.DebugContext(ctx, \"checking for nix libraries in package\", \"pkg\", pkg)\n\n\t\tpkgFS := newPackageFS(pkg)\n\t\tlibs, err := fs.Glob(pkgFS, \"lib*/*.so*\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"glob nix package libraries: %v\", err)\n\t\t}\n\n\t\tsonameRegexp := regexp.MustCompile(`(^|/).+\\.so\\.\\d+`)\n\t\tfor _, lib := range libs {\n\t\t\tif !sonameRegexp.MatchString(lib) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tneed, err := pkgFS.OSPath(lib)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"get absolute path to nix package library: %v\", err)\n\t\t\t}\n\t\t\td.glibcPatcher.needed = append(d.glibcPatcher.needed, need)\n\n\t\t\tslog.DebugContext(ctx, \"added DT_NEEDED entry for nix library\", \"path\", need)\n\t\t}\n\t}\n\treturn nil\n}\n\n// packageFS is the tree of files for a package in the Nix store.\ntype packageFS struct {\n\tfs.FS\n\tstorePath string\n}\n\n// newPackageFS returns a packageFS for the given store path.\nfunc newPackageFS(storePath string) *packageFS {\n\treturn &packageFS{\n\t\tFS:        os.DirFS(storePath),\n\t\tstorePath: storePath,\n\t}\n}\n\n// Readlink returns the destination of a symlink.\nfunc (p *packageFS) Readlink(path string) (string, error) {\n\tosPath, err := p.OSPath(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// TODO(gcurtis): check that the symlink isn't absolute or points\n\t// outside the Nix store.\n\treturn os.Readlink(osPath)\n}\n\n// OSPath translates a package-relative path to an operating system path.\nfunc (p *packageFS) OSPath(path string) (string, error) {\n\tlocal, err := filepath.Localize(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(p.storePath, local), nil\n}\n\n// allFiles iterates over all files in fsys starting at root. It silently\n// ignores errors.\nfunc allFiles(fsys fs.FS, root string) iter.Seq2[string, fs.DirEntry] {\n\treturn func(yield func(string, fs.DirEntry) bool) {\n\t\t_ = fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {\n\t\t\tif err == nil {\n\t\t\t\tif !yield(path, d) {\n\t\t\t\t\treturn filepath.SkipAll\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n}\n\n// lookPath is like [exec.lookPath], but first checks if there's an environment\n// variable with the name prog. If there is, it returns $prog/bin/prog instead\n// of consulting PATH.\n//\n// For example, lookPath would be able to find bash and patchelf in the\n// following derivation:\n//\n//\tderivation {\n//\t  inherit (nixpkgs.legacyPackages.x86_64-linux) bash patchelf;\n//\t  builder = devbox;\n//\t}\nfunc lookPath(prog string) string {\n\tpkgPath := os.Getenv(prog)\n\tif pkgPath == \"\" {\n\t\treturn prog\n\t}\n\treturn filepath.Join(pkgPath, \"bin\", prog)\n}\n\nfunc isExecutable(mode fs.FileMode) bool { return mode&0o111 != 0 }\nfunc isSymlink(mode fs.FileMode) bool    { return mode&fs.ModeSymlink != 0 }\n"
  },
  {
    "path": "internal/patchpkg/elf.go",
    "content": "package patchpkg\n\nimport (\n\t\"debug/elf\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"iter\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nvar (\n\t// SystemLibSearchPaths match the system library paths for common Linux\n\t// distributions.\n\tSystemLibSearchPaths = []string{\n\t\t\"/lib*/*-linux-gnu\", // Debian\n\t\t\"/lib*\",             // Red Hat\n\t\t\"/var/lib*/*/lib*\",  // Docker\n\t}\n\n\t// EnvLibrarySearchPath matches the paths in the LIBRARY_PATH\n\t// environment variable.\n\tEnvLibrarySearchPath = filepath.SplitList(os.Getenv(\"LIBRARY_PATH\"))\n\n\t// EnvLDLibrarySearchPath matches the paths in the LD_LIBRARY_PATH\n\t// environment variable.\n\tEnvLDLibrarySearchPath = filepath.SplitList(os.Getenv(\"LD_LIBRARY_PATH\"))\n\n\t// CUDALibSearchPaths match the common installation directories for CUDA\n\t// libraries.\n\tCUDALibSearchPaths = []string{\n\t\t// Common non-package manager locations.\n\t\t\"/opt/cuda/lib*\",\n\t\t\"/opt/nvidia/lib*\",\n\t\t\"/usr/local/cuda/lib*\",\n\t\t\"/usr/local/nvidia/lib*\",\n\n\t\t// Unlikely, but might as well try.\n\t\t\"/lib*/nvidia\",\n\t\t\"/lib*/cuda\",\n\t\t\"/usr/lib*/nvidia\",\n\t\t\"/usr/lib*/cuda\",\n\t\t\"/usr/local/lib*\",\n\t\t\"/usr/local/lib*/nvidia\",\n\t\t\"/usr/local/lib*/cuda\",\n\t}\n)\n\n// SharedLibrary describes an ELF shared library (object).\n//\n// Note that the various name fields document the common naming and versioning\n// conventions, but it is possible for a library to deviate from them.\n//\n// For an introduction to Linux shared libraries, see\n// https://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html\ntype SharedLibrary struct {\n\t*os.File\n\n\t// LinkerName is the soname without any version suffix (libfoo.so). It\n\t// is typically a symlink pointing to Soname. The build-time linker\n\t// looks for this name by default.\n\tLinkerName string\n\n\t// Soname is the shared object name from the library's DT_SONAME field.\n\t// It usually includes a version number suffix (libfoo.so.1). Other ELF\n\t// binaries that depend on this library typically specify this name in\n\t// the DT_NEEDED field.\n\tSoname string\n\n\t// RealName is the absolute path to the file that actually contains the\n\t// library code. It is typically the soname plus a minor version and\n\t// release number (libfoo.so.1.0.0).\n\tRealName string\n}\n\n// OpenSharedLibrary opens a shared library file. Unlike with ld, name must be\n// an exact path. To search for a library in the usual locations, use\n// [FindSharedLibrary] instead.\nfunc OpenSharedLibrary(name string) (SharedLibrary, error) {\n\tlib := SharedLibrary{}\n\tvar err error\n\tlib.File, err = os.Open(name)\n\tif err != nil {\n\t\treturn lib, err\n\t}\n\n\tdir, file := filepath.Split(name)\n\ti := strings.Index(file, \".so\")\n\tif i != -1 {\n\t\tlib.LinkerName = dir + file[:i+3]\n\t}\n\n\telfFile, err := elf.NewFile(lib)\n\tif err == nil {\n\t\tsoname, _ := elfFile.DynString(elf.DT_SONAME)\n\t\tif len(soname) != 0 {\n\t\t\tlib.Soname = soname[0]\n\t\t}\n\t}\n\n\treal, err := filepath.EvalSymlinks(name)\n\tif err == nil {\n\t\tlib.RealName, _ = filepath.Abs(real)\n\t}\n\treturn lib, nil\n}\n\n// FindSharedLibrary searches the directories in searchPath for a shared\n// library. It yields any libraries in the search path directories that have\n// name as a prefix. For example, \"libcuda.so\" will match \"libcuda.so\",\n// \"libcuda.so.1\", and \"libcuda.so.550.107.02\". The underlying file is only\n// valid for a single iteration, after which it is closed.\n//\n// The search path may contain [filepath.Glob] patterns. See\n// [SystemLibSearchPaths] for some predefined search paths. If name is an\n// absolute path, then FindSharedLibrary opens it directly and doesn't perform\n// any searching.\nfunc FindSharedLibrary(name string, searchPath ...string) iter.Seq[SharedLibrary] {\n\treturn func(yield func(SharedLibrary) bool) {\n\t\tif filepath.IsAbs(name) {\n\t\t\tlib, err := OpenSharedLibrary(name)\n\t\t\tif err == nil {\n\t\t\t\tyield(lib)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tif libPath := os.Getenv(\"LD_LIBRARY_PATH\"); libPath != \"\" {\n\t\t\tsearchPath = append(searchPath, filepath.SplitList(os.Getenv(\"LD_LIBRARY_PATH\"))...)\n\t\t}\n\t\tif libPath := os.Getenv(\"LIBRARY_PATH\"); libPath != \"\" {\n\t\t\tsearchPath = append(searchPath, filepath.SplitList(libPath)...)\n\t\t}\n\t\tsearchPath = append(searchPath,\n\t\t\t\"/lib*/*-linux-gnu\", // Debian\n\t\t\t\"/lib*\",             // Red Hat\n\t\t)\n\n\t\tsuffix := globEscape(name) + \"*\"\n\t\tpatterns := make([]string, len(searchPath))\n\t\tfor i := range searchPath {\n\t\t\tpatterns[i] = filepath.Join(searchPath[i], suffix)\n\t\t}\n\t\tfor match := range searchGlobs(patterns) {\n\t\t\tlib, err := OpenSharedLibrary(match)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tok := yield(lib)\n\t\t\t_ = lib.Close()\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// CopyAndLink copies the shared library to dir and creates the LinkerName and\n// Soname symlinks for it. It creates dir if it doesn't already exist.\nfunc (lib SharedLibrary) CopyAndLink(dir string) error {\n\terr := os.MkdirAll(dir, 0o755)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdstPath := filepath.Join(dir, filepath.Base(lib.RealName))\n\tdst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o666)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer dst.Close()\n\n\t_, err = io.Copy(dst, lib)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = dst.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsonameLink := filepath.Join(dir, filepath.Base(lib.Soname))\n\tvar sonameErr error\n\tif lib.Soname != \"\" {\n\t\t// Symlink must be relative.\n\t\tsonameErr = os.Symlink(filepath.Base(lib.RealName), sonameLink)\n\t}\n\n\tlinkerNameLink := filepath.Join(dir, filepath.Base(lib.LinkerName))\n\tvar linkerNameErr error\n\tif lib.LinkerName != \"\" {\n\t\t// Symlink must be relative.\n\t\tif sonameErr == nil {\n\t\t\tlinkerNameErr = os.Symlink(filepath.Base(sonameLink), linkerNameLink)\n\t\t} else {\n\t\t\tlinkerNameErr = os.Symlink(filepath.Base(dstPath), linkerNameLink)\n\t\t}\n\t}\n\n\terr = errors.Join(sonameErr, linkerNameErr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"patchpkg: create symlinks for shared library: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (lib SharedLibrary) LogValue() slog.Value {\n\treturn slog.GroupValue(\n\t\tslog.String(\"path\", lib.Name()),\n\t\tslog.String(\"linkername\", lib.LinkerName),\n\t\tslog.String(\"soname\", lib.Soname),\n\t\tslog.String(\"realname\", lib.RealName),\n\t)\n}\n"
  },
  {
    "path": "internal/patchpkg/glibc-patch.bash",
    "content": "#!/bin/bash\n\nset -euo pipefail\n\ndeclare -r pkg # package that we're patching\ndeclare -r out # nix output path that will contain the patched package\n\n# Paths to this script's dependencies set by nix.\ndeclare -r coreutils gnused ripgrep\n\n# Explicitly declare the specific commands that this script depends on.\nhash -p \"$coreutils/bin/chmod\" chmod\nhash -p \"$coreutils/bin/dirname\" dirname\nhash -p \"$coreutils/bin/echo\" echo\nhash -p \"$coreutils/bin/stat\" stat\nhash -p \"$coreutils/bin/wc\" wc\nhash -p \"$gnused/bin/sed\" sed\nhash -p \"$ripgrep/bin/rg\" rg\n\npatch_store_path() {\n\tdeclare -r path=\"$1\"\n\tdeclare -r perm=$(stat -c \"%a\" \"$path\")\n\n\t# sed creates a temporary sibling file for in-place edits, so we need to\n\t# ensure that the file's directory is writeable.\n\tdeclare -r dir=\"$(dirname \"$path\")\"\n\tdeclare -r dperm=$(stat -c \"%a\" \"$dir\")\n\n\techo \"running sed file=$path file_perm=$perm dir=$dir dir_perm=$dperm\"\n\tchmod u+w \"$path\" \"$dir\"\n\tsed -i -e \"$sedexpr\" \"$path\"\n\tchmod \"$perm\" \"$path\"\n\tchmod \"$dperm\" \"$dir\"\n}\n\n# -uu search ignored and hidden files\n# -l list filenames\n# -F exact substring search (faster, no escaping needed)\nfiles=\"$(rg -uu -l -F \"$pkg\" \"$out\")\"\ncount=\"$(echo \"$files\" | wc -l)\"\nsedexpr=\"s|$pkg|$out|g\"\necho \"patching files with old store path references count=$count sed=$sedexpr\"\nfor f in $files; do\n\tpatch_store_path \"$f\"\ndone\n"
  },
  {
    "path": "internal/patchpkg/patch.go",
    "content": "package patchpkg\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os/exec\"\n\t\"path\"\n\t\"slices\"\n\t\"strings\"\n)\n\n// libPatcher patches ELF binaries to use an alternative version of glibc.\ntype libPatcher struct {\n\t// ld is the absolute path to the new dynamic linker (ld.so).\n\tld string\n\n\t// rpath is the new RPATH with the directories containing the new libc\n\t// shared objects (libc.so) and other libraries.\n\trpath []string\n\n\t// needed are shared libraries to add as dependencies (DT_NEEDED).\n\tneeded []string\n}\n\n// setGlibc configures the patcher to use the dynamic linker and libc libraries\n// in pkg.\nfunc (p *libPatcher) setGlibc(pkg *packageFS) error {\n\t// Verify that we can find a directory with libc in it.\n\tglob := \"lib*/libc.so*\"\n\tmatches, _ := fs.Glob(pkg, glob)\n\tif len(matches) == 0 {\n\t\treturn fmt.Errorf(\"cannot find libc.so file matching %q\", glob)\n\t}\n\tfor i := range matches {\n\t\tmatches[i] = path.Dir(matches[i])\n\t}\n\t// Pick the shortest name: lib < lib32 < lib64 < libx32\n\t//\n\t// - lib is usually a symlink to the correct arch (e.g., lib -> lib64)\n\t// - *.so is usually a symlink to the correct version (e.g., foo.so -> foo.so.2)\n\tslices.Sort(matches)\n\n\tlib, err := pkg.OSPath(matches[0])\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.rpath = append(p.rpath, lib)\n\tslog.Debug(\"found new libc directory\", \"path\", lib)\n\n\t// Verify that we can find the new dynamic linker.\n\tglob = \"lib*/ld-linux*.so*\"\n\tmatches, _ = fs.Glob(pkg, glob)\n\tif len(matches) == 0 {\n\t\treturn fmt.Errorf(\"cannot find ld.so file matching %q\", glob)\n\t}\n\tslices.Sort(matches)\n\tp.ld, err = pkg.OSPath(matches[0])\n\tif err != nil {\n\t\treturn err\n\t}\n\tslog.Debug(\"found new dynamic linker\", \"path\", p.ld)\n\treturn nil\n}\n\n// setGlibc configures the patcher to use the standard C++ and gcc libraries in\n// pkg.\nfunc (p *libPatcher) setGcc(pkg *packageFS) error {\n\t// Verify that we can find a directory with libstdc++.so in it.\n\tglob := \"lib*/libstdc++.so*\"\n\tmatches, _ := fs.Glob(pkg, glob)\n\tif len(matches) == 0 {\n\t\treturn fmt.Errorf(\"cannot find libstdc++.so file matching %q\", glob)\n\t}\n\tfor i := range matches {\n\t\tmatches[i] = path.Dir(matches[i])\n\t}\n\t// Pick the shortest name: lib < lib32 < lib64 < libx32\n\t//\n\t// - lib is usually a symlink to the correct arch (e.g., lib -> lib64)\n\t// - *.so is usually a symlink to the correct version (e.g., foo.so -> foo.so.2)\n\tslices.Sort(matches)\n\n\tlib, err := pkg.OSPath(matches[0])\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.rpath = append(p.rpath, lib)\n\tp.needed = append(p.needed, \"libstdc++.so\")\n\tslog.Debug(\"found new libstdc++ directory\", \"path\", lib)\n\treturn nil\n}\n\nfunc (p *libPatcher) prependRPATH(libPkg *packageFS) {\n\tglob := \"lib*/*.so*\"\n\tmatches, _ := fs.Glob(libPkg, glob)\n\tif len(matches) == 0 {\n\t\tslog.Debug(\"not prepending package to RPATH because no shared libraries were found\", \"pkg\", libPkg.storePath)\n\t\treturn\n\t}\n\tfor i := range matches {\n\t\tmatches[i] = path.Dir(matches[i])\n\t}\n\tslices.Sort(matches)\n\tmatches = slices.Compact(matches)\n\tfor i := range matches {\n\t\tvar err error\n\t\tmatches[i], err = libPkg.OSPath(matches[i])\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t}\n\tp.rpath = append(p.rpath, matches...)\n\tslog.Debug(\"prepended package lib dirs to RPATH\", \"pkg\", libPkg.storePath, \"dirs\", matches)\n}\n\n// patch applies glibc patches to a binary and writes the patched result to\n// outPath. It does not modify the original binary in-place.\nfunc (p *libPatcher) patch(ctx context.Context, path, outPath string) error {\n\tcmd := &patchelf{PrintInterpreter: true}\n\tout, err := cmd.run(ctx, path)\n\tif err != nil {\n\t\treturn err\n\t}\n\toldInterp := string(out)\n\n\tcmd = &patchelf{PrintRPATH: true}\n\tout, err = cmd.run(ctx, path)\n\tif err != nil {\n\t\treturn err\n\t}\n\toldRpath := strings.Split(string(out), \":\")\n\n\tcmd = &patchelf{\n\t\tSetInterpreter: p.ld,\n\t\tSetRPATH:       append(p.rpath, oldRpath...),\n\t\tAddNeeded:      p.needed,\n\t\tOutput:         outPath,\n\t}\n\tslog.Debug(\"patching glibc on binary\",\n\t\t\"path\", path, \"outPath\", cmd.Output,\n\t\t\"old_interp\", oldInterp, \"new_interp\", cmd.SetInterpreter,\n\t\t\"old_rpath\", oldRpath, \"new_rpath\", cmd.SetRPATH,\n\t)\n\t_, err = cmd.run(ctx, path)\n\treturn err\n}\n\n// patchelf runs the patchelf command.\ntype patchelf struct {\n\tSetRPATH   []string\n\tPrintRPATH bool\n\n\tSetInterpreter   string\n\tPrintInterpreter bool\n\n\tAddNeeded []string\n\n\tOutput string\n}\n\n// run runs patchelf on an ELF binary and returns its output.\nfunc (p *patchelf) run(ctx context.Context, elf string) ([]byte, error) {\n\tcmd := exec.CommandContext(ctx, lookPath(\"patchelf\"))\n\tif len(p.SetRPATH) != 0 {\n\t\tcmd.Args = append(cmd.Args, \"--force-rpath\", \"--set-rpath\", strings.Join(p.SetRPATH, \":\"))\n\t}\n\tif p.PrintRPATH {\n\t\tcmd.Args = append(cmd.Args, \"--print-rpath\")\n\t}\n\tif p.SetInterpreter != \"\" {\n\t\tcmd.Args = append(cmd.Args, \"--set-interpreter\", p.SetInterpreter)\n\t}\n\tif p.PrintInterpreter {\n\t\tcmd.Args = append(cmd.Args, \"--print-interpreter\")\n\t}\n\tfor _, needed := range p.AddNeeded {\n\t\tcmd.Args = append(cmd.Args, \"--add-needed\", needed)\n\t}\n\tif p.Output != \"\" {\n\t\tcmd.Args = append(cmd.Args, \"--output\", p.Output)\n\t}\n\tcmd.Args = append(cmd.Args, elf)\n\tout, err := cmd.Output()\n\treturn bytes.TrimSpace(out), err\n}\n"
  },
  {
    "path": "internal/patchpkg/search.go",
    "content": "package patchpkg\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"iter\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"unicode/utf8\"\n)\n\n// maxFileSize limits the amount of data to load from a file when\n// searching.\nconst maxFileSize = 1 << 30 // 1 GiB\n\n// reRemovedRefs matches a removed Nix store path where the hash is\n// overwritten with e's (making it an invalid nix hash).\nvar reRemovedRefs = regexp.MustCompile(`e{32}-[^$\"'{}/[\\] \\t\\r\\n]+`)\n\n// fileSlice is a slice of data within a file.\ntype fileSlice struct {\n\tpath   string\n\tdata   []byte\n\toffset int64\n}\n\nfunc (f fileSlice) String() string {\n\treturn fmt.Sprintf(\"%s@%d: %s\", f.path, f.offset, f.data)\n}\n\n// searchFile searches a single file for a regular expression. It limits the\n// search to the first [maxFileSize] bytes of the file to avoid consuming too\n// much memory.\nfunc searchFile(fsys fs.FS, path string, re *regexp.Regexp) ([]fileSlice, error) {\n\tf, err := fsys.Open(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer f.Close()\n\n\tr := &io.LimitedReader{R: f, N: maxFileSize}\n\tdata, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlocs := re.FindAllIndex(data, -1)\n\tif len(locs) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tmatches := make([]fileSlice, len(locs))\n\tfor i := range locs {\n\t\tstart, end := locs[i][0], locs[i][1]\n\t\tmatches[i] = fileSlice{\n\t\t\tpath:   path,\n\t\t\tdata:   data[start:end],\n\t\t\toffset: int64(start),\n\t\t}\n\t}\n\treturn matches, nil\n}\n\nvar envValues = sync.OnceValue(func() []string {\n\tenv := os.Environ()\n\tvalues := make([]string, len(env))\n\tfor i := range env {\n\t\t_, values[i], _ = strings.Cut(env[i], \"=\")\n\t}\n\treturn values\n})\n\nfunc searchEnv(re *regexp.Regexp) string {\n\tfor _, env := range envValues() {\n\t\tmatch := re.FindString(env)\n\t\tif match != \"\" {\n\t\t\treturn match\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// searchGlobs iterates over the paths matched by multiple [filepath.Glob]\n// patterns. It will not yield a path more than once, even if the path matches\n// multiple patterns. It silently ignores any pattern syntax errors.\nfunc searchGlobs(patterns []string) iter.Seq[string] {\n\tseen := make(map[string]bool, len(patterns))\n\treturn func(yield func(string) bool) {\n\t\tfor _, pattern := range patterns {\n\t\t\tglob, err := filepath.Glob(pattern)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, match := range glob {\n\t\t\t\tif seen[match] {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tseen[match] = true\n\n\t\t\t\tif !yield(match) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// globEscape escapes all metacharacters ('*', '?', '\\\\', '[') in s so that they\n// match their literal values in a [filepath.Glob] or [fs.Glob] pattern.\nfunc globEscape(s string) string {\n\tif !strings.ContainsAny(s, `*?\\[`) {\n\t\treturn s\n\t}\n\n\tb := make([]byte, 0, len(s)+1)\n\tfor _, r := range s {\n\t\tswitch r {\n\t\tcase '*', '?', '\\\\', '[':\n\t\t\tb = append(b, '\\\\')\n\t\t}\n\t\tb = utf8.AppendRune(b, r)\n\t}\n\treturn string(b)\n}\n"
  },
  {
    "path": "internal/plugin/files.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage plugin\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/devconfig/configfile\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/plugins\"\n)\n\nfunc getConfigIfAny(inc Includable, projectDir string) (*Config, error) {\n\tswitch includable := inc.(type) {\n\tcase *devpkg.Package:\n\t\treturn getBuiltinPluginConfigIfExists(includable, projectDir)\n\tcase *githubPlugin:\n\t\tcontent, err := includable.Fetch()\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithStack(err)\n\t\t}\n\t\treturn buildConfig(includable, projectDir, string(content))\n\tcase *gitPlugin:\n\t\tcontent, err := includable.Fetch()\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithStack(err)\n\t\t}\n\t\treturn buildConfig(includable, projectDir, string(content))\n\tcase *LocalPlugin:\n\t\tcontent, err := os.ReadFile(includable.Path())\n\t\tif err != nil && !os.IsNotExist(err) {\n\t\t\treturn nil, errors.WithStack(err)\n\t\t}\n\t\treturn buildConfig(includable, projectDir, string(content))\n\t}\n\treturn nil, errors.Errorf(\"unknown plugin type %T\", inc)\n}\n\nfunc getBuiltinPluginConfigIfExists(\n\tpkg *devpkg.Package,\n\tprojectDir string,\n) (*Config, error) {\n\tif pkg.DisablePlugin {\n\t\treturn nil, nil\n\t}\n\tcontent, err := plugins.BuiltInForPackage(pkg.CanonicalName())\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil, nil\n\t}\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn buildConfig(pkg, projectDir, string(content))\n}\n\nfunc GetBuiltinsForPackages(\n\tpackages []configfile.Package,\n\tlockfile *lock.File,\n) ([]*Config, error) {\n\tbuiltIns := []*Config{}\n\tfor _, pkg := range devpkg.PackagesFromConfig(packages, lockfile) {\n\t\tconfig, err := getBuiltinPluginConfigIfExists(pkg, lockfile.ProjectDir())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif config != nil {\n\t\t\tbuiltIns = append(builtIns, config)\n\t\t}\n\t}\n\treturn builtIns, nil\n}\n"
  },
  {
    "path": "internal/plugin/git.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage plugin\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\ntype gitPlugin struct {\n\tref  *flake.Ref\n\tname string\n}\n\n// newGitPlugin creates a Git plugin from a flake reference.\n// It uses git clone to fetch the repository.\nfunc newGitPlugin(ref flake.Ref) (*gitPlugin, error) {\n\tif ref.Type != flake.TypeGit {\n\t\treturn nil, fmt.Errorf(\"expected git flake reference, got %s\", ref.Type)\n\t}\n\n\tname := generateGitPluginName(ref)\n\n\treturn &gitPlugin{\n\t\tref:  &ref,\n\t\tname: name,\n\t}, nil\n}\n\nfunc generateGitPluginName(ref flake.Ref) string {\n\t// Extract repository name from URL and append directory if specified\n\turl := ref.URL\n\tif url == \"\" {\n\t\treturn \"unknown.git\"\n\t}\n\n\t// Remove query parameters to get clean URL\n\tif strings.Contains(url, \"?\") {\n\t\turl = strings.Split(url, \"?\")[0]\n\t}\n\n\turl = strings.TrimSuffix(url, \".git\")\n\n\tparts := strings.Split(url, \"/\")\n\tif len(parts) < 2 {\n\t\treturn \"unknown.git\"\n\t}\n\n\t// Use last two path components (e.g., \"owner/repo\")\n\trepoParts := parts[len(parts)-2:]\n\n\tname := strings.Join(repoParts, \".\")\n\tname = strings.ReplaceAll(name, \"/\", \".\")\n\n\t// Append directory to make name unique when multiple plugins\n\t// from same repo are used\n\tif ref.Dir != \"\" {\n\t\tdirName := strings.ReplaceAll(ref.Dir, \"/\", \".\")\n\t\tname = name + \".\" + dirName\n\t}\n\n\treturn name\n}\n\n// getBaseURL extracts the base Git URL without query parameters.\n// Query parameters like ?dir=path are used by Nix flakes but not by git clone.\nfunc (p *gitPlugin) getBaseURL() string {\n\tbaseURL := p.ref.URL\n\tif strings.Contains(baseURL, \"?\") {\n\t\tbaseURL = strings.Split(baseURL, \"?\")[0]\n\t}\n\treturn baseURL\n}\n\nfunc (p *gitPlugin) Fetch() ([]byte, error) {\n\tcontent, err := p.FileContent(\"plugin.json\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn content, nil\n}\n\nfunc (p *gitPlugin) cloneAndRead(subpath string) ([]byte, error) {\n\ttempDir, err := os.MkdirTemp(\"\", \"devbox-git-plugin-*\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create temp directory: %w\", err)\n\t}\n\tdefer os.RemoveAll(tempDir)\n\n\tbaseURL := p.getBaseURL()\n\n\tcloneArgs := []string{\"clone\"}\n\tif p.ref.Ref != \"\" {\n\t\tcloneArgs = append(cloneArgs, \"--depth\", \"1\", \"--branch\", p.ref.Ref)\n\t} else if p.ref.Rev == \"\" {\n\t\tcloneArgs = append(cloneArgs, \"--depth\", \"1\")\n\t}\n\tcloneArgs = append(cloneArgs, baseURL, tempDir)\n\tcloneCmd := exec.Command(\"git\", cloneArgs...)\n\n\tif isSSHURL(baseURL) {\n\t\tgitSSHCommand := os.Getenv(\"GIT_SSH_COMMAND\")\n\t\tif gitSSHCommand == \"\" {\n\t\t\tgitSSHCommand = \"ssh -o StrictHostKeyChecking=accept-new\"\n\t\t}\n\t\tcloneCmd.Env = append(os.Environ(), \"GIT_SSH_COMMAND=\"+gitSSHCommand)\n\t}\n\n\toutput, err := cloneCmd.CombinedOutput()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to clone repository %s: %w\\nOutput: %s\", p.ref.URL, err, string(output))\n\t}\n\n\tif p.ref.Rev != \"\" {\n\t\tcheckoutCmd := exec.Command(\"git\", \"checkout\", p.ref.Rev)\n\t\tcheckoutCmd.Dir = tempDir\n\t\toutput, err := checkoutCmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to checkout revision %s: %w\\nOutput: %s\", p.ref.Rev, err, string(output))\n\t\t}\n\t}\n\n\t// Read file from repository root or specified directory\n\tfilePath := filepath.Join(tempDir, subpath)\n\tif p.ref.Dir != \"\" {\n\t\tfilePath = filepath.Join(tempDir, p.ref.Dir, subpath)\n\t}\n\n\tcontent, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read file %s: %w\", filePath, err)\n\t}\n\n\treturn content, nil\n}\n\n// isSSHURL checks if the given URL is an SSH URL.\n// SSH URLs can be in formats:\n//   - ssh://user@host/path\n//   - git@host:path\n//   - ssh://git@host/path\nfunc isSSHURL(url string) bool {\n\turl = strings.TrimSpace(url)\n\t// Check for explicit ssh:// protocol\n\tif strings.HasPrefix(url, \"ssh://\") {\n\t\treturn true\n\t}\n\t// Check for git@host:path format (SCP-like syntax)\n\t// This format uses colon after host, not port number\n\tif strings.HasPrefix(url, \"git@\") && strings.Contains(url, \":\") {\n\t\t// Make sure it's not an HTTPS URL with port (e.g., https://git@host:443/path)\n\t\tif !strings.HasPrefix(url, \"http://\") && !strings.HasPrefix(url, \"https://\") {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isBranchName(ref string) bool {\n\t// Full commit hashes are 40 hex characters\n\tif len(ref) == 40 {\n\t\tfor _, c := range ref {\n\t\t\tif !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (p *gitPlugin) CanonicalName() string {\n\treturn p.name\n}\n\n// Hash returns a unique hash for this plugin including directory.\n// This ensures plugins from the same repo with different dirs are unique.\nfunc (p *gitPlugin) Hash() string {\n\treturn fmt.Sprintf(\"%s-%s-%s-%s\", p.ref.URL, p.ref.Rev, p.ref.Ref, p.ref.Dir)\n}\n\nfunc (p *gitPlugin) FileContent(subpath string) ([]byte, error) {\n\treturn p.cloneAndRead(subpath)\n}\n\nfunc (p *gitPlugin) LockfileKey() string {\n\treturn p.ref.String()\n}\n"
  },
  {
    "path": "internal/plugin/git_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage plugin\n\nimport (\n\t\"testing\"\n\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\nfunc TestGitPlugin(t *testing.T) {\n\ttestCases := []struct {\n\t\tname     string\n\t\tref      string\n\t\texpected *gitPlugin\n\t}{\n\t\t{\n\t\t\tname: \"basic git plugin\",\n\t\t\tref:  \"git+https://github.com/jetify-com/devbox-plugins.git\",\n\t\t\texpected: &gitPlugin{\n\t\t\t\tref: &flake.Ref{\n\t\t\t\t\tType: flake.TypeGit,\n\t\t\t\t\tURL:  \"https://github.com/jetify-com/devbox-plugins.git\",\n\t\t\t\t},\n\t\t\t\tname: \"jetify-com.devbox-plugins\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"git plugin with ref\",\n\t\t\tref:  \"git+https://github.com/jetify-com/devbox-plugins.git?ref=main\",\n\t\t\texpected: &gitPlugin{\n\t\t\t\tref: &flake.Ref{\n\t\t\t\t\tType: flake.TypeGit,\n\t\t\t\t\tURL:  \"https://github.com/jetify-com/devbox-plugins.git\",\n\t\t\t\t\tRef:  \"main\",\n\t\t\t\t},\n\t\t\t\tname: \"jetify-com.devbox-plugins\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"git plugin with rev\",\n\t\t\tref:  \"git+https://github.com/jetify-com/devbox-plugins.git?rev=abc123\",\n\t\t\texpected: &gitPlugin{\n\t\t\t\tref: &flake.Ref{\n\t\t\t\t\tType: flake.TypeGit,\n\t\t\t\t\tURL:  \"https://github.com/jetify-com/devbox-plugins.git\",\n\t\t\t\t\tRev:  \"abc123\",\n\t\t\t\t},\n\t\t\t\tname: \"jetify-com.devbox-plugins\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"git plugin with directory\",\n\t\t\tref:  \"git+https://github.com/jetify-com/devbox-plugins.git?dir=mongodb\",\n\t\t\texpected: &gitPlugin{\n\t\t\t\tref: &flake.Ref{\n\t\t\t\t\tType: flake.TypeGit,\n\t\t\t\t\tURL:  \"https://github.com/jetify-com/devbox-plugins.git?dir=mongodb\",\n\t\t\t\t\tDir:  \"mongodb\",\n\t\t\t\t},\n\t\t\t\tname: \"jetify-com.devbox-plugins.mongodb\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"git plugin with directory and ref\",\n\t\t\tref:  \"git+https://github.com/jetify-com/devbox-plugins.git?dir=mongodb&ref=my-branch\",\n\t\t\texpected: &gitPlugin{\n\t\t\t\tref: &flake.Ref{\n\t\t\t\t\tType: flake.TypeGit,\n\t\t\t\t\tURL:  \"https://github.com/jetify-com/devbox-plugins.git?dir=mongodb\",\n\t\t\t\t\tDir:  \"mongodb\",\n\t\t\t\t\tRef:  \"my-branch\",\n\t\t\t\t},\n\t\t\t\tname: \"jetify-com.devbox-plugins.mongodb\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"git plugin with subgroups\",\n\t\t\tref:  \"git+https://gitlab.com/group/subgroup/repo.git\",\n\t\t\texpected: &gitPlugin{\n\t\t\t\tref: &flake.Ref{\n\t\t\t\t\tType: flake.TypeGit,\n\t\t\t\t\tURL:  \"https://gitlab.com/group/subgroup/repo.git\",\n\t\t\t\t},\n\t\t\t\tname: \"subgroup.repo\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"git plugin with SSH URL\",\n\t\t\tref:  \"git+ssh://git@github.com/jetify-com/devbox-plugins.git\",\n\t\t\texpected: &gitPlugin{\n\t\t\t\tref: &flake.Ref{\n\t\t\t\t\tType: flake.TypeGit,\n\t\t\t\t\tURL:  \"ssh://git@github.com/jetify-com/devbox-plugins.git\",\n\t\t\t\t},\n\t\t\t\tname: \"jetify-com.devbox-plugins\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"git plugin with file URL\",\n\t\t\tref:  \"git+file:///tmp/local-repo.git\",\n\t\t\texpected: &gitPlugin{\n\t\t\t\tref: &flake.Ref{\n\t\t\t\t\tType: flake.TypeGit,\n\t\t\t\t\tURL:  \"file:///tmp/local-repo.git\",\n\t\t\t\t},\n\t\t\t\tname: \"tmp.local-repo\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tref, err := flake.ParseRef(testCase.ref)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Failed to parse ref %q: %v\", testCase.ref, err)\n\t\t\t}\n\n\t\t\tplugin, err := newGitPlugin(ref)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Failed to create Git plugin: %v\", err)\n\t\t\t}\n\n\t\t\tif plugin.ref.Type != testCase.expected.ref.Type {\n\t\t\t\tt.Errorf(\"Expected type %q, got %q\", testCase.expected.ref.Type, plugin.ref.Type)\n\t\t\t}\n\t\t\tif plugin.ref.URL != testCase.expected.ref.URL {\n\t\t\t\tt.Errorf(\"Expected URL %q, got %q\", testCase.expected.ref.URL, plugin.ref.URL)\n\t\t\t}\n\t\t\tif plugin.ref.Ref != testCase.expected.ref.Ref {\n\t\t\t\tt.Errorf(\"Expected ref %q, got %q\", testCase.expected.ref.Ref, plugin.ref.Ref)\n\t\t\t}\n\t\t\tif plugin.ref.Rev != testCase.expected.ref.Rev {\n\t\t\t\tt.Errorf(\"Expected rev %q, got %q\", testCase.expected.ref.Rev, plugin.ref.Rev)\n\t\t\t}\n\t\t\tif plugin.ref.Dir != testCase.expected.ref.Dir {\n\t\t\t\tt.Errorf(\"Expected dir %q, got %q\", testCase.expected.ref.Dir, plugin.ref.Dir)\n\t\t\t}\n\t\t\tif plugin.name != testCase.expected.name {\n\t\t\t\tt.Errorf(\"Expected name %q, got %q\", testCase.expected.name, plugin.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGenerateGitPluginName(t *testing.T) {\n\ttestCases := []struct {\n\t\tname     string\n\t\tref      flake.Ref\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"github repository\",\n\t\t\tref: flake.Ref{\n\t\t\t\tURL: \"https://github.com/jetify-com/devbox-plugins.git\",\n\t\t\t},\n\t\t\texpected: \"jetify-com.devbox-plugins\",\n\t\t},\n\t\t{\n\t\t\tname: \"gitlab repository with subgroups\",\n\t\t\tref: flake.Ref{\n\t\t\t\tURL: \"https://gitlab.com/group/subgroup/repo.git\",\n\t\t\t},\n\t\t\texpected: \"subgroup.repo\",\n\t\t},\n\t\t{\n\t\t\tname: \"repository without .git suffix\",\n\t\t\tref: flake.Ref{\n\t\t\t\tURL: \"https://github.com/jetify-com/devbox-plugins\",\n\t\t\t},\n\t\t\texpected: \"jetify-com.devbox-plugins\",\n\t\t},\n\t\t{\n\t\t\tname: \"repository with single path component\",\n\t\t\tref: flake.Ref{\n\t\t\t\tURL: \"https://github.com/repo\",\n\t\t\t},\n\t\t\texpected: \"github.com.repo\",\n\t\t},\n\t\t{\n\t\t\tname: \"SSH repository\",\n\t\t\tref: flake.Ref{\n\t\t\t\tURL: \"ssh://git@github.com/jetify-com/devbox-plugins.git\",\n\t\t\t},\n\t\t\texpected: \"jetify-com.devbox-plugins\",\n\t\t},\n\t\t{\n\t\t\tname: \"file repository\",\n\t\t\tref: flake.Ref{\n\t\t\t\tURL: \"file:///tmp/local-repo.git\",\n\t\t\t},\n\t\t\texpected: \"tmp.local-repo\",\n\t\t},\n\t\t{\n\t\t\tname: \"repository with directory\",\n\t\t\tref: flake.Ref{\n\t\t\t\tURL: \"https://github.com/jetify-com/devbox-plugins.git\",\n\t\t\t\tDir: \"mongodb\",\n\t\t\t},\n\t\t\texpected: \"jetify-com.devbox-plugins.mongodb\",\n\t\t},\n\t\t{\n\t\t\tname: \"repository with nested directory\",\n\t\t\tref: flake.Ref{\n\t\t\t\tURL: \"https://github.com/jetify-com/devbox-plugins.git\",\n\t\t\t\tDir: \"plugins/python\",\n\t\t\t},\n\t\t\texpected: \"jetify-com.devbox-plugins.plugins.python\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tresult := generateGitPluginName(testCase.ref)\n\t\t\tif result != testCase.expected {\n\t\t\t\tt.Errorf(\"Expected name %q, got %q\", testCase.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGitPluginURL(t *testing.T) {\n\ttestCases := []struct {\n\t\tname     string\n\t\tref      string\n\t\tsubpath  string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"basic plugin.json\",\n\t\t\tref:      \"git+https://github.com/jetify-com/devbox-plugins.git\",\n\t\t\tsubpath:  \"plugin.json\",\n\t\t\texpected: \"plugin.json\",\n\t\t},\n\t\t{\n\t\t\tname:     \"plugin with directory\",\n\t\t\tref:      \"git+https://github.com/jetify-com/devbox-plugins.git?dir=mongodb\",\n\t\t\tsubpath:  \"plugin.json\",\n\t\t\texpected: \"mongodb/plugin.json\",\n\t\t},\n\t\t{\n\t\t\tname:     \"plugin with ref\",\n\t\t\tref:      \"git+https://github.com/jetify-com/devbox-plugins.git?ref=main\",\n\t\t\tsubpath:  \"plugin.json\",\n\t\t\texpected: \"plugin.json\",\n\t\t},\n\t\t{\n\t\t\tname:     \"plugin with directory and ref\",\n\t\t\tref:      \"git+https://github.com/jetify-com/devbox-plugins.git?dir=mongodb&ref=my-branch\",\n\t\t\tsubpath:  \"plugin.json\",\n\t\t\texpected: \"mongodb/plugin.json\",\n\t\t},\n\t\t{\n\t\t\tname:     \"plugin with subgroups\",\n\t\t\tref:      \"git+https://gitlab.com/group/subgroup/repo.git\",\n\t\t\tsubpath:  \"plugin.json\",\n\t\t\texpected: \"plugin.json\",\n\t\t},\n\t\t{\n\t\t\tname:     \"plugin with SSH URL\",\n\t\t\tref:      \"git+ssh://git@github.com/jetify-com/devbox-plugins.git\",\n\t\t\tsubpath:  \"plugin.json\",\n\t\t\texpected: \"plugin.json\",\n\t\t},\n\t\t{\n\t\t\tname:     \"plugin with file URL\",\n\t\t\tref:      \"git+file:///tmp/local-repo.git\",\n\t\t\tsubpath:  \"plugin.json\",\n\t\t\texpected: \"plugin.json\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tref, err := flake.ParseRef(testCase.ref)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Failed to parse ref %q: %v\", testCase.ref, err)\n\t\t\t}\n\n\t\t\tplugin, err := newGitPlugin(ref)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Failed to create Git plugin: %v\", err)\n\t\t\t}\n\n\t\t\t// Test that the plugin can be created and the subpath is handled correctly\n\t\t\t// The actual file path will be constructed in FileContent method\n\t\t\tif plugin.ref.Dir != \"\" {\n\t\t\t\texpectedPath := plugin.ref.Dir + \"/\" + testCase.subpath\n\t\t\t\tif expectedPath != testCase.expected {\n\t\t\t\t\tt.Errorf(\"Expected path %q, got %q\", testCase.expected, expectedPath)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif testCase.subpath != testCase.expected {\n\t\t\t\t\tt.Errorf(\"Expected subpath %q, got %q\", testCase.expected, testCase.subpath)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsBranchName(t *testing.T) {\n\ttestCases := []struct {\n\t\tname     string\n\t\tref      string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"branch name\",\n\t\t\tref:      \"main\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"branch name with slash\",\n\t\t\tref:      \"feature/new-feature\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"commit hash\",\n\t\t\tref:      \"abc123def456\",\n\t\t\texpected: true, // Not 40 chars, so treated as branch\n\t\t},\n\t\t{\n\t\t\tname:     \"full commit hash\",\n\t\t\tref:      \"a1b2c3d4e5f6789012345678901234567890abcd\",\n\t\t\texpected: false, // 40 chars, looks like commit hash\n\t\t},\n\t\t{\n\t\t\tname:     \"short commit hash\",\n\t\t\tref:      \"abc123\",\n\t\t\texpected: true, // Not 40 chars, so treated as branch\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tresult := isBranchName(testCase.ref)\n\t\t\tif result != testCase.expected {\n\t\t\t\tt.Errorf(\"Expected %v for %q, got %v\", testCase.expected, testCase.ref, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsSSHURL(t *testing.T) {\n\ttestCases := []struct {\n\t\tname     string\n\t\turl      string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"SSH URL with ssh:// protocol\",\n\t\t\turl:      \"ssh://git@github.com/user/repo.git\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"SSH URL with git@ format\",\n\t\t\turl:      \"git@github.com:user/repo.git\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"SSH URL with user@host format\",\n\t\t\turl:      \"ssh://user@gitlab.com/project/repo.git\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTPS URL should not be SSH\",\n\t\t\turl:      \"https://github.com/user/repo.git\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTP URL should not be SSH\",\n\t\t\turl:      \"http://github.com/user/repo.git\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTPS URL with port should not be SSH\",\n\t\t\turl:      \"https://git@github.com:443/user/repo.git\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTPS URL with authentication should not be SSH\",\n\t\t\turl:      \"https://user@github.com/user/repo.git\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"File URL should not be SSH\",\n\t\t\turl:      \"file:///tmp/repo.git\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tresult := isSSHURL(testCase.url)\n\t\t\tif result != testCase.expected {\n\t\t\t\tt.Errorf(\"Expected %v for %q, got %v\", testCase.expected, testCase.url, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/plugin/github.go",
    "content": "package plugin\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/cachehash\"\n\t\"go.jetify.com/devbox/nix/flake\"\n\t\"go.jetify.com/pkg/filecache\"\n)\n\nvar githubCache = filecache.New[[]byte](\"devbox/plugin/github\")\n\ntype githubPlugin struct {\n\tref  flake.Ref\n\tname string\n}\n\n// Github only allows alphanumeric, hyphen, underscore, and period in repo names.\n// but we clean up just in case.\nvar githubNameRegexp = regexp.MustCompile(\"[^a-zA-Z0-9-_.]+\")\n\nfunc newGithubPlugin(ref flake.Ref) (*githubPlugin, error) {\n\tplugin := &githubPlugin{ref: ref}\n\t// For backward compatibility, we don't strictly require name to be present\n\t// in github plugins. If it's missing, we just use the directory as the name.\n\tname, err := getPluginNameFromContent(plugin)\n\tif err != nil && !errors.Is(err, errNameMissing) {\n\t\treturn nil, err\n\t}\n\tif name == \"\" {\n\t\tname = strings.ReplaceAll(ref.Dir, \"/\", \"-\")\n\t}\n\tplugin.name = githubNameRegexp.ReplaceAllString(\n\t\tstrings.Join(lo.Compact([]string{ref.Owner, ref.Repo, name}), \".\"),\n\t\t\" \",\n\t)\n\treturn plugin, nil\n}\n\nfunc (p *githubPlugin) Fetch() ([]byte, error) {\n\tcontent, err := p.FileContent(pluginConfigName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn jsonPurifyPluginContent(content)\n}\n\nfunc (p *githubPlugin) CanonicalName() string {\n\treturn p.name\n}\n\nfunc (p *githubPlugin) Hash() string {\n\treturn cachehash.Bytes([]byte(p.ref.String()))\n}\n\nfunc (p *githubPlugin) FileContent(subpath string) ([]byte, error) {\n\tcontentURL, err := p.url(subpath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Cache for 24 hours. Once we store the plugin in the lockfile, we\n\t// should cache this indefinitely and only invalidate if the plugin\n\t// is updated.\n\tttl := 24 * time.Hour\n\n\t// This is a stopgap until plugin is stored in lockfile.\n\t// DEVBOX_X indicates this is an experimental env var.\n\t// Use DEVBOX_X_GITHUB_PLUGIN_CACHE_TTL to override the default TTL.\n\t// e.g. DEVBOX_X_GITHUB_PLUGIN_CACHE_TTL=1h will cache the plugin for 1 hour.\n\t// Note: If you want to disable cache, we recommend using a low second value instead of zero to\n\t// ensure only one network request is made.\n\tttlStr := os.Getenv(\"DEVBOX_X_GITHUB_PLUGIN_CACHE_TTL\")\n\tif ttlStr != \"\" {\n\t\tttl, err = time.ParseDuration(ttlStr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn githubCache.GetOrSet(\n\t\tcontentURL+ttlStr,\n\t\tfunc() ([]byte, time.Duration, error) {\n\t\t\treq, err := p.request(contentURL)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, err\n\t\t\t}\n\n\t\t\tclient := &http.Client{}\n\t\t\tres, err := client.Do(req)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, err\n\t\t\t}\n\t\t\tdefer res.Body.Close()\n\t\t\tif res.StatusCode != http.StatusOK {\n\t\t\t\tauthInfo := \"No auth header was sent with this request.\"\n\t\t\t\tif req.Header.Get(\"Authorization\") != \"\" {\n\t\t\t\t\tauthInfo = fmt.Sprintf(\n\t\t\t\t\t\t\"The auth header `%s` was sent with this request.\",\n\t\t\t\t\t\tgetRedactedAuthHeader(req),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\treturn nil, 0, usererr.New(\n\t\t\t\t\t\"failed to get plugin %s @ %s (Status code %d).\\n%s\\nPlease make \"+\n\t\t\t\t\t\t\"sure a plugin.json file exists in plugin directory.\",\n\t\t\t\t\tp.LockfileKey(),\n\t\t\t\t\treq.URL.String(),\n\t\t\t\t\tres.StatusCode,\n\t\t\t\t\tauthInfo,\n\t\t\t\t)\n\t\t\t}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, err\n\t\t\t}\n\n\t\t\treturn body, ttl, nil\n\t\t},\n\t)\n}\n\nfunc (p *githubPlugin) url(subpath string) (string, error) {\n\t// Github redirects \"master\" to \"main\" in new repos. They don't do the reverse\n\t// so setting master here is better.\n\treturn url.JoinPath(\n\t\t\"https://raw.githubusercontent.com/\",\n\t\tp.ref.Owner,\n\t\tp.ref.Repo,\n\t\tcmp.Or(p.ref.Rev, p.ref.Ref, \"master\"),\n\t\tp.ref.Dir,\n\t\tsubpath,\n\t)\n}\n\nfunc (p *githubPlugin) request(contentURL string) (*http.Request, error) {\n\treq, err := http.NewRequest(http.MethodGet, contentURL, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Add github token to request if available\n\tghToken := os.Getenv(\"GITHUB_TOKEN\")\n\n\tif ghToken != \"\" {\n\t\tauthValue := fmt.Sprintf(\"token %s\", ghToken)\n\t\treq.Header.Add(\"Authorization\", authValue)\n\t\tslog.Debug(\n\t\t\t\"GITHUB_TOKEN env var found, adding to request's auth header\",\n\t\t\t\"headerValue\",\n\t\t\tgetRedactedAuthHeader(req),\n\t\t)\n\t}\n\n\treturn req, nil\n}\n\nfunc (p *githubPlugin) LockfileKey() string {\n\treturn p.ref.String()\n}\n\nfunc getRedactedAuthHeader(req *http.Request) string {\n\tauthHeader := req.Header.Get(\"Authorization\")\n\tparts := strings.SplitN(authHeader, \" \", 2)\n\n\tif len(authHeader) < 10 || len(parts) < 2 {\n\t\t// too short to safely reveal any part\n\t\treturn strings.Repeat(\"*\", len(authHeader))\n\t}\n\n\tauthType, token := parts[0], parts[1]\n\tif len(token) < 10 {\n\t\t// second word too short to reveal any, but show first word\n\t\treturn authType + \" \" + strings.Repeat(\"*\", len(token))\n\t}\n\n\t// show first 4 chars of token to help with debugging (will often be \"ghp_\")\n\treturn authType + \" \" + token[:4] + strings.Repeat(\"*\", len(token)-4)\n}\n"
  },
  {
    "path": "internal/plugin/github_test.go",
    "content": "package plugin\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\nfunc TestNewGithubPlugin(t *testing.T) {\n\ttestCases := []struct {\n\t\tname        string\n\t\tInclude     string\n\t\texpected    githubPlugin\n\t\texpectedURL string\n\t}{\n\t\t{\n\t\t\tname:    \"parse basic github plugin\",\n\t\t\tInclude: \"github:jetify-com/devbox-plugins\",\n\t\t\texpected: githubPlugin{\n\t\t\t\tref: flake.Ref{\n\t\t\t\t\tType:  \"github\",\n\t\t\t\t\tOwner: \"jetify-com\",\n\t\t\t\t\tRepo:  \"devbox-plugins\",\n\t\t\t\t},\n\t\t\t\tname: \"jetify-com.devbox-plugins\",\n\t\t\t},\n\t\t\texpectedURL: \"https://raw.githubusercontent.com/jetify-com/devbox-plugins/master\",\n\t\t},\n\t\t{\n\t\t\tname:    \"parse github plugin with dir param\",\n\t\t\tInclude: \"github:jetify-com/devbox-plugins?dir=mongodb\",\n\t\t\texpected: githubPlugin{\n\t\t\t\tref: flake.Ref{\n\t\t\t\t\tType:  \"github\",\n\t\t\t\t\tOwner: \"jetify-com\",\n\t\t\t\t\tRepo:  \"devbox-plugins\",\n\t\t\t\t\tDir:   \"mongodb\",\n\t\t\t\t},\n\t\t\t\tname: \"jetify-com.devbox-plugins.mongodb\",\n\t\t\t},\n\t\t\texpectedURL: \"https://raw.githubusercontent.com/jetify-com/devbox-plugins/master/mongodb\",\n\t\t},\n\t\t{\n\t\t\tname:    \"parse github plugin with dir param and rev\",\n\t\t\tInclude: \"github:jetify-com/devbox-plugins/my-branch?dir=mongodb\",\n\t\t\texpected: githubPlugin{\n\t\t\t\tref: flake.Ref{\n\t\t\t\t\tType:  \"github\",\n\t\t\t\t\tOwner: \"jetify-com\",\n\t\t\t\t\tRepo:  \"devbox-plugins\",\n\t\t\t\t\tRef:   \"my-branch\",\n\t\t\t\t\tDir:   \"mongodb\",\n\t\t\t\t},\n\t\t\t\tname: \"jetify-com.devbox-plugins.mongodb\",\n\t\t\t},\n\t\t\texpectedURL: \"https://raw.githubusercontent.com/jetify-com/devbox-plugins/my-branch/mongodb\",\n\t\t},\n\t\t{\n\t\t\tname:    \"parse github plugin with dir param and rev\",\n\t\t\tInclude: \"github:jetify-com/devbox-plugins/initials/my-branch?dir=mongodb\",\n\t\t\texpected: githubPlugin{\n\t\t\t\tref: flake.Ref{\n\t\t\t\t\tType:  \"github\",\n\t\t\t\t\tOwner: \"jetify-com\",\n\t\t\t\t\tRepo:  \"devbox-plugins\",\n\t\t\t\t\tRef:   \"initials/my-branch\",\n\t\t\t\t\tDir:   \"mongodb\",\n\t\t\t\t},\n\t\t\t\tname: \"jetify-com.devbox-plugins.mongodb\",\n\t\t\t},\n\t\t\texpectedURL: \"https://raw.githubusercontent.com/jetify-com/devbox-plugins/initials/my-branch/mongodb\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tactual, err := newGithubPluginForTest(testCase.Include)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, &testCase.expected, actual)\n\t\t\tu, err := testCase.expected.url(\"\")\n\t\t\tassert.Nil(t, err)\n\t\t\tassert.Equal(t, testCase.expectedURL, u)\n\t\t})\n\t}\n}\n\n// keep in sync with newGithubPlugin\nfunc newGithubPluginForTest(include string) (*githubPlugin, error) {\n\tref, err := flake.ParseRef(include)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tplugin := &githubPlugin{ref: ref}\n\tname := strings.ReplaceAll(ref.Dir, \"/\", \"-\")\n\tplugin.name = githubNameRegexp.ReplaceAllString(\n\t\tstrings.Join(lo.Compact([]string{ref.Owner, ref.Repo, name}), \".\"),\n\t\t\" \",\n\t)\n\treturn plugin, nil\n}\n\nfunc TestGithubPluginAuth(t *testing.T) {\n\tgithubPlugin := githubPlugin{\n\t\tref: flake.Ref{\n\t\t\tType:  \"github\",\n\t\t\tOwner: \"jetpack-io\",\n\t\t\tRepo:  \"devbox-plugins\",\n\t\t},\n\t\tname: \"jetpack-io.devbox-plugins\",\n\t}\n\n\texpectedURL := \"https://raw.githubusercontent.com/jetpack-io/devbox-plugins/master/test\"\n\n\tt.Run(\"generate request for public Github repository\", func(t *testing.T) {\n\t\tt.Setenv(\"GITHUB_TOKEN\", \"\")\n\t\turl, err := githubPlugin.url(\"test\")\n\t\tassert.NoError(t, err)\n\t\tactual, err := githubPlugin.request(url)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedURL, actual.URL.String())\n\t\tassert.Equal(t, \"\", actual.Header.Get(\"Authorization\"))\n\t})\n\n\tt.Run(\"generate request for private Github repository\", func(t *testing.T) {\n\t\tt.Setenv(\"GITHUB_TOKEN\", \"gh_abcd\")\n\t\turl, err := githubPlugin.url(\"test\")\n\t\tassert.NoError(t, err)\n\t\tactual, err := githubPlugin.request(url)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedURL, actual.URL.String())\n\t\tassert.Equal(t, \"token gh_abcd\", actual.Header.Get(\"Authorization\"))\n\t})\n}\n\nfunc TestGetRedactedAuthHeader(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\tauthHeader string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\t\"normal length token partially readable for debugging\",\n\t\t\t\"token ghp_61b296fb898349778e20532cb65ce38e\",\n\t\t\t\"token ghp_********************************\",\n\t\t},\n\t\t{\n\t\t\t\"short token redacted\",\n\t\t\t\"token ghp_61b29\",\n\t\t\t\"token *********\",\n\t\t},\n\t\t{\n\t\t\t\"short header fully redacted\",\n\t\t\t\"token xyz\",\n\t\t\t\"*********\",\n\t\t},\n\t\t{\n\t\t\t\"no token returns empty string\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\treq, err := http.NewRequest(http.MethodGet, \"https://example.com\", nil)\n\t\t\tassert.NoError(t, err)\n\t\t\treq.Header.Add(\"Authorization\", testCase.authHeader)\n\t\t\tassert.Equal(t, testCase.expected, getRedactedAuthHeader(req))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/plugin/includable.go",
    "content": "package plugin\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\ntype Includable interface {\n\tCanonicalName() string\n\tFileContent(subpath string) ([]byte, error)\n\tHash() string\n\tLockfileKey() string\n}\n\nfunc parseIncludable(includableRef, workingDir string) (Includable, error) {\n\tref, err := flake.ParseRef(includableRef)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch ref.Type {\n\tcase flake.TypePath:\n\t\treturn newLocalPlugin(ref, workingDir)\n\tcase flake.TypeGitHub:\n\t\treturn newGithubPlugin(ref)\n\tcase flake.TypeGit:\n\t\treturn newGitPlugin(ref)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported ref type %q\", ref.Type)\n\t}\n}\n\ntype fetcher interface {\n\tIncludable\n\tFetch() ([]byte, error)\n}\n\nvar (\n\tnameRegex      = regexp.MustCompile(`^[a-zA-Z0-9_\\- ]+$`)\n\terrNameMissing = usererr.New(\"'name' is missing\")\n)\n\nfunc getPluginNameFromContent(plugin fetcher) (string, error) {\n\tcontent, err := plugin.Fetch()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tm := map[string]any{}\n\tif err := json.Unmarshal(content, &m); err != nil {\n\t\treturn \"\", err\n\t}\n\tname, ok := m[\"name\"].(string)\n\tif !ok || name == \"\" {\n\t\treturn \"\",\n\t\t\tfmt.Errorf(\"%w in plugin %s\", errNameMissing, plugin.LockfileKey())\n\t}\n\tif !nameRegex.MatchString(name) {\n\t\treturn \"\", usererr.New(\n\t\t\t\"plugin %s has an invalid name %q. Name must match %s\",\n\t\t\tplugin.LockfileKey(), name, nameRegex,\n\t\t)\n\t}\n\treturn name, nil\n}\n"
  },
  {
    "path": "internal/plugin/includes.go",
    "content": "package plugin\n\nimport (\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/lock\"\n)\n\nfunc LoadConfigFromInclude(include string, lockfile *lock.File, workingDir string) (*Config, error) {\n\tvar includable Includable\n\tvar err error\n\tif t, name, _ := strings.Cut(include, \":\"); t == \"plugin\" {\n\t\tincludable = devpkg.PackageFromStringWithDefaults(\n\t\t\tname,\n\t\t\tlockfile,\n\t\t)\n\t} else {\n\t\tincludable, err = parseIncludable(include, workingDir)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn getConfigIfAny(includable, lockfile.ProjectDir())\n}\n"
  },
  {
    "path": "internal/plugin/info.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage plugin\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"runtime/trace\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/services\"\n)\n\nfunc Readme(ctx context.Context,\n\tpkg *devpkg.Package,\n\tprojectDir string,\n\tmarkdown bool,\n) (string, error) {\n\tdefer trace.StartRegion(ctx, \"Readme\").End()\n\n\tcfg, err := getConfigIfAny(pkg, projectDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif cfg == nil {\n\t\treturn \"\", nil\n\t}\n\n\tbuf := bytes.NewBuffer(nil)\n\n\t_, _ = fmt.Fprintln(buf, \"\")\n\n\tif err = printReadme(cfg, buf, markdown); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err = printServices(cfg, pkg, buf, markdown); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err = printCreateFiles(cfg, buf, markdown); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err = printEnv(cfg, buf, markdown); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err = printInfoInstructions(pkg.CanonicalName(), buf); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn buf.String(), nil\n}\n\nfunc printReadme(cfg *Config, w io.Writer, markdown bool) error {\n\tif cfg.Description() == \"\" {\n\t\treturn nil\n\t}\n\t_, err := fmt.Fprintf(\n\t\tw,\n\t\t\"%s%s NOTES:\\n%s\\n\\n\",\n\t\tlo.Ternary(markdown, \"### \", \"\"),\n\t\tcfg.Name,\n\t\tcfg.Description(),\n\t)\n\treturn errors.WithStack(err)\n}\n\nfunc printServices(cfg *Config, pkg *devpkg.Package, w io.Writer, markdown bool) error {\n\t_, contentPath := cfg.ProcessComposeYaml()\n\tif contentPath == \"\" {\n\t\treturn nil\n\t}\n\tcontent, err := pkg.FileContent(contentPath)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tserviceNames, err := services.NamesFromProcessCompose(content)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tif len(serviceNames) == 0 {\n\t\treturn nil\n\t}\n\tservices := \"\"\n\tfor _, serviceName := range serviceNames {\n\t\tservices += fmt.Sprintf(\"* %[1]s\\n\", serviceName)\n\t}\n\n\t_, err = fmt.Fprintf(\n\t\tw,\n\t\t\"%sServices:\\n%s\\nUse `devbox services start|stop [service]` to interact with services\\n\\n\",\n\t\tlo.Ternary(markdown, \"### \", \"\"),\n\t\tservices,\n\t)\n\treturn errors.WithStack(err)\n}\n\nfunc printCreateFiles(cfg *Config, w io.Writer, markdown bool) error {\n\tif len(cfg.CreateFiles) == 0 {\n\t\treturn nil\n\t}\n\n\tshims := \"\"\n\tfor name, src := range cfg.CreateFiles {\n\t\tif src != \"\" {\n\t\t\tshims += fmt.Sprintf(\"* %s\\n\", name)\n\t\t}\n\t}\n\n\t_, err := fmt.Fprintf(\n\t\tw,\n\t\t\"%sThis plugin creates the following helper files:\\n%s\\n\",\n\t\tlo.Ternary(markdown, \"### \", \"\"),\n\t\tshims,\n\t)\n\treturn errors.WithStack(err)\n}\n\nfunc printEnv(cfg *Config, w io.Writer, markdown bool) error {\n\tif len(cfg.Env) == 0 {\n\t\treturn nil\n\t}\n\n\tenvVars := \"\"\n\tfor name, value := range cfg.Env {\n\t\tenvVars += fmt.Sprintf(\"* %s=%s\\n\", name, value)\n\t}\n\n\t_, err := fmt.Fprintf(\n\t\tw,\n\t\t\"%sThis plugin sets the following environment variables:\\n%s\\n\",\n\t\tlo.Ternary(markdown, \"### \", \"\"),\n\t\tenvVars,\n\t)\n\treturn errors.WithStack(err)\n}\n\nfunc printInfoInstructions(pkg string, w io.Writer) error {\n\t_, err := fmt.Fprintf(\n\t\tw,\n\t\t\"To show this information, run `devbox info %s`\\n\\n\",\n\t\tpkg,\n\t)\n\treturn errors.WithStack(err)\n}\n"
  },
  {
    "path": "internal/plugin/local.go",
    "content": "package plugin\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/cachehash\"\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\ntype LocalPlugin struct {\n\tref       flake.Ref\n\tname      string\n\tpluginDir string\n}\n\nfunc newLocalPlugin(ref flake.Ref, pluginDir string) (*LocalPlugin, error) {\n\tplugin := &LocalPlugin{ref: ref, pluginDir: pluginDir}\n\tname, err := getPluginNameFromContent(plugin)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tplugin.name = name\n\treturn plugin, nil\n}\n\nfunc (l *LocalPlugin) Fetch() ([]byte, error) {\n\tcontent, err := os.ReadFile(addFilenameIfMissing(l.Path()))\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn jsonPurifyPluginContent(content)\n}\n\nfunc (l *LocalPlugin) CanonicalName() string {\n\treturn l.name\n}\n\nfunc (l *LocalPlugin) IsLocal() bool {\n\treturn true\n}\n\nfunc (l *LocalPlugin) Hash() string {\n\treturn cachehash.Bytes([]byte(filepath.Clean(l.Path())))\n}\n\nfunc (l *LocalPlugin) FileContent(subpath string) ([]byte, error) {\n\treturn os.ReadFile(filepath.Join(filepath.Dir(l.Path()), subpath))\n}\n\nfunc (l *LocalPlugin) LockfileKey() string {\n\treturn l.ref.String()\n}\n\nfunc (l *LocalPlugin) Path() string {\n\tpath := os.ExpandEnv(l.ref.Path)\n\tif !strings.HasSuffix(path, pluginConfigName) {\n\t\tpath = filepath.Join(path, pluginConfigName)\n\t}\n\tif filepath.IsAbs(path) {\n\t\treturn path\n\t}\n\treturn filepath.Join(l.pluginDir, path)\n}\n\nfunc addFilenameIfMissing(s string) string {\n\tif strings.HasSuffix(s, pluginConfigName) {\n\t\treturn s\n\t}\n\treturn filepath.Join(s, pluginConfigName)\n}\n"
  },
  {
    "path": "internal/plugin/manager.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage plugin\n\nimport (\n\t\"go.jetify.com/devbox/internal/lock\"\n)\n\ntype Manager struct {\n\tdevboxProject\n\n\tlockfile *lock.File\n}\n\ntype devboxProject interface {\n\tAllPackageNamesIncludingRemovedTriggerPackages() []string\n\tProjectDir() string\n}\n\ntype managerOption func(*Manager)\n\nfunc NewManager(opts ...managerOption) *Manager {\n\tm := &Manager{}\n\tm.ApplyOptions(opts...)\n\treturn m\n}\n\nfunc WithLockfile(lockfile *lock.File) managerOption {\n\treturn func(m *Manager) {\n\t\tm.lockfile = lockfile\n\t}\n}\n\nfunc WithDevbox(provider devboxProject) managerOption {\n\treturn func(m *Manager) {\n\t\tm.devboxProject = provider\n\t}\n}\n\nfunc (m *Manager) ApplyOptions(opts ...managerOption) {\n\tfor _, opt := range opts {\n\t\topt(m)\n\t}\n}\n"
  },
  {
    "path": "internal/plugin/plugin.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage plugin\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"encoding/json\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/tailscale/hujson\"\n\t\"go.jetify.com/devbox/internal/devconfig/configfile\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/services\"\n)\n\nconst (\n\t// TODO rename to devboxPluginUserConfigDirName\n\tdevboxDirName       = \"devbox.d\"\n\tdevboxHiddenDirName = \".devbox\"\n\tpluginConfigName    = \"plugin.json\"\n)\n\nvar (\n\tVirtenvPath    = filepath.Join(devboxHiddenDirName, \"virtenv\")\n\tVirtenvBinPath = filepath.Join(VirtenvPath, \"bin\")\n)\n\ntype Config struct {\n\tconfigfile.ConfigFile\n\tPluginOnlyData\n}\n\ntype PluginOnlyData struct {\n\tCreateFiles           map[string]string `json:\"create_files\"`\n\tDeprecatedDescription string            `json:\"readme\"`\n\t// If true, we remove the package that triggered this plugin from the environment\n\t// Useful when we want to replace with flake\n\tRemoveTriggerPackage bool   `json:\"__remove_trigger_package,omitempty\"`\n\tVersion              string `json:\"version\"`\n\t// Source is the includable that triggered this plugin. There are two ways to include a plugin:\n\t// 1. Built-in plugins are triggered by packages (See plugins.builtInMap)\n\t// 2. Plugins can be added via the \"include\" field in devbox.json or plugin.json\n\tSource Includable\n}\n\nfunc (c *Config) ProcessComposeYaml() (string, string) {\n\tfor file, contentPath := range c.CreateFiles {\n\t\tif strings.HasSuffix(file, \"process-compose.yaml\") || strings.HasSuffix(file, \"process-compose.yml\") {\n\t\t\treturn file, contentPath\n\t\t}\n\t}\n\treturn \"\", \"\"\n}\n\nfunc (c *Config) Services() (services.Services, error) {\n\tif file, _ := c.ProcessComposeYaml(); file != \"\" {\n\t\treturn services.FromProcessCompose(file)\n\t}\n\treturn nil, nil\n}\n\nfunc (m *Manager) CreateFilesForConfig(cfg *Config) error {\n\tvirtenvPath := filepath.Join(m.ProjectDir(), VirtenvPath)\n\tpkg := cfg.Source\n\tlocked := m.lockfile.Packages[pkg.LockfileKey()]\n\n\tname := pkg.CanonicalName()\n\n\t// Always create this dir because some plugins depend on it.\n\tif err := createDir(filepath.Join(virtenvPath, name)); err != nil {\n\t\treturn err\n\t}\n\n\tslog.Debug(\"creating files for package\", \"pkg\", pkg)\n\tfor filePath, contentPath := range cfg.CreateFiles {\n\t\tif !m.shouldCreateFile(locked, filePath) {\n\t\t\tcontinue\n\t\t}\n\n\t\tdirPath := filepath.Dir(filePath)\n\t\tif contentPath == \"\" {\n\t\t\tdirPath = filePath\n\t\t}\n\t\tif err := createDir(dirPath); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\n\t\tif contentPath == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := m.createFile(pkg, filePath, contentPath, virtenvPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *Manager) UpdateLockfileVersion(cfg *Config) error {\n\tpkg := cfg.Source\n\tlocked := m.lockfile.Packages[pkg.LockfileKey()]\n\t// plugins that are not triggered by packages don't have a lockfile entry\n\t// this may change if we decide to store all plugins in the lockfile\n\tif locked == nil {\n\t\treturn nil\n\t}\n\tlocked.PluginVersion = cfg.Version\n\treturn m.lockfile.Save()\n}\n\nfunc (m *Manager) createFile(\n\tpkg Includable,\n\tfilePath, contentPath, virtenvPath string,\n) error {\n\tname := pkg.CanonicalName()\n\tslog.Debug(\"Creating file %q from contentPath: %q\", filePath, contentPath)\n\tcontent, err := pkg.FileContent(contentPath)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\ttmpl, err := template.New(filePath + \"-template\").Parse(string(content))\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tvar urlForInput, attributePath string\n\n\tif pkg, ok := pkg.(*devpkg.Package); ok {\n\t\tattributePath, err = pkg.PackageAttributePath()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\turlForInput = pkg.URLForFlakeInput()\n\t}\n\n\tvar buf bytes.Buffer\n\tif err = tmpl.Execute(&buf, map[string]any{\n\t\t\"DevboxDir\":            filepath.Join(m.ProjectDir(), devboxDirName, name),\n\t\t\"DevboxDirRoot\":        filepath.Join(m.ProjectDir(), devboxDirName),\n\t\t\"DevboxProfileDefault\": filepath.Join(m.ProjectDir(), nix.ProfilePath),\n\t\t\"PackageAttributePath\": attributePath,\n\t\t\"Packages\":             m.AllPackageNamesIncludingRemovedTriggerPackages(),\n\t\t\"System\":               nix.System(),\n\t\t\"URLForInput\":          urlForInput,\n\t\t\"Virtenv\":              filepath.Join(virtenvPath, name),\n\t}); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tvar fileMode fs.FileMode = 0o644\n\tif strings.Contains(filePath, \"bin/\") {\n\t\tfileMode = 0o755\n\t}\n\n\tif err := os.WriteFile(filePath, buf.Bytes(), fileMode); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tif fileMode == 0o755 {\n\t\tif err := createSymlink(m.ProjectDir(), filePath); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// buildConfig returns a plugin.Config\nfunc buildConfig(pkg Includable, projectDir, content string) (*Config, error) {\n\tcfg := &Config{PluginOnlyData: PluginOnlyData{Source: pkg}}\n\tname := pkg.CanonicalName()\n\tt, err := template.New(name + \"-template\").Parse(content)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tvar buf bytes.Buffer\n\tif err = t.Execute(&buf, map[string]string{\n\t\t\"DevboxProjectDir\":     projectDir,\n\t\t\"DevboxDir\":            filepath.Join(projectDir, devboxDirName, name),\n\t\t\"DevboxDirRoot\":        filepath.Join(projectDir, devboxDirName),\n\t\t\"DevboxProfileDefault\": filepath.Join(projectDir, nix.ProfilePath),\n\t\t\"Virtenv\":              filepath.Join(projectDir, VirtenvPath, name),\n\t}); err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\tjsonb, err := jsonPurifyPluginContent(buf.Bytes())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cfg, errors.WithStack(json.Unmarshal(jsonb, cfg))\n}\n\nfunc jsonPurifyPluginContent(content []byte) ([]byte, error) {\n\treturn hujson.Standardize(slices.Clone(content))\n}\n\nfunc createDir(path string) error {\n\tif path == \"\" {\n\t\treturn nil\n\t}\n\treturn errors.WithStack(os.MkdirAll(path, 0o755))\n}\n\nfunc createSymlink(root, filePath string) error {\n\tname := filepath.Base(filePath)\n\tnewname := filepath.Join(root, VirtenvBinPath, name)\n\n\t// Create bin path just in case it doesn't exist\n\tif err := os.MkdirAll(filepath.Join(root, VirtenvBinPath), 0o755); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tif _, err := os.Lstat(newname); err == nil {\n\t\tif err = os.Remove(newname); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\n\treturn errors.WithStack(os.Symlink(filePath, newname))\n}\n\nfunc (m *Manager) shouldCreateFile(\n\tpkg *lock.Package,\n\tfilePath string,\n) bool {\n\tsep := string(filepath.Separator)\n\n\t// Only create files in devbox.d directory if they are not in the lockfile\n\tpluginInstalled := pkg != nil && pkg.PluginVersion != \"\"\n\tif strings.Contains(filePath, sep+devboxDirName+sep) && pluginInstalled {\n\t\treturn false\n\t}\n\n\t// Hidden .devbox files are always replaceable, so ok to recreate\n\tif strings.Contains(filePath, sep+devboxHiddenDirName+sep) {\n\t\treturn true\n\t}\n\t_, err := os.Stat(filePath)\n\t// File doesn't exist, so we should create it.\n\treturn errors.Is(err, fs.ErrNotExist)\n}\n\nfunc (c *Config) Description() string {\n\tif c == nil {\n\t\treturn \"\"\n\t}\n\treturn cmp.Or(c.ConfigFile.Description, c.DeprecatedDescription)\n}\n"
  },
  {
    "path": "internal/plugin/rm.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage plugin\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/pkg/errors\"\n)\n\nfunc Remove(projectDir string, pkgs []string) error {\n\tfor _, pkg := range pkgs {\n\t\tif err := os.RemoveAll(filepath.Join(projectDir, VirtenvPath, pkg)); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc RemoveInvalidSymlinks(projectDir string) error {\n\tbinPath := filepath.Join(projectDir, VirtenvBinPath)\n\tif _, err := os.Stat(binPath); errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil\n\t}\n\tdirEntry, err := os.ReadDir(binPath)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tfor _, entry := range dirEntry {\n\t\t_, err := os.Stat(filepath.Join(projectDir, VirtenvBinPath, entry.Name()))\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\tos.Remove(filepath.Join(projectDir, VirtenvBinPath, entry.Name()))\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/plugin/services.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage plugin\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"go.jetify.com/devbox/internal/services\"\n)\n\nfunc GetServices(configs []*Config) (services.Services, error) {\n\tallSvcs := services.Services{}\n\tfor _, conf := range configs {\n\t\tsvcs, err := conf.Services()\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(\n\t\t\t\tos.Stderr,\n\t\t\t\t\"error reading services in plugin \\\"%s\\\", skipping\",\n\t\t\t\tconf.Name,\n\t\t\t)\n\t\t\tcontinue\n\t\t}\n\t\tfor name, svc := range svcs {\n\t\t\tallSvcs[name] = svc\n\t\t}\n\t}\n\n\treturn allSvcs, nil\n}\n"
  },
  {
    "path": "internal/plugin/update.go",
    "content": "package plugin\n\nfunc Update() error {\n\treturn githubCache.Clear()\n}\n"
  },
  {
    "path": "internal/pullbox/config.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage pullbox\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"go.jetify.com/devbox/internal/cuecfg\"\n\t\"go.jetify.com/devbox/internal/devconfig\"\n\t\"go.jetify.com/devbox/internal/fileutil\"\n)\n\nfunc (p *pullbox) IsTextDevboxConfig() bool {\n\tif u, err := url.Parse(p.URL); err == nil {\n\t\text := filepath.Ext(u.Path)\n\t\treturn cuecfg.IsSupportedExtension(ext)\n\t}\n\t// For invalid URLS, just look at the extension\n\text := filepath.Ext(p.URL)\n\treturn cuecfg.IsSupportedExtension(ext)\n}\n\nfunc (p *pullbox) pullTextDevboxConfig(ctx context.Context) error {\n\tif p.isLocalConfig() {\n\t\treturn p.copyToProfile(p.URL)\n\t}\n\n\tcfg, err := devconfig.LoadConfigFromURL(ctx, p.URL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttmpDir, err := fileutil.CreateDevboxTempDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err = cfg.Root.SaveTo(tmpDir); err != nil {\n\t\treturn err\n\t}\n\n\treturn p.copyToProfile(tmpDir)\n}\n\nfunc (p *pullbox) isLocalConfig() bool {\n\t_, err := os.Stat(p.URL)\n\treturn err == nil\n}\n"
  },
  {
    "path": "internal/pullbox/download.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage pullbox\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\n// Download downloads a file from the specified URL\nfunc download(url string) ([]byte, error) {\n\tresponse, err := http.Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer response.Body.Close()\n\n\tif response.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"failed to download file: %s\", response.Status)\n\t}\n\n\tdata, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn data, nil\n}\n"
  },
  {
    "path": "internal/pullbox/files.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage pullbox\n\nimport (\n\t\"io/fs\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/cmdutil\"\n\t\"go.jetify.com/devbox/internal/devconfig\"\n\t\"go.jetify.com/devbox/internal/devconfig/configfile\"\n\t\"go.jetify.com/devbox/internal/fileutil\"\n)\n\nfunc (p *pullbox) copyToProfile(src string) error {\n\tsrcFileInfo, err := os.Stat(src)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tvar srcFiles []fs.FileInfo\n\tif srcFileInfo.IsDir() {\n\t\tentries, err := os.ReadDir(src)\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\tfor _, entry := range entries {\n\t\t\tinfo, err := entry.Info()\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\tsrcFiles = append(srcFiles, info)\n\t\t}\n\t} else {\n\t\tsrcFiles = []fs.FileInfo{srcFileInfo}\n\t}\n\n\tif err := fileutil.ClearDir(p.ProjectDir()); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, srcFile := range srcFiles {\n\t\tsrcPath := src\n\t\tif srcFileInfo.IsDir() {\n\t\t\tsrcPath = filepath.Join(src, srcFile.Name())\n\t\t}\n\t\tcmd := cmdutil.CommandTTY(\"cp\", \"-rf\", srcPath, p.ProjectDir())\n\t\tif err := cmd.Run(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc profileIsNotEmpty(path string) (bool, error) {\n\tentries, err := os.ReadDir(path)\n\tif err != nil {\n\t\treturn false, errors.WithStack(err)\n\t}\n\tfor _, entry := range entries {\n\t\tif entry.Name() != configfile.DefaultName ||\n\t\t\tisModifiedConfig(filepath.Join(path, entry.Name())) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\nfunc isModifiedConfig(path string) bool {\n\tif filepath.Base(path) == configfile.DefaultName {\n\t\treturn !devconfig.IsDefault(path)\n\t}\n\treturn false\n}\n\n// urlIsArchive checks if a file URL points to an archive file\nfunc urlIsArchive(url string) (bool, error) {\n\tresponse, err := http.Head(url)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer response.Body.Close()\n\tcontentType := response.Header.Get(\"Content-Type\")\n\treturn strings.Contains(contentType, \"tar\") ||\n\t\tstrings.Contains(contentType, \"zip\") ||\n\t\tstrings.Contains(contentType, \"octet-stream\"), nil\n}\n"
  },
  {
    "path": "internal/pullbox/git/git.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage git\n\nimport (\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.jetify.com/devbox/internal/cmdutil\"\n\t\"go.jetify.com/devbox/internal/fileutil\"\n)\n\nfunc CloneToTmp(repo string) (string, error) {\n\ttmpDir, err := fileutil.CreateDevboxTempDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err := clone(repo, tmpDir); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn tmpDir, nil\n}\n\nfunc IsRepoURL(url string) bool {\n\t// For now only support ssh\n\treturn strings.HasPrefix(url, \"git@\") ||\n\t\t(strings.HasPrefix(url, \"https://\") && strings.HasSuffix(url, \".git\"))\n}\n\nfunc clone(repo, dir string) error {\n\tcmd := cmdutil.CommandTTY(\"git\", \"clone\", repo, dir)\n\tcmd.Dir = dir\n\terr := cmd.Run()\n\treturn errors.WithStack(err)\n}\n"
  },
  {
    "path": "internal/pullbox/git/push.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage git\n\nimport (\n\t\"context\"\n\t\"runtime/trace\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.jetify.com/devbox/internal/cmdutil\"\n\t\"go.jetify.com/devbox/internal/fileutil\"\n)\n\nconst nothingToCommitErrorText = \"nothing to commit\"\n\nfunc Push(ctx context.Context, dir, url string) error {\n\tdefer trace.StartRegion(ctx, \"Push\").End()\n\n\ttmpDir, err := fileutil.CreateDevboxTempDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := cloneGitHistory(url, tmpDir); err != nil {\n\t\treturn err\n\t}\n\n\tif err := fileutil.CopyAll(dir, tmpDir); err != nil {\n\t\treturn err\n\t}\n\n\tif err := createCommit(tmpDir); err != nil {\n\t\treturn err\n\t}\n\n\treturn push(tmpDir)\n}\n\nfunc cloneGitHistory(url, dst string) error {\n\t// See https://stackoverflow.com/questions/38999901/clone-only-the-git-directory-of-a-git-repo\n\tcmd := cmdutil.CommandTTY(\"git\", \"clone\", \"--no-checkout\", url, dst)\n\tcmd.Dir = dst\n\treturn errors.WithStack(cmd.Run())\n}\n\nfunc createCommit(dir string) error {\n\tcmd := cmdutil.CommandTTY(\"git\", \"add\", \".\")\n\tcmd.Dir = dir\n\tif err := cmd.Run(); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tcmd, buf := cmdutil.CommandTTYWithBuffer(\n\t\t\"git\", \"commit\", \"-m\", \"devbox commit\")\n\tcmd.Dir = dir\n\terr := cmd.Run()\n\tif strings.Contains(buf.String(), nothingToCommitErrorText) {\n\t\treturn nil\n\t}\n\treturn errors.WithStack(err)\n}\n\nfunc push(dir string) error {\n\tcmd := cmdutil.CommandTTY(\"git\", \"push\")\n\tcmd.Dir = dir\n\terr := cmd.Run()\n\treturn errors.WithStack(err)\n}\n"
  },
  {
    "path": "internal/pullbox/pullbox.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage pullbox\n\nimport (\n\t\"context\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime/trace\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/pullbox/git\"\n\t\"go.jetify.com/devbox/internal/pullbox/s3\"\n\t\"go.jetify.com/devbox/internal/pullbox/tar\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\ntype devboxProject interface {\n\tProjectDir() string\n}\n\ntype pullbox struct {\n\tdevboxProject\n\tdevopt.PullboxOpts\n}\n\nfunc New(devbox devboxProject, opts devopt.PullboxOpts) *pullbox {\n\treturn &pullbox{devbox, opts}\n}\n\n// Pull\n// This can be rewritten to be more readable and less repetitive. Possibly\n// something like:\n// puller := getPullerForURL(url)\n// return puller.Pull()\nfunc (p *pullbox) Pull(ctx context.Context) error {\n\tdefer trace.StartRegion(ctx, \"Pull\").End()\n\tvar err error\n\n\tnotEmpty, err := profileIsNotEmpty(p.ProjectDir())\n\tif err != nil {\n\t\treturn err\n\t} else if notEmpty && !p.Overwrite {\n\t\treturn fs.ErrExist\n\t}\n\n\tif p.URL != \"\" {\n\t\tux.Finfof(os.Stderr, \"Pulling global config from %s\\n\", p.URL)\n\t} else {\n\t\tux.Finfof(os.Stderr, \"Pulling global config\\n\")\n\t}\n\n\tvar tmpDir string\n\n\tif p.URL == \"\" {\n\t\tif p.Credentials.IDToken == \"\" {\n\t\t\treturn usererr.New(\"Not logged in\")\n\t\t}\n\t\tprofile := \"default\" // TODO: make this editable\n\t\tif tmpDir, err = s3.PullToTmp(ctx, &p.Credentials, profile); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn p.copyToProfile(tmpDir)\n\t}\n\n\tif git.IsRepoURL(p.URL) {\n\t\tif tmpDir, err = git.CloneToTmp(p.URL); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// Remove the .git directory, we don't want to keep state\n\t\tif err := os.RemoveAll(filepath.Join(tmpDir, \".git\")); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\treturn p.copyToProfile(tmpDir)\n\t}\n\n\tif p.IsTextDevboxConfig() {\n\t\treturn p.pullTextDevboxConfig(ctx)\n\t}\n\n\tif isArchive, err := urlIsArchive(p.URL); err != nil {\n\t\treturn err\n\t} else if isArchive {\n\t\tdata, err := download(p.URL)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif tmpDir, err = tar.Extract(data); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn p.copyToProfile(tmpDir)\n\t}\n\n\treturn usererr.New(\"Could not determine how to pull %s\", p.URL)\n}\n\nfunc (p *pullbox) Push(ctx context.Context) error {\n\tif p.URL != \"\" {\n\t\tux.Finfof(os.Stderr, \"Pushing global config to %s\\n\", p.URL)\n\t} else {\n\t\tux.Finfof(os.Stderr, \"Pushing global config\\n\")\n\t}\n\n\tif p.URL == \"\" {\n\t\tprofile := \"default\" // TODO: make this editable\n\t\tif p.Credentials.IDToken == \"\" {\n\t\t\treturn usererr.New(\"Not logged in\")\n\t\t}\n\t\tux.Finfof(\n\t\t\tos.Stderr,\n\t\t\t\"Logged in as %s, pushing to to devbox cloud (profile: %s)\\n\",\n\t\t\tp.Credentials.Email,\n\t\t\tprofile,\n\t\t)\n\t\treturn s3.Push(ctx, &p.Credentials, p.ProjectDir(), profile)\n\t}\n\treturn git.Push(ctx, p.ProjectDir(), p.URL)\n}\n"
  },
  {
    "path": "internal/pullbox/s3/config.go",
    "content": "package s3\n\nimport (\n\t\"context\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sts\"\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n)\n\n// TODO(landau): We could make these customizable so folks can use their own\n// buckets and roles. Would require removing user from this lib.\nconst (\n\troleArn = \"arn:aws:iam::984256416385:role/JetpackS3Federated\"\n\tbucket  = \"devbox.sh\"\n\t// this is a fixed value the bucket resides in this region, otherwise,\n\t// user's default region will get pulled from config and region mismatch\n\t// will result in user not being able to run global push\n\tregion = \"us-east-2\"\n)\n\nfunc assumeRole(ctx context.Context, c *devopt.Credentials) (*aws.Config, error) {\n\tnoPermsConfig, _ := config.LoadDefaultConfig(ctx)\n\tstsClient := sts.NewFromConfig(noPermsConfig)\n\tcreds, err := stsClient.AssumeRoleWithWebIdentity(\n\t\tctx,\n\t\t&sts.AssumeRoleWithWebIdentityInput{\n\t\t\tRoleArn:          aws.String(roleArn),\n\t\t\tRoleSessionName:  aws.String(c.Email),\n\t\t\tWebIdentityToken: aws.String(c.IDToken),\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconfig, err := config.LoadDefaultConfig(\n\t\tctx,\n\t\tconfig.WithCredentialsProvider(\n\t\t\tcredentials.NewStaticCredentialsProvider(\n\t\t\t\t*creds.Credentials.AccessKeyId,\n\t\t\t\t*creds.Credentials.SecretAccessKey,\n\t\t\t\t*creds.Credentials.SessionToken,\n\t\t\t),\n\t\t),\n\t)\n\tconfig.Region = region\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn &config, err\n}\n"
  },
  {
    "path": "internal/pullbox/s3/pull.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage s3\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/feature/s3/manager\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/pullbox/tar\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\nvar ErrProfileNotFound = errors.New(\"profile not found\")\n\nfunc PullToTmp(\n\tctx context.Context,\n\tcreds *devopt.Credentials,\n\tprofile string,\n) (string, error) {\n\tconfig, err := assumeRole(ctx, creds)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// TODO(landau), before pulling, ensure that the profile exists in the cloud\n\ts3Client := manager.NewDownloader(s3.NewFromConfig(*config))\n\tbuf := manager.WriteAtBuffer{}\n\n\tux.Finfof(\n\t\tos.Stderr,\n\t\t\"Logged in as %s, pulling from jetify cloud (profile: %s)\\n\",\n\t\tcreds.Email,\n\t\tprofile,\n\t)\n\n\tif _, err = s3Client.Download(\n\t\tctx,\n\t\t&buf,\n\t\t&s3.GetObjectInput{\n\t\t\tBucket: aws.String(bucket),\n\t\t\tKey: aws.String(\n\t\t\t\tfmt.Sprintf(\n\t\t\t\t\t\"profiles/%s/%s.tar.gz\",\n\t\t\t\t\tcreds.Sub,\n\t\t\t\t\tprofile,\n\t\t\t\t),\n\t\t\t),\n\t\t},\n\t\t// TODO, we can use an s3 list objects to make this more accurate\n\t); err != nil && strings.Contains(err.Error(), \"AccessDenied\") {\n\t\treturn \"\", ErrProfileNotFound\n\t} else if err != nil {\n\t\treturn \"\", errors.WithStack(err)\n\t}\n\n\tdir, err := tar.Extract(buf.Bytes())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tux.Fsuccessf(\n\t\tos.Stderr,\n\t\t\"Profile successfully pulled (profile: %s)\\n\",\n\t\tprofile,\n\t)\n\n\treturn dir, nil\n}\n"
  },
  {
    "path": "internal/pullbox/s3/push.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage s3\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/feature/s3/manager\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/pullbox/tar\"\n\t\"go.jetify.com/devbox/internal/ux\"\n)\n\nfunc Push(\n\tctx context.Context,\n\tcreds *devopt.Credentials,\n\tdir, profile string,\n) error {\n\tarchivePath, err := tar.Compress(dir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconfig, err := assumeRole(ctx, creds)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts3Client := manager.NewUploader(s3.NewFromConfig(*config))\n\tfile, err := os.Open(archivePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\t_, err = s3Client.Upload(ctx, &s3.PutObjectInput{\n\t\tBucket: aws.String(bucket),\n\t\tKey: aws.String(\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"profiles/%s/%s.tar.gz\",\n\t\t\t\tcreds.Sub,\n\t\t\t\tprofile,\n\t\t\t),\n\t\t),\n\t\tBody: io.Reader(file),\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tux.Fsuccessf(\n\t\tos.Stderr,\n\t\t\"Profile successfully pushed (profile: %s)\\n\",\n\t\tprofile,\n\t)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/pullbox/tar/tar.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage tar\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"syscall\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/cmdutil\"\n\t\"go.jetify.com/devbox/internal/fileutil\"\n)\n\n// extract decompresses a tar file and saves it to a tmp directory\nfunc Extract(data []byte) (string, error) {\n\ttempFile, err := os.CreateTemp(\"\", \"temp.tar.gz\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer os.Remove(tempFile.Name())\n\tdefer tempFile.Close()\n\n\t_, err = tempFile.Write(data)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\ttempDir, err := os.MkdirTemp(\"\", \"temp\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmd := exec.Command(\"tar\", \"-xf\", tempFile.Name(), \"-C\", tempDir)\n\n\tif err = cmd.Run(); err != nil {\n\t\tvar exitErr *exec.ExitError\n\t\tif errors.As(err, &exitErr) {\n\t\t\twaitStatus := exitErr.Sys().(syscall.WaitStatus)\n\t\t\treturn \"\", fmt.Errorf(\n\t\t\t\t\"tar extraction failed with exit code: %d\",\n\t\t\t\twaitStatus.ExitStatus(),\n\t\t\t)\n\t\t}\n\t\treturn \"\", err\n\t}\n\n\treturn tempDir, nil\n}\n\nfunc Compress(dir string) (string, error) {\n\ttmpDir, err := fileutil.CreateDevboxTempDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ttarget := filepath.Join(tmpDir, \"archive.tar.gz\")\n\tcmd := cmdutil.CommandTTY(\"tar\", \"-czf\", target, \".\")\n\n\tcmd.Dir = dir\n\n\tif err = cmd.Start(); err != nil {\n\t\treturn \"\", errors.WithStack(err)\n\t}\n\n\treturn target, nil\n}\n"
  },
  {
    "path": "internal/redact/redact.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\n// Package redact implements functions to redact sensitive information from\n// errors.\n//\n// Redacting an error replaces its message with a placeholder while still\n// maintaining wrapped errors:\n//\n//\twrapped := errors.New(\"not found\")\n//\tname := \"Alex\"\n//\terr := fmt.Errorf(\"error getting user %s: %w\", name, wrapped)\n//\n//\tfmt.Println(err)\n//\t// error getting user Alex: not found\n//\n//\tfmt.Println(Error(err))\n//\t// <redacted *fmt.wrapError>: <redacted *errors.errorString>\n//\n// If an error implements a Redact() string method, then it is said to be\n// redactable. A redactable error defines an alternative message for its\n// redacted form:\n//\n//\ttype userErr struct{ name string }\n//\n//\tfunc (e *userErr) Error() string {\n//\t\treturn fmt.Sprintf(\"user %s not found\", e.name)\n//\t}\n//\n//\tfunc (e *userErr) Redact() string {\n//\t\treturn fmt.Sprintf(\"user %x not found\", sha256.Sum256([]byte(e.name)))\n//\t}\n//\n//\tfunc main() {\n//\t\terr := &userErr{name: \"Alex\"}\n//\t\tfmt.Println(err)\n//\t\t// user Alex not found\n//\n//\t\tfmt.Println(Error(err))\n//\t\t// user db74c940d447e877d119df613edd2700c4a84cd1cf08beb7cbc319bcfaeab97a not found\n//\t}\n//\n// The [Errorf] function creates redactable errors that retain their literal\n// format text, but redact any arguments. The format string spec is identical\n// to that of [fmt.Errorf]. Calling [Safe] on an [Errorf] argument will include\n// it in the redacted message.\n//\n//\tname := \"Alex\"\n//\tid := 5\n//\terr := Errorf(\"error getting user %s with ID %d\", name, Safe(id))\n//\n//\tfmt.Println(err)\n//\t// error getting user Alex with ID 5\n//\n//\tfmt.Println(Error(err))\n//\t// error getting user <redacted string> with ID 5\n//\n//nolint:errorlint\npackage redact\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime\"\n)\n\n// Error returns a redacted error that wraps err. If err has a Redact() string\n// method, then Error uses it for the redacted error message. Otherwise, Error\n// recursively redacts each wrapped error, joining them with \": \" to create the\n// final error message. If it encounters an error that has a Redact() method,\n// then it appends the result of Redact() to the message and stops unwrapping.\nfunc Error(err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tswitch t := err.(type) {\n\tcase *redactedError:\n\t\t// Don't redact an already redacted error, otherwise its redacted message\n\t\t// will be replaced with a placeholder.\n\t\treturn err\n\tcase redactor:\n\t\treturn &redactedError{\n\t\t\tmsg:     t.Redact(),\n\t\t\twrapped: err,\n\t\t}\n\tdefault:\n\t\tmsg := placeholder(err)\n\t\twrapped := err\n\t\tfor {\n\t\t\twrapped = errors.Unwrap(wrapped)\n\t\t\tif wrapped == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif redactor, ok := wrapped.(redactor); ok {\n\t\t\t\tmsg += \": \" + redactor.Redact()\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tmsg += \": \" + placeholder(wrapped)\n\t\t}\n\t\treturn &redactedError{\n\t\t\tmsg:     msg,\n\t\t\twrapped: err,\n\t\t}\n\t}\n}\n\n// Errorf creates a redactable error that has an error string identical to that\n// of a [fmt.Errorf] error. Calling [Redact] on the result will redact all\n// format arguments from the error message instead of redacting the entire\n// string.\n//\n// When redacting the error string, Errorf replaces arguments that implement a\n// Redact() string method with the result of that method. To include an\n// argument as-is in the redacted error, first call [Safe]. For example:\n//\n//\tusername := \"bob\"\n//\tErrorf(\"cannot find user %s\", username).Error()\n//\t// cannot find user <redacted string>\n//\n//\tErrorf(\"cannot find user %s\", Safe(username)).Error()\n//\t// cannot find user bob\nfunc Errorf(format string, a ...any) error {\n\t// Capture a stack trace.\n\tsafeErr := &safeError{\n\t\tcallers: make([]uintptr, 32),\n\t}\n\tn := runtime.Callers(2, safeErr.callers)\n\tsafeErr.callers = safeErr.callers[:n]\n\n\t// Create the \"normal\" unredacted error. We need to remove the safe wrapper\n\t// from any args so that fmt.Errorf can detect and format their type\n\t// correctly.\n\targs := make([]any, len(a))\n\tfor i := range a {\n\t\tif safe, ok := a[i].(safe); ok {\n\t\t\targs[i] = safe.a\n\t\t} else {\n\t\t\targs[i] = a[i]\n\t\t}\n\t}\n\tsafeErr.err = fmt.Errorf(format, args...)\n\n\t// Now create the redacted error by replacing all args with their redacted\n\t// version or by inserting a placeholder if the arg can't be redacted.\n\tfor i := range a {\n\t\tswitch t := a[i].(type) {\n\t\tcase safe:\n\t\t\targs[i] = t.a\n\t\tcase error:\n\t\t\targs[i] = Error(t)\n\t\tcase redactor:\n\t\t\targs[i] = formatter(t.Redact())\n\t\tdefault:\n\t\t\targs[i] = formatter(placeholder(t))\n\t\t}\n\t}\n\tsafeErr.redacted = fmt.Errorf(format, args...)\n\treturn safeErr\n}\n\n// redactor defines the Redact interface for types that can format themselves\n// in redacted errors.\ntype redactor interface {\n\tRedact() string\n}\n\n// safe wraps a value that is marked as safe for including in a redacted error.\ntype safe struct{ a any }\n\n// Safe marks a value as safe for including in a redacted error.\nfunc Safe(a any) any {\n\treturn safe{a}\n}\n\n// safeError is an error that can redact its message.\ntype safeError struct {\n\terr      error\n\tredacted error\n\tcallers  []uintptr\n}\n\nfunc (e *safeError) Error() string  { return e.err.Error() }\nfunc (e *safeError) Redact() string { return e.redacted.Error() }\nfunc (e *safeError) Unwrap() error  { return e.err }\n\nfunc (e *safeError) StackTrace() []runtime.Frame {\n\tif len(e.callers) == 0 {\n\t\treturn nil\n\t}\n\tframeIter := runtime.CallersFrames(e.callers)\n\tframes := make([]runtime.Frame, 0, len(e.callers))\n\tfor {\n\t\tframe, more := frameIter.Next()\n\t\tframes = append(frames, frame)\n\t\tif !more {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn frames\n}\n\nfunc (e *safeError) Format(f fmt.State, verb rune) {\n\tswitch verb {\n\tcase 'v', 's':\n\t\tf.Write([]byte(e.Error())) //nolint:errcheck\n\t\tif f.Flag('+') {\n\t\t\tfor _, fr := range e.StackTrace() {\n\t\t\t\tfmt.Fprintf(f, \"\\n%s\\n\\t%s:%d\", fr.Function, fr.File, fr.Line)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\tcase 'q':\n\t\tfmt.Fprintf(f, \"%q\", e.Error())\n\t}\n}\n\n// redactedError is an error containing a redacted message. It is usually the\n// result of calling Error(safeError).\ntype redactedError struct {\n\tmsg     string\n\twrapped error\n}\n\nfunc (e *redactedError) Error() string { return e.msg }\nfunc (e *redactedError) Unwrap() error { return e.wrapped }\n\n// formatter allows a string to be formatted by any fmt verb.\n// For example, fmt.Sprintf(\"%d\", formatter(\"100\")) will return \"100\" without\n// an error.\ntype formatter string\n\nfunc (f formatter) Format(s fmt.State, verb rune) {\n\ts.Write([]byte(f)) //nolint:errcheck\n}\n\n// placeholder generates a placeholder string for values that don't satisfy\n// redactor.\nfunc placeholder(a any) string {\n\treturn fmt.Sprintf(\"<redacted %T>\", a)\n}\n"
  },
  {
    "path": "internal/redact/redact_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\n//nolint:errorlint\npackage redact\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc ExampleError() {\n\t// Each error string in a chain of wrapped errors is redacted with a\n\t// placeholder describing the error's type.\n\twrapped := errors.New(\"not found\")\n\tname := \"Alex\"\n\terr := fmt.Errorf(\"error getting user %s: %w\", name, wrapped)\n\n\tfmt.Println(\"Normal:\", err)\n\tfmt.Println(\"Redacted:\", Error(err))\n\t// Output:\n\t// Normal: error getting user Alex: not found\n\t// Redacted: <redacted *fmt.wrapError>: <redacted *errors.errorString>\n}\n\nfunc ExampleErrorf() {\n\t// Errors created with Errorf are redacted by omitting any arguments not\n\t// marked as safe. The literal portion of the format string is kept.\n\twrapped := errors.New(\"not found\")\n\tname := \"Alex\"\n\tid := 5\n\terr := Errorf(\"error getting user %s with ID %d: %w\", name, Safe(id), wrapped)\n\n\tfmt.Println(\"Normal:\", err)\n\tfmt.Println(\"Redacted:\", Error(err))\n\t// Output:\n\t// Normal: error getting user Alex with ID 5: not found\n\t// Redacted: error getting user <redacted string> with ID 5: <redacted *errors.errorString>\n}\n\nfunc ExampleError_wrapped() {\n\t// If an error wraps another, then redacting it results in a message with the\n\t// redacted version of each error in the chain up until the first redactable\n\t// error.\n\tname := \"Alex\"\n\terr := fmt.Errorf(\"fatal error: %w\",\n\t\tErrorf(\"error getting user %s: %w\", name,\n\t\t\terrors.New(\"not found\")))\n\n\tfmt.Println(\"Normal:\", err)\n\tfmt.Println(\"Redacted:\", Error(err))\n\t// Output:\n\t// Normal: fatal error: error getting user Alex: not found\n\t// Redacted: <redacted *fmt.wrapError>: error getting user <redacted string>: <redacted *errors.errorString>\n}\n\nfunc TestNil(t *testing.T) {\n\tcheckUnredactedError(t, nil, \"<nil>\")\n\tcheckRedactedError(t, nil, \"<nil>\")\n}\n\nfunc TestSimple(t *testing.T) {\n\terr := errors.New(\"simple\")\n\tcheckUnredactedError(t, err, \"simple\")\n\tcheckRedactedError(t, err, \"<redacted *errors.errorString>\")\n}\n\nfunc TestSimpleWrapSimple(t *testing.T) {\n\twrapped := errors.New(\"error 2\")\n\terr := fmt.Errorf(\"error 1: %w\", wrapped)\n\tcheckUnredactedError(t, err, \"error 1: error 2\")\n\tcheckRedactedError(t, err, \"<redacted *fmt.wrapError>: <redacted *errors.errorString>\")\n\tif !errors.Is(err, wrapped) {\n\t\tt.Error(\"got errors.Is(err, wrapped) == false\")\n\t}\n}\n\nfunc TestRedactor(t *testing.T) {\n\terr := &testRedactor{msg: \"sensitive\", redactedMsg: \"safe\"}\n\tcheckUnredactedError(t, err, \"sensitive\")\n\tcheckRedactedError(t, err, \"safe\")\n}\n\nfunc TestRedactorWrapRedactor(t *testing.T) {\n\twrapped := &testRedactor{\n\t\tmsg:         \"wrapped sensitive\",\n\t\tredactedMsg: \"wrapped safe\",\n\t}\n\terr := &testRedactor{\n\t\tmsg:         \"sensitive\",\n\t\tredactedMsg: \"safe\",\n\t\terr:         wrapped,\n\t}\n\tcheckUnredactedError(t, err, \"sensitive\")\n\tcheckRedactedError(t, err, \"safe\")\n\tif !errors.Is(err, wrapped) {\n\t\tt.Error(\"got errors.Is(err, wrapped) == false\")\n\t}\n}\n\nfunc TestSimpleWrapRedactor(t *testing.T) {\n\twrapped := &testRedactor{\n\t\tmsg:         \"wrapped sensitive\",\n\t\tredactedMsg: \"wrapped safe\",\n\t}\n\terr := fmt.Errorf(\"error: %w\", wrapped)\n\tcheckUnredactedError(t, err, \"error: wrapped sensitive\")\n\tcheckRedactedError(t, err, \"<redacted *fmt.wrapError>: wrapped safe\")\n\tif !errors.Is(err, wrapped) {\n\t\tt.Error(\"got errors.Is(err, wrapped) == false\")\n\t}\n}\n\nfunc TestNestedWrapRedactor(t *testing.T) {\n\tnestedWrapped := &testRedactor{\n\t\tmsg:         \"wrapped sensitive\",\n\t\tredactedMsg: \"wrapped safe\",\n\t}\n\twrapped := fmt.Errorf(\"error 2: %w\", nestedWrapped)\n\terr := fmt.Errorf(\"error 1: %w\", wrapped)\n\tcheckUnredactedError(t, err, \"error 1: error 2: wrapped sensitive\")\n\tcheckRedactedError(t, err, \"<redacted *fmt.wrapError>: <redacted *fmt.wrapError>: wrapped safe\")\n\tif !errors.Is(err, wrapped) {\n\t\tt.Error(\"got errors.Is(err, wrapped) == false\")\n\t}\n\tif !errors.Is(err, nestedWrapped) {\n\t\tt.Error(\"got errors.Is(err, nestedWrapped) == false\")\n\t}\n}\n\nfunc TestErrorf(t *testing.T) {\n\terr := Errorf(\"quoted = %q, quotedSafe = %q, int = %d, intSafe = %d\",\n\t\t\"sensitive\", Safe(\"safe\"),\n\t\t123, Safe(789),\n\t)\n\tcheckUnredactedError(t, err, `quoted = \"sensitive\", quotedSafe = \"safe\", int = 123, intSafe = 789`)\n\tcheckRedactedError(t, err, `quoted = <redacted string>, quotedSafe = \"safe\", int = <redacted int>, intSafe = 789`)\n\n\t// Redact again to check that we don't wipe out the already-redacted message.\n\tcheckRedactedError(t, Error(err), `quoted = <redacted string>, quotedSafe = \"safe\", int = <redacted int>, intSafe = 789`)\n}\n\nfunc TestErrorfWrapErrorf(t *testing.T) {\n\twrapped := Errorf(\"wrapped string = %s, wrapped safe string = %s\", \"sensitive\", Safe(\"safe\"))\n\terr := Errorf(\"error: %w\", wrapped)\n\tcheckUnredactedError(t, err, \"error: wrapped string = sensitive, wrapped safe string = safe\")\n\tcheckRedactedError(t, err, \"error: wrapped string = <redacted string>, wrapped safe string = safe\")\n}\n\nfunc TestErrorfAs(t *testing.T) {\n\twrapped := &customError{\n\t\tmsg:   \"sensitive\",\n\t\tvalue: \"value\",\n\t}\n\terr := Errorf(\"error: %w\", wrapped)\n\tcheckUnredactedError(t, err, \"error: sensitive\")\n\tcheckRedactedError(t, err, \"error: <redacted *redact.customError>\")\n\n\tvar unwrapped *customError\n\tif !errors.As(err, &unwrapped) {\n\t\tt.Error(\"got errors.As(err, unwrapped) == false\")\n\t}\n\tif unwrapped.value != wrapped.value {\n\t\tt.Error(\"got unwrapped.value != wrapped.value\")\n\t}\n\n\tvar unwrappedRedacted *customError\n\tif !errors.As(Error(err), &unwrappedRedacted) {\n\t\tt.Error(\"got errors.As(Error(err), &unwrappedRedacted) == false\")\n\t}\n\tif unwrappedRedacted.value != wrapped.value {\n\t\tt.Error(\"got unwrappedRedacted.value != wrapped.value\")\n\t}\n}\n\nfunc TestErrorfRedactableArg(t *testing.T) {\n\terr := Errorf(\"%d\", redactableInt(123))\n\tcheckUnredactedError(t, err, \"123\")\n\tcheckRedactedError(t, err, \"0\")\n}\n\nfunc TestErrorFormat(t *testing.T) {\n\t// Capture the first line of output as the error message and all following\n\t// lines as the stack trace.\n\tre := regexp.MustCompile(`(.+)?((?s)\n.+/redact.TestErrorFormat\n\t.+/redact/redact_test.go:\\d+\n.*\nruntime.goexit\n\t.+:\\d+.*)?`)\n\n\tcases := []struct {\n\t\tformat    string\n\t\terr       error\n\t\twantMsg   string\n\t\twantStack bool\n\t}{\n\t\t{\"%v\", Errorf(\"error %%v\"), \"error %v\", false},\n\t\t{\"%+v\", Errorf(\"error %%+v\"), \"error %+v\", true},\n\t\t{\"%s\", Errorf(\"error %%s\"), \"error %s\", false},\n\t\t{\"%+s\", Errorf(\"error %%+s\"), \"error %+s\", true},\n\t\t{\"%q\", Errorf(\"error %%q\"), `\"error %q\"`, false},\n\t}\n\tfor _, test := range cases {\n\t\tt.Run(test.format, func(t *testing.T) {\n\t\t\tgot := fmt.Sprintf(test.format, test.err)\n\t\t\tgroups := re.FindStringSubmatch(got)\n\t\t\tif groups == nil {\n\t\t\t\tt.Fatal(\"formatted error doesn't match regexp\")\n\t\t\t}\n\t\t\tt.Logf(\"got formatted stack trace:\\n%q\", got)\n\t\t\tif got := groups[1]; got != test.wantMsg {\n\t\t\t\tt.Errorf(\"got error message %q, want %q\", got, test.wantMsg)\n\t\t\t}\n\t\t\tif test.wantStack && (len(groups) < 3 || groups[2] == \"\") {\n\t\t\t\tt.Error(\"got formatted error without stack trace, wanted with stack trace\")\n\t\t\t} else if !test.wantStack && len(groups) > 2 && groups[2] != \"\" {\n\t\t\t\tt.Error(\"got formatted error with stack trace, wanted without stack trace\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStackTrace(t *testing.T) {\n\terr := Errorf(\"error\")\n\tstack := err.(interface{ StackTrace() []runtime.Frame }).StackTrace()\n\tif len(stack) == 0 {\n\t\tt.Fatal(\"got empty stack trace\")\n\t}\n\tstackTrace := \"got stack trace:\\n\"\n\tfor _, frame := range stack {\n\t\tstackTrace += fmt.Sprintf(\"%v\\n\", frame)\n\t}\n\tt.Log(stackTrace)\n\n\tif !strings.HasSuffix(stack[0].Function, t.Name()) {\n\t\tt.Errorf(\"got stack starting with function name %q, want function ending with test name %q\",\n\t\t\tstack[0].Function, t.Name())\n\t}\n\tlastFrame := stack[len(stack)-1]\n\twantFrame := \"runtime.goexit\"\n\tif lastFrame.Function != wantFrame {\n\t\tt.Errorf(\"got stack ending with function name %q, want function name %q\",\n\t\t\tlastFrame.Function, wantFrame)\n\t}\n}\n\nfunc TestMissingStackTrace(t *testing.T) {\n\tvar err interface{ StackTrace() []runtime.Frame } = &safeError{}\n\tstack := err.StackTrace()\n\tif stack != nil {\n\t\tt.Errorf(\"got stack with length %d, want nil\", len(stack))\n\t}\n}\n\ntype testRedactor struct {\n\tmsg         string\n\tredactedMsg string\n\terr         error\n}\n\nfunc (e *testRedactor) Error() string  { return e.msg }\nfunc (e *testRedactor) Redact() string { return e.redactedMsg }\nfunc (e *testRedactor) Unwrap() error  { return e.err }\n\ntype customError struct {\n\tmsg   string\n\tvalue string\n}\n\nfunc (e *customError) Error() string {\n\treturn e.msg\n}\n\ntype redactableInt int\n\nfunc (r redactableInt) Redact() string {\n\treturn \"0\"\n}\n\nfunc checkUnredactedError(t *testing.T, got error, wantMsg string) {\n\tt.Helper()\n\n\tgotMsg := fmt.Sprint(got)\n\tif gotMsg != wantMsg {\n\t\tt.Errorf(\"got wrong unredacted error:\\ngot:  %q\\nwant: %q\", gotMsg, wantMsg)\n\t}\n}\n\nfunc checkRedactedError(t *testing.T, got error, wantMsg string) {\n\tt.Helper()\n\n\tgotMsg := fmt.Sprint(Error(got))\n\tif gotMsg != wantMsg {\n\t\tt.Errorf(\"got wrong redacted error:\\ngot:  %q\\nwant: %q\", gotMsg, wantMsg)\n\t}\n}\n"
  },
  {
    "path": "internal/searcher/client.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage searcher\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"runtime\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/redact\"\n)\n\nconst searchAPIEndpoint = \"https://search.devbox.sh\"\n\nvar ErrNotFound = errors.New(\"Not found\")\n\ntype client struct {\n\thost string\n}\n\nfunc Client() *client {\n\treturn &client{\n\t\thost: envir.GetValueOrDefault(envir.DevboxSearchHost, searchAPIEndpoint),\n\t}\n}\n\nfunc (c *client) Search(ctx context.Context, query string) (*SearchResults, error) {\n\tif query == \"\" {\n\t\treturn nil, fmt.Errorf(\"query should not be empty\")\n\t}\n\n\tendpoint, err := url.JoinPath(c.host, \"v1/search\")\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tsearchURL := endpoint + \"?q=\" + url.QueryEscape(query)\n\n\treturn execGet[SearchResults](ctx, searchURL)\n}\n\n// Resolve calls the /resolve endpoint of the search service. This returns\n// the latest version of the package that matches the version constraint.\nfunc (c *client) Resolve(name, version string) (*PackageVersion, error) {\n\tif name == \"\" || version == \"\" {\n\t\treturn nil, fmt.Errorf(\"name and version should not be empty\")\n\t}\n\n\tendpoint, err := url.JoinPath(c.host, \"v1/resolve\")\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tsearchURL := endpoint +\n\t\t\"?name=\" + url.QueryEscape(name) +\n\t\t\"&version=\" + url.QueryEscape(version)\n\n\treturn execGet[PackageVersion](context.TODO(), searchURL)\n}\n\n// Resolve calls the /resolve endpoint of the search service. This returns\n// the latest version of the package that matches the version constraint.\nfunc (c *client) ResolveV2(ctx context.Context, name, version string) (*ResolveResponse, error) {\n\tif name == \"\" {\n\t\treturn nil, redact.Errorf(\"name is empty\")\n\t}\n\tif version == \"\" {\n\t\treturn nil, redact.Errorf(\"version is empty\")\n\t}\n\n\tendpoint, err := url.JoinPath(c.host, \"v2/resolve\")\n\tif err != nil {\n\t\treturn nil, redact.Errorf(\"invalid search endpoint host %q: %w\", redact.Safe(c.host), redact.Safe(err))\n\t}\n\tsearchURL := endpoint +\n\t\t\"?name=\" + url.QueryEscape(name) +\n\t\t\"&version=\" + url.QueryEscape(version)\n\n\treturn execGet[ResolveResponse](ctx, searchURL)\n}\n\nvar userAgent = fmt.Sprintf(\"Devbox/%s (%s; %s)\", build.Version, runtime.GOOS, runtime.GOARCH)\n\nfunc execGet[T any](ctx context.Context, url string) (*T, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, redact.Errorf(\"GET %s: %w\", redact.Safe(url), redact.Safe(err))\n\t}\n\treq.Header.Set(\"User-Agent\", userAgent)\n\n\tresponse, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, redact.Errorf(\"GET %s: %w\", redact.Safe(url), redact.Safe(err))\n\t}\n\tdefer response.Body.Close()\n\tdata, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn nil, redact.Errorf(\"GET %s: read respoonse body: %w\", redact.Safe(url), redact.Safe(err))\n\t}\n\tif response.StatusCode == http.StatusNotFound {\n\t\treturn nil, ErrNotFound\n\t}\n\tif response.StatusCode >= 400 {\n\t\treturn nil, redact.Errorf(\"GET %s: unexpected status code %s: %s\",\n\t\t\tredact.Safe(url),\n\t\t\tredact.Safe(response.Status),\n\t\t\tredact.Safe(data),\n\t\t)\n\t}\n\tvar result T\n\tif err := json.Unmarshal(data, &result); err != nil {\n\t\treturn nil, redact.Errorf(\"GET %s: unmarshal response JSON: %w\", redact.Safe(url), redact.Safe(err))\n\t}\n\treturn &result, nil\n}\n"
  },
  {
    "path": "internal/searcher/model.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage searcher\n\nimport (\n\t\"time\"\n\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\ntype SearchResults struct {\n\tNumResults int       `json:\"num_results\"`\n\tPackages   []Package `json:\"packages,omitempty\"`\n}\n\ntype Package struct {\n\tName        string           `json:\"name\"`\n\tNumVersions int              `json:\"num_versions\"`\n\tVersions    []PackageVersion `json:\"versions,omitempty\"`\n}\n\ntype PackageVersion struct {\n\tPackageInfo\n\n\tName    string                 `json:\"name\"`\n\tSystems map[string]PackageInfo `json:\"systems,omitempty\"`\n}\n\ntype PackageInfo struct {\n\tID           int      `json:\"id\"`\n\tCommitHash   string   `json:\"commit_hash\"`\n\tSystem       string   `json:\"system\"`\n\tLastUpdated  int      `json:\"last_updated\"`\n\tStoreHash    string   `json:\"store_hash\"`\n\tStoreName    string   `json:\"store_name\"`\n\tStoreVersion string   `json:\"store_version\"`\n\tMetaName     string   `json:\"meta_name\"`\n\tMetaVersion  []string `json:\"meta_version\"`\n\tAttrPaths    []string `json:\"attr_paths\"`\n\tVersion      string   `json:\"version\"`\n\tSummary      string   `json:\"summary\"`\n}\n\n// ResolveResponse is a response from the /v2/resolve endpoint.\ntype ResolveResponse struct {\n\t// Name is the resolved name of the package. For packages that are\n\t// identifiable by multiple names or attribute paths, this is the\n\t// \"canonical\" name.\n\tName string `json:\"name\"`\n\n\t// Version is the resolved package version.\n\tVersion string `json:\"version\"`\n\n\t// Summary is a short package description.\n\tSummary string `json:\"summary,omitempty\"`\n\n\t// Systems contains information about the package that can vary across\n\t// systems. It will always have at least one system. The keys match a\n\t// Nix system identifier (aarch64-darwin, x86_64-linux, etc.).\n\tSystems map[string]struct {\n\t\t// FlakeInstallable is a Nix installable that specifies how to\n\t\t// install the resolved package version.\n\t\t//\n\t\t// [Nix installable]: https://nixos.org/manual/nix/stable/command-ref/new-cli/nix#installables\n\t\tFlakeInstallable flake.Installable `json:\"flake_installable\"`\n\n\t\t// LastUpdated is the timestamp of the most recent change to the\n\t\t// package.\n\t\tLastUpdated time.Time `json:\"last_updated\"`\n\n\t\t// Outputs provides additional information about the Nix store\n\t\t// paths that this package installs. This field is not available\n\t\t// for some (especially older) packages.\n\t\tOutputs []struct {\n\t\t\t// Name is the output's name. Nix appends the name to\n\t\t\t// the output's store path unless it's the default name\n\t\t\t// of \"out\". Output names can be anything, but\n\t\t\t// conventionally they follow the various \"make install\"\n\t\t\t// directories such as \"bin\", \"lib\", \"src\", \"man\", etc.\n\t\t\tName string `json:\"name,omitempty\"`\n\n\t\t\t// Path is the absolute store path (with the /nix/store/\n\t\t\t// prefix) of the output.\n\t\t\tPath string `json:\"path,omitempty\"`\n\n\t\t\t// Default indicates if Nix installs this output by\n\t\t\t// default.\n\t\t\tDefault bool `json:\"default,omitempty\"`\n\n\t\t\t// NAR is set to the package's NAR archive URL when the\n\t\t\t// output exists in the cache.nixos.org binary cache.\n\t\t\tNAR string `json:\"nar,omitempty\"`\n\t\t} `json:\"outputs,omitempty\"`\n\t} `json:\"systems\"`\n}\n"
  },
  {
    "path": "internal/searcher/parse.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage searcher\n\nimport (\n\t\"strings\"\n)\n\n// ParseVersionedPackage checks if the given package is a versioned package\n// (`python@3.10`) and returns its name and version\nfunc ParseVersionedPackage(versionedName string) (name, version string, found bool) {\n\t// use the last @ symbol as the version delimiter, some packages have @ in the name\n\tatSymbolIndex := strings.LastIndex(versionedName, \"@\")\n\tif atSymbolIndex == -1 {\n\t\treturn \"\", \"\", false\n\t}\n\tif atSymbolIndex == len(versionedName)-1 {\n\t\t// This case handles packages that end with `@` in the name\n\t\t// example: `emacsPackages.@`\n\t\treturn \"\", \"\", false\n\t}\n\n\t// Common case: package@version\n\tname, version = versionedName[:atSymbolIndex], versionedName[atSymbolIndex+1:]\n\treturn name, version, true\n}\n"
  },
  {
    "path": "internal/searcher/parse_test.go",
    "content": "package searcher\n\nimport (\n\t\"testing\"\n)\n\nfunc TestParseVersionedPackage(t *testing.T) {\n\ttestCases := []struct {\n\t\tname            string\n\t\tinput           string\n\t\texpectedFound   bool\n\t\texpectedName    string\n\t\texpectedVersion string\n\t}{\n\t\t{\n\t\t\tname:            \"no-version\",\n\t\t\tinput:           \"python\",\n\t\t\texpectedFound:   false,\n\t\t\texpectedName:    \"\",\n\t\t\texpectedVersion: \"\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with-version-latest\",\n\t\t\tinput:           \"python@latest\",\n\t\t\texpectedFound:   true,\n\t\t\texpectedName:    \"python\",\n\t\t\texpectedVersion: \"latest\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with-version\",\n\t\t\tinput:           \"python@1.2.3\",\n\t\t\texpectedFound:   true,\n\t\t\texpectedName:    \"python\",\n\t\t\texpectedVersion: \"1.2.3\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with-two-@-signs\",\n\t\t\tinput:           \"emacsPackages.@@latest\",\n\t\t\texpectedFound:   true,\n\t\t\texpectedName:    \"emacsPackages.@\",\n\t\t\texpectedVersion: \"latest\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with-trailing-@-sign\",\n\t\t\tinput:           \"emacsPackages.@\",\n\t\t\texpectedFound:   false,\n\t\t\texpectedName:    \"\",\n\t\t\texpectedVersion: \"\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tname, version, found := ParseVersionedPackage(testCase.input)\n\t\t\tif found != testCase.expectedFound {\n\t\t\t\tt.Errorf(\"expected: %v, got: %v\", testCase.expectedFound, found)\n\t\t\t}\n\t\t\tif name != testCase.expectedName {\n\t\t\t\tt.Errorf(\"expected: %v, got: %v\", testCase.expectedName, name)\n\t\t\t}\n\t\t\tif version != testCase.expectedVersion {\n\t\t\t\tt.Errorf(\"expected: %v, got: %v\", testCase.expectedVersion, version)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/services/client.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage services\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/f1bonacc1/process-compose/src/types\"\n)\n\ntype processStates = types.ProcessesState\n\ntype Process struct {\n\tPID       int\n\tName      string\n\tNamespace string\n\tStatus    string\n\tAge       time.Duration\n\tHealth    string\n\tRestarts  int\n\tExitCode  int\n}\n\nfunc StartServices(ctx context.Context, w io.Writer, serviceName, projectDir string) error {\n\tpath := fmt.Sprintf(\"/process/start/%s\", serviceName)\n\n\tbody, status, err := clientRequest(path, http.MethodPost, projectDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch status {\n\tcase http.StatusOK:\n\t\tfmt.Fprintf(w, \"Service %s started.\\n\", serviceName)\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"error starting service %s: %s\", serviceName, body)\n\t}\n}\n\nfunc StopServices(ctx context.Context, serviceName, projectDir string, w io.Writer) error {\n\tpath := fmt.Sprintf(\"/process/stop/%s\", serviceName)\n\n\tbody, status, err := clientRequest(path, http.MethodPatch, projectDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch status {\n\tcase http.StatusOK:\n\t\tfmt.Fprintf(w, \"Service %s stopped.\\n\", serviceName)\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"error stopping service %s: %s\", serviceName, body)\n\t}\n}\n\nfunc RestartServices(ctx context.Context, serviceName, projectDir string, w io.Writer) error {\n\tpath := fmt.Sprintf(\"/process/restart/%s\", serviceName)\n\n\tbody, status, err := clientRequest(path, http.MethodPost, projectDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch status {\n\tcase http.StatusOK:\n\t\tfmt.Fprintf(w, \"Service %s restarted.\\n\", serviceName)\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"error restarting service %s: %s\", serviceName, body)\n\t}\n}\n\nfunc ListServices(ctx context.Context, projectDir string, w io.Writer) ([]Process, error) {\n\tpath := \"/processes\"\n\tresults := []Process{}\n\n\tbody, status, err := clientRequest(path, http.MethodGet, projectDir)\n\tif err != nil {\n\t\treturn results, err\n\t}\n\n\tswitch status {\n\tcase http.StatusOK:\n\t\tvar processes processStates\n\t\terr := json.Unmarshal([]byte(body), &processes)\n\t\tif err != nil {\n\t\t\treturn results, err\n\t\t}\n\t\tfor _, process := range processes.States {\n\t\t\tresults = append(results, Process{\n\t\t\t\tPID:       process.Pid,\n\t\t\t\tName:      process.Name,\n\t\t\t\tNamespace: process.Namespace,\n\t\t\t\tStatus:    process.Status,\n\t\t\t\tAge:       process.Age.Round(time.Second),\n\t\t\t\tHealth:    process.Health,\n\t\t\t\tRestarts:  process.Restarts,\n\t\t\t\tExitCode:  process.ExitCode,\n\t\t\t})\n\t\t}\n\t\treturn results, nil\n\tdefault:\n\t\treturn results, fmt.Errorf(\"unable to list services: %s\", body)\n\t}\n}\n\nfunc clientRequest(path, method, projectDir string) (string, int, error) {\n\tport, err := GetProcessManagerPort(projectDir)\n\tif err != nil {\n\t\terr := fmt.Errorf(\"unable to connect to process-compose server: %s\", err.Error())\n\t\treturn \"\", 0, err\n\t}\n\n\treq, err := http.NewRequest(method, fmt.Sprintf(\"http://localhost:%d%s\", port, path), nil)\n\tif err != nil {\n\t\treturn \"\", 0, err\n\t}\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", 0, err\n\t}\n\n\tdefer resp.Body.Close()\n\tbuf := new(bytes.Buffer)\n\t_, err = buf.ReadFrom(resp.Body)\n\tif err != nil {\n\t\treturn \"\", 0, err\n\t}\n\tbody := buf.String()\n\tstatus := resp.StatusCode\n\n\treturn body, status, nil\n}\n"
  },
  {
    "path": "internal/services/config.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage services\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/f1bonacc1/process-compose/src/types\"\n\t\"github.com/pkg/errors\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"go.jetify.com/devbox/internal/cuecfg\"\n)\n\nfunc FromUserProcessCompose(projectDir, userProcessCompose string) Services {\n\tprocessComposeYaml := lookupProcessCompose(projectDir, userProcessCompose)\n\tif processComposeYaml == \"\" {\n\t\treturn nil\n\t}\n\n\tuserSvcs, err := FromProcessCompose(processComposeYaml)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"error reading process-compose.yaml: %s, skipping\", err)\n\t\treturn nil\n\t}\n\treturn userSvcs\n}\n\nfunc FromProcessCompose(path string) (Services, error) {\n\tprocessCompose := &types.Project{}\n\tservices := Services{}\n\terr := errors.WithStack(cuecfg.ParseFile(path, processCompose))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor name := range processCompose.Processes {\n\t\tsvc := Service{\n\t\t\tName:               name,\n\t\t\tProcessComposePath: path,\n\t\t}\n\t\tservices[name] = svc\n\t}\n\n\treturn services, nil\n}\n\nfunc NamesFromProcessCompose(content []byte) ([]string, error) {\n\tvar processCompose types.Project\n\tif err := yaml.Unmarshal(content, &processCompose); err != nil {\n\t\treturn nil, err\n\t}\n\tnames := []string{}\n\tfor name := range processCompose.Processes {\n\t\tnames = append(names, name)\n\t}\n\treturn names, nil\n}\n\nfunc lookupProcessCompose(projectDir, path string) string {\n\tif path == \"\" {\n\t\tpath = projectDir\n\t}\n\tif !filepath.IsAbs(path) {\n\t\tpath = filepath.Join(projectDir, path)\n\t}\n\n\tpathsToCheck := []string{\n\t\tpath,\n\t\tfilepath.Join(path, \"process-compose.yaml\"),\n\t\tfilepath.Join(path, \"process-compose.yml\"),\n\t}\n\n\tfor _, p := range pathsToCheck {\n\t\tif fi, err := os.Stat(p); err == nil && !fi.IsDir() {\n\t\t\treturn p\n\t\t}\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/services/manager.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage services\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/cuecfg\"\n\t\"go.jetify.com/devbox/internal/xdg\"\n)\n\nconst (\n\tprocessComposeLogfile = \".devbox/compose.log\"\n\tfileLockTimeout       = 5 * time.Second\n)\n\ntype instance struct {\n\tPid  int `json:\"pid\"`\n\tPort int `json:\"port\"`\n}\n\ntype instanceMap = map[string]instance\n\ntype globalProcessComposeConfig struct {\n\tInstances instanceMap\n\tPath      string   `json:\"-\"`\n\tFile      *os.File `json:\"-\"`\n}\n\ntype ProcessComposeOpts struct {\n\tBinPath            string\n\tExtraFlags         []string\n\tBackground         bool\n\tProcessComposePort int\n}\n\nfunc newGlobalProcessComposeConfig() *globalProcessComposeConfig {\n\treturn &globalProcessComposeConfig{Instances: map[string]instance{}}\n}\n\nfunc globalProcessComposeJSONPath() (string, error) {\n\tpath := xdg.DataSubpath(filepath.Join(\"devbox\", \"global\"))\n\treturn filepath.Join(path, \"process-compose.json\"), errors.WithStack(os.MkdirAll(path, 0o755))\n}\n\nfunc readGlobalProcessComposeJSON(file *os.File) *globalProcessComposeConfig {\n\tconfig := newGlobalProcessComposeConfig()\n\n\terr := errors.WithStack(cuecfg.ParseFile(file.Name(), &config.Instances))\n\tif err != nil {\n\t\treturn config\n\t}\n\tconfig.Path = file.Name()\n\treturn config\n}\n\nfunc writeGlobalProcessComposeJSON(config *globalProcessComposeConfig, file *os.File) error {\n\t// convert config to json using cue\n\tjson, err := cuecfg.MarshalJSON(config.Instances)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert config to json: %w\", err)\n\t}\n\n\tif err := file.Truncate(0); err != nil {\n\t\treturn fmt.Errorf(\"failed to truncate global config file: %w\", err)\n\t}\n\n\tif _, err := file.Write(json); err != nil {\n\t\treturn fmt.Errorf(\"failed to write global config file: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc openGlobalConfigFile() (*os.File, error) {\n\tconfigPath, err := globalProcessComposeJSONPath()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get config path: %w\", err)\n\t}\n\n\tglobalConfigFile, err := os.OpenFile(configPath, os.O_WRONLY|os.O_CREATE, 0o664)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open config file: %w\", err)\n\t}\n\n\terr = lockFile(globalConfigFile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to lock file: %w\", err)\n\t}\n\n\treturn globalConfigFile, nil\n}\n\nfunc StartProcessManager(\n\tw io.Writer,\n\trequestedServices []string,\n\tavailableServices Services,\n\tprojectDir string,\n\tprocessComposeConfig ProcessComposeOpts,\n) error {\n\t// Check if process-compose is already running\n\tif ProcessManagerIsRunning(projectDir) {\n\t\treturn fmt.Errorf(\"process-compose is already running. To stop it, run `devbox services stop`\")\n\t}\n\n\t// Get the file and lock it right at the start\n\n\tconfigFile, err := openGlobalConfigFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer configFile.Close()\n\n\t// Read the global config file\n\tconfig := readGlobalProcessComposeJSON(configFile)\n\tconfig.File = configFile\n\n\tport, err := selectPort(processComposeConfig.ProcessComposePort)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to select port: %v\", err)\n\t}\n\n\t// Start building the process-compose command\n\tflags := []string{\"-p\", strconv.Itoa(port)}\n\tupCommand := []string{\"up\"}\n\n\tif len(requestedServices) > 0 {\n\t\tflags = append(requestedServices, flags...)\n\t\tflags = append(upCommand, flags...)\n\t\tfmt.Fprintf(w, \"Starting services: %s \\n\", strings.Join(requestedServices, \", \"))\n\t} else {\n\t\tservices := []string{}\n\t\tfor k := range availableServices {\n\t\t\tservices = append(services, k)\n\t\t}\n\t\tfmt.Fprintf(w, \"Starting all services: %s \\n\", strings.Join(services, \", \"))\n\t}\n\n\tseenPCFiles := make(map[string]bool)\n\tfor _, s := range availableServices {\n\t\tif !seenPCFiles[s.ProcessComposePath] {\n\t\t\t// Only add -f flag if we haven't seen this file path before\n\t\t\tflags = append(flags, \"-f\", s.ProcessComposePath)\n\t\t\tseenPCFiles[s.ProcessComposePath] = true\n\t\t}\n\t}\n\n\tflags = append(flags, processComposeConfig.ExtraFlags...)\n\n\tif processComposeConfig.Background {\n\t\tflags = append(flags, \"-t=false\")\n\t\tcmd := exec.Command(processComposeConfig.BinPath, flags...)\n\t\treturn runProcessManagerInBackground(cmd, config, port, projectDir, w)\n\t}\n\n\tcmd := exec.Command(processComposeConfig.BinPath, flags...)\n\treturn runProcessManagerInForeground(cmd, config, port, projectDir, w)\n}\n\nfunc runProcessManagerInForeground(cmd *exec.Cmd, config *globalProcessComposeConfig, port int, projectDir string, w io.Writer) error {\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"failed to start process-compose: %w\", err)\n\t}\n\n\tprojectConfig := instance{\n\t\tPid:  cmd.Process.Pid,\n\t\tPort: port,\n\t}\n\n\tconfig.Instances[projectDir] = projectConfig\n\n\terr := writeGlobalProcessComposeJSON(config, config.File)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// We're waiting now, so we can unlock the file\n\tconfig.File.Close()\n\n\terr = cmd.Wait()\n\tif err != nil {\n\t\tif err.Error() == \"exit status 1\" {\n\t\t\tfmt.Fprintf(w, \"Process-compose was terminated remotely, %s\\n\", err.Error())\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\n\tconfigFile, err := openGlobalConfigFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconfig = readGlobalProcessComposeJSON(configFile)\n\n\tdelete(config.Instances, projectDir)\n\treturn writeGlobalProcessComposeJSON(config, configFile)\n}\n\nfunc runProcessManagerInBackground(cmd *exec.Cmd, config *globalProcessComposeConfig, port int, projectDir string, w io.Writer) error {\n\tlogdir := filepath.Join(projectDir, processComposeLogfile)\n\tlogfile, err := os.OpenFile(logdir, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_TRUNC, 0o664)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open process-compose log file: %w\", err)\n\t}\n\n\tcmd.Stdout = logfile\n\tcmd.Stderr = logfile\n\n\t// These attributes set the process group ID to the process ID of process-compose\n\t// Starting in it's own process group means it won't be terminated if the shell crashes\n\tcmd.SysProcAttr = &syscall.SysProcAttr{\n\t\tSetpgid: true,\n\t\tPgid:    0,\n\t}\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"failed to start process-compose: %w\", err)\n\t}\n\n\tfmt.Fprintf(w, \"Process-compose is now running on port %d\\n\", port)\n\tfmt.Fprintf(w, \"To stop your services, run `devbox services stop`\\n\")\n\n\tprojectConfig := instance{\n\t\tPid:  cmd.Process.Pid,\n\t\tPort: port,\n\t}\n\n\tconfig.Instances[projectDir] = projectConfig\n\n\terr = writeGlobalProcessComposeJSON(config, config.File)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write global process-compose config: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc StopProcessManager(ctx context.Context, projectDir string, w io.Writer) error {\n\tconfigFile, err := openGlobalConfigFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer configFile.Close()\n\n\tconfig := readGlobalProcessComposeJSON(configFile)\n\n\tproject, ok := config.Instances[projectDir]\n\tif !ok {\n\t\treturn fmt.Errorf(\"process-compose is not running or it's config is missing. To start it, run `devbox services up`\")\n\t}\n\n\tdefer func() {\n\t\tdelete(config.Instances, projectDir)\n\t\terr = writeGlobalProcessComposeJSON(config, configFile)\n\t}()\n\n\tpid, _ := os.FindProcess(project.Pid)\n\terr = pid.Signal(os.Interrupt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"process-compose is not running. To start it, run `devbox services up`\")\n\t}\n\n\tfmt.Fprintf(w, \"Process-compose stopped successfully.\\n\")\n\treturn nil\n}\n\nfunc StopAllProcessManagers(ctx context.Context, w io.Writer) error {\n\tconfigFile, err := openGlobalConfigFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer configFile.Close()\n\n\tconfig := readGlobalProcessComposeJSON(configFile)\n\n\tfor _, project := range config.Instances {\n\t\tpid, _ := os.FindProcess(project.Pid)\n\t\terr := pid.Signal(os.Interrupt)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"process-compose is not running. To start it, run `devbox services up`\")\n\t\t}\n\t}\n\n\tconfig.Instances = make(map[string]instance)\n\n\terr = writeGlobalProcessComposeJSON(config, configFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write global process-compose config: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc AttachToProcessManager(ctx context.Context, w io.Writer, projectDir string, processComposeConfig ProcessComposeOpts) error {\n\tconfigFile, err := openGlobalConfigFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconfig := readGlobalProcessComposeJSON(configFile)\n\tconfigFile.Close() // release the lock as this command is long running\n\n\tproject, ok := config.Instances[projectDir]\n\tif !ok {\n\t\treturn fmt.Errorf(\"process-compose is not running for this project. To start it, run `devbox services up`\")\n\t}\n\n\tflags := []string{\"attach\", \"-p\", strconv.Itoa(project.Port)}\n\tcmd := exec.Command(processComposeConfig.BinPath, flags...)\n\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdin = os.Stdin\n\n\treturn cmd.Run()\n}\n\nfunc ProcessManagerIsRunning(projectDir string) bool {\n\tconfigFile, err := openGlobalConfigFile()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tdefer configFile.Close()\n\n\tconfig := readGlobalProcessComposeJSON(configFile)\n\n\tproject, ok := config.Instances[projectDir]\n\tif !ok {\n\t\treturn false\n\t}\n\n\tprocess, _ := os.FindProcess(project.Pid)\n\n\terr = process.Signal(syscall.Signal(0))\n\tif err != nil {\n\t\tdelete(config.Instances, projectDir)\n\t\t_ = writeGlobalProcessComposeJSON(config, configFile)\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc GetProcessManagerPort(projectDir string) (int, error) {\n\tconfigFile, err := openGlobalConfigFile()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tconfig := readGlobalProcessComposeJSON(configFile)\n\n\tproject, ok := config.Instances[projectDir]\n\tif !ok {\n\t\treturn 0, usererr.WithUserMessage(fmt.Errorf(\"failed to find projectDir %s in config.Instances\", projectDir), \"process-compose is not running or it's config is missing. To start it, run `devbox services up`\")\n\t}\n\n\treturn project.Port, nil\n}\n\nfunc lockFile(file *os.File) error {\n\tlockResult := make(chan error)\n\n\tgo func() {\n\t\terr := syscall.Flock(int(file.Fd()), syscall.LOCK_EX)\n\t\tlockResult <- err\n\t}()\n\n\tselect {\n\tcase err := <-lockResult:\n\t\tif err != nil {\n\t\t\tfile.Close()\n\t\t\treturn fmt.Errorf(\"failed to lock file: %w\", err)\n\t\t}\n\t\treturn nil\n\n\tcase <-time.After(fileLockTimeout):\n\t\tfile.Close()\n\t\treturn fmt.Errorf(\"process-compose file lock timed out after %d seconds\", fileLockTimeout/time.Second)\n\t}\n}\n"
  },
  {
    "path": "internal/services/ports.go",
    "content": "package services\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/pkg/errors\"\n)\n\nvar disallowedPorts = map[int]string{\n\t// Anything <= 1024\n\t1433: \"MS-SQL (Microsoft SQL Server database management system)\",\n\t1434: \"MS-SQL (Microsoft SQL Server database management system)\",\n\t1521: \"Oracle SQL\",\n\t1701: \"L2TP (Layer 2 Tunneling Protocol)\",\n\t1723: \"PPTP (Point-to-Point Tunneling Protocol)\",\n\t2049: \"NFS (Network File System)\",\n\t3000: \"Node.js (Server-side JavaScript environment)\",\n\t3001: \"Node.js (Server-side JavaScript environment)\",\n\t3306: \"MySQL (Database system)\",\n\t3389: \"RDP (Remote Desktop Protocol)\",\n\t5060: \"SIP (Session Initiation Protocol)\",\n\t5145: \"RSH (Remote Shell)\",\n\t5353: \"mDNS (Multicast DNS)\",\n\t5432: \"PostgreSQL (Database system)\",\n\t5900: \"VNC (Virtual Network Computing)\",\n\t6379: \"Redis (Database system)\",\n\t8000: \"HTTP Alternate (http_alt)\",\n\t8080: \"HTTP Alternate (http_alt)\",\n\t8082: \"PHP FPM\",\n\t8443: \"HTTPS Alternate (https_alt)\",\n\t9443: \"Redis Enterprise (Database system)\",\n}\n\nfunc getAvailablePort() (int, error) {\n\tget := func() (int, error) {\n\t\tport, err := isPortAvailable(0)\n\t\tif err != nil {\n\t\t\treturn 0, errors.WithStack(err)\n\t\t}\n\t\treturn port, nil\n\t}\n\n\tfor range 1000 {\n\t\tport, err := get()\n\t\tif err != nil {\n\t\t\treturn 0, errors.WithStack(err)\n\t\t}\n\n\t\tif isAllowed(port) {\n\t\t\treturn port, nil\n\t\t}\n\t}\n\n\treturn 0, errors.New(\"no available port\")\n}\n\nfunc selectPort(configPort int) (int, error) {\n\tif configPort != 0 {\n\t\treturn isPortAvailable(configPort)\n\t}\n\n\tif portStr, exists := os.LookupEnv(\"DEVBOX_PC_PORT_NUM\"); exists {\n\t\tport, err := strconv.Atoi(portStr)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"invalid DEVBOX_PC_PORT_NUM environment variable: %v\", err)\n\t\t}\n\t\tif port <= 0 {\n\t\t\treturn 0, fmt.Errorf(\"invalid DEVBOX_PC_PORT_NUM environment variable: ports cannot be less than 0\")\n\t\t}\n\t\treturn isPortAvailable(port)\n\t}\n\n\treturn getAvailablePort()\n}\n\nfunc isAllowed(port int) bool {\n\treturn port > 1024 && disallowedPorts[port] == \"\"\n}\n\nfunc isPortAvailable(port int) (int, error) {\n\tln, err := net.Listen(\"tcp\", fmt.Sprintf(\"localhost:%d\", port))\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"port %d is already in use\", port)\n\t}\n\tdefer ln.Close()\n\treturn ln.Addr().(*net.TCPAddr).Port, nil\n}\n"
  },
  {
    "path": "internal/services/services.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage services\n\ntype Services map[string]Service // name -> Service\n\ntype Service struct {\n\tName               string\n\tProcessComposePath string\n}\n"
  },
  {
    "path": "internal/services/status.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\n//lint:ignore U1000 Ignore unused function temporarily for debugging\n\npackage services\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/fsnotify/fsnotify\"\n\t\"github.com/pkg/errors\"\n\n\t\"go.jetify.com/devbox/internal/envir\"\n)\n\n// updateFunc returns a possibly updated service status and a boolean indicating\n// whether the status file should be saved. This prevents infinite loops when\n// the status file is updated.\ntype updateFunc func(status *ServiceStatus) (*ServiceStatus, bool)\n\ntype ListenerOpts struct {\n\tHostID     string\n\tProjectDir string\n\tUpdateFunc updateFunc\n\tWriter     io.Writer\n}\n\nfunc ListenToChanges(ctx context.Context, opts *ListenerOpts) error {\n\tif err := initCloudDir(opts.ProjectDir, opts.HostID); err != nil {\n\t\treturn err\n\t}\n\n\t// Create new watcher.\n\twatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\twatcher.Close()\n\t}()\n\n\t// Start listening for events.\n\tgo listenToEvents(watcher, opts)\n\n\t// We only want events for the specific host.\n\treturn errors.WithStack(watcher.Add(filepath.Join(cloudFilePath(opts.ProjectDir), opts.HostID)))\n}\n\nfunc listenToEvents(watcher *fsnotify.Watcher, opts *ListenerOpts) {\n\tfor {\n\t\tselect {\n\t\tcase event, ok := <-watcher.Events:\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// mutagen sync changes show up as create events\n\t\t\tif event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {\n\t\t\t\tstatus, err := readServiceStatus(event.Name)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Fprintf(opts.Writer, \"Error reading status file: %s\\n\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tstatus, saveChanges := opts.UpdateFunc(status)\n\t\t\t\tif saveChanges {\n\t\t\t\t\tif err := writeServiceStatusFile(event.Name, status); err != nil {\n\t\t\t\t\t\tfmt.Fprintf(opts.Writer, \"Error updating status file: %s\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase err, ok := <-watcher.Errors:\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfmt.Fprintf(opts.Writer, \"error: %s\\n\", err)\n\t\t}\n\t}\n}\n\nfunc cloudFilePath(projectDir string) string {\n\treturn filepath.Join(projectDir, \".devbox.cloud\")\n}\n\n// initCloudDir creates the service status directory and a .gitignore file\nfunc initCloudDir(projectDir, hostID string) error {\n\tcloudDirPath := cloudFilePath(projectDir)\n\t_ = os.MkdirAll(filepath.Join(cloudDirPath, hostID), 0o755)\n\tgitignorePath := filepath.Join(cloudDirPath, \".gitignore\")\n\t_, err := os.Stat(gitignorePath)\n\tif !errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil\n\t}\n\treturn errors.WithStack(os.WriteFile(gitignorePath, []byte(\"*\"), 0o644))\n}\n\ntype ServiceStatus struct {\n\tLocalPort string `json:\"local_port\"`\n\tName      string `json:\"name\"`\n\tPort      string `json:\"port\"`\n\tRunning   bool   `json:\"running\"`\n}\n\nfunc writeServiceStatusFile(path string, status *ServiceStatus) error {\n\tcontent, err := json.Marshal(status)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\t_ = os.MkdirAll(filepath.Dir(path), 0o755) // create path, ignore error\n\treturn errors.WithStack(os.WriteFile(path, content, 0o644))\n}\n\n//lint:ignore U1000 Ignore unused function temporarily for debugging\nfunc updateServiceStatusOnRemote(projectDir string, s *ServiceStatus) error {\n\tif !envir.IsDevboxCloud() {\n\t\treturn nil\n\t}\n\thost, err := os.Hostname()\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tcloudDirPath := cloudFilePath(projectDir)\n\treturn writeServiceStatusFile(filepath.Join(cloudDirPath, host, s.Name+\".json\"), s)\n}\n\nfunc readServiceStatus(path string) (*ServiceStatus, error) {\n\t_, err := os.Stat(path)\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil, nil\n\t}\n\n\tcontent, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tstatus := &ServiceStatus{}\n\treturn status, errors.WithStack(json.Unmarshal(content, status))\n}\n"
  },
  {
    "path": "internal/setup/setup.go",
    "content": "// Package setup performs setup tasks and records metadata about when they're\n// run.\npackage setup\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlecAivazis/survey/v2\"\n\t\"github.com/mattn/go-isatty\"\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/redact\"\n\t\"go.jetify.com/devbox/internal/xdg\"\n)\n\n// ErrUserRefused indicates that the user responded no to an interactive\n// confirmation prompt.\nvar ErrUserRefused = errors.New(\"user refused run\")\n\n// ErrAlreadyRefused indicates that no confirmation prompt was shown because the\n// user previously refused to run the task. Call [Reset] to re-prompt the user\n// for confirmation.\nvar ErrAlreadyRefused = errors.New(\"already refused by user\")\n\ntype ctxKey string\n\n// ctxKeyTask tracks the current task key across processes when relaunching\n// with sudo.\nvar ctxKeyTask ctxKey = \"task\"\n\n// Task is a setup action that can conditionally run based on the state of a\n// previous run.\ntype Task interface {\n\tRun(ctx context.Context) error\n\n\t// NeedsRun returns true if the task needs to be run. It should assume\n\t// that lastRun persists across executions of the program and is unique\n\t// for each user.\n\t//\n\t// A task that should only run once can check if lastRun.Time is the zero value.\n\t// A task that only runs after an update can check if lastRun.Version < build.Version.\n\t// A retryable task can check lastRun.Error to see if the previous run failed.\n\tNeedsRun(ctx context.Context, lastRun RunInfo) bool\n}\n\n// RunInfo contains metadata that describes the most recent run of a task.\ntype RunInfo struct {\n\t// Time is the last time the task ran.\n\tTime time.Time `json:\"time\"`\n\n\t// Version is the version of Devbox that last ran the task.\n\tVersion string `json:\"version\"`\n\n\t// Error is the error message returned by the last run. It's empty if\n\t// the last run succeeded.\n\tError string `json:\"error,omitempty\"`\n}\n\n// TaskStatus describes the status of a task.\ntype TaskStatus int\n\nconst (\n\t// TaskDone indicates that a task doesn't need to run and that its most\n\t// recent run (if any) didn't report an error. Note that a task can be\n\t// done without ever running if its NeedsRun method returns false before\n\t// the first run.\n\tTaskDone TaskStatus = iota\n\n\t// TaskNeedsRun is the status of a task that needs to be run.\n\tTaskNeedsRun\n\n\t// TaskUserRefused indicates that the user answered no to a confirmation\n\t// prompt to run the task.\n\tTaskUserRefused\n\n\t// TaskError indicates that a task's most recent run failed and it\n\t// cannot be re-run without a call to [Reset].\n\tTaskError\n\n\t// TaskSudoing occurs when the caller of [Status] is running in a sudoed\n\t// process due to the task calling [SudoDevbox] from the parent process.\n\tTaskSudoing\n)\n\n// Status returns the status of a setup task.\nfunc Status(ctx context.Context, key string, task Task) TaskStatus {\n\tdefer debug.FunctionTimer().End()\n\tstate := loadState(key)\n\tswitch {\n\tcase isSudo(key):\n\t\treturn TaskSudoing\n\tcase state.ConfirmPrompt.Asked && !state.ConfirmPrompt.Allowed:\n\t\treturn TaskUserRefused\n\tcase task.NeedsRun(ctx, state.LastRun):\n\t\treturn TaskNeedsRun\n\tcase state.LastRun.Error == \"\":\n\t\treturn TaskDone\n\tcase state.LastRun.Error != \"\":\n\t\treturn TaskError\n\t}\n\tpanic(\"setup.Status switch isn't exhaustive\")\n}\n\n// Run runs a setup task and stores its state under a given key. Keys are\n// namespaced by user. It only calls the task's Run method when NeedsRun returns\n// true.\nfunc Run(ctx context.Context, key string, task Task) error {\n\treturn run(ctx, key, task, \"\")\n}\n\n// SudoDevbox relaunches Devbox as root using sudo, taking care to preserve\n// Devbox environment variables that can affect the new process. If the current\n// user is already root, then it returns (false, nil) to indicate that no sudo\n// process ran. The caller can use this as a hint to know if it's running as the\n// sudoed process. Typical usage is:\n//\n//\tfunc (*ConfigTask) Run(context.Context) error {\n//\t\tran, err := SudoDevbox(ctx, \"cache\", \"configure\")\n//\t\tif ran || err != nil {\n//\t\t\t// return early if we kicked off a sudo process or there\n//\t\t\t// was an error\n//\t\t\treturn err\n//\t\t}\n//\t\t// do things as root\n//\t}\n//\n//\tConfirmRun(ctx, key, &ConfigTask{}, \"Allow sudo to run Devbox as root?\")\n//\n// A task that calls SudoDevbox should pass command arguments that cause the new\n// Devbox process to rerun the task. The task executes unconditionally within\n// the sudo process without re-prompting the user or a second call to its\n// NeedsRun method.\nfunc SudoDevbox(ctx context.Context, arg ...string) (ran bool, err error) {\n\tif os.Getuid() == 0 {\n\t\treturn false, nil\n\t}\n\n\ttaskKey := \"\"\n\tif v := ctx.Value(ctxKeyTask); v != nil {\n\t\ttaskKey = v.(string)\n\t}\n\n\t// Ensure the state file and its directory exist before sudoing,\n\t// otherwise they will be owned by root. This is easier than recursively\n\t// chowning new directories/files after root creates them.\n\tif taskKey != \"\" {\n\t\tsaveState(taskKey, state{})\n\t}\n\n\t// Use the absolute path to Devbox instead of relying on PATH for two\n\t// reasons:\n\t//\n\t//  1. sudo isn't guaranteed to preserve the current PATH and the root\n\t//     user might not have devbox in its PATH.\n\t//  2. If we're running an alternative version of Devbox\n\t//     (such as a dev build) we want to use the same binary.\n\texe, err := devboxExecutable()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tsudoArgs := make([]string, 0, len(arg)+4)\n\tsudoArgs = append(sudoArgs, \"--preserve-env=\"+strings.Join([]string{\n\t\t// Keep writing debug logs from the sudo process.\n\t\t\"DEVBOX_DEBUG\",\n\n\t\t// Use the same Devbox API and auth token.\n\t\t\"DEVBOX_API_TOKEN\",\n\t\t\"DEVBOX_PROD\",\n\n\t\t// In case the Devbox version is overridden.\n\t\t\"DEVBOX_USE_VERSION\",\n\n\t\t// Use the same XDG directories for state, caching, etc.\n\t\t\"XDG_CACHE_HOME\",\n\t\t\"XDG_CONFIG_DIRS\",\n\t\t\"XDG_CONFIG_HOME\",\n\t\t\"XDG_DATA_DIRS\",\n\t\t\"XDG_DATA_HOME\",\n\t\t\"XDG_RUNTIME_DIR\",\n\t\t\"XDG_STATE_HOME\",\n\t}, \",\"))\n\tif taskKey != \"\" {\n\t\tsudoArgs = append(sudoArgs, \"DEVBOX_SUDO_TASK=\"+taskKey)\n\t}\n\tsudoArgs = append(sudoArgs, \"--\", exe)\n\tsudoArgs = append(sudoArgs, arg...)\n\n\tcmd := exec.CommandContext(ctx, \"sudo\", sudoArgs...)\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\tif taskKey == \"\" {\n\t\t\treturn false, redact.Errorf(\"setup: relaunch with sudo: %w\", err)\n\t\t}\n\t\treturn false, taskError(taskKey, redact.Errorf(\"relaunch with sudo: %w\", err))\n\t}\n\treturn true, nil\n}\n\n// Run interactively prompts the user to confirm that it's ok to run a setup\n// task. It only prompts the user if the task's NeedsRun method returns true. If\n// the user refuses to run the task, then ConfirmPrompt will not ask them again.\n// Call [Reset] to reset the task's state and re-prompt the user.\nfunc ConfirmRun(ctx context.Context, key string, task Task, prompt string) error {\n\tif prompt == \"\" {\n\t\treturn taskError(key, redact.Errorf(\"empty confirmation prompt\"))\n\t}\n\treturn run(ctx, key, task, prompt)\n}\n\nvar defaultPrompt = func(msg string) (response any, err error) {\n\tif isatty.IsTerminal(os.Stdin.Fd()) {\n\t\terr = survey.AskOne(&survey.Confirm{\n\t\t\tMessage: msg,\n\t\t\tDefault: true,\n\t\t}, &response)\n\t\treturn response, err\n\t}\n\tslog.Debug(\"setup: no tty detected, assuming yes to confirmation prompt\", \"prompt\", msg)\n\treturn true, nil\n}\n\nfunc run(ctx context.Context, key string, task Task, prompt string) error {\n\tctx = context.WithValue(ctx, ctxKeyTask, key)\n\n\tisSudo := isSudo(key)\n\tstate := loadState(key)\n\tif !isSudo && !task.NeedsRun(ctx, state.LastRun) {\n\t\treturn nil\n\t}\n\n\toldState, newState := state, &state\n\tdefer func() {\n\t\tif oldState != *newState {\n\t\t\tsaveState(key, *newState)\n\t\t}\n\t}()\n\n\tif !isSudo && prompt != \"\" {\n\t\tstate.ConfirmPrompt.Message = prompt\n\t\tif state.ConfirmPrompt.Asked && !state.ConfirmPrompt.Allowed {\n\t\t\t// We've asked before and the user said no.\n\t\t\treturn taskError(key, ErrAlreadyRefused)\n\t\t}\n\n\t\tresp, err := defaultPrompt(prompt)\n\t\tif err != nil {\n\t\t\treturn taskError(key, redact.Errorf(\"prompt for confirmation: %v\", err))\n\t\t}\n\t\tstate.ConfirmPrompt.Asked = true\n\t\tstate.ConfirmPrompt.Allowed, _ = resp.(bool)\n\t\tif !state.ConfirmPrompt.Allowed {\n\t\t\treturn taskError(key, ErrUserRefused)\n\t\t}\n\t}\n\n\tstate.LastRun = RunInfo{\n\t\tTime:    time.Now(),\n\t\tVersion: build.Version,\n\t}\n\tif err := task.Run(ctx); err != nil {\n\t\tstate.LastRun.Error = err.Error()\n\t\treturn taskError(key, err)\n\t}\n\treturn nil\n}\n\n// Reset removes a task's state so that it acts as if it has never run.\nfunc Reset(key string) {\n\terr := os.Remove(statePath(key))\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn\n\t}\n\tif err != nil {\n\t\terr = taskError(key, fmt.Errorf(\"remove state file: %v\", err))\n\t\tslog.Error(\"ignoring setup task reset error\", \"err\", err, \"task\", key)\n\t}\n}\n\ntype state struct {\n\tConfirmPrompt confirmPrompt `json:\"confirm_prompt,omitempty\"`\n\tLastRun       RunInfo       `json:\"last_run,omitempty\"`\n}\n\ntype confirmPrompt struct {\n\tMessage string `json:\"message\"`\n\tAsked   bool   `json:\"asked\"`\n\tAllowed bool   `json:\"allowed\"`\n}\n\nfunc loadState(key string) state {\n\tpath := statePath(key)\n\tb, err := os.ReadFile(path)\n\tif err != nil {\n\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\terr = taskError(key, fmt.Errorf(\"load state file: %v\", err))\n\t\t\tslog.Error(\"using empty setup task state due to an error\", \"err\", err, \"task\", key)\n\t\t}\n\t\treturn state{}\n\t}\n\tloaded := state{}\n\tif err := json.Unmarshal(b, &loaded); err != nil {\n\t\terr = taskError(key, fmt.Errorf(\"load state file %s: %v\", path, err))\n\t\tslog.Error(\"using empty setup task state due to an error\", \"err\", err, \"task\", key)\n\t\treturn state{}\n\t}\n\treturn loaded\n}\n\nfunc saveState(key string, s state) {\n\tpath := statePath(key)\n\tdata, err := json.MarshalIndent(s, \"\", \"  \")\n\tif err != nil {\n\t\terr = taskError(key, fmt.Errorf(\"save state file: %v\", err))\n\t\tslog.Error(\"not saving setup task state\", \"err\", err, \"task\", key)\n\t\treturn\n\t}\n\n\terr = os.MkdirAll(filepath.Dir(path), 0o755)\n\tif err == nil {\n\t\terr = os.WriteFile(path, data, 0o644)\n\t}\n\tif err != nil {\n\t\terr = taskError(key, fmt.Errorf(\"save state file: %v\", err))\n\t\tslog.Error(\"not saving setup task state\", \"err\", err, \"task\", key)\n\t\treturn\n\t}\n\n\tsudoUID, sudoGID := os.Getenv(\"SUDO_UID\"), os.Getenv(\"SUDO_GID\")\n\tif sudoUID != \"\" || sudoGID != \"\" {\n\t\tuid, err := strconv.Atoi(sudoUID)\n\t\tif err != nil {\n\t\t\tuid = -1\n\t\t}\n\t\tgid, err := strconv.Atoi(sudoGID)\n\t\tif err != nil {\n\t\t\tgid = -1\n\t\t}\n\t\terr = os.Chown(path, uid, gid)\n\t\tif err != nil {\n\t\t\terr = taskError(key, fmt.Errorf(\"chown state file to non-sudo user: %v\", err))\n\t\t\tslog.Error(\"cannot ensure task state is owned by sudoing user\", \"err\", err, \"task\", key, \"uid\", sudoUID, \"gid\", sudoGID)\n\t\t}\n\t}\n}\n\nfunc statePath(key string) string {\n\tdir := xdg.StateSubpath(\"devbox\")\n\tname := strings.ReplaceAll(key, \"/\", \"-\")\n\treturn filepath.Join(dir, name)\n}\n\nfunc taskError(key string, err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\treturn redact.Errorf(\"setup: task %s: %w\", key, err)\n}\n\n// devboxExecutable returns the path to the Devbox launcher script or the\n// current binary if the launcher is unavailable.\nfunc devboxExecutable() (string, error) {\n\tif exe := os.Getenv(envir.LauncherPath); exe != \"\" {\n\t\tif abs, err := filepath.Abs(exe); err == nil {\n\t\t\treturn abs, nil\n\t\t}\n\t}\n\n\texe, err := os.Executable()\n\tif err != nil {\n\t\treturn \"\", redact.Errorf(\"get path to devbox executable: %v\", err)\n\t}\n\treturn exe, nil\n}\n\nfunc isSudo(key string) bool {\n\t// DEVBOX_SUDO_TASK is set when a task relaunched Devbox by calling\n\t// SudoDevbox. If it matches the current task key, then the pre-sudo\n\t// process is already running this task and we can skip checking\n\t// task.NeedsRun and prompting the user.\n\tenvTask := os.Getenv(\"DEVBOX_SUDO_TASK\")\n\treturn envTask != \"\" && envTask == key\n}\n"
  },
  {
    "path": "internal/setup/setup_test.go",
    "content": "package setup\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n)\n\ntype testTask struct {\n\tRunFunc      func(ctx context.Context) error\n\tNeedsRunFunc func(ctx context.Context, lastRun RunInfo) bool\n}\n\nfunc (t *testTask) Run(ctx context.Context) error {\n\treturn t.RunFunc(ctx)\n}\n\nfunc (t *testTask) NeedsRun(ctx context.Context, lastRun RunInfo) bool {\n\treturn t.NeedsRunFunc(ctx, lastRun)\n}\n\nfunc TestTaskNeedsRunTrue(t *testing.T) {\n\ttempXDGStateDir(t)\n\n\tran := false\n\ttask := &testTask{\n\t\tRunFunc: func(ctx context.Context) error {\n\t\t\tran = true\n\t\t\treturn nil\n\t\t},\n\t\tNeedsRunFunc: func(context.Context, RunInfo) bool {\n\t\t\treturn true\n\t\t},\n\t}\n\n\terr := Run(t.Context(), t.Name(), task)\n\tif err != nil {\n\t\tt.Error(\"got non-nil error:\", err)\n\t}\n\tif !ran {\n\t\tt.Error(\"got ran = false, want true\")\n\t}\n}\n\nfunc TestTaskNeedsRunFalse(t *testing.T) {\n\ttempXDGStateDir(t)\n\n\tran := false\n\ttask := &testTask{\n\t\tRunFunc: func(ctx context.Context) error {\n\t\t\tran = true\n\t\t\treturn nil\n\t\t},\n\t\tNeedsRunFunc: func(context.Context, RunInfo) bool {\n\t\t\treturn false\n\t\t},\n\t}\n\n\terr := Run(t.Context(), t.Name(), task)\n\tif err != nil {\n\t\tt.Error(\"got non-nil error:\", err)\n\t}\n\tif ran {\n\t\tt.Error(\"got ran = true, want false\")\n\t}\n}\n\nfunc TestTaskLastRun(t *testing.T) {\n\ttempXDGStateDir(t)\n\n\ttask := &testTask{\n\t\tRunFunc:      func(ctx context.Context) error { return nil },\n\t\tNeedsRunFunc: func(context.Context, RunInfo) bool { return true },\n\t}\n\terr := Run(t.Context(), t.Name(), task)\n\tif err != nil {\n\t\tt.Error(\"got non-nil error on first run:\", err)\n\t}\n\n\ttask.NeedsRunFunc = func(ctx context.Context, lastRun RunInfo) bool {\n\t\tif lastRun.Time.IsZero() {\n\t\t\tt.Error(\"got zero lastRun.Time on second run\")\n\t\t}\n\t\tif lastRun.Version == \"\" {\n\t\t\tt.Error(\"got empty lastRun.Version on second run\")\n\t\t}\n\t\tif lastRun.Error != \"\" {\n\t\t\tt.Errorf(\"got non-empty lastRun.Error on second run: %v\", lastRun.Error)\n\t\t}\n\t\treturn false\n\t}\n\terr = Run(t.Context(), t.Name(), task)\n\tif err != nil {\n\t\tt.Error(\"got non-nil error on second run:\", err)\n\t}\n}\n\nfunc TestTaskConfirmPromptAllow(t *testing.T) {\n\ttempXDGStateDir(t)\n\n\ttask := &testTask{\n\t\tRunFunc:      func(ctx context.Context) error { return nil },\n\t\tNeedsRunFunc: func(context.Context, RunInfo) bool { return true },\n\t}\n\n\tsetPromptResponse(t, true)\n\terr := ConfirmRun(t.Context(), t.Name(), task, \"continue?\")\n\tif err != nil {\n\t\tt.Error(\"got non-nil error:\", err)\n\t}\n}\n\nfunc TestTaskConfirmPromptDeny(t *testing.T) {\n\ttempXDGStateDir(t)\n\n\ttask := &testTask{\n\t\tRunFunc:      func(ctx context.Context) error { return nil },\n\t\tNeedsRunFunc: func(context.Context, RunInfo) bool { return true },\n\t}\n\n\tsetPromptResponse(t, false)\n\terr := ConfirmRun(t.Context(), t.Name(), task, \"continue?\")\n\tif err == nil {\n\t\tt.Error(\"got nil error, want ErrUserRefused\")\n\t} else if !errors.Is(err, ErrUserRefused) {\n\t\tt.Error(\"got errors.Is(err, ErrUserRefused) == false for error:\", err)\n\t}\n}\n\n// TestSudoDevbox uses sudo on the current test binary to recursively call\n// itself as root. This test can only be run manually (because it needs sudo)\n// but is still useful for testing after making any changes to the sudo code.\n//\n//   - Within the test we check if os.Getuid() == 0 to act differently depending\n//     on if we're the sudo test process or the parent (non-sudo) test process.\n//   - The sudo version of the test creates a \"test-sudo-devbox-result\" file.\n//   - The non-sudo version of the test looks for the same file to know if the\n//     sudo worked.\nfunc TestSudoDevbox(t *testing.T) {\n\tt.Skip(\"this test must be run manually because it requires sudo\")\n\n\tctx := t.Context()\n\tkey := \"test-sudo-devbox\"\n\tresultFile := key + \"-result\"\n\n\t// Non-sudo process cleans up the result file.\n\tos.Remove(resultFile)\n\tt.Cleanup(func() {\n\t\tif os.Getuid() != 0 {\n\t\t\tos.Remove(resultFile)\n\t\t}\n\t})\n\n\ttask := &testTask{}\n\ttask.RunFunc = func(ctx context.Context) error {\n\t\tran, err := SudoDevbox(ctx, \"-test.run\", \"^\"+t.Name()+\"$\")\n\t\tif ran || err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Create a result file to indicate to the non-sudo process that\n\t\t// we ran as root successfully.\n\t\tif os.Getuid() == 0 {\n\t\t\treturn os.WriteFile(resultFile, nil, 0o666)\n\t\t}\n\t\terr = fmt.Errorf(\"task.NeedsRun not running as root after calling SudoDevbox\")\n\t\tt.Error(err)\n\t\treturn err\n\t}\n\ttask.NeedsRunFunc = func(ctx context.Context, lastRun RunInfo) bool {\n\t\tif os.Getuid() == 0 {\n\t\t\tt.Error(\"task.NeedsRun called in sudo process, but should only be called in user process\")\n\t\t}\n\t\treturn true\n\t}\n\n\told := defaultPrompt\n\tt.Cleanup(func() { defaultPrompt = old })\n\tdefaultPrompt = func(msg string) (response any, err error) {\n\t\tif os.Getuid() == 0 {\n\t\t\terr = fmt.Errorf(\"user prompted again while already running as sudo\")\n\t\t\tt.Error(err)\n\t\t\treturn false, err\n\t\t}\n\t\treturn true, nil\n\t}\n\n\terr := ConfirmRun(ctx, key, task, \"Allow sudo to run Devbox as root?\")\n\tif err != nil {\n\t\tt.Error(\"got ConfirmRun error:\", err)\n\t}\n\tif _, err := os.Stat(resultFile); err != nil {\n\t\tt.Error(\"got missing sudo result file:\", err)\n\t}\n}\n\nfunc tempXDGStateDir(t *testing.T) {\n\tt.Helper()\n\tt.Setenv(\"XDG_STATE_HOME\", t.TempDir())\n}\n\nfunc setPromptResponse(t *testing.T, a any) {\n\told := defaultPrompt\n\tt.Cleanup(func() { defaultPrompt = old })\n\tdefaultPrompt = func(string) (any, error) { return a, nil }\n}\n"
  },
  {
    "path": "internal/shellgen/doc.go",
    "content": "package shellgen\n\n// shellgen package is responsible for printing files in the .devbox/gen directory that\n// are needed to create the \"devbox shell environment\".\n//\n// This is flake.nix, and the init-hooks and scripts from the devbox.json.\n"
  },
  {
    "path": "internal/shellgen/flake_input.go",
    "content": "package shellgen\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"runtime/trace\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\nvar glibcPatchFlakeRef = flake.Ref{Type: flake.TypePath, Path: \"./glibc-patch\"}\n\ntype flakeInput struct {\n\tName     string\n\tPackages []*devpkg.Package\n\tRef      flake.Ref\n}\n\nfunc (f *flakeInput) HashFromNixPkgsURL() string {\n\treturn f.Ref.Rev\n}\n\nfunc (f *flakeInput) URLWithCaching() string {\n\tif !f.Ref.IsNixpkgs() {\n\t\treturn nix.FixInstallableArg(f.Ref.String())\n\t}\n\treturn getNixpkgsInfo(f.Ref.Rev).URL\n}\n\nfunc (f *flakeInput) PkgImportName() string {\n\treturn f.Name + \"-pkgs\"\n}\n\ntype SymlinkJoin struct {\n\tName  string\n\tPaths []string\n}\n\n// BuildInputsForSymlinkJoin returns a list of SymlinkJoin objects that can be used\n// as the buildInput. Used for packages that have non-default outputs that need to\n// be combined into a single buildInput.\nfunc (f *flakeInput) BuildInputsForSymlinkJoin() ([]*SymlinkJoin, error) {\n\tjoins := []*SymlinkJoin{}\n\tfor _, pkg := range f.Packages {\n\n\t\t// Skip packages that don't need a symlink join.\n\t\tif needs, err := needsSymlinkJoin(pkg); err != nil {\n\t\t\treturn nil, err\n\t\t} else if !needs {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Skip packages that are already in the binary cache. These will be directly\n\t\t// included in the buildInputs using `builtins.fetchClosure` of their store paths.\n\t\tinCache, err := pkg.IsInBinaryCache()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif inCache {\n\t\t\tcontinue\n\t\t}\n\n\t\tattributePath, err := pkg.FullPackageAttributePath()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif pkg.Patch {\n\t\t\treturn nil, errors.New(\"patch_glibc is not yet supported for packages with non-default outputs\")\n\t\t}\n\n\t\toutputNames, err := pkg.GetOutputNames()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tjoins = append(joins, &SymlinkJoin{\n\t\t\tName: pkg.String() + \"-combined\",\n\t\t\tPaths: lo.Map(outputNames, func(outputName string, _ int) string {\n\t\t\t\tif !f.Ref.IsNixpkgs() {\n\t\t\t\t\treturn f.Name + \".\" + attributePath + \".\" + outputName\n\t\t\t\t}\n\t\t\t\tparts := strings.Split(attributePath, \".\")\n\t\t\t\treturn f.PkgImportName() + \".\" + strings.Join(parts[2:], \".\") + \".\" + outputName\n\t\t\t}),\n\t\t})\n\t}\n\treturn joins, nil\n}\n\nfunc (f *flakeInput) BuildInputs() ([]string, error) {\n\tvar err error\n\n\t// Skip packages that will be handled in BuildInputsForSymlinkJoin\n\tpackages := []*devpkg.Package{}\n\tfor _, pkg := range f.Packages {\n\t\tif needs, err := needsSymlinkJoin(pkg); err != nil {\n\t\t\treturn nil, err\n\t\t} else if !needs {\n\t\t\tpackages = append(packages, pkg)\n\t\t}\n\t}\n\n\tattributePaths := lo.Map(packages, func(pkg *devpkg.Package, _ int) string {\n\t\tattributePath, attributePathErr := pkg.FullPackageAttributePath()\n\t\tif attributePathErr != nil {\n\t\t\terr = attributePathErr\n\t\t}\n\t\tif pkg.Patch {\n\t\t\t// When the package comes from the glibc flake, the\n\t\t\t// \"legacyPackages\" portion of the attribute path\n\t\t\t// becomes just \"packages\" (matching the standard flake\n\t\t\t// output schema).\n\t\t\treturn strings.Replace(attributePath, \"legacyPackages\", \"packages\", 1)\n\t\t}\n\t\treturn attributePath\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !f.Ref.IsNixpkgs() {\n\t\treturn lo.Map(attributePaths, func(pkg string, _ int) string {\n\t\t\treturn f.Name + \".\" + pkg\n\t\t}), nil\n\t}\n\treturn lo.Map(attributePaths, func(pkg string, _ int) string {\n\t\tparts := strings.Split(pkg, \".\")\n\t\t// Ugh, not sure if this is reliable?\n\t\treturn f.PkgImportName() + \".\" + strings.Join(parts[2:], \".\")\n\t}), nil\n}\n\n// flakeInputs returns a list of flake inputs for the top level flake.nix\n// created by devbox. We map packages to the correct flake and attribute path\n// and group flakes by URL to avoid duplication. All inputs should be locked\n// i.e. have a commit hash and always resolve to the same package/version.\n// Note: inputs returned by this function include plugin packages. (php only for now)\n// It's not entirely clear we always want to add plugin packages to the top level\nfunc flakeInputs(ctx context.Context, packages []*devpkg.Package) []flakeInput {\n\tdefer trace.StartRegion(ctx, \"flakeInputs\").End()\n\n\tvar flakeInputs keyedSlice\n\tfor _, pkg := range packages {\n\t\t// Non-nix packages (e.g. runx) don't belong in the flake\n\t\tif !pkg.IsNix() {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Don't include cached packages (like local or remote flakes)\n\t\t// that can be fetched from a Binary Cache Store.\n\t\t// TODO(savil): return error?\n\t\tcached, err := pkg.IsInBinaryCache()\n\t\tif err != nil {\n\t\t\tslog.Error(\"error checking if package is in binary cache\", \"err\", err)\n\t\t}\n\t\tif err == nil && cached {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Packages that need a glibc patch are assigned to the special\n\t\t// glibc-patched flake input. This input refers to the\n\t\t// glibc-patch.nix flake.\n\t\tif pkg.Patch {\n\t\t\tnixpkgsGlibc := flakeInputs.getOrAppend(glibcPatchFlakeRef.String())\n\t\t\tnixpkgsGlibc.Name = \"glibc-patch\"\n\t\t\tnixpkgsGlibc.Ref = glibcPatchFlakeRef\n\t\t\tnixpkgsGlibc.Packages = append(nixpkgsGlibc.Packages, pkg)\n\t\t\tcontinue\n\t\t}\n\n\t\tinstallable, err := pkg.FlakeInstallable()\n\t\tif err != nil {\n\t\t\t// I don't think this should happen at this point. The\n\t\t\t// packages should already be resolved to valid nixpkgs\n\t\t\t// packages.\n\t\t\tslog.Debug(\"error resolving package to flake installable\", \"err\", err)\n\t\t\tcontinue\n\t\t}\n\t\tflake := flakeInputs.getOrAppend(installable.Ref.String())\n\t\tflake.Name = pkg.FlakeInputName()\n\t\tflake.Ref = installable.Ref\n\n\t\t// TODO(gcurtis): is the uniqueness check necessary? We're\n\t\t// comparing pointers.\n\t\tif !slices.Contains(flake.Packages, pkg) {\n\t\t\tflake.Packages = append(flake.Packages, pkg)\n\t\t}\n\t}\n\treturn flakeInputs.slice\n}\n\n// keyedSlice keys the elements of an append-only slice for fast lookups.\ntype keyedSlice struct {\n\tslice  []flakeInput\n\tlookup map[string]int\n}\n\n// getOrAppend returns a pointer to the slice element with a given key. If the\n// key doesn't exist, a new element is automatically appended to the slice. The\n// pointer is valid until the next append.\nfunc (k *keyedSlice) getOrAppend(key string) *flakeInput {\n\tif k.lookup == nil {\n\t\tk.lookup = make(map[string]int)\n\t}\n\tif i, ok := k.lookup[key]; ok {\n\t\treturn &k.slice[i]\n\t}\n\tk.slice = append(k.slice, flakeInput{})\n\tk.lookup[key] = len(k.slice) - 1\n\treturn &k.slice[len(k.slice)-1]\n}\n\n// needsSymlinkJoin is used to filter packages with multiple outputs.\n// Multiple outputs -> SymlinkJoin.\n// Single or no output -> directly use in buildInputs\nfunc needsSymlinkJoin(pkg *devpkg.Package) (bool, error) {\n\toutputNames, err := pkg.GetOutputNames()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn len(outputNames) > 1, nil\n}\n"
  },
  {
    "path": "internal/shellgen/flake_plan.go",
    "content": "package shellgen\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime/trace\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/nix\"\n\t\"go.jetify.com/devbox/internal/patchpkg\"\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\n// flakePlan contains the data to populate the top level flake.nix file\n// that builds the devbox environment\ntype flakePlan struct {\n\tStdenv      flake.Ref\n\tPackages    []*devpkg.Package\n\tFlakeInputs []flakeInput\n\tSystem      string\n}\n\nfunc newFlakePlan(ctx context.Context, devbox devboxer) (*flakePlan, error) {\n\tctx, task := trace.NewTask(ctx, \"devboxFlakePlan\")\n\tdefer task.End()\n\n\tfor _, pluginConfig := range devbox.Config().IncludedPluginConfigs() {\n\t\tif err := devbox.PluginManager().CreateFilesForConfig(pluginConfig); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tpackages := devbox.InstallablePackages()\n\n\t// Fill the NarInfo Cache concurrently as a perf-optimization, prior to invoking\n\t// IsInBinaryCache in flakeInputs() and in the flake.nix template.\n\tif err := devpkg.FillNarInfoCache(ctx, packages...); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &flakePlan{\n\t\tFlakeInputs: flakeInputs(ctx, packages),\n\t\tStdenv:      devbox.Lockfile().Stdenv(),\n\t\tPackages:    packages,\n\t\tSystem:      nix.System(),\n\t}, nil\n}\n\nfunc (f *flakePlan) needsGlibcPatch() bool {\n\tfor _, in := range f.FlakeInputs {\n\t\tif in.Ref == glibcPatchFlakeRef {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\ntype glibcPatchFlake struct {\n\t// DevboxFlake provides the devbox binary that will act as the patch\n\t// flake's builder. By default it's set to \"github:jetify-com/devbox/\" +\n\t// [build.Version]. For dev builds, it's set to the local path to the\n\t// Devbox source code (this Go module) if it's available.\n\tDevboxFlake flake.Ref\n\n\t// NixpkgsGlibcFlakeRef is a flake reference to the nixpkgs flake\n\t// containing the new glibc package.\n\tNixpkgsGlibcFlakeRef string\n\n\t// Inputs is the attribute set of flake inputs. The key is the input\n\t// name and the value is a flake reference.\n\tInputs map[string]string\n\n\t// Outputs is the attribute set of flake outputs. It follows the\n\t// standard flake output schema of system.name = derivation. The\n\t// derivation can be any valid Nix expression.\n\tOutputs struct {\n\t\tPackages map[string]map[string]string\n\t}\n\n\t// Dependencies is set of extra packages that are dependencies of the\n\t// patched packages. For example, a patched Python interpreter might\n\t// need CUDA packages, but the CUDA packages themselves don't need\n\t// patching.\n\tDependencies []string\n}\n\nfunc newGlibcPatchFlake(nixpkgs flake.Ref, packages []*devpkg.Package) (glibcPatchFlake, error) {\n\tpatchFlake := glibcPatchFlake{\n\t\tDevboxFlake: flake.Ref{\n\t\t\tType:  flake.TypeGitHub,\n\t\t\tOwner: \"jetify-com\",\n\t\t\tRepo:  \"devbox\",\n\t\t\tRef:   build.Version,\n\t\t},\n\t\tNixpkgsGlibcFlakeRef: nixpkgs.String(),\n\t}\n\n\t// In dev builds, use the local Devbox flake for patching packages\n\t// instead of the one on GitHub. Using build.IsDev doesn't work because\n\t// DEVBOX_PROD=1 will attempt to download 0.0.0-dev from GitHub.\n\tif strings.HasPrefix(build.Version, \"0.0.0\") {\n\t\tsrc, err := build.SourceDir()\n\t\tif err != nil {\n\t\t\tslog.Error(\"can't find the local devbox flake for patching, falling back to the latest github release\", \"err\", err)\n\t\t\tpatchFlake.DevboxFlake = flake.Ref{\n\t\t\t\tType:  flake.TypeGitHub,\n\t\t\t\tOwner: \"jetify-com\",\n\t\t\t\tRepo:  \"devbox\",\n\t\t\t}\n\t\t} else {\n\t\t\tpatchFlake.DevboxFlake = flake.Ref{Type: flake.TypePath, Path: src}\n\t\t}\n\t}\n\n\tfor _, pkg := range packages {\n\t\t// Check to see if this is a CUDA package. If so, we need to add\n\t\t// it to the flake dependencies so that we can patch other\n\t\t// packages to reference it (like Python).\n\t\trelAttrPath, err := patchFlake.systemRelativeAttrPath(pkg)\n\t\tif err != nil {\n\t\t\treturn glibcPatchFlake{}, err\n\t\t}\n\t\tif strings.HasPrefix(relAttrPath, \"cudaPackages\") {\n\t\t\tif err := patchFlake.addDependency(pkg); err != nil {\n\t\t\t\treturn glibcPatchFlake{}, err\n\t\t\t}\n\t\t}\n\n\t\tif !pkg.Patch {\n\t\t\tcontinue\n\t\t}\n\t\tif err := patchFlake.addOutput(pkg); err != nil {\n\t\t\treturn glibcPatchFlake{}, err\n\t\t}\n\t}\n\n\tslog.Debug(\"creating new patch flake\", \"flake\", &patchFlake)\n\treturn patchFlake, nil\n}\n\n// addInput adds a flake input that provides pkg.\nfunc (g *glibcPatchFlake) addInput(pkg *devpkg.Package) error {\n\tif g.Inputs == nil {\n\t\tg.Inputs = make(map[string]string)\n\t}\n\tinstallable, err := pkg.FlakeInstallable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tinputName := pkg.FlakeInputName()\n\tg.Inputs[inputName] = installable.Ref.String()\n\treturn nil\n}\n\n// addOutput adds a flake output that provides the patched version of pkg.\nfunc (g *glibcPatchFlake) addOutput(pkg *devpkg.Package) error {\n\tif err := g.addInput(pkg); err != nil {\n\t\treturn err\n\t}\n\n\trelAttrPath, err := g.systemRelativeAttrPath(pkg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif g.Outputs.Packages == nil {\n\t\tg.Outputs.Packages = map[string]map[string]string{nix.System(): {}}\n\t}\n\tif cached, err := pkg.IsInBinaryCache(); err == nil && cached {\n\t\tif expr, err := g.fetchClosureExpr(pkg); err == nil {\n\t\t\tg.Outputs.Packages[nix.System()][relAttrPath] = expr\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tinputAttrPath, err := g.inputRelativeAttrPath(pkg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tg.Outputs.Packages[nix.System()][relAttrPath] = inputAttrPath\n\treturn nil\n}\n\n// addDependency adds pkg to the derivation's patchDependencies attribute,\n// making it available at patch build-time.\nfunc (g *glibcPatchFlake) addDependency(pkg *devpkg.Package) error {\n\tif err := g.addInput(pkg); err != nil {\n\t\treturn err\n\t}\n\tinputAttrPath, err := g.inputRelativeAttrPath(pkg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinstallable, err := pkg.FlakeInstallable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch installable.Outputs {\n\tcase flake.DefaultOutputs:\n\t\texpr := \"selectDefaultOutputs \" + inputAttrPath\n\t\tg.Dependencies = append(g.Dependencies, expr)\n\tcase flake.AllOutputs:\n\t\texpr := \"selectAllOutputs \" + inputAttrPath\n\t\tg.Dependencies = append(g.Dependencies, expr)\n\tdefault:\n\t\texpr := fmt.Sprintf(\"selectOutputs %s %q\", inputAttrPath, installable.SplitOutputs())\n\t\tg.Dependencies = append(g.Dependencies, expr)\n\t}\n\treturn nil\n}\n\n// systemRelativeAttrPath strips any leading \"legacyPackages.<system>\" prefix\n// from a package's attribute path.\nfunc (g *glibcPatchFlake) systemRelativeAttrPath(pkg *devpkg.Package) (string, error) {\n\tinstallable, err := pkg.FlakeInstallable()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif strings.HasPrefix(installable.AttrPath, \"legacyPackages\") {\n\t\t// Remove the legacyPackages.<system> prefix.\n\t\treturn strings.SplitN(installable.AttrPath, \".\", 3)[2], nil\n\t}\n\treturn installable.AttrPath, nil\n}\n\n// inputRelativeAttrPath joins the package's corresponding flake input with its\n// attribute path.\nfunc (g *glibcPatchFlake) inputRelativeAttrPath(pkg *devpkg.Package) (string, error) {\n\trelAttrPath, err := g.systemRelativeAttrPath(pkg)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tatrrPath := strings.Join([]string{\"pkgs\", pkg.FlakeInputName(), nix.System(), relAttrPath}, \".\")\n\treturn atrrPath, nil\n}\n\n// TODO: this only handles the first store path, but we should handle all of them\nfunc (g *glibcPatchFlake) fetchClosureExpr(pkg *devpkg.Package) (string, error) {\n\tstorePaths, err := pkg.InputAddressedPaths()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(storePaths) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no store path for package %s\", pkg.Raw)\n\t}\n\treturn fmt.Sprintf(`builtins.fetchClosure {\n  fromStore = \"%s\";\n  fromPath = \"%s\";\n  inputAddressed = true;\n}`, \"devpkg.BinaryCache\", storePaths[0]), nil\n}\n\n// copySystemCUDALib searches for the system's libcuda.so shared library and\n// copies it in the flake's lib directory.\nfunc (g *glibcPatchFlake) copySystemCUDALib(flakeDir string) error {\n\tslog.Debug(\"found CUDA package in devbox environment, attempting to find system driver libraries\")\n\n\tsearchPath := slices.Concat(\n\t\tpatchpkg.EnvLDLibrarySearchPath,\n\t\tpatchpkg.EnvLibrarySearchPath,\n\t\tpatchpkg.SystemLibSearchPaths,\n\t\tpatchpkg.CUDALibSearchPaths,\n\t)\n\tfor lib := range patchpkg.FindSharedLibrary(\"libcuda.so\", searchPath...) {\n\t\tlogger := slog.With(\"lib\", lib)\n\t\tlogger.Debug(\"found potential system CUDA library\")\n\n\t\tstat, err := lib.Stat()\n\t\tif err != nil {\n\t\t\tlogger.Error(\"skipping system CUDA library because of stat error\", \"err\", err)\n\t\t}\n\t\tconst mib = 1 << 20\n\t\tif stat.Size() < 1*mib {\n\t\t\tlogger.Debug(\"skipping system CUDA library because it looks like a stub (size < 1 MiB)\", \"size\", stat.Size())\n\t\t\tcontinue\n\t\t}\n\t\tif lib.Soname == \"\" {\n\t\t\tlogger.Debug(\"skipping system CUDA library because it's missing a soname\")\n\t\t\tcontinue\n\t\t}\n\n\t\tlibDir := filepath.Join(flakeDir, \"lib\")\n\t\tif err := lib.CopyAndLink(libDir); err == nil {\n\t\t\tslog.Debug(\"copied system CUDA library to flake directory\", \"dst\", libDir)\n\t\t} else {\n\t\t\tslog.Error(\"can't copy system CUDA library to flake directory\", \"err\", err)\n\t\t}\n\t\treturn err\n\t}\n\treturn fmt.Errorf(\"can't find the system CUDA library\")\n}\n\nfunc (g *glibcPatchFlake) writeTo(dir string) error {\n\twantCUDA := slices.ContainsFunc(g.Dependencies, func(dep string) bool {\n\t\treturn strings.Contains(dep, \"cudaPackages\")\n\t})\n\tif wantCUDA {\n\t\terr := g.copySystemCUDALib(dir)\n\t\tif err != nil {\n\t\t\tslog.Debug(\"error copying system libcuda.so to flake\", \"dir\", dir)\n\t\t}\n\t}\n\tchanged, err := writeFromTemplate(dir, g, \"glibc-patch.nix\", \"flake.nix\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif changed {\n\t\t_ = os.Remove(filepath.Join(dir, \"flake.lock\"))\n\t}\n\treturn nil\n}\n\nfunc (g *glibcPatchFlake) LogValue() slog.Value {\n\tinputs := make([]slog.Attr, 0, 2+len(g.Inputs))\n\tinputs = append(inputs,\n\t\tslog.String(\"devbox\", g.DevboxFlake.String()),\n\t\tslog.String(\"nixpkgs-glibc\", g.NixpkgsGlibcFlakeRef),\n\t)\n\tfor k, v := range g.Inputs {\n\t\tinputs = append(inputs, slog.String(k, v))\n\t}\n\n\tvar outputs []string\n\tfor _, pkg := range g.Outputs.Packages {\n\t\tfor attrPath := range pkg {\n\t\t\toutputs = append(outputs, attrPath)\n\t\t}\n\t}\n\treturn slog.GroupValue(\n\t\tslog.Attr{Key: \"inputs\", Value: slog.GroupValue(inputs...)},\n\t\tslog.Attr{Key: \"outputs\", Value: slog.AnyValue(outputs)},\n\t)\n}\n"
  },
  {
    "path": "internal/shellgen/flake_plan_test.go",
    "content": "package shellgen\n\nimport (\n\t\"testing\"\n\n\t\"go.jetify.com/devbox/internal/devbox/devopt\"\n\t\"go.jetify.com/devbox/internal/devconfig/configfile\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\ntype lockMock struct{}\n\nfunc (l *lockMock) Get(key string) *lock.Package {\n\treturn nil\n}\n\nfunc (l *lockMock) Stdenv() flake.Ref {\n\treturn flake.Ref{}\n}\n\nfunc (l *lockMock) ProjectDir() string {\n\treturn \"\"\n}\n\nfunc (l *lockMock) Resolve(key string) (*lock.Package, error) {\n\treturn &lock.Package{\n\t\tResolved: \"github:NixOS/nixpkgs/10b813040df67c4039086db0f6eaf65c536886c6#python312\",\n\t}, nil\n}\n\nfunc TestNewGlibcPatchFlake(t *testing.T) {\n\tstdenv := flake.Ref{\n\t\tType: flake.TypeGitHub,\n\t\tURL:  \"https://github.com/NixOS/nixpkgs\",\n\t\tRef:  \"nixpkgs-unstable\",\n\t}\n\n\tpackages := devpkg.PackagesFromStringsWithOptions([]string{\"python@latest\"}, &lockMock{}, devopt.AddOpts{\n\t\tPatch: string(configfile.PatchAlways),\n\t})\n\n\tpatchFlake, err := newGlibcPatchFlake(stdenv, packages)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\n\tif patchFlake.NixpkgsGlibcFlakeRef != stdenv.String() {\n\t\tt.Errorf(\"expected NixpkgsGlibcFlakeRef to be %s, got %s\", stdenv.String(), patchFlake.NixpkgsGlibcFlakeRef)\n\t}\n\n\tif len(patchFlake.Outputs.Packages) != 1 {\n\t\tt.Errorf(\"expected 1 package in Outputs, got %d\", len(patchFlake.Outputs.Packages))\n\t}\n}\n"
  },
  {
    "path": "internal/shellgen/generate.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage shellgen\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"embed\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime/trace\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/cuecfg\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/redact\"\n)\n\n//go:embed tmpl/*\nvar tmplFS embed.FS\n\n// GenerateForPrintEnv will create all the files necessary for processing\n// devbox.PrintEnv, which is the core function from which devbox shell/run/direnv\n// functionality is derived.\nfunc GenerateForPrintEnv(ctx context.Context, devbox devboxer) error {\n\tdefer trace.StartRegion(ctx, \"GenerateForPrintEnv\").End()\n\n\tplan, err := newFlakePlan(ctx, devbox)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\toutPath := genPath(devbox)\n\n\t// Preserving shell.nix to avoid breaking old-style .envrc users\n\t_, err = writeFromTemplate(outPath, plan, \"shell.nix\", \"shell.nix\")\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// Gitignore file is added to the .devbox directory\n\t_, err = writeFromTemplate(filepath.Join(devbox.ProjectDir(), \".devbox\"), plan, \".gitignore\", \".gitignore\")\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tif plan.needsGlibcPatch() {\n\t\tpatch, err := newGlibcPatchFlake(devbox.Lockfile().Stdenv(), plan.Packages)\n\t\tif err != nil {\n\t\t\treturn redact.Errorf(\"generate glibc patch flake: %v\", err)\n\t\t}\n\t\tif err := patch.writeTo(filepath.Join(FlakePath(devbox), \"glibc-patch\")); err != nil {\n\t\t\treturn redact.Errorf(\"write glibc patch flake to directory: %v\", err)\n\t\t}\n\t}\n\tif err := makeFlakeFile(devbox, plan); err != nil {\n\t\treturn err\n\t}\n\n\treturn WriteScriptsToFiles(devbox)\n}\n\n// Cache and buffers for generating templated files.\nvar (\n\ttmplCache = map[string]*template.Template{}\n\ttmplBuf   bytes.Buffer\n)\n\nfunc writeFromTemplate(path string, plan any, tmplName, generatedName string) (changed bool, err error) {\n\ttmplKey := tmplName + \".tmpl\"\n\ttmpl := tmplCache[tmplKey]\n\tif tmpl == nil {\n\t\ttmpl = template.New(tmplKey)\n\t\ttmpl.Funcs(templateFuncs)\n\n\t\tvar err error\n\t\tglob := \"tmpl/\" + tmplKey\n\t\ttmpl, err = tmpl.ParseFS(tmplFS, glob)\n\t\tif err != nil {\n\t\t\treturn false, redact.Errorf(\"parse embedded tmplFS glob %q: %v\", redact.Safe(glob), redact.Safe(err))\n\t\t}\n\t\ttmplCache[tmplKey] = tmpl\n\t}\n\ttmplBuf.Reset()\n\tif err := tmpl.Execute(&tmplBuf, plan); err != nil {\n\t\treturn false, redact.Errorf(\"execute template %s: %v\", redact.Safe(tmplKey), err)\n\t}\n\n\t// In some circumstances, Nix looks at the mod time of a file when\n\t// caching, so we only want to update the file if something has\n\t// changed. Blindly overwriting the file could invalidate Nix's cache\n\t// every time, slowing down evaluation considerably.\n\tchanged, err = overwriteFileIfChanged(filepath.Join(path, generatedName), tmplBuf.Bytes(), 0o644)\n\tif err != nil {\n\t\treturn changed, redact.Errorf(\"write %s to file: %v\", redact.Safe(tmplName), err)\n\t}\n\treturn changed, nil\n}\n\n// overwriteFileIfChanged checks that the contents of f == data, and overwrites\n// f if they differ. It also ensures that f's permissions are set to perm.\nfunc overwriteFileIfChanged(path string, data []byte, perm os.FileMode) (changed bool, err error) {\n\tflag := os.O_RDWR | os.O_CREATE\n\tfile, err := os.OpenFile(path, flag, perm)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\t// Definitely a new file if we had to make the directory.\n\t\treturn true, os.WriteFile(path, data, perm)\n\t}\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer file.Close()\n\n\tfi, err := file.Stat()\n\tif err != nil || fi.Mode().Perm() != perm {\n\t\tif err := file.Chmod(perm); err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\t// Fast path - check if the lengths differ.\n\tif err == nil && fi.Size() != int64(len(data)) {\n\t\treturn true, overwriteFile(file, data, 0)\n\t}\n\n\tr := bufio.NewReader(file)\n\tfor offset := range data {\n\t\tb, err := r.ReadByte()\n\t\tif err != nil || b != data[offset] {\n\t\t\treturn true, overwriteFile(file, data, offset)\n\t\t}\n\t}\n\treturn false, nil\n}\n\n// overwriteFile truncates f to len(data) and writes data[offset:] beginning at\n// the same offset in f.\nfunc overwriteFile(f *os.File, data []byte, offset int) error {\n\terr := f.Truncate(int64(len(data)))\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = f.WriteAt(data[offset:], int64(offset))\n\treturn err\n}\n\nfunc toJSON(a any) string {\n\tdata, err := cuecfg.MarshalJSON(a)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn string(data)\n}\n\nvar templateFuncs = template.FuncMap{\n\t\"json\":     toJSON,\n\t\"contains\": strings.Contains,\n\t\"debug\":    debug.IsEnabled,\n}\n\nfunc makeFlakeFile(d devboxer, plan *flakePlan) error {\n\tflakeDir := FlakePath(d)\n\tchanged, err := writeFromTemplate(flakeDir, plan, \"flake.nix\", \"flake.nix\")\n\tif changed {\n\t\t_ = os.Remove(filepath.Join(flakeDir, \"flake.lock\"))\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "internal/shellgen/generate_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage shellgen\n\nimport (\n\t\"flag\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/searcher\"\n\t\"go.jetify.com/devbox/nix/flake\"\n)\n\n// update overwrites golden files with the new test results.\nvar update = flag.Bool(\"update\", false, \"update the golden files with the test results\")\n\n// TestWriteFromTemplate will verify that the flake.nix code generation works as expected.\n// Note: this test was derived from an older flake.nix, prior to having the builtins.FetchClosures\n// and so may be a bit out of date. It could be updated to be better and more exhaustive.\nfunc TestWriteFromTemplate(t *testing.T) {\n\tt.Setenv(\"__DEVBOX_NIX_SYSTEM\", \"x86_64-linux\")\n\tdir := filepath.Join(t.TempDir(), \"makeme\")\n\toutPath := filepath.Join(dir, \"flake.nix\")\n\t_, err := writeFromTemplate(dir, testFlakeTmplPlan, \"flake.nix\", \"flake.nix\")\n\tif err != nil {\n\t\tt.Fatal(\"got error writing flake template:\", err)\n\t}\n\tcmpGoldenFile(t, outPath, \"testdata/flake.nix.golden\")\n\n\tt.Run(\"WriteUnmodified\", func(t *testing.T) {\n\t\t_, err = writeFromTemplate(dir, testFlakeTmplPlan, \"flake.nix\", \"flake.nix\")\n\t\tif err != nil {\n\t\t\tt.Fatal(\"got error writing flake template:\", err)\n\t\t}\n\t\tcmpGoldenFile(t, outPath, \"testdata/flake.nix.golden\")\n\t})\n\tt.Run(\"WriteModifiedSmaller\", func(t *testing.T) {\n\t\temptyPlan := &flakePlan{\n\t\t\tPackages:    []*devpkg.Package{},\n\t\t\tFlakeInputs: []flakeInput{},\n\t\t\tSystem:      \"x86_64-linux\",\n\t\t}\n\t\t_, err = writeFromTemplate(dir, emptyPlan, \"flake.nix\", \"flake.nix\")\n\t\tif err != nil {\n\t\t\tt.Fatal(\"got error writing flake template:\", err)\n\t\t}\n\t\tcmpGoldenFile(t, outPath, \"testdata/flake-empty.nix.golden\")\n\t})\n}\n\nfunc cmpGoldenFile(t *testing.T, gotPath, wantGoldenPath string) {\n\tgot, err := os.ReadFile(gotPath)\n\tif err != nil {\n\t\tt.Fatal(\"got error reading generated file:\", err)\n\t}\n\tif *update {\n\t\terr = os.WriteFile(wantGoldenPath, got, 0o666)\n\t\tif err != nil {\n\t\t\tt.Error(\"got error updating golden file:\", err)\n\t\t}\n\t}\n\tgolden, err := os.ReadFile(wantGoldenPath)\n\tif err != nil {\n\t\tt.Fatal(\"got error reading golden file:\", err)\n\t}\n\tdiff := cmp.Diff(golden, got)\n\tif diff != \"\" {\n\t\tt.Errorf(strings.TrimSpace(`\ngot wrong file contents (-%s +%s):\n\n%s\nIf the new file is correct, you can update the golden file with:\n\n\tgo test -run \"^%s$\" -update`),\n\t\t\tfilepath.Base(wantGoldenPath), filepath.Base(gotPath),\n\t\t\tdiff, t.Name())\n\t}\n}\n\nvar (\n\tlocker            = &lockmock{}\n\ttestFlakeTmplPlan = &flakePlan{\n\t\tPackages: []*devpkg.Package{}, // TODO savil\n\t\tFlakeInputs: []flakeInput{\n\t\t\t{\n\t\t\t\tName: \"nixpkgs\",\n\t\t\t\tRef:  flake.Ref{Type: flake.TypeGitHub, Owner: \"NixOS\", Repo: \"nixpkgs\", Rev: \"b9c00c1d41ccd6385da243415299b39aa73357be\"},\n\t\t\t\tPackages: []*devpkg.Package{\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"php@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"php81Packages.composer@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"php81Extensions.blackfire@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"flyctl@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"postgresql@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"tree@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"git@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"zsh@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"openssh@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"vim@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"sqlite@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"jq@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"delve@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"ripgrep@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"shellcheck@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"terraform@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"xz@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"zstd@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"gnupg@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"go_1_20@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"python3@latest\", locker),\n\t\t\t\t\tdevpkg.PackageFromStringWithDefaults(\"graphviz@latest\", locker),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tSystem: \"x86_64-linux\",\n\t}\n)\n\ntype lockmock struct{}\n\nfunc (*lockmock) Resolve(pkg string) (*lock.Package, error) {\n\tname, _, _ := searcher.ParseVersionedPackage(pkg)\n\treturn &lock.Package{\n\t\tResolved: \"github:NixOS/nixpkgs/b22db301217578a8edfccccf5cedafe5fc54e78b#\" + name,\n\t}, nil\n}\n\nfunc (*lockmock) Get(pkg string) *lock.Package { return nil }\nfunc (*lockmock) Stdenv() flake.Ref            { return flake.Ref{} }\nfunc (*lockmock) ProjectDir() string           { return \"\" }\n"
  },
  {
    "path": "internal/shellgen/nixpkgs.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage shellgen\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"go.jetify.com/devbox/internal/envir\"\n)\n\n// Contains default nixpkgs used for mkShell\ntype NixpkgsInfo struct {\n\tURL    string\n\tTarURL string\n}\n\nfunc getNixpkgsInfo(commitHash string) *NixpkgsInfo {\n\turl := fmt.Sprintf(\"github:NixOS/nixpkgs/%s\", commitHash)\n\tif mirror := nixpkgsMirrorURL(commitHash); mirror != \"\" {\n\t\turl = mirror\n\t}\n\treturn &NixpkgsInfo{\n\t\tURL: url,\n\t\t// legacy, used for shell.nix (which is no longer used, but some direnv users still need it)\n\t\tTarURL: fmt.Sprintf(\"https://github.com/nixos/nixpkgs/archive/%s.tar.gz\", commitHash),\n\t}\n}\n\nfunc nixpkgsMirrorURL(commitHash string) string {\n\tbaseURL := os.Getenv(envir.DevboxCache)\n\tif baseURL == \"\" {\n\t\treturn \"\"\n\t}\n\n\t// Check that the mirror is responsive and has the tar file. We can't\n\t// leave this up to Nix because fetchTarball will retry indefinitely.\n\tclient := &http.Client{Timeout: 3 * time.Second}\n\tmirrorURL := fmt.Sprintf(\"%s/nixos/nixpkgs/archive/%s.tar.gz\", baseURL, commitHash)\n\tresp, err := client.Head(mirrorURL)\n\tif err != nil || resp.StatusCode != http.StatusOK {\n\t\treturn \"\"\n\t}\n\treturn mirrorURL\n}\n"
  },
  {
    "path": "internal/shellgen/path.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage shellgen\n\nimport \"path/filepath\"\n\nfunc genPath(d devboxer) string {\n\treturn filepath.Join(d.ProjectDir(), \".devbox/gen\")\n}\n\nfunc FlakePath(d devboxer) string {\n\treturn filepath.Join(genPath(d), \"flake\")\n}\n"
  },
  {
    "path": "internal/shellgen/scripts.go",
    "content": "package shellgen\n\nimport (\n\t\"bytes\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.jetify.com/devbox/internal/boxcli/featureflag\"\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/devconfig\"\n\t\"go.jetify.com/devbox/internal/devpkg\"\n\t\"go.jetify.com/devbox/internal/lock\"\n\t\"go.jetify.com/devbox/internal/plugin\"\n)\n\n//go:embed tmpl/script-wrapper.tmpl\nvar scriptWrapperTmplString string\nvar scriptWrapperTmpl = template.Must(template.New(\"script-wrapper\").Parse(scriptWrapperTmplString))\n\nconst scriptsDir = \".devbox/gen/scripts\"\n\nconst HooksFilename = \".hooks\"\n\ntype devboxer interface {\n\tConfig() *devconfig.Config\n\tLockfile() *lock.File\n\tInstallablePackages() []*devpkg.Package\n\tPluginManager() *plugin.Manager\n\tProjectDir() string\n\tSkipInitHookEnvName() string\n}\n\n// WriteScriptsToFiles writes scripts defined in devbox.json into files inside .devbox/gen/scripts.\n// Scripts (and hooks) are persisted so that we can easily call them from devbox run (inside or outside shell).\nfunc WriteScriptsToFiles(devbox devboxer) error {\n\tdefer debug.FunctionTimer().End()\n\terr := os.MkdirAll(filepath.Join(devbox.ProjectDir(), scriptsDir), 0o755) // Ensure directory exists.\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// Read dir contents before writing, so we can clean up later.\n\tentries, err := os.ReadDir(filepath.Join(devbox.ProjectDir(), scriptsDir))\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// Write all hooks to a file.\n\twritten := map[string]struct{}{} // set semantics; value is irrelevant\n\t// always write it, even if there are no hooks, because scripts will source it.\n\terr = writeRawInitHookFile(devbox, devbox.Config().InitHook().String())\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\twritten[HooksFilename] = struct{}{}\n\n\t// Write scripts to files.\n\tfor name, body := range devbox.Config().Scripts() {\n\t\tscriptBody, err := ScriptBody(devbox, body.String())\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\terr = WriteScriptFile(devbox, name, scriptBody)\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\twritten[name] = struct{}{}\n\t}\n\n\t// Delete any files that weren't written just now.\n\tfor _, entry := range entries {\n\t\tscriptName := strings.TrimSuffix(entry.Name(), \".sh\")\n\t\tif _, ok := written[scriptName]; !ok && !entry.IsDir() {\n\t\t\terr := os.Remove(ScriptPath(devbox.ProjectDir(), scriptName))\n\t\t\tif err != nil {\n\t\t\t\tslog.Debug(\"failed to clean up script file %s, error = %s\", entry.Name(), err) // no need to fail run\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc writeRawInitHookFile(devbox devboxer, body string) (err error) {\n\tscript, err := createScriptFile(devbox, HooksFilename)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tdefer script.Close() // best effort: close file\n\n\t_, err = script.WriteString(body)\n\treturn errors.WithStack(err)\n}\n\nfunc WriteScriptFile(devbox devboxer, name, body string) (err error) {\n\tscript, err := createScriptFile(devbox, name)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tdefer script.Close() // best effort: close file\n\n\tif featureflag.ScriptExitOnError.Enabled() {\n\t\t// NOTE: Devbox scripts run using `sh` for consistency.\n\t\tbody = fmt.Sprintf(\"set -e\\n\\n%s\", body)\n\t}\n\t_, err = script.WriteString(body)\n\treturn errors.WithStack(err)\n}\n\nfunc createScriptFile(devbox devboxer, name string) (script *os.File, err error) {\n\tscript, err = os.Create(ScriptPath(devbox.ProjectDir(), name))\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tdefer func() {\n\t\t// best effort: close file if there was some subsequent error\n\t\tif err != nil {\n\t\t\t_ = script.Close()\n\t\t}\n\t}()\n\n\terr = script.Chmod(0o755)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\treturn script, nil\n}\n\nfunc ScriptPath(projectDir, scriptName string) string {\n\treturn filepath.Join(projectDir, scriptsDir, scriptName+\".sh\")\n}\n\nfunc ScriptBody(d devboxer, body string) (string, error) {\n\tvar buf bytes.Buffer\n\terr := scriptWrapperTmpl.Execute(&buf, map[string]string{\n\t\t\"Body\":             body,\n\t\t\"SkipInitHookHash\": d.SkipInitHookEnvName(),\n\t\t\"InitHookPath\":     ScriptPath(d.ProjectDir(), HooksFilename),\n\t})\n\tif err != nil {\n\t\treturn \"\", errors.WithStack(err)\n\t}\n\treturn buf.String(), nil\n}\n"
  },
  {
    "path": "internal/shellgen/testdata/flake-empty.nix.golden",
    "content": "{\n   description = \"A devbox shell\";\n\n   inputs = {\n     nixpkgs.url = \"\";\n   };\n\n   outputs = {\n     self,\n     nixpkgs,\n   }:\n      let\n        pkgs = nixpkgs.legacyPackages.x86_64-linux;\n      in\n      {\n        devShells.x86_64-linux.default = pkgs.mkShell {\n          buildInputs = [\n          ];\n        };\n      };\n }\n"
  },
  {
    "path": "internal/shellgen/testdata/flake.nix.golden",
    "content": "{\n   description = \"A devbox shell\";\n\n   inputs = {\n     nixpkgs.url = \"\";\n     nixpkgs.url = \"github:NixOS/nixpkgs/b9c00c1d41ccd6385da243415299b39aa73357be\";\n   };\n\n   outputs = {\n     self,\n     nixpkgs,\n     nixpkgs,\n   }:\n      let\n        pkgs = nixpkgs.legacyPackages.x86_64-linux;\n        nixpkgs-pkgs = (import nixpkgs {\n          system = \"x86_64-linux\";\n          config.allowUnfree = true;\n          config.permittedInsecurePackages = [\n          ];\n        });\n      in\n      {\n        devShells.x86_64-linux.default = pkgs.mkShell {\n          buildInputs = [\n            (builtins.trace \"evaluating nixpkgs-pkgs.php\" nixpkgs-pkgs.php)\n            (builtins.trace \"evaluating nixpkgs-pkgs.php81Packages.composer\" nixpkgs-pkgs.php81Packages.composer)\n            (builtins.trace \"evaluating nixpkgs-pkgs.php81Extensions.blackfire\" nixpkgs-pkgs.php81Extensions.blackfire)\n            (builtins.trace \"evaluating nixpkgs-pkgs.flyctl\" nixpkgs-pkgs.flyctl)\n            (builtins.trace \"evaluating nixpkgs-pkgs.postgresql\" nixpkgs-pkgs.postgresql)\n            (builtins.trace \"evaluating nixpkgs-pkgs.tree\" nixpkgs-pkgs.tree)\n            (builtins.trace \"evaluating nixpkgs-pkgs.git\" nixpkgs-pkgs.git)\n            (builtins.trace \"evaluating nixpkgs-pkgs.zsh\" nixpkgs-pkgs.zsh)\n            (builtins.trace \"evaluating nixpkgs-pkgs.openssh\" nixpkgs-pkgs.openssh)\n            (builtins.trace \"evaluating nixpkgs-pkgs.vim\" nixpkgs-pkgs.vim)\n            (builtins.trace \"evaluating nixpkgs-pkgs.sqlite\" nixpkgs-pkgs.sqlite)\n            (builtins.trace \"evaluating nixpkgs-pkgs.jq\" nixpkgs-pkgs.jq)\n            (builtins.trace \"evaluating nixpkgs-pkgs.delve\" nixpkgs-pkgs.delve)\n            (builtins.trace \"evaluating nixpkgs-pkgs.ripgrep\" nixpkgs-pkgs.ripgrep)\n            (builtins.trace \"evaluating nixpkgs-pkgs.shellcheck\" nixpkgs-pkgs.shellcheck)\n            (builtins.trace \"evaluating nixpkgs-pkgs.terraform\" nixpkgs-pkgs.terraform)\n            (builtins.trace \"evaluating nixpkgs-pkgs.xz\" nixpkgs-pkgs.xz)\n            (builtins.trace \"evaluating nixpkgs-pkgs.zstd\" nixpkgs-pkgs.zstd)\n            (builtins.trace \"evaluating nixpkgs-pkgs.gnupg\" nixpkgs-pkgs.gnupg)\n            (builtins.trace \"evaluating nixpkgs-pkgs.go_1_20\" nixpkgs-pkgs.go_1_20)\n            (builtins.trace \"evaluating nixpkgs-pkgs.python3\" nixpkgs-pkgs.python3)\n            (builtins.trace \"evaluating nixpkgs-pkgs.graphviz\" nixpkgs-pkgs.graphviz)\n          ];\n        };\n      };\n }\n"
  },
  {
    "path": "internal/shellgen/tmpl/.gitignore.tmpl",
    "content": "*\n.*\n"
  },
  {
    "path": "internal/shellgen/tmpl/flake.nix.tmpl",
    "content": "{\n   description = \"A devbox shell\";\n\n   inputs = {\n     nixpkgs.url = \"{{ .Stdenv }}\";\n\n     {{- range .FlakeInputs }}\n     {{.Name}}.url = \"{{.URLWithCaching}}\";\n     {{- end }}\n   };\n\n   outputs = {\n     self,\n     nixpkgs,\n     {{- range .FlakeInputs }}\n     {{.Name}},\n     {{- end }}\n   }:\n      let\n        pkgs = nixpkgs.legacyPackages.{{ .System }};\n        {{- range $_, $flake := .FlakeInputs }}\n        {{- if $flake.Ref.IsNixpkgs }}\n        {{.PkgImportName}} = (import {{.Name}} {\n          system = \"{{ $.System }}\";\n          config.allowUnfree = true;\n          config.permittedInsecurePackages = [\n            {{- range $flake.Packages }}\n            {{- range .AllowInsecure }}\n            \"{{ . }}\"\n            {{- end }}\n            {{- end }}\n          ];\n        });\n        {{- end }}\n        {{- end }}\n      in\n      {\n        devShells.{{ .System }}.default = pkgs.mkShell {\n          buildInputs = [\n            {{- range $_, $pkg := .Packages }}\n            {{- range $_, $output := $pkg.GetOutputsWithCache }}\n            {{ if $output.CacheURI -}}\n            (builtins.trace \"downloading {{ $pkg.Versioned }}\" (builtins.fetchClosure {\n              {{/*\n                HACK HACK HACK! fetchClosure only supports http(s) caches and not\n                s3 caches. Until we implement that, we put a fake store here.\n                Since we pre-build everything, fetchClosure will not actually\n                fetch anything and just use the local version. This may break\n                if user somehow removes the local store path.\n              */}}\n              fromStore = \"https://cache.nixos.org\";\n              fromPath = \"{{ $pkg.InputAddressedPathForOutput $output.Name }}\";\n              inputAddressed = true;\n            }))\n            {{- end }}\n            {{- end }}\n            {{- end }}\n            {{- range $_, $flakeInput := .FlakeInputs }}\n            {{- range .BuildInputsForSymlinkJoin }}\n            (pkgs.symlinkJoin {\n              name = \"{{.Name}}\";\n              paths = [\n                {{- range .Paths }}\n                (builtins.trace \"evaluating {{.}}\" {{.}})\n                {{- end }}\n              ];\n            })\n            {{- end }}\n            {{- range .BuildInputs }}\n            (builtins.trace \"evaluating {{.}}\" {{.}})\n            {{- end }}\n            {{- end }}\n          ];\n        };\n      };\n }\n"
  },
  {
    "path": "internal/shellgen/tmpl/glibc-patch.nix.tmpl",
    "content": "{\n  description = \"Patches packages to use a newer version of glibc\";\n\n  inputs = {\n    devbox.url = \"{{ .DevboxFlake }}\";\n    nixpkgs-glibc.url = \"{{ .NixpkgsGlibcFlakeRef }}\";\n\n    {{- range $name, $flakeref := .Inputs }}\n    {{ $name }}.url = \"{{ $flakeref }}\";\n    {{- end }}\n  };\n\n  outputs = args@{ self, devbox, nixpkgs-glibc {{- range $name, $_ := .Inputs -}}, {{ $name }} {{- end }} }:\n    let\n      # Initialize each nixpkgs input into a new attribute set with the\n      # schema \"pkgs.<input>.<system>.<package>\".\n      #\n      # Example: pkgs.nixpkgs-80c24e.x86_64-linux.python37\n      pkgs = builtins.mapAttrs (name: flake:\n        if builtins.hasAttr \"legacyPackages\" flake then\n          {\n            {{- range $system, $_ := .Outputs.Packages }}\n            {{ $system }} = (import flake {\n              system = \"{{ $system }}\";\n              config.allowUnfree = true;\n              config.allowInsecurePredicate = pkg: true;\n            });\n            {{- end }}\n          }\n        else null) args;\n\n      # selectDefaultOutputs takes a derivation and returns a list of its\n      # default outputs.\n      selectDefaultOutputs = drv: selectOutputs drv (drv.meta.outputsToInstall or [ drv.out ]);\n\n      # selectAllOutputs takes a derivation and returns all of its outputs (^*).\n      selectAllOutputs = drv: drv.all;\n\n      # selectOutputs takes a derivation and a list of output names, and returns\n      # those outputs.\n      #\n      # Example: selectOutputs nixpkgs#foo [ \"out\", \"lib\" ]\n      selectOutputs = drv: builtins.map (output: drv.${output});\n\n      patchDependencies = [\n        {{- range .Dependencies }}\n        ({{ . }})\n        {{- end }}\n      ];\n\n      patchGlibc = pkg: derivation rec {\n        # The package we're patching and any dependencies the patch needs.\n        inherit pkg patchDependencies;\n\n        # Keep the name the same as the package we're patching so that the\n        # length of the store path doesn't change. Otherwise patching binaries\n        # becomes trickier.\n        name = pkg.name;\n        system = pkg.system;\n\n        # buildDependencies is the package's build dependencies as a list of\n        # store paths. It includes transitive dependencies.\n        #\n        # Setting this environment variable provides a corpus of store paths\n        # that the `devbox patch --restore-refs` flag can use to restore\n        # references to Python build-time dependencies.\n        buildDependencies =\n          let\n            # mkNodes makes tree nodes for a list of derivation (package)\n            # outputs. A node is just the package with a \"key\" attribute added\n            # to it so it works with builtins.genericClosure.\n            mkNodes = builtins.map (drv: drv // { key = drv.outPath; });\n\n            # mkTree recursively traverses the buildInputs of the package we're\n            # patching. It returns a list of nodes, where each node represents\n            # a package output path in the dependency tree.\n            mkTree = builtins.genericClosure {\n              # Start with the package's buildInputs + the packages in its\n              # stdenv.\n              startSet = mkNodes (pkg.buildInputs ++ pkg.stdenv.initialPath);\n\n              # For each package, generate nodes for all of its outputs\n              # (node.all) and all of its buildInputs. Then visit those nodes.\n              operator = node: mkNodes (node.all or [ ] ++ node.buildInputs or [ ]);\n            };\n          in\n          builtins.map (drv: drv.outPath) mkTree;\n\n        # Programs needed by glibc-patch.bash.\n        inherit (nixpkgs-glibc.legacyPackages.\"${system}\") bash coreutils gnused patchelf ripgrep;\n\n        isLinux = (builtins.match \".*linux.*\" system) != null;\n        glibc = if isLinux then nixpkgs-glibc.legacyPackages.\"${system}\".glibc else null;\n        gcc = if isLinux then nixpkgs-glibc.legacyPackages.\"${system}\".stdenv.cc.cc.lib else null;\n\n        DEVBOX_DEBUG = 1;\n\tsrc = self;\n        builder = \"${devbox.packages.${system}.default}/bin/devbox\";\n        args = [ \"patch\" \"--restore-refs\" ] ++\n          (if glibc != null then [ \"--glibc\" \"${glibc}\" ] else [ ]) ++\n          (if gcc != null then [ \"--gcc\" \"${gcc}\" ] else [ ]) ++\n          [ pkg ];\n      };\n    in\n    {\n      {{- with .Outputs }}\n      packages = {\n        {{- range $system, $packages := .Packages }}\n        {{ $system }} = {\n          {{- range $name, $derivation := $packages }}\n          {{ $name }} = patchGlibc {{ $derivation }};\n          {{- end }}\n        };\n        {{- end }}\n      };\n\n      formatter = {\n        {{- range $system, $_ := .Packages }}\n        {{ $system }} = nixpkgs-glibc.legacyPackages.{{ $system }}.nixpkgs-fmt;\n        {{- end }}\n      };\n      {{- end }}\n    };\n}\n"
  },
  {
    "path": "internal/shellgen/tmpl/script-wrapper.tmpl",
    "content": "{{/*\n    This wraps user scripts in devbox.json. The idea is to only run the init\n    hooks once, even if the init hook calls devbox run again. This will also\n    protect against using devbox service in the init hook.\n\n    Scripts always use sh to run, so POSIX is OK. We don't (yet) support fish\n    scripts. (though users can run a fish script within their script)\n*/ -}}\n\nif [ -z \"${{ .SkipInitHookHash }}\" ]; then\n    . \"{{ .InitHookPath }}\"\nfi\n\n{{ .Body }}\n"
  },
  {
    "path": "internal/shellgen/tmpl/shell.nix.tmpl",
    "content": "let\n  pkgs = import\n    (fetchTarball {\n      url = \"https://github.com/nixos/nixpkgs/archive/b9c00c1d41ccd6385da243415299b39aa73357be.tar.gz\";\n    })\n    { };\nin\nwith pkgs;\nmkShell {\n  packages = [];\n}\n"
  },
  {
    "path": "internal/telemetry/segment.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage telemetry\n\nimport (\n\t\"cmp\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\tsegment \"github.com/segmentio/analytics-go\"\n\t\"go.jetify.com/devbox/nix\"\n\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/envir\"\n)\n\nvar segmentClient segment.Client\n\nfunc initSegmentClient() bool {\n\tif build.TelemetryKey == \"\" {\n\t\treturn false\n\t}\n\n\tvar err error\n\tsegmentClient, err = segment.NewWithConfig(build.TelemetryKey, segment.Config{\n\t\tLogger:  segment.StdLogger(log.New(io.Discard, \"\", 0)),\n\t\tVerbose: false,\n\t})\n\treturn err == nil\n}\n\nfunc newTrackMessage(name string, meta Metadata) *segment.Track {\n\tnixVersion := cmp.Or(nix.Version(), \"unknown\")\n\n\tdur := time.Since(procStartTime)\n\tif !meta.EventStart.IsZero() {\n\t\tdur = time.Since(meta.EventStart)\n\t}\n\tuid := userID()\n\ttrack := &segment.Track{\n\t\tMessageId: newEventID(),\n\t\tType:      \"track\",\n\t\t// Only set anonymous ID if user ID is not set. Otherwise segment will\n\t\t// drop the UserId.\n\t\tAnonymousId: lo.Ternary(uid == \"\", deviceID, \"\"),\n\t\tUserId:      uid,\n\t\tTimestamp:   time.Now(),\n\t\tEvent:       name,\n\t\tContext: &segment.Context{\n\t\t\tDevice: segment.DeviceInfo{\n\t\t\t\tId: deviceID,\n\t\t\t},\n\t\t\tApp: segment.AppInfo{\n\t\t\t\tName:    appName,\n\t\t\t\tVersion: build.Version,\n\t\t\t},\n\t\t\tOS: segment.OSInfo{\n\t\t\t\tName: build.OS(),\n\t\t\t},\n\t\t},\n\t\tProperties: segment.Properties{\n\t\t\t\"command\":      meta.Command,\n\t\t\t\"command_args\": meta.CommandFlags,\n\t\t\t\"duration\":     dur.Milliseconds(),\n\t\t\t\"nix_version\":  nixVersion,\n\t\t\t\"org_id\":       orgID(),\n\t\t\t\"packages\":     meta.Packages,\n\t\t\t\"shell\":        os.Getenv(envir.Shell),\n\t\t\t\"shell_access\": shellAccess(),\n\t\t},\n\t}\n\n\t// Property keys match the API events.\n\tinsertEnv := func(envKey, propKey string) {\n\t\tv, ok := os.LookupEnv(envKey)\n\t\tif ok {\n\t\t\ttrack.Properties[propKey] = v\n\t\t}\n\t}\n\tinsertEnv(\"_JETIFY_SANDBOX_ID\", \"devspace\")\n\tinsertEnv(\"_JETIFY_GH_REPO\", \"repo\")\n\tinsertEnv(\"_JETIFY_GIT_REF\", \"ref\")\n\tinsertEnv(\"_JETIFY_GIT_SUBDIR\", \"subdir\")\n\n\treturn track\n}\n\n// bufferSegmentMessage buffers a Segment message to disk so that Report can\n// upload it later.\nfunc bufferSegmentMessage(id string, msg segment.Message) {\n\tbufferEvent(filepath.Join(segmentBufferDir, id+\".json\"), msg)\n}\n\ntype shellAccessKind string\n\nconst (\n\tlocal   shellAccessKind = \"local\"\n\tssh     shellAccessKind = \"ssh\"\n\tbrowser shellAccessKind = \"browser\"\n)\n\nfunc shellAccess() shellAccessKind {\n\t// Check if running in devbox cloud\n\tif envir.IsDevboxCloud() {\n\t\t// Check if running via ssh tty (i.e. ssh shell)\n\t\tif os.Getenv(envir.SSHTTY) != \"\" {\n\t\t\treturn ssh\n\t\t}\n\t\treturn browser\n\t}\n\treturn local\n}\n"
  },
  {
    "path": "internal/telemetry/sentry.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage telemetry\n\nimport (\n\t\"path\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n\n\t\"github.com/getsentry/sentry-go\"\n\t\"github.com/pkg/errors\"\n\n\t\"go.jetify.com/devbox/internal/build\"\n)\n\nvar ExecutionID = newEventID()\n\nfunc initSentryClient(appName string) bool {\n\tif appName == \"\" {\n\t\tpanic(\"telemetry.Start: app name is empty\")\n\t}\n\tif build.SentryDSN == \"\" {\n\t\treturn false\n\t}\n\n\ttransport := sentry.NewHTTPTransport()\n\ttransport.Timeout = time.Second * 2\n\tenvironment := \"production\"\n\tif build.IsDev {\n\t\tenvironment = \"development\"\n\t}\n\terr := sentry.Init(sentry.ClientOptions{\n\t\tDsn:              build.SentryDSN,\n\t\tEnvironment:      environment,\n\t\tRelease:          appName + \"@\" + build.Version,\n\t\tTransport:        transport,\n\t\tTracesSampleRate: 1,\n\t\tBeforeSend: func(event *sentry.Event, _ *sentry.EventHint) *sentry.Event {\n\t\t\t// redact the hostname, which the SDK automatically adds\n\t\t\tevent.ServerName = \"\"\n\t\t\treturn event\n\t\t},\n\t})\n\treturn err == nil\n}\n\nfunc newSentryException(errToLog error) []sentry.Exception {\n\terrMsg := errToLog.Error()\n\tbinPkg := \"\"\n\tmodPath := \"\"\n\tif build, ok := debug.ReadBuildInfo(); ok {\n\t\tbinPkg = build.Path\n\t\tmodPath = build.Main.Path\n\t}\n\n\t// Unwrap in a loop to get the most recent stack trace. stFunc is set to a\n\t// function that can generate a stack trace for the most recent error. This\n\t// avoids computing the full stack trace for every error.\n\tvar stFunc func() []runtime.Frame\n\terrType := \"Generic Error\"\n\tfor {\n\t\tif t := exportedErrType(errToLog); t != \"\" {\n\t\t\terrType = t\n\t\t}\n\n\t\t//nolint:errorlint\n\t\tswitch stackErr := errToLog.(type) {\n\t\t// If the error implements the StackTrace method in the redact package, then\n\t\t// prefer that. The Sentry SDK gets some things wrong when guessing how\n\t\t// to extract the stack trace.\n\t\tcase interface{ StackTrace() []runtime.Frame }:\n\t\t\tstFunc = stackErr.StackTrace\n\t\t// Otherwise use the pkg/errors StackTracer interface.\n\t\tcase interface{ StackTrace() errors.StackTrace }:\n\t\t\t// Normalize the pkgs/errors.StackTrace type to a slice of runtime.Frame.\n\t\t\tstFunc = func() []runtime.Frame {\n\t\t\t\tpkgStack := stackErr.StackTrace()\n\t\t\t\tpc := make([]uintptr, len(pkgStack))\n\t\t\t\tfor i := range pkgStack {\n\t\t\t\t\tpc[i] = uintptr(pkgStack[i])\n\t\t\t\t}\n\t\t\t\tframeIter := runtime.CallersFrames(pc)\n\t\t\t\tframes := make([]runtime.Frame, 0, len(pc))\n\t\t\t\tfor {\n\t\t\t\t\tframe, more := frameIter.Next()\n\t\t\t\t\tframes = append(frames, frame)\n\t\t\t\t\tif !more {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn frames\n\t\t\t}\n\t\t}\n\t\tuw := errors.Unwrap(errToLog)\n\t\tif uw == nil {\n\t\t\tbreak\n\t\t}\n\t\terrToLog = uw\n\t}\n\tex := []sentry.Exception{{Type: errType, Value: errMsg}}\n\tif stFunc != nil {\n\t\tex[0].Stacktrace = newSentryStack(stFunc(), binPkg, modPath)\n\t}\n\treturn ex\n}\n\nfunc newSentryStack(frames []runtime.Frame, binPkg, modPath string) *sentry.Stacktrace {\n\tstack := &sentry.Stacktrace{\n\t\tFrames: make([]sentry.Frame, len(frames)),\n\t}\n\tfor i, frame := range frames {\n\t\tpkgName, funcName := splitPkgFunc(frame.Function)\n\n\t\t// The entrypoint has the full function name \"main.main\". Replace the\n\t\t// package name with its full package path to make it easier to find.\n\t\tif pkgName == \"main\" {\n\t\t\tpkgName = binPkg\n\t\t}\n\n\t\t// The file path will be absolute unless the binary was built with -trimpath\n\t\t// (which releases should be). Absolute paths make it more difficult for\n\t\t// Sentry to correctly group errors, but there's no way to infer a relative\n\t\t// path from an absolute path at runtime.\n\t\tvar absPath, relPath string\n\t\tif filepath.IsAbs(frame.File) {\n\t\t\tabsPath = frame.File\n\t\t} else {\n\t\t\trelPath = frame.File\n\t\t}\n\n\t\t// Reverse the frames - Sentry wants the most recent call first.\n\t\tstack.Frames[len(frames)-i-1] = sentry.Frame{\n\t\t\tFunction: funcName,\n\t\t\tModule:   pkgName,\n\t\t\tFilename: relPath,\n\t\t\tAbsPath:  absPath,\n\t\t\tLineno:   frame.Line,\n\t\t\tInApp:    strings.HasPrefix(frame.Function, modPath) || pkgName == binPkg,\n\t\t}\n\t}\n\treturn stack\n}\n\n// exportedErrType returns the underlying type name of err if it's exported.\n// Otherwise, it returns an empty string.\nfunc exportedErrType(err error) string {\n\tt := reflect.TypeOf(err)\n\tif t == nil {\n\t\treturn \"\"\n\t}\n\tif t.Kind() == reflect.Pointer {\n\t\tt = t.Elem()\n\t}\n\tname := t.Name()\n\tif r, _ := utf8.DecodeRuneInString(name); unicode.IsUpper(r) {\n\t\treturn t.String()\n\t}\n\treturn \"\"\n}\n\n// splitPkgFunc splits a fully-qualified function or method name into its\n// package path and base name components.\nfunc splitPkgFunc(name string) (pkgPath, funcName string) {\n\t// Using the following fully-qualified function name as an example:\n\t// go.jetify.com/devbox/internal/devbox.(*Devbox).RunScript\n\n\t// dir = go.jetify.com/devbox/internal/\n\t// base = devbox.(*Devbox).RunScript\n\tdir, base := path.Split(name)\n\n\t// pkgName = devbox\n\t// fn = (*Devbox).RunScript\n\tpkgName, fn, _ := strings.Cut(base, \".\")\n\n\t// pkgPath = go.jetify.com/devbox/internal/devbox\n\t// funcName = (*Devbox).RunScript\n\treturn dir + pkgName, fn\n}\n\n// bufferSentryEvent buffers a Sentry event to disk so that Report can upload it\n// later.\nfunc bufferSentryEvent(event *sentry.Event) {\n\tbufferEvent(filepath.Join(sentryBufferDir, string(event.EventID)+\".json\"), event)\n}\n"
  },
  {
    "path": "internal/telemetry/telemetry.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage telemetry\n\nimport (\n\t\"cmp\"\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/denisbrodbeck/machineid\"\n\t\"github.com/getsentry/sentry-go\"\n\t\"github.com/google/uuid\"\n\t\"github.com/pkg/errors\"\n\tsegment \"github.com/segmentio/analytics-go\"\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/devbox/providers/identity\"\n\t\"go.jetify.com/devbox/nix\"\n\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/redact\"\n\t\"go.jetify.com/devbox/internal/xdg\"\n)\n\nconst appName = \"devbox\"\n\ntype EventName int\n\nconst (\n\tEventCommandSuccess EventName = iota\n\tEventShellInteractive\n\tEventShellReady\n\tEventNixBuildSuccess\n\tEventNixBuildWithSubstitutersFailed\n)\n\nvar (\n\tdeviceID string\n\n\t// procStartTime records the start time of the current process.\n\tprocStartTime = time.Now()\n\tneedsFlush    atomic.Bool\n\tstarted       bool\n)\n\n// Start enables telemetry for the current program.\nfunc Start() {\n\tif started || envir.DoNotTrack() || build.SentryDSN == \"\" || build.TelemetryKey == \"\" {\n\t\treturn\n\t}\n\n\tconst deviceSalt = \"64ee464f-9450-4b14-8d9c-014c0012ac1a\"\n\tdeviceID, _ = machineid.ProtectedID(deviceSalt)\n\n\tstarted = true\n}\n\nfunc userID() string {\n\t// TODO, once we add access token parsing, use that instead of id token.\n\t// that will work with API_TOKEN as well.\n\tif tok, err := identity.Peek(); err == nil && tok.IDClaims() != nil {\n\t\treturn tok.IDClaims().Subject\n\t}\n\tif username := os.Getenv(envir.GitHubUsername); username != \"\" {\n\t\tconst uidSalt = \"d6134cd5-347d-4b7c-a2d0-295c0f677948\"\n\t\tconst githubPrefix = \"github:\"\n\n\t\t// userID is a v5 UUID which is basically a SHA hash of the username.\n\t\t// See https://www.uuidtools.com/uuid-versions-explained for a comparison of UUIDs.\n\t\treturn uuid.NewSHA1(uuid.MustParse(uidSalt), []byte(githubPrefix+username)).String()\n\t}\n\treturn \"\"\n}\n\nfunc orgID() string {\n\t// TODO, once we add access token parsing, use that instead of id token.\n\t// that will work with API_TOKEN as well.\n\tif tok, err := identity.Peek(); err == nil && tok.IDClaims() != nil {\n\t\treturn tok.IDClaims().OrgID\n\t}\n\treturn \"\"\n}\n\n// Stop stops gathering telemetry and flushes buffered events to disk.\nfunc Stop() {\n\tif !started || !needsFlush.Load() {\n\t\treturn\n\t}\n\n\t// Report errors in a separate process so we don't block exiting.\n\texe, err := os.Executable()\n\tif err == nil {\n\t\t_ = exec.Command(exe, \"upload-telemetry\").Start()\n\t}\n\tstarted = false\n}\n\nfunc Event(e EventName, meta Metadata) {\n\tif !started {\n\t\treturn\n\t}\n\n\tswitch e {\n\tcase EventCommandSuccess:\n\t\tbufferSegmentMessage(commandEvent(meta))\n\tcase EventShellInteractive:\n\t\tname := fmt.Sprintf(\"[%s] Shell Event: interactive\", appName)\n\t\tmsg := newTrackMessage(name, meta)\n\t\tbufferSegmentMessage(msg.MessageId, msg)\n\tcase EventShellReady:\n\t\tname := fmt.Sprintf(\"[%s] Shell Event: ready\", appName)\n\t\tmsg := newTrackMessage(name, meta)\n\t\tbufferSegmentMessage(msg.MessageId, msg)\n\tcase EventNixBuildSuccess:\n\t\tname := fmt.Sprintf(\"[%s] Nix Build Event: success\", appName)\n\t\tmsg := newTrackMessage(name, meta)\n\t\tbufferSegmentMessage(msg.MessageId, msg)\n\t}\n}\n\nfunc commandEvent(meta Metadata) (id string, msg *segment.Track) {\n\tname := fmt.Sprintf(\"[%s] Command: %s\", appName, meta.Command)\n\tmsg = newTrackMessage(name, meta)\n\treturn msg.MessageId, msg\n}\n\n// Error reports an error to the telemetry server.\nfunc Error(err error, meta Metadata) {\n\terrToLog := err // use errToLog to avoid shadowing err later. Use err to keep API clean.\n\n\tif !started || !usererr.ShouldLogError(errToLog) {\n\t\treturn\n\t}\n\n\tnixVersion := cmp.Or(nix.Version(), \"unknown\")\n\n\tevent := &sentry.Event{\n\t\tEventID:   sentry.EventID(ExecutionID),\n\t\tLevel:     sentry.LevelError,\n\t\tUser:      sentry.User{ID: deviceID},\n\t\tException: newSentryException(redact.Error(errToLog)),\n\t\tContexts: map[string]map[string]any{\n\t\t\t\"os\": {\n\t\t\t\t\"name\": build.OS(),\n\t\t\t},\n\t\t\t\"device\": {\n\t\t\t\t\"arch\": runtime.GOARCH,\n\t\t\t},\n\t\t\t\"runtime\": {\n\t\t\t\t\"name\":    \"Go\",\n\t\t\t\t\"version\": strings.TrimPrefix(runtime.Version(), \"go\"),\n\t\t\t},\n\t\t\t\"nix\": {\n\t\t\t\t\"version\": nixVersion,\n\t\t\t},\n\t\t},\n\t}\n\tif meta.Command != \"\" {\n\t\tevent.Tags = map[string]string{\"command\": meta.Command}\n\t}\n\tif sentryCtx := meta.cmdContext(); len(sentryCtx) > 0 {\n\t\tevent.Contexts[\"Command\"] = sentryCtx\n\t}\n\tif sentryCtx := meta.envContext(); len(sentryCtx) > 0 {\n\t\tevent.Contexts[\"Devbox Environment\"] = sentryCtx\n\t}\n\tif sentryCtx := meta.featureContext(); len(sentryCtx) > 0 {\n\t\tevent.Contexts[\"Feature Flags\"] = sentryCtx\n\t}\n\tif sentryCtx := meta.pkgContext(); len(sentryCtx) > 0 {\n\t\tevent.Contexts[\"Devbox Packages\"] = sentryCtx\n\t}\n\n\t// Prefer using the user ID instead of the device ID when it's\n\t// available.\n\tif uid := userID(); uid != \"\" {\n\t\tevent.User.ID = uid\n\t}\n\tbufferSentryEvent(event)\n\n\tmsgID, msg := commandEvent(meta)\n\tmsg.Properties[\"failed\"] = true\n\tmsg.Properties[\"sentry_event_id\"] = event.EventID\n\tbufferSegmentMessage(msgID, msg)\n}\n\ntype Metadata struct {\n\tCommand      string\n\tCommandFlags []string\n\tEventStart   time.Time\n\tFeatureFlags map[string]bool\n\n\tInShell   bool\n\tInCloud   bool\n\tInBrowser bool\n\n\tNixpkgsHash string\n\tPackages    []string\n\n\tCloudRegion string\n\tCloudCache  string\n}\n\nfunc (m *Metadata) cmdContext() map[string]any {\n\tsentryCtx := map[string]any{}\n\tif m.Command != \"\" {\n\t\tsentryCtx[\"Name\"] = m.Command\n\t}\n\tif len(m.CommandFlags) > 0 {\n\t\tsentryCtx[\"Flags\"] = m.CommandFlags\n\t}\n\treturn sentryCtx\n}\n\nfunc (m *Metadata) envContext() map[string]any {\n\tsentryCtx := map[string]any{\n\t\t\"In Shell\":   m.InShell,\n\t\t\"In Cloud\":   m.InCloud,\n\t\t\"In Browser\": m.InBrowser,\n\t}\n\tif m.CloudCache != \"\" {\n\t\tsentryCtx[\"Cloud Cache\"] = m.CloudCache\n\t}\n\tif m.CloudRegion != \"\" {\n\t\tsentryCtx[\"Cloud Region\"] = m.CloudRegion\n\t}\n\treturn sentryCtx\n}\n\nfunc (m *Metadata) featureContext() map[string]any {\n\tif len(m.FeatureFlags) == 0 {\n\t\treturn nil\n\t}\n\n\tsentryCtx := make(map[string]any, len(m.FeatureFlags))\n\tfor name, enabled := range m.FeatureFlags {\n\t\tsentryCtx[name] = enabled\n\t}\n\treturn sentryCtx\n}\n\nfunc (m *Metadata) pkgContext() map[string]any {\n\tif len(m.Packages) == 0 {\n\t\treturn nil\n\t}\n\n\t// Every package currently has the same commit hash as its version, but this\n\t// format will allow us to use individual package versions in the future.\n\tpkgVersion := \"nixpkgs\"\n\tif m.NixpkgsHash != \"\" {\n\t\tpkgVersion += \"/\" + m.NixpkgsHash\n\t}\n\tpkgVersion += \"#\"\n\tpkgContext := make(map[string]any, len(m.Packages))\n\tfor _, pkg := range m.Packages {\n\t\tpkgContext[pkg] = pkgVersion + pkg\n\t}\n\treturn pkgContext\n}\n\nvar (\n\tsentryBufferDir  = xdg.StateSubpath(filepath.FromSlash(\"devbox/sentry\"))\n\tsegmentBufferDir = xdg.StateSubpath(filepath.FromSlash(\"devbox/segment\"))\n)\n\nfunc Upload() {\n\twg := sync.WaitGroup{} //nolint:varnamelen\n\twg.Add(2)\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\tif !initSentryClient(appName) {\n\t\t\treturn\n\t\t}\n\n\t\tevents := restoreEvents[sentry.Event](sentryBufferDir)\n\t\tfor _, e := range events {\n\t\t\tsentry.CaptureEvent(&e)\n\t\t}\n\t\tsentry.Flush(3 * time.Second)\n\t}()\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\tif !initSegmentClient() {\n\t\t\treturn\n\t\t}\n\t\tevents := restoreEvents[segment.Track](segmentBufferDir)\n\t\tfor _, e := range events {\n\t\t\tsegmentClient.Enqueue(e) //nolint:errcheck\n\t\t}\n\t\tsegmentClient.Close()\n\t}()\n\twg.Wait()\n}\n\nfunc restoreEvents[E any](dir string) []E {\n\tdirEntries, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tvar events []E\n\tfor _, entry := range dirEntries {\n\t\tif !entry.Type().IsRegular() || filepath.Ext(entry.Name()) != \".json\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tpath := filepath.Join(dir, entry.Name())\n\t\tdata, err := os.ReadFile(path)\n\t\t// Always delete the file so we don't end up with an infinitely growing\n\t\t// backlog of errors.\n\t\t_ = os.Remove(path)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar event E\n\t\tif err := json.Unmarshal(data, &event); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tevents = append(events, event)\n\t}\n\treturn events\n}\n\nfunc bufferEvent(file string, event any) {\n\tdata, err := json.Marshal(event)\n\tif err != nil {\n\t\treturn\n\t}\n\n\terr = os.WriteFile(file, data, 0o600)\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\t// XDG specifies perms 0700.\n\t\tif err := os.MkdirAll(filepath.Dir(file), 0o700); err != nil {\n\t\t\treturn\n\t\t}\n\t\terr = os.WriteFile(file, data, 0o600)\n\t}\n\tif err == nil {\n\t\tneedsFlush.Store(true)\n\t}\n}\n\nfunc newEventID() string {\n\t// Generate event UUIDs the same way the Sentry SDK does:\n\t// https://github.com/getsentry/sentry-go/blob/d9ce5344e7e1819921ea4901dd31e47a200de7e0/util.go#L15\n\tid := make([]byte, 16)\n\t_, _ = rand.Read(id)\n\tid[6] &= 0x0F\n\tid[6] |= 0x40\n\tid[8] &= 0x3F\n\tid[8] |= 0x80\n\treturn hex.EncodeToString(id)\n}\n\nfunc ShellStart() time.Time {\n\treturn ParseShellStart(os.Getenv(envir.DevboxShellStartTime))\n}\n\nfunc FormatShellStart(t time.Time) string {\n\tif t.IsZero() {\n\t\treturn \"\"\n\t}\n\treturn strconv.FormatInt(t.Unix(), 10)\n}\n\nfunc ParseShellStart(s string) time.Time {\n\tif s == \"\" {\n\t\treturn time.Time{}\n\t}\n\tunix, err := strconv.ParseInt(s, 10, 64)\n\tif err != nil {\n\t\treturn time.Time{}\n\t}\n\treturn time.Unix(unix, 0)\n}\n"
  },
  {
    "path": "internal/telemetry/telemetry_test.go",
    "content": "package telemetry\n\nimport (\n\t\"errors\"\n\t\"testing\"\n)\n\n// TestErrorBasic does a very simple sanity check to ensure the error can be sent\n// to the sentry and segment buffers\nfunc TestErrorBasic(t *testing.T) {\n\tsegmentBufferDir = t.TempDir()\n\tsentryBufferDir = t.TempDir()\n\tstarted = true\n\n\tfakeErr := errors.New(\"fake error\")\n\tmeta := Metadata{}\n\n\tError(fakeErr, meta)\n}\n"
  },
  {
    "path": "internal/templates/template.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage templates\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/build\"\n)\n\nfunc InitFromName(w io.Writer, template, target string) error {\n\ttemplatePath, ok := templates[template]\n\tif !ok {\n\t\treturn usererr.New(\"unknown template name or format %q\", template)\n\t}\n\treturn InitFromRepo(w, \"https://github.com/jetify-com/devbox\", templatePath, target)\n}\n\nfunc InitFromRepo(w io.Writer, repo, subdir, target string) error {\n\tif err := createDirAndEnsureEmpty(target); err != nil {\n\t\treturn err\n\t}\n\tparsedRepoURL, err := ParseRepoURL(repo)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\ttmp, err := os.MkdirTemp(\"\", \"devbox-template\")\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tcmd := exec.Command(\n\t\t\"git\", \"clone\", parsedRepoURL,\n\t\t// Clone and checkout a specific ref\n\t\t\"-b\", lo.Ternary(build.IsDev, \"main\", build.Version),\n\t\t// Create shallow clone with depth of 1\n\t\t\"--depth\", \"1\",\n\t\ttmp,\n\t)\n\tfmt.Fprintf(w, \"%s\\n\", cmd)\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdout = os.Stdout\n\tif err = cmd.Run(); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tcmd = exec.Command(\n\t\t\"sh\", \"-c\",\n\t\tfmt.Sprintf(\"cp -r %s %s\", filepath.Join(tmp, subdir, \"*\"), target),\n\t)\n\tfmt.Fprintf(w, \"%s\\n\", cmd)\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdout = os.Stdout\n\treturn errors.WithStack(cmd.Run())\n}\n\nfunc List(w io.Writer, showAll bool) {\n\tfmt.Fprintf(w, \"Templates:\\n\\n\")\n\tkeysToShow := popularTemplates\n\tif showAll {\n\t\tkeysToShow = lo.Keys(templates)\n\t}\n\n\tslices.Sort(keysToShow)\n\tfor _, key := range keysToShow {\n\t\tfmt.Fprintf(w, \"* %-15s %s\\n\", key, templates[key])\n\t}\n}\n\nfunc createDirAndEnsureEmpty(dir string) error {\n\tentries, err := os.ReadDir(dir)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\tif err = os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t} else if err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tif len(entries) > 0 {\n\t\treturn usererr.New(\"directory %q is not empty\", dir)\n\t}\n\n\treturn nil\n}\n\nfunc ParseRepoURL(repo string) (string, error) {\n\tu, err := url.Parse(repo)\n\tif err != nil || u.Scheme == \"\" || u.Host == \"\" {\n\t\treturn \"\", usererr.New(\"Invalid URL format for --repo %s\", repo)\n\t}\n\t// this is to handle cases where user puts repo url with .git at the end\n\t// like: https://github.com/jetify-com/devbox.git\n\treturn strings.TrimSuffix(repo, \".git\"), nil\n}\n"
  },
  {
    "path": "internal/templates/templates.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage templates\n\nvar popularTemplates = []string{\n\t\"node-npm\",\n\t\"node-typescript\",\n\t\"node-yarn\",\n\t\"python-pip\",\n\t\"python-pipenv\",\n\t\"python-poetry\",\n\t\"php\",\n\t\"ruby\",\n\t\"rust\",\n\t\"go\",\n}\n\nvar templates = map[string]string{\n\t\"apache\":          \"examples/servers/apache/\",\n\t\"argo\":            \"examples/cloud_development/argo-workflows/\",\n\t\"bun\":             \"examples/development/bun/\",\n\t\"caddy\":           \"examples/servers/caddy/\",\n\t\"django\":          \"examples/stacks/django/\",\n\t\"dotnet\":          \"examples/development/csharp/hello-world/\",\n\t\"drupal\":          \"examples/stacks/drupal/\",\n\t\"elixir\":          \"examples/development/elixir/elixir_hello/\",\n\t\"fsharp\":          \"examples/development/fsharp/hello-world/\",\n\t\"go\":              \"examples/development/go/hello-world/\",\n\t\"gradio\":          \"examples/data_science/pytorch/gradio/\",\n\t\"haskell\":         \"examples/development/haskell/\",\n\t\"java-gradle\":     \"examples/development/java/gradle/hello-world/\",\n\t\"java-maven\":      \"examples/development/java/maven/hello-world/\",\n\t\"jekyll\":          \"examples/stacks/jekyll/\",\n\t\"jupyter\":         \"examples/data_science/jupyter/\",\n\t\"lapp-stack\":      \"examples/stacks/lapp-stack/\",\n\t\"laravel\":         \"examples/stacks/laravel/\",\n\t\"lepp-stack\":      \"examples/stacks/lepp-stack/\",\n\t\"llama\":           \"examples/data_science/llama/\",\n\t\"maelstrom\":       \"examples/cloud_development/maelstrom/\",\n\t\"minikube\":        \"examples/cloud_development/minikube/\",\n\t\"mariadb\":         \"examples/databases/mariadb/\",\n\t\"mysql\":           \"examples/databases/mysql/\",\n\t\"nginx\":           \"examples/servers/nginx/\",\n\t\"nim\":             \"examples/development/nim/spinnytest/\",\n\t\"node-npm\":        \"examples/development/nodejs/nodejs-npm/\",\n\t\"node-pnpm\":       \"examples/development/nodejs/nodejs-pnpm/\",\n\t\"node-typescript\": \"examples/development/nodejs/nodejs-typescript/\",\n\t\"node-yarn\":       \"examples/development/nodejs/nodejs-yarn/\",\n\t\"php\":             \"examples/development/php/latest/\",\n\t\"postgres\":        \"examples/databases/postgres/\",\n\t\"python-pip\":      \"examples/development/python/pip/\",\n\t\"python-pipenv\":   \"examples/development/python/pipenv/\",\n\t\"python-poetry\":   \"examples/development/python/poetry/poetry-demo/\",\n\t\"pytorch\":         \"examples/data_science/pytorch/basic-example/\",\n\t\"rails\":           \"examples/stacks/rails/\",\n\t\"redis\":           \"examples/databases/redis/\",\n\t\"ruby\":            \"examples/development/ruby/\",\n\t\"rust\":            \"examples/development/rust/rust-stable-hello-world/\",\n\t\"temporal\":        \"examples/cloud_development/temporal/\",\n\t\"tensorflow\":      \"examples/data_science/tensorflow/\",\n\t\"tutorial\":        \"examples/tutorial/\",\n\t\"zig\":             \"examples/development/zig/zig-hello-world/\",\n}\n"
  },
  {
    "path": "internal/templates/templates_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\npackage templates\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTemplatesExist(t *testing.T) {\n\tcurDir := \"\"\n\t// Try to find examples dir. After 10 hops, we give up.\n\tfor i := 0; i < 10; i++ {\n\t\tif _, err := os.Stat(curDir + \"examples\"); err == nil {\n\t\t\tbreak\n\t\t}\n\t\tcurDir += \"../\"\n\t}\n\tfor _, path := range templates {\n\t\t_, err := os.Stat(filepath.Join(curDir, path, \"devbox.json\"))\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tt.Errorf(\"Directory/devbox.json for %s does not exist\", path)\n\t\t}\n\t}\n}\n\nfunc TestParseRepoURL(t *testing.T) {\n\t// devbox create --repo=\"http:::/not.valid/a//a??a?b=&&c#hi\"\n\t_, err := ParseRepoURL(\"http:::/not.valid/a//a??a?b=&&c#hi\")\n\tassert.Error(t, err)\n\t_, err = ParseRepoURL(\"http//github.com\")\n\tassert.Error(t, err)\n\t_, err = ParseRepoURL(\"github.com\")\n\tassert.Error(t, err)\n\t_, err = ParseRepoURL(\"/foo/bar\")\n\tassert.Error(t, err)\n\t_, err = ParseRepoURL(\"http://\")\n\tassert.Error(t, err)\n\t_, err = ParseRepoURL(\"git@github.com:jetify-com/devbox.git\")\n\tassert.Error(t, err)\n\tu, err := ParseRepoURL(\"http://github.com\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"http://github.com\", u)\n}\n"
  },
  {
    "path": "internal/ux/messages.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage ux\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar (\n\tsuccess = color.New(color.FgHiGreen)\n\tinfo    = color.New(color.FgYellow)\n\twarning = color.New(color.FgHiYellow)\n\terror   = color.New(color.FgHiRed)\n)\n\nfunc Fsuccess(w io.Writer, a ...any) {\n\tsuccess.Fprint(w, \"Success: \")\n\tfmt.Fprint(w, a...)\n}\n\nfunc Fsuccessf(w io.Writer, format string, a ...any) {\n\tsuccess.Fprint(w, \"Success: \")\n\tfmt.Fprintf(w, format, a...)\n}\n\nfunc Finfo(w io.Writer, a ...any) {\n\tinfo.Fprint(w, \"Info: \")\n\tfmt.Fprint(w, a...)\n}\n\nfunc Finfof(w io.Writer, format string, a ...any) {\n\tinfo.Fprint(w, \"Info: \")\n\tfmt.Fprintf(w, format, a...)\n}\n\nfunc Fwarning(w io.Writer, a ...any) {\n\twarning.Fprint(w, \"Warning: \")\n\tfmt.Fprint(w, a...)\n}\n\nfunc Fwarningf(w io.Writer, format string, a ...any) {\n\twarning.Fprint(w, \"Warning: \")\n\tfmt.Fprintf(w, format, a...)\n}\n\nfunc Ferror(w io.Writer, a ...any) {\n\terror.Fprint(w, \"Error: \")\n\tfmt.Fprint(w, a...)\n}\n\nfunc Ferrorf(w io.Writer, format string, a ...any) {\n\terror.Fprint(w, \"Error: \")\n\tfmt.Fprintf(w, format, a...)\n}\n\n// Hidable messages allow the use of context to disable a message. Messages can be hidden\n// by their format string.\n\ntype ctxKey string\n\nfunc HideMessage(ctx context.Context, format string) context.Context {\n\treturn context.WithValue(ctx, ctxKey(format), true)\n}\n\nfunc FHidableWarning(ctx context.Context, w io.Writer, format string, a ...any) {\n\tif isHidden(ctx, format) {\n\t\treturn\n\t}\n\tFwarningf(w, format, a...)\n}\n\nfunc isHidden(ctx context.Context, format string) bool {\n\tisHidden, _ := ctx.Value(ctxKey(format)).(bool)\n\treturn isHidden\n}\n"
  },
  {
    "path": "internal/vercheck/vercheck.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage vercheck\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"golang.org/x/mod/semver\"\n\n\t\"go.jetify.com/devbox/internal/boxcli/usererr\"\n\t\"go.jetify.com/devbox/internal/build\"\n\t\"go.jetify.com/devbox/internal/cmdutil\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/ux\"\n\t\"go.jetify.com/devbox/internal/xdg\"\n)\n\n// Keep this in-sync with latest version in launch.sh.\n// If this version is newer than the version in launch.sh, we'll print a notice.\nconst expectedLauncherVersion = \"v0.2.2\"\n\n// envName determines whether the version check has already occurred.\n// We set this env-var so that this devbox command invoking other devbox commands\n// do not re-run the version check and print the notice again.\nconst envName = \"__DEVBOX_VERSION_CHECK\"\n\n// currentDevboxVersion is the version of the devbox CLI binary that is currently running.\n// We use this variable so that we can mock it in tests.\nvar currentDevboxVersion = build.Version\n\n// isDevBuild determines whether this CLI binary was built during development, or published\n// as a release.\n// We use this variable so we can mock it in tests.\nvar isDevBuild = build.IsDev\n\nvar commandSkipList = []string{\n\t\"devbox global shellenv\",\n\t\"devbox shellenv\",\n\t\"devbox version update\",\n\t\"devbox log\",\n}\n\n// CheckVersion checks the launcher and binary versions and prints a notice if\n// they are out of date.\n//\n// It will set the checkVersionEnvName to indicate that the version check was done.\n// Callers should call ClearEnvVar after their work is done.\nfunc CheckVersion(w io.Writer, commandPath string) {\n\tif isDevBuild {\n\t\treturn\n\t}\n\n\tif os.Getenv(envName) == \"1\" {\n\t\treturn\n\t}\n\n\tif envir.IsDevboxCloud() {\n\t\treturn\n\t}\n\n\thasSkipPrefix := lo.ContainsBy(\n\t\tcommandSkipList,\n\t\tfunc(skipPath string) bool { return strings.HasPrefix(commandPath, skipPath) },\n\t)\n\tif hasSkipPrefix {\n\t\treturn\n\t}\n\n\t// check launcher version\n\tlauncherNotice := launcherVersionNotice()\n\tif launcherNotice != \"\" {\n\t\tux.Finfo(w, launcherNotice)\n\n\t\t// fallthrough to alert the user about a new Devbox CLI binary being possibly available\n\t}\n\n\t// check devbox CLI version\n\tdevboxNotice := devboxVersionNotice()\n\tif devboxNotice != \"\" {\n\t\tux.Finfo(w, devboxNotice)\n\t}\n\n\tos.Setenv(envName, \"1\")\n}\n\n// SelfUpdate updates the devbox launcher and devbox CLI binary.\n// It ignores and deletes the version cache.\n//\n// The launcher is a wrapper bash script introduced to manage the auto-update process\n// for devbox. The production devbox application is actually this launcher script\n// that acts as \"devbox\" and delegates commands to the devbox CLI binary.\nfunc SelfUpdate(stdOut, stdErr io.Writer) error {\n\tif isNewLauncherAvailable() {\n\t\treturn selfUpdateLauncher(stdOut, stdErr)\n\t}\n\n\treturn selfUpdateDevbox(stdErr)\n}\n\nfunc selfUpdateLauncher(stdOut, stdErr io.Writer) error {\n\tinstallScript := \"\"\n\tif cmdutil.Exists(\"curl\") {\n\t\tinstallScript = \"curl -fsSL https://get.jetpack.io/devbox | bash\"\n\t} else if cmdutil.Exists(\"wget\") {\n\t\tinstallScript = \"wget -qO- https://get.jetpack.io/devbox | bash\"\n\t} else {\n\t\treturn usererr.New(\"curl or wget is required to update devbox. Please install either and try again.\")\n\t}\n\n\t// Delete current version file. This will trigger an update when invoking any devbox command;\n\t// in this case, inside triggerUpdate function.\n\tif err := removeCurrentVersionFile(); err != nil {\n\t\treturn err\n\t}\n\n\t// Fetch the new launcher. And installs the new devbox CLI binary.\n\tcmd := exec.Command(\"sh\", \"-c\", installScript)\n\tcmd.Stdout = stdOut\n\tcmd.Stderr = stdErr\n\tif err := cmd.Run(); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// Previously, we have already updated the binary. So, we call triggerUpdate\n\t// just to get the new version information.\n\tupdated, err := triggerUpdate(stdErr)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tprintSuccessMessage(stdErr, \"Launcher\", currentLauncherVersion(), updated.launcherVersion)\n\tprintSuccessMessage(stdErr, \"Devbox\", currentDevboxVersion, updated.devboxVersion)\n\n\treturn nil\n}\n\n// selfUpdateDevbox will update the devbox CLI binary to the latest version.\nfunc selfUpdateDevbox(stdErr io.Writer) error {\n\t// Delete current version file. This will trigger an update when the next devbox command is run;\n\t// in this case, inside triggerUpdate function.\n\tif err := removeCurrentVersionFile(); err != nil {\n\t\treturn err\n\t}\n\n\tupdated, err := triggerUpdate(stdErr)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\tprintSuccessMessage(stdErr, \"Devbox\", currentDevboxVersion, updated.devboxVersion)\n\n\treturn nil\n}\n\ntype updatedVersions struct {\n\tdevboxVersion   string\n\tlauncherVersion string\n}\n\n// triggerUpdate runs `devbox version -v` and triggers an update since a new\n// version is available. It parses the output to get the new launcher and\n// devbox versions.\nfunc triggerUpdate(stdErr io.Writer) (*updatedVersions, error) {\n\texePath := os.Getenv(envir.LauncherPath)\n\tif exePath == \"\" {\n\t\tux.Fwarningf(stdErr, \"expected LAUNCHER_PATH to be set. Defaulting to \\\"devbox\\\".\")\n\t\texePath = \"devbox\"\n\t}\n\n\t// TODO savil. Add a --json flag to devbox version and parse the output as JSON\n\tcmd := exec.Command(exePath, \"version\", \"-v\")\n\n\tbuf := new(bytes.Buffer)\n\tcmd.Stdout = io.MultiWriter(stdErr, buf)\n\tcmd.Stderr = stdErr\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\t// Parse the output to ascertain the new devbox and launcher versions\n\tupdated := &updatedVersions{}\n\tfor _, line := range strings.Split(buf.String(), \"\\n\") {\n\t\tif strings.HasPrefix(line, \"Version:\") {\n\t\t\tupdated.devboxVersion = strings.TrimSpace(strings.TrimPrefix(line, \"Version:\"))\n\t\t}\n\n\t\tif strings.HasPrefix(line, \"Launcher:\") {\n\t\t\tupdated.launcherVersion = strings.TrimSpace(strings.TrimPrefix(line, \"Launcher:\"))\n\t\t}\n\t}\n\treturn updated, nil\n}\n\nfunc printSuccessMessage(w io.Writer, toolName, oldVersion, newVersion string) {\n\tvar msg string\n\tif SemverCompare(oldVersion, newVersion) == 0 {\n\t\tmsg = fmt.Sprintf(\"already at %s version %s\", toolName, newVersion)\n\t} else {\n\t\tmsg = fmt.Sprintf(\"updated to %s version %s\", toolName, newVersion)\n\t}\n\n\t// Prints a <green>Success:</green> message to the writer.\n\t// Move to ux.Success. Not doing so to minimize merge-conflicts.\n\tfmt.Fprintf(w, \"%s%s\\n\", color.New(color.FgGreen).Sprint(\"Success: \"), msg)\n}\n\nfunc launcherVersionNotice() string {\n\tif !isNewLauncherAvailable() {\n\t\treturn \"\"\n\t}\n\n\treturn fmt.Sprintf(\n\t\t\"New launcher available: %s -> %s. Please run `devbox version update`.\\n\",\n\t\tcurrentLauncherVersion(),\n\t\texpectedLauncherVersion,\n\t)\n}\n\nfunc devboxVersionNotice() string {\n\tif !isNewDevboxAvailable() {\n\t\treturn \"\"\n\t}\n\n\treturn fmt.Sprintf(\n\t\t\"New devbox available: %s -> %s. Please run `devbox version update`.\\n\",\n\t\tcurrentDevboxVersion,\n\t\tlatestVersion(),\n\t)\n}\n\n// isNewLauncherAvailable returns true if a new launcher version is available.\nfunc isNewLauncherAvailable() bool {\n\tlauncherVersion := currentLauncherVersion()\n\tif launcherVersion == \"\" {\n\t\treturn false\n\t}\n\treturn SemverCompare(launcherVersion, expectedLauncherVersion) < 0\n}\n\n// isNewDevboxAvailable returns true if a new devbox CLI binary version is available.\nfunc isNewDevboxAvailable() bool {\n\tlatest := latestVersion()\n\tif latest == \"\" {\n\t\treturn false\n\t}\n\treturn SemverCompare(currentDevboxVersion, latest) < 0\n}\n\n// currentLauncherVersion returns launcher's version if it is\n// available, or empty string if it is not.\nfunc currentLauncherVersion() string {\n\tlauncherVersion := os.Getenv(envir.LauncherVersion)\n\tif launcherVersion == \"\" {\n\t\treturn \"\"\n\t}\n\treturn \"v\" + launcherVersion\n}\n\nfunc removeCurrentVersionFile() error {\n\t// currentVersionFilePath is the path to the file that contains the cached\n\t// version. The launcher checks this file to see if a new version is available.\n\t// If the version is newer, then the launcher updates.\n\t//\n\t// Note: keep this in sync with launch.sh code\n\tcurrentVersionFilePath := filepath.Join(xdg.CacheSubpath(\"devbox\"), \"current-version\")\n\n\tif err := os.Remove(currentVersionFilePath); err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\treturn usererr.WithLoggedUserMessage(\n\t\t\terr,\n\t\t\t\"Failed to delete version-cache at %s. Please manually delete it and try again.\",\n\t\t\tcurrentVersionFilePath,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc SemverCompare(ver1, ver2 string) int {\n\tif !strings.HasPrefix(ver1, \"v\") {\n\t\tver1 = \"v\" + ver1\n\t}\n\tif !strings.HasPrefix(ver2, \"v\") {\n\t\tver2 = \"v\" + ver2\n\t}\n\treturn semver.Compare(ver1, ver2)\n}\n\n// latestVersion returns the latest version available for the binary.\nfunc latestVersion() string {\n\tversion := os.Getenv(envir.DevboxLatestVersion)\n\tif version == \"\" {\n\t\treturn \"\"\n\t}\n\treturn \"v\" + version\n}\n"
  },
  {
    "path": "internal/vercheck/vercheck_test.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage vercheck\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"go.jetify.com/devbox/internal/envir\"\n)\n\nfunc TestCheckVersion(t *testing.T) {\n\tisDevBuild = false\n\n\tt.Run(\"skip_if_devbox_cloud\", func(t *testing.T) {\n\t\tdefer os.Unsetenv(envName)\n\t\t// if devbox cloud\n\t\tt.Setenv(envir.DevboxRegion, \"true\")\n\t\tbuf := new(bytes.Buffer)\n\t\tCheckVersion(buf, \"devbox shell\")\n\t\tif buf.String() != \"\" {\n\t\t\tt.Errorf(\"expected empty string, got %q\", buf.String())\n\t\t}\n\t\tt.Setenv(envir.DevboxRegion, \"\")\n\t})\n\n\t// no launcher version or latest-version env var\n\tt.Run(\"skip_if_no_launcher_version_or_latest_version\", func(t *testing.T) {\n\t\tdefer os.Unsetenv(envName)\n\t\tt.Setenv(envir.LauncherVersion, \"\")\n\t\tt.Setenv(envir.DevboxLatestVersion, \"\")\n\t\tbuf := new(bytes.Buffer)\n\t\tCheckVersion(buf, \"devbox shell\")\n\t\tif buf.String() != \"\" {\n\t\t\tt.Errorf(\"expected empty string, got %q\", buf.String())\n\t\t}\n\t})\n\n\tt.Run(\"print_if_launcher_version_outdated\", func(t *testing.T) {\n\t\tdefer os.Unsetenv(envName)\n\t\t// set older launcher version\n\t\tt.Setenv(envir.LauncherVersion, \"v0.1.0\")\n\n\t\tbuf := new(bytes.Buffer)\n\t\tCheckVersion(buf, \"devbox shell\")\n\t\tif !strings.Contains(buf.String(), \"New launcher available\") {\n\t\t\tt.Errorf(\"expected notice about new launcher version, got %q\", buf.String())\n\t\t}\n\t})\n\n\tt.Run(\"print_if_binary_version_outdated\", func(t *testing.T) {\n\t\tdefer os.Unsetenv(envName)\n\t\t// set the launcher version so that it is not outdated\n\t\tt.Setenv(envir.LauncherVersion, strings.TrimPrefix(expectedLauncherVersion, \"v\"))\n\n\t\t// set the latest version to be greater the current binary version\n\t\tt.Setenv(envir.DevboxLatestVersion, \"0.4.9\")\n\n\t\t// mock the existing binary version\n\t\tcurrentDevboxVersion = \"v0.4.8\"\n\n\t\tbuf := new(bytes.Buffer)\n\t\tCheckVersion(buf, \"devbox shell\")\n\t\tif !strings.Contains(buf.String(), \"New devbox available\") {\n\t\t\tt.Errorf(\"expected notice about new devbox version, got %q\", buf.String())\n\t\t}\n\t})\n\n\tt.Run(\"skip_if_all_versions_up_to_date\", func(t *testing.T) {\n\t\tdefer os.Unsetenv(envName)\n\n\t\t// set the launcher version so that it is not outdated\n\t\tt.Setenv(envir.LauncherVersion, strings.TrimPrefix(expectedLauncherVersion, \"v\"))\n\n\t\t// mock the existing binary version\n\t\tcurrentDevboxVersion = \"v0.4.8\"\n\n\t\t// set the latest version to the same as the current binary version\n\t\tt.Setenv(envir.DevboxLatestVersion, \"0.4.8\")\n\n\t\tbuf := new(bytes.Buffer)\n\t\tCheckVersion(buf, \"devbox shell\")\n\t\tif buf.String() != \"\" {\n\t\t\tt.Errorf(\"expected empty string, got %q\", buf.String())\n\t\t}\n\t})\n\n\tt.Run(\"skip_if_dev_build\", func(t *testing.T) {\n\t\tdefer os.Unsetenv(envName)\n\t\tisDevBuild = true\n\t\tdefer func() { isDevBuild = false }()\n\n\t\t// set older launcher version\n\t\tt.Setenv(envir.LauncherVersion, \"v0.1.0\")\n\n\t\tbuf := new(bytes.Buffer)\n\t\tCheckVersion(buf, \"devbox shell\")\n\t\tif buf.String() != \"\" {\n\t\t\tt.Errorf(\"expected empty string, got %q\", buf.String())\n\t\t}\n\t})\n\n\tt.Run(\"skip_if_command_path_skipped\", func(t *testing.T) {\n\t\tdefer os.Unsetenv(envName)\n\n\t\tfor _, cmdPath := range commandSkipList {\n\t\t\tcmdPathUnderscored := strings.ReplaceAll(cmdPath, \" \", \"_\")\n\t\t\tt.Run(\"skip_if_cmd_path_is_\"+cmdPathUnderscored, func(t *testing.T) {\n\t\t\t\t// set older launcher version\n\t\t\t\tt.Setenv(envir.LauncherVersion, \"v0.1.0\")\n\n\t\t\t\tbuf := new(bytes.Buffer)\n\t\t\t\tCheckVersion(buf, cmdPath)\n\t\t\t\tif buf.String() != \"\" {\n\t\t\t\t\tt.Errorf(\"expected empty string, got %q\", buf.String())\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/xdg/xdg.go",
    "content": "// Copyright 2024 Jetify Inc. and contributors. All rights reserved.\n// Use of this source code is governed by the license in the LICENSE file.\n\npackage xdg\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"go.jetify.com/devbox/internal/envir\"\n)\n\nfunc DataSubpath(subpath string) string {\n\treturn filepath.Join(dataDir(), subpath)\n}\n\nfunc ConfigSubpath(subpath string) string {\n\treturn filepath.Join(configDir(), subpath)\n}\n\nfunc CacheSubpath(subpath string) string {\n\treturn filepath.Join(cacheDir(), subpath)\n}\n\nfunc StateSubpath(subpath string) string {\n\treturn filepath.Join(stateDir(), subpath)\n}\n\nfunc dataDir() string   { return resolveDir(envir.XDGDataHome, \".local/share\") }\nfunc configDir() string { return resolveDir(envir.XDGConfigHome, \".config\") }\nfunc cacheDir() string  { return resolveDir(envir.XDGCacheHome, \".cache\") }\nfunc stateDir() string  { return resolveDir(envir.XDGStateHome, \".local/state\") }\n\nfunc resolveDir(envvar, defaultPath string) string {\n\tdir := os.Getenv(envvar)\n\tif dir != \"\" {\n\t\treturn dir\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\thome = \"~\"\n\t}\n\n\treturn filepath.Join(home, defaultPath)\n}\n"
  },
  {
    "path": "nix/command.go",
    "content": "package nix\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n)\n\n// Cmd is an external command that invokes a [*Nix] executable. It provides\n// improved error messages, graceful cancellation, and debug logging via\n// [log/slog]. Although it's possible to initialize a Cmd directly, calling the\n// [Command] function or [Nix.Command] method is more typical.\n//\n// Most methods and fields correspond to their [exec.Cmd] equivalent. See its\n// documentation for more details.\ntype Cmd struct {\n\t// Path is the absolute path to the nix executable. It is the only\n\t// mandatory field and must not be empty.\n\tPath string\n\n\t// Args are the command line arguments, including the command name in\n\t// Args[0]. Run formats each argument using [fmt.Sprint] before passing\n\t// them to Nix.\n\tArgs Args\n\n\tEnv    []string\n\tStdin  io.Reader\n\tStdout io.Writer\n\tStderr io.Writer\n\n\t// Logger emits debug logs when the command starts and exits. If nil, it\n\t// defaults to [slog.Default].\n\tLogger *slog.Logger\n\n\texecCmd *exec.Cmd\n\terr     error\n\tdur     time.Duration\n}\n\n// Command creates an arbitrary Nix command that uses the Path, ExtraArgs,\n// Logger and other defaults from n.\nfunc (n *Nix) Command(args ...any) *Cmd {\n\tcmd := &Cmd{\n\t\tArgs:   make(Args, 1, 1+len(n.ExtraArgs)+len(args)),\n\t\tLogger: n.logger(),\n\t}\n\tcmd.Path, cmd.err = n.resolvePath()\n\n\tif n.Path == \"\" {\n\t\tcmd.Args[0] = \"nix\" // resolved from $PATH\n\t} else {\n\t\tcmd.Args[0] = n.Path // explicitly set\n\t}\n\tcmd.Args = append(cmd.Args, n.ExtraArgs...)\n\tcmd.Args = append(cmd.Args, args...)\n\treturn cmd\n}\n\nfunc (c *Cmd) CombinedOutput(ctx context.Context) ([]byte, error) {\n\tdefer c.logRunFunc(ctx)()\n\n\tstart := time.Now()\n\tout, err := c.initExecCommand(ctx).CombinedOutput()\n\tc.dur = time.Since(start)\n\n\tc.err = c.error(ctx, err)\n\treturn out, c.err\n}\n\nfunc (c *Cmd) Output(ctx context.Context) ([]byte, error) {\n\tdefer c.logRunFunc(ctx)()\n\n\tstart := time.Now()\n\tout, err := c.initExecCommand(ctx).Output()\n\tc.dur = time.Since(start)\n\n\tc.err = c.error(ctx, err)\n\treturn out, c.err\n}\n\nfunc (c *Cmd) Run(ctx context.Context) error {\n\tdefer c.logRunFunc(ctx)()\n\n\tstart := time.Now()\n\terr := c.initExecCommand(ctx).Run()\n\tc.dur = time.Since(start)\n\n\tc.err = c.error(ctx, err)\n\treturn c.err\n}\n\nfunc (c *Cmd) LogValue() slog.Value {\n\tattrs := []slog.Attr{\n\t\tslog.Any(\"args\", c.Args),\n\t}\n\tif c.execCmd == nil {\n\t\treturn slog.GroupValue(attrs...)\n\t}\n\tattrs = append(attrs, slog.String(\"path\", c.execCmd.Path))\n\n\tvar exitErr *exec.ExitError\n\tif errors.As(c.err, &exitErr) {\n\t\tstderr := c.stderrExcerpt(exitErr.Stderr)\n\t\tif len(stderr) != 0 {\n\t\t\tattrs = append(attrs, slog.String(\"stderr\", stderr))\n\t\t}\n\t}\n\tif proc := c.execCmd.Process; proc != nil {\n\t\tattrs = append(attrs, slog.Int(\"pid\", proc.Pid))\n\t}\n\tif procState := c.execCmd.ProcessState; procState != nil {\n\t\tif procState.Exited() {\n\t\t\tattrs = append(attrs, slog.Int(\"code\", procState.ExitCode()))\n\t\t}\n\t\tif status, ok := procState.Sys().(syscall.WaitStatus); ok && status.Signaled() {\n\t\t\tif status.Signaled() {\n\t\t\t\tattrs = append(attrs, slog.String(\"signal\", status.Signal().String()))\n\t\t\t}\n\t\t}\n\t}\n\tif c.dur != 0 {\n\t\tattrs = append(attrs, slog.Duration(\"dur\", c.dur))\n\t}\n\treturn slog.GroupValue(attrs...)\n}\n\n// String returns c as a shell-quoted string.\nfunc (c *Cmd) String() string {\n\treturn c.Args.String()\n}\n\nfunc (c *Cmd) initExecCommand(ctx context.Context) *exec.Cmd {\n\tif c.execCmd != nil {\n\t\treturn c.execCmd\n\t}\n\n\tc.execCmd = exec.CommandContext(ctx, c.Path)\n\tc.execCmd.Path = c.Path\n\tc.execCmd.Args = c.Args.StringSlice()\n\tc.execCmd.Env = c.Env\n\tc.execCmd.Stdin = c.Stdin\n\tc.execCmd.Stdout = c.Stdout\n\tc.execCmd.Stderr = c.Stderr\n\n\tc.execCmd.Cancel = func() error {\n\t\t// Try to let Nix exit gracefully by sending an interrupt\n\t\t// instead of the default behavior of killing it.\n\t\tc.logger().DebugContext(ctx, \"sending interrupt to nix process\", slog.Group(\"cmd\",\n\t\t\t\"args\", c.Args,\n\t\t\t\"path\", c.execCmd.Path,\n\t\t\t\"pid\", c.execCmd.Process.Pid,\n\t\t))\n\t\terr := c.execCmd.Process.Signal(os.Interrupt)\n\t\tif errors.Is(err, os.ErrProcessDone) {\n\t\t\t// Nix already exited; execCmd.Wait will use the exit\n\t\t\t// code.\n\t\t\treturn err\n\t\t}\n\t\tif err != nil {\n\t\t\t// We failed to send SIGINT, so kill the process\n\t\t\t// instead.\n\t\t\t//\n\t\t\t// - If Nix already exited, Kill will return\n\t\t\t//   os.ErrProcessDone and execCmd.Wait will use\n\t\t\t//   the exit code.\n\t\t\t// - Otherwise, execCmd.Wait will always return an\n\t\t\t//   error.\n\t\t\tc.logger().DebugContext(ctx, \"error interrupting nix process, attempting to kill\",\n\t\t\t\t\"err\", err, slog.Group(\"cmd\",\n\t\t\t\t\t\"args\", c.Args,\n\t\t\t\t\t\"path\", c.execCmd.Path,\n\t\t\t\t\t\"pid\", c.execCmd.Process.Pid,\n\t\t\t\t))\n\t\t\treturn c.execCmd.Process.Kill()\n\t\t}\n\n\t\t// We sent the SIGINT successfully. It's still possible for Nix\n\t\t// to exit successfully, so return os.ErrProcessDone so that\n\t\t// execCmd.Wait uses the exit code instead of ctx.Err.\n\t\treturn os.ErrProcessDone\n\t}\n\t// Kill Nix if it doesn't exit within 15 seconds of Devbox sending an\n\t// interrupt.\n\tc.execCmd.WaitDelay = 15 * time.Second\n\treturn c.execCmd\n}\n\nfunc (c *Cmd) error(ctx context.Context, err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tcmdErr := &cmdError{err: err}\n\tif errors.Is(err, exec.ErrNotFound) {\n\t\tcmdErr.msg = fmt.Sprintf(\"nix: %s not found in $PATH\", c.Args[0])\n\t}\n\n\tswitch {\n\tcase errors.Is(ctx.Err(), context.Canceled):\n\t\tcmdErr.msg = \"nix: command canceled\"\n\tcase errors.Is(ctx.Err(), context.DeadlineExceeded):\n\t\tcmdErr.msg = \"nix: command timed out\"\n\tdefault:\n\t\tcmdErr.msg = \"nix: command error\"\n\t}\n\tcmdErr.msg += \": \" + c.String()\n\n\tvar exitErr *exec.ExitError\n\tif errors.As(err, &exitErr) {\n\t\tif stderr := c.stderrExcerpt(exitErr.Stderr); len(stderr) != 0 {\n\t\t\tcmdErr.msg += \": \" + stderr\n\t\t}\n\t\tif exitErr.Exited() {\n\t\t\tcmdErr.msg += fmt.Sprintf(\": exit code %d\", exitErr.ExitCode())\n\t\t\treturn cmdErr\n\t\t}\n\t\tif stat, ok := exitErr.Sys().(syscall.WaitStatus); ok && stat.Signaled() {\n\t\t\tcmdErr.msg += fmt.Sprintf(\": exit due to signal %d (%[1]s)\", stat.Signal())\n\t\t\treturn cmdErr\n\t\t}\n\t}\n\n\tif !errors.Is(err, ctx.Err()) {\n\t\tcmdErr.msg += \": \" + err.Error()\n\t}\n\treturn cmdErr\n}\n\nfunc (*Cmd) stderrExcerpt(stderr []byte) string {\n\tstderr = bytes.TrimSpace(stderr)\n\tif len(stderr) == 0 {\n\t\treturn \"\"\n\t}\n\n\tlines := bytes.Split(stderr, []byte(\"\\n\"))\n\tslices.Reverse(lines)\n\tfor _, line := range lines {\n\t\tline = bytes.TrimSpace(line)\n\t\tafter, found := bytes.CutPrefix(line, []byte(\"error: \"))\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\t\tafter = bytes.TrimSpace(after)\n\t\tif len(after) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tstderr = after\n\t\tbreak\n\n\t}\n\n\texcerpt := string(stderr)\n\tif !strconv.CanBackquote(excerpt) {\n\t\tquoted := strconv.Quote(excerpt)\n\t\texcerpt = quoted[1 : len(quoted)-1]\n\t}\n\treturn excerpt\n}\n\nfunc (c *Cmd) logger() *slog.Logger {\n\tif c.Logger == nil {\n\t\treturn slog.Default()\n\t}\n\treturn c.Logger\n}\n\n// logRunFunc logs the start and exit of c.execCmd. It adjusts the source\n// attribute of the log record to point to the caller of c.CombinedOutput,\n// c.Output, or c.Run. This assumes a specific stack depth, so do not call\n// logRunFunc from other methods or functions.\nfunc (c *Cmd) logRunFunc(ctx context.Context) func() {\n\tlogger := c.logger()\n\tif !logger.Enabled(ctx, slog.LevelDebug) {\n\t\treturn func() {}\n\t}\n\n\tvar pcs [1]uintptr\n\truntime.Callers(3, pcs[:]) // skip Callers, logRunFunc, CombinedOutput/Output/Run\n\tr := slog.NewRecord(time.Now(), slog.LevelDebug, \"nix command starting\", pcs[0])\n\tr.Add(\"cmd\", c)\n\t_ = logger.Handler().Handle(ctx, r)\n\n\treturn func() {\n\t\tr := slog.NewRecord(time.Now(), slog.LevelDebug, \"nix command exited\", pcs[0])\n\t\tr.Add(\"cmd\", c)\n\t\t_ = logger.Handler().Handle(ctx, r)\n\t}\n}\n\n// Args is a slice of [Cmd] arguments.\ntype Args []any\n\n// StringSlice formats each argument using [fmt.Sprint].\nfunc (a Args) StringSlice() []string {\n\ts := make([]string, len(a))\n\tfor i := range a {\n\t\ts[i] = fmt.Sprint(a[i])\n\t}\n\treturn s\n}\n\n// String returns the arguments as a shell command, quoting arguments with\n// spaces.\nfunc (a Args) String() string {\n\tif len(a) == 0 {\n\t\treturn \"\"\n\t}\n\n\tsb := &strings.Builder{}\n\ta.writeQuoted(sb, fmt.Sprint(a[0]))\n\tif len(a) == 1 {\n\t\treturn sb.String()\n\t}\n\n\tfor _, arg := range a[1:] {\n\t\tsb.WriteByte(' ')\n\t\ta.writeQuoted(sb, fmt.Sprint(arg))\n\t}\n\treturn sb.String()\n}\n\nfunc (Args) writeQuoted(dst *strings.Builder, str string) {\n\tneedsQuote := strings.ContainsAny(str, \";\\\"'()$|&><` \\t\\r\\n\\\\#{~*?[=\")\n\tif !needsQuote {\n\t\tdst.WriteString(str)\n\t\treturn\n\t}\n\n\tcanSingleQuote := !strings.Contains(str, \"'\")\n\tif canSingleQuote {\n\t\tdst.WriteByte('\\'')\n\t\tdst.WriteString(str)\n\t\tdst.WriteByte('\\'')\n\t\treturn\n\t}\n\n\tdst.WriteByte('\"')\n\tfor _, r := range str {\n\t\tswitch r {\n\t\t// Special characters inside double quotes:\n\t\t// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03\n\t\tcase '$', '`', '\"', '\\\\':\n\t\t\tdst.WriteRune('\\\\')\n\t\t}\n\t\tdst.WriteRune(r)\n\t}\n\tdst.WriteByte('\"')\n}\n\ntype cmdError struct {\n\tmsg string\n\terr error\n}\n\nfunc (c *cmdError) Redact() string {\n\treturn c.Error()\n}\n\nfunc (c *cmdError) Error() string {\n\treturn c.msg\n}\n\nfunc (c *cmdError) Unwrap() error {\n\treturn c.err\n}\n"
  },
  {
    "path": "nix/flake/flakeref.go",
    "content": "// Package flake parses and formats Nix flake references.\npackage flake\n\nimport (\n\t\"cmp\"\n\t\"net/url\"\n\t\"path\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/internal/redact\"\n)\n\n// Flake reference types supported by this package.\nconst (\n\tTypeIndirect = \"indirect\"\n\tTypePath     = \"path\"\n\tTypeFile     = \"file\"\n\tTypeGit      = \"git\"\n\tTypeGitHub   = \"github\"\n\tTypeTarball  = \"tarball\"\n)\n\n// Ref is a parsed Nix flake reference. A flake reference is a subset of the\n// Nix CLI \"installable\" syntax. An [Installable] may specify an attribute path\n// and derivation outputs with a flake reference using the '#' and '^'\n// characters. For example, the string \"nixpkgs\" and \"./flake\" are valid flake\n// references, but \"nixpkgs#hello\" and \"./flake#app^bin,dev\" are not.\n//\n// The JSON encoding of Ref corresponds to the exploded attribute set form of\n// the flake reference in Nix. See the [Nix manual] for details on flake\n// references.\n//\n// [Nix manual]: https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-flake\ntype Ref struct {\n\t// Type is the type of flake reference. Some valid types are \"indirect\",\n\t// \"path\", \"file\", \"git\", \"tarball\", and \"github\".\n\tType string `json:\"type,omitempty\"`\n\n\t// ID is the flake's identifier when Type is \"indirect\". A common\n\t// example is nixpkgs.\n\tID string `json:\"id,omitempty\"`\n\n\t// Path is the path to the flake directory when Type is \"path\".\n\tPath string `json:\"path,omitempty\"`\n\n\t// Owner and repo are the flake repository owner and name when Type is\n\t// \"github\".\n\tOwner string `json:\"owner,omitempty\"`\n\tRepo  string `json:\"repo,omitempty\"`\n\n\t// Rev and ref are the git revision (commit hash) and ref\n\t// (branch or tag) when Type is \"github\" or \"git\".\n\tRev string `json:\"rev,omitempty\"`\n\tRef string `json:\"ref,omitempty\"`\n\n\t// Dir is non-empty when the directory containing the flake.nix file is\n\t// not at the flake root. It corresponds to the optional \"dir\" query\n\t// parameter when Type is \"github\", \"git\", \"tarball\", or \"file\".\n\tDir string `json:\"dir,omitempty\"`\n\n\t// Host overrides the default VCS host when Type is \"github\", such as\n\t// when referring to a GitHub Enterprise instance. It corresponds to the\n\t// optional \"host\" query parameter when Type is \"github\".\n\tHost string `json:\"host,omitempty\"`\n\n\t// URL is the URL pointing to the flake when type is \"tarball\", \"file\",\n\t// or \"git\". Note that the URL is not the same as the raw unparsed\n\t// flake ref.\n\tURL string `json:\"url,omitempty\"`\n\n\t// NARHash is the SRI hash of the flake's source. Specify a NAR hash to\n\t// lock flakes that don't otherwise have a revision (such as \"path\" or\n\t// \"tarball\" flakes).\n\tNARHash string `json:\"narHash,omitempty\"`\n\n\t// LastModified is the last modification time of the flake.\n\tLastModified int64 `json:\"lastModified,omitempty\"`\n}\n\n// ParseRef parses a raw flake reference. Nix supports a variety of flake ref\n// formats, and isn't entirely consistent about how it parses them. ParseRef\n// attempts to mimic how Nix parses flake refs on the command line. The raw ref\n// can be one of the following:\n//\n//   - Indirect reference such as \"nixpkgs\" or \"nixpkgs/unstable\".\n//   - Path-like reference such as \"./flake\" or \"/path/to/flake\". They must\n//     start with a '.' or '/' and not contain a '#' or '?'.\n//   - URL-like reference which must be a valid URL with any special characters\n//     encoded. The scheme can be any valid flake ref type except for mercurial,\n//     gitlab, and sourcehut.\n//\n// ParseRef does not guarantee that a parsed flake ref is valid or that an\n// error indicates an invalid flake ref. Use the \"nix flake metadata\" command or\n// the builtins.parseFlakeRef Nix function to validate a flake ref.\nfunc ParseRef(ref string) (Ref, error) {\n\tif ref == \"\" {\n\t\treturn Ref{}, redact.Errorf(\"empty flake reference\")\n\t}\n\n\t// Handle path-style references first.\n\tparsed := Ref{}\n\tif ref[0] == '.' || ref[0] == '/' {\n\t\tif strings.ContainsAny(ref, \"?#\") {\n\t\t\t// The Nix CLI does seem to allow paths with a '?'\n\t\t\t// (contrary to the manual) but ignores everything that\n\t\t\t// comes after it. This is a bit surprising, so we just\n\t\t\t// don't allow it at all.\n\t\t\treturn Ref{}, redact.Errorf(\"path-style flake reference %q contains a '?' or '#'\", ref)\n\t\t}\n\t\tparsed.Type = TypePath\n\t\tparsed.Path = ref\n\t\treturn parsed, nil\n\t}\n\tparsed, fragment, err := parseURLRef(ref)\n\tif fragment != \"\" {\n\t\treturn Ref{}, redact.Errorf(\"flake reference %q contains a URL fragment\", ref)\n\t}\n\treturn parsed, err\n}\n\nfunc parseURLRef(ref string) (parsed Ref, fragment string, err error) {\n\t// A good way to test how Nix parses a flake reference is to run:\n\t//\n\t// \tnix eval --json --expr 'builtins.parseFlakeRef \"ref\"' | jq\n\trefURL, err := url.Parse(ref)\n\tif err != nil {\n\t\treturn Ref{}, \"\", redact.Errorf(\"parse flake reference as URL: %v\", err)\n\t}\n\n\t// ensure that the fragment is excluded from the parsed URL\n\t// since those are not valid in flake references.\n\tfragment = refURL.Fragment\n\trefURL.Fragment = \"\"\n\n\tswitch refURL.Scheme {\n\tcase \"\", \"flake\":\n\t\t// [flake:]<flake-id>(/<rev-or-ref>(/rev)?)?\n\n\t\tparsed.Type = TypeIndirect\n\t\tsplit, err := splitPathOrOpaque(refURL, -1)\n\t\tif err != nil {\n\t\t\treturn Ref{}, \"\", redact.Errorf(\"parse flake reference URL path: %v\", err)\n\t\t}\n\t\tparsed.ID = split[0]\n\t\tif len(split) > 1 {\n\t\t\tif isGitHash(split[1]) {\n\t\t\t\tparsed.Rev = split[1]\n\t\t\t} else {\n\t\t\t\tparsed.Ref = split[1]\n\t\t\t}\n\t\t}\n\t\tif len(split) > 2 && parsed.Rev == \"\" {\n\t\t\tparsed.Rev = split[2]\n\t\t}\n\tcase \"path\":\n\t\t// [path:]<path>(\\?<params)?\n\n\t\tparsed.Type = TypePath\n\t\tif refURL.Path == \"\" {\n\t\t\tparsed.Path, err = url.PathUnescape(refURL.Opaque)\n\t\t\tif err != nil {\n\t\t\t\treturn Ref{}, \"\", err\n\t\t\t}\n\t\t} else {\n\t\t\tparsed.Path = refURL.Path\n\t\t}\n\n\t\tquery := refURL.Query()\n\t\tparsed.NARHash = query.Get(\"narHash\")\n\t\tparsed.LastModified, err = atoiOmitZero(query.Get(\"lastModified\"))\n\t\tif err != nil {\n\t\t\treturn Ref{}, \"\", redact.Errorf(\"parse flake reference URL query parameter: lastModified=%s: %v\", redact.Safe(parsed.LastModified), redact.Safe(err))\n\t\t}\n\tcase \"http\", \"https\", \"file\":\n\t\tif isArchive(refURL.Path) {\n\t\t\tparsed.Type = TypeTarball\n\t\t} else {\n\t\t\tparsed.Type = TypeFile\n\t\t}\n\t\tquery := refURL.Query()\n\t\tparsed.Dir = query.Get(\"dir\")\n\t\tparsed.NARHash = query.Get(\"narHash\")\n\t\tparsed.LastModified, err = atoiOmitZero(query.Get(\"lastModified\"))\n\t\tif err != nil {\n\t\t\treturn Ref{}, \"\", redact.Errorf(\"parse flake reference URL query parameter: lastModified=%s: %v\", redact.Safe(parsed.LastModified), redact.Safe(err))\n\t\t}\n\n\t\t// lastModified and narHash get stripped from the query\n\t\t// parameters, but dir stays.\n\t\tquery.Del(\"lastModified\")\n\t\tquery.Del(\"narHash\")\n\t\trefURL.RawQuery = query.Encode()\n\t\tparsed.URL = refURL.String()\n\tcase \"tarball+http\", \"tarball+https\", \"tarball+file\":\n\t\tparsed.Type = TypeTarball\n\t\tquery := refURL.Query()\n\t\tparsed.Dir = query.Get(\"dir\")\n\t\tparsed.NARHash = query.Get(\"narHash\")\n\t\tparsed.LastModified, err = atoiOmitZero(query.Get(\"lastModified\"))\n\t\tif err != nil {\n\t\t\treturn Ref{}, \"\", redact.Errorf(\"parse flake reference URL query parameter: lastModified=%s: %v\", redact.Safe(parsed.LastModified), redact.Safe(err))\n\t\t}\n\n\t\t// lastModified and narHash get stripped from the query\n\t\t// parameters, but dir stays.\n\t\tquery.Del(\"lastModified\")\n\t\tquery.Del(\"narHash\")\n\t\trefURL.RawQuery = query.Encode()\n\t\trefURL.Scheme = refURL.Scheme[8:] // remove tarball+\n\t\tparsed.URL = refURL.String()\n\tcase \"file+http\", \"file+https\", \"file+file\":\n\t\tparsed.Type = TypeFile\n\t\tquery := refURL.Query()\n\t\tparsed.Dir = query.Get(\"dir\")\n\t\tparsed.NARHash = query.Get(\"narHash\")\n\t\tparsed.LastModified, err = atoiOmitZero(query.Get(\"lastModified\"))\n\t\tif err != nil {\n\t\t\treturn Ref{}, \"\", redact.Errorf(\"parse flake reference URL query parameter: lastModified=%s: %v\", redact.Safe(parsed.LastModified), redact.Safe(err))\n\t\t}\n\n\t\t// lastModified and narHash get stripped from the query\n\t\t// parameters, but dir stays.\n\t\tquery.Del(\"lastModified\")\n\t\tquery.Del(\"narHash\")\n\t\trefURL.RawQuery = query.Encode()\n\t\trefURL.Scheme = refURL.Scheme[5:] // remove file+\n\t\tparsed.URL = refURL.String()\n\tcase \"git\", \"git+http\", \"git+https\", \"git+ssh\", \"git+git\", \"git+file\":\n\t\tparsed.Type = TypeGit\n\t\tquery := refURL.Query()\n\t\tparsed.Dir = query.Get(\"dir\")\n\t\tparsed.Ref = query.Get(\"ref\")\n\t\tparsed.Rev = query.Get(\"rev\")\n\n\t\t// ref and rev get stripped from the query parameters, but dir\n\t\t// stays.\n\t\tquery.Del(\"ref\")\n\t\tquery.Del(\"rev\")\n\t\trefURL.RawQuery = query.Encode()\n\t\tif len(refURL.Scheme) > 3 {\n\t\t\trefURL.Scheme = refURL.Scheme[4:] // remove git+\n\t\t}\n\t\tparsed.URL = refURL.String()\n\tcase \"github\":\n\t\tif err := parseGitHubRef(refURL, &parsed); err != nil {\n\t\t\treturn Ref{}, \"\", err\n\t\t}\n\tdefault:\n\t\treturn Ref{}, \"\", redact.Errorf(\"unsupported flake reference URL scheme: %s\", redact.Safe(refURL.Scheme))\n\t}\n\treturn parsed, fragment, nil\n}\n\nfunc parseGitHubRef(refURL *url.URL, parsed *Ref) error {\n\t// github:<owner>/<repo>(/<rev-or-ref>)?(\\?<params>)?\n\n\tparsed.Type = TypeGitHub\n\n\t// Only split up to 3 times (owner, repo, ref/rev) so that we handle\n\t// refs that have slashes in them. For example,\n\t// \"github:jetify-com/devbox/gcurtis/flakeref\" parses as \"gcurtis/flakeref\".\n\tsplit, err := splitPathOrOpaque(refURL, 3)\n\tif err != nil {\n\t\treturn err\n\t}\n\tparsed.Owner = split[0]\n\tparsed.Repo = split[1]\n\tif len(split) > 2 {\n\t\tif revOrRef := split[2]; isGitHash(revOrRef) {\n\t\t\tparsed.Rev = revOrRef\n\t\t} else {\n\t\t\tparsed.Ref = revOrRef\n\t\t}\n\t}\n\n\tparsed.Host = refURL.Query().Get(\"host\")\n\tparsed.Dir = refURL.Query().Get(\"dir\")\n\tif qRef := refURL.Query().Get(\"ref\"); qRef != \"\" {\n\t\tif parsed.Rev != \"\" {\n\t\t\treturn redact.Errorf(\"github flake reference has a ref and a rev\")\n\t\t}\n\t\tif parsed.Ref != \"\" && qRef != parsed.Ref {\n\t\t\treturn redact.Errorf(\"github flake reference has a ref in the path (%q) and a ref query parameter (%q)\", parsed.Ref, qRef)\n\t\t}\n\t\tparsed.Ref = qRef\n\t}\n\tif qRev := refURL.Query().Get(\"rev\"); qRev != \"\" {\n\t\tif parsed.Ref != \"\" {\n\t\t\treturn redact.Errorf(\"github flake reference has a ref and a rev\")\n\t\t}\n\t\tif parsed.Rev != \"\" && qRev != parsed.Rev {\n\t\t\treturn redact.Errorf(\"github flake reference has a rev in the path (%q) and a rev query parameter (%q)\", parsed.Rev, qRev)\n\t\t}\n\t\tparsed.Rev = qRev\n\t}\n\tparsed.Dir = refURL.Query().Get(\"dir\")\n\tparsed.NARHash = refURL.Query().Get(\"narHash\")\n\treturn nil\n}\n\n// Locked reports whether r is locked. Locked flake references always resolve to\n// the same content. For some flake types, determining if a Ref is locked\n// depends on the local Nix configuration. In these cases, Locked conservatively\n// returns false.\nfunc (r Ref) Locked() bool {\n\t// Search for the implementations of InputScheme::isLocked in the nix\n\t// source.\n\t//\n\t// https://github.com/search?q=repo%3ANixOS%2Fnix+language%3AC%2B%2B+symbol%3AisLocked&type=code\n\n\tswitch r.Type {\n\tcase TypeFile, TypePath, TypeTarball:\n\t\treturn r.NARHash != \"\"\n\tcase TypeGit:\n\t\treturn r.Rev != \"\"\n\tcase TypeGitHub:\n\t\t// We technically can't determine if a github flake is locked\n\t\t// unless we know the trust-tarballs-from-git-forges Nix setting\n\t\t// (which defaults to true), so we have to be conservative and\n\t\t// check for rev and narHash.\n\t\t//\n\t\t// https://github.com/NixOS/nix/blob/3f3feae33e3381a2ea5928febe03329f0a578b20/src/libfetchers/github.cc#L304-L313\n\t\treturn r.Rev != \"\" && r.NARHash != \"\"\n\tcase TypeIndirect:\n\t\t// Never locked because they must be resolved against a flake\n\t\t// registry.\n\t\treturn false\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// String encodes the flake reference as a URL-like string. It normalizes the\n// result such that if two Ref values are equal, then their strings will also be\n// equal.\n//\n// There are multiple ways to encode a flake reference. String uses the\n// following rules to normalize the result:\n//\n//   - the URL-like form is always used, even for paths and indirects.\n//   - the scheme is always present, even if it's optional.\n//   - paths are cleaned per path.Clean.\n//   - fields that can be put in either the path or the query string are always\n//     put in the path.\n//   - query parameters are sorted by key.\n//\n// If r is missing a type or has any invalid fields, String returns an empty\n// string.\nfunc (r Ref) String() string {\n\tswitch r.Type {\n\tcase TypeFile:\n\t\tif r.URL == \"\" {\n\t\t\treturn \"\"\n\t\t}\n\n\t\turl, err := url.Parse(\"file+\" + r.URL)\n\t\tif err != nil {\n\t\t\t// This should be rare and only happen if the caller\n\t\t\t// messed with the parsed URL.\n\t\t\treturn \"\"\n\t\t}\n\t\turl.RawQuery = appendQueryString(url.Query(),\n\t\t\t\"lastModified\", itoaOmitZero(r.LastModified),\n\t\t\t\"narHash\", r.NARHash,\n\t\t)\n\t\treturn url.String()\n\tcase TypeGit:\n\t\tif r.URL == \"\" {\n\t\t\treturn \"\"\n\t\t}\n\t\tif !strings.HasPrefix(r.URL, \"git\") {\n\t\t\tr.URL = \"git+\" + r.URL\n\t\t}\n\n\t\t// Nix removes \"ref\" and \"rev\" from the query string\n\t\t// (but not other parameters) after parsing. If they're empty,\n\t\t// we can skip parsing the URL. Otherwise, we need to add them\n\t\t// back.\n\t\tif r.Ref == \"\" && r.Rev == \"\" {\n\t\t\treturn r.URL\n\t\t}\n\t\turl, err := url.Parse(r.URL)\n\t\tif err != nil {\n\t\t\t// This should be rare and only happen if the caller\n\t\t\t// messed with the parsed URL.\n\t\t\treturn \"\"\n\t\t}\n\t\turl.RawQuery = appendQueryString(url.Query(), \"ref\", r.Ref, \"rev\", r.Rev, \"dir\", r.Dir)\n\t\treturn url.String()\n\tcase TypeGitHub:\n\t\tif r.Owner == \"\" || r.Repo == \"\" {\n\t\t\treturn \"\"\n\t\t}\n\t\turl := &url.URL{\n\t\t\tScheme: \"github\",\n\t\t\tOpaque: buildEscapedPath(r.Owner, r.Repo, cmp.Or(r.Rev, r.Ref)),\n\t\t\tRawQuery: appendQueryString(nil,\n\t\t\t\t\"host\", r.Host,\n\t\t\t\t\"dir\", r.Dir,\n\t\t\t\t\"lastModified\", itoaOmitZero(r.LastModified),\n\t\t\t\t\"narHash\", r.NARHash,\n\t\t\t),\n\t\t}\n\t\treturn url.String()\n\tcase TypeIndirect:\n\t\tif r.ID == \"\" {\n\t\t\treturn \"\"\n\t\t}\n\t\turl := &url.URL{\n\t\t\tScheme: \"flake\",\n\t\t\tOpaque: buildEscapedPath(r.ID, r.Ref, r.Rev),\n\t\t\tRawQuery: appendQueryString(nil,\n\t\t\t\t\"dir\", r.Dir,\n\t\t\t\t\"lastModified\", itoaOmitZero(r.LastModified),\n\t\t\t\t\"narHash\", r.NARHash,\n\t\t\t),\n\t\t}\n\t\treturn url.String()\n\tcase TypePath:\n\t\tif r.Path == \"\" {\n\t\t\treturn \"\"\n\t\t}\n\t\tr.Path = path.Clean(r.Path)\n\t\turl := &url.URL{\n\t\t\tScheme: \"path\",\n\t\t\tOpaque: buildEscapedPath(strings.Split(r.Path, \"/\")...),\n\t\t}\n\n\t\t// Add the / prefix back if strings.Split removed it.\n\t\tif r.Path[0] == '/' {\n\t\t\turl.Opaque = \"/\" + url.Opaque\n\t\t} else if r.Path == \".\" {\n\t\t\turl.Opaque = \".\"\n\t\t}\n\n\t\turl.RawQuery = appendQueryString(nil,\n\t\t\t\"lastModified\", itoaOmitZero(r.LastModified),\n\t\t\t\"narHash\", r.NARHash,\n\t\t)\n\t\treturn url.String()\n\tcase TypeTarball:\n\t\tif r.URL == \"\" {\n\t\t\treturn \"\"\n\t\t}\n\t\tif !strings.HasPrefix(r.URL, \"tarball\") {\n\t\t\tr.URL = \"tarball+\" + r.URL\n\t\t}\n\n\t\turl, err := url.Parse(r.URL)\n\t\tif err != nil {\n\t\t\t// This should be rare and only happen if the caller\n\t\t\t// messed with the parsed URL.\n\t\t\treturn \"\"\n\t\t}\n\t\turl.RawQuery = appendQueryString(url.Query(),\n\t\t\t\"dir\", r.Dir,\n\t\t\t\"lastModified\", itoaOmitZero(r.LastModified),\n\t\t\t\"narHash\", r.NARHash,\n\t\t)\n\t\treturn url.String()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// IsNixpkgs reports whether the flake reference looks like a nixpkgs flake.\n//\n// While there are many ways to specify this input, devbox always uses\n// github:NixOS/nixpkgs/<hash> as the URL. If the user wishes to reference nixpkgs\n// themselves, this function may not return true.\nfunc (r Ref) IsNixpkgs() bool {\n\tswitch r.Type {\n\tcase TypeGitHub:\n\t\treturn r.Owner == \"NixOS\" && r.Repo == \"nixpkgs\"\n\tcase TypeIndirect:\n\t\treturn r.ID == \"nixpkgs\"\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc isGitHash(s string) bool {\n\tif len(s) != 40 {\n\t\treturn false\n\t}\n\tfor i := range s {\n\t\tisDigit := s[i] >= '0' && s[i] <= '9'\n\t\tisHexLetter := s[i] >= 'a' && s[i] <= 'f'\n\t\tif !isDigit && !isHexLetter {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc isArchive(path string) bool {\n\t// As documented under the tarball type:\n\t// https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-flake#types\n\treturn strings.HasSuffix(path, \".tar\") ||\n\t\tstrings.HasSuffix(path, \".tar.gz\") ||\n\t\tstrings.HasSuffix(path, \".tgz\") ||\n\t\tstrings.HasSuffix(path, \".tar.xz\") ||\n\t\tstrings.HasSuffix(path, \".tar.zst\") ||\n\t\tstrings.HasSuffix(path, \".tar.bz2\") ||\n\t\tstrings.HasSuffix(path, \".zip\")\n}\n\n// splitPathOrOpaque splits a URL path by '/'. If the path is empty, it splits\n// the opaque instead. Splitting happens before unescaping the path or opaque,\n// ensuring that path elements with an encoded '/' (%2F) are not split.\n// For example, \"/dir/file%2Fname\" becomes the elements \"dir\" and \"file/name\".\n// The count limits the number of substrings per [strings.SplitN]\nfunc splitPathOrOpaque(u *url.URL, n int) ([]string, error) {\n\tupath := u.EscapedPath()\n\tif upath == \"\" {\n\t\tupath = u.Opaque\n\t}\n\tupath = strings.TrimSpace(upath)\n\tif upath == \"\" {\n\t\treturn nil, nil\n\t}\n\n\t// We don't want an empty element if the path is rooted.\n\tif upath[0] == '/' {\n\t\tupath = upath[1:]\n\t}\n\tupath = path.Clean(upath)\n\n\tvar err error\n\tsplit := strings.SplitN(upath, \"/\", n)\n\tfor i := range split {\n\t\tsplit[i], err = url.PathUnescape(split[i])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn split, nil\n}\n\n// buildEscapedPath escapes and joins path elements for a URL flake ref. The\n// resulting path is cleaned according to url.JoinPath.\nfunc buildEscapedPath(elem ...string) string {\n\tfor i := range elem {\n\t\telem[i] = url.PathEscape(elem[i])\n\t}\n\tu := &url.URL{}\n\treturn u.JoinPath(elem...).String()\n}\n\n// appendQueryString builds a URL query string from a list of key-value string\n// pairs, omitting any keys with empty values.\nfunc appendQueryString(query url.Values, keyval ...string) string {\n\tif len(keyval)%2 != 0 {\n\t\tpanic(\"appendQueryString: odd number of key-value pairs\")\n\t}\n\n\tappended := make(url.Values, len(query)+len(keyval)/2)\n\tfor k, vals := range query {\n\t\tv := cmp.Or(vals...)\n\t\tif v != \"\" {\n\t\t\tappended.Set(k, v)\n\t\t}\n\t}\n\tfor i := 0; i < len(keyval); i += 2 {\n\t\tk, v := keyval[i], keyval[i+1]\n\t\tif v != \"\" {\n\t\t\tappended.Set(k, v)\n\t\t}\n\t}\n\treturn appended.Encode()\n}\n\n// itoaOmitZero returns an empty string if i == 0, otherwise it formats i as a\n// string in base-10.\nfunc itoaOmitZero(i int64) string {\n\tif i == 0 {\n\t\treturn \"\"\n\t}\n\treturn strconv.FormatInt(i, 10)\n}\n\n// atoiOmitZero returns 0 if s == \"\", otherwised it parses s as a base-10 int64.\nfunc atoiOmitZero(s string) (int64, error) {\n\tif s == \"\" {\n\t\treturn 0, nil\n\t}\n\treturn strconv.ParseInt(s, 10, 64)\n}\n\n// Special values for [Installable].Outputs.\nconst (\n\t// DefaultOutputs specifies that the package-defined default outputs\n\t// should be installed.\n\tDefaultOutputs = \"\"\n\n\t// AllOutputs specifies that all package outputs should be installed.\n\tAllOutputs = \"*\"\n)\n\n// Installable is a Nix command line argument that specifies how to install a\n// flake. It can be a plain flake reference, or a flake reference with an\n// attribute path and/or output specification.\n//\n// Some examples are:\n//\n//   - \".\" installs the default attribute from the flake in the current\n//     directory.\n//   - \".#hello\" installs the hello attribute from the flake in the current\n//     directory.\n//   - \"nixpkgs#hello\" installs the hello attribute from the nixpkgs flake.\n//   - \"github:NixOS/nixpkgs/unstable#curl^lib\" installs the the lib output of\n//     curl attribute from the flake on the nixpkgs unstable branch.\n//\n// The flake installable syntax is only valid in Nix command line arguments, not\n// in Nix expressions. See [Ref] and the [Nix manual] for details on the\n// differences between flake references and installables.\n//\n// [Nix manual]: https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix#installables\ntype Installable struct {\n\t// Ref is the flake reference portion of the installable.\n\tRef Ref `json:\"ref,omitempty\"`\n\n\t// AttrPath is an attribute path of the flake, encoded as a URL\n\t// fragment.\n\tAttrPath string `json:\"attr_path,omitempty\"`\n\n\t// Outputs is the installable's output spec, which is a comma-separated\n\t// list of package outputs to install. The outputs spec is anything\n\t// after the last caret '^' in an installable. Unlike the\n\t// attribute path, output specs are not URL-encoded.\n\t//\n\t// The special values DefaultOutputs (\"\") and AllOutputs (\"*\") specify\n\t// the default set of package outputs and all package outputs,\n\t// respectively.\n\t//\n\t// ParseInstallable cleans the list of outputs by removing empty\n\t// elements and sorting the results. Lists containing a \"*\" are\n\t// simplified to a single \"*\".\n\tOutputs string `json:\"outputs,omitempty\"`\n}\n\n// ParseInstallable parses a flake installable. The raw string must contain\n// a valid flake reference parsable by [ParseRef], optionally followed by an\n// #attrpath and/or an ^output.\nfunc ParseInstallable(raw string) (Installable, error) {\n\tif raw == \"\" {\n\t\treturn Installable{}, redact.Errorf(\"empty flake installable\")\n\t}\n\n\t// The output spec must be parsed and removed first, otherwise it will\n\t// be parsed as part of the flake ref's URL fragment.\n\tinstall := Installable{}\n\traw, install.Outputs = splitOutputSpec(raw)\n\tinstall.Outputs = strings.Join(install.SplitOutputs(), \",\") // clean the outputs\n\n\t// Interpret installables with path-style flake refs as URLs to extract\n\t// the attribute path (fragment). This means that path-style flake refs\n\t// cannot point to files with a '#' or '?' in their name, since those\n\t// would be parsed as the URL fragment or query string. This mimic's\n\t// Nix's CLI behavior.\n\tif raw[0] == '.' || raw[0] == '/' {\n\t\traw = \"path:\" + raw\n\t}\n\n\tvar err error\n\tinstall.Ref, install.AttrPath, err = parseURLRef(raw)\n\tif err != nil {\n\t\treturn Installable{}, err\n\t}\n\treturn install, nil\n}\n\n// SplitOutputs splits and sorts the comma-separated list of outputs. It skips\n// any empty outputs. If one or more of the outputs is a \"*\", then the result\n// will be a slice with a single \"*\" element.\nfunc (fi Installable) SplitOutputs() []string {\n\tif fi.Outputs == \"\" {\n\t\treturn []string{}\n\t}\n\n\tsplit := strings.Split(fi.Outputs, \",\")\n\ti := 0\n\tfor _, out := range split {\n\t\t// A wildcard takes priority over any other outputs.\n\t\tif out == \"*\" {\n\t\t\treturn []string{\"*\"}\n\t\t}\n\t\tif out != \"\" {\n\t\t\tsplit[i] = out\n\t\t\ti++\n\t\t}\n\t}\n\tsplit = split[:i]\n\tslices.Sort(split)\n\treturn split\n}\n\n// String encodes the installable as a Nix command line argument. It normalizes\n// the result such that if two installable values are equal, then their strings\n// will also be equal.\n//\n// String always cleans the outputs spec as described by the Outputs field's\n// documentation. The same normalization rules from [Ref.String] still apply.\nfunc (fi Installable) String() string {\n\tstr := fi.Ref.String()\n\tif str == \"\" {\n\t\treturn \"\"\n\t}\n\tif fi.AttrPath != \"\" {\n\t\turl, err := url.Parse(str)\n\t\tif err != nil {\n\t\t\t// This should never happen. Even an empty string is a\n\t\t\t// valid URL.\n\t\t\tpanic(\"invalid URL from Ref.String: \" + str)\n\t\t}\n\t\turl.Fragment = fi.AttrPath\n\t\tstr = url.String()\n\t}\n\tif fi.Outputs != \"\" {\n\t\tclean := strings.Join(fi.SplitOutputs(), \",\")\n\t\tif clean != \"\" {\n\t\t\tstr += \"^\" + clean\n\t\t}\n\t}\n\treturn str\n}\n\n// splitOutputSpec cuts a flake installable around the last instance of ^.\nfunc splitOutputSpec(s string) (before, after string) {\n\tif i := strings.LastIndexByte(s, '^'); i >= 0 {\n\t\treturn s[:i], s[i+1:]\n\t}\n\treturn s, \"\"\n}\n"
  },
  {
    "path": "nix/flake/flakeref_test.go",
    "content": "package flake\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestParseFlakeRef(t *testing.T) {\n\tcases := map[string]Ref{\n\t\t// Path-like references start with a '.' or '/'.\n\t\t// This distinguishes them from indirect references\n\t\t// (./nixpkgs is a directory; nixpkgs is an indirect).\n\t\t\".\":                {Type: TypePath, Path: \".\"},\n\t\t\"./\":               {Type: TypePath, Path: \"./\"},\n\t\t\"./flake\":          {Type: TypePath, Path: \"./flake\"},\n\t\t\"./relative/flake\": {Type: TypePath, Path: \"./relative/flake\"},\n\t\t\"/\":                {Type: TypePath, Path: \"/\"},\n\t\t\"/flake\":           {Type: TypePath, Path: \"/flake\"},\n\t\t\"/absolute/flake\":  {Type: TypePath, Path: \"/absolute/flake\"},\n\n\t\t// Path-like references can have raw unicode characters unlike\n\t\t// path: URL references.\n\t\t\"./Ûñî©ôδ€/flake\\n\": {Type: TypePath, Path: \"./Ûñî©ôδ€/flake\\n\"},\n\t\t\"/Ûñî©ôδ€/flake\\n\":  {Type: TypePath, Path: \"/Ûñî©ôδ€/flake\\n\"},\n\n\t\t// URL-like path references.\n\t\t\"path:\":                      {Type: TypePath, Path: \"\"},\n\t\t\"path:.\":                     {Type: TypePath, Path: \".\"},\n\t\t\"path:./\":                    {Type: TypePath, Path: \"./\"},\n\t\t\"path:./flake\":               {Type: TypePath, Path: \"./flake\"},\n\t\t\"path:./relative/flake\":      {Type: TypePath, Path: \"./relative/flake\"},\n\t\t\"path:./relative/my%20flake\": {Type: TypePath, Path: \"./relative/my flake\"},\n\t\t\"path:/\":                     {Type: TypePath, Path: \"/\"},\n\t\t\"path:/flake\":                {Type: TypePath, Path: \"/flake\"},\n\t\t\"path:/absolute/flake\":       {Type: TypePath, Path: \"/absolute/flake\"},\n\t\t\"path:/absolute/flake?lastModified=1734435836&narHash=sha256-kMBQ5PRiFLagltK0sH%2B08aiNt3zGERC2297iB6vrvlU%3D\": {Type: TypePath, Path: \"/absolute/flake\", LastModified: 1734435836, NARHash: \"sha256-kMBQ5PRiFLagltK0sH+08aiNt3zGERC2297iB6vrvlU=\"},\n\n\t\t// URL-like paths can omit the \"./\" prefix for relative\n\t\t// directories.\n\t\t\"path:flake\":          {Type: TypePath, Path: \"flake\"},\n\t\t\"path:relative/flake\": {Type: TypePath, Path: \"relative/flake\"},\n\n\t\t// Indirect references.\n\t\t\"flake:indirect\":          {Type: TypeIndirect, ID: \"indirect\"},\n\t\t\"flake:indirect/ref\":      {Type: TypeIndirect, ID: \"indirect\", Ref: \"ref\"},\n\t\t\"flake:indirect/my%2Fref\": {Type: TypeIndirect, ID: \"indirect\", Ref: \"my/ref\"},\n\t\t\"flake:indirect/5233fd2ba76a3accb5aaa999c00509a11fd0793c\":     {Type: TypeIndirect, ID: \"indirect\", Rev: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"},\n\t\t\"flake:indirect/ref/5233fd2ba76a3accb5aaa999c00509a11fd0793c\": {Type: TypeIndirect, ID: \"indirect\", Ref: \"ref\", Rev: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"},\n\n\t\t// Indirect references can omit their \"indirect:\" type prefix.\n\t\t\"indirect\":     {Type: TypeIndirect, ID: \"indirect\"},\n\t\t\"indirect/ref\": {Type: TypeIndirect, ID: \"indirect\", Ref: \"ref\"},\n\t\t\"indirect/5233fd2ba76a3accb5aaa999c00509a11fd0793c\":     {Type: TypeIndirect, ID: \"indirect\", Rev: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"},\n\t\t\"indirect/ref/5233fd2ba76a3accb5aaa999c00509a11fd0793c\": {Type: TypeIndirect, ID: \"indirect\", Ref: \"ref\", Rev: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"},\n\n\t\t// GitHub references.\n\t\t\"github:NixOS/nix\":            {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\"},\n\t\t\"github:NixOS/nix/v1.2.3\":     {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"v1.2.3\"},\n\t\t\"github:NixOS/nix?ref=v1.2.3\": {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"v1.2.3\"},\n\t\t\"github:NixOS/nix?ref=5233fd2ba76a3accb5aaa999c00509a11fd0793c\": {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"},\n\t\t\"github:NixOS/nix/main\": {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"main\"},\n\t\t\"github:NixOS/nix/main/5233fd2ba76a3accb5aaa999c00509a11fd0793c\": {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"main/5233fd2ba76a3accb5aaa999c00509a11fd0793c\"},\n\t\t\"github:NixOS/nix/5233fd2bb76a3accb5aaa999c00509a11fd0793z\":      {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"5233fd2bb76a3accb5aaa999c00509a11fd0793z\"},\n\t\t\"github:NixOS/nix/5233fd2ba76a3accb5aaa999c00509a11fd0793c\":      {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Rev: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"},\n\t\t\"github:NixOS/nix?rev=5233fd2ba76a3accb5aaa999c00509a11fd0793c\":  {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Rev: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"},\n\t\t\"github:NixOS/nix?host=example.com\":                              {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Host: \"example.com\"},\n\t\t\"github:NixOS/nix?host=example.com&dir=subdir\":                   {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Host: \"example.com\", Dir: \"subdir\"},\n\t\t\"github:NixOS/nix?host=example.com&dir=subdir&lastModified=1734435836&narHash=sha256-kMBQ5PRiFLagltK0sH%2B08aiNt3zGERC2297iB6vrvlU%3D\": {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Host: \"example.com\", Dir: \"subdir\", NARHash: \"sha256-kMBQ5PRiFLagltK0sH+08aiNt3zGERC2297iB6vrvlU=\"},\n\n\t\t// The github type allows clone-style URLs. The username and\n\t\t// host are ignored.\n\t\t\"github://git@github.com/NixOS/nix\":                                              {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\"},\n\t\t\"github://git@github.com/NixOS/nix/v1.2.3\":                                       {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"v1.2.3\"},\n\t\t\"github://git@github.com/NixOS/nix?ref=v1.2.3\":                                   {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"v1.2.3\"},\n\t\t\"github://git@github.com/NixOS/nix?ref=5233fd2ba76a3accb5aaa999c00509a11fd0793c\": {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"},\n\t\t\"github://git@github.com/NixOS/nix?rev=5233fd2ba76a3accb5aaa999c00509a11fd0793c\": {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Rev: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"},\n\t\t\"github://git@github.com/NixOS/nix?host=example.com\":                             {Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Host: \"example.com\"},\n\n\t\t// Git references.\n\t\t\"git://example.com/repo/flake\":         {Type: TypeGit, URL: \"git://example.com/repo/flake\"},\n\t\t\"git+https://example.com/repo/flake\":   {Type: TypeGit, URL: \"https://example.com/repo/flake\"},\n\t\t\"git+ssh://git@example.com/repo/flake\": {Type: TypeGit, URL: \"ssh://git@example.com/repo/flake\"},\n\t\t\"git:/repo/flake\":                      {Type: TypeGit, URL: \"git:/repo/flake\"},\n\t\t\"git+file:///repo/flake\":               {Type: TypeGit, URL: \"file:///repo/flake\"},\n\t\t\"git://example.com/repo/flake?ref=unstable&rev=e486d8d40e626a20e06d792db8cc5ac5aba9a5b4&dir=subdir\":                                                                                         {Type: TypeGit, URL: \"git://example.com/repo/flake?dir=subdir\", Ref: \"unstable\", Rev: \"e486d8d40e626a20e06d792db8cc5ac5aba9a5b4\", Dir: \"subdir\"},\n\t\t\"git://example.com/repo/flake?ref=unstable&rev=e486d8d40e626a20e06d792db8cc5ac5aba9a5b4&dir=subdir&lastModified=1734435836&narHash=sha256-kMBQ5PRiFLagltK0sH%2B08aiNt3zGERC2297iB6vrvlU%3D\": {Type: TypeGit, URL: \"git://example.com/repo/flake?dir=subdir&lastModified=1734435836&narHash=sha256-kMBQ5PRiFLagltK0sH%2B08aiNt3zGERC2297iB6vrvlU%3D\", Ref: \"unstable\", Rev: \"e486d8d40e626a20e06d792db8cc5ac5aba9a5b4\", Dir: \"subdir\"},\n\n\t\t// Tarball references.\n\t\t\"tarball+http://example.com/flake\":  {Type: TypeTarball, URL: \"http://example.com/flake\"},\n\t\t\"tarball+https://example.com/flake\": {Type: TypeTarball, URL: \"https://example.com/flake\"},\n\t\t\"tarball+file:///home/flake\":        {Type: TypeTarball, URL: \"file:///home/flake\"},\n\n\t\t// Regular URLs have the tarball type if they have a known\n\t\t// archive extension:\n\t\t// .zip, .tar, .tgz, .tar.gz, .tar.xz, .tar.bz2 or .tar.zst\n\t\t\"http://example.com/flake.zip\":            {Type: TypeTarball, URL: \"http://example.com/flake.zip\"},\n\t\t\"http://example.com/flake.tar\":            {Type: TypeTarball, URL: \"http://example.com/flake.tar\"},\n\t\t\"http://example.com/flake.tgz\":            {Type: TypeTarball, URL: \"http://example.com/flake.tgz\"},\n\t\t\"http://example.com/flake.tar.gz\":         {Type: TypeTarball, URL: \"http://example.com/flake.tar.gz\"},\n\t\t\"http://example.com/flake.tar.xz\":         {Type: TypeTarball, URL: \"http://example.com/flake.tar.xz\"},\n\t\t\"http://example.com/flake.tar.bz2\":        {Type: TypeTarball, URL: \"http://example.com/flake.tar.bz2\"},\n\t\t\"http://example.com/flake.tar.zst\":        {Type: TypeTarball, URL: \"http://example.com/flake.tar.zst\"},\n\t\t\"http://example.com/flake.tar?dir=subdir\": {Type: TypeTarball, URL: \"http://example.com/flake.tar?dir=subdir\", Dir: \"subdir\"},\n\t\t\"http://example.com/flake.tar?dir=subdir&lastModified=1734435836&narHash=sha256-kMBQ5PRiFLagltK0sH%2B08aiNt3zGERC2297iB6vrvlU%3D\": {Type: TypeTarball, URL: \"http://example.com/flake.tar?dir=subdir\", Dir: \"subdir\", LastModified: 1734435836, NARHash: \"sha256-kMBQ5PRiFLagltK0sH+08aiNt3zGERC2297iB6vrvlU=\"},\n\t\t\"file:///flake.zip\":            {Type: TypeTarball, URL: \"file:///flake.zip\"},\n\t\t\"file:///flake.tar\":            {Type: TypeTarball, URL: \"file:///flake.tar\"},\n\t\t\"file:///flake.tgz\":            {Type: TypeTarball, URL: \"file:///flake.tgz\"},\n\t\t\"file:///flake.tar.gz\":         {Type: TypeTarball, URL: \"file:///flake.tar.gz\"},\n\t\t\"file:///flake.tar.xz\":         {Type: TypeTarball, URL: \"file:///flake.tar.xz\"},\n\t\t\"file:///flake.tar.bz2\":        {Type: TypeTarball, URL: \"file:///flake.tar.bz2\"},\n\t\t\"file:///flake.tar.zst\":        {Type: TypeTarball, URL: \"file:///flake.tar.zst\"},\n\t\t\"file:///flake.tar?dir=subdir\": {Type: TypeTarball, URL: \"file:///flake.tar?dir=subdir\", Dir: \"subdir\"},\n\n\t\t// File URL references.\n\t\t\"file+file:///flake\":                           {Type: TypeFile, URL: \"file:///flake\"},\n\t\t\"file+http://example.com/flake\":                {Type: TypeFile, URL: \"http://example.com/flake\"},\n\t\t\"file+http://example.com/flake.git\":            {Type: TypeFile, URL: \"http://example.com/flake.git\"},\n\t\t\"file+http://example.com/flake.tar?dir=subdir\": {Type: TypeFile, URL: \"http://example.com/flake.tar?dir=subdir\", Dir: \"subdir\"},\n\n\t\t// Regular URLs have the file type if they don't have a known\n\t\t// archive extension.\n\t\t\"http://example.com/flake\":            {Type: TypeFile, URL: \"http://example.com/flake\"},\n\t\t\"http://example.com/flake.git\":        {Type: TypeFile, URL: \"http://example.com/flake.git\"},\n\t\t\"http://example.com/flake?dir=subdir\": {Type: TypeFile, URL: \"http://example.com/flake?dir=subdir\", Dir: \"subdir\"},\n\t}\n\tfor ref, want := range cases {\n\t\tt.Run(ref, func(t *testing.T) {\n\t\t\tgot, err := ParseRef(ref)\n\t\t\tif diff := cmp.Diff(want, got); diff != \"\" {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"got error: %s\", err)\n\t\t\t\t}\n\t\t\t\tt.Errorf(\"wrong flakeref (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseFlakeRefError(t *testing.T) {\n\tt.Run(\"EmptyString\", func(t *testing.T) {\n\t\tref := \"\"\n\t\t_, err := ParseRef(ref)\n\t\tif err == nil {\n\t\t\tt.Error(\"got nil error for bad flakeref:\", ref)\n\t\t}\n\t})\n\tt.Run(\"InvalidURL\", func(t *testing.T) {\n\t\tref := \"://bad/url\"\n\t\t_, err := ParseRef(ref)\n\t\tif err == nil {\n\t\t\tt.Error(\"got nil error for bad flakeref:\", ref)\n\t\t}\n\t})\n\tt.Run(\"InvalidURLEscape\", func(t *testing.T) {\n\t\tref := \"path:./relative/my%flake\"\n\t\t_, err := ParseRef(ref)\n\t\tif err == nil {\n\t\t\tt.Error(\"got nil error for bad flakeref:\", ref)\n\t\t}\n\t})\n\tt.Run(\"UnsupportedURLScheme\", func(t *testing.T) {\n\t\tref := \"runx:mvdan/gofumpt@latest\"\n\t\t_, err := ParseRef(ref)\n\t\tif err == nil {\n\t\t\tt.Error(\"got nil error for bad flakeref:\", ref)\n\t\t}\n\t})\n\tt.Run(\"PathLikeWith?#\", func(t *testing.T) {\n\t\tin := []string{\n\t\t\t\"./invalid#path\",\n\t\t\t\"./invalid?path\",\n\t\t\t\"/invalid#path\",\n\t\t\t\"/invalid?path\",\n\t\t\t\"/#\",\n\t\t\t\"/?\",\n\t\t}\n\t\tfor _, ref := range in {\n\t\t\t_, err := ParseRef(ref)\n\t\t\tif err == nil {\n\t\t\t\tt.Error(\"got nil error for bad flakeref:\", ref)\n\t\t\t}\n\t\t}\n\t})\n\tt.Run(\"GitHubInvalidRefRevCombo\", func(t *testing.T) {\n\t\tin := []string{\n\t\t\t\"github:NixOS/nix?ref=v1.2.3&rev=5233fd2ba76a3accb5aaa999c00509a11fd0793c\",\n\t\t\t\"github:NixOS/nix/v1.2.3?ref=v4.5.6\",\n\t\t\t\"github:NixOS/nix/5233fd2ba76a3accb5aaa999c00509a11fd0793c?rev=e486d8d40e626a20e06d792db8cc5ac5aba9a5b4\",\n\t\t\t\"github:NixOS/nix/5233fd2ba76a3accb5aaa999c00509a11fd0793c?ref=v1.2.3\",\n\t\t}\n\t\tfor _, ref := range in {\n\t\t\t_, err := ParseRef(ref)\n\t\t\tif err == nil {\n\t\t\t\tt.Error(\"got nil error for bad flakeref:\", ref)\n\t\t\t}\n\t\t}\n\t})\n\tt.Run(\"URLFragment\", func(t *testing.T) {\n\t\tref := \"https://github.com/NixOS/patchelf/archive/master.tar.gz#patchelf\"\n\t\t_, err := ParseRef(ref)\n\t\tif err == nil {\n\t\t\tt.Error(\"got nil error for flakeref with fragment:\", ref)\n\t\t}\n\t})\n}\n\nfunc TestFlakeRefString(t *testing.T) {\n\tcases := map[Ref]string{\n\t\t{}: \"\",\n\n\t\t// Path references.\n\t\t{Type: TypePath, Path: \".\"}:                \"path:.\",\n\t\t{Type: TypePath, Path: \"./\"}:               \"path:.\",\n\t\t{Type: TypePath, Path: \"./flake\"}:          \"path:flake\",\n\t\t{Type: TypePath, Path: \"./relative/flake\"}: \"path:relative/flake\",\n\t\t{Type: TypePath, Path: \"/\"}:                \"path:/\",\n\t\t{Type: TypePath, Path: \"/flake\"}:           \"path:/flake\",\n\t\t{Type: TypePath, Path: \"/absolute/flake\"}:  \"path:/absolute/flake\",\n\n\t\t// Path references with escapes.\n\t\t{Type: TypePath, Path: \"%\"}:                 \"path:%25\",\n\t\t{Type: TypePath, Path: \"/%2F\"}:              \"path:/%252F\",\n\t\t{Type: TypePath, Path: \"./Ûñî©ôδ€/flake\\n\"}: \"path:%C3%9B%C3%B1%C3%AE%C2%A9%C3%B4%CE%B4%E2%82%AC/flake%0A\",\n\t\t{Type: TypePath, Path: \"/Ûñî©ôδ€/flake\\n\"}:  \"path:/%C3%9B%C3%B1%C3%AE%C2%A9%C3%B4%CE%B4%E2%82%AC/flake%0A\",\n\n\t\t// Indirect references.\n\t\t{Type: TypeIndirect, ID: \"indirect\"}:                                                              \"flake:indirect\",\n\t\t{Type: TypeIndirect, ID: \"indirect\", Dir: \"sub/dir\"}:                                              \"flake:indirect?dir=sub%2Fdir\",\n\t\t{Type: TypeIndirect, ID: \"indirect\", Ref: \"ref\"}:                                                  \"flake:indirect/ref\",\n\t\t{Type: TypeIndirect, ID: \"indirect\", Ref: \"my/ref\"}:                                               \"flake:indirect/my%2Fref\",\n\t\t{Type: TypeIndirect, ID: \"indirect\", Rev: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"}:             \"flake:indirect/5233fd2ba76a3accb5aaa999c00509a11fd0793c\",\n\t\t{Type: TypeIndirect, ID: \"indirect\", Ref: \"ref\", Rev: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"}: \"flake:indirect/ref/5233fd2ba76a3accb5aaa999c00509a11fd0793c\",\n\n\t\t// GitHub references.\n\t\t{Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\"}:                                                               \"github:NixOS/nix\",\n\t\t{Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"v1.2.3\"}:                                                \"github:NixOS/nix/v1.2.3\",\n\t\t{Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"my/ref\"}:                                                \"github:NixOS/nix/my%2Fref\",\n\t\t{Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\"}:              \"github:NixOS/nix/5233fd2ba76a3accb5aaa999c00509a11fd0793c\",\n\t\t{Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Ref: \"5233fd2bb76a3accb5aaa999c00509a11fd0793z\"}:              \"github:NixOS/nix/5233fd2bb76a3accb5aaa999c00509a11fd0793z\",\n\t\t{Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Rev: \"5233fd2ba76a3accb5aaa999c00509a11fd0793c\", Ref: \"main\"}: \"github:NixOS/nix/5233fd2ba76a3accb5aaa999c00509a11fd0793c\",\n\t\t{Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Dir: \"sub/dir\"}:                                               \"github:NixOS/nix?dir=sub%2Fdir\",\n\t\t{Type: TypeGitHub, Owner: \"NixOS\", Repo: \"nix\", Dir: \"sub/dir\", Host: \"example.com\"}:                          \"github:NixOS/nix?dir=sub%2Fdir&host=example.com\",\n\n\t\t// Git references.\n\t\t{Type: TypeGit, URL: \"git://example.com/repo/flake\"}:                                                                     \"git://example.com/repo/flake\",\n\t\t{Type: TypeGit, URL: \"https://example.com/repo/flake\"}:                                                                   \"git+https://example.com/repo/flake\",\n\t\t{Type: TypeGit, URL: \"ssh://git@example.com/repo/flake\"}:                                                                 \"git+ssh://git@example.com/repo/flake\",\n\t\t{Type: TypeGit, URL: \"git:/repo/flake\"}:                                                                                  \"git:/repo/flake\",\n\t\t{Type: TypeGit, URL: \"file:///repo/flake\"}:                                                                               \"git+file:///repo/flake\",\n\t\t{Type: TypeGit, URL: \"ssh://git@example.com/repo/flake\", Ref: \"my/ref\", Rev: \"e486d8d40e626a20e06d792db8cc5ac5aba9a5b4\"}: \"git+ssh://git@example.com/repo/flake?ref=my%2Fref&rev=e486d8d40e626a20e06d792db8cc5ac5aba9a5b4\",\n\t\t{Type: TypeGit, URL: \"ssh://git@example.com/repo/flake?dir=sub%2Fdir\", Ref: \"my/ref\", Rev: \"e486d8d40e626a20e06d792db8cc5ac5aba9a5b4\", Dir: \"sub/dir\"}: \"git+ssh://git@example.com/repo/flake?dir=sub%2Fdir&ref=my%2Fref&rev=e486d8d40e626a20e06d792db8cc5ac5aba9a5b4\",\n\t\t{Type: TypeGit, URL: \"git:repo/flake?dir=sub%2Fdir\", Ref: \"my/ref\", Rev: \"e486d8d40e626a20e06d792db8cc5ac5aba9a5b4\", Dir: \"sub/dir\"}:                   \"git:repo/flake?dir=sub%2Fdir&ref=my%2Fref&rev=e486d8d40e626a20e06d792db8cc5ac5aba9a5b4\",\n\n\t\t// Tarball references.\n\t\t{Type: TypeTarball, URL: \"http://example.com/flake\"}:                  \"tarball+http://example.com/flake\",\n\t\t{Type: TypeTarball, URL: \"https://example.com/flake\"}:                 \"tarball+https://example.com/flake\",\n\t\t{Type: TypeTarball, URL: \"https://example.com/flake\", Dir: \"sub/dir\"}: \"tarball+https://example.com/flake?dir=sub%2Fdir\",\n\t\t{Type: TypeTarball, URL: \"file:///home/flake\"}:                        \"tarball+file:///home/flake\",\n\n\t\t// File URL references.\n\t\t{Type: TypeFile, URL: \"file:///flake\"}:                                              \"file+file:///flake\",\n\t\t{Type: TypeFile, URL: \"http://example.com/flake\"}:                                   \"file+http://example.com/flake\",\n\t\t{Type: TypeFile, URL: \"http://example.com/flake.git\"}:                               \"file+http://example.com/flake.git\",\n\t\t{Type: TypeFile, URL: \"http://example.com/flake.tar?dir=sub%2Fdir\", Dir: \"sub/dir\"}: \"file+http://example.com/flake.tar?dir=sub%2Fdir\",\n\t}\n\n\tfor ref, want := range cases {\n\t\tt.Run(want, func(t *testing.T) {\n\t\t\tt.Logf(\"input = %#v\", ref)\n\t\t\tgot := ref.String()\n\t\t\tif got != want {\n\t\t\t\tt.Errorf(\"got %#q, want %#q\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseFlakeInstallable(t *testing.T) {\n\tcases := map[string]Installable{\n\t\t// Empty string is not a valid installable.\n\t\t\"\": {},\n\n\t\t// Not a path and not a valid URL.\n\t\t\"://bad/url\": {},\n\n\t\t\".\":             {Ref: Ref{Type: TypePath, Path: \".\"}},\n\t\t\".#app\":         {AttrPath: \"app\", Ref: Ref{Type: TypePath, Path: \".\"}},\n\t\t\".#app^out\":     {AttrPath: \"app\", Outputs: \"out\", Ref: Ref{Type: TypePath, Path: \".\"}},\n\t\t\".#app^out,lib\": {AttrPath: \"app\", Outputs: \"lib,out\", Ref: Ref{Type: TypePath, Path: \".\"}},\n\t\t\".#app^*\":       {AttrPath: \"app\", Outputs: \"*\", Ref: Ref{Type: TypePath, Path: \".\"}},\n\t\t\".^*\":           {Outputs: \"*\", Ref: Ref{Type: TypePath, Path: \".\"}},\n\n\t\t\"./flake\":             {Ref: Ref{Type: TypePath, Path: \"./flake\"}},\n\t\t\"./flake#app\":         {AttrPath: \"app\", Ref: Ref{Type: TypePath, Path: \"./flake\"}},\n\t\t\"./flake#app^out\":     {AttrPath: \"app\", Outputs: \"out\", Ref: Ref{Type: TypePath, Path: \"./flake\"}},\n\t\t\"./flake#app^out,lib\": {AttrPath: \"app\", Outputs: \"lib,out\", Ref: Ref{Type: TypePath, Path: \"./flake\"}},\n\t\t\"./flake^out\":         {Outputs: \"out\", Ref: Ref{Type: TypePath, Path: \"./flake\"}},\n\n\t\t\"indirect\":            {Ref: Ref{Type: TypeIndirect, ID: \"indirect\"}},\n\t\t\"nixpkgs#app\":         {AttrPath: \"app\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}},\n\t\t\"nixpkgs#app^out\":     {AttrPath: \"app\", Outputs: \"out\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}},\n\t\t\"nixpkgs#app^out,lib\": {AttrPath: \"app\", Outputs: \"lib,out\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}},\n\t\t\"nixpkgs^out\":         {Outputs: \"out\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}},\n\n\t\t\"%23#app\":       {AttrPath: \"app\", Ref: Ref{Type: TypeIndirect, ID: \"#\"}},\n\t\t\"./%23#app\":     {AttrPath: \"app\", Ref: Ref{Type: TypePath, Path: \"./#\"}},\n\t\t\"/%23#app\":      {AttrPath: \"app\", Ref: Ref{Type: TypePath, Path: \"/#\"}},\n\t\t\"path:/%23#app\": {AttrPath: \"app\", Ref: Ref{Type: TypePath, Path: \"/#\"}},\n\n\t\t\"http://example.com/%23.tar#app\":   {AttrPath: \"app\", Ref: Ref{Type: TypeTarball, URL: \"http://example.com/%23.tar\"}},\n\t\t\"file:///flake#app\":                {AttrPath: \"app\", Ref: Ref{Type: TypeFile, URL: \"file:///flake\"}},\n\t\t\"git://example.com/repo/flake#app\": {AttrPath: \"app\", Ref: Ref{Type: TypeGit, URL: \"git://example.com/repo/flake\"}},\n\t}\n\n\tfor installable, want := range cases {\n\t\tt.Run(installable, func(t *testing.T) {\n\t\t\tgot, err := ParseInstallable(installable)\n\t\t\tif diff := cmp.Diff(want, got); diff != \"\" {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"got error: %s\", err)\n\t\t\t\t}\n\t\t\t\tt.Errorf(\"wrong installable (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFlakeInstallableString(t *testing.T) {\n\tcases := map[Installable]string{\n\t\t{}: \"\",\n\n\t\t// No attribute or outputs.\n\t\t{Ref: Ref{Type: TypePath, Path: \".\"}}:          \"path:.\",\n\t\t{Ref: Ref{Type: TypePath, Path: \"./flake\"}}:    \"path:flake\",\n\t\t{Ref: Ref{Type: TypePath, Path: \"/flake\"}}:     \"path:/flake\",\n\t\t{Ref: Ref{Type: TypeIndirect, ID: \"indirect\"}}: \"flake:indirect\",\n\n\t\t// Attribute without outputs.\n\t\t{AttrPath: \"app\", Ref: Ref{Type: TypePath, Path: \".\"}}:            \"path:.#app\",\n\t\t{AttrPath: \"my#app\", Ref: Ref{Type: TypePath, Path: \".\"}}:         \"path:.#my%23app\",\n\t\t{AttrPath: \"app\", Ref: Ref{Type: TypePath, Path: \"./flake\"}}:      \"path:flake#app\",\n\t\t{AttrPath: \"my#app\", Ref: Ref{Type: TypePath, Path: \"./flake\"}}:   \"path:flake#my%23app\",\n\t\t{AttrPath: \"app\", Ref: Ref{Type: TypePath, Path: \"/flake\"}}:       \"path:/flake#app\",\n\t\t{AttrPath: \"my#app\", Ref: Ref{Type: TypePath, Path: \"/flake\"}}:    \"path:/flake#my%23app\",\n\t\t{AttrPath: \"app\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}}:    \"flake:nixpkgs#app\",\n\t\t{AttrPath: \"my#app\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}}: \"flake:nixpkgs#my%23app\",\n\n\t\t// Attribute with single output.\n\t\t{AttrPath: \"app\", Outputs: \"out\", Ref: Ref{Type: TypePath, Path: \".\"}}:         \"path:.#app^out\",\n\t\t{AttrPath: \"app\", Outputs: \"out\", Ref: Ref{Type: TypePath, Path: \"./flake\"}}:   \"path:flake#app^out\",\n\t\t{AttrPath: \"app\", Outputs: \"out\", Ref: Ref{Type: TypePath, Path: \"/flake\"}}:    \"path:/flake#app^out\",\n\t\t{AttrPath: \"app\", Outputs: \"out\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}}: \"flake:nixpkgs#app^out\",\n\n\t\t// Attribute with multiple outputs.\n\t\t{AttrPath: \"app\", Outputs: \"out,lib\", Ref: Ref{Type: TypePath, Path: \".\"}}:         \"path:.#app^lib,out\",\n\t\t{AttrPath: \"app\", Outputs: \"out,lib\", Ref: Ref{Type: TypePath, Path: \"./flake\"}}:   \"path:flake#app^lib,out\",\n\t\t{AttrPath: \"app\", Outputs: \"out,lib\", Ref: Ref{Type: TypePath, Path: \"/flake\"}}:    \"path:/flake#app^lib,out\",\n\t\t{AttrPath: \"app\", Outputs: \"out,lib\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}}: \"flake:nixpkgs#app^lib,out\",\n\n\t\t// Outputs are cleaned and sorted.\n\t\t{AttrPath: \"app\", Outputs: \"out,lib\", Ref: Ref{Type: TypePath, Path: \".\"}}:       \"path:.#app^lib,out\",\n\t\t{AttrPath: \"app\", Outputs: \"lib,out\", Ref: Ref{Type: TypePath, Path: \"./flake\"}}: \"path:flake#app^lib,out\",\n\t\t{AttrPath: \"app\", Outputs: \"out,,\", Ref: Ref{Type: TypePath, Path: \"/flake\"}}:    \"path:/flake#app^out\",\n\t\t{AttrPath: \"app\", Outputs: \",lib,out\", Ref: Ref{Type: TypePath, Path: \"/flake\"}}: \"path:/flake#app^lib,out\",\n\t\t{AttrPath: \"app\", Outputs: \",\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}}:     \"flake:nixpkgs#app\",\n\n\t\t// Wildcard replaces other outputs.\n\t\t{AttrPath: \"app\", Outputs: \"*\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}}:     \"flake:nixpkgs#app^*\",\n\t\t{AttrPath: \"app\", Outputs: \"out,*\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}}: \"flake:nixpkgs#app^*\",\n\t\t{AttrPath: \"app\", Outputs: \",*\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}}:    \"flake:nixpkgs#app^*\",\n\n\t\t// Outputs are not percent-encoded.\n\t\t{AttrPath: \"app\", Outputs: \"%\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}}:   \"flake:nixpkgs#app^%\",\n\t\t{AttrPath: \"app\", Outputs: \"/\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}}:   \"flake:nixpkgs#app^/\",\n\t\t{AttrPath: \"app\", Outputs: \"%2F\", Ref: Ref{Type: TypeIndirect, ID: \"nixpkgs\"}}: \"flake:nixpkgs#app^%2F\",\n\n\t\t// Missing or invalid fields.\n\t\t{AttrPath: \"app\", Ref: Ref{Type: TypeFile, URL: \"\"}}:     \"\",\n\t\t{AttrPath: \"app\", Ref: Ref{Type: TypeGit, URL: \"\"}}:      \"\",\n\t\t{AttrPath: \"app\", Ref: Ref{Type: TypeGitHub, Owner: \"\"}}: \"\",\n\t\t{AttrPath: \"app\", Ref: Ref{Type: TypeIndirect, ID: \"\"}}:  \"\",\n\t\t{AttrPath: \"app\", Ref: Ref{Type: TypePath, Path: \"\"}}:    \"\",\n\t\t{AttrPath: \"app\", Ref: Ref{Type: TypeTarball, URL: \"\"}}:  \"\",\n\t}\n\n\tfor installable, want := range cases {\n\t\tt.Run(want, func(t *testing.T) {\n\t\t\tt.Logf(\"input = %#v\", installable)\n\t\t\tgot := installable.String()\n\t\t\tif got != want {\n\t\t\t\tt.Errorf(\"got %#q, want %#q\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBuildQueryString(t *testing.T) {\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Error(\"wanted panic for odd-number of key-value parameters\")\n\t\t}\n\t}()\n\n\t// staticcheck impressively catches buildQueryString calls that have an\n\t// odd number of parameters. Build the slice in a convoluted way to\n\t// throw it off and suppress the warning (gopls doesn't have nolint\n\t// directives).\n\tvar elems []string\n\telems = append(elems, \"1\")\n\tappendQueryString(nil, elems...)\n}\n"
  },
  {
    "path": "nix/install.go",
    "content": "package nix\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n)\n\n// Installer downloads and installs Nix.\ntype Installer struct {\n\t// Path is the path to the Nix installer. If it's empty, Download and\n\t// Install will automatically download the installer and set Path to the\n\t// downloaded file before returning.\n\tPath string\n}\n\n// Download downloads the Nix installer without running it.\nfunc (i *Installer) Download(ctx context.Context) error {\n\tif i.Path != \"\" {\n\t\treturn fmt.Errorf(\"installer already downloaded: %s\", i.Path)\n\t}\n\n\tsystem := \"\"\n\tswitch runtime.GOARCH {\n\tcase \"amd64\":\n\t\tswitch runtime.GOOS {\n\t\tcase \"darwin\":\n\t\t\tsystem = \"x86_64-darwin\"\n\t\tcase \"linux\":\n\t\t\tsystem = \"x86_64-linux\"\n\t\t}\n\tcase \"arm64\":\n\t\tswitch runtime.GOOS {\n\t\tcase \"darwin\":\n\t\t\tsystem = \"aarch64-darwin\"\n\t\tcase \"linux\":\n\t\t\tsystem = \"aarch64-linux\"\n\t\t}\n\t}\n\n\turl := \"https://artifacts.nixos.org/experimental-installer/nix-installer-\" + system\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create request: %v\", err)\n\t}\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"do request: %v\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"status %s\", resp.Status)\n\t}\n\tinstaller, err := writeTempFile(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = os.Chmod(installer, 0o755)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"chmod 0755 installer: %v\", err)\n\t}\n\ti.Path = installer\n\treturn nil\n}\n\n// Run downloads and installs Nix.\nfunc (i *Installer) Run(ctx context.Context) error {\n\tif i.Path == \"\" {\n\t\terr := i.Download(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcmd := exec.CommandContext(ctx, i.Path, \"install\")\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcmd.Args = append(cmd.Args, \"macos\")\n\tcase \"linux\":\n\t\tcmd.Args = append(cmd.Args, \"linux\")\n\t\t_, err := os.Stat(\"/run/systemd/system\")\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t// Respect any env var settings from the user.\n\t\t\t_, ok := os.LookupEnv(\"NIX_INSTALLER_INIT\")\n\t\t\tif !ok {\n\t\t\t\tcmd.Args = append(cmd.Args, \"--init\", \"none\")\n\t\t\t}\n\t\t}\n\t}\n\tcmd.Args = append(cmd.Args, \"--no-confirm\")\n\tcmd.Cancel = func() error {\n\t\treturn cmd.Process.Signal(os.Interrupt)\n\t}\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"run installer: %v\", err)\n\t}\n\t_, _ = SourceProfile()\n\treturn nil\n}\n\nfunc writeTempFile(r io.Reader) (path string, err error) {\n\ttempFile, err := os.CreateTemp(\"\", \"devbox-nix-installer-\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"create temp file: %v\", err)\n\t}\n\n\t_, err = io.Copy(tempFile, r)\n\tcloseErr := tempFile.Close()\n\tif err == nil && closeErr != nil {\n\t\terr = fmt.Errorf(\"close temp file: %v\", closeErr)\n\t}\n\n\tif err != nil {\n\t\tos.Remove(tempFile.Name())\n\t\treturn \"\", err\n\t}\n\treturn tempFile.Name(), nil\n}\n"
  },
  {
    "path": "nix/nix.go",
    "content": "package nix\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"go.jetify.com/devbox/internal/redact\"\n\t\"golang.org/x/mod/semver\"\n)\n\n// Default is the default Nix installation.\nvar Default = &Nix{}\n\n// Command creates an arbitrary command using the Nix executable found in $PATH.\n// It's the same as calling [Nix.Command] on the default Nix installation.\nfunc Command(args ...any) *Cmd {\n\treturn Default.Command(args...)\n}\n\n// System calls [Nix.System] on the default Nix installation.\nfunc System() string {\n\treturn Default.System()\n}\n\n// Version calls [Nix.Version] on the default Nix installation.\nfunc Version() string {\n\treturn Default.Version()\n}\n\n// AtLeast reports if the default Nix installation's version is equal to or\n// newer than the given version. It returns false if it cannot determine the\n// Nix version.\nfunc AtLeast(version string) bool {\n\tinfo, _ := Default.Info()\n\treturn info.AtLeast(version)\n}\n\n// Nix provides an interface for interacting with Nix. The zero-value is valid\n// and uses the first Nix executable found in $PATH.\ntype Nix struct {\n\t// Path is the absolute path to the nix executable. If it is empty,\n\t// nix commands use the executable found in $PATH.\n\tPath     string\n\tlookPath atomic.Pointer[string]\n\n\t// ExtraArgs are command line arguments to pass to every Nix command.\n\tExtraArgs Args\n\n\tinfo     Info\n\tinfoErr  error\n\tinfoOnce sync.Once\n\n\t// Logger logs information at [slog.LevelDebug] about Nix command\n\t// starts and exits. If nil, it defaults to [slog.Default].\n\tLogger *slog.Logger\n}\n\n// resolvePath resolves the path to the Nix executable. It returns n.Path if it\n// is non-empty and a valid executable. Otherwise it searches for a nix\n// executable in $PATH and common installation directories.\nfunc (n *Nix) resolvePath() (string, error) {\n\tif n.Path != \"\" {\n\t\treturn exec.LookPath(n.Path) // verify it's an executable.\n\t}\n\n\t// Re-use the cached path if we've already found Nix before.\n\tcached := n.lookPath.Load()\n\tif cached != nil && *cached != \"\" {\n\t\treturn *cached, nil\n\t}\n\n\t_, _ = SourceProfile()\n\tpath, pathErr := exec.LookPath(\"nix\")\n\tif pathErr == nil {\n\t\tn.lookPath.Store(&path)\n\t\treturn path, nil\n\t}\n\n\ttry := []string{\n\t\t\"/nix/var/nix/profiles/default/bin/nix\",\n\t\t\"/run/current-system/sw/bin\",\n\t}\n\tfor _, path := range try {\n\t\tstat, err := os.Stat(path)\n\t\tif err == nil {\n\t\t\t// Is it executable and not a directory?\n\t\t\tm := stat.Mode()\n\t\t\tif !m.IsDir() && m.Perm()&0o111 != 0 {\n\t\t\t\tn.lookPath.Store(&path)\n\t\t\t\treturn path, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", pathErr\n}\n\nfunc (n *Nix) logger() *slog.Logger {\n\tif n.Logger == nil {\n\t\treturn slog.Default()\n\t}\n\treturn n.Logger\n}\n\n// System returns the system from [Nix.Info] or an empty string if there was an\n// error.\nfunc (n *Nix) System() string {\n\tinfo, _ := n.Info()\n\treturn info.System\n}\n\n// Version returns the version from [Nix.Info] or an empty string if there was\n// an error.\nfunc (n *Nix) Version() string {\n\tinfo, _ := n.Info()\n\treturn info.Version\n}\n\n// Info returns Nix version information. It caches the result after the first\n// call, which means it won't reflect any configuration changes to Nix. Create a\n// new Nix instance to retrieve uncached information.\nfunc (n *Nix) Info() (Info, error) {\n\t// Create the command first, which will catch any errors finding the Nix\n\t// executable outside of the once. This allows us to retry after\n\t// installing Nix.\n\tcmd := n.Command(\"--version\", \"--debug\")\n\tif cmd.err != nil {\n\t\treturn Info{}, cmd.err\n\t}\n\n\tn.infoOnce.Do(func() {\n\t\tout, err := cmd.Output(context.Background())\n\t\tif err != nil {\n\t\t\tvar exitErr *exec.ExitError\n\t\t\tif errors.As(err, &exitErr) && len(exitErr.Stderr) != 0 {\n\t\t\t\tn.infoErr = redact.Errorf(\"nix command: %s: %q: %v\", redact.Safe(cmd), exitErr.Stderr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tn.infoErr = redact.Errorf(\"nix command: %s: %v\", redact.Safe(cmd), err)\n\t\t\treturn\n\t\t}\n\t\tn.info, n.infoErr = parseInfo(out)\n\t})\n\treturn n.info, n.infoErr\n}\n\n// All major Nix versions supported by Devbox.\nconst (\n\tVersion2_12 = \"2.12.0\"\n\tVersion2_13 = \"2.13.0\"\n\tVersion2_14 = \"2.14.0\"\n\tVersion2_15 = \"2.15.0\"\n\tVersion2_16 = \"2.16.0\"\n\tVersion2_17 = \"2.17.0\"\n\tVersion2_18 = \"2.18.0\"\n\tVersion2_19 = \"2.19.0\"\n\tVersion2_20 = \"2.20.0\"\n\tVersion2_21 = \"2.21.0\"\n\tVersion2_22 = \"2.22.0\"\n\tVersion2_23 = \"2.23.0\"\n\tVersion2_24 = \"2.24.0\"\n\tVersion2_25 = \"2.25.0\"\n\n\tMinVersion = Version2_18\n)\n\n// versionRegexp matches the first line of \"nix --version\" output.\n//\n// The semantic component is sourced from <https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string>.\n// It's been modified to tolerate Nix prerelease versions, which don't have a\n// hyphen before the prerelease component and contain underscores.\nvar versionRegexp = regexp.MustCompile(`^(.+) \\(.+\\) ((?P<major>0|[1-9]\\d*)\\.(?P<minor>0|[1-9]\\d*)\\.(?P<patch>0|[1-9]\\d*)(?:(?:-|pre)(?P<prerelease>(?:0|[1-9]\\d*|\\d*[_a-zA-Z-][_0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[_a-zA-Z-][_0-9a-zA-Z-]*))*))?(?:\\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)$`)\n\n// preReleaseRegexp matches Nix prerelease version strings, which are not valid\n// semvers.\nvar preReleaseRegexp = regexp.MustCompile(`pre(?P<date>[0-9]+)_(?P<commit>[a-f0-9]{4,40})$`)\n\n// Info contains information about a Nix installation.\ntype Info struct {\n\t// Name identifies the Nix implementation. It is usually \"nix\" but may\n\t// also be a fork like \"lix\".\n\tName string\n\n\t// Version is the semantic Nix version string.\n\tVersion string\n\n\t// System is the Nix system tuple. It follows the pattern <arch>-<os>\n\t// and does not use the same values as GOOS or GOARCH. Note that the Nix\n\t// system is configurable and may not represent the actual operating\n\t// system or architecture.\n\tSystem string\n\n\t// ExtraSystems are other systems that the current machine supports.\n\t// Usually set by the extra-platforms setting in nix.conf.\n\tExtraSystems []string\n\n\t// Features are the capabilities that the Nix binary was compiled with.\n\tFeatures []string\n\n\t// SystemConfig is the path to the Nix system configuration file,\n\t// usually /etc/nix/nix.conf.\n\tSystemConfig string\n\n\t// UserConfigs is a list of paths to the user's Nix configuration files.\n\tUserConfigs []string\n\n\t// StoreDir is the path to the Nix store directory, usually /nix/store.\n\tStoreDir string\n\n\t// StateDir is the path to the Nix state directory, usually\n\t// /nix/var/nix.\n\tStateDir string\n\n\t// DataDir is the path to the Nix data directory, usually somewhere\n\t// within the Nix store. This field is empty for Nix versions <= 2.12.\n\tDataDir string\n}\n\nfunc parseInfo(data []byte) (Info, error) {\n\t// Example nix --version --debug output from Nix versions 2.12 to 2.21.\n\t// Version 2.12 omits the data directory, but they're otherwise\n\t// identical.\n\t//\n\t// See https://github.com/NixOS/nix/blob/5b9cb8b3722b85191ee8cce8f0993170e0fc234c/src/libmain/shared.cc#L284-L305\n\t//\n\t// nix (Nix) 2.21.2\n\t// System type: aarch64-darwin\n\t// Additional system types: x86_64-darwin\n\t// Features: gc, signed-caches\n\t// System configuration file: /etc/nix/nix.conf\n\t// User configuration files: /Users/nobody/.config/nix/nix.conf:/etc/xdg/nix/nix.conf\n\t// Store directory: /nix/store\n\t// State directory: /nix/var/nix\n\t// Data directory: /nix/store/m0ns07v8by0458yp6k30rfq1rs3kaz6g-nix-2.21.2/share\n\n\tinfo := Info{}\n\tif len(data) == 0 {\n\t\treturn info, redact.Errorf(\"empty nix --version output\")\n\t}\n\n\tlines := strings.Split(string(data), \"\\n\")\n\tmatches := versionRegexp.FindStringSubmatch(lines[0])\n\tif len(matches) < 3 {\n\t\treturn info, redact.Errorf(\"parse nix version: %s\", redact.Safe(lines[0]))\n\t}\n\tinfo.Name = matches[1]\n\tinfo.Version = matches[2]\n\tfor _, line := range lines {\n\t\tname, value, found := strings.Cut(line, \": \")\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch name {\n\t\tcase \"System type\":\n\t\t\tinfo.System = value\n\t\tcase \"Additional system types\":\n\t\t\tinfo.ExtraSystems = strings.Split(value, \", \")\n\t\tcase \"Features\":\n\t\t\tinfo.Features = strings.Split(value, \", \")\n\t\tcase \"System configuration file\":\n\t\t\tinfo.SystemConfig = value\n\t\tcase \"User configuration files\":\n\t\t\tinfo.UserConfigs = strings.Split(value, \":\")\n\t\tcase \"Store directory\":\n\t\t\tinfo.StoreDir = value\n\t\tcase \"State directory\":\n\t\t\tinfo.StateDir = value\n\t\tcase \"Data directory\":\n\t\t\tinfo.DataDir = value\n\t\t}\n\t}\n\treturn info, nil\n}\n\n// AtLeast returns true if i.Version is >= version per semantic versioning. It\n// always returns false if i.Version is empty or invalid, such as when the\n// current Nix version cannot be parsed. It panics if version is an invalid\n// semver.\nfunc (i Info) AtLeast(version string) bool {\n\tif !strings.HasPrefix(version, \"v\") {\n\t\tversion = \"v\" + version\n\t}\n\tif !semver.IsValid(version) {\n\t\tpanic(fmt.Sprintf(\"nix.atLeast: invalid version %q\", version[1:]))\n\t}\n\tif semver.IsValid(\"v\" + i.Version) {\n\t\treturn semver.Compare(\"v\"+i.Version, version) >= 0\n\t}\n\n\t// If the version isn't a valid semver, check to see if it's a\n\t// prerelease (e.g., 2.23.0pre20240526_7de033d6) and coerce it to a\n\t// valid version (2.23.0-pre.20240526+7de033d6) so we can compare it.\n\tprerelease := preReleaseRegexp.ReplaceAllString(i.Version, \"-pre.$date+$commit\")\n\treturn semver.Compare(\"v\"+prerelease, version) >= 0\n}\n\n// sourceProfileMutex guards against multiple goroutines attempting to source\n// the Nix profile scripts concurrently.\nvar sourceProfileMutex sync.Mutex\n\n// SourceProfile adds environment variables from the Nix profile shell scripts\n// to the current process's environment. This ensures that PATH contains the nix\n// bin directory and that NIX_PROFILES and NIX_SSL_CERT_FILE are set.\n//\n// For properly configured Nix installations, the user's login shell handles\n// sourcing the profile and SourceProfile has no effect.\nfunc SourceProfile() (sourced bool, err error) {\n\tif profileSourced() {\n\t\treturn false, nil\n\t}\n\tsourceProfileMutex.Lock()\n\tdefer sourceProfileMutex.Unlock()\n\n\tif profileSourced() {\n\t\treturn false, nil\n\t}\n\n\tshell := os.Getenv(\"SHELL\")\n\tif shell == \"\" {\n\t\tshell = \"sh\"\n\t}\n\tshell, _ = exec.LookPath(\"sh\")\n\tif shell == \"\" {\n\t\tshell = \"/bin/sh\"\n\t}\n\n\ttrySource := func(path string) error {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)\n\t\tdefer cancel()\n\n\t\twantEnv := map[string]bool{\n\t\t\t\"NIX_PROFILES\":              true,\n\t\t\t\"NIX_SSL_CERT_FILE\":         true,\n\t\t\t\"PATH\":                      true,\n\t\t\t\"XDG_DATA_DIRS\":             true,\n\t\t\t\"__ETC_PROFILE_NIX_SOURCED\": true,\n\t\t}\n\t\tscript := fmt.Sprintf(\". \\\"%s\\\"\\n\", path)\n\t\tfor name := range wantEnv {\n\t\t\tscript += fmt.Sprintf(\"echo %s=\\\"$%[1]s\\\"\\n\", name)\n\t\t}\n\n\t\tcmd := exec.CommandContext(ctx, shell, \"-e\", \"-c\", script)\n\t\tstdout, err := cmd.Output()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, line := range strings.Split(string(stdout), \"\\n\") {\n\t\t\tname, value, ok := strings.Cut(line, \"=\")\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif wantEnv[name] {\n\t\t\t\terr = os.Setenv(name, value)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdelete(wantEnv, name)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor _, path := range profilePaths() {\n\t\terr = trySource(path)\n\t\tif err == nil {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, fmt.Errorf(\"unable to source Nix profile\")\n}\n\n// profileSourced checks if the Nix profile shell scripts (such as\n// /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh) have already been\n// successfully sourced.\nfunc profileSourced() bool {\n\t// Check if we're already in a Nix environment. Use NIX_PROFILES instead\n\t// of __ETC_PROFILE_NIX_SOURCED because it's set for single-user\n\t// installs and on NixOS (whereas __ETC_PROFILE_NIX_SOURCED is not).\n\t_, ok := os.LookupEnv(\"NIX_PROFILES\")\n\treturn ok\n}\n\n// profilePaths returns the paths where the Nix profile shell scripts might be.\n// None of the paths are guaranteed to be readable or even exist.\nfunc profilePaths() []string {\n\t// os.UserHomeDir only checks $HOME, user.Current reads /etc/passwd or\n\t// uses libc. This can help when running in an isolated environment\n\t// where $HOME isn't set.\n\thome, _ := os.UserHomeDir()\n\tif home == \"\" {\n\t\tif u, err := user.Current(); err == nil {\n\t\t\thome = u.HomeDir\n\t\t}\n\t}\n\tif home == \"\" {\n\t\t// Might as well check the root home directory if we've got\n\t\t// nothing else.\n\t\thome = \"/root\"\n\t}\n\txdgState := os.Getenv(\"XDG_STATE_HOME\")\n\tif xdgState == \"\" {\n\t\txdgState = filepath.Join(home, \".local/state\")\n\t}\n\n\tdirs := make([]string, 0, 5)\n\tif nixExe, err := exec.LookPath(\"nix\"); err == nil {\n\t\tdirs = append(dirs, filepath.Clean(nixExe+\"/../../etc/profile.d\"))\n\t}\n\tif !slices.Contains(dirs, \"/nix/var/nix/profiles/default/etc/profile.d\") {\n\t\tdirs = append(dirs, \"/nix/var/nix/profiles/default/etc/profile.d\")\n\t}\n\tdirs = append(dirs,\n\t\tfilepath.Join(home, \".nix-profile/etc/profile.d\"),\n\t\tfilepath.Join(xdgState, \"nix/profile/etc/profile.d\"),\n\t\tfilepath.Join(xdgState, \"nix/profiles/profile/etc/profile.d\"),\n\t)\n\n\t// Try sourcing scripts in the following order:\n\t//\n\t//  1. nix-daemon.sh: because nix.sh is a no-op when $USER isn't set\n\t//     (this happens in containers).\n\t//  2. nix-daemon.fish: same, but for fish users.\n\t//  3. nix.sh, nix.fish: for old single-user installs.\n\tfiles := make([]string, 0, len(dirs)*4)\n\tfor _, dir := range dirs {\n\t\tfiles = append(files, filepath.Join(dir, \"nix-daemon.sh\"))\n\t}\n\tfor _, dir := range dirs {\n\t\tfiles = append(files, filepath.Join(dir, \"nix-daemon.fish\"))\n\t}\n\tfor _, dir := range dirs {\n\t\tfiles = append(files, filepath.Join(dir, \"nix.sh\"))\n\t}\n\tfor _, dir := range dirs {\n\t\tfiles = append(files, filepath.Join(dir, \"nix.fish\"))\n\t}\n\treturn files\n}\n"
  },
  {
    "path": "nix/nix_test.go",
    "content": "//nolint:dupl\npackage nix\n\nimport (\n\t\"slices\"\n\t\"testing\"\n)\n\nfunc TestParseVersionInfo(t *testing.T) {\n\traw := `nix (Nix) 2.21.2\nSystem type: aarch64-darwin\nAdditional system types: x86_64-darwin\nFeatures: gc, signed-caches\nSystem configuration file: /etc/nix/nix.conf\nUser configuration files: /Users/nobody/.config/nix/nix.conf:/etc/xdg/nix/nix.conf\nStore directory: /nix/store\nState directory: /nix/var/nix\nData directory: /nix/store/m0ns07v8by0458yp6k30rfq1rs3kaz6g-nix-2.21.2/share\n`\n\n\tinfo, err := parseInfo([]byte(raw))\n\tif err != nil {\n\t\tt.Error(\"got parse error:\", err)\n\t}\n\tif got, want := info.Name, \"nix\"; got != want {\n\t\tt.Errorf(\"got Name = %q, want %q\", got, want)\n\t}\n\tif got, want := info.Version, \"2.21.2\"; got != want {\n\t\tt.Errorf(\"got Version = %q, want %q\", got, want)\n\t}\n\tif got, want := info.System, \"aarch64-darwin\"; got != want {\n\t\tt.Errorf(\"got System = %q, want %q\", got, want)\n\t}\n\tif got, want := info.ExtraSystems, []string{\"x86_64-darwin\"}; !slices.Equal(got, want) {\n\t\tt.Errorf(\"got ExtraSystems = %q, want %q\", got, want)\n\t}\n\tif got, want := info.Features, []string{\"gc\", \"signed-caches\"}; !slices.Equal(got, want) {\n\t\tt.Errorf(\"got Features = %q, want %q\", got, want)\n\t}\n\tif got, want := info.SystemConfig, \"/etc/nix/nix.conf\"; got != want {\n\t\tt.Errorf(\"got SystemConfig = %q, want %q\", got, want)\n\t}\n\tif got, want := info.UserConfigs, []string{\"/Users/nobody/.config/nix/nix.conf\", \"/etc/xdg/nix/nix.conf\"}; !slices.Equal(got, want) {\n\t\tt.Errorf(\"got UserConfigs = %q, want %q\", got, want)\n\t}\n\tif got, want := info.StoreDir, \"/nix/store\"; got != want {\n\t\tt.Errorf(\"got StoreDir = %q, want %q\", got, want)\n\t}\n\tif got, want := info.StateDir, \"/nix/var/nix\"; got != want {\n\t\tt.Errorf(\"got StateDir = %q, want %q\", got, want)\n\t}\n\tif got, want := info.DataDir, \"/nix/store/m0ns07v8by0458yp6k30rfq1rs3kaz6g-nix-2.21.2/share\"; got != want {\n\t\tt.Errorf(\"got DataDir = %q, want %q\", got, want)\n\t}\n}\n\nfunc TestParseLixVersionInfo(t *testing.T) {\n\traw := `nix (Lix, like Nix) 2.90.0-beta.1\nSystem type: aarch64-darwin\nAdditional system types: x86_64-darwin\nFeatures: gc, signed-caches\nSystem configuration file: /etc/nix/nix.conf\nUser configuration files: /Users/nobody/.config/nix/nix.conf:/etc/xdg/nix/nix.conf\nStore directory: /nix/store\nState directory: /nix/var/nix\nData directory: /nix/store/12asl5a17ffj78njcy2fj31v59rdmanx-lix-2.90-beta.1/share\n`\n\n\tinfo, err := parseInfo([]byte(raw))\n\tif err != nil {\n\t\tt.Error(\"got parse error:\", err)\n\t}\n\tif got, want := info.Name, \"nix\"; got != want {\n\t\tt.Errorf(\"got Name = %q, want %q\", got, want)\n\t}\n\tif got, want := info.Version, \"2.90.0-beta.1\"; got != want {\n\t\tt.Errorf(\"got Version = %q, want %q\", got, want)\n\t}\n\tif got, want := info.System, \"aarch64-darwin\"; got != want {\n\t\tt.Errorf(\"got System = %q, want %q\", got, want)\n\t}\n\tif got, want := info.ExtraSystems, []string{\"x86_64-darwin\"}; !slices.Equal(got, want) {\n\t\tt.Errorf(\"got ExtraSystems = %q, want %q\", got, want)\n\t}\n\tif got, want := info.Features, []string{\"gc\", \"signed-caches\"}; !slices.Equal(got, want) {\n\t\tt.Errorf(\"got Features = %q, want %q\", got, want)\n\t}\n\tif got, want := info.SystemConfig, \"/etc/nix/nix.conf\"; got != want {\n\t\tt.Errorf(\"got SystemConfig = %q, want %q\", got, want)\n\t}\n\tif got, want := info.UserConfigs, []string{\"/Users/nobody/.config/nix/nix.conf\", \"/etc/xdg/nix/nix.conf\"}; !slices.Equal(got, want) {\n\t\tt.Errorf(\"got UserConfigs = %q, want %q\", got, want)\n\t}\n\tif got, want := info.StoreDir, \"/nix/store\"; got != want {\n\t\tt.Errorf(\"got StoreDir = %q, want %q\", got, want)\n\t}\n\tif got, want := info.StateDir, \"/nix/var/nix\"; got != want {\n\t\tt.Errorf(\"got StateDir = %q, want %q\", got, want)\n\t}\n\tif got, want := info.DataDir, \"/nix/store/12asl5a17ffj78njcy2fj31v59rdmanx-lix-2.90-beta.1/share\"; got != want {\n\t\tt.Errorf(\"got DataDir = %q, want %q\", got, want)\n\t}\n}\n\nfunc TestParseVersionInfoShort(t *testing.T) {\n\tcases := []struct {\n\t\tin      string\n\t\tname    string\n\t\tversion string\n\t}{\n\t\t{\"nix (Nix) 2.21.2\", \"nix\", \"2.21.2\"},\n\t\t{\"nix (Nix) 2.23.0pre20240526_7de033d6\", \"nix\", \"2.23.0pre20240526_7de033d6\"},\n\t\t{\"command (Nix) name (Nix) 2.21.2\", \"command (Nix) name\", \"2.21.2\"},\n\t\t{\"nix (Lix, like Nix) 2.90.0-beta.1\", \"nix\", \"2.90.0-beta.1\"},\n\t}\n\n\tfor _, tt := range cases {\n\t\tt.Run(tt.in, func(t *testing.T) {\n\t\t\tgot, err := parseInfo([]byte(tt.in))\n\t\t\tif err != nil {\n\t\t\t\tt.Error(\"got parse error:\", err)\n\t\t\t}\n\t\t\tif got.Name != tt.name {\n\t\t\t\tt.Errorf(\"got Name = %q, want %q\", got.Name, tt.name)\n\t\t\t}\n\t\t\tif got.Version != tt.version {\n\t\t\t\tt.Errorf(\"got Version = %q, want %q\", got.Version, tt.version)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseVersionInfoError(t *testing.T) {\n\tt.Run(\"NilOutput\", func(t *testing.T) {\n\t\t_, err := parseInfo(nil)\n\t\tif err == nil {\n\t\t\tt.Error(\"want non-nil error\")\n\t\t}\n\t})\n\tt.Run(\"EmptyOutput\", func(t *testing.T) {\n\t\t_, err := parseInfo([]byte{})\n\t\tif err == nil {\n\t\t\tt.Error(\"want non-nil error\")\n\t\t}\n\t})\n\tt.Run(\"MissingVersionOutput\", func(t *testing.T) {\n\t\t_, err := parseInfo([]byte(\"nix output without a version\"))\n\t\tif err == nil {\n\t\t\tt.Error(\"want non-nil error\")\n\t\t}\n\t})\n}\n\nfunc TestVersionInfoAtLeast(t *testing.T) {\n\tinfo := Info{}\n\tif info.AtLeast(Version2_12) {\n\t\tt.Errorf(\"got empty current version >= %s\", Version2_12)\n\t}\n\n\tinfo.Version = Version2_13\n\tif !info.AtLeast(Version2_12) {\n\t\tt.Errorf(\"got %s < %s\", info.Version, Version2_12)\n\t}\n\tif !info.AtLeast(Version2_13) {\n\t\tt.Errorf(\"got %s < %s\", info.Version, Version2_13)\n\t}\n\tif info.AtLeast(Version2_14) {\n\t\tt.Errorf(\"got %s >= %s\", info.Version, Version2_14)\n\t}\n\n\t// https://github.com/jetify-com/devbox/issues/2128\n\tinfo.Version = \"2.23.0pre20240526_7de033d6\"\n\tif !info.AtLeast(Version2_12) {\n\t\tt.Errorf(\"got %s < %s\", info.Version, Version2_12)\n\t}\n\tif info.AtLeast(\"2.23.0\") {\n\t\tt.Errorf(\"got %s > %s\", info.Version, \"2.23.0\")\n\t}\n\tif info.AtLeast(\"2.24.0\") {\n\t\tt.Errorf(\"got %s > %s\", info.Version, \"2.24.0\")\n\t}\n\tif info.AtLeast(\"2.23.0-pre.99999999\") {\n\t\tt.Errorf(\"got %s > %s\", info.Version, \"2.23.0-pre.99999999\")\n\t}\n\tif !info.AtLeast(\"2.23.0-pre.1\") {\n\t\tt.Errorf(\"got %s < %s\", info.Version, \"2.23.0-pre.1\")\n\t}\n\n\tt.Run(\"ArgEmptyPanic\", func(t *testing.T) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\tt.Error(\"want panic for empty version\")\n\t\t\t}\n\t\t}()\n\t\tinfo.AtLeast(\"\")\n\t})\n\tt.Run(\"ArgInvalidPanic\", func(t *testing.T) {\n\t\tv := \"notasemver\"\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\tt.Errorf(\"want panic for invalid version %q\", v)\n\t\t\t}\n\t\t}()\n\t\tinfo.AtLeast(v)\n\t})\n}\n"
  },
  {
    "path": "pkg/autodetect/autodetect.go",
    "content": "package autodetect\n\nimport (\n\t\"context\"\n\n\t\"go.jetify.com/devbox/internal/devconfig\"\n\t\"go.jetify.com/devbox/pkg/autodetect/detector\"\n)\n\nfunc InitConfig(ctx context.Context, path string) error {\n\tconfig, err := devconfig.Init(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = populateConfig(ctx, path, config); err != nil {\n\t\treturn err\n\t}\n\n\treturn config.Root.Save()\n}\n\nfunc DryRun(ctx context.Context, path string) ([]byte, error) {\n\tconfig := devconfig.DefaultConfig()\n\tif err := populateConfig(ctx, path, config); err != nil {\n\t\treturn nil, err\n\t}\n\treturn config.Root.Bytes(), nil\n}\n\nfunc populateConfig(ctx context.Context, path string, config *devconfig.Config) error {\n\tpkgs, err := packages(ctx, path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, pkg := range pkgs {\n\t\tconfig.PackageMutator().Add(pkg)\n\t}\n\tenv, err := env(ctx, path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tconfig.Root.SetEnv(env)\n\treturn nil\n}\n\nfunc detectors(path string) []detector.Detector {\n\treturn []detector.Detector{\n\t\t&detector.GoDetector{Root: path},\n\t\t&detector.NodeJSDetector{Root: path},\n\t\t&detector.PHPDetector{Root: path},\n\t\t&detector.PoetryDetector{Root: path},\n\t\t&detector.PythonDetector{Root: path},\n\t}\n}\n\nfunc packages(ctx context.Context, path string) ([]string, error) {\n\tmostRelevantDetector, err := relevantDetector(path)\n\tif err != nil || mostRelevantDetector == nil {\n\t\treturn nil, err\n\t}\n\treturn mostRelevantDetector.Packages(ctx)\n}\n\nfunc env(ctx context.Context, path string) (map[string]string, error) {\n\tmostRelevantDetector, err := relevantDetector(path)\n\tif err != nil || mostRelevantDetector == nil {\n\t\treturn nil, err\n\t}\n\treturn mostRelevantDetector.Env(ctx)\n}\n\n// relevantDetector returns the most relevant detector for the given path.\n// We could modify this to return a list of detectors and their scores or\n// possibly grouped detectors by category (e.g. python, server, etc.)\nfunc relevantDetector(path string) (detector.Detector, error) {\n\trelevantScore := 0.0\n\tvar mostRelevantDetector detector.Detector\n\tfor _, detector := range detectors(path) {\n\t\tif d, ok := detector.(interface {\n\t\t\tInit() error\n\t\t}); ok {\n\t\t\tif err := d.Init(); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tscore, err := detector.Relevance(path)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif score > relevantScore {\n\t\t\trelevantScore = score\n\t\t\tmostRelevantDetector = detector\n\t\t}\n\t}\n\treturn mostRelevantDetector, nil\n}\n"
  },
  {
    "path": "pkg/autodetect/detector/detector.go",
    "content": "package detector\n\nimport \"context\"\n\ntype Detector interface {\n\tRelevance(path string) (float64, error)\n\tPackages(ctx context.Context) ([]string, error)\n\tEnv(ctx context.Context) (map[string]string, error)\n}\n"
  },
  {
    "path": "pkg/autodetect/detector/go.go",
    "content": "package detector\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n)\n\ntype GoDetector struct {\n\tRoot string\n}\n\nvar _ Detector = &GoDetector{}\n\nfunc (d *GoDetector) Relevance(path string) (float64, error) {\n\tgoModPath := filepath.Join(d.Root, \"go.mod\")\n\t_, err := os.Stat(goModPath)\n\tif err == nil {\n\t\treturn 1.0, nil\n\t}\n\tif os.IsNotExist(err) {\n\t\treturn 0, nil\n\t}\n\treturn 0, err\n}\n\nfunc (d *GoDetector) Packages(ctx context.Context) ([]string, error) {\n\tgoModPath := filepath.Join(d.Root, \"go.mod\")\n\tgoModContent, err := os.ReadFile(goModPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Parse the Go version from go.mod\n\tgoVersion := parseGoVersion(string(goModContent))\n\tgoVersion = determineBestVersion(ctx, \"go\", goVersion)\n\treturn []string{\"go@\" + goVersion}, nil\n}\n\nfunc (d *GoDetector) Env(ctx context.Context) (map[string]string, error) {\n\treturn map[string]string{}, nil\n}\n\nfunc parseGoVersion(goModContent string) string {\n\t// Use a regular expression to find the Go version directive\n\tre := regexp.MustCompile(`(?m)^go\\s+(\\d+\\.\\d+(\\.\\d+)?)`)\n\tmatch := re.FindStringSubmatch(goModContent)\n\n\tif len(match) >= 2 {\n\t\treturn match[1]\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/autodetect/detector/go_test.go",
    "content": "package detector\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGoDetectorRelevance(t *testing.T) {\n\ttempDir := t.TempDir()\n\tdetector := &GoDetector{Root: tempDir}\n\n\tt.Run(\"No go.mod file\", func(t *testing.T) {\n\t\trelevance, err := detector.Relevance(tempDir)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0.0, relevance)\n\t})\n\n\tt.Run(\"With go.mod file\", func(t *testing.T) {\n\t\terr := os.WriteFile(filepath.Join(tempDir, \"go.mod\"), []byte(\"module example.com\"), 0o644)\n\t\tassert.NoError(t, err)\n\n\t\trelevance, err := detector.Relevance(tempDir)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1.0, relevance)\n\t})\n}\n\nfunc TestGoDetectorPackages(t *testing.T) {\n\ttempDir := t.TempDir()\n\tdetector := &GoDetector{Root: tempDir}\n\n\tt.Run(\"No go.mod file\", func(t *testing.T) {\n\t\tpackages, err := detector.Packages(t.Context())\n\t\tassert.Error(t, err)\n\t\tassert.Nil(t, packages)\n\t})\n\n\tt.Run(\"With go.mod file and no version\", func(t *testing.T) {\n\t\terr := os.WriteFile(filepath.Join(tempDir, \"go.mod\"), []byte(\"module example.com\"), 0o644)\n\t\tassert.NoError(t, err)\n\n\t\tpackages, err := detector.Packages(t.Context())\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, []string{\"go@latest\"}, packages)\n\t})\n\n\tt.Run(\"With go.mod file and specific version\", func(t *testing.T) {\n\t\tgoModContent := `\nmodule example.com\n\ngo 1.18\n`\n\t\terr := os.WriteFile(filepath.Join(tempDir, \"go.mod\"), []byte(goModContent), 0o644)\n\t\tassert.NoError(t, err)\n\n\t\tpackages, err := detector.Packages(t.Context())\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, []string{\"go@1.18\"}, packages)\n\t})\n}\n\nfunc TestParseGoVersion(t *testing.T) {\n\ttestCases := []struct {\n\t\tname     string\n\t\tcontent  string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"No version\",\n\t\t\tcontent:  \"module example.com\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"With version\",\n\t\t\tcontent: `\nmodule example.com\n\ngo 1.18\n`,\n\t\t\texpected: \"1.18\",\n\t\t},\n\t\t{\n\t\t\tname: \"With patch version\",\n\t\t\tcontent: `\nmodule example.com\n\ngo 1.18.3\n`,\n\t\t\texpected: \"1.18.3\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tversion := parseGoVersion(tc.content)\n\t\t\tassert.Equal(t, tc.expected, version)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/autodetect/detector/nodejs.go",
    "content": "package detector\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n)\n\ntype packageJSON struct {\n\tEngines struct {\n\t\tNode string `json:\"node\"`\n\t} `json:\"engines\"`\n}\n\ntype NodeJSDetector struct {\n\tRoot        string\n\tpackageJSON *packageJSON\n}\n\nvar _ Detector = &NodeJSDetector{}\n\nfunc (d *NodeJSDetector) Init() error {\n\tpkgJSON, err := loadPackageJSON(d.Root)\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn err\n\t}\n\td.packageJSON = pkgJSON\n\treturn nil\n}\n\nfunc (d *NodeJSDetector) Relevance(path string) (float64, error) {\n\tif d.packageJSON == nil {\n\t\treturn 0, nil\n\t}\n\treturn 1, nil\n}\n\nfunc (d *NodeJSDetector) Packages(ctx context.Context) ([]string, error) {\n\treturn []string{\"nodejs@\" + d.nodeVersion(ctx)}, nil\n}\n\nfunc (d *NodeJSDetector) Env(ctx context.Context) (map[string]string, error) {\n\treturn map[string]string{\"DEVBOX_COREPACK_ENABLED\": \"1\"}, nil\n}\n\nfunc (d *NodeJSDetector) nodeVersion(ctx context.Context) string {\n\tif d.packageJSON == nil || d.packageJSON.Engines.Node == \"\" {\n\t\treturn \"latest\" // Default to latest if not specified\n\t}\n\n\t// Remove any non-semver characters (e.g. \">=\", \"^\", etc)\n\tversion := \"latest\"\n\tsemverRegex := regexp.MustCompile(`\\d+(\\.\\d+)?(\\.\\d+)?`)\n\tif match := semverRegex.FindString(d.packageJSON.Engines.Node); match != \"\" {\n\t\tversion = match\n\t}\n\n\treturn determineBestVersion(ctx, \"nodejs\", version)\n}\n\nfunc loadPackageJSON(root string) (*packageJSON, error) {\n\tpath := filepath.Join(root, \"package.json\")\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar pkg packageJSON\n\tif err := json.Unmarshal(data, &pkg); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pkg, nil\n}\n"
  },
  {
    "path": "pkg/autodetect/detector/nodejs_test.go",
    "content": "package detector\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNodeJSDetector_Relevance(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\tfs               fstest.MapFS\n\t\texpected         float64\n\t\texpectedPackages []string\n\t\texpectedEnv      map[string]string\n\t}{\n\t\t{\n\t\t\tname: \"package.json in root\",\n\t\t\tfs: fstest.MapFS{\n\t\t\t\t\"package.json\": &fstest.MapFile{\n\t\t\t\t\tData: []byte(`{}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected:         1,\n\t\t\texpectedPackages: []string{\"nodejs@latest\"},\n\t\t\texpectedEnv:      map[string]string{\"DEVBOX_COREPACK_ENABLED\": \"1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"package.json with node version\",\n\t\t\tfs: fstest.MapFS{\n\t\t\t\t\"package.json\": &fstest.MapFile{\n\t\t\t\t\tData: []byte(`{\n\t\t\t\t\t\t\"engines\": {\n\t\t\t\t\t\t\t\"node\": \">=18.0.0\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected:         1,\n\t\t\texpectedPackages: []string{\"nodejs@18.0.0\"},\n\t\t\texpectedEnv:      map[string]string{\"DEVBOX_COREPACK_ENABLED\": \"1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"no nodejs files\",\n\t\t\tfs: fstest.MapFS{\n\t\t\t\t\"main.py\": &fstest.MapFile{\n\t\t\t\t\tData: []byte(``),\n\t\t\t\t},\n\t\t\t\t\"requirements.txt\": &fstest.MapFile{\n\t\t\t\t\tData: []byte(``),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected:         0,\n\t\t\texpectedPackages: []string{},\n\t\t\texpectedEnv:      map[string]string{},\n\t\t},\n\t\t{\n\t\t\tname:             \"empty directory\",\n\t\t\tfs:               fstest.MapFS{},\n\t\t\texpected:         0,\n\t\t\texpectedPackages: []string{},\n\t\t\texpectedEnv:      map[string]string{},\n\t\t},\n\t}\n\n\tfor _, curTest := range tests {\n\t\tt.Run(curTest.name, func(t *testing.T) {\n\t\t\tdir := t.TempDir()\n\t\t\tfor name, file := range curTest.fs {\n\t\t\t\tfullPath := filepath.Join(dir, name)\n\t\t\t\terr := os.MkdirAll(filepath.Dir(fullPath), 0o755)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\terr = os.WriteFile(fullPath, file.Data, 0o644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tdetector := &NodeJSDetector{Root: dir}\n\t\t\terr := detector.Init()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tscore, err := detector.Relevance(dir)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, curTest.expected, score)\n\t\t\tif score > 0 {\n\t\t\t\tpackages, err := detector.Packages(t.Context())\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, curTest.expectedPackages, packages)\n\n\t\t\t\tenv, err := detector.Env(t.Context())\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, curTest.expectedEnv, env)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNodeJSDetector_Packages(t *testing.T) {\n\td := &NodeJSDetector{}\n\tpackages, err := d.Packages(t.Context())\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"nodejs@latest\"}, packages)\n}\n\nfunc TestNodeJSDetector_Env(t *testing.T) {\n\td := &NodeJSDetector{}\n\tenv, err := d.Env(t.Context())\n\trequire.NoError(t, err)\n\tassert.Equal(t, map[string]string{\"DEVBOX_COREPACK_ENABLED\": \"1\"}, env)\n}\n"
  },
  {
    "path": "pkg/autodetect/detector/php.go",
    "content": "package detector\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"go.jetify.com/devbox/internal/searcher\"\n)\n\ntype composerJSON struct {\n\tRequire map[string]string `json:\"require\"`\n}\n\ntype PHPDetector struct {\n\tRoot         string\n\tcomposerJSON *composerJSON\n}\n\nvar _ Detector = &PHPDetector{}\n\nfunc (d *PHPDetector) Init() error {\n\tcomposer, err := loadComposerJSON(d.Root)\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn err\n\t}\n\td.composerJSON = composer\n\treturn nil\n}\n\nfunc (d *PHPDetector) Relevance(path string) (float64, error) {\n\tif d.composerJSON == nil {\n\t\treturn 0, nil\n\t}\n\treturn 1, nil\n}\n\nfunc (d *PHPDetector) Packages(ctx context.Context) ([]string, error) {\n\tpackages := []string{fmt.Sprintf(\"php@%s\", d.phpVersion(ctx))}\n\textensions, err := d.phpExtensions(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpackages = append(packages, extensions...)\n\treturn packages, nil\n}\n\nfunc (d *PHPDetector) Env(ctx context.Context) (map[string]string, error) {\n\treturn map[string]string{}, nil\n}\n\nfunc (d *PHPDetector) phpVersion(ctx context.Context) string {\n\trequire := d.composerJSON.Require\n\n\tif require[\"php\"] == \"\" {\n\t\treturn \"latest\"\n\t}\n\t// Remove the caret (^) if present\n\tversion := strings.TrimPrefix(require[\"php\"], \"^\")\n\n\t// Extract version in the format x, x.y, or x.y.z\n\tre := regexp.MustCompile(`^(\\d+(\\.\\d+){0,2})`)\n\tmatch := re.FindString(version)\n\n\treturn determineBestVersion(ctx, \"php\", match)\n}\n\nfunc (d *PHPDetector) phpExtensions(ctx context.Context) ([]string, error) {\n\tresolved, err := searcher.Client().ResolveV2(ctx, \"php\", d.phpVersion(ctx))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// extract major-minor from resolved.Version\n\tre := regexp.MustCompile(`^(\\d+)\\.(\\d+)`)\n\tmatches := re.FindStringSubmatch(resolved.Version)\n\tif len(matches) < 3 {\n\t\treturn nil, fmt.Errorf(\"could not parse PHP version: %s\", resolved.Version)\n\t}\n\tmajorMinor := matches[1] + matches[2]\n\n\textensions := []string{}\n\tfor key := range d.composerJSON.Require {\n\t\tif strings.HasPrefix(key, \"ext-\") {\n\t\t\t// The way nix versions php extensions is inconsistent. Sometimes the version is the PHP\n\t\t\t// version, sometimes it's the extension version. We just use @latest everywhere which in\n\t\t\t// practice will just use the version of the extension that exists in the same nixpkgs as\n\t\t\t// the php version.\n\t\t\textensions = append(\n\t\t\t\textensions,\n\t\t\t\tfmt.Sprintf(\"php%sExtensions.%s@latest\", majorMinor, strings.TrimPrefix(key, \"ext-\")),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn extensions, nil\n}\n\nfunc loadComposerJSON(root string) (*composerJSON, error) {\n\tcomposerPath := filepath.Join(root, \"composer.json\")\n\tcomposerData, err := os.ReadFile(composerPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar composer composerJSON\n\terr = json.Unmarshal(composerData, &composer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &composer, nil\n}\n"
  },
  {
    "path": "pkg/autodetect/detector/php_test.go",
    "content": "package detector\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPHPDetector_Relevance(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\tfs               fstest.MapFS\n\t\texpected         float64\n\t\texpectedPackages []string\n\t}{\n\t\t{\n\t\t\tname:             \"no composer.json\",\n\t\t\tfs:               fstest.MapFS{},\n\t\t\texpected:         0,\n\t\t\texpectedPackages: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"with composer.json\",\n\t\t\tfs: fstest.MapFS{\n\t\t\t\t\"composer.json\": &fstest.MapFile{\n\t\t\t\t\tData: []byte(`{\n\t\t\t\t\t\t\"require\": {\n\t\t\t\t\t\t\t\"php\": \"^8.1\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected:         1,\n\t\t\texpectedPackages: []string{\"php@8.1\"},\n\t\t},\n\t}\n\n\tfor _, curTest := range tests {\n\t\tt.Run(curTest.name, func(t *testing.T) {\n\t\t\tdir := t.TempDir()\n\t\t\tfor name, file := range curTest.fs {\n\t\t\t\terr := os.WriteFile(filepath.Join(dir, name), file.Data, 0o644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\td := &PHPDetector{Root: dir}\n\t\t\terr := d.Init()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tscore, err := d.Relevance(dir)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, curTest.expected, score)\n\n\t\t\tif score > 0 {\n\t\t\t\tpackages, err := d.Packages(t.Context())\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, curTest.expectedPackages, packages)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPHPDetector_Packages(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\tfs               fstest.MapFS\n\t\texpectedPHP      string\n\t\texpectedError    bool\n\t\texpectedPackages []string\n\t}{\n\t\t{\n\t\t\tname: \"no php version specified\",\n\t\t\tfs: fstest.MapFS{\n\t\t\t\t\"composer.json\": &fstest.MapFile{\n\t\t\t\t\tData: []byte(`{\n\t\t\t\t\t\t\"require\": {}\n\t\t\t\t\t}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedPHP:      \"php@latest\",\n\t\t\texpectedPackages: []string{\"php@latest\"},\n\t\t},\n\t\t{\n\t\t\tname: \"specific php version\",\n\t\t\tfs: fstest.MapFS{\n\t\t\t\t\"composer.json\": &fstest.MapFile{\n\t\t\t\t\tData: []byte(`{\n\t\t\t\t\t\t\"require\": {\n\t\t\t\t\t\t\t\"php\": \"^8.1\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedPHP:      \"php@8.1\",\n\t\t\texpectedPackages: []string{\"php@8.1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"php version with patch\",\n\t\t\tfs: fstest.MapFS{\n\t\t\t\t\"composer.json\": &fstest.MapFile{\n\t\t\t\t\tData: []byte(`{\n\t\t\t\t\t\t\"require\": {\n\t\t\t\t\t\t\t\"php\": \"^8.1.2\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedPHP:      \"php@8.1.2\",\n\t\t\texpectedPackages: []string{\"php@8.1.2\"},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid composer.json\",\n\t\t\tfs: fstest.MapFS{\n\t\t\t\t\"composer.json\": &fstest.MapFile{\n\t\t\t\t\tData: []byte(`invalid json`),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedError:    true,\n\t\t\texpectedPackages: nil,\n\t\t},\n\t}\n\n\tfor _, curTest := range tests {\n\t\tt.Run(curTest.name, func(t *testing.T) {\n\t\t\tdir := t.TempDir()\n\t\t\tfor name, file := range curTest.fs {\n\t\t\t\terr := os.WriteFile(filepath.Join(dir, name), file.Data, 0o644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\td := &PHPDetector{Root: dir}\n\t\t\terr := d.Init()\n\t\t\tif curTest.expectedError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\n\t\t\tpackages, err := d.Packages(t.Context())\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, curTest.expectedPackages, packages)\n\t\t})\n\t}\n}\n\nfunc TestPHPDetector_PHPExtensions(t *testing.T) {\n\ttests := []struct {\n\t\tname               string\n\t\tfs                 fstest.MapFS\n\t\texpectedExtensions []string\n\t\texpectedPackages   []string\n\t}{\n\t\t{\n\t\t\tname: \"no extensions\",\n\t\t\tfs: fstest.MapFS{\n\t\t\t\t\"composer.json\": &fstest.MapFile{\n\t\t\t\t\tData: []byte(`{\n\t\t\t\t\t\t\"require\": {\n\t\t\t\t\t\t\t\"php\": \"^8.1\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedExtensions: []string{},\n\t\t\texpectedPackages:   []string{\"php@8.1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple extensions\",\n\t\t\tfs: fstest.MapFS{\n\t\t\t\t\"composer.json\": &fstest.MapFile{\n\t\t\t\t\tData: []byte(`{\n\t\t\t\t\t\t\"require\": {\n\t\t\t\t\t\t\t\"php\": \"^8.1\",\n\t\t\t\t\t\t\t\"ext-mbstring\": \"*\",\n\t\t\t\t\t\t\t\"ext-imagick\": \"*\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedExtensions: []string{\n\t\t\t\t\"php81Extensions.mbstring@latest\",\n\t\t\t\t\"php81Extensions.imagick@latest\",\n\t\t\t},\n\t\t\texpectedPackages: []string{\n\t\t\t\t\"php@8.1\",\n\t\t\t\t\"php81Extensions.mbstring@latest\",\n\t\t\t\t\"php81Extensions.imagick@latest\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, curTest := range tests {\n\t\tt.Run(curTest.name, func(t *testing.T) {\n\t\t\tdir := t.TempDir()\n\t\t\tfor name, file := range curTest.fs {\n\t\t\t\terr := os.WriteFile(filepath.Join(dir, name), file.Data, 0o644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\td := &PHPDetector{Root: dir}\n\t\t\terr := d.Init()\n\t\t\trequire.NoError(t, err)\n\n\t\t\textensions, err := d.phpExtensions(t.Context())\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, curTest.expectedExtensions, extensions)\n\n\t\t\tpackages, err := d.Packages(t.Context())\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, curTest.expectedPackages, packages)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/autodetect/detector/poetry.go",
    "content": "package detector\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/pelletier/go-toml/v2\"\n\t\"go.jetify.com/devbox/internal/searcher\"\n)\n\ntype PoetryDetector struct {\n\tPythonDetector\n\tRoot string\n}\n\nvar _ Detector = &PoetryDetector{}\n\nfunc (d *PoetryDetector) Relevance(path string) (float64, error) {\n\tpyprojectPath := filepath.Join(d.Root, \"pyproject.toml\")\n\t_, err := os.Stat(pyprojectPath)\n\tif err == nil {\n\t\treturn d.maxRelevance(), nil\n\t}\n\tif os.IsNotExist(err) {\n\t\treturn 0, nil\n\t}\n\treturn 0, err\n}\n\nfunc (d *PoetryDetector) Packages(ctx context.Context) ([]string, error) {\n\tpyprojectPath := filepath.Join(d.Root, \"pyproject.toml\")\n\tpyproject, err := os.ReadFile(pyprojectPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar pyprojectToml struct {\n\t\tTool struct {\n\t\t\tPoetry struct {\n\t\t\t\tVersion      string `toml:\"version\"`\n\t\t\t\tDependencies struct {\n\t\t\t\t\tPython string `toml:\"python\"`\n\t\t\t\t} `toml:\"dependencies\"`\n\t\t\t} `toml:\"poetry\"`\n\t\t} `toml:\"tool\"`\n\t}\n\terr = toml.Unmarshal(pyproject, &pyprojectToml)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpoetryVersion := determineBestVersion(ctx, \"poetry\", pyprojectToml.Tool.Poetry.Version)\n\tpythonVersion := determineBestVersion(ctx, \"python\", pyprojectToml.Tool.Poetry.Dependencies.Python)\n\n\treturn []string{\"python@\" + pythonVersion, \"poetry@\" + poetryVersion}, nil\n}\n\nfunc (d *PoetryDetector) Env(ctx context.Context) (map[string]string, error) {\n\treturn d.PythonDetector.Env(ctx)\n}\n\nfunc determineBestVersion(ctx context.Context, pkg, version string) string {\n\tif version == \"\" {\n\t\treturn \"latest\"\n\t}\n\n\tversion = sanitizeVersion(version)\n\n\t_, err := searcher.Client().ResolveV2(ctx, pkg, version)\n\tif err != nil {\n\t\treturn \"latest\"\n\t}\n\n\treturn version\n}\n\nfunc sanitizeVersion(version string) string {\n\t// Remove non-numeric characters and 'v' prefix\n\tsanitized := strings.TrimPrefix(version, \"v\")\n\treturn regexp.MustCompile(`[^\\d.]`).ReplaceAllString(sanitized, \"\")\n}\n\nfunc (d *PoetryDetector) maxRelevance() float64 {\n\t// this is arbitrary, but we want to prioritize poetry over python\n\treturn d.PythonDetector.maxRelevance() + 1\n}\n"
  },
  {
    "path": "pkg/autodetect/detector/python.go",
    "content": "package detector\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\ntype PythonDetector struct {\n\tRoot string\n}\n\nvar _ Detector = &PythonDetector{}\n\nfunc (d *PythonDetector) Relevance(path string) (float64, error) {\n\trequirementsPath := filepath.Join(d.Root, \"requirements.txt\")\n\t_, err := os.Stat(requirementsPath)\n\tif err == nil {\n\t\treturn d.maxRelevance(), nil\n\t}\n\tif os.IsNotExist(err) {\n\t\treturn 0, nil\n\t}\n\treturn 0, err\n}\n\nfunc (d *PythonDetector) Packages(ctx context.Context) ([]string, error) {\n\treturn []string{\"python@latest\"}, nil\n}\n\nfunc (d *PythonDetector) Env(ctx context.Context) (map[string]string, error) {\n\treturn map[string]string{}, nil\n}\n\nfunc (d *PythonDetector) maxRelevance() float64 {\n\treturn 1.0\n}\n"
  },
  {
    "path": "plugins/README.md",
    "content": "# Contributing a Plugin\n\nPlugins make it easier to get started with packages that require additional setup when installed with Nix, and they offer a familiar interface for configuring packages. They also help keep all of your project's configuration within your project directory, which helps maintain portability and isolation.\n\n## Getting Started\n\nBefore writing a plugin, we recommend reading the [User Documentation](https://www.jetify.com/devbox/docs/guides/plugins/) on plugins, as well as inspecting and testing a few of the plugins in this directory. Note that the plugins in this directory are compiled into the Devbox binary, but your plugin can be sourced from a local directory or from within your project.\n\nIf you're looking for plugin ideas, check out our [Issues page](https://github.com/jetify-com/devbox/issues?q=is%3Aissue+is%3Aopen+label%3A%22plugin+request%22) for any user requests.\n\nBefore contributing, please consult our [Contributing Guide](../CONTRIBUTING.md) and [Code of Conduct](../CODE_OF_CONDUCT.md) for details on how to contribute to Devbox.\n\n### Testing your Plugin\n\n1. Create a new `devbox.json` in an empty directory using `devbox init`.\n2. Add your plugin to the `include` section of the `devbox.json` file. Add any expected packages using `devbox add <pkg>`.\n3. Check that your plugin creates the correct files and environment variables when running `devbox shell`\n4. If you are looking for sample projects to test your plugin with, check out our [examples](https://github.com/jetify-com/devbox/tree/main/examples).\n\n## Plugin Design\n\nPlugins are defined as Go JSON Template files, using the following schema:\n\n```json\n{\n  \"name\": \"\",\n  \"version\": \"\",\n  \"description\": \"\",\n  \"env\": {\n    \"<key>\": \"<value>\"\n  },\n  \"create_files\": {\n    \"<destination>\": \"<source>\"\n  },\n  \"init_hook\": [\n    \"<bash commands>\"\n  ]\n}\n```\n\nA plugin can define services by adding a `process-compose.yaml` file in its `create_files` stanza.\n\n### Plugin Lifecycle\n\nPlugins are activated whenever a developer runs `devbox shell`, runs a script with `devbox run`, or starts a service using `devbox services start|restart`. The lifecycle of a devbox shell with plugins works as follows:\n\n```mermaid\n---\ntitle: Devbox Shell Lifecycle\n---\nflowchart TD\n   A[Plugin env] --> B\n   B[User env] --> C\n   C[Plugin init_hook] --> D[User Init Hook]\n   D -->  E{Start Shell}\n   E --> F & G & H\n   F[Interactive Shell]\n   G[Run Scripts]\n   H[Start Services]\n```\n\n### Template Placeholders\n\nDevbox's Plugin System provides a few special placeholders that should be used when specifying paths for env variables and helper files:\n\n* `{{ .DevboxDirRoot }}` – points to the root folder of their project, where the user's `devbox.json` is stored.\n* `{{ .DevboxDir }}` – points to `<projectDir>/devbox.d/<plugin.name>`. This directory is public and added to source control by default. This directory is not modified or recreated by Devbox after the initial package installation. You should use this location for files that a user will want to modify and check-in to source control alongside their project (e.g., `.conf` files or other configs).\n* `{{ .Virtenv }}` – points to `<projectDir>/.devbox/virtenv/<plugin_name>` whenever the plugin activates. This directory is hidden and added to `.gitignore` by default You should use this location for files or variables that a user should not check-in or edit directly. Files in this directory should be considered managed by Devbox, and may be recreated or modified after the initial installation.\n\n### Fields\n\n#### `name` *string*\n\nThe name of your plugin. This is used to identify your plugin when a user runs `devbox info`. If `match` is not set, the plugin will automatically activate when a package is added to a devbox.json project that matches `name`.\n\n#### `version` *string*\n\nThe version of your plugin. You should start your version at 0.0.1 and bump it whenever you merge an update to the plugin.\n\n#### `match` *string*\n\nA regex expression that is used to identify when the plugin will be activated. Devbox will activate your plugin when a package installed with `devbox add` matches this regular expression.\n\nThe regex you provide should match a package name. You can look up packages at `nixhub.io`\n\n#### `readme` *string*\n\nSpecial usage instructions or notes to display when your plugin activates or when a user runs `devbox info`. You do not need to document variables, helper files, or services, since these are automatically printed when a user runs `devbox info`.\n\n#### `env` *object*\n\nA map of `\"key\" : \"value\"` pairs used to set environment variables in `devbox shell` when the plugin is activated. These variables will be printed when a user runs `devbox info`, and can be overridden by a user's `devbox.json`.\n\n#### `create_files` *object*\n\nA map of `\"destination\":\"source\"` pairs that can be used to create or copy files into the user's devbox directory when the plugin is activated. For example:\n\n```json\n\"create_files\": {\n    \"{{ .DevboxDir }}/Caddyfile\": \"caddy/Caddyfile\"\n}\n```\n\nWill copy the Caddyfile in the `plugins/caddy` folder to `devbox.d/caddy/Caddyfile` in the user's project directory.\n\nYou should use this to copy starter config files or templates needed to run the plugin's package.\n\n#### `init_hook` *string | string[]*\n\nA single `bash` command or list of `bash` commands that should run before the user's shell is initialized. This will run every time a shell is started, so you should avoid any resource heavy or long running processes in this step.\n\n### Adding Services\n\nDevbox uses [Process Compose](https://github.com/F1bonacc1/process-compose) to run services and background processes.\n\nPlugins can add services to a user's project by adding a `process-compose.yaml` file to the `create_files` stanza. This file will be automatically detected by Devbox, and started when a user runs `devbox services up` or `devbox services start`.\n\nSee the process compose [docs](https://github.com/F1bonacc1/process-compose) for details on how to write define services in `process-compose.yaml`. You can also check the plugins in this directory for examples on how to write services.\n\n## Tips for Writing Plugins\n\n* Only add plugins for packages that require configuration to work with Devbox.\n* Plugins should try to use the same configuration conventions (environment variables, configuration files) as their packages. This lets developers configure their packages in a way that they are familiar with, using existing documentation.\n* If you think a user may want to override or change a parameter, define it as an environment variable in `env`. This makes it possible for a developer to override the parameter in their `devbox.json` file\n* If you're adding a helper file that you think a developer would want check into source control, create it in `{{ .DevboxDir }}`. If you're creating a file that would not be checked into source control, create it in `{{ .Virtenv }}`.\n* Unless there is a very good reason, we do not recommend creating files outside of `{{ .DevboxDir }}` or `{{ .Virtenv }}`. This helps keep user projects clean and well organized.\n"
  },
  {
    "path": "plugins/apache/httpd.conf",
    "content": "ServerAdmin             \"root@localhost\"\nServerName              \"devbox-apache\"\nListen                  \"${HTTPD_PORT}\"\nPidFile                 \"${HTTPD_CONFDIR}/apache.pid\"\n\nLoadModule mpm_event_module modules/mod_mpm_event.so\nLoadModule authz_host_module modules/mod_authz_host.so\nLoadModule authz_core_module modules/mod_authz_core.so\nLoadModule auth_basic_module modules/mod_auth_basic.so\nLoadModule mime_module modules/mod_mime.so\nLoadModule headers_module modules/mod_headers.so\nLoadModule unixd_module modules/mod_unixd.so\nLoadModule status_module modules/mod_status.so\nLoadModule proxy_module modules/mod_proxy.so\nLoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so\nLoadModule dir_module modules/mod_dir.so\nLoadModule alias_module modules/mod_alias.so\nLoadModule log_config_module modules/mod_log_config.so\n\n<IfModule unixd_module>\n    User daemon\n    Group daemon\n</IfModule>\n\n<Directory />\n    AllowOverride none\n    Require all denied\n</Directory>\n\nDocumentRoot  \"${HTTPD_CONFDIR}/../web\"\n<Directory \"${HTTPD_CONFDIR}/../web\">\n    Options Indexes FollowSymLinks\n    AllowOverride None\n    Require all granted\n</Directory>\n\n<Files \".ht*\">\n    Require all denied\n</Files>\nErrorLog \"${HTTPD_ERROR_LOG_FILE}\"\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %b\" access\nCustomLog \"${HTTPD_ACCESS_LOG_FILE}\" access\n<IfModule headers_module>\n    RequestHeader unset Proxy early\n</IfModule>\n\n<VirtualHost \"*:${HTTPD_PORT}\">\n    ServerAdmin webmaster@localhost\n    ServerName  php_localhost\n\n    UseCanonicalName    Off\n    DocumentRoot \"${HTTPD_CONFDIR}/../web\"\n\n    <Directory \"${HTTPD_CONFDIR}/../web\">\n        Options All\n        AllowOverride All\n        <IfModule mod_authz_host.c>\n            Require all granted\n        </IfModule>\n    </Directory>\n\n    ## Added for php-fpm\n    ProxyPassMatch ^/(.*\\.php(/.*)?)$ fcgi://127.0.0.1:8082/${HTTPD_DEVBOX_CONFIG_DIR}/devbox.d/web/$1\n    DirectoryIndex index.html \n\n</VirtualHost>\n"
  },
  {
    "path": "plugins/apache/process-compose.yaml",
    "content": "version: \"0.5\"\n\nprocesses:\n  apache:\n    command: \"echo \\\"Apache starting on port $HTTPD_PORT\\ http://localhost:$HTTPD_PORT\\\" && apachectl start -f $HTTPD_CONFDIR/httpd.conf -D FOREGROUND\"\n    availability:\n      restart: on_failure\n      max_restarts: 5\n    depends_on:\n      apache-error:\n        condition: process_started\n      apache-access:\n        condition: process_started\n  apache-error:\n    command: \"tail -f $HTTPD_ERROR_LOG_FILE\"\n    availability:\n      restart: \"always\"\n  apache-access:\n    command: \"tail -f $HTTPD_ACCESS_LOG_FILE\"\n    availability:\n      restart: \"always\"\n"
  },
  {
    "path": "plugins/apacheHttpd.json",
    "content": "{\n  \"name\": \"apache\",\n  \"version\": \"0.0.2\",\n  \"description\": \"If you with to edit the config file, please copy it out of the .devbox directory.\",\n  \"env\": {\n    \"HTTPD_DEVBOX_CONFIG_DIR\": \"{{ .DevboxProjectDir }}\",\n    \"HTTPD_CONFDIR\": \"{{ .DevboxDir }}\",\n    \"HTTPD_ERROR_LOG_FILE\": \"{{ .Virtenv }}/error.log\",\n    \"HTTPD_ACCESS_LOG_FILE\": \"{{ .Virtenv }}/access.log\",\n    \"HTTPD_PORT\": \"8080\"\n  },\n  \"create_files\": {\n    \"{{ .DevboxDir }}/httpd.conf\": \"apache/httpd.conf\",\n    \"{{ .DevboxDirRoot }}/web/index.html\": \"web/index.html\",\n    \"{{ .Virtenv }}/process-compose.yaml\": \"apache/process-compose.yaml\"\n  }\n}\n"
  },
  {
    "path": "plugins/builtins.go",
    "content": "package plugins\n\nimport (\n\t\"embed\"\n\t\"io/fs\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n)\n\n//go:embed *.json */*\nvar builtIn embed.FS\n\nfunc Builtins() ([]fs.DirEntry, error) {\n\tentries, err := builtIn.ReadDir(\".\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn lo.Filter(entries, func(e fs.DirEntry, _ int) bool {\n\t\treturn !e.IsDir() && !strings.HasSuffix(e.Name(), \".go\")\n\t}), nil\n}\n\ntype BuiltIn struct{}\n\nvar builtInMap = map[*regexp.Regexp]string{\n\tregexp.MustCompile(`^(apache|apacheHttpd)$`):                       \"apacheHttpd\",\n\tregexp.MustCompile(`^(gradle|gradle_[0-9])$`):                      \"gradle\",\n\tregexp.MustCompile(`^elixir_?([0-9_]*[0-9]+)?$`):                   \"elixir\",\n\tregexp.MustCompile(`^(ghc|haskell\\.compiler\\.(.*))$`):              \"haskell\",\n\tregexp.MustCompile(`(^mariadb(-embedded)?_?[0-9]*$|^mysql$)`):      \"mariadb\",\n\tregexp.MustCompile(`^mysql(8[0-9]|57|50)$`):                        \"mysql\",\n\tregexp.MustCompile(`^nodejs(-slim)?_?[0-9]*$`):                     \"nodejs\",\n\tregexp.MustCompile(`^php[0-9]*$`):                                  \"php\",\n\tregexp.MustCompile(`^python3[0-9]*Packages.pip$`):                  \"pip\",\n\tregexp.MustCompile(`^(\\w*\\.)?poetry$`):                             \"poetry\",\n\tregexp.MustCompile(`^postgresql(_[0-9]+)?$`):                       \"postgresql\",\n\tregexp.MustCompile(`^python[0-9]*(Full|Minimal|-full|-minimal)?$`): \"python\",\n\tregexp.MustCompile(`^redis$`):                                      \"redis\",\n\tregexp.MustCompile(`^j?ruby([0-9_]*[0-9]+)?$`):                     \"ruby\",\n\tregexp.MustCompile(`^valkey$`):                                     \"valkey\",\n}\n\nfunc BuiltInForPackage(pkgName string) ([]byte, error) {\n\tfor re, name := range builtInMap {\n\t\tif re.MatchString(pkgName) {\n\t\t\treturn builtIn.ReadFile(name + \".json\")\n\t\t}\n\t}\n\treturn builtIn.ReadFile(pkgName + \".json\")\n}\n\nfunc (f *BuiltIn) FileContent(contentPath string) ([]byte, error) {\n\treturn builtIn.ReadFile(contentPath)\n}\n"
  },
  {
    "path": "plugins/builtins_test.go",
    "content": "package plugins\n\nimport (\n\t\"testing\"\n)\n\nfunc TestBuiltInMap(t *testing.T) {\n\ttestCases := map[string]string{\n\t\t\"apache\":                                 \"apacheHttpd\",\n\t\t\"apacheHttpd\":                            \"apacheHttpd\",\n\t\t\"php\":                                    \"php\",\n\t\t\"php81\":                                  \"php\",\n\t\t\"php82\":                                  \"php\",\n\t\t\"gradle\":                                 \"gradle\",\n\t\t\"gradle_7\":                               \"gradle\",\n\t\t\"ghc\":                                    \"haskell\",\n\t\t\"haskell.compiler.abc\":                   \"haskell\",\n\t\t\"haskell.compiler.native-bignum.ghcHEAD\": \"haskell\",\n\t\t\"haskell.compiler.native-bignum.ghc962\":  \"haskell\",\n\t\t\"mariadb\":                                \"mariadb\",\n\t\t\"mariadb_1011\":                           \"mariadb\",\n\t\t\"mariadb-embedded\":                       \"mariadb\",\n\t\t\"mysql\":                                  \"mariadb\",\n\t\t\"mysql80\":                                \"mysql\",\n\t\t\"python3Packages.pip\":                    \"pip\",\n\t\t\"python\":                                 \"python\",\n\t\t\"python3\":                                \"python\",\n\t\t\"python3Full\":                            \"python\",\n\t\t\"python2Minimal\":                         \"python\",\n\t\t\"python-full\":                            \"python\",\n\t\t\"python-minimal\":                         \"python\",\n\t\t\"redis\":                                  \"redis\",\n\t\t\"ruby\":                                   \"ruby\",\n\t\t\"ruby_21\":                                \"ruby\",\n\t\t\"ruby_2_6\":                               \"ruby\",\n\t\t\"ruby_2_6_5\":                             \"ruby\",\n\t\t\"ruby_2_6_5_1\":                           \"ruby\",\n\t\t\"ruby_2_6_5_1_2\":                         \"ruby\",\n\t\t\"jruby\":                                  \"ruby\",\n\t\t\"ruby_\":                                  \"\",\n\t\t\"ruby_abc\":                               \"\",\n\t\t\"ruby_2_\":                                \"\",\n\t}\n\n\tfor input, expected := range testCases {\n\t\tmatched := false\n\t\tfor re, value := range builtInMap {\n\t\t\tif re.MatchString(input) {\n\t\t\t\tmatched = true\n\t\t\t\tif value != expected {\n\t\t\t\t\tt.Errorf(\"Regex match failed for input: %s. Expected: %s, Got: %s\", input, expected, value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !matched && expected != \"\" {\n\t\t\tt.Errorf(\"Regex match failed for input: %s. Expected: %s, Got: %s\", input, expected, \"\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "plugins/caddy/Caddyfile",
    "content": "# See https://caddyserver.com/docs/caddyfile for more details\n{\n\tadmin 0.0.0.0:2020\n\tauto_https disable_certs\n\thttp_port 8800\n\thttps_port 4443\n}\n\n:8082 {\n\troot * {$CADDY_ROOT_DIR}\n\tlog {\n\t\toutput file {$CADDY_LOG_DIR}/caddy.log\n\t}\n\tfile_server\n}\n"
  },
  {
    "path": "plugins/caddy/process-compose.yaml",
    "content": "version: \"0.6\"\n\nprocesses:\n  caddy:\n    command: \"caddy run --config=$CADDY_CONFIG\"\n    availability:\n      restart: on_failure\n      max_restarts: 5"
  },
  {
    "path": "plugins/caddy.json",
    "content": "{\n    \"name\": \"caddy\",\n    \"version\": \"0.0.3\",\n    \"description\": \"You can customize the config used by the caddy service by modifying the Caddyfile in devbox.d/caddy, or by changing the CADDY_CONFIG environment variable to point to a custom config. The custom config must be either JSON or Caddyfile format.\",\n    \"env\": {\n        \"CADDY_CONFIG\": \"{{ .DevboxDir }}/Caddyfile\",\n        \"CADDY_LOG_DIR\": \"{{ .Virtenv }}/log\",\n        \"CADDY_ROOT_DIR\": \"{{ .DevboxDirRoot }}/web\"\n    },\n    \"create_files\": {\n        \"{{ .DevboxDir }}/Caddyfile\": \"caddy/Caddyfile\",\n        \"{{ .DevboxDirRoot }}/web/index.html\": \"web/index.html\",\n        \"{{ .Virtenv }}/process-compose.yaml\": \"caddy/process-compose.yaml\"\n    }\n}\n"
  },
  {
    "path": "plugins/elixir.json",
    "content": "{\n  \"name\": \"elixir\",\n  \"version\": \"0.0.2\",\n  \"env\": {\n    \"MIX_HOME\": \"{{ .Virtenv }}/mix\",\n    \"HEX_HOME\": \"{{ .Virtenv }}/hex\",\n    \"ERL_AFLAGS\": \"-kernel shell_history enabled\"\n  }\n}\n"
  },
  {
    "path": "plugins/gradle.json",
    "content": "{\n    \"name\": \"gradle\",\n    \"version\": \"0.0.1\",\n    \"description\": \"You can customize which JDK gradle will use by specifying the value of `org.gradle.java.home` in gradle.properties file\",\n    \"shell\": {\n        \"init_hook\": [\n            \"[ -s gradle.properties ] || echo org.gradle.java.home=$JAVA_HOME >> gradle.properties\"\n        ]\n    }\n}\n"
  },
  {
    "path": "plugins/haskell/flake.nix",
    "content": "{\n  description = \"A flake that outputs haskell with custom packages. Used by the devbox haskell plugin\";\n\n  inputs = {\n    nixpkgs.url = \"{{ .URLForInput }}\";\n  };\n\n  outputs = { self, nixpkgs }:\n    let\n      version = builtins.elemAt (builtins.match \"^haskell\\.compiler\\.(.*)$\" \"{{ .PackageAttributePath }}\") 0;\n      \n      ghcWithPackages = if \"{{ .PackageAttributePath }}\" == \"ghc\" \n        then nixpkgs.legacyPackages.{{ .System }}.pkgs.haskellPackages.ghcWithPackages\n        else nixpkgs.legacyPackages.{{ .System }}.pkgs.haskell.packages.${version}.ghcWithPackages;\n\n      haskellPackages = builtins.concatLists(builtins.filter (x: x != null) [\n        {{- range .Packages }}\n        # Test if {{ . }} is a haskell package\n        (builtins.match \"^(stack|cabal-install)$\" \"{{ . }}\")\n        (builtins.match \"^haskellPackages\\.(.*)$\" \"{{ . }}\")\n        (builtins.match \"^haskell\\.packages\\.[^.]*\\.(.*)$\" \"{{ . }}\")\n        {{- end }}\n      ]);\n    in\n    {\n      packages.{{ .System }} = {\n        default = ghcWithPackages (ps: with ps;\n          map (haskellPackage: ps.${haskellPackage}) haskellPackages\n        );\n      };\n    };\n}\n"
  },
  {
    "path": "plugins/haskell.json",
    "content": "{\n  \"name\": \"haskell\",\n  \"version\": \"0.0.2\",\n  \"description\": \"Haskell plugin\",\n  \"packages\": [\n    \"path:{{ .Virtenv }}/flake\"\n  ],\n  \"__remove_trigger_package\": true,\n  \"create_files\": {\n    \"{{ .Virtenv }}/flake/flake.nix\": \"haskell/flake.nix\"\n  }\n}\n"
  },
  {
    "path": "plugins/mariadb/flake.nix",
    "content": "{\n  description = \"A flake that outputs MariaDB with custom configuration and aliases to work in Devbox\";\n\n  inputs = {\n    nixpkgs.url = \"{{.URLForInput}}\";\n  };\n\n  outputs = {self, nixpkgs}:\n    let\n      mariadb-bin =  nixpkgs.legacyPackages.{{.System}}.symlinkJoin {\n\n        name = \"mariadb-wrapped\";\n        paths = [nixpkgs.legacyPackages.{{ .System }}.{{.PackageAttributePath}}];\n        nativeBuildInputs = [ nixpkgs.legacyPackages.{{.System}}.makeWrapper];\n        postBuild = ''\n\n          wrapProgram $out/bin/mysqld \\\n            --add-flags '--basedir=$out --datadir=''$MYSQL_DATADIR --pid-file=''$MYSQL_PID_FILE --socket=''$MYSQL_UNIX_PORT';\n\n          wrapProgram $out/bin/mariadbd \\\n            --add-flags '--basedir=$out --datadir=''$MYSQL_DATADIR --pid-file=''$MYSQL_PID_FILE --socket=''$MYSQL_UNIX_PORT';\n\n          wrapProgram $out/bin/mysqld_safe \\\n            --add-flags '--basedir=$out --datadir=''$MYSQL_DATADIR --pid-file=''$MYSQL_PID_FILE --socket=''$MYSQL_UNIX_PORT';\n\n          if [-f $out/bin/mariadbd-safe]; then\n            wrapProgram $out/bin/mariadbd_safe \\\n              --add-flags '--basedir=$out --datadir=''$MYSQL_DATADIR --pid-file=''$MYSQL_PID_FILE --socket=''$MYSQL_UNIX_PORT';\n          fi\n\n          wrapProgram \"$out/bin/mysql_install_db\" \\\n            --add-flags '--basedir=$out --datadir=''$MYSQL_DATADIR --pid-file=''$MYSQL_PID_FILE --basedir=''$MYSQL_BASEDIR';\n\n          if [-f $out/bin/mariadb-install-db]; then\n            wrapProgram \"$out/bin/mariadb_install_db\" \\\n              --add-flags '--basedir=$out --datadir=''$MYSQL_DATADIR --pid-file=''$MYSQL_PID_FILE --basedir=''$MYSQL_BASEDIR';\n          fi\n        '';\n      };\n    in{\n      packages.{{.System}} = {\n        default = mariadb-bin;\n      };\n    };\n}\n"
  },
  {
    "path": "plugins/mariadb/my.cnf",
    "content": "# MySQL configuration file\n\n[mariadbd]\n# Change this port if 3306 is already used\n#port = 3306\nlog_error=mysql.log\n"
  },
  {
    "path": "plugins/mariadb/process-compose.yaml",
    "content": "version: \"0.5\"\n\nprocesses:\n  mariadb:\n    command: \"echo 'Starting mysqld... check mariadb_logs for details'; mariadbd --log-error=$MYSQL_HOME/mysql.log\"\n    is_daemon: false\n    shutdown:\n      command: \"mariadb-admin -u root shutdown\"\n    availability:\n      restart: \"always\"\n  mariadb_logs:\n    command: \"tail -f $MYSQL_HOME/mysql.log\"\n    availability:\n      restart: \"always\"\n"
  },
  {
    "path": "plugins/mariadb/setup_db.sh",
    "content": "#! bash\n\nif [ ! -d \"$MYSQL_DATADIR\" ]; then\n# Install the Database\n    mysql_install_db --auth-root-authentication-method=normal \\\n        --datadir=$MYSQL_DATADIR --basedir=$MYSQL_BASEDIR \\\n        --pid-file=$MYSQL_PID_FILE\nfi\n\nif [ -e \"$MYSQL_CONF\" ]; then\n  ln -fs \"$MYSQL_CONF\" \"$MYSQL_HOME/my.cnf\"\nfi"
  },
  {
    "path": "plugins/mariadb.json",
    "content": "{\n  \"name\": \"mariadb\",\n  \"version\": \"0.0.4\",\n  \"description\": \"* This plugin wraps mysqld and mysql_install_db to work in your local project\\n* This plugin will create a new database for your project in MYSQL_DATADIR if one doesn't exist on shell init\\n* Use mysqld to manually start the server, and `mysqladmin -u root shutdown` to manually stop it\",\n  \"env\": {\n    \"MYSQL_BASEDIR\": \"{{ .DevboxProfileDefault }}\",\n    \"MYSQL_HOME\": \"{{ .Virtenv }}/run\",\n    \"MYSQL_DATADIR\": \"{{ .Virtenv }}/data\",\n    \"MYSQL_UNIX_PORT\": \"{{ .Virtenv }}/run/mysql.sock\",\n    \"MYSQL_PID_FILE\": \"{{ .Virtenv }}/run/mysql.pid\",\n    \"MYSQL_CONF\": \"{{ .DevboxDir }}/my.cnf\"\n  },\n  \"create_files\": {\n    \"{{ .Virtenv }}/run\": \"\",\n    \"{{ .Virtenv }}/flake/flake.nix\": \"mariadb/flake.nix\",\n    \"{{ .Virtenv }}/setup_db.sh\": \"mariadb/setup_db.sh\",\n    \"{{ .Virtenv }}/process-compose.yaml\": \"mariadb/process-compose.yaml\",\n    \"{{ .DevboxDir }}/my.cnf\": \"mariadb/my.cnf\"\n  },\n  \"packages\": {\n    \"path:{{ .Virtenv }}/flake\": {},\n    \"glibcLocales\": {\n      \"version\": \"latest\",\n      \"platforms\": [\"x86_64-linux\", \"aarch64-linux\"]\n    }\n  },\n  \"__remove_trigger_package\": true,\n  \"shell\": {\n    \"init_hook\": [\n      \"bash {{ .Virtenv }}/setup_db.sh\"\n    ]\n  }\n}\n"
  },
  {
    "path": "plugins/mysql/flake.nix",
    "content": "{\n  description = \"A flake that outputs MySQL with custom configuration and aliases to work in Devbox\";\n\n  inputs = {\n    nixpkgs.url = \"{{.URLForInput}}\";\n  };\n\n  outputs = {self, nixpkgs}:\n    let\n      mysql-bin =  nixpkgs.legacyPackages.{{.System}}.symlinkJoin {\n\n        name = \"mysql-wrapped\";\n        paths = [nixpkgs.legacyPackages.{{ .System }}.{{.PackageAttributePath}}];\n        nativeBuildInputs = [ nixpkgs.legacyPackages.{{.System}}.makeWrapper];\n        postBuild = ''\n\n          wrapProgram $out/bin/mysqld \\\n            --add-flags '--basedir=''$MYSQL_BASEDIR --datadir=''$MYSQL_DATADIR --pid-file=''$MYSQL_PID_FILE --socket=''$MYSQL_UNIX_PORT';\n\n          wrapProgram $out/bin/mysqld_safe \\\n            --add-flags '--basedir=''$MYSQL_BASEDIR --datadir=''$MYSQL_DATADIR --pid-file=''$MYSQL_PID_FILE --socket=''$MYSQL_UNIX_PORT';\n        '';\n      };\n    in{\n      packages.{{.System}} = {\n        default = mysql-bin;\n      };\n    };\n}\n"
  },
  {
    "path": "plugins/mysql/my.cnf",
    "content": "# MySQL configuration file\n\n# [mysqld]\n# skip-log-bin\n# Change this port if 3306 is already used\n#port = 3306\n"
  },
  {
    "path": "plugins/mysql/process-compose.yaml",
    "content": "version: \"0.5\"\n\nprocesses:\n  mysql:\n    command: \"echo 'Starting mysqld... check mysql_logs for details'; mysqld --log-error=$MYSQL_HOME/mysql.log\"\n    is_daemon: false\n    shutdown:\n      command: \"mysqladmin -u root shutdown\"\n    availability:\n      restart: \"always\"\n    depends_on:\n      mysql_logs:\n        condition: \"process_started\"\n  mysql_logs:\n    command: \"tail -f $MYSQL_HOME/mysql.log\"\n    availability:\n      restart: \"always\"\n"
  },
  {
    "path": "plugins/mysql/setup_db.sh",
    "content": "#! bash\n\nif [ ! -d \"$MYSQL_DATADIR\" ]; then\n  # Install the Database\n  mkdir -p $MYSQL_DATADIR\n  mysqld --initialize-insecure --basedir=$MYSQL_BASEDIR\nfi\n\n# Create run directory for socket files if it doesn't exist\nMYSQL_RUN_DIR=\"$(dirname $MYSQL_UNIX_PORT)\"\nif [ ! -d \"$MYSQL_RUN_DIR\" ]; then\n  mkdir -p \"$MYSQL_RUN_DIR\"\nfi\n\nif [ -e \"$MYSQL_CONF\" ]; then\n  ln -fs \"$MYSQL_CONF\" \"$MYSQL_HOME/my.cnf\"\nfi\n"
  },
  {
    "path": "plugins/mysql.json",
    "content": "{\n  \"name\": \"mysql\",\n  \"version\": \"0.0.4\",\n  \"description\": \"* This plugin wraps mysqld and mysql_install_db to work in your local project\\n* This plugin will create a new database for your project in MYSQL_DATADIR if one doesn't exist on shell init. This DB will be started in `insecure` mode, so be sure to add a root password after creation if needed.\\n* Use mysqld to manually start the server, and `mysqladmin -u root shutdown` to manually stop it\",\n  \"env\": {\n    \"MYSQL_BASEDIR\": \"{{ .DevboxProfileDefault }}\",\n    \"MYSQL_HOME\": \"{{ .Virtenv }}/run\",\n    \"MYSQL_DATADIR\": \"{{ .Virtenv }}/data\",\n    \"MYSQL_UNIX_PORT\": \"{{ .Virtenv }}/run/mysql.sock\",\n    \"MYSQL_PID_FILE\": \"{{ .Virtenv }}/run/mysql.pid\",\n    \"MYSQL_CONF\": \"{{ .DevboxDir }}/my.cnf\"\n  },\n  \"create_files\": {\n    \"{{ .Virtenv }}/run\": \"\",\n    \"{{ .Virtenv }}/flake/flake.nix\": \"mysql/flake.nix\",\n    \"{{ .Virtenv }}/setup_db.sh\": \"mysql/setup_db.sh\",\n    \"{{ .Virtenv }}/process-compose.yaml\": \"mysql/process-compose.yaml\",\n    \"{{ .DevboxDir }}/my.cnf\": \"mysql/my.cnf\"\n  },\n  \"packages\": {\n    \"path:{{ .Virtenv }}/flake\": {},\n    \"glibcLocales\": {\n      \"version\": \"latest\",\n      \"platforms\": [\"x86_64-linux\", \"aarch64-linux\"]\n    }\n  },\n  \"__remove_trigger_package\": true,\n  \"shell\": {\n    \"init_hook\": [\"bash {{ .Virtenv }}/setup_db.sh\"]\n  }\n}\n"
  },
  {
    "path": "plugins/nginx/fastcgi.conf",
    "content": "fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;\nfastcgi_param  SERVER_SOFTWARE    nginx;\nfastcgi_param  QUERY_STRING       $query_string;\nfastcgi_param  REQUEST_METHOD     $request_method;\nfastcgi_param  CONTENT_TYPE       $content_type;\nfastcgi_param  CONTENT_LENGTH     $content_length;\nfastcgi_param  SCRIPT_FILENAME    $realpath_root$fastcgi_script_name;\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\nfastcgi_param  REQUEST_URI        $request_uri;\nfastcgi_param  DOCUMENT_URI       $document_uri;\nfastcgi_param  DOCUMENT_ROOT      $document_root;\nfastcgi_param  SERVER_PROTOCOL    $server_protocol;\nfastcgi_param  REMOTE_ADDR        $remote_addr;\nfastcgi_param  REMOTE_PORT        $remote_port;\nfastcgi_param  SERVER_ADDR        $server_addr;\nfastcgi_param  SERVER_PORT        $server_port;\nfastcgi_param  SERVER_NAME        $server_name;\n"
  },
  {
    "path": "plugins/nginx/nginx.conf",
    "content": "# The nginx.conf in this folder is automatically generated from nginx.template\n# To modify your NGINX config, edit the nginx.template file\n\nevents {}\nhttp{\nserver {\n         listen       8081;\n         listen       [::]:8081;\n         server_name  localhost;\n         root         ../../../devbox.d/web;\n\n         error_log error.log error;\n         access_log access.log;\n         client_body_temp_path temp/client_body;\n         proxy_temp_path temp/proxy;\n         fastcgi_temp_path temp/fastcgi;\n         uwsgi_temp_path temp/uwsgi;\n         scgi_temp_path temp/scgi;\n\n         index index.html;\n         server_tokens off;\n    }\n}\n"
  },
  {
    "path": "plugins/nginx/nginx.template",
    "content": "# The nginx.conf in this folder is automatically generated from nginx.template\n# To modify your NGINX config, edit the nginx.template file\n\nevents {}\nhttp{\nserver {\n         listen       $NGINX_WEB_PORT;\n         listen       [::]:$NGINX_WEB_PORT;\n         server_name  $NGINX_WEB_SERVER_NAME;\n         root         $NGINX_WEB_ROOT;\n\n         error_log error.log error;\n         access_log access.log;\n         client_body_temp_path temp/client_body;\n         proxy_temp_path temp/proxy;\n         fastcgi_temp_path temp/fastcgi;\n         uwsgi_temp_path temp/uwsgi;\n         scgi_temp_path temp/scgi;\n\n         index index.html;\n         server_tokens off;\n    }\n}\n"
  },
  {
    "path": "plugins/nginx/process-compose.yaml",
    "content": "version: \"0.5\"\n\nprocesses:\n  nginx:\n    command: |\n      if [ -f $NGINX_CONFDIR/nginx.template ]; then envsubst $(awk 'BEGIN {for (k in ENVIRON) {printf \"$\"k\",\"}}') < $NGINX_CONFDIR/nginx.template > $NGINX_CONFDIR/nginx.conf; fi\n      nginx -p $NGINX_PATH_PREFIX -c $NGINX_CONFDIR/nginx.conf -e error.log -g \"pid nginx.pid;daemon off;\"\n    availability:\n      restart: on_failure\n      max_restarts: 5\n  nginx-error:\n    command: \"tail -f $NGINX_PATH_PREFIX/error.log\"\n    availability:\n      restart: \"always\"\n  nginx-access:\n    command: \"tail -f $NGINX_PATH_PREFIX/access.log\"\n    availability:\n      restart: \"always\"\n"
  },
  {
    "path": "plugins/nginx.json",
    "content": "{\n  \"name\": \"nginx\",\n  \"version\": \"0.0.4\",\n  \"description\": \"nginx can be configured with env variables\\n\\nTo customize:\\n* Use $NGINX_CONFDIR to change the configuration directory\\n* Use $NGINX_TMPDIR to change the tmp directory. Use $NGINX_USER to change the user\\n* Use $NGINX_WEB_PORT to change the port NGINX runs on. \\n Note: This plugin uses envsubst when running `devbox services` to generate the nginx.conf file from the nginx.template file. To customize the nginx.conf file, edit the nginx.template file.\\n\",\n  \"packages\": [\"gettext@latest\", \"gawk@latest\"],\n  \"env\": {\n    \"NGINX_CONF\": \"{{ .DevboxDir }}/nginx.conf\",\n    \"NGINX_CONFDIR\": \"{{ .DevboxDir }}\",\n    \"NGINX_PATH_PREFIX\": \"{{ .Virtenv }}\",\n    \"NGINX_TMPDIR\": \"{{ .Virtenv }}/temp\",\n    \"NGINX_WEB_PORT\": \"8081\",\n    \"NGINX_WEB_ROOT\": \"../../../devbox.d/web\",\n    \"NGINX_WEB_SERVER_NAME\": \"localhost\"\n  },\n  \"create_files\": {\n    \"{{ .Virtenv }}/temp\": \"\",\n    \"{{ .Virtenv }}/process-compose.yaml\": \"nginx/process-compose.yaml\",\n    \"{{ .DevboxDir }}/nginx.template\": \"nginx/nginx.template\",\n    \"{{ .DevboxDir }}/nginx.conf\": \"nginx/nginx.conf\",\n    \"{{ .DevboxDir }}/fastcgi.conf\": \"nginx/fastcgi.conf\",\n    \"{{ .DevboxDirRoot }}/web/index.html\": \"web/index.html\"\n  }\n}\n"
  },
  {
    "path": "plugins/nodejs.json",
    "content": "{\n    \"$schema\": \"https://raw.githubusercontent.com/jetify-com/devbox/main/.schema/devbox-plugin.schema.json\",\n    \"version\": \"0.0.2\",\n    \"name\": \"nodejs\",\n    \"readme\": \"Devbox automatically configures Corepack for Nodejs when DEVBOX_COREPACK_ENABLED=1. You can install Yarn or Pnpm by adding them to your `package.json` file using `packageManager`\\nCorepack binaries will be installed in your local `.devbox` directory\",\n    \"shell\": {\n        \"init_hook\": [\n            \"test -z $DEVBOX_COREPACK_ENABLED || corepack enable --install-directory \\\"{{ .Virtenv }}/corepack-bin/\\\"\",\n            \"test -z $DEVBOX_COREPACK_ENABLED || export PATH=\\\"{{ .Virtenv }}/corepack-bin/:$PATH\\\"\"\n        ]\n    },\n    \"create_files\": {\n      \"{{ .Virtenv }}/corepack-bin\": \"\"\n    }\n}\n"
  },
  {
    "path": "plugins/php/flake.nix",
    "content": "{\n  description = \"A flake that outputs PHP with custom extensions. Used by the devbox php plugin\";\n\n  inputs = {\n    nixpkgs.url = \"{{ .URLForInput }}\";\n  };\n\n  outputs = { self, nixpkgs }:\n    let\n      extensions = builtins.concatLists(builtins.filter (x: x != null) [\n        {{- range .Packages }}\n        (builtins.match \"^php.*Extensions\\.([^@]*).*$\" \"{{ . }}\")\n        {{- end }}\n      ]);\n\n      php = nixpkgs.legacyPackages.{{ .System }}.{{ .PackageAttributePath }}.withExtensions (\n        { enabled, all }: enabled ++ (with all; \n          map (ext: all.${ext}) extensions\n        )\n      );\n    in\n    {\n      packages.{{ .System }} = {\n        default = php;\n        composer = php.packages.composer;\n      };\n    };\n}\n"
  },
  {
    "path": "plugins/php/php-fpm.conf",
    "content": "[global]\npid = ${PHPFPM_PID_FILE}\nerror_log = ${PHPFPM_ERROR_LOG_FILE}\ndaemonize = yes\n\n[www]\n; user = www-data\n; group = www-data\nlisten = 127.0.0.1:${PHPFPM_PORT}\n; listen.owner = www-data\n; listen.group = www-data\npm = dynamic\npm.max_children = 5\npm.start_servers = 2\npm.min_spare_servers = 1\npm.max_spare_servers = 3\nchdir = /\n"
  },
  {
    "path": "plugins/php/php.ini",
    "content": "[php]\n\n; Put your php.ini directives here. For the latest default php.ini file, see https://github.com/php/php-src/blob/master/php.ini-production\n\n; memory_limit = 128M\n; expose_php = Off\n"
  },
  {
    "path": "plugins/php/process-compose.yaml",
    "content": "version: \"0.5\"\n\nprocesses:\n  php-fpm:\n    command: \"php-fpm -y {{ .DevboxDir }}/php-fpm.conf --nodaemonize\"\n    availability:\n      restart: \"always\"\n  \n"
  },
  {
    "path": "plugins/php.json",
    "content": "{\n  \"name\": \"php\",\n  \"version\": \"0.0.3\",\n  \"description\": \"PHP is compiled with default extensions. If you would like to use non-default extensions you can add them with devbox add php81Extensions.{extension} . For example, for the memcache extension you can do `devbox add php81Extensions.memcached`.\",\n  \"packages\": [\n    \"path:{{ .Virtenv }}/flake\",\n    \"path:{{ .Virtenv }}/flake#composer\"\n  ],\n  \"__remove_trigger_package\": true,\n  \"env\": {\n    \"PHPFPM_ERROR_LOG_FILE\": \"{{ .Virtenv }}/php-fpm.log\",\n    \"PHPFPM_PID_FILE\": \"{{ .Virtenv }}/php-fpm.pid\",\n    \"PHPFPM_PORT\": \"8082\",\n    \"PHPRC\": \"{{ .DevboxDir }}\"\n  },\n  \"create_files\": {\n    \"{{ .DevboxDir }}/php-fpm.conf\": \"php/php-fpm.conf\",\n    \"{{ .DevboxDir }}/php.ini\": \"php/php.ini\",\n    \"{{ .Virtenv }}/process-compose.yaml\": \"php/process-compose.yaml\",\n    \"{{ .Virtenv }}/flake/flake.nix\": \"php/flake.nix\"\n  }\n}\n"
  },
  {
    "path": "plugins/poetry/initHook.sh",
    "content": "#!/bin/sh\n\npoetry env use $(command -v python) --directory=\"${DEVBOX_PYPROJECT_DIR:-$DEVBOX_DEFAULT_PYPROJECT_DIR}\" --no-interaction --quiet >&2\n"
  },
  {
    "path": "plugins/poetry.json",
    "content": "{\n    \"name\": \"poetry\",\n    \"version\": \"0.0.4\",\n    \"description\": \"This plugin automatically configures poetry to use the version of python installed in your Devbox shell, instead of the Python version that it is bundled with. The pyproject.toml location can be configured by setting DEVBOX_PYPROJECT_DIR (defaults to the devbox.json's directory).\",\n    \"env\": {\n        \"DEVBOX_DEFAULT_PYPROJECT_DIR\": \"{{ .DevboxProjectDir }}\",\n        \"POETRY_VIRTUALENVS_IN_PROJECT\": \"true\",\n        \"POETRY_VIRTUALENVS_CREATE\": \"true\",\n        \"POETRY_VIRTUALENVS_PATH\": \"{{.Virtenv}}/.virtualenvs\"\n    },\n    \"create_files\": {\n        \"{{ .Virtenv }}/bin/initHook.sh\": \"poetry/initHook.sh\"\n    },\n    \"shell\": {\n        \"init_hook\": [\n            \"{{ .Virtenv }}/bin/initHook.sh\"\n        ]\n    }\n}\n"
  },
  {
    "path": "plugins/postgresql/process-compose.yaml",
    "content": "version: \"0.5\"\n\nprocesses:\n  postgresql:\n    command: \"sh -c 'exec postgres -k \\\"$PGHOST\\\" -p \\\"${PGPORT:-5432}\\\"'\"\n    is_daemon: false\n    shutdown:\n      command: \"pg_ctl stop -m fast\"\n    availability:\n      restart: \"always\"\n    readiness_probe:\n      exec:\n        command: \"pg_isready -p \\\"${PGPORT:-5432}\\\"\"\n"
  },
  {
    "path": "plugins/postgresql.json",
    "content": "{\n    \"name\": \"postgresql\",\n    \"version\": \"0.0.2\",\n    \"description\": \"To initialize the database run `initdb`.\",\n    \"packages\": {\n        \"glibcLocales\": {\n            \"version\": \"latest\",\n            \"platforms\": [\"x86_64-linux\", \"aarch64-linux\"]\n        }\n    },\n    \"env\": {\n        \"PGDATA\": \"{{ .Virtenv }}/data\",\n        \"PGHOST\": \"{{ .Virtenv }}\"\n    },\n    \"create_files\": {\n        \"{{ .Virtenv }}/data\": \"\",\n        \"{{ .Virtenv }}/process-compose.yaml\": \"postgresql/process-compose.yaml\"\n    }\n}\n"
  },
  {
    "path": "plugins/python/venvShellHook.sh",
    "content": "#!/bin/sh\nset -eu\nSTATE_FILE=\"$DEVBOX_PROJECT_ROOT/.devbox/venv_check_completed\"\n\nis_valid_venv() {\n    [ -f \"$1/bin/activate\" ] && [ -f \"$1/bin/python\" ]\n}\n\nis_devbox_venv() {\n    [ \"$1/bin/python\" -ef \"$DEVBOX_PACKAGES_DIR/bin/python\" ]\n}\n\ncreate_venv() {\n    python -m venv \"$VENV_DIR\" --clear\n    echo \"*\\n.*\" >> \"$VENV_DIR/.gitignore\"\n}\n\n# Check that Python version supports venv\nif ! python -c 'import venv' 1> /dev/null 2> /dev/null; then\n    echo \"WARNING: Python version must be > 3.3 to create a virtual environment.\"\n    touch \"$STATE_FILE\"\n    exit 1\nfi\n\n# Check if the directory exists\nif [ -d \"$VENV_DIR\" ]; then\n    if is_valid_venv \"$VENV_DIR\"; then\n        # Check if we've already run this script\n        if [ -f \"$STATE_FILE\" ]; then\n            # \"We've already run this script. Exiting...\"\n            exit 0\n        fi\n        if ! is_devbox_venv \"$VENV_DIR\"; then\n            echo \"WARNING: Virtual environment at $VENV_DIR doesn't use Devbox Python.\"\n            echo \"Do you want to overwrite it? (y/n)\"\n            read reply\n            echo\n            if [[ $reply =~ ^[Yy]$ ]]; then\n                echo \"Overwriting existing virtual environment...\"\n                create_venv\n            elif [[ $reply =~ ^[Nn]$ ]]; then\n                echo \"Using your existing virtual environment. We recommend changing \\$VENV_DIR to a different location\"\n                touch \"$STATE_FILE\"\n                exit 0\n            else\n                echo \"Invalid input. Exiting...\"\n                exit 1\n            fi\n        fi\n    else\n        echo \"Directory exists but is not a valid virtual environment. Creating a new one...\"\n        create_venv\n    fi\nelse\n    echo \"Virtual environment directory doesn't exist. Creating new one...\"\n    create_venv\nfi\n"
  },
  {
    "path": "plugins/python.json",
    "content": "{\n  \"name\": \"python\",\n  \"version\": \"0.0.4\",\n  \"description\": \"Python in Devbox works best when used with a virtual environment (venv, virtualenv, etc.). Devbox will automatically create a virtual environment using `venv` for python3 projects, so you can install packages with pip as normal.\\nTo activate the environment, run `. $VENV_DIR/bin/activate` or add it to the init_hook of your devbox.json\\nTo change where your virtual environment is created, modify the $VENV_DIR environment variable in your init_hook\",\n  \"env\": {\n    \"VENV_DIR\": \"{{ .DevboxProjectDir }}/.venv\"\n  },\n  \"create_files\": {\n    \"{{ .Virtenv }}/bin/venvShellHook.sh\": \"python/venvShellHook.sh\"\n  },\n  \"shell\": {\n    \"init_hook\": [\n      \"export UV_PROJECT_ENVIRONMENT=\\\"$VENV_DIR\\\"\",\n      \"{{ .Virtenv }}/bin/venvShellHook.sh\"\n    ]\n  }\n}\n"
  },
  {
    "path": "plugins/redis/process-compose.yaml",
    "content": "version: \"0.5\"\n\nprocesses:\n  redis:\n    command: \"redis-server $REDIS_CONF --port $REDIS_PORT\"\n    availability:\n      restart: on_failure\n      max_restarts: 5"
  },
  {
    "path": "plugins/redis/redis.conf",
    "content": "# Redis configuration file example.\n#\n# Note that in order to read the configuration file, Redis must be\n# started with the file path as first argument:\n#\n# ./redis-server /path/to/redis.conf\n\n# Note on units: when memory size is needed, it is possible to specify\n# it in the usual form of 1k 5GB 4M and so forth:\n#\n# 1k => 1000 bytes\n# 1kb => 1024 bytes\n# 1m => 1000000 bytes\n# 1mb => 1024*1024 bytes\n# 1g => 1000000000 bytes\n# 1gb => 1024*1024*1024 bytes\n#\n# units are case insensitive so 1GB 1Gb 1gB are all the same.\n\n################################## INCLUDES ###################################\n\n# Include one or more other config files here.  This is useful if you\n# have a standard template that goes to all Redis servers but also need\n# to customize a few per-server settings.  Include files can include\n# other files, so use this wisely.\n#\n# Notice option \"include\" won't be rewritten by command \"CONFIG REWRITE\"\n# from admin or Redis Sentinel. Since Redis always uses the last processed\n# line as value of a configuration directive, you'd better put includes\n# at the beginning of this file to avoid overwriting config change at runtime.\n#\n# If instead you are interested in using includes to override configuration\n# options, it is better to use include as the last line.\n#\n# include /path/to/local.conf\n# include /path/to/other.conf\n\n################################## MODULES #####################################\n\n# Load modules at startup. If the server is not able to load modules\n# it will abort. It is possible to use multiple loadmodule directives.\n#\n# loadmodule /path/to/my_module.so\n# loadmodule /path/to/other_module.so\n\n################################## NETWORK #####################################\n\n# By default, if no \"bind\" configuration directive is specified, Redis listens\n# for connections from all the network interfaces available on the server.\n# It is possible to listen to just one or multiple selected interfaces using\n# the \"bind\" configuration directive, followed by one or more IP addresses.\n#\n# Examples:\n#\n# bind 192.168.1.100 10.0.0.1\n# bind 127.0.0.1 ::1\n#\n# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the\n# internet, binding to all the interfaces is dangerous and will expose the\n# instance to everybody on the internet. So by default we uncomment the\n# following bind directive, that will force Redis to listen only into\n# the IPv4 lookback interface address (this means Redis will be able to\n# accept connections only from clients running into the same computer it\n# is running).\n#\n# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES\n# JUST COMMENT THE FOLLOWING LINE.\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nbind 127.0.0.1\n\n# Protected mode is a layer of security protection, in order to avoid that\n# Redis instances left open on the internet are accessed and exploited.\n#\n# When protected mode is on and if:\n#\n# 1) The server is not binding explicitly to a set of addresses using the\n#    \"bind\" directive.\n# 2) No password is configured.\n#\n# The server only accepts connections from clients connecting from the\n# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain\n# sockets.\n#\n# By default protected mode is enabled. You should disable it only if\n# you are sure you want clients from other hosts to connect to Redis\n# even if no authentication is configured, nor a specific set of interfaces\n# are explicitly listed using the \"bind\" directive.\nprotected-mode yes\n\n# Accept connections on the specified port, default is 6379 (IANA #815344).\n# If port 0 is specified Redis will not listen on a TCP socket.\nport 6379\n\n# TCP listen() backlog.\n#\n# In high requests-per-second environments you need an high backlog in order\n# to avoid slow clients connections issues. Note that the Linux kernel\n# will silently truncate it to the value of /proc/sys/net/core/somaxconn so\n# make sure to raise both the value of somaxconn and tcp_max_syn_backlog\n# in order to get the desired effect.\ntcp-backlog 511\n\n# Unix socket.\n#\n# Specify the path for the Unix socket that will be used to listen for\n# incoming connections. There is no default, so Redis will not listen\n# on a unix socket when not specified.\n#\n# unixsocket /tmp/redis.sock\n# unixsocketperm 700\n\n# Close the connection after a client is idle for N seconds (0 to disable)\ntimeout 0\n\n# TCP keepalive.\n#\n# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence\n# of communication. This is useful for two reasons:\n#\n# 1) Detect dead peers.\n# 2) Take the connection alive from the point of view of network\n#    equipment in the middle.\n#\n# On Linux, the specified value (in seconds) is the period used to send ACKs.\n# Note that to close the connection the double of the time is needed.\n# On other kernels the period depends on the kernel configuration.\n#\n# A reasonable value for this option is 300 seconds, which is the new\n# Redis default starting with Redis 3.2.1.\ntcp-keepalive 300\n\n################################# GENERAL #####################################\n\n# By default Redis does not run as a daemon. Use 'yes' if you need it.\n# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.\ndaemonize no\n\n# If you run Redis from upstart or systemd, Redis can interact with your\n# supervision tree. Options:\n#   supervised no      - no supervision interaction\n#   supervised upstart - signal upstart by putting Redis into SIGSTOP mode\n#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET\n#   supervised auto    - detect upstart or systemd method based on\n#                        UPSTART_JOB or NOTIFY_SOCKET environment variables\n# Note: these supervision methods only signal \"process is ready.\"\n#       They do not enable continuous liveness pings back to your supervisor.\nsupervised no\n\n# If a pid file is specified, Redis writes it where specified at startup\n# and removes it at exit.\n#\n# When the server runs non daemonized, no pid file is created if none is\n# specified in the configuration. When the server is daemonized, the pid file\n# is used even if not specified, defaulting to \"/var/run/redis.pid\".\n#\n# Creating a pid file is best effort: if Redis is not able to create it\n# nothing bad happens, the server will start and run normally.\npidfile redis.pid\n\n# Specify the server verbosity level.\n# This can be one of:\n# debug (a lot of information, useful for development/testing)\n# verbose (many rarely useful info, but not a mess like the debug level)\n# notice (moderately verbose, what you want in production probably)\n# warning (only very important / critical messages are logged)\nloglevel notice\n\n# Specify the log file name. Also the empty string can be used to force\n# Redis to log on the standard output. Note that if you use standard\n# output for logging but daemonize, logs will be sent to /dev/null\n# logfile redis.log\n\n# To enable logging to the system logger, just set 'syslog-enabled' to yes,\n# and optionally update the other syslog parameters to suit your needs.\n# syslog-enabled no\n\n# Specify the syslog identity.\n# syslog-ident redis\n\n# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.\n# syslog-facility local0\n\n# Set the number of databases. The default database is DB 0, you can select\n# a different one on a per-connection basis using SELECT <dbid> where\n# dbid is a number between 0 and 'databases'-1\ndatabases 16\n\n# By default Redis shows an ASCII art logo only when started to log to the\n# standard output and if the standard output is a TTY. Basically this means\n# that normally a logo is displayed only in interactive sessions.\n#\n# However it is possible to force the pre-4.0 behavior and always show a\n# ASCII art logo in startup logs by setting the following option to yes.\nalways-show-logo yes\n\n################################ SNAPSHOTTING  ################################\n#\n# Save the DB on disk:\n#\n#   save <seconds> <changes>\n#\n#   Will save the DB if both the given number of seconds and the given\n#   number of write operations against the DB occurred.\n#\n#   In the example below the behaviour will be to save:\n#   after 900 sec (15 min) if at least 1 key changed\n#   after 300 sec (5 min) if at least 10 keys changed\n#   after 60 sec if at least 10000 keys changed\n#\n#   Note: you can disable saving completely by commenting out all \"save\" lines.\n#\n#   It is also possible to remove all the previously configured save\n#   points by adding a save directive with a single empty string argument\n#   like in the following example:\n#\n#   save \"\"\n\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# By default Redis will stop accepting writes if RDB snapshots are enabled\n# (at least one save point) and the latest background save failed.\n# This will make the user aware (in a hard way) that data is not persisting\n# on disk properly, otherwise chances are that no one will notice and some\n# disaster will happen.\n#\n# If the background saving process will start working again Redis will\n# automatically allow writes again.\n#\n# However if you have setup your proper monitoring of the Redis server\n# and persistence, you may want to disable this feature so that Redis will\n# continue to work as usual even if there are problems with disk,\n# permissions, and so forth.\nstop-writes-on-bgsave-error yes\n\n# Compress string objects using LZF when dump .rdb databases?\n# For default that's set to 'yes' as it's almost always a win.\n# If you want to save some CPU in the saving child set it to 'no' but\n# the dataset will likely be bigger if you have compressible values or keys.\nrdbcompression yes\n\n# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.\n# This makes the format more resistant to corruption but there is a performance\n# hit to pay (around 10%) when saving and loading RDB files, so you can disable it\n# for maximum performances.\n#\n# RDB files created with checksum disabled have a checksum of zero that will\n# tell the loading code to skip the check.\nrdbchecksum yes\n\n# The filename where to dump the DB\ndbfilename dump.rdb\n\n# The working directory.\n#\n# The DB will be written inside this directory, with the filename specified\n# above using the 'dbfilename' configuration directive.\n#\n# The Append Only File will also be created inside this directory.\n#\n# Note that you must specify a directory here, not a file name.\ndir .devbox/virtenv/redis/\n\n################################# REPLICATION #################################\n\n# Master-Slave replication. Use slaveof to make a Redis instance a copy of\n# another Redis server. A few things to understand ASAP about Redis replication.\n#\n# 1) Redis replication is asynchronous, but you can configure a master to\n#    stop accepting writes if it appears to be not connected with at least\n#    a given number of slaves.\n# 2) Redis slaves are able to perform a partial resynchronization with the\n#    master if the replication link is lost for a relatively small amount of\n#    time. You may want to configure the replication backlog size (see the next\n#    sections of this file) with a sensible value depending on your needs.\n# 3) Replication is automatic and does not need user intervention. After a\n#    network partition slaves automatically try to reconnect to masters\n#    and resynchronize with them.\n#\n# slaveof <masterip> <masterport>\n\n# If the master is password protected (using the \"requirepass\" configuration\n# directive below) it is possible to tell the slave to authenticate before\n# starting the replication synchronization process, otherwise the master will\n# refuse the slave request.\n#\n# masterauth <master-password>\n\n# When a slave loses its connection with the master, or when the replication\n# is still in progress, the slave can act in two different ways:\n#\n# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will\n#    still reply to client requests, possibly with out of date data, or the\n#    data set may just be empty if this is the first synchronization.\n#\n# 2) if slave-serve-stale-data is set to 'no' the slave will reply with\n#    an error \"SYNC with master in progress\" to all the kind of commands\n#    but to INFO and SLAVEOF.\n#\nslave-serve-stale-data yes\n\n# You can configure a slave instance to accept writes or not. Writing against\n# a slave instance may be useful to store some ephemeral data (because data\n# written on a slave will be easily deleted after resync with the master) but\n# may also cause problems if clients are writing to it because of a\n# misconfiguration.\n#\n# Since Redis 2.6 by default slaves are read-only.\n#\n# Note: read only slaves are not designed to be exposed to untrusted clients\n# on the internet. It's just a protection layer against misuse of the instance.\n# Still a read only slave exports by default all the administrative commands\n# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve\n# security of read only slaves using 'rename-command' to shadow all the\n# administrative / dangerous commands.\nslave-read-only yes\n\n# Replication SYNC strategy: disk or socket.\n#\n# -------------------------------------------------------\n# WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY\n# -------------------------------------------------------\n#\n# New slaves and reconnecting slaves that are not able to continue the replication\n# process just receiving differences, need to do what is called a \"full\n# synchronization\". An RDB file is transmitted from the master to the slaves.\n# The transmission can happen in two different ways:\n#\n# 1) Disk-backed: The Redis master creates a new process that writes the RDB\n#                 file on disk. Later the file is transferred by the parent\n#                 process to the slaves incrementally.\n# 2) Diskless: The Redis master creates a new process that directly writes the\n#              RDB file to slave sockets, without touching the disk at all.\n#\n# With disk-backed replication, while the RDB file is generated, more slaves\n# can be queued and served with the RDB file as soon as the current child producing\n# the RDB file finishes its work. With diskless replication instead once\n# the transfer starts, new slaves arriving will be queued and a new transfer\n# will start when the current one terminates.\n#\n# When diskless replication is used, the master waits a configurable amount of\n# time (in seconds) before starting the transfer in the hope that multiple slaves\n# will arrive and the transfer can be parallelized.\n#\n# With slow disks and fast (large bandwidth) networks, diskless replication\n# works better.\nrepl-diskless-sync no\n\n# When diskless replication is enabled, it is possible to configure the delay\n# the server waits in order to spawn the child that transfers the RDB via socket\n# to the slaves.\n#\n# This is important since once the transfer starts, it is not possible to serve\n# new slaves arriving, that will be queued for the next RDB transfer, so the server\n# waits a delay in order to let more slaves arrive.\n#\n# The delay is specified in seconds, and by default is 5 seconds. To disable\n# it entirely just set it to 0 seconds and the transfer will start ASAP.\nrepl-diskless-sync-delay 5\n\n# Slaves send PINGs to server in a predefined interval. It's possible to change\n# this interval with the repl_ping_slave_period option. The default value is 10\n# seconds.\n#\n# repl-ping-slave-period 10\n\n# The following option sets the replication timeout for:\n#\n# 1) Bulk transfer I/O during SYNC, from the point of view of slave.\n# 2) Master timeout from the point of view of slaves (data, pings).\n# 3) Slave timeout from the point of view of masters (REPLCONF ACK pings).\n#\n# It is important to make sure that this value is greater than the value\n# specified for repl-ping-slave-period otherwise a timeout will be detected\n# every time there is low traffic between the master and the slave.\n#\n# repl-timeout 60\n\n# Disable TCP_NODELAY on the slave socket after SYNC?\n#\n# If you select \"yes\" Redis will use a smaller number of TCP packets and\n# less bandwidth to send data to slaves. But this can add a delay for\n# the data to appear on the slave side, up to 40 milliseconds with\n# Linux kernels using a default configuration.\n#\n# If you select \"no\" the delay for data to appear on the slave side will\n# be reduced but more bandwidth will be used for replication.\n#\n# By default we optimize for low latency, but in very high traffic conditions\n# or when the master and slaves are many hops away, turning this to \"yes\" may\n# be a good idea.\nrepl-disable-tcp-nodelay no\n\n# Set the replication backlog size. The backlog is a buffer that accumulates\n# slave data when slaves are disconnected for some time, so that when a slave\n# wants to reconnect again, often a full resync is not needed, but a partial\n# resync is enough, just passing the portion of data the slave missed while\n# disconnected.\n#\n# The bigger the replication backlog, the longer the time the slave can be\n# disconnected and later be able to perform a partial resynchronization.\n#\n# The backlog is only allocated once there is at least a slave connected.\n#\n# repl-backlog-size 1mb\n\n# After a master has no longer connected slaves for some time, the backlog\n# will be freed. The following option configures the amount of seconds that\n# need to elapse, starting from the time the last slave disconnected, for\n# the backlog buffer to be freed.\n#\n# Note that slaves never free the backlog for timeout, since they may be\n# promoted to masters later, and should be able to correctly \"partially\n# resynchronize\" with the slaves: hence they should always accumulate backlog.\n#\n# A value of 0 means to never release the backlog.\n#\n# repl-backlog-ttl 3600\n\n# The slave priority is an integer number published by Redis in the INFO output.\n# It is used by Redis Sentinel in order to select a slave to promote into a\n# master if the master is no longer working correctly.\n#\n# A slave with a low priority number is considered better for promotion, so\n# for instance if there are three slaves with priority 10, 100, 25 Sentinel will\n# pick the one with priority 10, that is the lowest.\n#\n# However a special priority of 0 marks the slave as not able to perform the\n# role of master, so a slave with priority of 0 will never be selected by\n# Redis Sentinel for promotion.\n#\n# By default the priority is 100.\nslave-priority 100\n\n# It is possible for a master to stop accepting writes if there are less than\n# N slaves connected, having a lag less or equal than M seconds.\n#\n# The N slaves need to be in \"online\" state.\n#\n# The lag in seconds, that must be <= the specified value, is calculated from\n# the last ping received from the slave, that is usually sent every second.\n#\n# This option does not GUARANTEE that N replicas will accept the write, but\n# will limit the window of exposure for lost writes in case not enough slaves\n# are available, to the specified number of seconds.\n#\n# For example to require at least 3 slaves with a lag <= 10 seconds use:\n#\n# min-slaves-to-write 3\n# min-slaves-max-lag 10\n#\n# Setting one or the other to 0 disables the feature.\n#\n# By default min-slaves-to-write is set to 0 (feature disabled) and\n# min-slaves-max-lag is set to 10.\n\n# A Redis master is able to list the address and port of the attached\n# slaves in different ways. For example the \"INFO replication\" section\n# offers this information, which is used, among other tools, by\n# Redis Sentinel in order to discover slave instances.\n# Another place where this info is available is in the output of the\n# \"ROLE\" command of a master.\n#\n# The listed IP and address normally reported by a slave is obtained\n# in the following way:\n#\n#   IP: The address is auto detected by checking the peer address\n#   of the socket used by the slave to connect with the master.\n#\n#   Port: The port is communicated by the slave during the replication\n#   handshake, and is normally the port that the slave is using to\n#   list for connections.\n#\n# However when port forwarding or Network Address Translation (NAT) is\n# used, the slave may be actually reachable via different IP and port\n# pairs. The following two options can be used by a slave in order to\n# report to its master a specific set of IP and port, so that both INFO\n# and ROLE will report those values.\n#\n# There is no need to use both the options if you need to override just\n# the port or the IP address.\n#\n# slave-announce-ip 5.5.5.5\n# slave-announce-port 1234\n\n################################## SECURITY ###################################\n\n# Require clients to issue AUTH <PASSWORD> before processing any other\n# commands.  This might be useful in environments in which you do not trust\n# others with access to the host running redis-server.\n#\n# This should stay commented out for backward compatibility and because most\n# people do not need auth (e.g. they run their own servers).\n#\n# Warning: since Redis is pretty fast an outside user can try up to\n# 150k passwords per second against a good box. This means that you should\n# use a very strong password otherwise it will be very easy to break.\n#\n# requirepass foobared\n\n# Command renaming.\n#\n# It is possible to change the name of dangerous commands in a shared\n# environment. For instance the CONFIG command may be renamed into something\n# hard to guess so that it will still be available for internal-use tools\n# but not available for general clients.\n#\n# Example:\n#\n# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52\n#\n# It is also possible to completely kill a command by renaming it into\n# an empty string:\n#\n# rename-command CONFIG \"\"\n#\n# Please note that changing the name of commands that are logged into the\n# AOF file or transmitted to slaves may cause problems.\n\n################################### CLIENTS ####################################\n\n# Set the max number of connected clients at the same time. By default\n# this limit is set to 10000 clients, however if the Redis server is not\n# able to configure the process file limit to allow for the specified limit\n# the max number of allowed clients is set to the current file limit\n# minus 32 (as Redis reserves a few file descriptors for internal uses).\n#\n# Once the limit is reached Redis will close all the new connections sending\n# an error 'max number of clients reached'.\n#\n# maxclients 10000\n\n############################## MEMORY MANAGEMENT ################################\n\n# Set a memory usage limit to the specified amount of bytes.\n# When the memory limit is reached Redis will try to remove keys\n# according to the eviction policy selected (see maxmemory-policy).\n#\n# If Redis can't remove keys according to the policy, or if the policy is\n# set to 'noeviction', Redis will start to reply with errors to commands\n# that would use more memory, like SET, LPUSH, and so on, and will continue\n# to reply to read-only commands like GET.\n#\n# This option is usually useful when using Redis as an LRU or LFU cache, or to\n# set a hard memory limit for an instance (using the 'noeviction' policy).\n#\n# WARNING: If you have slaves attached to an instance with maxmemory on,\n# the size of the output buffers needed to feed the slaves are subtracted\n# from the used memory count, so that network problems / resyncs will\n# not trigger a loop where keys are evicted, and in turn the output\n# buffer of slaves is full with DELs of keys evicted triggering the deletion\n# of more keys, and so forth until the database is completely emptied.\n#\n# In short... if you have slaves attached it is suggested that you set a lower\n# limit for maxmemory so that there is some free RAM on the system for slave\n# output buffers (but this is not needed if the policy is 'noeviction').\n#\n# maxmemory <bytes>\n\n# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory\n# is reached. You can select among five behaviors:\n#\n# volatile-lru -> Evict using approximated LRU among the keys with an expire set.\n# allkeys-lru -> Evict any key using approximated LRU.\n# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.\n# allkeys-lfu -> Evict any key using approximated LFU.\n# volatile-random -> Remove a random key among the ones with an expire set.\n# allkeys-random -> Remove a random key, any key.\n# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)\n# noeviction -> Don't evict anything, just return an error on write operations.\n#\n# LRU means Least Recently Used\n# LFU means Least Frequently Used\n#\n# Both LRU, LFU and volatile-ttl are implemented using approximated\n# randomized algorithms.\n#\n# Note: with any of the above policies, Redis will return an error on write\n#       operations, when there are no suitable keys for eviction.\n#\n#       At the date of writing these commands are: set setnx setex append\n#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd\n#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby\n#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby\n#       getset mset msetnx exec sort\n#\n# The default is:\n#\n# maxmemory-policy noeviction\n\n# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated\n# algorithms (in order to save memory), so you can tune it for speed or\n# accuracy. For default Redis will check five keys and pick the one that was\n# used less recently, you can change the sample size using the following\n# configuration directive.\n#\n# The default of 5 produces good enough results. 10 Approximates very closely\n# true LRU but costs more CPU. 3 is faster but not very accurate.\n#\n# maxmemory-samples 5\n\n############################# LAZY FREEING ####################################\n\n# Redis has two primitives to delete keys. One is called DEL and is a blocking\n# deletion of the object. It means that the server stops processing new commands\n# in order to reclaim all the memory associated with an object in a synchronous\n# way. If the key deleted is associated with a small object, the time needed\n# in order to execute the DEL command is very small and comparable to most other\n# O(1) or O(log_N) commands in Redis. However if the key is associated with an\n# aggregated value containing millions of elements, the server can block for\n# a long time (even seconds) in order to complete the operation.\n#\n# For the above reasons Redis also offers non blocking deletion primitives\n# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and\n# FLUSHDB commands, in order to reclaim memory in background. Those commands\n# are executed in constant time. Another thread will incrementally free the\n# object in the background as fast as possible.\n#\n# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.\n# It's up to the design of the application to understand when it is a good\n# idea to use one or the other. However the Redis server sometimes has to\n# delete keys or flush the whole database as a side effect of other operations.\n# Specifically Redis deletes objects independently of a user call in the\n# following scenarios:\n#\n# 1) On eviction, because of the maxmemory and maxmemory policy configurations,\n#    in order to make room for new data, without going over the specified\n#    memory limit.\n# 2) Because of expire: when a key with an associated time to live (see the\n#    EXPIRE command) must be deleted from memory.\n# 3) Because of a side effect of a command that stores data on a key that may\n#    already exist. For example the RENAME command may delete the old key\n#    content when it is replaced with another one. Similarly SUNIONSTORE\n#    or SORT with STORE option may delete existing keys. The SET command\n#    itself removes any old content of the specified key in order to replace\n#    it with the specified string.\n# 4) During replication, when a slave performs a full resynchronization with\n#    its master, the content of the whole database is removed in order to\n#    load the RDB file just transferred.\n#\n# In all the above cases the default is to delete objects in a blocking way,\n# like if DEL was called. However you can configure each case specifically\n# in order to instead release memory in a non-blocking way like if UNLINK\n# was called, using the following configuration directives:\n\nlazyfree-lazy-eviction no\nlazyfree-lazy-expire no\nlazyfree-lazy-server-del no\nslave-lazy-flush no\n\n############################## APPEND ONLY MODE ###############################\n\n# By default Redis asynchronously dumps the dataset on disk. This mode is\n# good enough in many applications, but an issue with the Redis process or\n# a power outage may result into a few minutes of writes lost (depending on\n# the configured save points).\n#\n# The Append Only File is an alternative persistence mode that provides\n# much better durability. For instance using the default data fsync policy\n# (see later in the config file) Redis can lose just one second of writes in a\n# dramatic event like a server power outage, or a single write if something\n# wrong with the Redis process itself happens, but the operating system is\n# still running correctly.\n#\n# AOF and RDB persistence can be enabled at the same time without problems.\n# If the AOF is enabled on startup Redis will load the AOF, that is the file\n# with the better durability guarantees.\n#\n# Please check http://redis.io/topics/persistence for more information.\n\nappendonly no\n\n# The name of the append only file (default: \"appendonly.aof\")\n\nappendfilename \"appendonly.aof\"\n\n# The fsync() call tells the Operating System to actually write data on disk\n# instead of waiting for more data in the output buffer. Some OS will really flush\n# data on disk, some other OS will just try to do it ASAP.\n#\n# Redis supports three different modes:\n#\n# no: don't fsync, just let the OS flush the data when it wants. Faster.\n# always: fsync after every write to the append only log. Slow, Safest.\n# everysec: fsync only one time every second. Compromise.\n#\n# The default is \"everysec\", as that's usually the right compromise between\n# speed and data safety. It's up to you to understand if you can relax this to\n# \"no\" that will let the operating system flush the output buffer when\n# it wants, for better performances (but if you can live with the idea of\n# some data loss consider the default persistence mode that's snapshotting),\n# or on the contrary, use \"always\" that's very slow but a bit safer than\n# everysec.\n#\n# More details please check the following article:\n# http://antirez.com/post/redis-persistence-demystified.html\n#\n# If unsure, use \"everysec\".\n\n# appendfsync always\nappendfsync everysec\n# appendfsync no\n\n# When the AOF fsync policy is set to always or everysec, and a background\n# saving process (a background save or AOF log background rewriting) is\n# performing a lot of I/O against the disk, in some Linux configurations\n# Redis may block too long on the fsync() call. Note that there is no fix for\n# this currently, as even performing fsync in a different thread will block\n# our synchronous write(2) call.\n#\n# In order to mitigate this problem it's possible to use the following option\n# that will prevent fsync() from being called in the main process while a\n# BGSAVE or BGREWRITEAOF is in progress.\n#\n# This means that while another child is saving, the durability of Redis is\n# the same as \"appendfsync none\". In practical terms, this means that it is\n# possible to lose up to 30 seconds of log in the worst scenario (with the\n# default Linux settings).\n#\n# If you have latency problems turn this to \"yes\". Otherwise leave it as\n# \"no\" that is the safest pick from the point of view of durability.\n\nno-appendfsync-on-rewrite no\n\n# Automatic rewrite of the append only file.\n# Redis is able to automatically rewrite the log file implicitly calling\n# BGREWRITEAOF when the AOF log size grows by the specified percentage.\n#\n# This is how it works: Redis remembers the size of the AOF file after the\n# latest rewrite (if no rewrite has happened since the restart, the size of\n# the AOF at startup is used).\n#\n# This base size is compared to the current size. If the current size is\n# bigger than the specified percentage, the rewrite is triggered. Also\n# you need to specify a minimal size for the AOF file to be rewritten, this\n# is useful to avoid rewriting the AOF file even if the percentage increase\n# is reached but it is still pretty small.\n#\n# Specify a percentage of zero in order to disable the automatic AOF\n# rewrite feature.\n\nauto-aof-rewrite-percentage 100\nauto-aof-rewrite-min-size 64mb\n\n# An AOF file may be found to be truncated at the end during the Redis\n# startup process, when the AOF data gets loaded back into memory.\n# This may happen when the system where Redis is running\n# crashes, especially when an ext4 filesystem is mounted without the\n# data=ordered option (however this can't happen when Redis itself\n# crashes or aborts but the operating system still works correctly).\n#\n# Redis can either exit with an error when this happens, or load as much\n# data as possible (the default now) and start if the AOF file is found\n# to be truncated at the end. The following option controls this behavior.\n#\n# If aof-load-truncated is set to yes, a truncated AOF file is loaded and\n# the Redis server starts emitting a log to inform the user of the event.\n# Otherwise if the option is set to no, the server aborts with an error\n# and refuses to start. When the option is set to no, the user requires\n# to fix the AOF file using the \"redis-check-aof\" utility before to restart\n# the server.\n#\n# Note that if the AOF file will be found to be corrupted in the middle\n# the server will still exit with an error. This option only applies when\n# Redis will try to read more data from the AOF file but not enough bytes\n# will be found.\naof-load-truncated yes\n\n# When rewriting the AOF file, Redis is able to use an RDB preamble in the\n# AOF file for faster rewrites and recoveries. When this option is turned\n# on the rewritten AOF file is composed of two different stanzas:\n#\n#   [RDB file][AOF tail]\n#\n# When loading Redis recognizes that the AOF file starts with the \"REDIS\"\n# string and loads the prefixed RDB file, and continues loading the AOF\n# tail.\n#\n# This is currently turned off by default in order to avoid the surprise\n# of a format change, but will at some point be used as the default.\naof-use-rdb-preamble no\n\n################################ LUA SCRIPTING  ###############################\n\n# Max execution time of a Lua script in milliseconds.\n#\n# If the maximum execution time is reached Redis will log that a script is\n# still in execution after the maximum allowed time and will start to\n# reply to queries with an error.\n#\n# When a long running script exceeds the maximum execution time only the\n# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be\n# used to stop a script that did not yet called write commands. The second\n# is the only way to shut down the server in the case a write command was\n# already issued by the script but the user doesn't want to wait for the natural\n# termination of the script.\n#\n# Set it to 0 or a negative value for unlimited execution without warnings.\nlua-time-limit 5000\n\n################################ REDIS CLUSTER  ###############################\n#\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n# WARNING EXPERIMENTAL: Redis Cluster is considered to be stable code, however\n# in order to mark it as \"mature\" we need to wait for a non trivial percentage\n# of users to deploy it in production.\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n#\n# Normal Redis instances can't be part of a Redis Cluster; only nodes that are\n# started as cluster nodes can. In order to start a Redis instance as a\n# cluster node enable the cluster support uncommenting the following:\n#\n# cluster-enabled yes\n\n# Every cluster node has a cluster configuration file. This file is not\n# intended to be edited by hand. It is created and updated by Redis nodes.\n# Every Redis Cluster node requires a different cluster configuration file.\n# Make sure that instances running in the same system do not have\n# overlapping cluster configuration file names.\n#\n# cluster-config-file nodes-6379.conf\n\n# Cluster node timeout is the amount of milliseconds a node must be unreachable\n# for it to be considered in failure state.\n# Most other internal time limits are multiple of the node timeout.\n#\n# cluster-node-timeout 15000\n\n# A slave of a failing master will avoid to start a failover if its data\n# looks too old.\n#\n# There is no simple way for a slave to actually have an exact measure of\n# its \"data age\", so the following two checks are performed:\n#\n# 1) If there are multiple slaves able to failover, they exchange messages\n#    in order to try to give an advantage to the slave with the best\n#    replication offset (more data from the master processed).\n#    Slaves will try to get their rank by offset, and apply to the start\n#    of the failover a delay proportional to their rank.\n#\n# 2) Every single slave computes the time of the last interaction with\n#    its master. This can be the last ping or command received (if the master\n#    is still in the \"connected\" state), or the time that elapsed since the\n#    disconnection with the master (if the replication link is currently down).\n#    If the last interaction is too old, the slave will not try to failover\n#    at all.\n#\n# The point \"2\" can be tuned by user. Specifically a slave will not perform\n# the failover if, since the last interaction with the master, the time\n# elapsed is greater than:\n#\n#   (node-timeout * slave-validity-factor) + repl-ping-slave-period\n#\n# So for example if node-timeout is 30 seconds, and the slave-validity-factor\n# is 10, and assuming a default repl-ping-slave-period of 10 seconds, the\n# slave will not try to failover if it was not able to talk with the master\n# for longer than 310 seconds.\n#\n# A large slave-validity-factor may allow slaves with too old data to failover\n# a master, while a too small value may prevent the cluster from being able to\n# elect a slave at all.\n#\n# For maximum availability, it is possible to set the slave-validity-factor\n# to a value of 0, which means, that slaves will always try to failover the\n# master regardless of the last time they interacted with the master.\n# (However they'll always try to apply a delay proportional to their\n# offset rank).\n#\n# Zero is the only value able to guarantee that when all the partitions heal\n# the cluster will always be able to continue.\n#\n# cluster-slave-validity-factor 10\n\n# Cluster slaves are able to migrate to orphaned masters, that are masters\n# that are left without working slaves. This improves the cluster ability\n# to resist to failures as otherwise an orphaned master can't be failed over\n# in case of failure if it has no working slaves.\n#\n# Slaves migrate to orphaned masters only if there are still at least a\n# given number of other working slaves for their old master. This number\n# is the \"migration barrier\". A migration barrier of 1 means that a slave\n# will migrate only if there is at least 1 other working slave for its master\n# and so forth. It usually reflects the number of slaves you want for every\n# master in your cluster.\n#\n# Default is 1 (slaves migrate only if their masters remain with at least\n# one slave). To disable migration just set it to a very large value.\n# A value of 0 can be set but is useful only for debugging and dangerous\n# in production.\n#\n# cluster-migration-barrier 1\n\n# By default Redis Cluster nodes stop accepting queries if they detect there\n# is at least an hash slot uncovered (no available node is serving it).\n# This way if the cluster is partially down (for example a range of hash slots\n# are no longer covered) all the cluster becomes, eventually, unavailable.\n# It automatically returns available as soon as all the slots are covered again.\n#\n# However sometimes you want the subset of the cluster which is working,\n# to continue to accept queries for the part of the key space that is still\n# covered. In order to do so, just set the cluster-require-full-coverage\n# option to no.\n#\n# cluster-require-full-coverage yes\n\n# This option, when set to yes, prevents slaves from trying to failover its\n# master during master failures. However the master can still perform a\n# manual failover, if forced to do so.\n#\n# This is useful in different scenarios, especially in the case of multiple\n# data center operations, where we want one side to never be promoted if not\n# in the case of a total DC failure.\n#\n# cluster-slave-no-failover no\n\n# In order to setup your cluster make sure to read the documentation\n# available at http://redis.io web site.\n\n########################## CLUSTER DOCKER/NAT support  ########################\n\n# In certain deployments, Redis Cluster nodes address discovery fails, because\n# addresses are NAT-ted or because ports are forwarded (the typical case is\n# Docker and other containers).\n#\n# In order to make Redis Cluster working in such environments, a static\n# configuration where each node knows its public address is needed. The\n# following two options are used for this scope, and are:\n#\n# * cluster-announce-ip\n# * cluster-announce-port\n# * cluster-announce-bus-port\n#\n# Each instruct the node about its address, client port, and cluster message\n# bus port. The information is then published in the header of the bus packets\n# so that other nodes will be able to correctly map the address of the node\n# publishing the information.\n#\n# If the above options are not used, the normal Redis Cluster auto-detection\n# will be used instead.\n#\n# Note that when remapped, the bus port may not be at the fixed offset of\n# clients port + 10000, so you can specify any port and bus-port depending\n# on how they get remapped. If the bus-port is not set, a fixed offset of\n# 10000 will be used as usually.\n#\n# Example:\n#\n# cluster-announce-ip 10.1.1.5\n# cluster-announce-port 6379\n# cluster-announce-bus-port 6380\n\n################################## SLOW LOG ###################################\n\n# The Redis Slow Log is a system to log queries that exceeded a specified\n# execution time. The execution time does not include the I/O operations\n# like talking with the client, sending the reply and so forth,\n# but just the time needed to actually execute the command (this is the only\n# stage of command execution where the thread is blocked and can not serve\n# other requests in the meantime).\n#\n# You can configure the slow log with two parameters: one tells Redis\n# what is the execution time, in microseconds, to exceed in order for the\n# command to get logged, and the other parameter is the length of the\n# slow log. When a new command is logged the oldest one is removed from the\n# queue of logged commands.\n\n# The following time is expressed in microseconds, so 1000000 is equivalent\n# to one second. Note that a negative number disables the slow log, while\n# a value of zero forces the logging of every command.\nslowlog-log-slower-than 10000\n\n# There is no limit to this length. Just be aware that it will consume memory.\n# You can reclaim memory used by the slow log with SLOWLOG RESET.\nslowlog-max-len 128\n\n################################ LATENCY MONITOR ##############################\n\n# The Redis latency monitoring subsystem samples different operations\n# at runtime in order to collect data related to possible sources of\n# latency of a Redis instance.\n#\n# Via the LATENCY command this information is available to the user that can\n# print graphs and obtain reports.\n#\n# The system only logs operations that were performed in a time equal or\n# greater than the amount of milliseconds specified via the\n# latency-monitor-threshold configuration directive. When its value is set\n# to zero, the latency monitor is turned off.\n#\n# By default latency monitoring is disabled since it is mostly not needed\n# if you don't have latency issues, and collecting data has a performance\n# impact, that while very small, can be measured under big load. Latency\n# monitoring can easily be enabled at runtime using the command\n# \"CONFIG SET latency-monitor-threshold <milliseconds>\" if needed.\nlatency-monitor-threshold 0\n\n############################# EVENT NOTIFICATION ##############################\n\n# Redis can notify Pub/Sub clients about events happening in the key space.\n# This feature is documented at http://redis.io/topics/notifications\n#\n# For instance if keyspace events notification is enabled, and a client\n# performs a DEL operation on key \"foo\" stored in the Database 0, two\n# messages will be published via Pub/Sub:\n#\n# PUBLISH __keyspace@0__:foo del\n# PUBLISH __keyevent@0__:del foo\n#\n# It is possible to select the events that Redis will notify among a set\n# of classes. Every class is identified by a single character:\n#\n#  K     Keyspace events, published with __keyspace@<db>__ prefix.\n#  E     Keyevent events, published with __keyevent@<db>__ prefix.\n#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...\n#  $     String commands\n#  l     List commands\n#  s     Set commands\n#  h     Hash commands\n#  z     Sorted set commands\n#  x     Expired events (events generated every time a key expires)\n#  e     Evicted events (events generated when a key is evicted for maxmemory)\n#  A     Alias for g$lshzxe, so that the \"AKE\" string means all the events.\n#\n#  The \"notify-keyspace-events\" takes as argument a string that is composed\n#  of zero or multiple characters. The empty string means that notifications\n#  are disabled.\n#\n#  Example: to enable list and generic events, from the point of view of the\n#           event name, use:\n#\n#  notify-keyspace-events Elg\n#\n#  Example 2: to get the stream of the expired keys subscribing to channel\n#             name __keyevent@0__:expired use:\n#\n#  notify-keyspace-events Ex\n#\n#  By default all notifications are disabled because most users don't need\n#  this feature and the feature has some overhead. Note that if you don't\n#  specify at least one of K or E, no events will be delivered.\nnotify-keyspace-events \"\"\n\n############################### ADVANCED CONFIG ###############################\n\n# Hashes are encoded using a memory efficient data structure when they have a\n# small number of entries, and the biggest entry does not exceed a given\n# threshold. These thresholds can be configured using the following directives.\nhash-max-ziplist-entries 512\nhash-max-ziplist-value 64\n\n# Lists are also encoded in a special way to save a lot of space.\n# The number of entries allowed per internal list node can be specified\n# as a fixed maximum size or a maximum number of elements.\n# For a fixed maximum size, use -5 through -1, meaning:\n# -5: max size: 64 Kb  <-- not recommended for normal workloads\n# -4: max size: 32 Kb  <-- not recommended\n# -3: max size: 16 Kb  <-- probably not recommended\n# -2: max size: 8 Kb   <-- good\n# -1: max size: 4 Kb   <-- good\n# Positive numbers mean store up to _exactly_ that number of elements\n# per list node.\n# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),\n# but if your use case is unique, adjust the settings as necessary.\nlist-max-ziplist-size -2\n\n# Lists may also be compressed.\n# Compress depth is the number of quicklist ziplist nodes from *each* side of\n# the list to *exclude* from compression.  The head and tail of the list\n# are always uncompressed for fast push/pop operations.  Settings are:\n# 0: disable all list compression\n# 1: depth 1 means \"don't start compressing until after 1 node into the list,\n#    going from either the head or tail\"\n#    So: [head]->node->node->...->node->[tail]\n#    [head], [tail] will always be uncompressed; inner nodes will compress.\n# 2: [head]->[next]->node->node->...->node->[prev]->[tail]\n#    2 here means: don't compress head or head->next or tail->prev or tail,\n#    but compress all nodes between them.\n# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]\n# etc.\nlist-compress-depth 0\n\n# Sets have a special encoding in just one case: when a set is composed\n# of just strings that happen to be integers in radix 10 in the range\n# of 64 bit signed integers.\n# The following configuration setting sets the limit in the size of the\n# set in order to use this special memory saving encoding.\nset-max-intset-entries 512\n\n# Similarly to hashes and lists, sorted sets are also specially encoded in\n# order to save a lot of space. This encoding is only used when the length and\n# elements of a sorted set are below the following limits:\nzset-max-ziplist-entries 128\nzset-max-ziplist-value 64\n\n# HyperLogLog sparse representation bytes limit. The limit includes the\n# 16 bytes header. When an HyperLogLog using the sparse representation crosses\n# this limit, it is converted into the dense representation.\n#\n# A value greater than 16000 is totally useless, since at that point the\n# dense representation is more memory efficient.\n#\n# The suggested value is ~ 3000 in order to have the benefits of\n# the space efficient encoding without slowing down too much PFADD,\n# which is O(N) with the sparse encoding. The value can be raised to\n# ~ 10000 when CPU is not a concern, but space is, and the data set is\n# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.\nhll-sparse-max-bytes 3000\n\n# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in\n# order to help rehashing the main Redis hash table (the one mapping top-level\n# keys to values). The hash table implementation Redis uses (see dict.c)\n# performs a lazy rehashing: the more operation you run into a hash table\n# that is rehashing, the more rehashing \"steps\" are performed, so if the\n# server is idle the rehashing is never complete and some more memory is used\n# by the hash table.\n#\n# The default is to use this millisecond 10 times every second in order to\n# actively rehash the main dictionaries, freeing memory when possible.\n#\n# If unsure:\n# use \"activerehashing no\" if you have hard latency requirements and it is\n# not a good thing in your environment that Redis can reply from time to time\n# to queries with 2 milliseconds delay.\n#\n# use \"activerehashing yes\" if you don't have such hard requirements but\n# want to free memory asap when possible.\nactiverehashing yes\n\n# The client output buffer limits can be used to force disconnection of clients\n# that are not reading data from the server fast enough for some reason (a\n# common reason is that a Pub/Sub client can't consume messages as fast as the\n# publisher can produce them).\n#\n# The limit can be set differently for the three different classes of clients:\n#\n# normal -> normal clients including MONITOR clients\n# slave  -> slave clients\n# pubsub -> clients subscribed to at least one pubsub channel or pattern\n#\n# The syntax of every client-output-buffer-limit directive is the following:\n#\n# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>\n#\n# A client is immediately disconnected once the hard limit is reached, or if\n# the soft limit is reached and remains reached for the specified number of\n# seconds (continuously).\n# So for instance if the hard limit is 32 megabytes and the soft limit is\n# 16 megabytes / 10 seconds, the client will get disconnected immediately\n# if the size of the output buffers reach 32 megabytes, but will also get\n# disconnected if the client reaches 16 megabytes and continuously overcomes\n# the limit for 10 seconds.\n#\n# By default normal clients are not limited because they don't receive data\n# without asking (in a push way), but just after a request, so only\n# asynchronous clients may create a scenario where data is requested faster\n# than it can read.\n#\n# Instead there is a default limit for pubsub and slave clients, since\n# subscribers and slaves receive data in a push fashion.\n#\n# Both the hard or the soft limit can be disabled by setting them to zero.\nclient-output-buffer-limit normal 0 0 0\nclient-output-buffer-limit slave 256mb 64mb 60\nclient-output-buffer-limit pubsub 32mb 8mb 60\n\n# Client query buffers accumulate new commands. They are limited to a fixed\n# amount by default in order to avoid that a protocol desynchronization (for\n# instance due to a bug in the client) will lead to unbound memory usage in\n# the query buffer. However you can configure it here if you have very special\n# needs, such us huge multi/exec requests or alike.\n#\n# client-query-buffer-limit 1gb\n\n# In the Redis protocol, bulk requests, that are, elements representing single\n# strings, are normally limited to 512 mb. However you can change this limit\n# here.\n#\n# proto-max-bulk-len 512mb\n\n# Redis calls an internal function to perform many background tasks, like\n# closing connections of clients in timeout, purging expired keys that are\n# never requested, and so forth.\n#\n# Not all tasks are performed with the same frequency, but Redis checks for\n# tasks to perform according to the specified \"hz\" value.\n#\n# By default \"hz\" is set to 10. Raising the value will use more CPU when\n# Redis is idle, but at the same time will make Redis more responsive when\n# there are many keys expiring at the same time, and timeouts may be\n# handled with more precision.\n#\n# The range is between 1 and 500, however a value over 100 is usually not\n# a good idea. Most users should use the default of 10 and raise this up to\n# 100 only in environments where very low latency is required.\nhz 10\n\n# When a child rewrites the AOF file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\naof-rewrite-incremental-fsync yes\n\n# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good\n# idea to start with the default settings and only change them after investigating\n# how to improve the performances and how the keys LFU change over time, which\n# is possible to inspect via the OBJECT FREQ command.\n#\n# There are two tunable parameters in the Redis LFU implementation: the\n# counter logarithm factor and the counter decay time. It is important to\n# understand what the two parameters mean before changing them.\n#\n# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis\n# uses a probabilistic increment with logarithmic behavior. Given the value\n# of the old counter, when a key is accessed, the counter is incremented in\n# this way:\n#\n# 1. A random number R between 0 and 1 is extracted.\n# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).\n# 3. The counter is incremented only if R < P.\n#\n# The default lfu-log-factor is 10. This is a table of how the frequency\n# counter changes with a different number of accesses with different\n# logarithmic factors:\n#\n# +--------+------------+------------+------------+------------+------------+\n# | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |\n# +--------+------------+------------+------------+------------+------------+\n# | 0      | 104        | 255        | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 1      | 18         | 49         | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 10     | 10         | 18         | 142        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 100    | 8          | 11         | 49         | 143        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n#\n# NOTE: The above table was obtained by running the following commands:\n#\n#   redis-benchmark -n 1000000 incr foo\n#   redis-cli object freq foo\n#\n# NOTE 2: The counter initial value is 5 in order to give new objects a chance\n# to accumulate hits.\n#\n# The counter decay time is the time, in minutes, that must elapse in order\n# for the key counter to be divided by two (or decremented if it has a value\n# less <= 10).\n#\n# The default value for the lfu-decay-time is 1. A Special value of 0 means to\n# decay the counter every time it happens to be scanned.\n#\n# lfu-log-factor 10\n# lfu-decay-time 1\n\n########################### ACTIVE DEFRAGMENTATION #######################\n#\n# WARNING THIS FEATURE IS EXPERIMENTAL. However it was stress tested\n# even in production and manually tested by multiple engineers for some\n# time.\n#\n# What is active defragmentation?\n# -------------------------------\n#\n# Active (online) defragmentation allows a Redis server to compact the\n# spaces left between small allocations and deallocations of data in memory,\n# thus allowing to reclaim back memory.\n#\n# Fragmentation is a natural process that happens with every allocator (but\n# less so with Jemalloc, fortunately) and certain workloads. Normally a server\n# restart is needed in order to lower the fragmentation, or at least to flush\n# away all the data and create it again. However thanks to this feature\n# implemented by Oran Agra for Redis 4.0 this process can happen at runtime\n# in an \"hot\" way, while the server is running.\n#\n# Basically when the fragmentation is over a certain level (see the\n# configuration options below) Redis will start to create new copies of the\n# values in contiguous memory regions by exploiting certain specific Jemalloc\n# features (in order to understand if an allocation is causing fragmentation\n# and to allocate it in a better place), and at the same time, will release the\n# old copies of the data. This process, repeated incrementally for all the keys\n# will cause the fragmentation to drop back to normal values.\n#\n# Important things to understand:\n#\n# 1. This feature is disabled by default, and only works if you compiled Redis\n#    to use the copy of Jemalloc we ship with the source code of Redis.\n#    This is the default with Linux builds.\n#\n# 2. You never need to enable this feature if you don't have fragmentation\n#    issues.\n#\n# 3. Once you experience fragmentation, you can enable this feature when\n#    needed with the command \"CONFIG SET activedefrag yes\".\n#\n# The configuration parameters are able to fine tune the behavior of the\n# defragmentation process. If you are not sure about what they mean it is\n# a good idea to leave the defaults untouched.\n\n# Enabled active defragmentation\n# activedefrag yes\n\n# Minimum amount of fragmentation waste to start active defrag\n# active-defrag-ignore-bytes 100mb\n\n# Minimum percentage of fragmentation to start active defrag\n# active-defrag-threshold-lower 10\n\n# Maximum percentage of fragmentation at which we use maximum effort\n# active-defrag-threshold-upper 100\n\n# Minimal effort for defrag in CPU percentage\n# active-defrag-cycle-min 25\n\n# Maximal effort for defrag in CPU percentage\n# active-defrag-cycle-max 75\n"
  },
  {
    "path": "plugins/redis.json",
    "content": "{\n    \"name\": \"redis\",\n    \"version\": \"0.0.2\",\n    \"description\": \"Running `devbox services start redis` will start redis as a daemon in the background. \\n\\nYou can manually start Redis in the foreground by running `redis-server $REDIS_CONF --port $REDIS_PORT`. \\n\\nLogs, pidfile, and data dumps are stored in `.devbox/virtenv/redis`. You can change this by modifying the `dir` directive in `devbox.d/redis/redis.conf`\",\n    \"env\": {\n        \"REDIS_PORT\": \"6379\",\n        \"REDIS_CONF\": \"{{ .DevboxDir }}/redis.conf\"\n    },\n    \"create_files\": {\n        \"{{ .DevboxDir }}/redis.conf\": \"redis/redis.conf\",\n        \"{{ .Virtenv }}/process-compose.yaml\": \"redis/process-compose.yaml\"\n    }\n}\n"
  },
  {
    "path": "plugins/ruby.json",
    "content": "{\n  \"name\": \"ruby\",\n  \"version\": \"0.0.2\",\n  \"env\": {\n    \"PATH\": \"{{ .Virtenv }}/bin/:$PATH\",\n    \"RUBY_CONFDIR\": \"{{ .Virtenv }}\",\n    \"GEMRC\": \"{{ .Virtenv }}/.gemrc\",\n    \"GEM_HOME\": \"{{ .Virtenv }}\"\n  }\n}\n"
  },
  {
    "path": "plugins/rustc.json",
    "content": "{\n  \"name\": \"rustc\",\n  \"version\": \"0.0.1\",\n  \"description\": \"As an alternative to rustc you may add rustup to manage your rust versions.\"\n}\n"
  },
  {
    "path": "plugins/rustup.json",
    "content": "{\n  \"name\": \"rustup\",\n  \"version\": \"0.0.1\",\n  \"description\": \"If using this on macOS you may need to install `libiconv` as well\",\n  \"env\": {\n    \"RUSTUP_HOME\": \"{{ .Virtenv }}\",\n    \"LIBRARY_PATH\": \"{{ .DevboxProfileDefault }}/lib\"\n  }\n}\n"
  },
  {
    "path": "plugins/valkey/process-compose.yaml",
    "content": "version: \"0.5\"\n\nprocesses:\n  valkey:\n    command: \"valkey-server $VALKEY_CONF --port $VALKEY_PORT\"\n    availability:\n      restart: on_failure\n      max_restarts: 5"
  },
  {
    "path": "plugins/valkey/valkey.conf",
    "content": "# Valkey configuration file example.\n#\n# Note that in order to read the configuration file, Valkey must be\n# started with the file path as first argument:\n#\n# ./valkey-server /path/to/valkey.conf\n\n# Note on units: when memory size is needed, it is possible to specify\n# it in the usual form of 1k 5GB 4M and so forth:\n#\n# 1k => 1000 bytes\n# 1kb => 1024 bytes\n# 1m => 1000000 bytes\n# 1mb => 1024*1024 bytes\n# 1g => 1000000000 bytes\n# 1gb => 1024*1024*1024 bytes\n#\n# units are case insensitive so 1GB 1Gb 1gB are all the same.\n\n################################## INCLUDES ###################################\n\n# Include one or more other config files here.  This is useful if you\n# have a standard template that goes to all Valkey servers but also need\n# to customize a few per-server settings.  Include files can include\n# other files, so use this wisely.\n#\n# Notice option \"include\" won't be rewritten by command \"CONFIG REWRITE\"\n# from admin or Valkey Sentinel. Since Valkey always uses the last processed\n# line as value of a configuration directive, you'd better put includes\n# at the beginning of this file to avoid overwriting config change at runtime.\n#\n# If instead you are interested in using includes to override configuration\n# options, it is better to use include as the last line.\n#\n# include /path/to/local.conf\n# include /path/to/other.conf\n\n################################## MODULES #####################################\n\n# Load modules at startup. If the server is not able to load modules\n# it will abort. It is possible to use multiple loadmodule directives.\n#\n# loadmodule /path/to/my_module.so\n# loadmodule /path/to/other_module.so\n\n################################## NETWORK #####################################\n\n# By default, if no \"bind\" configuration directive is specified, Valkey listens\n# for connections from all the network interfaces available on the server.\n# It is possible to listen to just one or multiple selected interfaces using\n# the \"bind\" configuration directive, followed by one or more IP addresses.\n#\n# Examples:\n#\n# bind 192.168.1.100 10.0.0.1\n# bind 127.0.0.1 ::1\n#\n# ~~~ WARNING ~~~ If the computer running Valkey is directly exposed to the\n# internet, binding to all the interfaces is dangerous and will expose the\n# instance to everybody on the internet. So by default we uncomment the\n# following bind directive, that will force Valkey to listen only into\n# the IPv4 lookback interface address (this means Valkey will be able to\n# accept connections only from clients running into the same computer it\n# is running).\n#\n# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES\n# JUST COMMENT THE FOLLOWING LINE.\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nbind 127.0.0.1\n\n# Protected mode is a layer of security protection, in order to avoid that\n# Valkey instances left open on the internet are accessed and exploited.\n#\n# When protected mode is on and if:\n#\n# 1) The server is not binding explicitly to a set of addresses using the\n#    \"bind\" directive.\n# 2) No password is configured.\n#\n# The server only accepts connections from clients connecting from the\n# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain\n# sockets.\n#\n# By default protected mode is enabled. You should disable it only if\n# you are sure you want clients from other hosts to connect to Valkey\n# even if no authentication is configured, nor a specific set of interfaces\n# are explicitly listed using the \"bind\" directive.\nprotected-mode yes\n\n# Accept connections on the specified port, default is 6379 (IANA #815344).\n# If port 0 is specified Valkey will not listen on a TCP socket.\nport 6379\n\n# TCP listen() backlog.\n#\n# In high requests-per-second environments you need an high backlog in order\n# to avoid slow clients connections issues. Note that the Linux kernel\n# will silently truncate it to the value of /proc/sys/net/core/somaxconn so\n# make sure to raise both the value of somaxconn and tcp_max_syn_backlog\n# in order to get the desired effect.\ntcp-backlog 511\n\n# Unix socket.\n#\n# Specify the path for the Unix socket that will be used to listen for\n# incoming connections. There is no default, so Valkey will not listen\n# on a unix socket when not specified.\n#\n# unixsocket /tmp/valkey.sock\n# unixsocketperm 700\n\n# Close the connection after a client is idle for N seconds (0 to disable)\ntimeout 0\n\n# TCP keepalive.\n#\n# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence\n# of communication. This is useful for two reasons:\n#\n# 1) Detect dead peers.\n# 2) Take the connection alive from the point of view of network\n#    equipment in the middle.\n#\n# On Linux, the specified value (in seconds) is the period used to send ACKs.\n# Note that to close the connection the double of the time is needed.\n# On other kernels the period depends on the kernel configuration.\n#\n# A reasonable value for this option is 300 seconds, which is the new\n# Valkey default starting with Valkey 3.2.1.\ntcp-keepalive 300\n\n################################# GENERAL #####################################\n\n# By default Valkey does not run as a daemon. Use 'yes' if you need it.\n# Note that Valkey will write a pid file in /var/run/valkey.pid when daemonized.\ndaemonize no\n\n# If you run Valkey from upstart or systemd, Valkey can interact with your\n# supervision tree. Options:\n#   supervised no      - no supervision interaction\n#   supervised upstart - signal upstart by putting Valkey into SIGSTOP mode\n#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET\n#   supervised auto    - detect upstart or systemd method based on\n#                        UPSTART_JOB or NOTIFY_SOCKET environment variables\n# Note: these supervision methods only signal \"process is ready.\"\n#       They do not enable continuous liveness pings back to your supervisor.\nsupervised no\n\n# If a pid file is specified, Valkey writes it where specified at startup\n# and removes it at exit.\n#\n# When the server runs non daemonized, no pid file is created if none is\n# specified in the configuration. When the server is daemonized, the pid file\n# is used even if not specified, defaulting to \"/var/run/valkey.pid\".\n#\n# Creating a pid file is best effort: if Valkey is not able to create it\n# nothing bad happens, the server will start and run normally.\npidfile valkey.pid\n\n# Specify the server verbosity level.\n# This can be one of:\n# debug (a lot of information, useful for development/testing)\n# verbose (many rarely useful info, but not a mess like the debug level)\n# notice (moderately verbose, what you want in production probably)\n# warning (only very important / critical messages are logged)\nloglevel notice\n\n# Specify the log file name. Also the empty string can be used to force\n# Valkey to log on the standard output. Note that if you use standard\n# output for logging but daemonize, logs will be sent to /dev/null\n# logfile valkey.log\n\n# To enable logging to the system logger, just set 'syslog-enabled' to yes,\n# and optionally update the other syslog parameters to suit your needs.\n# syslog-enabled no\n\n# Specify the syslog identity.\n# syslog-ident valkey\n\n# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.\n# syslog-facility local0\n\n# Set the number of databases. The default database is DB 0, you can select\n# a different one on a per-connection basis using SELECT <dbid> where\n# dbid is a number between 0 and 'databases'-1\ndatabases 16\n\n# By default Valkey shows an ASCII art logo only when started to log to the\n# standard output and if the standard output is a TTY. Basically this means\n# that normally a logo is displayed only in interactive sessions.\n#\n# However it is possible to force the pre-4.0 behavior and always show a\n# ASCII art logo in startup logs by setting the following option to yes.\nalways-show-logo yes\n\n################################ SNAPSHOTTING  ################################\n#\n# Save the DB on disk:\n#\n#   save <seconds> <changes>\n#\n#   Will save the DB if both the given number of seconds and the given\n#   number of write operations against the DB occurred.\n#\n#   In the example below the behaviour will be to save:\n#   after 900 sec (15 min) if at least 1 key changed\n#   after 300 sec (5 min) if at least 10 keys changed\n#   after 60 sec if at least 10000 keys changed\n#\n#   Note: you can disable saving completely by commenting out all \"save\" lines.\n#\n#   It is also possible to remove all the previously configured save\n#   points by adding a save directive with a single empty string argument\n#   like in the following example:\n#\n#   save \"\"\n\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# By default Valkey will stop accepting writes if RDB snapshots are enabled\n# (at least one save point) and the latest background save failed.\n# This will make the user aware (in a hard way) that data is not persisting\n# on disk properly, otherwise chances are that no one will notice and some\n# disaster will happen.\n#\n# If the background saving process will start working again Valkey will\n# automatically allow writes again.\n#\n# However if you have setup your proper monitoring of the Valkey server\n# and persistence, you may want to disable this feature so that Valkey will\n# continue to work as usual even if there are problems with disk,\n# permissions, and so forth.\nstop-writes-on-bgsave-error yes\n\n# Compress string objects using LZF when dump .rdb databases?\n# For default that's set to 'yes' as it's almost always a win.\n# If you want to save some CPU in the saving child set it to 'no' but\n# the dataset will likely be bigger if you have compressible values or keys.\nrdbcompression yes\n\n# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.\n# This makes the format more resistant to corruption but there is a performance\n# hit to pay (around 10%) when saving and loading RDB files, so you can disable it\n# for maximum performances.\n#\n# RDB files created with checksum disabled have a checksum of zero that will\n# tell the loading code to skip the check.\nrdbchecksum yes\n\n# The filename where to dump the DB\ndbfilename dump.rdb\n\n# The working directory.\n#\n# The DB will be written inside this directory, with the filename specified\n# above using the 'dbfilename' configuration directive.\n#\n# The Append Only File will also be created inside this directory.\n#\n# Note that you must specify a directory here, not a file name.\ndir .devbox/virtenv/valkey/\n\n################################# REPLICATION #################################\n\n# Master-Slave replication. Use slaveof to make a Valkey instance a copy of\n# another Valkey server. A few things to understand ASAP about Valkey replication.\n#\n# 1) Valkey replication is asynchronous, but you can configure a master to\n#    stop accepting writes if it appears to be not connected with at least\n#    a given number of slaves.\n# 2) Valkey slaves are able to perform a partial resynchronization with the\n#    master if the replication link is lost for a relatively small amount of\n#    time. You may want to configure the replication backlog size (see the next\n#    sections of this file) with a sensible value depending on your needs.\n# 3) Replication is automatic and does not need user intervention. After a\n#    network partition slaves automatically try to reconnect to masters\n#    and resynchronize with them.\n#\n# slaveof <masterip> <masterport>\n\n# If the master is password protected (using the \"requirepass\" configuration\n# directive below) it is possible to tell the slave to authenticate before\n# starting the replication synchronization process, otherwise the master will\n# refuse the slave request.\n#\n# masterauth <master-password>\n\n# When a slave loses its connection with the master, or when the replication\n# is still in progress, the slave can act in two different ways:\n#\n# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will\n#    still reply to client requests, possibly with out of date data, or the\n#    data set may just be empty if this is the first synchronization.\n#\n# 2) if slave-serve-stale-data is set to 'no' the slave will reply with\n#    an error \"SYNC with master in progress\" to all the kind of commands\n#    but to INFO and SLAVEOF.\n#\nslave-serve-stale-data yes\n\n# You can configure a slave instance to accept writes or not. Writing against\n# a slave instance may be useful to store some ephemeral data (because data\n# written on a slave will be easily deleted after resync with the master) but\n# may also cause problems if clients are writing to it because of a\n# misconfiguration.\n#\n# Since Valkey 2.6 by default slaves are read-only.\n#\n# Note: read only slaves are not designed to be exposed to untrusted clients\n# on the internet. It's just a protection layer against misuse of the instance.\n# Still a read only slave exports by default all the administrative commands\n# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve\n# security of read only slaves using 'rename-command' to shadow all the\n# administrative / dangerous commands.\nslave-read-only yes\n\n# Replication SYNC strategy: disk or socket.\n#\n# -------------------------------------------------------\n# WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY\n# -------------------------------------------------------\n#\n# New slaves and reconnecting slaves that are not able to continue the replication\n# process just receiving differences, need to do what is called a \"full\n# synchronization\". An RDB file is transmitted from the master to the slaves.\n# The transmission can happen in two different ways:\n#\n# 1) Disk-backed: The Valkey master creates a new process that writes the RDB\n#                 file on disk. Later the file is transferred by the parent\n#                 process to the slaves incrementally.\n# 2) Diskless: The Valkey master creates a new process that directly writes the\n#              RDB file to slave sockets, without touching the disk at all.\n#\n# With disk-backed replication, while the RDB file is generated, more slaves\n# can be queued and served with the RDB file as soon as the current child producing\n# the RDB file finishes its work. With diskless replication instead once\n# the transfer starts, new slaves arriving will be queued and a new transfer\n# will start when the current one terminates.\n#\n# When diskless replication is used, the master waits a configurable amount of\n# time (in seconds) before starting the transfer in the hope that multiple slaves\n# will arrive and the transfer can be parallelized.\n#\n# With slow disks and fast (large bandwidth) networks, diskless replication\n# works better.\nrepl-diskless-sync no\n\n# When diskless replication is enabled, it is possible to configure the delay\n# the server waits in order to spawn the child that transfers the RDB via socket\n# to the slaves.\n#\n# This is important since once the transfer starts, it is not possible to serve\n# new slaves arriving, that will be queued for the next RDB transfer, so the server\n# waits a delay in order to let more slaves arrive.\n#\n# The delay is specified in seconds, and by default is 5 seconds. To disable\n# it entirely just set it to 0 seconds and the transfer will start ASAP.\nrepl-diskless-sync-delay 5\n\n# Slaves send PINGs to server in a predefined interval. It's possible to change\n# this interval with the repl_ping_slave_period option. The default value is 10\n# seconds.\n#\n# repl-ping-slave-period 10\n\n# The following option sets the replication timeout for:\n#\n# 1) Bulk transfer I/O during SYNC, from the point of view of slave.\n# 2) Master timeout from the point of view of slaves (data, pings).\n# 3) Slave timeout from the point of view of masters (REPLCONF ACK pings).\n#\n# It is important to make sure that this value is greater than the value\n# specified for repl-ping-slave-period otherwise a timeout will be detected\n# every time there is low traffic between the master and the slave.\n#\n# repl-timeout 60\n\n# Disable TCP_NODELAY on the slave socket after SYNC?\n#\n# If you select \"yes\" Valkey will use a smaller number of TCP packets and\n# less bandwidth to send data to slaves. But this can add a delay for\n# the data to appear on the slave side, up to 40 milliseconds with\n# Linux kernels using a default configuration.\n#\n# If you select \"no\" the delay for data to appear on the slave side will\n# be reduced but more bandwidth will be used for replication.\n#\n# By default we optimize for low latency, but in very high traffic conditions\n# or when the master and slaves are many hops away, turning this to \"yes\" may\n# be a good idea.\nrepl-disable-tcp-nodelay no\n\n# Set the replication backlog size. The backlog is a buffer that accumulates\n# slave data when slaves are disconnected for some time, so that when a slave\n# wants to reconnect again, often a full resync is not needed, but a partial\n# resync is enough, just passing the portion of data the slave missed while\n# disconnected.\n#\n# The bigger the replication backlog, the longer the time the slave can be\n# disconnected and later be able to perform a partial resynchronization.\n#\n# The backlog is only allocated once there is at least a slave connected.\n#\n# repl-backlog-size 1mb\n\n# After a master has no longer connected slaves for some time, the backlog\n# will be freed. The following option configures the amount of seconds that\n# need to elapse, starting from the time the last slave disconnected, for\n# the backlog buffer to be freed.\n#\n# Note that slaves never free the backlog for timeout, since they may be\n# promoted to masters later, and should be able to correctly \"partially\n# resynchronize\" with the slaves: hence they should always accumulate backlog.\n#\n# A value of 0 means to never release the backlog.\n#\n# repl-backlog-ttl 3600\n\n# The slave priority is an integer number published by Valkey in the INFO output.\n# It is used by Valkey Sentinel in order to select a slave to promote into a\n# master if the master is no longer working correctly.\n#\n# A slave with a low priority number is considered better for promotion, so\n# for instance if there are three slaves with priority 10, 100, 25 Sentinel will\n# pick the one with priority 10, that is the lowest.\n#\n# However a special priority of 0 marks the slave as not able to perform the\n# role of master, so a slave with priority of 0 will never be selected by\n# Valkey Sentinel for promotion.\n#\n# By default the priority is 100.\nslave-priority 100\n\n# It is possible for a master to stop accepting writes if there are less than\n# N slaves connected, having a lag less or equal than M seconds.\n#\n# The N slaves need to be in \"online\" state.\n#\n# The lag in seconds, that must be <= the specified value, is calculated from\n# the last ping received from the slave, that is usually sent every second.\n#\n# This option does not GUARANTEE that N replicas will accept the write, but\n# will limit the window of exposure for lost writes in case not enough slaves\n# are available, to the specified number of seconds.\n#\n# For example to require at least 3 slaves with a lag <= 10 seconds use:\n#\n# min-slaves-to-write 3\n# min-slaves-max-lag 10\n#\n# Setting one or the other to 0 disables the feature.\n#\n# By default min-slaves-to-write is set to 0 (feature disabled) and\n# min-slaves-max-lag is set to 10.\n\n# A Valkey master is able to list the address and port of the attached\n# slaves in different ways. For example the \"INFO replication\" section\n# offers this information, which is used, among other tools, by\n# Valkey Sentinel in order to discover slave instances.\n# Another place where this info is available is in the output of the\n# \"ROLE\" command of a master.\n#\n# The listed IP and address normally reported by a slave is obtained\n# in the following way:\n#\n#   IP: The address is auto detected by checking the peer address\n#   of the socket used by the slave to connect with the master.\n#\n#   Port: The port is communicated by the slave during the replication\n#   handshake, and is normally the port that the slave is using to\n#   list for connections.\n#\n# However when port forwarding or Network Address Translation (NAT) is\n# used, the slave may be actually reachable via different IP and port\n# pairs. The following two options can be used by a slave in order to\n# report to its master a specific set of IP and port, so that both INFO\n# and ROLE will report those values.\n#\n# There is no need to use both the options if you need to override just\n# the port or the IP address.\n#\n# slave-announce-ip 5.5.5.5\n# slave-announce-port 1234\n\n################################## SECURITY ###################################\n\n# Require clients to issue AUTH <PASSWORD> before processing any other\n# commands.  This might be useful in environments in which you do not trust\n# others with access to the host running valkey-server.\n#\n# This should stay commented out for backward compatibility and because most\n# people do not need auth (e.g. they run their own servers).\n#\n# Warning: since Valkey is pretty fast an outside user can try up to\n# 150k passwords per second against a good box. This means that you should\n# use a very strong password otherwise it will be very easy to break.\n#\n# requirepass foobared\n\n# Command renaming.\n#\n# It is possible to change the name of dangerous commands in a shared\n# environment. For instance the CONFIG command may be renamed into something\n# hard to guess so that it will still be available for internal-use tools\n# but not available for general clients.\n#\n# Example:\n#\n# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52\n#\n# It is also possible to completely kill a command by renaming it into\n# an empty string:\n#\n# rename-command CONFIG \"\"\n#\n# Please note that changing the name of commands that are logged into the\n# AOF file or transmitted to slaves may cause problems.\n\n################################### CLIENTS ####################################\n\n# Set the max number of connected clients at the same time. By default\n# this limit is set to 10000 clients, however if the Valkey server is not\n# able to configure the process file limit to allow for the specified limit\n# the max number of allowed clients is set to the current file limit\n# minus 32 (as Valkey reserves a few file descriptors for internal uses).\n#\n# Once the limit is reached Valkey will close all the new connections sending\n# an error 'max number of clients reached'.\n#\n# maxclients 10000\n\n############################## MEMORY MANAGEMENT ################################\n\n# Set a memory usage limit to the specified amount of bytes.\n# When the memory limit is reached Valkey will try to remove keys\n# according to the eviction policy selected (see maxmemory-policy).\n#\n# If Valkey can't remove keys according to the policy, or if the policy is\n# set to 'noeviction', Valkey will start to reply with errors to commands\n# that would use more memory, like SET, LPUSH, and so on, and will continue\n# to reply to read-only commands like GET.\n#\n# This option is usually useful when using Valkey as an LRU or LFU cache, or to\n# set a hard memory limit for an instance (using the 'noeviction' policy).\n#\n# WARNING: If you have slaves attached to an instance with maxmemory on,\n# the size of the output buffers needed to feed the slaves are subtracted\n# from the used memory count, so that network problems / resyncs will\n# not trigger a loop where keys are evicted, and in turn the output\n# buffer of slaves is full with DELs of keys evicted triggering the deletion\n# of more keys, and so forth until the database is completely emptied.\n#\n# In short... if you have slaves attached it is suggested that you set a lower\n# limit for maxmemory so that there is some free RAM on the system for slave\n# output buffers (but this is not needed if the policy is 'noeviction').\n#\n# maxmemory <bytes>\n\n# MAXMEMORY POLICY: how Valkey will select what to remove when maxmemory\n# is reached. You can select among five behaviors:\n#\n# volatile-lru -> Evict using approximated LRU among the keys with an expire set.\n# allkeys-lru -> Evict any key using approximated LRU.\n# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.\n# allkeys-lfu -> Evict any key using approximated LFU.\n# volatile-random -> Remove a random key among the ones with an expire set.\n# allkeys-random -> Remove a random key, any key.\n# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)\n# noeviction -> Don't evict anything, just return an error on write operations.\n#\n# LRU means Least Recently Used\n# LFU means Least Frequently Used\n#\n# Both LRU, LFU and volatile-ttl are implemented using approximated\n# randomized algorithms.\n#\n# Note: with any of the above policies, Valkey will return an error on write\n#       operations, when there are no suitable keys for eviction.\n#\n#       At the date of writing these commands are: set setnx setex append\n#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd\n#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby\n#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby\n#       getset mset msetnx exec sort\n#\n# The default is:\n#\n# maxmemory-policy noeviction\n\n# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated\n# algorithms (in order to save memory), so you can tune it for speed or\n# accuracy. For default Valkey will check five keys and pick the one that was\n# used less recently, you can change the sample size using the following\n# configuration directive.\n#\n# The default of 5 produces good enough results. 10 Approximates very closely\n# true LRU but costs more CPU. 3 is faster but not very accurate.\n#\n# maxmemory-samples 5\n\n############################# LAZY FREEING ####################################\n\n# Valkey has two primitives to delete keys. One is called DEL and is a blocking\n# deletion of the object. It means that the server stops processing new commands\n# in order to reclaim all the memory associated with an object in a synchronous\n# way. If the key deleted is associated with a small object, the time needed\n# in order to execute the DEL command is very small and comparable to most other\n# O(1) or O(log_N) commands in Valkey. However if the key is associated with an\n# aggregated value containing millions of elements, the server can block for\n# a long time (even seconds) in order to complete the operation.\n#\n# For the above reasons Valkey also offers non blocking deletion primitives\n# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and\n# FLUSHDB commands, in order to reclaim memory in background. Those commands\n# are executed in constant time. Another thread will incrementally free the\n# object in the background as fast as possible.\n#\n# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.\n# It's up to the design of the application to understand when it is a good\n# idea to use one or the other. However the Valkey server sometimes has to\n# delete keys or flush the whole database as a side effect of other operations.\n# Specifically Valkey deletes objects independently of a user call in the\n# following scenarios:\n#\n# 1) On eviction, because of the maxmemory and maxmemory policy configurations,\n#    in order to make room for new data, without going over the specified\n#    memory limit.\n# 2) Because of expire: when a key with an associated time to live (see the\n#    EXPIRE command) must be deleted from memory.\n# 3) Because of a side effect of a command that stores data on a key that may\n#    already exist. For example the RENAME command may delete the old key\n#    content when it is replaced with another one. Similarly SUNIONSTORE\n#    or SORT with STORE option may delete existing keys. The SET command\n#    itself removes any old content of the specified key in order to replace\n#    it with the specified string.\n# 4) During replication, when a slave performs a full resynchronization with\n#    its master, the content of the whole database is removed in order to\n#    load the RDB file just transferred.\n#\n# In all the above cases the default is to delete objects in a blocking way,\n# like if DEL was called. However you can configure each case specifically\n# in order to instead release memory in a non-blocking way like if UNLINK\n# was called, using the following configuration directives:\n\nlazyfree-lazy-eviction no\nlazyfree-lazy-expire no\nlazyfree-lazy-server-del no\nslave-lazy-flush no\n\n############################## APPEND ONLY MODE ###############################\n\n# By default Valkey asynchronously dumps the dataset on disk. This mode is\n# good enough in many applications, but an issue with the Valkey process or\n# a power outage may result into a few minutes of writes lost (depending on\n# the configured save points).\n#\n# The Append Only File is an alternative persistence mode that provides\n# much better durability. For instance using the default data fsync policy\n# (see later in the config file) Valkey can lose just one second of writes in a\n# dramatic event like a server power outage, or a single write if something\n# wrong with the Valkey process itself happens, but the operating system is\n# still running correctly.\n#\n# AOF and RDB persistence can be enabled at the same time without problems.\n# If the AOF is enabled on startup Valkey will load the AOF, that is the file\n# with the better durability guarantees.\n#\n# Please check https://valkey.io/docs/topics/persistence/ for more information.\n\nappendonly no\n\n# The name of the append only file (default: \"appendonly.aof\")\n\nappendfilename \"appendonly.aof\"\n\n# The fsync() call tells the Operating System to actually write data on disk\n# instead of waiting for more data in the output buffer. Some OS will really flush\n# data on disk, some other OS will just try to do it ASAP.\n#\n# Valkey supports three different modes:\n#\n# no: don't fsync, just let the OS flush the data when it wants. Faster.\n# always: fsync after every write to the append only log. Slow, Safest.\n# everysec: fsync only one time every second. Compromise.\n#\n# The default is \"everysec\", as that's usually the right compromise between\n# speed and data safety. It's up to you to understand if you can relax this to\n# \"no\" that will let the operating system flush the output buffer when\n# it wants, for better performances (but if you can live with the idea of\n# some data loss consider the default persistence mode that's snapshotting),\n# or on the contrary, use \"always\" that's very slow but a bit safer than\n# everysec.\n#\n# More details please check the following article:\n# http://antirez.com/post/redis-persistence-demystified.html\n#\n# If unsure, use \"everysec\".\n\n# appendfsync always\nappendfsync everysec\n# appendfsync no\n\n# When the AOF fsync policy is set to always or everysec, and a background\n# saving process (a background save or AOF log background rewriting) is\n# performing a lot of I/O against the disk, in some Linux configurations\n# Valkey may block too long on the fsync() call. Note that there is no fix for\n# this currently, as even performing fsync in a different thread will block\n# our synchronous write(2) call.\n#\n# In order to mitigate this problem it's possible to use the following option\n# that will prevent fsync() from being called in the main process while a\n# BGSAVE or BGREWRITEAOF is in progress.\n#\n# This means that while another child is saving, the durability of Valkey is\n# the same as \"appendfsync none\". In practical terms, this means that it is\n# possible to lose up to 30 seconds of log in the worst scenario (with the\n# default Linux settings).\n#\n# If you have latency problems turn this to \"yes\". Otherwise leave it as\n# \"no\" that is the safest pick from the point of view of durability.\n\nno-appendfsync-on-rewrite no\n\n# Automatic rewrite of the append only file.\n# Valkey is able to automatically rewrite the log file implicitly calling\n# BGREWRITEAOF when the AOF log size grows by the specified percentage.\n#\n# This is how it works: Valkey remembers the size of the AOF file after the\n# latest rewrite (if no rewrite has happened since the restart, the size of\n# the AOF at startup is used).\n#\n# This base size is compared to the current size. If the current size is\n# bigger than the specified percentage, the rewrite is triggered. Also\n# you need to specify a minimal size for the AOF file to be rewritten, this\n# is useful to avoid rewriting the AOF file even if the percentage increase\n# is reached but it is still pretty small.\n#\n# Specify a percentage of zero in order to disable the automatic AOF\n# rewrite feature.\n\nauto-aof-rewrite-percentage 100\nauto-aof-rewrite-min-size 64mb\n\n# An AOF file may be found to be truncated at the end during the Valkey\n# startup process, when the AOF data gets loaded back into memory.\n# This may happen when the system where Valkey is running\n# crashes, especially when an ext4 filesystem is mounted without the\n# data=ordered option (however this can't happen when Valkey itself\n# crashes or aborts but the operating system still works correctly).\n#\n# Valkey can either exit with an error when this happens, or load as much\n# data as possible (the default now) and start if the AOF file is found\n# to be truncated at the end. The following option controls this behavior.\n#\n# If aof-load-truncated is set to yes, a truncated AOF file is loaded and\n# the Valkey server starts emitting a log to inform the user of the event.\n# Otherwise if the option is set to no, the server aborts with an error\n# and refuses to start. When the option is set to no, the user requires\n# to fix the AOF file using the \"valkey-check-aof\" utility before to restart\n# the server.\n#\n# Note that if the AOF file will be found to be corrupted in the middle\n# the server will still exit with an error. This option only applies when\n# Valkey will try to read more data from the AOF file but not enough bytes\n# will be found.\naof-load-truncated yes\n\n# When rewriting the AOF file, Valkey is able to use an RDB preamble in the\n# AOF file for faster rewrites and recoveries. When this option is turned\n# on the rewritten AOF file is composed of two different stanzas:\n#\n#   [RDB file][AOF tail]\n#\n# When loading Valkey recognizes that the AOF file starts with the \"VALKEY\"\n# string and loads the prefixed RDB file, and continues loading the AOF\n# tail.\n#\n# This is currently turned off by default in order to avoid the surprise\n# of a format change, but will at some point be used as the default.\naof-use-rdb-preamble no\n\n################################ LUA SCRIPTING  ###############################\n\n# Max execution time of a Lua script in milliseconds.\n#\n# If the maximum execution time is reached Valkey will log that a script is\n# still in execution after the maximum allowed time and will start to\n# reply to queries with an error.\n#\n# When a long running script exceeds the maximum execution time only the\n# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be\n# used to stop a script that did not yet called write commands. The second\n# is the only way to shut down the server in the case a write command was\n# already issued by the script but the user doesn't want to wait for the natural\n# termination of the script.\n#\n# Set it to 0 or a negative value for unlimited execution without warnings.\nlua-time-limit 5000\n\n################################ VALKEY CLUSTER  ###############################\n#\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n# WARNING EXPERIMENTAL: Valkey Cluster is considered to be stable code, however\n# in order to mark it as \"mature\" we need to wait for a non trivial percentage\n# of users to deploy it in production.\n# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n#\n# Normal Valkey instances can't be part of a Valkey Cluster; only nodes that are\n# started as cluster nodes can. In order to start a Valkey instance as a\n# cluster node enable the cluster support uncommenting the following:\n#\n# cluster-enabled yes\n\n# Every cluster node has a cluster configuration file. This file is not\n# intended to be edited by hand. It is created and updated by Valkey nodes.\n# Every Valkey Cluster node requires a different cluster configuration file.\n# Make sure that instances running in the same system do not have\n# overlapping cluster configuration file names.\n#\n# cluster-config-file nodes-6379.conf\n\n# Cluster node timeout is the amount of milliseconds a node must be unreachable\n# for it to be considered in failure state.\n# Most other internal time limits are multiple of the node timeout.\n#\n# cluster-node-timeout 15000\n\n# A slave of a failing master will avoid to start a failover if its data\n# looks too old.\n#\n# There is no simple way for a slave to actually have an exact measure of\n# its \"data age\", so the following two checks are performed:\n#\n# 1) If there are multiple slaves able to failover, they exchange messages\n#    in order to try to give an advantage to the slave with the best\n#    replication offset (more data from the master processed).\n#    Slaves will try to get their rank by offset, and apply to the start\n#    of the failover a delay proportional to their rank.\n#\n# 2) Every single slave computes the time of the last interaction with\n#    its master. This can be the last ping or command received (if the master\n#    is still in the \"connected\" state), or the time that elapsed since the\n#    disconnection with the master (if the replication link is currently down).\n#    If the last interaction is too old, the slave will not try to failover\n#    at all.\n#\n# The point \"2\" can be tuned by user. Specifically a slave will not perform\n# the failover if, since the last interaction with the master, the time\n# elapsed is greater than:\n#\n#   (node-timeout * slave-validity-factor) + repl-ping-slave-period\n#\n# So for example if node-timeout is 30 seconds, and the slave-validity-factor\n# is 10, and assuming a default repl-ping-slave-period of 10 seconds, the\n# slave will not try to failover if it was not able to talk with the master\n# for longer than 310 seconds.\n#\n# A large slave-validity-factor may allow slaves with too old data to failover\n# a master, while a too small value may prevent the cluster from being able to\n# elect a slave at all.\n#\n# For maximum availability, it is possible to set the slave-validity-factor\n# to a value of 0, which means, that slaves will always try to failover the\n# master regardless of the last time they interacted with the master.\n# (However they'll always try to apply a delay proportional to their\n# offset rank).\n#\n# Zero is the only value able to guarantee that when all the partitions heal\n# the cluster will always be able to continue.\n#\n# cluster-slave-validity-factor 10\n\n# Cluster slaves are able to migrate to orphaned masters, that are masters\n# that are left without working slaves. This improves the cluster ability\n# to resist to failures as otherwise an orphaned master can't be failed over\n# in case of failure if it has no working slaves.\n#\n# Slaves migrate to orphaned masters only if there are still at least a\n# given number of other working slaves for their old master. This number\n# is the \"migration barrier\". A migration barrier of 1 means that a slave\n# will migrate only if there is at least 1 other working slave for its master\n# and so forth. It usually reflects the number of slaves you want for every\n# master in your cluster.\n#\n# Default is 1 (slaves migrate only if their masters remain with at least\n# one slave). To disable migration just set it to a very large value.\n# A value of 0 can be set but is useful only for debugging and dangerous\n# in production.\n#\n# cluster-migration-barrier 1\n\n# By default Valkey Cluster nodes stop accepting queries if they detect there\n# is at least an hash slot uncovered (no available node is serving it).\n# This way if the cluster is partially down (for example a range of hash slots\n# are no longer covered) all the cluster becomes, eventually, unavailable.\n# It automatically returns available as soon as all the slots are covered again.\n#\n# However sometimes you want the subset of the cluster which is working,\n# to continue to accept queries for the part of the key space that is still\n# covered. In order to do so, just set the cluster-require-full-coverage\n# option to no.\n#\n# cluster-require-full-coverage yes\n\n# This option, when set to yes, prevents slaves from trying to failover its\n# master during master failures. However the master can still perform a\n# manual failover, if forced to do so.\n#\n# This is useful in different scenarios, especially in the case of multiple\n# data center operations, where we want one side to never be promoted if not\n# in the case of a total DC failure.\n#\n# cluster-slave-no-failover no\n\n# In order to setup your cluster make sure to read the documentation\n# available at http://valkey.io web site.\n\n########################## CLUSTER DOCKER/NAT support  ########################\n\n# In certain deployments, Valkey Cluster nodes address discovery fails, because\n# addresses are NAT-ted or because ports are forwarded (the typical case is\n# Docker and other containers).\n#\n# In order to make Valkey Cluster working in such environments, a static\n# configuration where each node knows its public address is needed. The\n# following two options are used for this scope, and are:\n#\n# * cluster-announce-ip\n# * cluster-announce-port\n# * cluster-announce-bus-port\n#\n# Each instruct the node about its address, client port, and cluster message\n# bus port. The information is then published in the header of the bus packets\n# so that other nodes will be able to correctly map the address of the node\n# publishing the information.\n#\n# If the above options are not used, the normal Valkey Cluster auto-detection\n# will be used instead.\n#\n# Note that when remapped, the bus port may not be at the fixed offset of\n# clients port + 10000, so you can specify any port and bus-port depending\n# on how they get remapped. If the bus-port is not set, a fixed offset of\n# 10000 will be used as usually.\n#\n# Example:\n#\n# cluster-announce-ip 10.1.1.5\n# cluster-announce-port 6379\n# cluster-announce-bus-port 6380\n\n################################## SLOW LOG ###################################\n\n# The Valkey Slow Log is a system to log queries that exceeded a specified\n# execution time. The execution time does not include the I/O operations\n# like talking with the client, sending the reply and so forth,\n# but just the time needed to actually execute the command (this is the only\n# stage of command execution where the thread is blocked and can not serve\n# other requests in the meantime).\n#\n# You can configure the slow log with two parameters: one tells Valkey\n# what is the execution time, in microseconds, to exceed in order for the\n# command to get logged, and the other parameter is the length of the\n# slow log. When a new command is logged the oldest one is removed from the\n# queue of logged commands.\n\n# The following time is expressed in microseconds, so 1000000 is equivalent\n# to one second. Note that a negative number disables the slow log, while\n# a value of zero forces the logging of every command.\nslowlog-log-slower-than 10000\n\n# There is no limit to this length. Just be aware that it will consume memory.\n# You can reclaim memory used by the slow log with SLOWLOG RESET.\nslowlog-max-len 128\n\n################################ LATENCY MONITOR ##############################\n\n# The Valkey latency monitoring subsystem samples different operations\n# at runtime in order to collect data related to possible sources of\n# latency of a Valkey instance.\n#\n# Via the LATENCY command this information is available to the user that can\n# print graphs and obtain reports.\n#\n# The system only logs operations that were performed in a time equal or\n# greater than the amount of milliseconds specified via the\n# latency-monitor-threshold configuration directive. When its value is set\n# to zero, the latency monitor is turned off.\n#\n# By default latency monitoring is disabled since it is mostly not needed\n# if you don't have latency issues, and collecting data has a performance\n# impact, that while very small, can be measured under big load. Latency\n# monitoring can easily be enabled at runtime using the command\n# \"CONFIG SET latency-monitor-threshold <milliseconds>\" if needed.\nlatency-monitor-threshold 0\n\n############################# EVENT NOTIFICATION ##############################\n\n# Valkey can notify Pub/Sub clients about events happening in the key space.\n# This feature is documented at https://valkey.io/docs/topics/notifications/\n#\n# For instance if keyspace events notification is enabled, and a client\n# performs a DEL operation on key \"foo\" stored in the Database 0, two\n# messages will be published via Pub/Sub:\n#\n# PUBLISH __keyspace@0__:foo del\n# PUBLISH __keyevent@0__:del foo\n#\n# It is possible to select the events that Valkey will notify among a set\n# of classes. Every class is identified by a single character:\n#\n#  K     Keyspace events, published with __keyspace@<db>__ prefix.\n#  E     Keyevent events, published with __keyevent@<db>__ prefix.\n#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...\n#  $     String commands\n#  l     List commands\n#  s     Set commands\n#  h     Hash commands\n#  z     Sorted set commands\n#  x     Expired events (events generated every time a key expires)\n#  e     Evicted events (events generated when a key is evicted for maxmemory)\n#  A     Alias for g$lshzxe, so that the \"AKE\" string means all the events.\n#\n#  The \"notify-keyspace-events\" takes as argument a string that is composed\n#  of zero or multiple characters. The empty string means that notifications\n#  are disabled.\n#\n#  Example: to enable list and generic events, from the point of view of the\n#           event name, use:\n#\n#  notify-keyspace-events Elg\n#\n#  Example 2: to get the stream of the expired keys subscribing to channel\n#             name __keyevent@0__:expired use:\n#\n#  notify-keyspace-events Ex\n#\n#  By default all notifications are disabled because most users don't need\n#  this feature and the feature has some overhead. Note that if you don't\n#  specify at least one of K or E, no events will be delivered.\nnotify-keyspace-events \"\"\n\n############################### ADVANCED CONFIG ###############################\n\n# Hashes are encoded using a memory efficient data structure when they have a\n# small number of entries, and the biggest entry does not exceed a given\n# threshold. These thresholds can be configured using the following directives.\nhash-max-ziplist-entries 512\nhash-max-ziplist-value 64\n\n# Lists are also encoded in a special way to save a lot of space.\n# The number of entries allowed per internal list node can be specified\n# as a fixed maximum size or a maximum number of elements.\n# For a fixed maximum size, use -5 through -1, meaning:\n# -5: max size: 64 Kb  <-- not recommended for normal workloads\n# -4: max size: 32 Kb  <-- not recommended\n# -3: max size: 16 Kb  <-- probably not recommended\n# -2: max size: 8 Kb   <-- good\n# -1: max size: 4 Kb   <-- good\n# Positive numbers mean store up to _exactly_ that number of elements\n# per list node.\n# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),\n# but if your use case is unique, adjust the settings as necessary.\nlist-max-ziplist-size -2\n\n# Lists may also be compressed.\n# Compress depth is the number of quicklist ziplist nodes from *each* side of\n# the list to *exclude* from compression.  The head and tail of the list\n# are always uncompressed for fast push/pop operations.  Settings are:\n# 0: disable all list compression\n# 1: depth 1 means \"don't start compressing until after 1 node into the list,\n#    going from either the head or tail\"\n#    So: [head]->node->node->...->node->[tail]\n#    [head], [tail] will always be uncompressed; inner nodes will compress.\n# 2: [head]->[next]->node->node->...->node->[prev]->[tail]\n#    2 here means: don't compress head or head->next or tail->prev or tail,\n#    but compress all nodes between them.\n# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]\n# etc.\nlist-compress-depth 0\n\n# Sets have a special encoding in just one case: when a set is composed\n# of just strings that happen to be integers in radix 10 in the range\n# of 64 bit signed integers.\n# The following configuration setting sets the limit in the size of the\n# set in order to use this special memory saving encoding.\nset-max-intset-entries 512\n\n# Similarly to hashes and lists, sorted sets are also specially encoded in\n# order to save a lot of space. This encoding is only used when the length and\n# elements of a sorted set are below the following limits:\nzset-max-ziplist-entries 128\nzset-max-ziplist-value 64\n\n# HyperLogLog sparse representation bytes limit. The limit includes the\n# 16 bytes header. When an HyperLogLog using the sparse representation crosses\n# this limit, it is converted into the dense representation.\n#\n# A value greater than 16000 is totally useless, since at that point the\n# dense representation is more memory efficient.\n#\n# The suggested value is ~ 3000 in order to have the benefits of\n# the space efficient encoding without slowing down too much PFADD,\n# which is O(N) with the sparse encoding. The value can be raised to\n# ~ 10000 when CPU is not a concern, but space is, and the data set is\n# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.\nhll-sparse-max-bytes 3000\n\n# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in\n# order to help rehashing the main Valkey hash table (the one mapping top-level\n# keys to values). The hash table implementation Valkey uses (see dict.c)\n# performs a lazy rehashing: the more operation you run into a hash table\n# that is rehashing, the more rehashing \"steps\" are performed, so if the\n# server is idle the rehashing is never complete and some more memory is used\n# by the hash table.\n#\n# The default is to use this millisecond 10 times every second in order to\n# actively rehash the main dictionaries, freeing memory when possible.\n#\n# If unsure:\n# use \"activerehashing no\" if you have hard latency requirements and it is\n# not a good thing in your environment that Valkey can reply from time to time\n# to queries with 2 milliseconds delay.\n#\n# use \"activerehashing yes\" if you don't have such hard requirements but\n# want to free memory asap when possible.\nactiverehashing yes\n\n# The client output buffer limits can be used to force disconnection of clients\n# that are not reading data from the server fast enough for some reason (a\n# common reason is that a Pub/Sub client can't consume messages as fast as the\n# publisher can produce them).\n#\n# The limit can be set differently for the three different classes of clients:\n#\n# normal -> normal clients including MONITOR clients\n# slave  -> slave clients\n# pubsub -> clients subscribed to at least one pubsub channel or pattern\n#\n# The syntax of every client-output-buffer-limit directive is the following:\n#\n# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>\n#\n# A client is immediately disconnected once the hard limit is reached, or if\n# the soft limit is reached and remains reached for the specified number of\n# seconds (continuously).\n# So for instance if the hard limit is 32 megabytes and the soft limit is\n# 16 megabytes / 10 seconds, the client will get disconnected immediately\n# if the size of the output buffers reach 32 megabytes, but will also get\n# disconnected if the client reaches 16 megabytes and continuously overcomes\n# the limit for 10 seconds.\n#\n# By default normal clients are not limited because they don't receive data\n# without asking (in a push way), but just after a request, so only\n# asynchronous clients may create a scenario where data is requested faster\n# than it can read.\n#\n# Instead there is a default limit for pubsub and slave clients, since\n# subscribers and slaves receive data in a push fashion.\n#\n# Both the hard or the soft limit can be disabled by setting them to zero.\nclient-output-buffer-limit normal 0 0 0\nclient-output-buffer-limit slave 256mb 64mb 60\nclient-output-buffer-limit pubsub 32mb 8mb 60\n\n# Client query buffers accumulate new commands. They are limited to a fixed\n# amount by default in order to avoid that a protocol desynchronization (for\n# instance due to a bug in the client) will lead to unbound memory usage in\n# the query buffer. However you can configure it here if you have very special\n# needs, such us huge multi/exec requests or alike.\n#\n# client-query-buffer-limit 1gb\n\n# In the Valkey protocol, bulk requests, that are, elements representing single\n# strings, are normally limited to 512 mb. However you can change this limit\n# here.\n#\n# proto-max-bulk-len 512mb\n\n# Valkey calls an internal function to perform many background tasks, like\n# closing connections of clients in timeout, purging expired keys that are\n# never requested, and so forth.\n#\n# Not all tasks are performed with the same frequency, but Valkey checks for\n# tasks to perform according to the specified \"hz\" value.\n#\n# By default \"hz\" is set to 10. Raising the value will use more CPU when\n# Valkey is idle, but at the same time will make Valkey more responsive when\n# there are many keys expiring at the same time, and timeouts may be\n# handled with more precision.\n#\n# The range is between 1 and 500, however a value over 100 is usually not\n# a good idea. Most users should use the default of 10 and raise this up to\n# 100 only in environments where very low latency is required.\nhz 10\n\n# When a child rewrites the AOF file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\naof-rewrite-incremental-fsync yes\n\n# Valkey LFU eviction (see maxmemory setting) can be tuned. However it is a good\n# idea to start with the default settings and only change them after investigating\n# how to improve the performances and how the keys LFU change over time, which\n# is possible to inspect via the OBJECT FREQ command.\n#\n# There are two tunable parameters in the Valkey LFU implementation: the\n# counter logarithm factor and the counter decay time. It is important to\n# understand what the two parameters mean before changing them.\n#\n# The LFU counter is just 8 bits per key, it's maximum value is 255, so Valkey\n# uses a probabilistic increment with logarithmic behavior. Given the value\n# of the old counter, when a key is accessed, the counter is incremented in\n# this way:\n#\n# 1. A random number R between 0 and 1 is extracted.\n# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).\n# 3. The counter is incremented only if R < P.\n#\n# The default lfu-log-factor is 10. This is a table of how the frequency\n# counter changes with a different number of accesses with different\n# logarithmic factors:\n#\n# +--------+------------+------------+------------+------------+------------+\n# | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |\n# +--------+------------+------------+------------+------------+------------+\n# | 0      | 104        | 255        | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 1      | 18         | 49         | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 10     | 10         | 18         | 142        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 100    | 8          | 11         | 49         | 143        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n#\n# NOTE: The above table was obtained by running the following commands:\n#\n#   valkey-benchmark -n 1000000 incr foo\n#   valkey-cli object freq foo\n#\n# NOTE 2: The counter initial value is 5 in order to give new objects a chance\n# to accumulate hits.\n#\n# The counter decay time is the time, in minutes, that must elapse in order\n# for the key counter to be divided by two (or decremented if it has a value\n# less <= 10).\n#\n# The default value for the lfu-decay-time is 1. A Special value of 0 means to\n# decay the counter every time it happens to be scanned.\n#\n# lfu-log-factor 10\n# lfu-decay-time 1\n\n########################### ACTIVE DEFRAGMENTATION #######################\n#\n# WARNING THIS FEATURE IS EXPERIMENTAL. However it was stress tested\n# even in production and manually tested by multiple engineers for some\n# time.\n#\n# What is active defragmentation?\n# -------------------------------\n#\n# Active (online) defragmentation allows a Valkey server to compact the\n# spaces left between small allocations and deallocations of data in memory,\n# thus allowing to reclaim back memory.\n#\n# Fragmentation is a natural process that happens with every allocator (but\n# less so with Jemalloc, fortunately) and certain workloads. Normally a server\n# restart is needed in order to lower the fragmentation, or at least to flush\n# away all the data and create it again. However thanks to this feature\n# implemented by Oran Agra for Valkey 4.0 this process can happen at runtime\n# in an \"hot\" way, while the server is running.\n#\n# Basically when the fragmentation is over a certain level (see the\n# configuration options below) Valkey will start to create new copies of the\n# values in contiguous memory regions by exploiting certain specific Jemalloc\n# features (in order to understand if an allocation is causing fragmentation\n# and to allocate it in a better place), and at the same time, will release the\n# old copies of the data. This process, repeated incrementally for all the keys\n# will cause the fragmentation to drop back to normal values.\n#\n# Important things to understand:\n#\n# 1. This feature is disabled by default, and only works if you compiled Valkey\n#    to use the copy of Jemalloc we ship with the source code of Valkey.\n#    This is the default with Linux builds.\n#\n# 2. You never need to enable this feature if you don't have fragmentation\n#    issues.\n#\n# 3. Once you experience fragmentation, you can enable this feature when\n#    needed with the command \"CONFIG SET activedefrag yes\".\n#\n# The configuration parameters are able to fine tune the behavior of the\n# defragmentation process. If you are not sure about what they mean it is\n# a good idea to leave the defaults untouched.\n\n# Enabled active defragmentation\n# activedefrag yes\n\n# Minimum amount of fragmentation waste to start active defrag\n# active-defrag-ignore-bytes 100mb\n\n# Minimum percentage of fragmentation to start active defrag\n# active-defrag-threshold-lower 10\n\n# Maximum percentage of fragmentation at which we use maximum effort\n# active-defrag-threshold-upper 100\n\n# Minimal effort for defrag in CPU percentage\n# active-defrag-cycle-min 25\n\n# Maximal effort for defrag in CPU percentage\n# active-defrag-cycle-max 75"
  },
  {
    "path": "plugins/valkey.json",
    "content": "{\n    \"name\": \"valkey\",\n    \"version\": \"0.0.1\",\n    \"description\": \"Running `devbox services start valkey` will start valkey as a daemon in the background. \\n\\nYou can manually start Valkey in the foreground by running `valkey-server $VALKEY_CONF --port $VALKEY_PORT`. \\n\\nLogs, pidfile, and data dumps are stored in `.devbox/virtenv/valkey`. You can change this by modifying the `dir` directive in `devbox.d/valkey/valkey.conf`\",\n    \"env\": {\n        \"VALKEY_PORT\": \"6379\",\n        \"VALKEY_CONF\": \"{{ .DevboxDir }}/valkey.conf\"\n    },\n    \"create_files\": {\n        \"{{ .DevboxDir }}/valkey.conf\": \"valkey/valkey.conf\",\n        \"{{ .Virtenv }}/process-compose.yaml\": \"valkey/process-compose.yaml\"\n    }\n}"
  },
  {
    "path": "plugins/web/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hello World!</title>\n  </head>\n  <body>\n    Hello World!\n  </body>\n</html>\n"
  },
  {
    "path": "scripts/gofumpt.sh",
    "content": "#!/bin/bash\n\nfd --extension go --exec-batch go tool gofumpt -extra -w\n\nif [ -n \"${CI:-}\" ]; then\n\tgit diff --exit-code\nfi\n"
  },
  {
    "path": "testscripts/Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n\n# This Dockerfile is designed to be as distro-agnostic as possible. By default\n# it uses ubuntu:noble as its base image, but this can be overridden by setting\n# the BASEIMAGE and BASETAG build args. This makes it easier to run Devbox tests\n# against different distros/versions without creating a bunch of separate\n# Dockerfiles.\n\nARG BASEIMAGE=ubuntu\nARG BASETAG=noble\n\nFROM scratch AS installer\n\nADD --chmod=0755 --link https://install.determinate.systems/nix/nix-installer-x86_64-linux /nix-installer-linux-amd64\nADD --chmod=0755 --link https://install.determinate.systems/nix/nix-installer-aarch64-linux /nix-installer-linux-arm64\n\nFROM $BASEIMAGE:$BASETAG\n\nARG TARGETOS\nARG TARGETARCH\n\nENV XDG_CACHE_HOME=/nix/cache\n\nCOPY --from=installer --link /nix-installer-$TARGETOS-$TARGETARCH nix-installer\nRUN <<EOF\nset -e\n\nmkdir -p \"$XDG_CACHE_HOME\"\n\n# Setting this env var is the same as passing --extra-conf to nix-installer.\nexport NIX_INSTALLER_EXTRA_CONF=\"filter-syscalls = false\nsandbox = false\nexperimental-features = nix-command flakes fetch-closure ca-derivations\nuse-xdg-base-directories = true\"\n\n./nix-installer install linux --no-confirm --logger full --init none\nrm nix-installer\nEOF\nENV SSL_CERT_FILE=/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt\n\nCOPY --link */*.test.txt /devbox/testscripts/\nCOPY --link testscripts-$TARGETOS-$TARGETARCH /devbox/testscripts/test\n\nVOLUME \"/nix\"\nWORKDIR /devbox/testscripts\nENTRYPOINT [ \"/devbox/testscripts/test\" ]\n"
  },
  {
    "path": "testscripts/README.md",
    "content": "Test devbox using the testscripts framework.\n\nThis directory contains testscripts: files ending in `.test.txt` that we\nautomatically run using the testscripts framework.\n\nFor details on how to write these types of files see:\n+ https://bitfieldconsulting.com/golang/test-scripts\n+ https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript\n\nIn addition to the standard testscript commands, we also support running devbox\ncommands. Examples include:\n+ `devbox init`\n+ `devbox add <pkg>`\n+ ...\n\nWe've also added some handy comparison functions\n+ `path.len <number>`: verifies that the PATH environment variable has the given number of entries\n+ `json.superset <superset.json> <subset.json>`: verifies that `superset.json` has all the keys and values present in `subset.json`\n"
  },
  {
    "path": "testscripts/add/add.test.txt",
    "content": "# Testscript for exercising adding packages\n\nexec devbox init\n\n# Add a package that is not part of the Devbox Search index.\n# This exercises the fallback codepath for adding packages.\nexec devbox add stdenv.cc.cc.lib\njson.superset devbox.json expected_devbox1.json\n\n# Add regular packages. Even though this is the more common scenario,\n# we test this later, because the source.path below removes \"devbox\"\n# from the PATH.\n! exec rg --version\n! exec vim --version\nexec devbox add ripgrep vim\n\nexec devbox shellenv\nsource.path\nexec rg --version\nexec vim --version\njson.superset devbox.json expected_devbox2.json\n\n-- devbox.json --\n{\n  \"packages\": [\n  ]\n}\n\n-- expected_devbox1.json --\n{\n  \"packages\": [\n    \"stdenv.cc.cc.lib\"\n  ]\n}\n\n-- expected_devbox2.json --\n{\n  \"packages\": [\n    \"ripgrep@latest\",\n    \"vim@latest\",\n    \"stdenv.cc.cc.lib\"\n  ]\n}\n"
  },
  {
    "path": "testscripts/add/add_insecure.tst.txt",
    "content": "# Tests installing an insecure package.\n# This test is pretty slow, maybe there's a different package we can\n# use for testing.\n\n# we could also isolate this test and run on its own.\n\nexec devbox init\nexec devbox add python@2.7.18.6 --allow-insecure python-2.7.18.6\nexec devbox install\n"
  },
  {
    "path": "testscripts/add/add_outputs.test.txt",
    "content": "# Testscript to add packages with non-default outputs\n\nexec devbox init\n\n# Add prometheus with default outputs. It will not have promtool.\nexec devbox add prometheus\nexec devbox run -- prometheus --version\n! exec devbox run -- promtool --version\n\n# Add prometheus with cli and out outputs. It will have promtool as well.\nexec devbox add prometheus --outputs cli,out\njson.superset devbox.json expected_devbox.json\nexec devbox run -- promtool --version\nexec devbox run -- prometheus --version\n\n\n\n-- devbox.json --\n{\n  \"packages\": [\n  ]\n}\n\n-- expected_devbox.json --\n{\n  \"packages\": {\n    \"prometheus\": {\n      \"version\": \"latest\",\n      \"outputs\": [\"cli\", \"out\"]\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/add/add_platforms.test.txt",
    "content": "# Testscript for exercising adding packages\n\n#### Part 1: Adding with a single platform or exclude-platform\n\nexec devbox install\n! exec rg --version\n! exec vim --version\n\n# First, add a --platform, and verify that the []string packages\n# becomes a map[string]any packages\nexec devbox add ripgrep --platform x86_64-darwin\njson.superset devbox.json expected_devbox1.json\n\n# Second, add another platform: verify that it adds to the platforms array\nexec devbox add ripgrep --platform x86_64-linux\n# Third, add an excluded-platform too\nexec devbox add vim --exclude-platform x86_64-linux\n\njson.superset devbox.json expected_devbox2.json\n\n#### Part 2: Adding with multiple platforms or exclude-platforms\n\nexec devbox add hello --platform x86_64-darwin,x86_64-linux --platform aarch64-darwin\njson.superset devbox.json expected_devbox3.json\n\nexec devbox add cowsay --exclude-platform x86_64-darwin,x86_64-linux --exclude-platform aarch64-darwin\njson.superset devbox.json expected_devbox4.json\n\n### Part 3: Ensure we error to prevent inconsistent state\n\n! exec devbox add cowsay --platform x86_64-darwin\nstderr 'Error: cannot add any platform for package cowsay@latest because it already has `excluded_platforms` defined'\n\n! exec devbox add hello --exclude-platform x86_64-darwin\nstderr 'Error: cannot exclude any platform for package hello@latest because it already has `platforms` defined'\n\n-- devbox.json --\n{\n  \"packages\": [\n    \"hello\",\n    \"cowsay@latest\"\n  ]\n}\n\n-- expected_devbox1.json --\n{\n  \"packages\": {\n    \"hello\": \"\",\n    \"cowsay\": \"latest\",\n    \"ripgrep\": {\n      \"version\": \"latest\",\n      \"platforms\": [\"x86_64-darwin\"]\n    }\n  }\n}\n\n-- expected_devbox2.json --\n{\n  \"packages\": {\n    \"hello\": \"\",\n    \"cowsay\": \"latest\",\n    \"ripgrep\": {\n      \"version\": \"latest\",\n      \"platforms\": [\"x86_64-darwin\", \"x86_64-linux\"]\n    },\n    \"vim\": {\n      \"version\": \"latest\",\n      \"excluded_platforms\": [\"x86_64-linux\"]\n    }\n  }\n}\n\n-- expected_devbox3.json --\n\n{\n  \"packages\": {\n    \"hello\": \"\",\n    \"cowsay\": \"latest\",\n    \"ripgrep\": {\n      \"version\": \"latest\",\n      \"platforms\": [\"x86_64-darwin\", \"x86_64-linux\"]\n    },\n    \"vim\": {\n      \"version\": \"latest\",\n      \"excluded_platforms\": [\"x86_64-linux\"]\n    },\n    \"hello\": {\n        \"version\": \"latest\",\n        \"platforms\": [\"x86_64-darwin\", \"x86_64-linux\", \"aarch64-darwin\"]\n    }\n  }\n}\n\n-- expected_devbox4.json --\n\n{\n  \"packages\": {\n    \"hello\": \"\",\n    \"cowsay\": {\n      \"version\": \"latest\",\n      \"excluded_platforms\": [\"x86_64-darwin\", \"x86_64-linux\", \"aarch64-darwin\"]\n    },\n    \"ripgrep\": {\n      \"version\": \"latest\",\n      \"platforms\": [\"x86_64-darwin\", \"x86_64-linux\"]\n    },\n    \"vim\": {\n      \"version\": \"latest\",\n      \"excluded_platforms\": [\"x86_64-linux\"]\n    },\n    \"hello\": {\n        \"version\": \"latest\",\n        \"platforms\": [\"x86_64-darwin\", \"x86_64-linux\", \"aarch64-darwin\"]\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/add/add_platforms_flakeref.test.txt",
    "content": "# Testscript for exercising adding packages using a flake ref\n\nexec devbox install\n\n# aside: choose armv7l-linux to verify that the add actually works on the\n# current host that is unlikely to be armv7l-linux\n\nexec devbox add github:F1bonacc1/process-compose/v1.87.0 --exclude-platform armv7l-linux\njson.superset devbox.json expected_devbox1.json\n\n# verify that the package is installed on this platform\nexec devbox run -- process-compose version\nstdout '1.87.0'\n\n-- devbox.json --\n{\n  \"packages\": [\n    \"hello\",\n    \"cowsay@latest\"\n  ]\n}\n\n-- expected_devbox1.json --\n{\n  \"packages\": {\n    \"hello\": \"\",\n    \"cowsay\": \"latest\",\n    \"github:F1bonacc1/process-compose/v1.87.0\": {\n      \"excluded_platforms\": [\"armv7l-linux\"]\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/add/add_replace.test.txt",
    "content": "# Testscript for exercising adding packages\n\nexec devbox init\n\nexec devbox add go@1.20\ndevboxjson.packages.contains devbox.json go@1.20\n! devboxjson.packages.contains devbox.json go@1.19\n\nexec devbox add go@1.19\n! devboxjson.packages.contains devbox.json go@1.20\ndevboxjson.packages.contains devbox.json go@1.19\n\n-- devbox.json --\n{\n  \"packages\": [\n    \"go@1.19\"\n  ]\n}\n"
  },
  {
    "path": "testscripts/add/global_add.test.txt",
    "content": "# Testscript for exercising adding packages\n\n! exec rg --version\n! exec vim --version\nexec devbox global add ripgrep vim\n\nexec devbox global shellenv --recompute\nsource.path\nexec rg --version\nexec vim --version\n\n-- devbox.json --\n{\n  \"packages\": [\n  ]\n}\n"
  },
  {
    "path": "testscripts/assert/assert.test.txt",
    "content": "# Tests assert functions (since some are relatively complex)\n\nexec print a:b:c:d:e:fg:h\npath.order 'a' 'c' 'e'\n! path.order 'a' 'b' 'a'\npath.order 'a' 'f'\n! path.order 'a' 'c' 'h' 'h'\n"
  },
  {
    "path": "testscripts/basic/default_test_env.test.txt",
    "content": "# Test that the environment is setup correctly by our testing framework.\n\n# PATH should have a single entry: the one setup by the testing framework\nenv.path.len 1\n\n# Through that path we should be able to execute devbox:\nexec devbox version\n\n# But nothing else (including common tools):\n! exec grep --version\n! exec echo \"echo should not be in path\"\n"
  },
  {
    "path": "testscripts/basic/install_hello.test.txt",
    "content": "# Devbox should be able to install a very simple package like 'hello'\n# and it should work.\n\n# Ensure hello is not found anywhere in the environment\n! exec hello\n! exec devbox run hello\n\n# Initialize devbox\nexec devbox init\n\n# Add the package and run hello with devbox\nexec devbox add hello\n! exec hello\n\n# Run hello and check it prints the right output\nexec devbox run hello\nstdout 'Hello, world!'\n\n# Once we have better progress output, we should check that stderr is empty, with:\n# ! stderr .+  # No stderr output\n# As is, we always print 'Ensuring packages are installed'."
  },
  {
    "path": "testscripts/basic/path_whitespace.test.txt",
    "content": "# Test that Devbox handles whitespace in project paths.\n\nmkdir 'my project'\ncd 'my project'\n\nexec devbox run -- hello\nstdout 'Hello, world!'\n\nexec devbox run -- touch 'file1 with spaces'\nexists 'file1 with spaces'\n\nexec devbox run test\nexists 'file2 with spaces'\n\n-- my project/devbox.json --\n{\n  \"packages\": [\"hello@latest\"],\n  \"shell\": {\n    \"scripts\": {\n      \"test\": \"touch 'file2 with spaces'\"\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/generate/devcontainer.test.txt",
    "content": "exec devbox init\nexec devbox generate devcontainer\nexists .devcontainer/Dockerfile\nexists .devcontainer/devcontainer.json\n"
  },
  {
    "path": "testscripts/generate/direnv-config-envflag.test.txt",
    "content": "# Testscript to validate generating the contents of the .envrc file.\n# Note that since --envrc-dir was NOT specified, the .envrc will be in the `dir` directory and\n# the config will be found there, which means the `--print-env` doesn't need to specify the dir.\n# This matches the mode of operation prior to the addition of the --envrc-dir flag.\n\nmkdir dir\nexec devbox init dir\nexists dir/devbox.json\n\nexec devbox generate direnv --env x=y --config dir\ngrep 'eval \"\\$\\(devbox generate direnv --print-envrc --env x=y\\)\"' dir/.envrc\n\ncd dir\nexec devbox generate direnv --print-envrc --env x=y\n\ncmp stdout ../expected-results.txt\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias --env x=y )\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv-config.test.txt",
    "content": "# Testscript to validate generating the contents of the .envrc file.\n# Note that since --envrc-dir was NOT specified, the .envrc will be in the `dir` directory and\n# the config will be found there, which means the `--print-env` doesn't need to specify the dir.\n# This matches the mode of operation prior to the addition of the --envrc-dir flag.\n\nmkdir dir\nexec devbox init dir\nexists dir/devbox.json\n\nexec devbox generate direnv --config dir\ngrep 'eval \"\\$\\(devbox generate direnv --print-envrc\\)\"' dir/.envrc\n\ncd dir\nexec devbox generate direnv --print-envrc\n\ncmp stdout ../expected-results.txt\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias)\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv-envflag.test.txt",
    "content": "# Testscript to validate generating the contents of the .envrc file.\n\nexec devbox init\nexists devbox.json\n\nexec devbox generate direnv --env x=y\ngrep 'eval \"\\$\\(devbox generate direnv --print-envrc --env x=y\\)\"' .envrc\n\nexec devbox generate direnv --print-envrc --env x=y\n\ncmp stdout expected-results.txt\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias --env x=y )\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv-envrcdir-config-parent.test.txt",
    "content": "# Testscript to validate generating a direnv .envrc in a specified location (./dir).\n# The devbox config is in the current dir (parent to ./dir). Since no --config\n# is specified, the normal config-finding will find the config.\n\nexec devbox init\nexists ./devbox.json\n\nmkdir dir\n\nexec devbox generate direnv --envrc-dir dir\ngrep 'eval \"\\$\\(devbox generate direnv --print-envrc\\)\"' dir/.envrc\n! grep '--config' dir/.envrc # redundant, but making expectations obvious\n\ncd dir\nexec devbox generate direnv --print-envrc\n\ncmp stdout ../expected-results.txt\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias)\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv-envrcdir-config-sibling.test.txt",
    "content": "# Testscript to validate generating a direnv .envrc in a specified location (./dir) that also\n# references a devbox config in another dir (./cfg) that is a sibling to the first.\n\nmkdir cfg\nexec devbox init cfg\nexists cfg/devbox.json\n\nmkdir dir\nexec devbox generate direnv --envrc-dir dir --config cfg\ngrep 'eval \"\\$\\(devbox generate direnv --print-envrc --config ../cfg\\)\"' dir/.envrc\n\ncd dir\nexec devbox generate direnv --print-envrc --config ../cfg\n\ncmp stdout ../expected-results.txt\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias --config ../cfg)\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv-envrcdir-config-subdir-envflag.test.txt",
    "content": "# Testscript to validate generating a direnv .envrc in a specified location (./dir) that also\n# references a devbox config in another dir (./dir/cfg) that is a subdir of the first.\n\nmkdir dir/cfg\nexec devbox init dir/cfg\nexists dir/cfg/devbox.json\n\nexec devbox generate direnv --envrc-dir dir --config dir/cfg --env x=y\ngrep 'eval \"\\$\\(devbox generate direnv --print-envrc --env x=y --config cfg\\)\"' dir/.envrc\n\ncd dir\nexec devbox generate direnv --print-envrc --env x=y --config cfg\n\ncmp stdout ../expected-results.txt\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias --env x=y  --config cfg)\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv-envrcdir-config-subdir.test.txt",
    "content": "# Testscript to validate generating a direnv .envrc in a specified location (./dir) that also\n# references a devbox config in another dir (./dir/cfg) that is a subdir of the first.\n\nmkdir dir/cfg\nexec devbox init dir/cfg\nexists dir/cfg/devbox.json\n\nexec devbox generate direnv --envrc-dir dir --config dir/cfg\ngrep 'eval \"\\$\\(devbox generate direnv --print-envrc --config cfg\\)\"' dir/.envrc\n\ncd dir\nexec devbox generate direnv --print-envrc --config cfg\n\ncmp stdout ../expected-results.txt\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias --config cfg)\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv-envrcdir-current-config-sub.test.txt",
    "content": "# Testscript to validate generating a direnv .envrc in the current location that\n# references a devbox config in a subdir (./cfg).\n\nmkdir cfg\nexec devbox init cfg\nexists cfg/devbox.json\n\nexec devbox generate direnv --envrc-dir . --config cfg\ngrep 'eval \"\\$\\(devbox generate direnv --print-envrc --config cfg\\)\"' ./.envrc\n\nexec devbox generate direnv --print-envrc --config cfg\n\ncmp stdout expected-results.txt\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias --config cfg)\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv-envrcdir-fail-no-config.test.txt",
    "content": "# Testscript to validate generating a direnv .envrc in a specified location (./cfg) that also\n# contains a config, but NO --config is specified so it fails to find it.\n\nmkdir cfg\nexec devbox init cfg\nexists cfg/devbox.json\n\n! exec devbox generate direnv --envrc-dir cfg\nstderr 'no devbox.json found in the current directory \\(or any parent directories\\). Did you run `devbox init` yet\\?'\n"
  },
  {
    "path": "testscripts/generate/direnv-envrcdir-parent-no-config.test.txt",
    "content": "# Testscript to validate generating a direnv .envrc in the parent of the current\n# dir. The devbox config is located in a sub dir, so from the parent dir,\n# devbox will not be able to search \"up\" to find it.\n\nmkdir cfg\nexec devbox init cfg\nexists cfg/devbox.json\n\nmkdir dir\n! exec devbox generate direnv --envrc-dir dir\nstderr 'Error: no devbox.json found in the current directory \\(or any parent directories\\). Did you run `devbox init` yet\\?'\n"
  },
  {
    "path": "testscripts/generate/direnv-envrcdir-parent.test.txt",
    "content": "# Testscript to validate generating a direnv .envrc in the parent of the current\n# dir, which is where the devbox config is located.\n\nmkdir cfg\nexec devbox init cfg\nexists cfg/devbox.json\n\ncd cfg\n\nexec devbox generate direnv --envrc-dir ..\ngrep 'eval \"\\$\\(devbox generate direnv --print-envrc --config cfg\\)\"' ../.envrc\n\ncd ..\nexec devbox generate direnv --print-envrc --config cfg\n\ncmp stdout expected-results.txt\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias --config cfg)\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv-envrcdir.test.txt",
    "content": "# Testscript to validate generating a direnv .envrc in a specified location (./cfg) that also\n# contains a devbox config in the same location. --config is required as the\n# target dir is a subdir to the current dir which means the normal config-finding\n# would not find it.\n\nmkdir cfg\nexec devbox init cfg\nexists cfg/devbox.json\n\nexec devbox generate direnv --envrc-dir cfg --config cfg\ngrep 'eval \"\\$\\(devbox generate direnv --print-envrc\\)\"' cfg/.envrc\n! grep '--config' cfg/.envrc\n\ncd cfg\nexec devbox generate direnv --print-envrc\n\ncmp stdout ../expected-results.txt\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias)\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv-printenvrc-config.test.txt",
    "content": "# Testscript to validate generating the contents of the .envrc file.\n\nmkdir config-dir\nexec devbox init config-dir\nexists config-dir/devbox.json\n\nexec devbox generate direnv --print-envrc --config config-dir\n\ncmp stdout expected-results.txt\n\n! exists .envrc\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias --config config-dir)\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv-printenvrc-envrcdir.test.txt",
    "content": "# Testscript to validate that the --print-envrc and --envrc-dir params are not allowed\n# to be used at the same time.\n\nexec devbox init\nexists ./devbox.json\n\n! exec devbox generate direnv --print-envrc --envrc-dir dir\nstderr 'Cannot use --print-envrc with --envrc-dir'"
  },
  {
    "path": "testscripts/generate/direnv-printenvrc.test.txt",
    "content": "# Testscript to validate the output of --print-envrc\n\nexec devbox init\nexists ./devbox.json\n\nexec devbox generate direnv --print-envrc\n\ncmp stdout expected-results.txt\n\n! exists .envrc\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias)\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/direnv.test.txt",
    "content": "# Testscript to validate generating the contents of the .envrc file.\n\nexec devbox init\nexists ./devbox.json\n\nexec devbox generate direnv\ngrep 'eval \"\\$\\(devbox generate direnv --print-envrc\\)\"' .envrc\n\nexec devbox generate direnv --print-envrc\n\ncmp stdout expected-results.txt\n\n# Note: The contents of the following file ends with two blank lines. This is\n# necessary to match the blank line that follows \"use devbox\" in the actual output.\n-- expected-results.txt --\nuse_devbox() {\n    eval \"$(devbox shellenv --init-hook --install --no-refresh-alias)\"\n    watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock\n}\nuse devbox\n\n"
  },
  {
    "path": "testscripts/generate/dockerfile.test.txt",
    "content": "exec devbox init\nexec devbox generate dockerfile\nexists Dockerfile\n"
  },
  {
    "path": "testscripts/info/info.test.txt",
    "content": "exec devbox init\nexec devbox info hello\nstdout 'hello '\n\nexec devbox init\nexec devbox info hello@latest\nstdout 'hello '\n\nexec devbox init\n! exec devbox info notapackage\nstderr 'Package \"notapackage\" not found'\n"
  },
  {
    "path": "testscripts/init/empty.test.txt",
    "content": "# Start on an empty directory and check that devbox init works correctly.\n\n! exists devbox.json\nexec devbox init\nexists devbox.json\n\njson.superset devbox.json expected.json\n\n# Second init should be a no-op with a warning\nexec devbox init\nstderr 'devbox.json already exists in'\n\n-- expected.json --\n{\n  \"packages\": []\n}\n"
  },
  {
    "path": "testscripts/languages/php.test.txt",
    "content": "exec devbox run php index.php\nstdout 'done\\n'\n\nexec devbox rm php83Extensions.ds\nexec devbox run php index.php\nstdout 'ds extension is not enabled'\n\nexec devbox add php83Extensions.ds\nexec devbox run php index.php\nstdout 'done\\n'\n\n-- devbox.json --\n{\n  \"packages\": [\n    \"php@latest\",\n    \"php83Extensions.ds@latest\"\n  ]\n}\n\n-- index.php --\n<?php\n\n// Check that the extension is loaded.\nif (!extension_loaded('ds')) {\n    echo(\"ds extension is not enabled\");\n    exit(0);\n}\n\n$vec = new \\Ds\\Vector([\"hello\", \"world\"]);\n\necho(\"Original vector elements\\n\");\nforeach ($vec as $idx => $elem) {\n  echo(\"idx: $idx and elem: $elem\\n\");\n}\necho(\"done\\n\");\n"
  },
  {
    "path": "testscripts/languages/python_patch_cuda.test.txt",
    "content": "# Python Patch CUDA Test\n#\n# Install TensorFlow as a binary wheel using pip and verify that it's able to\n# use CUDA.\n#\n# Nix is unable to install the CUDA driver and driver library (libcuda.so) on\n# non-NixOS distros. Even when they're installed via the system's package\n# manager, Nix-built binaries are unable to find them.\n#\n# Devbox attempts to find those system libraries, copy them to the Nix store,\n# and patch the DT_NEEDED section of the Python binary so it can find them.\n\n[!env:DEVBOX_RUN_FAILING_TESTS] skip 'this test requires a CUDA-enabled GPU'\n\nexec devbox install\n\n# pip install tensorflow\nexec devbox run venv -- pip install tf_nightly==2.18.0.dev20240910\nstdout 'Successfully installed.* tf_nightly-2.18.0.dev20240910'\n\n# run a python test script that prints the tensorflow devices and check that a\n# GPU is found.\nexec devbox run -e LD_DEBUG=files,libs,versions -e LD_DEBUG_OUTPUT=lddebug venv -- python main.py\nstdout 'TensorFlow Version: 2.18.0-dev20240910'\nstdout 'CUDA Built with: 12.3.2'\nstdout 'cuDNN Built with: 9'\nstdout 'Device: /device:GPU:\\d+'\n! stderr 'libstdc\\+\\+\\.so\\.6: cannot open shared object file: No such file or directory'\n! stderr 'Could not find cuda drivers on your machine, GPU will not be used.'\n! stderr 'unable to find libcuda.so'\n\n-- main.py --\nimport tensorflow as tf\n\nprint(\"TensorFlow Version:\", tf.__version__)\nprint(\"CUDA Built with:\", tf.sysconfig.get_build_info()[\"cuda_version\"])\nprint(\"cuDNN Built with:\", tf.sysconfig.get_build_info()[\"cudnn_version\"])\n\nfrom tensorflow.python.client import device_lib\n\nfor device in device_lib.list_local_devices():\n    if device.device_type == 'GPU':\n        print(f\"Device: {device.name}\")\n        print(f\"  Type: {device.device_type}\")\n        print(f\"  Memory Limit: {device.memory_limit / (1024**3):.2f} GB\")\n        print(f\"  Description: {device.physical_device_desc}\\n\")\n\n-- devbox.json --\n{\n  \"packages\": {\n    \"python\":                   \"latest\",\n    \"cudaPackages.cudatoolkit\": \"latest\",\n    \"cudaPackages.cuda_cudart\": {\"version\": \"latest\", \"outputs\": [\"lib\"]},\n    \"cudaPackages.cudnn\":       {\"version\": \"latest\", \"outputs\": [\"lib\"]},\n    \"cudaPackages.libcublas\":   {\"version\": \"latest\", \"outputs\": [\"lib\"]}\n  },\n  \"env\": {\n    \"PIP_DISABLE_PIP_VERSION_CHECK\": \"1\",\n    \"PIP_NO_CACHE_DIR\":              \"1\",\n    \"PIP_NO_INPUT\":                  \"1\",\n    \"PIP_NO_PYTHON_VERSION_WARNING\": \"1\",\n    \"PIP_ONLY_BINARY\":               \"tf_nightly\",\n    \"PIP_PROGRESS_BAR\":              \"off\",\n    \"PIP_REQUIRE_VIRTUALENV\":        \"1\",\n    \"PIP_ROOT_USER_ACTION\":          \"ignore\",\n    \"TF_CPP_MIN_LOG_LEVEL\":          \"0\"\n  },\n  \"shell\": {\n    \"scripts\": {\n      \"venv\": \". $VENV_DIR/bin/activate && \\\"$@\\\"\"\n    }\n  }\n}\n\n\n-- devbox.lock --\n{\n  \"lockfile_version\": \"1\",\n  \"packages\": {\n    \"cudaPackages.cuda_cudart@latest\": {\n      \"last_modified\": \"2024-09-20T05:11:28Z\",\n      \"resolved\": \"github:NixOS/nixpkgs/79454ee9aacc9714653a4e7eb2a52b717728caff#cudaPackages.cuda_cudart\",\n      \"source\": \"devbox-search\",\n      \"version\": \"12.4.99\",\n      \"systems\": {\n        \"aarch64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/iq0qdg5810zvhr490wlzpbj6nqj7v3w9-cuda_cudart-12.4.99\",\n              \"default\": true\n            },\n            {\n              \"name\": \"stubs\",\n              \"path\": \"/nix/store/p3dyhp4wbizigkhz19shf74yv1q02pnz-cuda_cudart-12.4.99-stubs\"\n            },\n            {\n              \"name\": \"dev\",\n              \"path\": \"/nix/store/fi1rpc8qym035bx0sfm1avpfmiwflvf0-cuda_cudart-12.4.99-dev\"\n            },\n            {\n              \"name\": \"lib\",\n              \"path\": \"/nix/store/m2ziw7vwj9wjw6ms0c929qqvmw2040hb-cuda_cudart-12.4.99-lib\"\n            },\n            {\n              \"name\": \"static\",\n              \"path\": \"/nix/store/8k0mfk5530xsd6ln1pvpdlskfvkbijzn-cuda_cudart-12.4.99-static\"\n            }\n          ],\n          \"store_path\": \"/nix/store/iq0qdg5810zvhr490wlzpbj6nqj7v3w9-cuda_cudart-12.4.99\"\n        },\n        \"x86_64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/lss47wk7xb7fx2p80z4hfzra1awrhizf-cuda_cudart-12.4.99\",\n              \"default\": true\n            },\n            {\n              \"name\": \"stubs\",\n              \"path\": \"/nix/store/p6ca6q92i6fnj9gl9cqj81v1v2r0svix-cuda_cudart-12.4.99-stubs\"\n            },\n            {\n              \"name\": \"dev\",\n              \"path\": \"/nix/store/38aycjz9r5y1fdn7wy89jcyinag1qb1p-cuda_cudart-12.4.99-dev\"\n            },\n            {\n              \"name\": \"lib\",\n              \"path\": \"/nix/store/jzp0hpr9avl6i7gkx19dz59xirp0q7m2-cuda_cudart-12.4.99-lib\"\n            },\n            {\n              \"name\": \"static\",\n              \"path\": \"/nix/store/w86y50a4m33l57aw2a6acprc1m2ynpm8-cuda_cudart-12.4.99-static\"\n            }\n          ],\n          \"store_path\": \"/nix/store/lss47wk7xb7fx2p80z4hfzra1awrhizf-cuda_cudart-12.4.99\"\n        }\n      }\n    },\n    \"cudaPackages.cudatoolkit@latest\": {\n      \"last_modified\": \"2024-09-10T15:01:03Z\",\n      \"resolved\": \"github:NixOS/nixpkgs/5ed627539ac84809c78b2dd6d26a5cebeb5ae269#cudaPackages.cudatoolkit\",\n      \"source\": \"devbox-search\",\n      \"version\": \"12.4\",\n      \"systems\": {\n        \"aarch64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/p2dga6q4kclqrg8fbppm8v0swi9438dd-cuda-merged-12.4\",\n              \"default\": true\n            }\n          ],\n          \"store_path\": \"/nix/store/p2dga6q4kclqrg8fbppm8v0swi9438dd-cuda-merged-12.4\"\n        },\n        \"x86_64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/6y5smq0gvqvwsarlmqnn7x6w40098yg6-cuda-merged-12.4\",\n              \"default\": true\n            }\n          ],\n          \"store_path\": \"/nix/store/6y5smq0gvqvwsarlmqnn7x6w40098yg6-cuda-merged-12.4\"\n        }\n      }\n    },\n    \"cudaPackages.cudnn@latest\": {\n      \"last_modified\": \"2024-09-19T11:39:46Z\",\n      \"resolved\": \"github:NixOS/nixpkgs/268bb5090a3c6ac5e1615b38542a868b52ef8088#cudaPackages.cudnn\",\n      \"source\": \"devbox-search\",\n      \"version\": \"9.3.0.75\",\n      \"systems\": {\n        \"aarch64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/xxissjyczq2dvb1cwars5dygny0701a1-cudnn-9.3.0.75\",\n              \"default\": true\n            },\n            {\n              \"name\": \"dev\",\n              \"path\": \"/nix/store/5fgcg8yaz5c5bdkgh52h4kqf5i76x16v-cudnn-9.3.0.75-dev\"\n            },\n            {\n              \"name\": \"lib\",\n              \"path\": \"/nix/store/x36hqzpz3q28rbmrd45l5llag9nibn25-cudnn-9.3.0.75-lib\"\n            },\n            {\n              \"name\": \"static\",\n              \"path\": \"/nix/store/f9v3gjzc861jw48lqilgysj9clmiab3b-cudnn-9.3.0.75-static\"\n            }\n          ],\n          \"store_path\": \"/nix/store/xxissjyczq2dvb1cwars5dygny0701a1-cudnn-9.3.0.75\"\n        },\n        \"x86_64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/kb14br97pimdmx43xdkaqdlxj7gih2ap-cudnn-9.3.0.75\",\n              \"default\": true\n            },\n            {\n              \"name\": \"dev\",\n              \"path\": \"/nix/store/v94m7vd9wczw72gnwvkx7iqvdqq5wmjb-cudnn-9.3.0.75-dev\"\n            },\n            {\n              \"name\": \"lib\",\n              \"path\": \"/nix/store/gzh9l8q8v35lvgp8ywmbna9njz5zw2k8-cudnn-9.3.0.75-lib\"\n            },\n            {\n              \"name\": \"static\",\n              \"path\": \"/nix/store/3c5hc4q6gaf99pgsrc6n4ylzg4k2c2nn-cudnn-9.3.0.75-static\"\n            }\n          ],\n          \"store_path\": \"/nix/store/kb14br97pimdmx43xdkaqdlxj7gih2ap-cudnn-9.3.0.75\"\n        }\n      }\n    },\n    \"cudaPackages.libcublas@latest\": {\n      \"last_modified\": \"2024-09-20T22:35:44Z\",\n      \"resolved\": \"github:NixOS/nixpkgs/a1d92660c6b3b7c26fb883500a80ea9d33321be2#cudaPackages.libcublas\",\n      \"source\": \"devbox-search\",\n      \"version\": \"12.4.2.65\",\n      \"systems\": {\n        \"aarch64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/3744yg0pfywwz0l4n5n9hhsg0h61i3j9-libcublas-12.4.2.65\",\n              \"default\": true\n            },\n            {\n              \"name\": \"dev\",\n              \"path\": \"/nix/store/ysmhl62cdag39gvm612vv6cb7aph5var-libcublas-12.4.2.65-dev\"\n            },\n            {\n              \"name\": \"lib\",\n              \"path\": \"/nix/store/g59p0ml3v53bzag7qzfndhxjvjyscvqr-libcublas-12.4.2.65-lib\"\n            },\n            {\n              \"name\": \"static\",\n              \"path\": \"/nix/store/gm716qm8m7syh2gbcrv7k3qjyjj4gyal-libcublas-12.4.2.65-static\"\n            }\n          ],\n          \"store_path\": \"/nix/store/3744yg0pfywwz0l4n5n9hhsg0h61i3j9-libcublas-12.4.2.65\"\n        },\n        \"x86_64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/zk22yb9jdhnzvfyar58390bq2kimxm41-libcublas-12.4.2.65\",\n              \"default\": true\n            },\n            {\n              \"name\": \"static\",\n              \"path\": \"/nix/store/f4jdxn797sr8bwcxb3q21rrwxb5g784c-libcublas-12.4.2.65-static\"\n            },\n            {\n              \"name\": \"dev\",\n              \"path\": \"/nix/store/8s8a7ilxi8h9yamq63ddsmjkhn5jnf5n-libcublas-12.4.2.65-dev\"\n            },\n            {\n              \"name\": \"lib\",\n              \"path\": \"/nix/store/gj56jqxsgmvzgf9kdnbhciw5p48h78lb-libcublas-12.4.2.65-lib\"\n            }\n          ],\n          \"store_path\": \"/nix/store/zk22yb9jdhnzvfyar58390bq2kimxm41-libcublas-12.4.2.65\"\n        }\n      }\n    },\n    \"python@latest\": {\n      \"last_modified\": \"2024-08-31T10:12:23Z\",\n      \"plugin_version\": \"0.0.4\",\n      \"resolved\": \"github:NixOS/nixpkgs/5629520edecb69630a3f4d17d3d33fc96c13f6fe#python3\",\n      \"source\": \"devbox-search\",\n      \"version\": \"3.12.5\",\n      \"systems\": {\n        \"aarch64-darwin\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/75j38g8ii1nqkmpf6sdlj3s5dyah3gas-python3-3.12.5\",\n              \"default\": true\n            }\n          ],\n          \"store_path\": \"/nix/store/75j38g8ii1nqkmpf6sdlj3s5dyah3gas-python3-3.12.5\"\n        },\n        \"aarch64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/ajjwc8k8sk3ksrl3dq4fsg83m1j8n8s3-python3-3.12.5\",\n              \"default\": true\n            },\n            {\n              \"name\": \"debug\",\n              \"path\": \"/nix/store/9qvs74485a1v5255w2ps0xf4rxww6w89-python3-3.12.5-debug\"\n            }\n          ],\n          \"store_path\": \"/nix/store/ajjwc8k8sk3ksrl3dq4fsg83m1j8n8s3-python3-3.12.5\"\n        },\n        \"x86_64-darwin\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/rv3rj95fxv57c7qwgl43qa7n0fabdy0a-python3-3.12.5\",\n              \"default\": true\n            }\n          ],\n          \"store_path\": \"/nix/store/rv3rj95fxv57c7qwgl43qa7n0fabdy0a-python3-3.12.5\"\n        },\n        \"x86_64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/pgb120fb7srbh418v4i2a70aq1w9dawd-python3-3.12.5\",\n              \"default\": true\n            },\n            {\n              \"name\": \"debug\",\n              \"path\": \"/nix/store/4ws5lqhgsxdpfb924n49ma6ll7i8x0hf-python3-3.12.5-debug\"\n            }\n          ],\n          \"store_path\": \"/nix/store/pgb120fb7srbh418v4i2a70aq1w9dawd-python3-3.12.5\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/languages/python_patch_missing_ref.test.txt",
    "content": "# Python Auto-Patch Handles Missing Ref\n#\n# Check that `devbox patch --restore-refs` doesn't break the flake build when a\n# a store path cannot be restored.\n#\n# The nixpkgs commit hash and version of Python chosen in this test is very\n# specific. Most versions don't encounter this error, so be careful that the\n# test still fails with Devbox v0.13.0 if changing the devbox.lock.\n#\n# https://github.com/jetify-com/devbox/issues/2289\n\nexec devbox install\n\n-- devbox.json --\n{\n  \"packages\": {\n    \"python\": \"latest\"\n  },\n  \"env\": {\n    \"PIP_DISABLE_PIP_VERSION_CHECK\": \"1\",\n    \"PIP_NO_INPUT\":                  \"1\",\n    \"PIP_NO_PYTHON_VERSION_WARNING\": \"1\",\n    \"PIP_PROGRESS_BAR\":              \"off\",\n    \"PIP_REQUIRE_VIRTUALENV\":        \"1\",\n    \"PIP_ROOT_USER_ACTION\":          \"ignore\"\n  },\n  \"shell\": {\n    \"scripts\": {\n      \"venv\": \". $VENV_DIR/bin/activate && \\\"$@\\\"\"\n    }\n  }\n}\n\n-- devbox.lock --\n{\n  \"lockfile_version\": \"1\",\n  \"packages\": {\n    \"python@latest\": {\n      \"last_modified\": \"2024-09-10T15:01:03Z\",\n      \"plugin_version\": \"0.0.4\",\n      \"resolved\": \"github:NixOS/nixpkgs/5ed627539ac84809c78b2dd6d26a5cebeb5ae269#python3\",\n      \"source\": \"devbox-search\",\n      \"version\": \"3.12.5\",\n      \"systems\": {\n        \"aarch64-darwin\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/9pj4rzx5pbynkkxq1srzwjhywmcfxws3-python3-3.12.5\",\n              \"default\": true\n            }\n          ],\n          \"store_path\": \"/nix/store/9pj4rzx5pbynkkxq1srzwjhywmcfxws3-python3-3.12.5\"\n        },\n        \"aarch64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/6iq3nhgdyp8a5wzwf097zf2mn4zyqxr6-python3-3.12.5\",\n              \"default\": true\n            },\n            {\n              \"name\": \"debug\",\n              \"path\": \"/nix/store/xc4hygp28y7g1rvjf0vi7fj0d83a75pj-python3-3.12.5-debug\"\n            }\n          ],\n          \"store_path\": \"/nix/store/6iq3nhgdyp8a5wzwf097zf2mn4zyqxr6-python3-3.12.5\"\n        },\n        \"x86_64-darwin\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/ks8acr22s4iggnmvxydm5czl30racy32-python3-3.12.5\",\n              \"default\": true\n            }\n          ],\n          \"store_path\": \"/nix/store/ks8acr22s4iggnmvxydm5czl30racy32-python3-3.12.5\"\n        },\n        \"x86_64-linux\": {\n          \"outputs\": [\n            {\n              \"name\": \"out\",\n              \"path\": \"/nix/store/h3i0acpmr8mrjx07519xxmidv8mpax4y-python3-3.12.5\",\n              \"default\": true\n            },\n            {\n              \"name\": \"debug\",\n              \"path\": \"/nix/store/0a39pi2s6kxqc3kjjz2y9yzibd62zhhb-python3-3.12.5-debug\"\n            }\n          ],\n          \"store_path\": \"/nix/store/h3i0acpmr8mrjx07519xxmidv8mpax4y-python3-3.12.5\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/languages/python_patch_missing_so.test.txt",
    "content": "# Python Missing Shared Library Test\n#\n# Install NumPy as a binary wheel using pip and verify that it loads.\n# The PIP_ONLY_BINARY environment variable forces downloading the binary wheel.\n#\n# Naively installing and loading NumPy fails because it cannot find\n# libstdc++.so. The nixpkgs Python interpreter doesn't search standard system\n# paths, so Devbox must patch it or provide the location of native dependencies.\n\nexec devbox install\n\n# pip install numpy\nexec devbox run venv -- pip install numpy==2.1.0\nstdout 'Successfully installed numpy'\n\n# run python test script that imports numpy\nexec devbox run venv -- python main.py\n! stderr 'libstdc\\+\\+\\.so\\.6: cannot open shared object file: No such file or directory'\n\n-- main.py --\nimport numpy\n\narray = numpy.array([1, 2, 3, 4, 5])\nprint(\"Array:\", array)\nprint(\"Mean:\", numpy.mean(array))\n\n-- devbox.json --\n{\n  \"packages\": {\n    \"python\": \"latest\"\n  },\n  \"env\": {\n    \"PIP_DISABLE_PIP_VERSION_CHECK\": \"1\",\n    \"PIP_NO_CACHE_DIR\":              \"1\",\n    \"PIP_NO_INPUT\":                  \"1\",\n    \"PIP_NO_PYTHON_VERSION_WARNING\": \"1\",\n    \"PIP_ONLY_BINARY\":               \"numpy\",\n    \"PIP_PROGRESS_BAR\":              \"off\",\n    \"PIP_REQUIRE_VIRTUALENV\":        \"1\",\n    \"PIP_ROOT_USER_ACTION\":          \"ignore\"\n  },\n  \"shell\": {\n    \"scripts\": {\n      \"venv\": \". $VENV_DIR/bin/activate && \\\"$@\\\"\"\n    }\n  }\n}\n\n-- devbox.lock --\n{\n  \"lockfile_version\": \"1\",\n  \"packages\": {\n    \"python@latest\": {\n      \"last_modified\":  \"2024-07-07T07:43:47Z\",\n      \"plugin_version\": \"0.0.3\",\n      \"resolved\":       \"github:NixOS/nixpkgs/b60793b86201040d9dee019a05089a9150d08b5b#python3\",\n      \"source\":         \"devbox-search\",\n      \"version\":        \"3.12.4\",\n      \"systems\": {\n        \"aarch64-darwin\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/3swy1vadi125g0c1vxqp8ykdr749803j-python3-3.12.4\",\n              \"default\": true\n            }\n          ],\n          \"store_path\": \"/nix/store/3swy1vadi125g0c1vxqp8ykdr749803j-python3-3.12.4\"\n        },\n        \"aarch64-linux\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/sz2facg15yq3ziqkidb1dkkglwzkkg8a-python3-3.12.4\",\n              \"default\": true\n            },\n            {\n              \"name\": \"debug\",\n              \"path\": \"/nix/store/19vjjqg7jbfblqapf63nm9ich1xdq9dx-python3-3.12.4-debug\"\n            }\n          ],\n          \"store_path\": \"/nix/store/sz2facg15yq3ziqkidb1dkkglwzkkg8a-python3-3.12.4\"\n        },\n        \"x86_64-darwin\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/3y5wy1i9nq5293knm23mxsj5l6w41h2l-python3-3.12.4\",\n              \"default\": true\n            }\n          ],\n          \"store_path\": \"/nix/store/3y5wy1i9nq5293knm23mxsj5l6w41h2l-python3-3.12.4\"\n        },\n        \"x86_64-linux\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/z7xxy35k7620hs6fn6la5fg2lgklv72l-python3-3.12.4\",\n              \"default\": true\n            },\n            {\n              \"name\": \"debug\",\n              \"path\": \"/nix/store/3x6jqv5yw212v8rlwql88cn94dginq32-python3-3.12.4-debug\"\n            }\n          ],\n          \"store_path\": \"/nix/store/z7xxy35k7620hs6fn6la5fg2lgklv72l-python3-3.12.4\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/languages/python_patch_old_glibc.test.txt",
    "content": "# Python Old glibc Test\n#\n# Check that an older version of the Python interpreter (3.7) can import and run\n# pip packages that are built from source.\n\nexec devbox install\n\n# pip install psycopg2\nexec devbox run venv -- pip install psycopg2==2.9.5\nstdout 'Successfully installed psycopg2'\n\n# run python test script that imports psycopg2\nexec devbox run venv -- python main.py\n! stderr '.*glibc-2.35-224/lib/libc\\.so\\.6: version `GLIBC_2.38'' not found \\(required by .*/site-packages/psycopg2/_psycopg\\.cpython-37m-x86_64-linux-gnu\\.so\\)'\n\n-- main.py --\nimport psycopg2\n\ntry:\n    conn = psycopg2.connect(dbname=\"test\", user=\"postgres\")\nexcept psycopg2.OperationalError:\n    pass\n\n-- devbox.json --\n{\n  \"packages\": {\n    \"python\":     \"3.7\",\n    \"postgresql\": \"15.5\"\n  },\n  \"env\": {\n    \"PIP_DISABLE_PIP_VERSION_CHECK\": \"1\",\n    \"PIP_NO_INPUT\":                  \"1\",\n    \"PIP_NO_PYTHON_VERSION_WARNING\": \"1\",\n    \"PIP_PROGRESS_BAR\":              \"off\",\n    \"PIP_REQUIRE_VIRTUALENV\":        \"1\",\n    \"PIP_ROOT_USER_ACTION\":          \"ignore\"\n  },\n  \"shell\": {\n    \"scripts\": {\n      \"venv\": \". $VENV_DIR/bin/activate && \\\"$@\\\"\"\n    }\n  }\n}\n\n-- devbox.lock --\n{\n  \"lockfile_version\": \"1\",\n  \"packages\": {\n    \"postgresql@latest\": {\n      \"last_modified\":  \"2024-02-22T01:07:56Z\",\n      \"plugin_version\": \"0.0.2\",\n      \"resolved\":       \"github:NixOS/nixpkgs/98b00b6947a9214381112bdb6f89c25498db4959#postgresql\",\n      \"source\":         \"devbox-search\",\n      \"version\":        \"15.5\",\n      \"systems\": {\n        \"aarch64-darwin\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/6cn0kmav77wba54xibfg9clqzbpan74b-postgresql-15.5\",\n              \"default\": true\n            },\n            {\n              \"name\":    \"man\",\n              \"path\":    \"/nix/store/588y60371pqh3vc9rasjawfwmchpac9d-postgresql-15.5-man\",\n              \"default\": true\n            },\n            {\n              \"name\": \"doc\",\n              \"path\": \"/nix/store/dxivb9x0iwssqzz8wsswis9q9r1sjm18-postgresql-15.5-doc\"\n            },\n            {\n              \"name\": \"lib\",\n              \"path\": \"/nix/store/dbc9hjh5ll5pjgxwl3r9nymdxw7sw8cl-postgresql-15.5-lib\"\n            }\n          ]\n        },\n        \"aarch64-linux\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/kvpjir3cjbijs2w8b20yzqjq0nsd63mp-postgresql-15.5\",\n              \"default\": true\n            },\n            {\n              \"name\":    \"man\",\n              \"path\":    \"/nix/store/4kcdjf0gg9jl4n9kxvj5iq92byry6b7l-postgresql-15.5-man\",\n              \"default\": true\n            },\n            {\n              \"name\": \"debug\",\n              \"path\": \"/nix/store/srqwd7alwglrsjclsfnrlx01n69iyy9s-postgresql-15.5-debug\"\n            },\n            {\n              \"name\": \"doc\",\n              \"path\": \"/nix/store/5fn32sdar6nk5ha9d5zb6rfpndgdbg68-postgresql-15.5-doc\"\n            },\n            {\n              \"name\": \"lib\",\n              \"path\": \"/nix/store/addi70hgggl75jm74p0s435bfaay6m1w-postgresql-15.5-lib\"\n            }\n          ]\n        },\n        \"x86_64-darwin\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/v5ym92k3kss1af7n1788653vis1d6qsc-postgresql-15.5\",\n              \"default\": true\n            },\n            {\n              \"name\":    \"man\",\n              \"path\":    \"/nix/store/x9hm4ip61cichmhzhzpykzypn3pqkh01-postgresql-15.5-man\",\n              \"default\": true\n            },\n            {\n              \"name\": \"doc\",\n              \"path\": \"/nix/store/nd1mhmgpm9w5rfpiibg6m7g4difpl5af-postgresql-15.5-doc\"\n            },\n            {\n              \"name\": \"lib\",\n              \"path\": \"/nix/store/q8lijs7rmlkx4qssmh0sjyy77f41y2jh-postgresql-15.5-lib\"\n            }\n          ]\n        },\n        \"x86_64-linux\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/vvd65gjggb2n8wxbsk1cyxx0wpfidagf-postgresql-15.5\",\n              \"default\": true\n            },\n            {\n              \"name\":    \"man\",\n              \"path\":    \"/nix/store/88jhk99imah1v19xqkldi1lfyaayni71-postgresql-15.5-man\",\n              \"default\": true\n            },\n            {\n              \"name\": \"lib\",\n              \"path\": \"/nix/store/w109qgbl14afcg5akhnahf8r0hkdqqb6-postgresql-15.5-lib\"\n            },\n            {\n              \"name\": \"debug\",\n              \"path\": \"/nix/store/ia44jr4m4jyf3a48qwpf6vgrr95jig46-postgresql-15.5-debug\"\n            },\n            {\n              \"name\": \"doc\",\n              \"path\": \"/nix/store/7vfnvfb6scmf23y6yj5zx8p5r3wsgnq5-postgresql-15.5-doc\"\n            }\n          ]\n        }\n      }\n    },\n    \"python@3.7\": {\n      \"last_modified\":  \"2022-12-17T09:19:40Z\",\n      \"plugin_version\": \"0.0.3\",\n      \"resolved\":       \"github:NixOS/nixpkgs/80c24eeb9ff46aa99617844d0c4168659e35175f#python37\",\n      \"source\":         \"devbox-search\",\n      \"version\":        \"3.7.16\",\n      \"systems\": {\n        \"aarch64-darwin\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/a89sd5jwn01cdg97lkspl8cpf75y5142-python3-3.7.16\",\n              \"default\": true\n            }\n          ],\n          \"store_path\": \"/nix/store/a89sd5jwn01cdg97lkspl8cpf75y5142-python3-3.7.16\"\n        },\n        \"aarch64-linux\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/ymrbxfmljyl73rmh5cfk0bzk3ydcbqg8-python3-3.7.16\",\n              \"default\": true\n            },\n            {\n              \"name\": \"debug\",\n              \"path\": \"/nix/store/3x7736j3fyw6j9fzn1y9fc0iqyf1rncc-python3-3.7.16-debug\"\n            }\n          ],\n          \"store_path\": \"/nix/store/ymrbxfmljyl73rmh5cfk0bzk3ydcbqg8-python3-3.7.16\"\n        },\n        \"x86_64-darwin\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/i028a4nf177g23ksa7kc63ld9nys17nb-python3-3.7.16\",\n              \"default\": true\n            }\n          ],\n          \"store_path\": \"/nix/store/i028a4nf177g23ksa7kc63ld9nys17nb-python3-3.7.16\"\n        },\n        \"x86_64-linux\": {\n          \"outputs\": [\n            {\n              \"name\":    \"out\",\n              \"path\":    \"/nix/store/ik7s754pwxhiky396mjagzmjs1kp0wzq-python3-3.7.16\",\n              \"default\": true\n            },\n            {\n              \"name\": \"debug\",\n              \"path\": \"/nix/store/l0xi13a88d4vjn8ada3a58zkwm88hq7h-python3-3.7.16-debug\"\n            }\n          ],\n          \"store_path\": \"/nix/store/ik7s754pwxhiky396mjagzmjs1kp0wzq-python3-3.7.16\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/lockfile/lockfile_tidy.test.txt",
    "content": "# Testscript to ensure lockfile is updated to remove the older version of a package\n\n# start with a devbox.json having go@1.19\ncp devbox_original.json devbox.json\nexec devbox install\ndevboxlock.packages.contains devbox.lock go@1.19\n\n# change devbox.json to instead have go@1.20\ncp devbox_changed.json devbox.json\nexec devbox install\ndevboxlock.packages.contains devbox.lock go@1.20\n! devboxlock.packages.contains devbox.lock go@1.19\n\n\n-- devbox_original.json --\n {\n   \"packages\": [\n     \"go@1.19\"\n   ]\n }\n\n-- devbox_changed.json --\n{\n  \"packages\": [\n    \"go@1.20\"\n  ]\n}\n\n"
  },
  {
    "path": "testscripts/lockfile/nopaths.txt",
    "content": "# Test installing a package without outputs in the store path. \n# NOTE: Purposefully using a weird version to ensure it is not already in store.\n\nexec devbox run curl --version | grep -o 'curl\\s7\\.87\\.0'\nstdout 'curl 7.87.0'\n\n-- devbox.json --\n{\n  \"packages\": [\"curl@7.87.0\"],\n}\n\n-- devbox.lock --\n{\n  \"lockfile_version\": \"1\",\n  \"packages\": {\n    \"curl@7.87.0\": {\n      \"last_modified\": \"2023-02-26T03:47:33Z\",\n      \"resolved\": \"github:NixOS/nixpkgs/9952d6bc395f5841262b006fbace8dd7e143b634#curl\",\n      \"source\": \"devbox-search\",\n      \"version\": \"7.87.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/packages/flakes.test.txt",
    "content": "! exec devbox run hello\nexec devbox add path:my-flake\n\nexec devbox run hello\n\n-- devbox.json --\n{\n  \"packages\": [\n  ]\n}\n\n-- my-flake/flake.nix --\n{\n  description = \"Test\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = { self, nixpkgs, flake-utils }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let\n        pkgs = nixpkgs.legacyPackages.${system};\n      in\n      {\n        packages = {\n          default = pkgs.hello;\n        };\n      });\n}\n"
  },
  {
    "path": "testscripts/packages/unfree.test.txt",
    "content": "# Test ensures that we can add and remove \"unfree\" nix packages\n\nexec devbox init\n\n# we could test with slack and/or vscode. Using slack since it is lighter.\nexec devbox add slack\nstderr 'Adding package \"slack@latest\" to devbox.json'\n\nexec devbox rm slack\n"
  },
  {
    "path": "testscripts/plugin/disable-plugin.test.txt",
    "content": "# Testscript for testing disable plugin\n\nexec devbox init\nexec devbox add python --disable-plugin\n! stderr 'This plugin' \n\njson.superset devbox.json expected_devbox.json\n\n! exec devbox run ls .devbox/virtenv/python\n\n# remove disable plugin option\nexec devbox add python\n\njson.superset devbox.json expected_devbox2.json\n\n-- expected_devbox.json --\n{\n  \"packages\": {\n    \"python\": {\n      \"version\": \"latest\",\n      \"disable_plugin\": true\n    }\n  }\n}\n\n-- expected_devbox2.json --\n{\n  \"packages\": {\n    \"python\": {\n      \"version\": \"latest\",\n      \"disable_plugin\": false\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/plugin/plugin.cycle.test.txt",
    "content": "! exec devbox install\nstderr 'circular or duplicate include detected:'\n\n! exec devbox install -c ./duplicate\nstderr 'circular or duplicate include detected:'\n\nexec devbox install -c ./no-cycle\nstderr 'Finished installing packages.'\n\n-- devbox.json --\n{\n  \"name\": \"test-with-cycle\",\n  \"include\": [\"./plugin1\"]\n}\n\n-- plugin1/plugin.json --\n{\n  \"name\": \"plugin1\",\n  \"include\": [\"../plugin2\"]\n}\n\n-- plugin2/plugin.json --\n{\n  \"name\": \"plugin2\",\n  \"include\": [\"../plugin1\"]\n}\n\n-- no-cycle/devbox.json --\n{\n  \"name\": \"test-without-cycle\",\n  \"include\": [\"./plugin3\"]\n}\n\n-- no-cycle/plugin3/plugin.json --\n{\n  \"name\": \"plugin3\"\n}\n\n-- duplicate/devbox.json --\n{\n  \"name\": \"test-with-duplicate\",\n  \"include\": [\n    \"./plugin4\",\n    \"./plugin4\"\n  ]\n}\n\n-- duplicate/plugin4/plugin.json --\n{\n  \"name\": \"plugin4\"\n}\n"
  },
  {
    "path": "testscripts/plugin/plugin.test.txt",
    "content": "# Testscript for testing plugin\n\nexec devbox init\nexec devbox add python\nstderr 'This plugin'\n\nexec devbox run ls .devbox/virtenv/python\n\njson.superset devbox.json expected_devbox.json\n\nexec devbox add python --disable-plugin\nexec devbox add hello\n\njson.superset devbox.json expected_devbox2.json\n\n-- expected_devbox.json --\n{\n  \"packages\": [\n    \"python@latest\"\n  ]\n}\n\n-- expected_devbox2.json --\n{\n  \"packages\": {\n    \"hello\": \"latest\",\n    \"python\": {\n      \"version\": \"latest\",\n      \"disable_plugin\": true\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/rm/add-rm.test.txt",
    "content": "exec devbox init\n\nexec devbox add hello vim cowsay php\njson.superset devbox.json all.json\n\nexec devbox rm vim hello php\njson.superset devbox.json cowsay.json\n\nexec devbox add vim hello vim hello vim hello vim hello cowsay php php\njson.superset devbox.json all.json\n\nexec devbox rm vim hello cowsay cowsay php\njson.superset devbox.json empty.json\n\n-- all.json --\n{\n  \"packages\": [\"hello@latest\", \"vim@latest\", \"cowsay@latest\", \"php@latest\"]\n}\n\n-- cowsay.json --\n{\n  \"packages\": [\"cowsay@latest\"]\n}\n\n\n-- empty.json --\n{\n  \"packages\": []\n}\n"
  },
  {
    "path": "testscripts/rm/manual.test.txt",
    "content": "exec devbox run hello\nstdout 'Hello, world!'\n\n# Simulate deleting the packages manually.\ncp empty.json devbox.json\n\n! exec devbox run hello\n! stdout 'Hello, world!'\n\n-- devbox.json --\n{\n  \"packages\": [\"hello\"]\n}\n\n-- empty.json --\n{\n  \"packages\": []\n}\n"
  },
  {
    "path": "testscripts/rm/multi.test.txt",
    "content": "exec devbox init\n\nexec devbox add hello vim\nexec devbox run hello\nstdout 'Hello, world!'\n\nexec devbox rm vim hello\n! exec devbox run hello\n! exec devbox run vim\n\njson.superset devbox.json expected.json\n\n# Check that profile history was cleaned up. There should only be\n# default and default-N-link.\nglob -count=2 .devbox/nix/profile/*\n\n-- expected.json --\n{\n  \"packages\": []\n}\n"
  },
  {
    "path": "testscripts/rm/rm.test.txt",
    "content": "exec devbox init\n\nexec devbox add hello vim\nexec devbox run hello\nstdout 'Hello, world!'\n\nexec devbox rm hello\n! exec devbox run hello\n! stdout 'Hello, world!'\n\njson.superset devbox.json expected.json\n\n-- expected.json --\n{\n  \"packages\": [\"vim@latest\"]\n}\n"
  },
  {
    "path": "testscripts/run/args.test.txt",
    "content": "# Test passing arguments to a script\nexec devbox run ekko hello there\nstdout 'hello there'\n\n-- devbox.json --\n{\n  \"packages\": [],\n  \"shell\": {\n    \"scripts\": {\n      \"ekko\": \"echo $@\"\n    }\n  }\n}"
  },
  {
    "path": "testscripts/run/env.test.txt",
    "content": "# Tests related to setting the environment for devbox run.\n\n# Parent shell vars should leak into the run environment\nenv HOMETEST=/home/test\nenv USER=test-user\nenv FOO=bar\nexec devbox run echo '$HOMETEST'\nstdout '/home/test'\nexec devbox run echo '$USER'\nstdout 'test-user'\nexec devbox run echo '$FOO'\nstdout 'bar'\n\n# DEVBOX_* vars are passed through\nenv DEVBOX_FOO=baz\nexec devbox run echo '$DEVBOX_FOO'\nstdout 'baz'\n\n# Vars defined in devbox.json are passed through\nenv DEVBOX_FEATURE_ENV_CONFIG=1\nexec devbox run echo '$CONFIG_VAR1'\nstdout 'abc'\n\n# Vars defined in devbox.json that reference another variable are set\nenv DEVBOX_FEATURE_ENV_CONFIG=1\nenv DEVBOX_FOO=baz\nexec devbox run echo '$CONFIG_VAR2'\nstdout 'baz'\n\n# Vars in devbox that refer to $PWD should get the project dir\nenv PWD=/test-pwd\nexec devbox run echo '$CONFIG_VAR3'\n! stdout '/test-pwd'\n\n# Variables are applied in order: nix vars, DEVBOX_*, leaked, leakedForShell, fixed/hard-coded vars,\n# plugin vars, and config vars. It really only makes sense to test for plugin and config vars order.\n# Note that the nginx plugin defines NGINX_CONFDIR, NGINX_PATH_PREFIX, and NGINX_TMPDIR.\nenv NGINX_TMPDIR=\"to-be-overwritten-by-plugin\"\nexec devbox run echo '$NGINX_TMPDIR'\n! stdout 'to-be-overwritten-by-plugin'\nstdout '/nginx/temp'\n\nexec devbox run echo '$NGINX_CONFDIR'\nstdout 'devbox-json-override'\n\n-- devbox.json --\n{\n  \"packages\": [\"nginx@latest\"],\n  \"env\": {\n    \"CONFIG_VAR1\": \"abc\",\n    \"CONFIG_VAR2\": \"$DEVBOX_FOO\",\n    \"CONFIG_VAR3\": \"${PWD}\",\n    \"NGINX_CONFDIR\": \"devbox-json-override\"\n  }\n}\n"
  },
  {
    "path": "testscripts/run/envfrom.test.txt",
    "content": "# Tests related to setting the env_from for devbox run.\n\nexec devbox run test\nstdout 'BAR'\n\nexec devbox run test2\nstdout 'BAZ'\n\nexec devbox run test3\nstdout 'BAS'\n\nexec devbox run test4\nstdout ''\n\n-- test.env --\nFOO=BAR\nFOO2 = BAZ\nFOO3=ToBeOverwrittenByDevboxJSON\n# FOO4=comment shouldn't be processed\n\n-- devbox.json --\n{\n  \"packages\": [],\n  \"env\": {\n    \"FOO3\": \"BAS\"\n  },\n  \"shell\": {\n    \"scripts\": {\n      \"test\": \"echo $FOO\",\n      \"test2\": \"echo $FOO2\",\n      \"test3\": \"echo $FOO3\",\n      \"test4\": \"echo $FOO4\"\n    }\n  },\n  \"env_from\": \"test.env\"\n}\n"
  },
  {
    "path": "testscripts/run/path.test.txt",
    "content": "exec devbox init\nexec devbox add which\n\n# Ensure nix is accessible from the default profile\nexec devbox run which nix\nstdout '/nix/var/nix/profiles/default/bin/nix'\n\n# Relative paths in PATH are removed, others are cleaned\nenv PATH=./relative/path:/some//dirty/../clean/path:$PATH\nexec devbox run echo '$PATH'\n! stdout 'relative/path'\n! stdout '/some//dirty/../clean/path'\nstdout '/some/clean/path'\n\n# Path contains path to installed nix packages in nix profile\nstdout '.devbox/nix/profile/default/bin'\n\n# Verify PATH is set in correct order: virtual env path nix packages, host path.\npath.order '.devbox/nix/profile/default/bin' '/some/clean/path'\n\n# TODO: verify that bashrc file prepends do not prepend before nix paths.\n"
  },
  {
    "path": "testscripts/run/pure.test.txt",
    "content": "# Tests related to having devbox run in pure mode.\n\nenv FOO=bar\nenv FOO2=bar2\n\nexec devbox run --pure echo '$FOO'\nstdout 'baz'\n\nexec devbox run --pure echo '$FOO2'\nstdout ''\n\nexec devbox run --pure hello\nstdout 'Hello, world!'\n\n-- devbox.json --\n{\n  \"packages\": [\"hello@latest\"],\n  \"env\": {\n    \"FOO\": \"baz\"\n  }\n}\n"
  },
  {
    "path": "testscripts/run/quote_escaping.test.txt",
    "content": "# ensure that we escape the arguments to `devbox run`\n\nexec devbox init\nexec devbox run -- echo 'this is a \"hello world\"'\nstdout 'this is a \"hello world\"'\n\nenv FOO=bar\nexec devbox run echo '$FOO'\nstdout 'bar'\n\nexec devbox run echo \"$FOO\"\nstdout 'bar'\n"
  },
  {
    "path": "testscripts/run/script.test.txt",
    "content": "# A single-line script should execute\nexec devbox run single_line\nstdout 'single line'\n\n# A multi-line script should execute\nexec devbox run multi_line\nstdout 'second line'\nstdout 'first line'\n\n# Ensure init hook is being run\nexec devbox run hook_runs\nstdout 'hook'\n\n# Use a package installed by devbox through a script\nexec devbox run hello_with_script\nstdout 'with script'\n\n# Use a package installed by devbox directly\nexec devbox run -- hello -g directly\nstdout 'directly'\n\n# TBD: Bad init hook should result in non-zero exit code\n#exec devbox --config bad_init run test\n#! stdout 'test'\n\n# NOTE: make sure each script prints out something unique. Otherwise,\n# we might get false positives when checking stdout.\n-- devbox.json --\n{\n  \"packages\": [\n    \"hello@latest\"\n  ],\n  \"shell\": {\n    \"init_hook\": \"export HOOK=hook\",\n    \"scripts\": {\n      \"single_line\": \"echo \\\"single line\\\"\",\n      \"multi_line\": [\n        \"echo \\\"first line\\\"\",\n        \"echo \\\"second line\\\"\"\n      ],\n      \"hook_runs\": \"echo $HOOK\",\n      \"hello_with_script\": \"hello -g \\\"with script\\\"\"\n    }\n  }\n}\n\n-- bad_init/devbox.json --\n{\n  \"packages\": [],\n  \"shell\": {\n    \"init_hook\": \"hello\",\n    \"scripts\": {\n      \"test\": \"echo \\\"test\\\"\"\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/run/script_exit_on_error.test.txt",
    "content": "# Testscript to ensure that the script exits on error.\n\n! exec devbox run multi_line\nstdout 'first line'\n! stdout 'second line'\n\n-- devbox.json --\n{\n  \"packages\": [\n  ],\n  \"shell\": {\n    \"scripts\": {\n      \"multi_line\": [\n        \"echo \\\"first line\\\"\",\n        \"exit 1\",\n        \"echo \\\"second line\\\"\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "testscripts/run/shellception.test.txt",
    "content": "# Do not support shell inception\nexec devbox init\nenv DEVBOX_SHELL_ENABLED=1\n! exec devbox shell\nstderr 'Error: You are already in an active devbox shell.'\n"
  },
  {
    "path": "testscripts/shell/shellenv.test.txt",
    "content": "exec devbox init\n\n# test adding and running hello\nexec devbox add hello\n! exec hello\n! stdout .\n\n# source shellenv and test again\nexec devbox shellenv\nsource.path\nexec hello\nstdout 'Hello, world!'\n"
  },
  {
    "path": "testscripts/shellenv/node/README.md",
    "content": "Inspired by https://github.com/amithgeorge/devbox-nodejs-repro-20230406\n\nThis example shows a wrapped binary calling setting an env variable (PATH) and\ncalling another wrapped binary without the PATH getting overwritten\n\n## Steps\n\n- devbox run run_test\n- exit code should be 0\n"
  },
  {
    "path": "testscripts/shellenv/node/devbox.json",
    "content": "{\n  \"packages\": [\n    \"nodejs@18\"\n  ],\n  \"shell\": {\n    \"init_hook\": [\n      \"npm install\"\n    ],\n    \"scripts\": {\n      \"run_test\": \"npm run run_test\"\n    }\n  },\n  \"nixpkgs\": {\n    \"commit\": \"4a65e9f64e53fdca6eed31adba836717a11247d2\"\n  }\n}\n"
  },
  {
    "path": "testscripts/shellenv/node/less-out/style.css",
    "content": "#header {\n  width: 10px;\n  height: 20px;\n}\n"
  },
  {
    "path": "testscripts/shellenv/node/less-src/style.less",
    "content": "@width: 10px;\n@height: @width + 10px;\n\n#header {\n  width: @width;\n  height: @height;\n}\n"
  },
  {
    "path": "testscripts/shellenv/node/package.json",
    "content": "{\n  \"name\": \"devbox-nodejs-repro\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"run_test\": \"less-watch-compiler less-src less-out --run-once\"\n  },\n  \"devDependencies\": {\n    \"less\": \"^4.1.3\",\n    \"less-watch-compiler\": \"^1.16.3\"\n  }\n}\n"
  },
  {
    "path": "testscripts/testrunner/assert.go",
    "content": "package testrunner\n\nimport (\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/rogpeppe/go-internal/testscript\"\n\n\t\"go.jetify.com/devbox/internal/devconfig\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/lock\"\n)\n\n// Usage: env.path.len <number>\n// Checks that the PATH environment variable has the expected number of entries.\nfunc assertPathLength(script *testscript.TestScript, neg bool, args []string) {\n\tif len(args) != 1 {\n\t\tscript.Fatalf(\"usage: env.path.len N\")\n\t}\n\texpectedN, err := strconv.Atoi(args[0])\n\tscript.Check(err)\n\n\tpath := script.Getenv(envir.Path)\n\tactualN := len(strings.Split(path, \":\"))\n\tif neg {\n\t\tif actualN == expectedN {\n\t\t\tscript.Fatalf(\"path length is %d, expected != %d\", actualN, expectedN)\n\t\t}\n\t} else {\n\t\tif actualN != expectedN {\n\t\t\tscript.Fatalf(\"path length is %d, expected %d\", actualN, expectedN)\n\t\t}\n\t}\n}\n\nfunc assertDevboxJSONPackagesContains(script *testscript.TestScript, neg bool, args []string) {\n\tif len(args) != 2 {\n\t\tscript.Fatalf(\"usage: devboxjson.packages.contains devbox.json value\")\n\t}\n\n\tdata := script.ReadFile(args[0])\n\tlist := devconfig.Config{}\n\terr := json.Unmarshal([]byte(data), &list.Root)\n\tscript.Check(err)\n\n\texpected := args[1]\n\tfor _, actual := range packagesVersionedNames(list) {\n\t\tif actual == expected {\n\t\t\tif neg {\n\t\t\t\tscript.Fatalf(\"value '%s' found in '%s'\", expected, packagesVersionedNames(list))\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\n\tif !neg {\n\t\tscript.Fatalf(\"value '%s' not found in '%s'\", expected, packagesVersionedNames(list))\n\t}\n}\n\nfunc assertDevboxLockPackagesContains(script *testscript.TestScript, neg bool, args []string) {\n\tif len(args) != 2 {\n\t\tscript.Fatalf(\"usage: devboxlock.packages.contains devbox.lock value\")\n\t}\n\n\tdata := script.ReadFile(args[0])\n\tlockfile := lock.File{}\n\terr := json.Unmarshal([]byte(data), &lockfile)\n\tscript.Check(err)\n\n\texpected := args[1]\n\tif _, ok := lockfile.Packages[expected]; ok {\n\t\tif neg {\n\t\t\tscript.Fatalf(\"value '%s' found in %s\", expected, args[0])\n\t\t}\n\t} else {\n\t\tif !neg {\n\t\t\tscript.Fatalf(\"value '%s' not found in '%s'\", expected, args[0])\n\t\t}\n\t}\n}\n\n// Usage: json.superset superset.json subset.json\n// Checks that the JSON in superset.json contains all the keys and values\n// present in subset.json.\nfunc assertJSONSuperset(script *testscript.TestScript, neg bool, args []string) {\n\tif len(args) != 2 {\n\t\tscript.Fatalf(\"usage: json.superset superset.json subset.json\")\n\t}\n\n\tif neg {\n\t\tscript.Fatalf(\"json.superset does not support negation\")\n\t}\n\n\tdata1 := script.ReadFile(args[0])\n\ttree1 := map[string]interface{}{}\n\terr := json.Unmarshal([]byte(data1), &tree1)\n\tscript.Check(err)\n\n\tdata2 := script.ReadFile(args[1])\n\ttree2 := map[string]interface{}{}\n\terr = json.Unmarshal([]byte(data2), &tree2)\n\tscript.Check(err)\n\n\tfor expectedKey, expectedValue := range tree2 {\n\t\tif actualValue, ok := tree1[expectedKey]; ok {\n\t\t\tsortIfPossible(actualValue)\n\t\t\tsortIfPossible(expectedValue)\n\n\t\t\tif !reflect.DeepEqual(actualValue, expectedValue) {\n\t\t\t\tscript.Fatalf(\"key '%s': expected '%v', got '%v'\", expectedKey, expectedValue, actualValue)\n\t\t\t}\n\t\t} else {\n\t\t\tscript.Fatalf(\"key '%s' not found, expected value '%v'\", expectedKey, expectedValue)\n\t\t}\n\t}\n}\n\n// Usage: path.order 'a' 'b' 'c'\n// Checks that whatever is in stdout, P, is a string in PATH format (i.e. colon-separated strings), and that\n// every one of the arguments ('a', 'b', and 'c') are contained in separate subpaths of P, exactly once, and\n// in order.\nfunc assertPathOrder(script *testscript.TestScript, neg bool, args []string) {\n\tpath := script.ReadFile(\"stdout\")\n\tsubpaths := strings.Split(strings.Replace(path, \"\\n\", \"\", -1), \":\")\n\n\tallInOrder := containsInOrder(subpaths, args)\n\tif !neg && !allInOrder {\n\t\tscript.Fatalf(\"Did not find all expected in order in subpaths.\\n\\nSubpaths: %v\\nExpected: %v\", subpaths, args)\n\t}\n\tif neg && allInOrder {\n\t\tscript.Fatalf(\"Found all expected in subpaths.\\n\\nSubpaths: %v\\nExpected: %v\", subpaths, args)\n\t}\n}\n\nfunc containsInOrder(subpaths, expected []string) bool {\n\tif len(expected) == 0 {\n\t\treturn true // no parts passed in, assertion trivially holds.\n\t}\n\n\tif len(subpaths) < len(expected) {\n\t\treturn false\n\t}\n\n\ti := 0\n\tj := 0\nouter:\n\tfor j < len(expected) {\n\t\tcurrentExpected := expected[j]\n\t\tfor i < len(subpaths) {\n\t\t\tif strings.Contains(subpaths[i], currentExpected) {\n\t\t\t\tj++\n\t\t\t\ti++\n\t\t\t\tcontinue outer // found expected, move on to the next expected\n\t\t\t} else {\n\t\t\t\ti++ // didn't find it, try the next subpath\n\t\t\t}\n\t\t}\n\t\treturn false // ran out of subpaths, but not out of expected, so we fail.\n\t}\n\n\treturn true // if we're here, we found everything\n}\n\nfunc sortIfPossible(v any) {\n\tif slice, ok := v.([]any); ok {\n\t\tfor i := 0; i < len(slice); i++ {\n\t\t\tfor j := i + 1; j < len(slice); j++ {\n\t\t\t\tif compare(slice[i], slice[j]) > 0 {\n\t\t\t\t\tslice[i], slice[j] = slice[j], slice[i]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc compare(one, two any) int {\n\taType, bType := reflect.TypeOf(one), reflect.TypeOf(two)\n\n\tif aType.Kind() == bType.Kind() {\n\t\tswitch aType.Kind() {\n\t\tcase reflect.Int:\n\t\t\taInt := one.(int)\n\t\t\tbInt := two.(int)\n\t\t\treturn aInt - bInt\n\t\tcase reflect.String:\n\t\t\taStr := one.(string)\n\t\t\tbStr := two.(string)\n\t\t\treturn strings.Compare(aStr, bStr)\n\t\t}\n\t}\n\n\treturn 0\n}\n\nfunc packagesVersionedNames(c devconfig.Config) []string {\n\tresult := make([]string, 0, len(c.Root.TopLevelPackages()))\n\tfor _, p := range c.Root.TopLevelPackages() {\n\t\tresult = append(result, p.VersionedName())\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "testscripts/testrunner/examplesrunner.go",
    "content": "package testrunner\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/rogpeppe/go-internal/testscript\"\n\n\t\"go.jetify.com/devbox/internal/devconfig\"\n\t\"go.jetify.com/devbox/internal/envir\"\n)\n\n// xdgStateHomeDir is the home directory for devbox state. We store symlinks to\n// virtenvs of devbox plugins in this directory. We need to use a custom\n// path that is intentionally short, since some plugins store unix sockets in\n// their virtenv and unix sockets require their paths to be short.\nconst xdgStateHomeDir = \"/tmp/devbox-testscripts\"\n\n// RunDevboxTestscripts generates and runs a testscript test for each Devbox project in dir.\n// For each project, runs `devbox run run_test` (if script exists) and asserts it succeeds.\nfunc RunDevboxTestscripts(t *testing.T, dir string) {\n\t// ensure the state home dir for devbox exists\n\terr := os.MkdirAll(xdgStateHomeDir, 0o700)\n\tif err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\tt.Error(err)\n\t}\n\n\terr = filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !entry.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\tconfigPath := filepath.Join(path, \"devbox.json\")\n\t\tconfig, err := devconfig.Open(configPath)\n\t\tif err != nil {\n\t\t\t// skip directories that do not have a devbox.json defined\n\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\t// skip configs that do not have a run_test defined\n\t\tif _, ok := config.Scripts()[\"run_test\"]; !ok {\n\t\t\tt.Logf(\"skipping config due to missing run_test at: %s\\n\", path)\n\t\t\treturn nil\n\t\t}\n\n\t\tif strings.Contains(path, \"pipenv\") {\n\t\t\t// pipenv takes 1100 seconds on CICD\n\n\t\t\t// CI env var is always true in GitHub Actions\n\t\t\t// https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables\n\t\t\tisInCI := envir.IsCI()\n\t\t\tif isInCI && runtime.GOOS == \"darwin\" {\n\t\t\t\tt.Logf(\"skipping pipenv on darwin in CI. config at: %s\\n\", path)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tif strings.Contains(path, \"drupal\") {\n\t\t\t// drupal has errors like: https://gist.github.com/savil/9c67ffa50a2c51d118f3a4ce29ab920d\n\t\t\tt.Logf(\"skipping drupal, config at: %s\\n\", path)\n\t\t\treturn nil\n\t\t}\n\n\t\tt.Logf(\"running testscript for example: %s\\n\", path)\n\t\trunSingleDevboxTestscript(t, dir, path)\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc runSingleDevboxTestscript(t *testing.T, dir, projectDir string) {\n\ttestscriptDir, err := generateTestscript(t, dir, projectDir)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tparams := getTestscriptParams(testscriptDir)\n\n\t// save a reference to the original params.Setup so that we can wrap it below\n\tsetup := params.Setup\n\tparams.Setup = func(envs *testscript.Env) error {\n\t\t// We set a custom XDG_STATE_HOME to an intentionally short path.\n\t\t// Reason: devbox plugins like postgres store unix socket files in their state dir.\n\t\tenvs.Setenv(envir.XDGStateHome, xdgStateHomeDir)\n\n\t\t// setup the devbox testscript environment\n\t\tif err := setup(envs); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\n\t\t// copy all the files and folders of the devbox-project being tested to the workdir\n\t\tslog.Debug(\"copying projectDir: %s to env.WorkDir: %s\\n\", projectDir, envs.WorkDir)\n\t\t// implementation detail: the period at the end of the projectDir/.\n\t\t// is important to ensure this works for both mac and linux.\n\t\t// Ref.https://dev.to/ackshaey/macos-vs-linux-the-cp-command-will-trip-you-up-2p00\n\n\t\tcmd := exec.Command(\"rm\", \"-rf\", projectDir+\"/.devbox\")\n\t\terr = cmd.Run()\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed %s before doing cp\", \"cmd\", cmd, \"err\", err)\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\n\t\tcmd = exec.Command(\"cp\", \"-r\", projectDir+\"/.\", envs.WorkDir)\n\t\tslog.Debug(\"running cmd\", \"cmd\", cmd)\n\t\terr = cmd.Run()\n\t\treturn errors.WithStack(err)\n\t}\n\n\ttestscript.Run(t, params)\n}\n\n// generateTestscript will create a temp-directory and place the generic\n// testscript file (.test.txt) for all devbox-projects in the dir.\n// It returns the directory containing the testscript file.\nfunc generateTestscript(t *testing.T, dir, projectDir string) (string, error) {\n\ttestPath, err := filepath.Rel(dir, projectDir)\n\tif err != nil {\n\t\treturn \"\", errors.WithStack(err)\n\t}\n\n\t// scriptName is the generic script file used for all devbox projects\n\tconst scriptName = \"run_test.test.txt\"\n\n\t// scriptNameForProject prefixes the project's path (with underscores) to the scriptName\n\t// so that the golang testing.T provides nice readable names for the test run\n\t// for each Example devbox-project.\n\tscriptNameForProject := fmt.Sprintf(\n\t\t\"%s_%s\",\n\t\tstrings.ReplaceAll(testPath, \"/\", \"_\"),\n\t\tscriptName,\n\t)\n\n\t// create a temp-dir to place the testscript file\n\ttestscriptDir := t.TempDir()\n\n\t// Copy the testscript file to the temp-dir\n\trunTestScriptPath := filepath.Join(\"testrunner\", scriptName)\n\tslog.Debug(\"copying run_test.test.txt from %s to %s\\n\", runTestScriptPath, testscriptDir)\n\t// Using os's cp command for expediency.\n\terr = exec.Command(\"cp\", runTestScriptPath, testscriptDir+\"/\"+scriptNameForProject).Run()\n\treturn testscriptDir, errors.WithStack(err)\n}\n"
  },
  {
    "path": "testscripts/testrunner/run_test.test.txt",
    "content": "env DEVBOX_FEATURE_SCRIPT_EXIT_ON_ERROR=1\nexec devbox run run_test\n"
  },
  {
    "path": "testscripts/testrunner/setupenv.go",
    "content": "package testrunner\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/rogpeppe/go-internal/testscript\"\n\n\t\"go.jetify.com/devbox/internal/debug\"\n\t\"go.jetify.com/devbox/internal/envir\"\n\t\"go.jetify.com/devbox/internal/xdg\"\n)\n\n// setupTestEnv configures env for devbox tests.\nfunc setupTestEnv(env *testscript.Env) error {\n\tsetupPATH(env)\n\tsetupHome(env)\n\tsetupCacheHome(env)\n\tpropagateEnvVars(env,\n\t\tdebug.DevboxDebug, // to enable extra logging\n\t\t\"SSL_CERT_FILE\",   // so HTTPS works with Nix-installed certs\n\t)\n\treturn nil\n}\n\n// setupHome sets the test's HOME to a unique temp directory. The testscript\n// package sets it to /no-home by default (presumably to improve isolation), but\n// this breaks most programs.\nfunc setupHome(env *testscript.Env) {\n\tenv.Setenv(envir.Home, env.T().(testing.TB).TempDir())\n}\n\n// setupPATH removes all directories from the test's PATH to ensure that it only\n// uses the PATH set by devbox. The one exception is the testscript's bin\n// directory, which contains the commands given to testscript.RunMain\n// (such as devbox itself).\nfunc setupPATH(env *testscript.Env) {\n\ts, _, _ := strings.Cut(env.Getenv(envir.Path), string(filepath.ListSeparator))\n\tenv.Setenv(envir.Path, s)\n}\n\n// setupCacheHome sets the test's XDG_CACHE_HOME to a unique temp directory so\n// that it doesn't share caches with other tests or the user's system. For\n// programs where this would make tests too slow, it symlinks specific cache\n// subdirectories to a shared location that persists between test runs. For\n// example, $WORK/.cache/nix would symlink to $XDG_CACHE_HOME/devbox-tests/nix\n// so that Nix doesn't re-download tarballs for every test.\nfunc setupCacheHome(env *testscript.Env) {\n\tt := env.T().(testing.TB) //nolint:varnamelen\n\n\tcacheHome := filepath.Join(env.WorkDir, \".cache\")\n\tenv.Setenv(envir.XDGCacheHome, cacheHome)\n\terr := os.MkdirAll(cacheHome, 0o755)\n\tif err != nil {\n\t\tt.Fatal(\"create XDG_CACHE_HOME for test:\", err)\n\t}\n\n\t// Symlink cache subdirectories that we want to share and persist\n\t// between tests.\n\tsharedCacheDir := xdg.CacheSubpath(\"devbox-tests\")\n\tfor _, subdir := range []string{\"nix\", \"pip\"} {\n\t\tsharedSubdir := filepath.Join(sharedCacheDir, subdir)\n\t\terr := os.MkdirAll(sharedSubdir, 0o755)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"create shared XDG_CACHE_HOME subdir:\", err)\n\t\t}\n\n\t\ttestSubdir := filepath.Join(cacheHome, subdir)\n\t\terr = os.Symlink(sharedSubdir, testSubdir)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"symlink test's XDG_CACHE_HOME subdir to shared XDG_CACHE_HOME subdir:\", err)\n\t\t}\n\t}\n}\n\n// propagateEnvVars propagates the values of environment variables to the test\n// environment.\nfunc propagateEnvVars(env *testscript.Env, vars ...string) {\n\tfor _, key := range vars {\n\t\tif v, ok := os.LookupEnv(key); ok {\n\t\t\tenv.Setenv(key, v)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "testscripts/testrunner/source.go",
    "content": "package testrunner\n\nimport (\n\t\"strings\"\n\n\t\"github.com/rogpeppe/go-internal/testscript\"\n\n\t\"go.jetify.com/devbox/internal/envir\"\n)\n\n// Sources whatever path is exported in stdout. Ignored everything else\n// Usage:\n// exec devbox shellenv\n// source.path\nfunc sourcePath(script *testscript.TestScript, neg bool, args []string) {\n\tif len(args) != 0 {\n\t\tscript.Fatalf(\"usage: source.path\")\n\t}\n\tif neg {\n\t\tscript.Fatalf(\"source.path does not support negation\")\n\t}\n\tsourcedScript := script.ReadFile(\"stdout\")\n\tfor _, line := range strings.Split(sourcedScript, \"\\n\") {\n\t\tif strings.HasPrefix(line, \"export PATH=\") {\n\t\t\tpath := strings.TrimPrefix(line, \"export PATH=\")\n\t\t\tpath = strings.Trim(path, \"\\\"\")\n\t\t\tscript.Setenv(envir.Path, path)\n\t\t\tbreak\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "testscripts/testrunner/testrunner.go",
    "content": "package testrunner\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/bmatcuk/doublestar/v4\"\n\t\"github.com/rogpeppe/go-internal/testscript\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.jetify.com/devbox/internal/boxcli\"\n)\n\nfunc Main(m *testing.M) {\n\tcommands := map[string]func(){\n\t\t\"devbox\": func() {\n\t\t\t// Call the devbox CLI directly:\n\t\t\tos.Exit(boxcli.Execute(context.Background(), os.Args[1:]))\n\t\t},\n\t\t\"print\": func() { // Not 'echo' because we don't expand variables\n\t\t\tfmt.Println(strings.Join(os.Args[1:], \" \"))\n\t\t},\n\t}\n\ttestscript.Main(m, commands)\n}\n\nfunc RunTestscripts(t *testing.T, testscriptsDir string) {\n\tglobPattern := filepath.Join(testscriptsDir, \"**/*.test.txt\")\n\tdirs := globDirs(globPattern)\n\trequire.NotEmpty(t, dirs, \"no test scripts found\")\n\n\t// Loop through all the directories and run all tests scripts (files ending\n\t// in .test.txt)\n\tfor _, dir := range dirs {\n\t\t// The testrunner dir has the testscript we use for projects in examples/ directory.\n\t\t// We should skip that one since it is run separately (see RunExamplesTestscripts).\n\t\tif filepath.Base(dir) == \"testrunner\" {\n\t\t\tcontinue\n\t\t}\n\n\t\ttestscript.Run(t, getTestscriptParams(dir))\n\t}\n}\n\n// Return directories that contain files matching the pattern.\nfunc globDirs(pattern string) []string {\n\tscripts, err := doublestar.FilepathGlob(pattern)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\t// List of directories with test scripts.\n\tdirectories := []string{}\n\tdups := map[string]bool{}\n\tfor _, script := range scripts {\n\t\tdir := filepath.Dir(script)\n\t\tif _, ok := dups[dir]; !ok {\n\t\t\tdirectories = append(directories, dir)\n\t\t\tdups[dir] = true\n\t\t}\n\t}\n\n\treturn directories\n}\n\n// copyFileCmd enables copying files within the WORKDIR\nfunc copyFileCmd(script *testscript.TestScript, neg bool, args []string) {\n\tif len(args) < 2 {\n\t\tscript.Fatalf(\"usage: cp <from-file> <to-file>\")\n\t}\n\tif neg {\n\t\tscript.Fatalf(\"neg does not make sense for this command\")\n\t}\n\terr := script.Exec(\"cp\", script.MkAbs(args[0]), script.MkAbs(args[1]))\n\tscript.Check(err)\n}\n\nfunc globCmd(script *testscript.TestScript, neg bool, args []string) {\n\tcount := -1\n\tif neg {\n\t\tcount = 0\n\t}\n\tif len(args) != 0 {\n\t\tafter, ok := strings.CutPrefix(args[0], \"-count=\")\n\t\tif ok {\n\t\t\tvar err error\n\t\t\tcount, err = strconv.Atoi(after)\n\t\t\tif err != nil {\n\t\t\t\tscript.Fatalf(\"invalid -count=: %v\", err)\n\t\t\t}\n\t\t\tif count < 1 {\n\t\t\t\tscript.Fatalf(\"invalid -count=: must be at least 1\")\n\t\t\t}\n\t\t\targs = args[1:]\n\t\t}\n\t}\n\tif len(args) == 0 {\n\t\tscript.Fatalf(\"usage: glob [-count=N] pattern\")\n\t}\n\n\tvar matches []string\n\tfor _, a := range args {\n\t\tglob := script.MkAbs(a)\n\t\tm, err := filepath.Glob(glob)\n\t\tif err != nil {\n\t\t\tscript.Fatalf(\"invalid glob pattern: %v\", err)\n\t\t}\n\t\tfor _, match := range m {\n\t\t\tscript.Logf(\"glob %q matched: %s\", glob, match)\n\t\t}\n\t\tmatches = append(matches, m...)\n\t}\n\n\t// -1 means that no -count= was given, so we want at least 1 match.\n\tif count == -1 {\n\t\tif len(matches) == 0 && !neg {\n\t\t\tscript.Fatalf(\"no matches for globs %q, want at least 1\", strings.Join(args, \" \"))\n\t\t}\n\t\treturn\n\t}\n\tif len(matches) != count {\n\t\tscript.Fatalf(\"got %d matches for globs %q, want %d\", len(matches), strings.Join(args, \" \"), count)\n\t}\n}\n\nfunc getTestscriptParams(dir string) testscript.Params {\n\treturn testscript.Params{\n\t\tDir:                 dir,\n\t\tRequireExplicitExec: true,\n\t\tTestWork:            false, // Set to true if you're trying to debug a test.\n\t\tSetup:               func(env *testscript.Env) error { return setupTestEnv(env) },\n\t\tCmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){\n\t\t\t\"cp\":                           copyFileCmd,\n\t\t\t\"devboxjson.packages.contains\": assertDevboxJSONPackagesContains,\n\t\t\t\"devboxlock.packages.contains\": assertDevboxLockPackagesContains,\n\t\t\t\"env.path.len\":                 assertPathLength,\n\t\t\t\"glob\":                         globCmd,\n\t\t\t\"json.superset\":                assertJSONSuperset,\n\t\t\t\"path.order\":                   assertPathOrder,\n\t\t\t\"source.path\":                  sourcePath,\n\t\t},\n\t\tCondition: func(cond string) (bool, error) {\n\t\t\tbefore, key, found := strings.Cut(cond, \":\")\n\t\t\tif found && before == \"env\" {\n\t\t\t\tif v, ok := os.LookupEnv(key); ok {\n\t\t\t\t\treturn strconv.ParseBool(v)\n\t\t\t\t}\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t\treturn false, fmt.Errorf(\"unknown condition: %v\", cond)\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "testscripts/testrunner/updater/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n)\n\n// updater is a tool that updates all examples/ in the devbox repo.\nfunc main() {\n\tif err := run(); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// run will loop over all examples that have run_test script\n// run `devbox update` on each such example\nfunc run() error {\n\tdevboxRepoDir, err := devboxRepoDir()\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\texamplesDir := filepath.Join(devboxRepoDir, \"examples\")\n\n\terr = filepath.WalkDir(\n\t\texamplesDir, func(path string, d fs.DirEntry, err error) error {\n\t\t\treturn walkExampleDir(devboxRepoDir, path, d, err)\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\treturn nil\n}\n\n// examplesToTry is a counter for the number of examples to try. Useful for debugging.\nvar examplesToTry = 0\n\nfunc walkExampleDir(devboxRepoDir, path string, dirEntry fs.DirEntry, err error) error {\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\t// Uncomment to try out changes\n\t// if examplesToTry > 3 {\n\t//\treturn nil\n\t// }\n\t_ = examplesToTry // silence linter\n\n\t// If it is a directory, then we don't continue.\n\tif dirEntry.IsDir() {\n\t\t// Skip if it is a directory that we don't want to process at all.\n\t\tskippedDirs := []string{\".devbox\", \"node_modules\"}\n\t\tif lo.Contains(skippedDirs, dirEntry.Name()) {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\t\treturn nil\n\t}\n\n\t// If it is not a devbox.json file, then we don't continue.\n\tif dirEntry.Name() != \"devbox.json\" {\n\t\treturn nil\n\t}\n\n\t// Read the devbox.json file\n\tcontentBytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tcontent := string(contentBytes)\n\n\t// Skip if it doesn't have a run_test script\n\tif !strings.Contains(content, \"run_test\") {\n\t\tfmt.Printf(\"SKIP: config at %s lacks run_test\\n\", path)\n\t\treturn nil\n\t}\n\n\t// run `devbox update` on this example\n\tdevboxExecutable := filepath.Join(devboxRepoDir, \"dist\", \"devbox\")\n\tcmd := exec.Command(devboxExecutable, \"update\", \"-c\", filepath.Dir(path))\n\tif err := cmd.Run(); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tfmt.Printf(\"Ran `devbox update` on %s\\n\", path)\n\texamplesToTry++\n\n\treturn nil\n}\n\nfunc devboxRepoDir() (string, error) {\n\t_, filename, _, ok := runtime.Caller(0)\n\tif !ok {\n\t\treturn \"\", errors.New(\"unable to get the current filename\")\n\t}\n\t// This file's directory\n\tfileDir := filepath.Dir(filename)\n\t// devbox repo directory is 3 levels up\n\treturn filepath.Join(fileDir, \"..\", \"..\", \"..\"), nil\n}\n"
  },
  {
    "path": "testscripts/testscripts_test.go",
    "content": "package testrunner\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"go.jetify.com/devbox/testscripts/testrunner\"\n)\n\n// When true, tests that `devbox run run_test` succeeds on every project (i.e. having devbox.json)\n// found in examples/.. and testscripts/..\nconst runProjectTests = \"DEVBOX_RUN_PROJECT_TESTS\"\n\nfunc TestScripts(t *testing.T) {\n\t// To run a specific test, say, testscripts/foo/bar.test.text, then run\n\t// go test ./testscripts -run TestScripts/bar\n\ttestrunner.RunTestscripts(t, \".\")\n}\n\nfunc TestMain(m *testing.M) {\n\ttestrunner.Main(m)\n}\n\n// TestExamples runs testscripts on the devbox-projects in the examples folder.\nfunc TestExamples(t *testing.T) {\n\tisOn, err := strconv.ParseBool(os.Getenv(runProjectTests))\n\tif err != nil || !isOn {\n\t\tt.Skipf(\"Skipping TestExamples. To enable, set %s=1.\", runProjectTests)\n\t}\n\n\t// To run a specific test, say, examples/foo/bar, then run\n\t// go test ./testscripts -run TestExamples/foo_bar_run_test\n\ttestrunner.RunDevboxTestscripts(t, \"../examples\")\n}\n\n// TestScriptsWithProjects runs testscripts on the devbox-projects in the testscripts folder.\nfunc TestScriptsWithProjects(t *testing.T) {\n\tisOn, err := strconv.ParseBool(os.Getenv(runProjectTests))\n\tif err != nil || !isOn {\n\t\tt.Skipf(\"Skipping TestScriptsWithProjects. To enable, set %s=1.\", runProjectTests)\n\t}\n\n\ttestrunner.RunDevboxTestscripts(t, \".\")\n}\n"
  },
  {
    "path": "testscripts/update/update.test.txt",
    "content": "# Testscript for exercising devbox update\n\nexec devbox install\n\nexec devbox update\n\n-- devbox.json --\n{\n  \"packages\": [\n    \"hello@latest\",\n  ]\n}\n\n-- devbox.lock --\n{\n  \"lockfile_version\": \"1\",\n  \"packages\": {\n    \"hello@2.10\": {\n      \"last_modified\": \"2022-01-26T13:01:16Z\",\n      \"resolved\": \"github:NixOS/nixpkgs/e722007bf05802573b41701c49da6c8814878171#hello\",\n      \"source\": \"devbox-search\",\n      \"version\": \"2.10\",\n      \"systems\": {\n        \"aarch64-darwin\": {\n          \"store_path\": \"/nix/store/c24460c0iw7kai6z5aan6mkgfclpl2qj-hello-2.10\"\n        },\n        \"x86_64-darwin\": {\n          \"store_path\": \"/nix/store/6wzargj47480y84cqqnm7n30xwqlbyrm-hello-2.10\"\n        },\n        \"x86_64-linux\": {\n          \"store_path\": \"/nix/store/nndmy96lswhxc4xp49n950i1905qlfpy-hello-2.10\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "typos.toml",
    "content": "[default.extend-words]\nAKE = \"AKE\"\n\n[files]\nextend-exclude=[\n  \"go.mod\", \n  \"*.svg\", \n  \"**/testdata/**\", \n  \"internal/cachehash/hash_test.go\", \n  \"internal/devpkg/package_test.go\",\n  \"internal/nix/store_test.go\",\n]\n"
  },
  {
    "path": "vendor-hash",
    "content": "sha256-xrN5AGc/f9CaI6WDfEFpJrRbPuBfxsjTGrEG4RbxVtM=\n"
  },
  {
    "path": "vscode-extension/.eslintrc.json",
    "content": "{\n  \"root\": true,\n  \"parser\": \"@typescript-eslint/parser\",\n  \"parserOptions\": {\n    \"ecmaVersion\": 6,\n    \"sourceType\": \"module\"\n  },\n  \"plugins\": [\n    \"@typescript-eslint\"\n  ],\n  \"rules\": {\n    \"@typescript-eslint/naming-convention\": \"warn\",\n    \"@typescript-eslint/semi\": \"warn\",\n    \"curly\": \"warn\",\n    \"eqeqeq\": \"warn\",\n    \"no-throw-literal\": \"warn\",\n    \"semi\": \"off\"\n  },\n  \"ignorePatterns\": [\n    \"out\",\n    \"dist\",\n    \"**/*.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "vscode-extension/.gitignore",
    "content": "node_modules\nout\n.vscode\n.DS_Store"
  },
  {
    "path": "vscode-extension/.vscodeignore",
    "content": ".vscode/**\n.vscode-test/**\nsrc/**\n.gitignore\n.yarnrc\nvsc-extension-quickstart.md\n**/tsconfig.json\n**/.eslintrc.json\n**/*.map\n**/*.ts\n"
  },
  {
    "path": "vscode-extension/.yarnrc",
    "content": "--ignore-engines true"
  },
  {
    "path": "vscode-extension/CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to the \"devbox\" extension will be documented in this file.\n\nCheck [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.\n\n## [0.1.8]\n\n- Added support \"Reopen in Devbox\" feature for Windows with WSL\n  - Credit: [Adrian Grucza](https://github.com/apgrucza)\n\n## [0.1.7]\n\n- Removed Open In Desktop feature since devbox.sh web app is deprecated.\n\n## [0.1.6]\n\n- Fixed an issue where reopen in devbox feature wasn't working for cursor and vscodium.\n- Removed remote-ssh as a dependency extension.\n\n## [0.1.5]\n\n- Rebranding changes from jetpack.io to jetify.com.\n\n## [0.1.4]\n\n- Added debug mode in extension settings (only supports logs for \"Reopen in Devbox Shell environment\" feature).\n\n## [0.1.3]\n\n- Added json validation for devbox.json files.\n\n## [0.1.2]\n\n- Fixed error handling when using `Reopen in Devbox shell` command in Windows and WSL\n\n## [0.1.1]\n\n- Fixed documentation\n- Added devbox install command\n- Added devbox update command\n- Added devbox search command\n\n## [0.1.0]\n\n- Added reopen in devbox shell environment feature that allows projects with devbox.json\n  reopen vscode in devbox environment. Note: It requires devbox CLI v0.5.5 and above\n  installed and in PATH. This feature is in beta. Please report any bugs/issues in [Github](https://github.com/jetify-com/devbox) or our [Discord](https://discord.gg/jetify).\n\n## [0.0.7]\n\n- Fixed a bug for `Open in VSCode` that ensures the directory in which\n  we save the VM's ssh key does exist.\n\n## [0.0.6]\n\n- Fixed a small bug connecting to a remote environment.\n- Added better error handling and messages if connecting to devbox cloud fails.\n\n## [0.0.5]\n\n- Added handling `Open In VSCode` button with `vscode://` style links.\n- Added ability for connecting to Devbox Cloud workspace.\n- Fixed a bug where devbox extension would run `devbox shell` when opening\na new terminal in vscode even if there was no devbox.json present in the workspace.\n\n## [0.0.4]\n\n- Added `Generate a Dockerfile from devbox.json` to the command palette\n- Changed `Generate Dev Containers config files` command's logic to use devbox CLI.\n\n## [0.0.3]\n\n- Small fix for DevContainers and Github CodeSpaces compatibility.\n\n## [0.0.2]\n\n- Added ability to run devbox commands from VSCode command palette\n- Added VSCode command to generate DevContainer files to run VSCode in local container or Github CodeSpaces.\n- Added customization in settings to turn on/off automatically running `devbox shell` when a terminal window is opened.\n\n## [0.0.1]\n\n- Initial release\n- When VScode Terminal is opened on a devbox project, this extension detects `devbox.json` and runs `devbox shell` so terminal is automatically in devbox shell environment.\n"
  },
  {
    "path": "vscode-extension/LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "vscode-extension/README.md",
    "content": "# devbox VSCode Extension\n\nThis is the official VSCode extension for [devbox](https://github.com/jetify-com/devbox) open source project by [jetify.com](https://www.jetify.com)\n\n## Features\n\n### Auto Shell on a devbox project\n\nWhen VSCode Terminal is opened on a devbox project, this extension detects `devbox.json` and runs `devbox shell` so terminal is automatically in devbox shell environment. Can be turned off in settings.\n\n### Reopen in Devbox shell environment\n\nIf the opened workspace in VSCode has a devbox.json file, from command palette, invoking the devbox command `Reopen in Devbox shell environment` will do the following:\n\n1. Installs devbox packages if missing.\n2. Update workspace settings for MacOS to create terminals without creating a login shell [learn more](https://code.visualstudio.com/docs/terminal/profiles#_why-are-there-duplicate-paths-in-the-terminals-path-environment-variable-andor-why-are-they-reversed-on-macos)\n3. Interact with Devbox CLI to setup a devbox shell.\n4. Close current VSCode window and reopen it in a devbox shell environment as if VSCode was opened from a devbox shell terminal.\n\nNOTE: Requires devbox CLI v0.5.5 and above\n  installed and in PATH. This feature is in beta. Please report any bugs/issues in [Github](https://github.com/jetify-com/devbox) or our [Discord](https://discord.gg/jetify).\n\nNOTE2: This feature works with Linux, MacOS, and Windows with WSL (project files should reside in WSL and devbox CLI needs to be installed and in PATH in WSL)\n\n### Run devbox commands from command palette\n\n`cmd/ctrl + shift + p` opens vscode's command palette. Typing devbox filters all available commands devbox extension can run. Those commands are:\n\n- **Init:** Creates a devbox.json file\n- **Add:** adds a package to devbox.json\n- **Remove:** Removes a package from devbox.json\n- **Shell:** Opens a terminal and runs devbox shell\n- **Run:** Runs a script from devbox.json if specified\n- **Install** Install packages specified in devbox.json\n- **Update** Update packages specified in devbox.json\n- **Search** Search for packages to add to your devbox project\n- **Generate DevContainer files:** Generates devcontainer.json & Dockerfile inside .devcontainers directory. This allows for running vscode in a container or GitHub Codespaces.\n- **Generate a Dockerfile from devbox.json:** Generates a Dockerfile a project's root directory. This allows for running the devbox project in a container.\n- **Reopen in Devbox shell environment:** Allows projects with devbox.json\n  reopen VSCode in devbox environment. Note: It requires devbox CLI v0.5.5 and above\n  installed and in PATH.\n\n### JSON validation when writing a devbox.json file\n\nNo need to take any action for this feature. When writing a devbox.json, if this extension is installed, it will validate and highlight any disallowed fields or values on a devbox.json file.\n\n---\n\n### Debug Mode\n\nEnabling debug mode in extension settings will create a sequence of logs in the file: `.devbox/extension.log`. This feature only tracks the logs for `\"Devbox: Reopen in Devbox Shell environment\"`.\n\n## Following extension guidelines\n\nEnsure that you've read through the extensions guidelines and follow the best practices for creating your extension.\n\n- [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)\n\n## Publishing\n\nSteps:\n\n1. Bump the version in `package.json`, and add notes to `CHANGELOG.md`. Sample PR: #951.\n2. Manually trigger the [`vscode-ext-release` in Github Actions](https://github.com/jetify-com/devbox/actions/workflows/vscode-ext-release.yaml).\n"
  },
  {
    "path": "vscode-extension/package.json",
    "content": "{\n  \"name\": \"devbox\",\n  \"displayName\": \"devbox by Jetify\",\n  \"description\": \"devbox integration for VSCode\",\n  \"version\": \"0.1.8\",\n  \"icon\": \"assets/icon.png\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/jetify-com/devbox.git\",\n    \"directory\": \"vscode-extension\"\n  },\n  \"author\": \"Jetify\",\n  \"publisher\": \"jetpack-io\",\n  \"engines\": {\n    \"vscode\": \"^1.72.0\"\n  },\n  \"categories\": [\n    \"Other\"\n  ],\n  \"activationEvents\": [\n    \"onStartupFinished\"\n  ],\n  \"main\": \"./out/extension.js\",\n  \"contributes\": {\n    \"commands\": [\n      {\n        \"command\": \"devbox.setupDevContainer\",\n        \"title\": \"Devbox: Generate Dev Containers config files\"\n      },\n      {\n        \"command\": \"devbox.reopen\",\n        \"title\": \"Devbox: Reopen in Devbox shell environment\"\n      },\n      {\n        \"command\": \"devbox.install\",\n        \"title\": \"Devbox: Install - Install packages in your devbox project\"\n      },\n      {\n        \"command\": \"devbox.update\",\n        \"title\": \"Devbox: Update - Update packages in your devbox project\"\n      },\n      {\n        \"command\": \"devbox.search\",\n        \"title\": \"Devbox: Search - Search for packages for your devbox project\"\n      },\n      {\n        \"command\": \"devbox.generateDockerfile\",\n        \"title\": \"Devbox: Generate a Dockerfile from devbox.json\"\n      },\n      {\n        \"command\": \"devbox.add\",\n        \"title\": \"Devbox: Add - add packages to your devbox project\"\n      },\n      {\n        \"command\": \"devbox.remove\",\n        \"title\": \"Devbox: Remove - remove packages from your devbox project\"\n      },\n      {\n        \"command\": \"devbox.run\",\n        \"title\": \"Devbox: Run - execute scripts specified in devbox.json\"\n      },\n      {\n        \"command\": \"devbox.shell\",\n        \"title\": \"Devbox: Shell - Go to devbox shell in the terminal\"\n      },\n      {\n        \"command\": \"devbox.init\",\n        \"title\": \"Devbox: Init - Initiate a devbox project\"\n      }\n    ],\n    \"menus\": {\n      \"commandPalette\": [\n        {\n          \"command\": \"devbox.setupDevContainer\",\n          \"when\": \"devbox.configFileExists == true\"\n        },\n        {\n          \"command\": \"devbox.reopen\",\n          \"when\": \"devbox.configFileExists == true\"\n        },\n        {\n          \"command\": \"devbox.install\",\n          \"when\": \"devbox.configFileExists == true\"\n        },\n        {\n          \"command\": \"devbox.update\",\n          \"when\": \"devbox.configFileExists == true\"\n        },\n        {\n          \"command\": \"devbox.search\",\n          \"when\": \"devbox.configFileExists == true\"\n        },\n        {\n          \"command\": \"devbox.add\",\n          \"when\": \"devbox.configFileExists == true\"\n        },\n        {\n          \"command\": \"devbox.remove\",\n          \"when\": \"devbox.configFileExists == true\"\n        },\n        {\n          \"command\": \"devbox.run\",\n          \"when\": \"devbox.configFileExists == true\"\n        },\n        {\n          \"command\": \"devbox.shell\",\n          \"when\": \"devbox.configFileExists == true\"\n        },\n        {\n          \"command\": \"devbox.init\",\n          \"when\": \"devbox.configFileExists == false\"\n        }\n      ]\n    },\n    \"jsonValidation\": [\n      {\n        \"fileMatch\": \"devbox.json\",\n        \"url\": \"https://raw.githubusercontent.com/jetify-com/devbox/main/.schema/devbox.schema.json\"\n      }\n    ],\n    \"configuration\": {\n      \"title\": \"devbox\",\n      \"properties\": {\n        \"devbox.autoShellOnTerminal\": {\n          \"type\": \"boolean\",\n          \"default\": true,\n          \"description\": \"Automatically run devbox shell when terminal is opened.\"\n        },\n        \"devbox.enableDebugMode\": {\n          \"type\": \"boolean\",\n          \"default\": false,\n          \"description\": \"Enables debug mode for this extension which creates an extension.log in .devbox/ directory. Currently only works for 'Devbox: Reopen in Devbox shell environment' command.\"\n        }\n      }\n    }\n  },\n  \"scripts\": {\n    \"vscode:prepublish\": \"yarn run compile\",\n    \"compile\": \"tsc -p ./\",\n    \"watch\": \"tsc -watch -p ./\",\n    \"pretest\": \"yarn run compile && yarn run lint\",\n    \"lint\": \"eslint src --ext ts\",\n    \"test\": \"node ./out/test/runTest.js\"\n  },\n  \"devDependencies\": {\n    \"@types/glob\": \"^8.0.0\",\n    \"@types/mocha\": \"^10.0.0\",\n    \"@types/node\": \"16.x\",\n    \"@types/node-fetch\": \"^2\",\n    \"@types/vscode\": \"^1.72.0\",\n    \"@types/which\": \"^3.0.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.38.1\",\n    \"@typescript-eslint/parser\": \"^5.38.1\",\n    \"@vscode/test-electron\": \"^2.1.5\",\n    \"eslint\": \"^8.24.0\",\n    \"glob\": \"^8.0.3\",\n    \"mocha\": \"^10.0.0\",\n    \"typescript\": \"^4.8.4\"\n  },\n  \"dependencies\": {\n    \"@types/node\": \"16.x\",\n    \"form-data\": \"^4.0.4\",\n    \"js-yaml\": \"^4.1.1\",\n    \"node-fetch\": \"^2\",\n    \"which\": \"^3.0.0\"\n  }\n}\n"
  },
  {
    "path": "vscode-extension/src/devbox.ts",
    "content": "import { window, workspace, commands, ProgressLocation, Uri, ConfigurationTarget, env } from 'vscode';\nimport { writeFile, open } from 'fs/promises';\nimport { spawn, spawnSync } from 'node:child_process';\n\n\ninterface Message {\n    status: string\n}\n\nconst appNameBinaryMap: {[key: string]: string} = {\n  \"vscodium\": \"codium\",\n  // eslint-disable-next-line @typescript-eslint/naming-convention\n  \"visual studio code\": \"code\",\n  \"cursor\": \"cursor\",\n};\n\nexport async function devboxReopen() {\n  if (process.platform === 'win32') {\n    const seeDocs = 'See Devbox docs';\n    const result = await window.showErrorMessage(\n      'This feature is not supported on your platform. \\\n      Please open VSCode from inside devbox shell in WSL using the CLI.', seeDocs\n    );\n    if (result === seeDocs) {\n      env.openExternal(Uri.parse('https://www.jetify.com/devbox/docs/ide_configuration/vscode/#windows-setup'));\n      return;\n    }\n  }\n  await window.withProgress({\n    location: ProgressLocation.Notification,\n    title: \"Setting up your Devbox environment. Please don't close vscode.\",\n    cancellable: true\n  },\n    async (progress, token) => {\n      token.onCancellationRequested(() => {\n        console.log(\"User canceled the long running operation\");\n      });\n\n      const p = new Promise<void>(async (resolve, reject) => {\n        if (workspace.workspaceFolders) {\n          const workingDir = workspace.workspaceFolders[0].uri;\n          const dotdevbox = Uri.joinPath(workingDir, '/.devbox');\n          await logToFile(dotdevbox, 'Installing devbox packages');\n          progress.report({ message: 'Installing devbox packages...', increment: 25 });\n          await setupDotDevbox(workingDir, dotdevbox);\n          \n          // setup required vscode settings\n          await logToFile(dotdevbox, 'Updating editor configurations');\n          progress.report({ message: 'Updating configurations...', increment: 50 });\n          updateVSCodeConf();\n\n          // Calling CLI to compute devbox env\n          await logToFile(dotdevbox, 'Calling \"devbox integrate\" to setup environment');\n          progress.report({ message: 'Calling Devbox to setup environment...', increment: 80 });\n          // To use a custom compiled devbox when testing, change this to an absolute path.\n          const devbox = 'devbox';\n          // run devbox integrate and then close this window\n          const debugModeFlag = workspace.getConfiguration(\"devbox\").get(\"enableDebugMode\");\n          // name of the currently open editor\n          const ideName = appNameBinaryMap[env.appName.toLocaleLowerCase()] || 'code';\n          let child = spawn(devbox, ['integrate', 'vscode', '--debugmode='+debugModeFlag, '--ide='+ideName], {\n            cwd: workingDir.path,\n            stdio: [0, 1, 2, 'ipc']\n          });\n          // if CLI closes before sending \"finished\" message\n          child.on('close', (code: number) => {\n            console.log(\"child process closed with exit code:\", code);\n            logToFile(dotdevbox, 'child process closed with exit code: ' + code);\n            window.showErrorMessage(\"Failed to setup devbox environment.\");\n            reject();\n          });\n          // send config path to CLI\n          child.send({ configDir: workingDir.path });\n          // handle CLI finishing the env and sending  \"finished\"\n          child.on('message', function (msg: Message, handle) {\n            if (msg.status === \"finished\") {\n              progress.report({ message: 'Finished setting up! Reloading the window...', increment: 100 });\n              resolve();\n              commands.executeCommand(\"workbench.action.closeWindow\");\n            }\n            else {\n              console.log(msg);\n              logToFile(dotdevbox, 'Failed to setup devbox environment.' + String(msg));\n              window.showErrorMessage(\"Failed to setup devbox environment.\");\n              reject();\n            }\n          });\n        }\n      });\n      return p;\n    }\n  );\n}\n\nasync function setupDotDevbox(workingDir: Uri, dotdevbox: Uri) {\n  try {\n    // check if .devbox exists\n    await workspace.fs.stat(dotdevbox);\n  } catch (error) {\n    //.devbox doesn't exist\n    // running devbox shellenv to create it\n    spawnSync('devbox', ['shellenv'], {\n      cwd: workingDir.path\n    });\n  }\n}\n\nfunction updateVSCodeConf() {\n  if (process.platform === 'darwin') {\n    const shell = process.env[\"SHELL\"] ?? \"/bin/zsh\";\n    const shellArgsMap = (shellType: string) => {\n      switch (shellType) {\n        case \"fish\":\n          // We special case fish here because fish's `fish_add_path` function\n          // tends to prepend to PATH by default, hence sourcing the fish config after\n          // vscode reopens in devbox environment, overwrites devbox packages and \n          // might cause confusion for users as to why their system installed packages\n          // show up when they type for example `which go` as opposed to the packages\n          // installed by devbox.\n          return [\"--no-config\"];\n        default:\n          return [];\n      }\n    };\n    const shellTypeSlices = shell.split(\"/\");\n    const shellType = shellTypeSlices[shellTypeSlices.length - 1];\n    shellArgsMap(shellType);\n    const devboxCompatibleShell = {\n      \"devboxCompatibleShell\": {\n        \"path\": shell,\n        \"args\": shellArgsMap(shellType)\n      }\n    };\n\n    workspace.getConfiguration().update(\n      'terminal.integrated.profiles.osx',\n      devboxCompatibleShell,\n      ConfigurationTarget.Workspace\n    );\n    workspace.getConfiguration().update(\n      'terminal.integrated.defaultProfile.osx',\n      'devboxCompatibleShell',\n      ConfigurationTarget.Workspace\n    );\n  }\n}\n\nasync function logToFile(dotDevboxPath: Uri, message: string) {\n  // only print to log file if debug mode config is set to true\n  if (workspace.getConfiguration(\"devbox\").get(\"enableDebugMode\")){\n    try {   \n      const logFilePath = Uri.joinPath(dotDevboxPath, 'extension.log');\n      const timestamp = new Date().toUTCString();\n      const fileHandler = await open(logFilePath.fsPath, 'a');\n      const logData = new Uint8Array(Buffer.from(`[${timestamp}] ${message}\\n`));\n      await writeFile(fileHandler, logData, {flag: 'a'} );\n      await fileHandler.close();\n    } catch (error) {\n      console.log(\"failed to write to extension.log file\");\n      console.error(error);\n    }\n  }\n}\n"
  },
  {
    "path": "vscode-extension/src/extension.ts",
    "content": "// The module 'vscode' contains the VS Code extensibility API\nimport { workspace, window, commands, Uri, ExtensionContext } from 'vscode';\nimport { posix } from 'path';\n\nimport { handleOpenInVSCode } from './openinvscode';\nimport { devboxReopen } from './devbox';\n\n// This method is called when your extension is activated\n// Your extension is activated the very first time the command is executed\nexport function activate(context: ExtensionContext) {\n\t// This line of code will only be executed once when your extension is activated\n\tinitialCheckDevboxJSON(context);\n\t// Creating file watchers to watch for events on devbox.json\n\tconst fswatcher = workspace.createFileSystemWatcher(\"**/devbox.json\", false, false, false);\n\n\tfswatcher.onDidDelete(e => {\n\t\tcommands.executeCommand('setContext', 'devbox.configFileExists', false);\n\t\tcontext.workspaceState.update(\"configFileExists\", false);\n\t});\n\tfswatcher.onDidCreate(e => {\n\t\tcommands.executeCommand('setContext', 'devbox.configFileExists', true);\n\t\tcontext.workspaceState.update(\"configFileExists\", true);\n\t});\n\tfswatcher.onDidChange(e => initialCheckDevboxJSON(context));\n\n\t// Check for devbox.json when a new folder is opened\n\tworkspace.onDidChangeWorkspaceFolders(async (e) => initialCheckDevboxJSON(context));\n\n\t// run devbox shell when terminal is opened\n\twindow.onDidOpenTerminal(async (e) => {\n\t\tif (workspace.getConfiguration(\"devbox\").get(\"autoShellOnTerminal\")\n\t\t\t&& e.name !== \"DevboxTerminal\"\n\t\t\t&& context.workspaceState.get(\"configFileExists\")\n\t\t) {\n\t\t\tawait runInTerminal('devbox shell', true);\n\t\t}\n\t});\n\n\t// open in vscode URI handler\n\tconst handleVSCodeUri = window.registerUriHandler({ handleUri: handleOpenInVSCode });\n\n\tconst devboxAdd = commands.registerCommand('devbox.add', async () => {\n\t\tconst result = await window.showInputBox({\n\t\t\tvalue: '',\n\t\t\tplaceHolder: 'Package to add to devbox. E.g., python39',\n\t\t});\n\t\tawait runInTerminal(`devbox add ${result}`, false);\n\t});\n\n\tconst devboxRun = commands.registerCommand('devbox.run', async () => {\n\t\tconst items = await getDevboxScripts();\n\t\tif (items.length > 0) {\n\t\t\tconst result = await window.showQuickPick(items);\n\t\t\tawait runInTerminal(`devbox run ${result}`, true);\n\t\t} else {\n\t\t\twindow.showInformationMessage(\"No scripts found in devbox.json\");\n\t\t}\n\t});\n\n\tconst devboxShell = commands.registerCommand('devbox.shell', async () => {\n\t\t// todo: add support for --config path to devbox.json\n\t\tawait runInTerminal('devbox shell', true);\n\t});\n\n\tconst devboxRemove = commands.registerCommand('devbox.remove', async () => {\n\t\tconst items = await getDevboxPackages();\n\t\tif (items.length > 0) {\n\t\t\tconst result = await window.showQuickPick(items);\n\t\t\tawait runInTerminal(`devbox rm ${result}`, false);\n\t\t} else {\n\t\t\twindow.showInformationMessage(\"No packages found in devbox.json\");\n\t\t}\n\t});\n\n\tconst devboxInit = commands.registerCommand('devbox.init', async () => {\n\t\tawait runInTerminal('devbox init', false);\n\t\tcommands.executeCommand('setContext', 'devbox.configFileExists', true);\n\t});\n\n\tconst devboxInstall = commands.registerCommand('devbox.install', async () => {\n\t\tawait runInTerminal('devbox install', true);\n\t});\n\n\tconst devboxUpdate = commands.registerCommand('devbox.update', async () => {\n\t\tawait runInTerminal('devbox update', true);\n\t});\n\n\tconst devboxSearch = commands.registerCommand('devbox.search', async () => {\n\t\tconst result = await window.showInputBox({ placeHolder: \"Name or a subset of a name of a package to search\" });\n\t\tawait runInTerminal(`devbox search ${result}`, true);\n\t});\n\n\tconst setupDevcontainer = commands.registerCommand('devbox.setupDevContainer', async () => {\n\t\tawait runInTerminal('devbox generate devcontainer', true);\n\t});\n\n\tconst generateDockerfile = commands.registerCommand('devbox.generateDockerfile', async () => {\n\t\tawait runInTerminal('devbox generate dockerfile', true);\n\t});\n\n\tconst reopen = commands.registerCommand('devbox.reopen', async () => {\n\t\tawait devboxReopen();\n\t});\n\n\tcontext.subscriptions.push(reopen);\n\tcontext.subscriptions.push(devboxAdd);\n\tcontext.subscriptions.push(devboxRun);\n\tcontext.subscriptions.push(devboxInit);\n\tcontext.subscriptions.push(devboxInstall);\n\tcontext.subscriptions.push(devboxSearch);\n\tcontext.subscriptions.push(devboxUpdate);\n\tcontext.subscriptions.push(devboxRemove);\n\tcontext.subscriptions.push(devboxShell);\n\tcontext.subscriptions.push(setupDevcontainer);\n\tcontext.subscriptions.push(generateDockerfile);\n\tcontext.subscriptions.push(handleVSCodeUri);\n}\n\nasync function initialCheckDevboxJSON(context: ExtensionContext) {\n\t// check if there is a workspace folder open\n\tif (workspace.workspaceFolders) {\n\t\tconst workspaceUri = workspace.workspaceFolders[0].uri;\n\t\ttry {\n\t\t\t// check if the folder has devbox.json in it\n\t\t\tawait workspace.fs.stat(Uri.joinPath(workspaceUri, \"devbox.json\"));\n\t\t\t// devbox.json exists setcontext for devbox commands to be available\n\t\t\tcommands.executeCommand('setContext', 'devbox.configFileExists', true);\n\t\t\tcontext.workspaceState.update(\"configFileExists\", true);\n\t\t} catch (err) {\n\t\t\tconsole.log(err);\n\t\t\t// devbox.json does not exist\n\t\t\tcommands.executeCommand('setContext', 'devbox.configFileExists', false);\n\t\t\tcontext.workspaceState.update(\"configFileExists\", false);\n\t\t\tconsole.log(\"devbox.json does not exist\");\n\t\t}\n\t}\n}\n\nasync function runInTerminal(cmd: string, showTerminal: boolean) {\n\t// check if a terminal is open\n\tif ((<any>window).terminals.length === 0) {\n\t\tconst terminalName = 'DevboxTerminal';\n\t\tconst terminal = window.createTerminal({ name: terminalName });\n\t\tif (showTerminal) {\n\t\t\tterminal.show();\n\t\t}\n\t\tterminal.sendText(cmd, true);\n\t} else {\n\t\t// A terminal is open\n\t\t// run the given cmd in terminal\n\t\tawait commands.executeCommand('workbench.action.terminal.sendSequence', {\n\t\t\t'text': `${cmd}\\r\\n`\n\t\t});\n\t}\n}\n\nasync function getDevboxScripts(): Promise<string[]> {\n\ttry {\n\t\tif (!workspace.workspaceFolders) {\n\t\t\twindow.showInformationMessage('No folder or workspace opened');\n\t\t\treturn [];\n\t\t}\n\t\tconst workspaceUri = workspace.workspaceFolders[0].uri;\n\t\tconst devboxJson = await readDevboxJson(workspaceUri);\n\t\treturn Object.keys(devboxJson['shell']['scripts']);\n\t} catch (error) {\n\t\tconsole.error('Error processing devbox.json - ', error);\n\t\treturn [];\n\t}\n}\n\nasync function getDevboxPackages(): Promise<string[]> {\n\ttry {\n\t\tif (!workspace.workspaceFolders) {\n\t\t\twindow.showInformationMessage('No folder or workspace opened');\n\t\t\treturn [];\n\t\t}\n\t\tconst workspaceUri = workspace.workspaceFolders[0].uri;\n\t\tconst devboxJson = await readDevboxJson(workspaceUri);\n\t\treturn devboxJson['packages'];\n\t} catch (error) {\n\t\tconsole.error('Error processing devbox.json - ', error);\n\t\treturn [];\n\t}\n}\n\nasync function readDevboxJson(workspaceUri: Uri) {\n\tconst fileUri = workspaceUri.with({ path: posix.join(workspaceUri.path, 'devbox.json') });\n\tconst readData = await workspace.fs.readFile(fileUri);\n\tconst readStr = Buffer.from(readData).toString('utf8');\n\tconst devboxJsonData = JSON.parse(readStr);\n\treturn devboxJsonData;\n}\n\n// This method is called when your extension is deactivated\nexport function deactivate() { }\n"
  },
  {
    "path": "vscode-extension/src/openinvscode.ts",
    "content": "import * as os from 'os';\nimport * as which from 'which';\nimport fetch from 'node-fetch';\nimport { exec } from 'child_process';\nimport * as FormData from 'form-data';\nimport { Uri, commands, window } from 'vscode';\nimport { chmod, mkdir, open, writeFile } from 'fs/promises';\n\ntype VmInfo = {\n  /* eslint-disable @typescript-eslint/naming-convention */\n  vm_id: string;\n  private_key: string;\n  username: string;\n  working_directory: string;\n  /* eslint-enable @typescript-eslint/naming-convention */\n};\n\nexport async function handleOpenInVSCode(uri: Uri) {\n  const queryParams = new URLSearchParams(uri.query);\n\n  if (queryParams.has('vm_id') && queryParams.has('token')) {\n    //Not yet supported for windows + WSL - will be added in future\n    if (os.platform() !== 'win32') {\n      window.showInformationMessage('Setting up devbox');\n      // getting ssh keys\n      try {\n        console.debug('Calling getVMInfo...');\n        const response = await getVMInfo(queryParams.get('token'), queryParams.get('vm_id'));\n        const res = await response.json() as VmInfo;\n        console.debug('getVMInfo response: ' + res);\n        // set ssh config\n        console.debug('Calling setupSSHConfig...');\n        await setupSSHConfig(res.vm_id, res.private_key);\n\n        // connect to remote vm\n        console.debug('Calling connectToRemote...');\n        connectToRemote(res.username, res.vm_id, res.working_directory);\n      } catch (err: any) {\n        console.error(err);\n        window.showInformationMessage('Failed to setup devbox remote connection.');\n      }\n    } else {\n      window.showErrorMessage('This function is not yet supported in Windows operating system.');\n    }\n  } else {\n    window.showErrorMessage('Error parsing information for remote environment.');\n    console.debug(queryParams.toString());\n  };\n}\n\nasync function getVMInfo(token: string | null, vmId: string | null): Promise<any> {\n  // send post request to gateway to get vm info and ssh keys\n  const gatewayHost = 'https://api.devbox.sh/g/vm_info';\n  const data = new FormData();\n  data.append(\"vm_id\", vmId);\n  console.debug(\"calling devbox to get vm_info...\");\n  const response = await fetch(gatewayHost, {\n    method: 'post',\n    body: data,\n    headers: {\n      authorization: `Bearer ${token}`\n    }\n  });\n  console.debug(\"API Call to api.devbox.sh response: \" + response);\n  return response;\n}\n\nasync function setupDevboxLauncher(): Promise<any> {\n  // download devbox launcher script\n  const gatewayHost = 'https://releases.jetify.com/devbox';\n  const response = await fetch(gatewayHost, {\n    method: 'get',\n  });\n  const launcherPath = `${process.env['HOME']}/.config/devbox/launcher.sh`;\n\n  try {\n    const launcherScript = await response.text();\n    const launcherData = new Uint8Array(Buffer.from(launcherScript));\n    const fileHandler = await open(launcherPath, 'w');\n    await writeFile(fileHandler, launcherData, { flag: 'w' });\n    await chmod(launcherPath, 0o711);\n    await fileHandler.close();\n  } catch (err: any) {\n    console.error(\"error setting up launcher script\" + err);\n    throw (err);\n  }\n  return launcherPath;\n}\n\nasync function setupSSHConfig(vmId: string, prKey: string) {\n  const devboxBinary = await which('devbox', { nothrow: true });\n  let devboxPath = 'devbox';\n  if (devboxBinary === null) {\n    devboxPath = await setupDevboxLauncher();\n  }\n  // For testing change devbox to path to a compiled devbox binary or add --config\n  exec(`${devboxPath} generate ssh-config`, (error, stdout, stderr) => {\n    if (error) {\n      window.showErrorMessage('Failed to setup ssh config. Run VSCode in debug mode to see logs.');\n      console.error(`Failed to setup ssh config: ${error}`);\n      return;\n    }\n    console.debug(`stdout: ${stdout}`);\n    console.debug(`stderr: ${stderr}`);\n  });\n\n  // save private key to file\n  const prkeyDir = `${process.env['HOME']}/.config/devbox/ssh/keys`;\n  await ensureDir(prkeyDir);\n  const prkeyPath = `${prkeyDir}/${vmId}.vm.devbox-vms.internal`;\n  try {\n    const prKeydata = new Uint8Array(Buffer.from(prKey));\n    const fileHandler = await open(prkeyPath, 'w');\n    await writeFile(fileHandler, prKeydata, { flag: 'w' });\n    await chmod(prkeyPath, 0o600);\n    await fileHandler.close();\n  } catch (err: any) {\n    // When a request is aborted - err is an AbortError\n    console.error('Failed to setup ssh config: ' + err);\n    throw (err);\n  }\n}\n\nfunction connectToRemote(username: string, vmId: string, workDir: string) {\n  try {\n    const host = `${username}@${vmId}.vm.devbox-vms.internal`;\n    const workspaceURI = `vscode-remote://ssh-remote+${host}${workDir}`;\n    const uriToOpen = Uri.parse(workspaceURI);\n    console.debug(\"uriToOpen: \", uriToOpen.toString());\n    commands.executeCommand(\"vscode.openFolder\", uriToOpen, false);\n  } catch (err: any) {\n    console.error('failed to connect to remote: ' + err);\n  }\n}\n\nasync function ensureDir(dir: string) {\n  try {\n    await mkdir(dir, {recursive: true, mode: 0o700});\n  } catch (err: any) {\n    if (err.code !== 'EEXIST') {\n      console.error('Failed to setup ssh keys directory: ' + err);\n      throw (err);\n    }\n  }\n}\n"
  },
  {
    "path": "vscode-extension/src/test/runTest.ts",
    "content": "import * as path from 'path';\n\nimport { runTests } from '@vscode/test-electron';\n\nasync function main() {\n\ttry {\n\t\t// The folder containing the Extension Manifest package.json\n\t\t// Passed to `--extensionDevelopmentPath`\n\t\tconst extensionDevelopmentPath = path.resolve(__dirname, '../../');\n\n\t\t// The path to test runner\n\t\t// Passed to --extensionTestsPath\n\t\tconst extensionTestsPath = path.resolve(__dirname, './suite/index');\n\n\t\t// Download VS Code, unzip it and run the integration test\n\t\tawait runTests({ extensionDevelopmentPath, extensionTestsPath });\n\t} catch (err) {\n\t\tconsole.error('Failed to run tests');\n\t\tprocess.exit(1);\n\t}\n}\n\nmain();\n"
  },
  {
    "path": "vscode-extension/src/test/suite/extension.test.ts",
    "content": "import * as assert from 'assert';\n\n// You can import and use all API from the 'vscode' module\n// as well as import your extension to test it\nimport * as vscode from 'vscode';\n// import * as myExtension from '../../extension';\n\nsuite('Extension Test Suite', () => {\n\tvscode.window.showInformationMessage('Start all tests.');\n\n\ttest('Sample test', () => {\n\t\tassert.strictEqual(-1, [1, 2, 3].indexOf(5));\n\t\tassert.strictEqual(-1, [1, 2, 3].indexOf(0));\n\t});\n});\n"
  },
  {
    "path": "vscode-extension/src/test/suite/index.ts",
    "content": "import * as path from 'path';\nimport * as Mocha from 'mocha';\nimport * as glob from 'glob';\n\nexport function run(): Promise<void> {\n\t// Create the mocha test\n\tconst mocha = new Mocha({\n\t\tui: 'tdd',\n\t\tcolor: true\n\t});\n\n\tconst testsRoot = path.resolve(__dirname, '..');\n\n\treturn new Promise((c, e) => {\n\t\tglob('**/**.test.js', { cwd: testsRoot }, (err, files) => {\n\t\t\tif (err) {\n\t\t\t\treturn e(err);\n\t\t\t}\n\n\t\t\t// Add files to the test suite\n\t\t\tfiles.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));\n\n\t\t\ttry {\n\t\t\t\t// Run the mocha test\n\t\t\t\tmocha.run(failures => {\n\t\t\t\t\tif (failures > 0) {\n\t\t\t\t\t\te(new Error(`${failures} tests failed.`));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tc();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(err);\n\t\t\t\te(err);\n\t\t\t}\n\t\t});\n\t});\n}\n"
  },
  {
    "path": "vscode-extension/tsconfig.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"module\": \"commonjs\",\n\t\t\"target\": \"ES2020\",\n\t\t\"outDir\": \"out\",\n\t\t\"lib\": [\n\t\t\t\"ES2020\"\n\t\t],\n\t\t\"sourceMap\": true,\n\t\t\"rootDir\": \"src\",\n\t\t\"strict\": true   /* enable all strict type-checking options */\n\t\t/* Additional Checks */\n\t\t// \"noImplicitReturns\": true, /* Report error when not all code paths in function return a value. */\n\t\t// \"noFallthroughCasesInSwitch\": true, /* Report errors for fallthrough cases in switch statement. */\n\t\t// \"noUnusedParameters\": true,  /* Report errors on unused parameters. */\n\t}\n}\n"
  }
]